pi-revit 0.3.1 → 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.
- package/AGENTS.md +167 -0
- package/CHANGELOG.md +114 -42
- package/README.md +598 -138
- package/bin/pi-revit.js +9 -9
- package/docs/architecture.md +271 -0
- package/docs/evaluation.md +434 -0
- package/docs/invariants.json +147 -0
- package/extensions/pi-revit/completion-monitor.ts +55 -0
- package/extensions/pi-revit/contracts.ts +146 -0
- package/extensions/pi-revit/discovery.ts +93 -0
- package/extensions/pi-revit/index.ts +231 -85
- package/extensions/pi-revit/instance-router.ts +86 -0
- package/extensions/pi-revit/platform-prompt.ts +40 -0
- package/extensions/pi-revit/scope-monitor.ts +114 -0
- package/extensions/pi-revit/script-library.ts +146 -0
- package/extensions/pi-revit/tool-catalog.ts +166 -0
- package/extensions/pi-revit/tool-documentation.ts +72 -0
- package/extensions/pi-revit/tool-schema.ts +8 -0
- package/package.json +65 -59
- package/scripts/build.ps1 +9 -9
- package/scripts/check-sdk.ps1 +66 -66
- package/scripts/check-tool-documentation.mjs +287 -0
- package/scripts/deploy.ps1 +16 -16
- package/scripts/generate-contracts.mjs +80 -0
- package/scripts/lib/platform.mjs +226 -0
- package/scripts/test-extension.mjs +15 -0
- package/skills/pi-revit/SKILL.md +39 -63
- package/skills/pi-revit/contracts.generated.json +3524 -0
- package/skills/pi-revit/references/execution-rules.md +41 -0
- package/skills/pi-revit/references/model-audit-export.md +40 -0
- package/skills/pi-revit/references/operation-recovery.md +33 -0
- package/skills/pi-revit/references/room-documentation.md +39 -0
- package/skills/pi-revit/references/tool-index.md +89 -0
- package/skills/pi-revit/references/tools/capture_view.md +62 -0
- package/skills/pi-revit/references/tools/change_element_types.md +65 -0
- package/skills/pi-revit/references/tools/create_tags.md +85 -0
- package/skills/pi-revit/references/tools/delete_elements.md +66 -0
- package/skills/pi-revit/references/tools/execute_csharp.md +81 -0
- package/skills/pi-revit/references/tools/export_documents.md +75 -0
- package/skills/pi-revit/references/tools/find_revit_tools.md +96 -0
- package/skills/pi-revit/references/tools/get_element_details.md +66 -0
- package/skills/pi-revit/references/tools/get_element_relationships.md +61 -0
- package/skills/pi-revit/references/tools/get_element_types.md +67 -0
- package/skills/pi-revit/references/tools/get_elements.md +87 -0
- package/skills/pi-revit/references/tools/get_linked_elements.md +79 -0
- package/skills/pi-revit/references/tools/get_linked_models.md +57 -0
- package/skills/pi-revit/references/tools/get_model_coordinates.md +64 -0
- package/skills/pi-revit/references/tools/get_model_health.md +53 -0
- package/skills/pi-revit/references/tools/get_model_overview.md +57 -0
- package/skills/pi-revit/references/tools/get_revit_operation.md +54 -0
- package/skills/pi-revit/references/tools/get_schedule_fields.md +62 -0
- package/skills/pi-revit/references/tools/get_schedules.md +71 -0
- package/skills/pi-revit/references/tools/manage_element_sets.md +92 -0
- package/skills/pi-revit/references/tools/manage_revit_instances.md +63 -0
- package/skills/pi-revit/references/tools/manage_revit_scripts.md +109 -0
- package/skills/pi-revit/references/tools/manage_schedules.md +90 -0
- package/skills/pi-revit/references/tools/manage_selection.md +66 -0
- package/skills/pi-revit/references/tools/manage_sheet_placements.md +82 -0
- package/skills/pi-revit/references/tools/manage_sheets.md +71 -0
- package/skills/pi-revit/references/tools/manage_views.md +95 -0
- package/skills/pi-revit/references/tools/measure_geometry.md +71 -0
- package/skills/pi-revit/references/tools/open_view.md +59 -0
- package/skills/pi-revit/references/tools/ping.md +41 -0
- package/skills/pi-revit/references/tools/query_spatial_elements.md +74 -0
- package/skills/pi-revit/references/tools/read_revit_result.md +53 -0
- package/skills/pi-revit/references/tools/search_api_docs.md +65 -0
- package/skills/pi-revit/references/tools/set_parameters.md +75 -0
- package/skills/pi-revit/references/tools/summarize_elements.md +64 -0
- package/skills/pi-revit/references/tools/transform_elements.md +79 -0
- package/skills/pi-revit/references/visual-verification.md +36 -0
- package/skills/pi-revit/tool-manifest.json +338 -0
- package/src/Revit/BridgeServer.cs +75 -19
- package/src/Revit/OperationStore.cs +178 -0
- package/src/Revit/ToolRegistry.cs +61 -8
- package/src/Revit/Tools/CaptureView.cs +9 -0
- package/src/Revit/Tools/ChangeElementTypes.cs +74 -0
- package/src/Revit/Tools/ChangeSet.cs +39 -0
- package/src/Revit/Tools/CreateTags.cs +107 -0
- package/src/Revit/Tools/DeleteElements.cs +53 -0
- package/src/Revit/Tools/DocumentGuard.cs +12 -2
- package/src/Revit/Tools/ElementNames.cs +103 -0
- package/src/Revit/Tools/ElementQueryScope.cs +27 -0
- package/src/Revit/Tools/ElementTraits.cs +53 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +26 -7
- package/src/Revit/Tools/ExportDocuments.cs +9 -0
- package/src/Revit/Tools/FailureGuard.cs +26 -26
- package/src/Revit/Tools/GetElementDetails.cs +28 -2
- package/src/Revit/Tools/GetElementRelationships.cs +82 -0
- package/src/Revit/Tools/GetElementTypes.cs +8 -0
- package/src/Revit/Tools/GetElements.cs +58 -55
- package/src/Revit/Tools/GetLinkedElements.cs +89 -0
- package/src/Revit/Tools/GetLinkedModels.cs +73 -0
- package/src/Revit/Tools/GetModelCoordinates.cs +56 -0
- package/src/Revit/Tools/GetModelHealth.cs +7 -0
- package/src/Revit/Tools/GetModelOverview.cs +187 -160
- package/src/Revit/Tools/GetScheduleFields.cs +44 -0
- package/src/Revit/Tools/GetSchedules.cs +96 -0
- package/src/Revit/Tools/InheritedState.Summary.cs +57 -0
- package/src/Revit/Tools/InheritedState.cs +144 -0
- package/src/Revit/Tools/ManageElementSets.cs +114 -0
- package/src/Revit/Tools/ManageSchedules.cs +174 -0
- package/src/Revit/Tools/ManageSelection.cs +9 -0
- package/src/Revit/Tools/ManageSheetPlacements.cs +113 -0
- package/src/Revit/Tools/ManageSheets.cs +72 -0
- package/src/Revit/Tools/ManageViews.cs +115 -0
- package/src/Revit/Tools/MeasureGeometry.cs +60 -0
- package/src/Revit/Tools/ModelChanges.cs +154 -0
- package/src/Revit/Tools/ModelEditBatch.cs +105 -0
- package/src/Revit/Tools/ModelEditInputs.cs +49 -0
- package/src/Revit/Tools/OpenView.cs +8 -0
- package/src/Revit/Tools/ParameterResolver.cs +94 -0
- package/src/Revit/Tools/QuerySpatialElements.cs +70 -0
- package/src/Revit/Tools/SearchApiDocs.cs +72 -4
- package/src/Revit/Tools/SetParameters.cs +50 -119
- package/src/Revit/Tools/SpatialBounds.cs +30 -0
- package/src/Revit/Tools/SummarizeElements.cs +94 -0
- package/src/Revit/Tools/ToolContract.cs +48 -0
- package/src/Revit/Tools/ToolSupport.cs +4 -0
- package/src/Revit/Tools/TransformElements.cs +73 -0
- package/workspace/AGENTS.md +54 -48
|
@@ -0,0 +1,96 @@
|
|
|
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 IReadOnlyList<string> DocumentKinds => DocumentKind.ProjectOnly;
|
|
10
|
+
public IReadOnlyList<string> Keywords => new[] { "schedule", "quantities", "takeoff", "table", "schedule cells", "schedule values", "bill of quantities" };
|
|
11
|
+
public IReadOnlyList<ToolLimit> Limits => new[]
|
|
12
|
+
{
|
|
13
|
+
new ToolLimit("Titleblock revision schedules and schedule templates", "api", "ViewSchedule.IsTitleblockRevisionSchedule; ViewSchedule.GetTableData"),
|
|
14
|
+
new ToolLimit("Graphical layout of a schedule on a sheet", "tool", "capture_view"),
|
|
15
|
+
};
|
|
16
|
+
public string Label => "Get Schedules";
|
|
17
|
+
public string Tier => "advanced";
|
|
18
|
+
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.";
|
|
19
|
+
public object ParametersSchema => new
|
|
20
|
+
{
|
|
21
|
+
type = "object",
|
|
22
|
+
properties = new
|
|
23
|
+
{
|
|
24
|
+
schedule_id = new { type = "integer" },
|
|
25
|
+
name_filter = new { type = "string" },
|
|
26
|
+
offset = new { type = "integer", minimum = 0, description = "List offset or body row offset." },
|
|
27
|
+
limit = new { type = "integer", minimum = 1, maximum = 200 },
|
|
28
|
+
column_offset = new { type = "integer", minimum = 0 },
|
|
29
|
+
column_limit = new { type = "integer", minimum = 1, maximum = 50 },
|
|
30
|
+
},
|
|
31
|
+
required = Array.Empty<string>(),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
public object Execute(JsonElement args, ToolContext context)
|
|
35
|
+
{
|
|
36
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
37
|
+
int offset = Math.Max(0, JsonArgs.GetInt(args, "offset", 0));
|
|
38
|
+
int limit = Math.Clamp(JsonArgs.GetInt(args, "limit", 50), 1, 200);
|
|
39
|
+
if (JsonArgs.GetLong(args, "schedule_id") is not { } id)
|
|
40
|
+
{
|
|
41
|
+
string needle = JsonArgs.GetString(args, "name_filter") ?? "";
|
|
42
|
+
var schedules = new FilteredElementCollector(doc).OfClass(typeof(ViewSchedule)).Cast<ViewSchedule>()
|
|
43
|
+
.Where(s => !s.IsTemplate && !s.IsTitleblockRevisionSchedule && s.Name.Contains(needle, StringComparison.OrdinalIgnoreCase))
|
|
44
|
+
.OrderBy(s => s.Id.Value).ToList();
|
|
45
|
+
var rows = schedules.Skip(offset).Take(limit).Select(s => new
|
|
46
|
+
{
|
|
47
|
+
id = s.Id.Value, name = s.Name, unique_id = s.UniqueId,
|
|
48
|
+
category_id = s.Definition.CategoryId.Value,
|
|
49
|
+
is_itemized = s.Definition.IsItemized,
|
|
50
|
+
field_count = s.Definition.GetFieldCount(),
|
|
51
|
+
}).ToArray();
|
|
52
|
+
return new { total_count = schedules.Count, returned_count = rows.Length, offset,
|
|
53
|
+
has_more = offset + rows.Length < schedules.Count,
|
|
54
|
+
next_offset = offset + rows.Length < schedules.Count ? (int?)(offset + rows.Length) : null, schedules = rows };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
var schedule = doc.GetElement(new ElementId(id)) as ViewSchedule
|
|
58
|
+
?? throw new ArgumentException("schedule_id is not a schedule in the active document.");
|
|
59
|
+
if (schedule.IsTemplate) throw new ArgumentException("Schedule templates do not contain displayed body rows.");
|
|
60
|
+
using var table = schedule.GetTableData();
|
|
61
|
+
using var body = table.GetSectionData(SectionType.Body);
|
|
62
|
+
int columnOffset = Math.Max(0, JsonArgs.GetInt(args, "column_offset", 0));
|
|
63
|
+
int columnLimit = Math.Clamp(JsonArgs.GetInt(args, "column_limit", 50), 1, 50);
|
|
64
|
+
int columnCount = Math.Min(columnLimit, Math.Max(0, body.NumberOfColumns - columnOffset));
|
|
65
|
+
int rowCount = Math.Min(limit, Math.Max(0, body.NumberOfRows - offset));
|
|
66
|
+
var cells = Enumerable.Range(0, rowCount).Select(row => new
|
|
67
|
+
{
|
|
68
|
+
row_index = body.FirstRowNumber + offset + row,
|
|
69
|
+
cells = Enumerable.Range(0, columnCount).Select(col => schedule.GetCellText(SectionType.Body,
|
|
70
|
+
body.FirstRowNumber + offset + row, body.FirstColumnNumber + columnOffset + col)).ToArray(),
|
|
71
|
+
}).ToArray();
|
|
72
|
+
var fields = schedule.Definition.GetFieldOrder().Select(fieldId =>
|
|
73
|
+
{
|
|
74
|
+
var field = schedule.Definition.GetField(fieldId);
|
|
75
|
+
return new { field_id = fieldId.IntegerValue, parameter_id = field.ParameterId.Value,
|
|
76
|
+
name = field.GetName(), heading = field.ColumnHeading, hidden = field.IsHidden, field_type = field.FieldType.ToString(),
|
|
77
|
+
spec_type_id = field.GetSpecTypeId().TypeId, grid_width_feet = field.GridColumnWidth, sheet_width_feet = field.SheetColumnWidth,
|
|
78
|
+
can_filter_value = schedule.Definition.CanFilterByValue(fieldId), can_filter_substring = schedule.Definition.CanFilterBySubstring(fieldId) };
|
|
79
|
+
}).ToArray();
|
|
80
|
+
return new
|
|
81
|
+
{
|
|
82
|
+
schedule_id = id, name = schedule.Name, is_itemized = schedule.Definition.IsItemized, fields,
|
|
83
|
+
sort_fields = schedule.Definition.GetSortGroupFields().Select(sort => new { field_id = sort.FieldId.IntegerValue, order = sort.SortOrder.ToString(), show_header = sort.ShowHeader }).ToArray(),
|
|
84
|
+
filters = schedule.Definition.GetFilters().Select(filter => new { field_id = filter.FieldId.IntegerValue, comparison = filter.FilterType.ToString(),
|
|
85
|
+
value = filter.IsStringValue ? (object)filter.GetStringValue() : filter.IsDoubleValue ? filter.GetDoubleValue() : filter.IsIntegerValue ? filter.GetIntegerValue() : filter.IsElementIdValue ? filter.GetElementIdValue().Value : null,
|
|
86
|
+
value_unit = filter.IsDoubleValue ? "internal" : null }).ToArray(),
|
|
87
|
+
total_rows = body.NumberOfRows, total_columns = body.NumberOfColumns,
|
|
88
|
+
offset, returned_rows = rowCount,
|
|
89
|
+
next_offset = offset + rowCount < body.NumberOfRows ? (int?)(offset + rowCount) : null,
|
|
90
|
+
column_offset = columnOffset, returned_columns = columnCount,
|
|
91
|
+
first_column_index = body.FirstColumnNumber + columnOffset,
|
|
92
|
+
next_column_offset = columnOffset + columnCount < body.NumberOfColumns ? (int?)(columnOffset + columnCount) : null,
|
|
93
|
+
rows = cells,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
namespace RevitBridge.Tools
|
|
2
|
+
{
|
|
3
|
+
/// <summary>The Revit-free part of InheritedState: limits, the facts record and the summary. Tested offline.</summary>
|
|
4
|
+
internal static partial class InheritedState
|
|
5
|
+
{
|
|
6
|
+
public const int ListCap = 30;
|
|
7
|
+
public const int SampleCap = 10;
|
|
8
|
+
public static readonly TimeSpan ScanBudget = TimeSpan.FromSeconds(4);
|
|
9
|
+
|
|
10
|
+
public const string ViewCheck = "Inherited from the source view. Compare this state with the request: show anything the request asks to show, or disclose what stays hidden, and report what was inherited.";
|
|
11
|
+
public const string NewViewCheck = "A new view starts with this visibility state; a duplicate inherits its source's state. Compare it with the request before reporting the view as complete.";
|
|
12
|
+
public const string ElementCheck = "Carried over from the source object. Check these values against the request; a copied Mark or Comment usually needs a new value.";
|
|
13
|
+
|
|
14
|
+
/// <summary>Facts read from one view. Lists hold category identities as "OST_Name|Localized name".</summary>
|
|
15
|
+
internal sealed record ViewFacts(
|
|
16
|
+
long? SourceId, long? TemplateId, string? TemplateName,
|
|
17
|
+
IReadOnlyList<string> HiddenCategories, int HiddenElementCount, IReadOnlyList<long> HiddenElementSample,
|
|
18
|
+
int ElementOverrideCount, bool ElementScanComplete, int CategoryOverrideCount,
|
|
19
|
+
IReadOnlyList<(long Id, string Name, bool Visible)> Filters, bool TemporaryHideIsolate,
|
|
20
|
+
string? DisplayStyle, string? DetailLevel, bool CropActive, bool? SectionBoxActive, bool? Perspective);
|
|
21
|
+
|
|
22
|
+
/// <summary>Pure summary of view facts: only non-empty sections, capped lists, and the check the caller must make.</summary>
|
|
23
|
+
public static Dictionary<string, object?> Summarize(ViewFacts facts, string check)
|
|
24
|
+
{
|
|
25
|
+
var state = new Dictionary<string, object?>();
|
|
26
|
+
if (facts.SourceId is long source) state["derived_from"] = source;
|
|
27
|
+
if (facts.TemplateId is long template && template > 0) state["template"] = new Dictionary<string, object?> { ["id"] = template, ["name"] = facts.TemplateName };
|
|
28
|
+
if (facts.HiddenCategories.Count > 0)
|
|
29
|
+
state["hidden_categories"] = new Dictionary<string, object?>
|
|
30
|
+
{
|
|
31
|
+
["count"] = facts.HiddenCategories.Count,
|
|
32
|
+
["categories"] = facts.HiddenCategories.Take(ListCap).ToList(),
|
|
33
|
+
["truncated"] = facts.HiddenCategories.Count > ListCap,
|
|
34
|
+
};
|
|
35
|
+
if (facts.HiddenElementCount > 0 || !facts.ElementScanComplete)
|
|
36
|
+
state["hidden_elements"] = new Dictionary<string, object?>
|
|
37
|
+
{
|
|
38
|
+
["count"] = facts.HiddenElementCount,
|
|
39
|
+
["sample_ids"] = facts.HiddenElementSample.Take(SampleCap).ToList(),
|
|
40
|
+
["complete"] = facts.ElementScanComplete,
|
|
41
|
+
};
|
|
42
|
+
if (facts.ElementOverrideCount > 0) state["element_overrides"] = facts.ElementOverrideCount;
|
|
43
|
+
if (facts.CategoryOverrideCount > 0) state["category_overrides"] = facts.CategoryOverrideCount;
|
|
44
|
+
if (facts.Filters.Count > 0)
|
|
45
|
+
state["filters"] = facts.Filters.Take(ListCap).Select(f => new Dictionary<string, object?> { ["id"] = f.Id, ["name"] = f.Name, ["visible"] = f.Visible }).ToList();
|
|
46
|
+
if (facts.TemporaryHideIsolate) state["temporary_hide_isolate"] = true;
|
|
47
|
+
var display = new Dictionary<string, object?> { ["display_style"] = facts.DisplayStyle, ["detail_level"] = facts.DetailLevel, ["crop_box_active"] = facts.CropActive };
|
|
48
|
+
if (facts.SectionBoxActive is bool box) display["section_box_active"] = box;
|
|
49
|
+
if (facts.Perspective is bool perspective) display["perspective"] = perspective;
|
|
50
|
+
state["display"] = display;
|
|
51
|
+
if (!facts.ElementScanComplete)
|
|
52
|
+
state["scan_note"] = $"The hidden-element scan stopped after {ScanBudget.TotalSeconds:0} s; counts are lower bounds.";
|
|
53
|
+
state["check"] = check;
|
|
54
|
+
return state;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
using System.Diagnostics;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
|
|
4
|
+
namespace RevitBridge.Tools
|
|
5
|
+
{
|
|
6
|
+
/// <summary>
|
|
7
|
+
/// Shared report of the state an object carries when it is derived from an existing one
|
|
8
|
+
/// (inv:derived-state-reported). A duplicated view keeps its source's hidden categories and
|
|
9
|
+
/// elements, filters, overrides and template; a copied element keeps its instance values; a
|
|
10
|
+
/// retyped element keeps its per-view graphics. Any tool that duplicates, copies, mirrors or
|
|
11
|
+
/// retypes reports this in its result, so the caller can compare it with the request instead
|
|
12
|
+
/// of relying on an image. The gate in scripts/check-tool-documentation.mjs makes every tool
|
|
13
|
+
/// that creates from an existing object use this class. Reading and summarizing are separate:
|
|
14
|
+
/// Summarize is pure and tested offline; the Read* methods only collect facts from Revit.
|
|
15
|
+
/// </summary>
|
|
16
|
+
internal static partial class InheritedState
|
|
17
|
+
{
|
|
18
|
+
/// <summary>Inherited state of a view derived from source (null when unknown), for a result.</summary>
|
|
19
|
+
public static Dictionary<string, object?> OfView(View view, long? sourceId, string check = ViewCheck)
|
|
20
|
+
=> Summarize(ReadView(view, sourceId), check);
|
|
21
|
+
|
|
22
|
+
/// <summary>Collect facts from Revit. The element scan is bounded by ScanBudget.</summary>
|
|
23
|
+
public static ViewFacts ReadView(View view, long? sourceId)
|
|
24
|
+
{
|
|
25
|
+
var doc = view.Document;
|
|
26
|
+
var hiddenCategories = new List<string>();
|
|
27
|
+
int categoryOverrides = 0;
|
|
28
|
+
foreach (Category category in doc.Settings.Categories)
|
|
29
|
+
{
|
|
30
|
+
try
|
|
31
|
+
{
|
|
32
|
+
if (view.CanCategoryBeHidden(category.Id) && view.GetCategoryHidden(category.Id))
|
|
33
|
+
hiddenCategories.Add($"{category.BuiltInCategory}|{category.Name}");
|
|
34
|
+
if (view.AreGraphicsOverridesAllowed() && view.IsCategoryOverridable(category.Id) && IsNonDefault(view.GetCategoryOverrides(category.Id)))
|
|
35
|
+
categoryOverrides++;
|
|
36
|
+
}
|
|
37
|
+
catch (Autodesk.Revit.Exceptions.ArgumentException) { }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
int hidden = 0, overridden = 0;
|
|
41
|
+
var sample = new List<long>();
|
|
42
|
+
bool complete = true;
|
|
43
|
+
var clock = Stopwatch.StartNew();
|
|
44
|
+
var candidates = new FilteredElementCollector(doc).WhereElementIsNotElementType().WhereElementIsViewIndependent()
|
|
45
|
+
.Concat(new FilteredElementCollector(doc).OwnedByView(view.Id));
|
|
46
|
+
foreach (var element in candidates)
|
|
47
|
+
{
|
|
48
|
+
if (clock.Elapsed > ScanBudget) { complete = false; break; }
|
|
49
|
+
try
|
|
50
|
+
{
|
|
51
|
+
if (element.CanBeHidden(view) && element.IsHidden(view))
|
|
52
|
+
{
|
|
53
|
+
hidden++;
|
|
54
|
+
if (sample.Count < SampleCap) sample.Add(element.Id.Value);
|
|
55
|
+
}
|
|
56
|
+
if (view.AreGraphicsOverridesAllowed() && IsNonDefault(view.GetElementOverrides(element.Id))) overridden++;
|
|
57
|
+
}
|
|
58
|
+
catch (Autodesk.Revit.Exceptions.ArgumentException) { }
|
|
59
|
+
catch (Autodesk.Revit.Exceptions.InvalidOperationException) { }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
var filters = new List<(long, string, bool)>();
|
|
63
|
+
try
|
|
64
|
+
{
|
|
65
|
+
foreach (var id in view.GetFilters())
|
|
66
|
+
filters.Add((id.Value, doc.GetElement(id)?.Name ?? "", view.GetFilterVisibility(id)));
|
|
67
|
+
}
|
|
68
|
+
catch (Autodesk.Revit.Exceptions.InvalidOperationException) { }
|
|
69
|
+
|
|
70
|
+
var template = view.ViewTemplateId;
|
|
71
|
+
var view3D = view as View3D;
|
|
72
|
+
return new ViewFacts(sourceId, template.Value > 0 ? template.Value : null, template.Value > 0 ? doc.GetElement(template)?.Name : null,
|
|
73
|
+
hiddenCategories, hidden, sample, overridden, complete, categoryOverrides, filters, Safe(() => view.IsTemporaryHideIsolateActive()),
|
|
74
|
+
Safe(() => view.DisplayStyle.ToString()), Safe(() => view.DetailLevel.ToString()), Safe(() => view.CropBoxActive),
|
|
75
|
+
view3D == null ? null : Safe(() => view3D.IsSectionBoxActive), view3D?.IsPerspective);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// <summary>Some view kinds (schedules, sheets) reject graphics properties; report them as unset.</summary>
|
|
79
|
+
private static T? Safe<T>(Func<T> read)
|
|
80
|
+
{
|
|
81
|
+
try { return read(); }
|
|
82
|
+
catch (Autodesk.Revit.Exceptions.InvalidOperationException) { return default; }
|
|
83
|
+
catch (Autodesk.Revit.Exceptions.ArgumentException) { return default; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// <summary>True when an override changes anything from the view's normal graphics.</summary>
|
|
87
|
+
public static bool IsNonDefault(OverrideGraphicSettings o) =>
|
|
88
|
+
o.Halftone || o.Transparency != 0 || o.DetailLevel != ViewDetailLevel.Undefined
|
|
89
|
+
|| o.ProjectionLineColor.IsValid || o.ProjectionLineWeight != OverrideGraphicSettings.InvalidPenNumber
|
|
90
|
+
|| o.CutLineColor.IsValid || o.CutLineWeight != OverrideGraphicSettings.InvalidPenNumber
|
|
91
|
+
|| o.SurfaceForegroundPatternId != ElementId.InvalidElementId || o.CutForegroundPatternId != ElementId.InvalidElementId
|
|
92
|
+
|| !o.IsSurfaceForegroundPatternVisible || !o.IsCutForegroundPatternVisible;
|
|
93
|
+
|
|
94
|
+
/// <summary>
|
|
95
|
+
/// Inherited state of an element derived from source: its special traits, identity-like
|
|
96
|
+
/// values that were copied (Mark, Comments), and views that hide or override it (bounded).
|
|
97
|
+
/// Per-view hiding and overrides are keyed by element ID, so a copy (new ID) never inherits
|
|
98
|
+
/// them; pass viewGraphics only when the element keeps its ID, as a retyped element does.
|
|
99
|
+
/// Views get the view report instead. Returns null when nothing was carried over.
|
|
100
|
+
/// </summary>
|
|
101
|
+
public static Dictionary<string, object?>? OfElement(Element derived, long? sourceId, bool viewGraphics = false)
|
|
102
|
+
{
|
|
103
|
+
if (derived is View view) return OfView(view, sourceId);
|
|
104
|
+
var state = new Dictionary<string, object?>();
|
|
105
|
+
if (sourceId is long source && source != derived.Id.Value) state["derived_from"] = source;
|
|
106
|
+
if (ElementTraits.For(derived) is { } traits) state["traits"] = traits;
|
|
107
|
+
var carried = new Dictionary<string, object?>();
|
|
108
|
+
foreach (var (key, parameter) in new[] { ("mark", BuiltInParameter.ALL_MODEL_MARK), ("comments", BuiltInParameter.ALL_MODEL_INSTANCE_COMMENTS) })
|
|
109
|
+
if (derived.get_Parameter(parameter)?.AsString() is { Length: > 0 } value) carried[key] = value;
|
|
110
|
+
if (carried.Count > 0) state["carried_values"] = carried;
|
|
111
|
+
if (viewGraphics && ViewGraphicsOf(derived) is { } graphics) state["view_graphics"] = graphics;
|
|
112
|
+
if (state.Count == 0 || (state.Count == 1 && state.ContainsKey("derived_from"))) return null;
|
|
113
|
+
state["check"] = ElementCheck;
|
|
114
|
+
return state;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// <summary>Views that hide or override this element, within ScanBudget.</summary>
|
|
118
|
+
private static Dictionary<string, object?>? ViewGraphicsOf(Element element)
|
|
119
|
+
{
|
|
120
|
+
var clock = Stopwatch.StartNew();
|
|
121
|
+
var hiddenIn = new List<long>();
|
|
122
|
+
int overriddenIn = 0;
|
|
123
|
+
bool complete = true;
|
|
124
|
+
foreach (var view in new FilteredElementCollector(element.Document).OfClass(typeof(View)).Cast<View>())
|
|
125
|
+
{
|
|
126
|
+
if (clock.Elapsed > ScanBudget) { complete = false; break; }
|
|
127
|
+
if (view.IsTemplate) continue;
|
|
128
|
+
try
|
|
129
|
+
{
|
|
130
|
+
if (element.CanBeHidden(view) && element.IsHidden(view)) hiddenIn.Add(view.Id.Value);
|
|
131
|
+
if (view.AreGraphicsOverridesAllowed() && IsNonDefault(view.GetElementOverrides(element.Id))) overriddenIn++;
|
|
132
|
+
}
|
|
133
|
+
catch (Autodesk.Revit.Exceptions.ArgumentException) { }
|
|
134
|
+
catch (Autodesk.Revit.Exceptions.InvalidOperationException) { }
|
|
135
|
+
}
|
|
136
|
+
if (hiddenIn.Count == 0 && overriddenIn == 0 && complete) return null;
|
|
137
|
+
return new Dictionary<string, object?>
|
|
138
|
+
{
|
|
139
|
+
["hidden_in_views"] = hiddenIn.Count, ["hidden_in_view_ids"] = hiddenIn.Take(SampleCap).ToList(),
|
|
140
|
+
["overridden_in_views"] = overriddenIn, ["complete"] = complete,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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 IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
|
|
17
|
+
public IReadOnlyList<string> Keywords => new[] { "set", "collection", "remember elements", "saved query", "reread members", "snapshot" };
|
|
18
|
+
public IReadOnlyList<ToolLimit> Limits => new[]
|
|
19
|
+
{
|
|
20
|
+
new ToolLimit("Saved Revit selection sets", "api", "SelectionFilterElement.Create"),
|
|
21
|
+
new ToolLimit("Live query subscriptions", "tool", "get_elements (run the query again)"),
|
|
22
|
+
};
|
|
23
|
+
public string? Verification => "none";
|
|
24
|
+
public string Label => "Manage Element Sets";
|
|
25
|
+
public string Tier => "advanced";
|
|
26
|
+
public IReadOnlyList<string> Effects => new[] { "session" };
|
|
27
|
+
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.";
|
|
28
|
+
public object ParametersSchema => new
|
|
29
|
+
{
|
|
30
|
+
type = "object",
|
|
31
|
+
properties = new
|
|
32
|
+
{
|
|
33
|
+
action = new { type = "string", @enum = new[] { "create", "list", "read", "forget" } },
|
|
34
|
+
query = ElementQueryScope.Schema(),
|
|
35
|
+
label = new { type = "string", maxLength = 160 },
|
|
36
|
+
set_id = new { type = "string" },
|
|
37
|
+
offset = new { type = "integer", minimum = 0 },
|
|
38
|
+
limit = new { type = "integer", minimum = 1, maximum = 1000 },
|
|
39
|
+
parameter_names = new { type = "array", maxItems = 20, items = new { type = "string" } },
|
|
40
|
+
include_type_parameters = new { type = "boolean" },
|
|
41
|
+
},
|
|
42
|
+
required = new[] { "action" },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
public object Execute(JsonElement args, ToolContext context)
|
|
46
|
+
{
|
|
47
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
48
|
+
string documentId = DocumentGuard.GetIdentity(doc);
|
|
49
|
+
var now = DateTimeOffset.UtcNow;
|
|
50
|
+
foreach (var expired in Sets.Where(pair => pair.Value.Expires <= now).Select(pair => pair.Key).ToArray()) Sets.Remove(expired);
|
|
51
|
+
string action = JsonArgs.GetString(args, "action") ?? "";
|
|
52
|
+
if (action == "list") return new
|
|
53
|
+
{
|
|
54
|
+
sets = Sets.Values.Where(set => set.DocumentId == documentId).OrderBy(set => set.Expires)
|
|
55
|
+
.Select(set => new { set_id = set.Id, label = set.Label, total_count = set.Members.Count, expires_at = set.Expires }).ToArray(),
|
|
56
|
+
};
|
|
57
|
+
if (action == "create")
|
|
58
|
+
{
|
|
59
|
+
if (Sets.Count >= MaxSets) throw new ArgumentException("Element-set capacity reached. Forget an existing set or wait for expiry.");
|
|
60
|
+
string label = JsonArgs.GetString(args, "label") ?? "Element set";
|
|
61
|
+
if (label.Length > 160) throw new ArgumentException("label must be at most 160 characters.");
|
|
62
|
+
var query = ElementQueryScope.Parse(args);
|
|
63
|
+
query["fields"] = new JsonArray("id"); query["count_only"] = true;
|
|
64
|
+
JsonElement Query() => JsonSerializer.SerializeToElement(((ToolOutput)new GetElements().Execute(JsonSerializer.SerializeToElement(query), context)!).Payload);
|
|
65
|
+
var count = Query();
|
|
66
|
+
int total = count.GetProperty("total_count").GetInt32();
|
|
67
|
+
if (total > MaxElements) throw new ArgumentException($"Query matches {total} elements; narrow the scope to at most {MaxElements}.");
|
|
68
|
+
query["count_only"] = false; query["limit"] = 1000;
|
|
69
|
+
var members = new List<Member>(total);
|
|
70
|
+
for (int offset = 0; offset < total; offset += 1000)
|
|
71
|
+
{
|
|
72
|
+
query["offset"] = offset;
|
|
73
|
+
foreach (var row in Query().GetProperty("elements").EnumerateArray())
|
|
74
|
+
{
|
|
75
|
+
var element = doc.GetElement(new ElementId(row.GetProperty("id").GetInt64()));
|
|
76
|
+
members.Add(new Member(element.Id.Value, element.UniqueId));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
var snapshot = new Snapshot(Guid.NewGuid().ToString("N"), documentId, label, now.AddMinutes(30), members);
|
|
80
|
+
Sets.Add(snapshot.Id, snapshot);
|
|
81
|
+
return new { set_id = snapshot.Id, document_id = documentId, label, total_count = total, expires_at = snapshot.Expires,
|
|
82
|
+
warnings = count.TryGetProperty("warnings", out var warnings) ? (object)warnings.Clone() : null };
|
|
83
|
+
}
|
|
84
|
+
if (action is not ("read" or "forget")) throw new ArgumentException("Unknown action; use create, list, read or forget.");
|
|
85
|
+
string setId = JsonArgs.GetString(args, "set_id") ?? "";
|
|
86
|
+
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.");
|
|
87
|
+
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.");
|
|
88
|
+
if (action == "forget") { Sets.Remove(setId); return new { set_id = setId, forgotten = true }; }
|
|
89
|
+
int start = Math.Max(0, JsonArgs.GetInt(args, "offset", 0));
|
|
90
|
+
int limit = Math.Clamp(JsonArgs.GetInt(args, "limit", 100), 1, 1000);
|
|
91
|
+
var names = JsonArgs.GetStringArray(args, "parameter_names");
|
|
92
|
+
if (names?.Count > 20) throw new ArgumentException("parameter_names allows at most 20 identities.");
|
|
93
|
+
bool includeType = JsonArgs.GetBool(args, "include_type_parameters", false);
|
|
94
|
+
var page = saved.Members.Skip(start).Take(limit).ToArray();
|
|
95
|
+
var rows = new List<object>();
|
|
96
|
+
var missing = new List<object>();
|
|
97
|
+
foreach (var member in page)
|
|
98
|
+
{
|
|
99
|
+
var element = doc.GetElement(new ElementId(member.Id));
|
|
100
|
+
if (element == null || element.UniqueId != member.UniqueId)
|
|
101
|
+
{
|
|
102
|
+
missing.Add(new { id = member.Id, unique_id = member.UniqueId, reason = element == null ? "deleted" : "identity_changed" });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
var row = ElementIdentity.Build(doc, element, ElementIdentity.Fields);
|
|
106
|
+
row["unique_id"] = member.UniqueId;
|
|
107
|
+
if (names is { Count: > 0 }) row["parameters"] = GetElementDetails.ProjectParameters(doc, element, names, includeType);
|
|
108
|
+
rows.Add(row);
|
|
109
|
+
}
|
|
110
|
+
return new { set_id = setId, document_id = documentId, snapshot_count = saved.Members.Count, expires_at = saved.Expires,
|
|
111
|
+
offset = start, visited_count = page.Length, returned_count = rows.Count,
|
|
112
|
+
next_offset = start + page.Length < saved.Members.Count ? (int?)(start + page.Length) : null, elements = rows, missing };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +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 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
|
+
}
|
|
@@ -12,6 +12,15 @@ namespace RevitBridge.Tools
|
|
|
12
12
|
internal sealed class ManageSelection : ITool
|
|
13
13
|
{
|
|
14
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" };
|
|
15
24
|
public string Label => "Manage Selection";
|
|
16
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.";
|
|
17
26
|
|