pi-revit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/extensions/pi-revit/index.ts +239 -0
- package/package.json +28 -0
- package/scripts/build.ps1 +37 -0
- package/scripts/deploy.ps1 +99 -0
- package/scripts/revit.ps1 +112 -0
- package/scripts/setup-workspace.ps1 +53 -0
- package/scripts/uninstall.ps1 +103 -0
- package/skills/pi-revit/SKILL.md +50 -0
- package/src/Revit/Application.cs +94 -0
- package/src/Revit/BridgeServer.cs +419 -0
- package/src/Revit/CommandQueue.cs +93 -0
- package/src/Revit/RevitBridge.addin +11 -0
- package/src/Revit/RevitBridge.csproj +48 -0
- package/src/Revit/ToolRegistry.cs +109 -0
- package/src/Revit/Tools/CaptureView.cs +168 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +418 -0
- package/src/Revit/Tools/ExportDocuments.cs +256 -0
- package/src/Revit/Tools/GetElementDetails.cs +318 -0
- package/src/Revit/Tools/GetElementTypes.cs +151 -0
- package/src/Revit/Tools/GetElements.cs +725 -0
- package/src/Revit/Tools/GetModelHealth.cs +191 -0
- package/src/Revit/Tools/GetModelOverview.cs +159 -0
- package/src/Revit/Tools/ManageSelection.cs +220 -0
- package/src/Revit/Tools/SearchApiDocs.cs +610 -0
- package/src/Revit/Tools/SetParameters.cs +382 -0
- package/src/Revit/Tools/ToolSupport.cs +261 -0
- package/workspace/AGENTS.md +23 -0
- package/workspace/pi-revit-global.cmd +31 -0
- package/workspace/pi-revit-here.cmd +5 -0
- package/workspace/project-AGENTS.md +8 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
|
|
4
|
+
namespace RevitBridge.Tools
|
|
5
|
+
{
|
|
6
|
+
/// <summary>
|
|
7
|
+
/// Model audit. Document.GetWarnings() returns FailureMessage API objects that
|
|
8
|
+
/// do not survive plain JSON serialization, so they are flattened here into
|
|
9
|
+
/// clean DTOs grouped by description, plus basic structure counts: worksharing
|
|
10
|
+
/// status and user worksets, phases, design options, in-place families, and
|
|
11
|
+
/// the total element count.
|
|
12
|
+
/// </summary>
|
|
13
|
+
internal sealed class GetModelHealth : ITool
|
|
14
|
+
{
|
|
15
|
+
private const int MaxGroups = 100;
|
|
16
|
+
private const int MaxElementsPerGroup = 20;
|
|
17
|
+
|
|
18
|
+
public string Name => "get_model_health";
|
|
19
|
+
public string Label => "Get Model Health";
|
|
20
|
+
public string Description => "Audit the open Revit model: every document warning grouped by description (description, severity, occurrence count, failing element ids+names — the top 100 groups ordered by count, up to 20 elements each), plus structure counts: worksharing status with user worksets, phases, design options, in-place families, and the total element count. Read-only; use it to review model quality, e.g. before and after bulk edits.";
|
|
21
|
+
public string Tier => "advanced";
|
|
22
|
+
|
|
23
|
+
public object ParametersSchema => new { type = "object", properties = new { }, required = Array.Empty<string>() };
|
|
24
|
+
|
|
25
|
+
public string? PromptSnippet => "Audit the model: warnings grouped by description plus workset/phase/design-option counts.";
|
|
26
|
+
public IReadOnlyList<string>? PromptGuidelines => new[]
|
|
27
|
+
{
|
|
28
|
+
"Run get_model_health to review Revit warnings (grouped, with failing element ids) before and after bulk modifications.",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
public object? Execute(JsonElement args, ToolContext context)
|
|
32
|
+
{
|
|
33
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
34
|
+
|
|
35
|
+
var warnings = doc.GetWarnings();
|
|
36
|
+
var groups = warnings
|
|
37
|
+
.GroupBy(SafeDescription, StringComparer.Ordinal)
|
|
38
|
+
.Select(group =>
|
|
39
|
+
{
|
|
40
|
+
var elementIds = group
|
|
41
|
+
.SelectMany(SafeFailingElements)
|
|
42
|
+
.Distinct()
|
|
43
|
+
.ToList();
|
|
44
|
+
return new Dictionary<string, object?>
|
|
45
|
+
{
|
|
46
|
+
["description"] = group.Key,
|
|
47
|
+
["severity"] = SafeSeverity(group.First()),
|
|
48
|
+
["count"] = group.Count(),
|
|
49
|
+
["elementCount"] = elementIds.Count,
|
|
50
|
+
["elements"] = elementIds
|
|
51
|
+
.Take(MaxElementsPerGroup)
|
|
52
|
+
.Select(id => new Dictionary<string, object?>
|
|
53
|
+
{
|
|
54
|
+
["id"] = id.Value,
|
|
55
|
+
["name"] = SafeName(doc, id),
|
|
56
|
+
})
|
|
57
|
+
.ToList(),
|
|
58
|
+
["elementsTruncated"] = elementIds.Count > MaxElementsPerGroup,
|
|
59
|
+
};
|
|
60
|
+
})
|
|
61
|
+
.OrderByDescending(group => (int)group["count"]!)
|
|
62
|
+
.ToList();
|
|
63
|
+
bool groupsTruncated = groups.Count > MaxGroups;
|
|
64
|
+
var topGroups = groups.Take(MaxGroups).ToList();
|
|
65
|
+
|
|
66
|
+
bool workshared = doc.IsWorkshared;
|
|
67
|
+
List<string>? worksetNames = null;
|
|
68
|
+
if (workshared)
|
|
69
|
+
{
|
|
70
|
+
worksetNames = new FilteredWorksetCollector(doc)
|
|
71
|
+
.OfKind(WorksetKind.UserWorkset)
|
|
72
|
+
.ToWorksets()
|
|
73
|
+
.Select(workset => workset.Name)
|
|
74
|
+
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
|
|
75
|
+
.ToList();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
var phaseNames = new List<string>();
|
|
79
|
+
foreach (Phase phase in doc.Phases)
|
|
80
|
+
phaseNames.Add(phase.Name);
|
|
81
|
+
|
|
82
|
+
var designOptionNames = new FilteredElementCollector(doc)
|
|
83
|
+
.OfClass(typeof(DesignOption))
|
|
84
|
+
.Select(option => option.Name)
|
|
85
|
+
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
|
|
86
|
+
.ToList();
|
|
87
|
+
|
|
88
|
+
int inPlaceFamilies = new FilteredElementCollector(doc)
|
|
89
|
+
.OfClass(typeof(Family))
|
|
90
|
+
.Cast<Family>()
|
|
91
|
+
.Count(family => family.IsInPlace);
|
|
92
|
+
|
|
93
|
+
int totalElements = new FilteredElementCollector(doc)
|
|
94
|
+
.WhereElementIsNotElementType()
|
|
95
|
+
.GetElementCount();
|
|
96
|
+
|
|
97
|
+
var payload = new Dictionary<string, object?>
|
|
98
|
+
{
|
|
99
|
+
["warnings"] = new Dictionary<string, object?>
|
|
100
|
+
{
|
|
101
|
+
["total"] = warnings.Count,
|
|
102
|
+
["groupCount"] = groups.Count,
|
|
103
|
+
["groups"] = topGroups,
|
|
104
|
+
["groupsTruncated"] = groupsTruncated,
|
|
105
|
+
},
|
|
106
|
+
["workshared"] = workshared,
|
|
107
|
+
["worksets"] = worksetNames is null ? null : new Dictionary<string, object?>
|
|
108
|
+
{
|
|
109
|
+
["count"] = worksetNames.Count,
|
|
110
|
+
["names"] = worksetNames,
|
|
111
|
+
},
|
|
112
|
+
["phases"] = new Dictionary<string, object?>
|
|
113
|
+
{
|
|
114
|
+
["count"] = phaseNames.Count,
|
|
115
|
+
["names"] = phaseNames,
|
|
116
|
+
},
|
|
117
|
+
["designOptions"] = new Dictionary<string, object?>
|
|
118
|
+
{
|
|
119
|
+
["count"] = designOptionNames.Count,
|
|
120
|
+
["names"] = designOptionNames,
|
|
121
|
+
},
|
|
122
|
+
["inPlaceFamilies"] = inPlaceFamilies,
|
|
123
|
+
["totalElements"] = totalElements,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
string topWarning = topGroups.Count > 0
|
|
127
|
+
? $" Top: '{Shorten((string)topGroups[0]["description"]!, 80)}' x{topGroups[0]["count"]}."
|
|
128
|
+
: string.Empty;
|
|
129
|
+
string compact = $"{warnings.Count} warning(s) in {groups.Count} group(s); "
|
|
130
|
+
+ (workshared ? $"workshared ({worksetNames!.Count} workset(s)); " : "not workshared; ")
|
|
131
|
+
+ $"{phaseNames.Count} phase(s); {designOptionNames.Count} design option(s); {inPlaceFamilies} in-place families; {totalElements} elements."
|
|
132
|
+
+ topWarning;
|
|
133
|
+
|
|
134
|
+
return new ToolOutput(payload, compact);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ------------------------------------------------- FailureMessage flattening
|
|
138
|
+
|
|
139
|
+
private static string SafeDescription(FailureMessage warning)
|
|
140
|
+
{
|
|
141
|
+
try
|
|
142
|
+
{
|
|
143
|
+
string text = warning.GetDescriptionText();
|
|
144
|
+
return string.IsNullOrWhiteSpace(text) ? "(no description)" : text.Trim();
|
|
145
|
+
}
|
|
146
|
+
catch
|
|
147
|
+
{
|
|
148
|
+
return "(no description)";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private static string? SafeSeverity(FailureMessage warning)
|
|
153
|
+
{
|
|
154
|
+
try
|
|
155
|
+
{
|
|
156
|
+
return warning.GetSeverity().ToString();
|
|
157
|
+
}
|
|
158
|
+
catch
|
|
159
|
+
{
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private static IEnumerable<ElementId> SafeFailingElements(FailureMessage warning)
|
|
165
|
+
{
|
|
166
|
+
try
|
|
167
|
+
{
|
|
168
|
+
return warning.GetFailingElements();
|
|
169
|
+
}
|
|
170
|
+
catch
|
|
171
|
+
{
|
|
172
|
+
return Enumerable.Empty<ElementId>();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private static string? SafeName(Document doc, ElementId id)
|
|
177
|
+
{
|
|
178
|
+
try
|
|
179
|
+
{
|
|
180
|
+
return doc.GetElement(id)?.Name;
|
|
181
|
+
}
|
|
182
|
+
catch
|
|
183
|
+
{
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private static string Shorten(string text, int max)
|
|
189
|
+
=> text.Length <= max ? text : text[..(max - 3)] + "...";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
|
|
4
|
+
namespace RevitBridge.Tools
|
|
5
|
+
{
|
|
6
|
+
internal sealed class GetModelOverview : ITool
|
|
7
|
+
{
|
|
8
|
+
public string Name => "get_model_overview";
|
|
9
|
+
public string Label => "Get Model Overview";
|
|
10
|
+
public string Description => "One-call orientation for the open Revit model: project metadata (title, name, number, client, address, file path, Revit version, display units), levels with elevations, grids, element counts for the major categories, and totals (elements, views, sheets). Use this first when starting work on a model.";
|
|
11
|
+
public object ParametersSchema => new { type = "object", properties = new { }, required = Array.Empty<string>() };
|
|
12
|
+
public string? PromptSnippet => "Get project info, units, levels, grids, and category counts for the open Revit model.";
|
|
13
|
+
public IReadOnlyList<string>? PromptGuidelines => new[]
|
|
14
|
+
{
|
|
15
|
+
"Call get_model_overview first when starting work on a Revit model; it covers project info, units, levels, grids, and per-category counts in one call.",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
private static readonly IReadOnlyDictionary<string, BuiltInCategory> MajorCategories = new Dictionary<string, BuiltInCategory>
|
|
19
|
+
{
|
|
20
|
+
["Walls"] = BuiltInCategory.OST_Walls,
|
|
21
|
+
["Doors"] = BuiltInCategory.OST_Doors,
|
|
22
|
+
["Windows"] = BuiltInCategory.OST_Windows,
|
|
23
|
+
["Floors"] = BuiltInCategory.OST_Floors,
|
|
24
|
+
["Rooms"] = BuiltInCategory.OST_Rooms,
|
|
25
|
+
["Areas"] = BuiltInCategory.OST_Areas,
|
|
26
|
+
["Ceilings"] = BuiltInCategory.OST_Ceilings,
|
|
27
|
+
["Roofs"] = BuiltInCategory.OST_Roofs,
|
|
28
|
+
["Columns"] = BuiltInCategory.OST_Columns,
|
|
29
|
+
["Structural Columns"] = BuiltInCategory.OST_StructuralColumns,
|
|
30
|
+
["Structural Framing"] = BuiltInCategory.OST_StructuralFraming,
|
|
31
|
+
["Stairs"] = BuiltInCategory.OST_Stairs,
|
|
32
|
+
["Furniture"] = BuiltInCategory.OST_Furniture,
|
|
33
|
+
["Generic Models"] = BuiltInCategory.OST_GenericModel,
|
|
34
|
+
["Pipes"] = BuiltInCategory.OST_PipeCurves,
|
|
35
|
+
["Ducts"] = BuiltInCategory.OST_DuctCurves,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
public object? Execute(JsonElement args, ToolContext context)
|
|
39
|
+
{
|
|
40
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
41
|
+
var info = doc.ProjectInformation;
|
|
42
|
+
var units = doc.GetUnits();
|
|
43
|
+
|
|
44
|
+
string system = doc.DisplayUnitSystem == DisplayUnit.IMPERIAL ? "imperial" : "metric";
|
|
45
|
+
string? lengthUnit = DisplayUnitName(units, SpecTypeId.Length);
|
|
46
|
+
var project = new Dictionary<string, object?>
|
|
47
|
+
{
|
|
48
|
+
["title"] = doc.Title,
|
|
49
|
+
["name"] = info?.Name,
|
|
50
|
+
["number"] = info?.Number,
|
|
51
|
+
["clientName"] = info?.ClientName,
|
|
52
|
+
["address"] = info?.Address,
|
|
53
|
+
["buildingName"] = info?.BuildingName,
|
|
54
|
+
["author"] = info?.Author,
|
|
55
|
+
["filePath"] = doc.PathName,
|
|
56
|
+
["revitVersion"] = doc.Application.VersionNumber,
|
|
57
|
+
["units"] = new Dictionary<string, object?>
|
|
58
|
+
{
|
|
59
|
+
["system"] = system,
|
|
60
|
+
["length"] = lengthUnit,
|
|
61
|
+
["area"] = DisplayUnitName(units, SpecTypeId.Area),
|
|
62
|
+
["volume"] = DisplayUnitName(units, SpecTypeId.Volume),
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
var levels = new FilteredElementCollector(doc)
|
|
67
|
+
.OfClass(typeof(Level))
|
|
68
|
+
.Cast<Level>()
|
|
69
|
+
.OrderBy(level => level.Elevation)
|
|
70
|
+
.Select(level => new Dictionary<string, object?>
|
|
71
|
+
{
|
|
72
|
+
["id"] = level.Id.Value,
|
|
73
|
+
["name"] = level.Name,
|
|
74
|
+
["elevation_ft"] = level.Elevation,
|
|
75
|
+
["elevation_display"] = FormatLength(units, level.Elevation),
|
|
76
|
+
})
|
|
77
|
+
.ToList();
|
|
78
|
+
|
|
79
|
+
var grids = new FilteredElementCollector(doc)
|
|
80
|
+
.OfClass(typeof(Grid))
|
|
81
|
+
.Cast<Grid>()
|
|
82
|
+
.OrderBy(grid => grid.Name, StringComparer.OrdinalIgnoreCase)
|
|
83
|
+
.Select(grid => new Dictionary<string, object?>
|
|
84
|
+
{
|
|
85
|
+
["id"] = grid.Id.Value,
|
|
86
|
+
["name"] = grid.Name,
|
|
87
|
+
})
|
|
88
|
+
.ToList();
|
|
89
|
+
|
|
90
|
+
var counts = new Dictionary<string, int>();
|
|
91
|
+
foreach (var (label, category) in MajorCategories)
|
|
92
|
+
{
|
|
93
|
+
try
|
|
94
|
+
{
|
|
95
|
+
counts[label] = new FilteredElementCollector(doc)
|
|
96
|
+
.OfCategory(category)
|
|
97
|
+
.WhereElementIsNotElementType()
|
|
98
|
+
.GetElementCount();
|
|
99
|
+
}
|
|
100
|
+
catch
|
|
101
|
+
{
|
|
102
|
+
counts[label] = 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
int totalElements = new FilteredElementCollector(doc).WhereElementIsNotElementType().GetElementCount();
|
|
107
|
+
int sheets = new FilteredElementCollector(doc).OfClass(typeof(ViewSheet)).GetElementCount();
|
|
108
|
+
int views = new FilteredElementCollector(doc)
|
|
109
|
+
.OfClass(typeof(View))
|
|
110
|
+
.Cast<View>()
|
|
111
|
+
.Count(view => !view.IsTemplate && view is not ViewSheet);
|
|
112
|
+
|
|
113
|
+
var totals = new Dictionary<string, int>
|
|
114
|
+
{
|
|
115
|
+
["elements"] = totalElements,
|
|
116
|
+
["views"] = views,
|
|
117
|
+
["sheets"] = sheets,
|
|
118
|
+
["levels"] = levels.Count,
|
|
119
|
+
["grids"] = grids.Count,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
var topCategories = counts
|
|
123
|
+
.Where(pair => pair.Value > 0)
|
|
124
|
+
.OrderByDescending(pair => pair.Value)
|
|
125
|
+
.Take(5)
|
|
126
|
+
.Select(pair => $"{pair.Key} {pair.Value}")
|
|
127
|
+
.ToList();
|
|
128
|
+
string numberPart = string.IsNullOrWhiteSpace(info?.Number) ? string.Empty : $" (project {info!.Number})";
|
|
129
|
+
string compact = $"'{doc.Title}'{numberPart}: {totalElements} elements, {levels.Count} levels, {grids.Count} grids, {sheets} sheets, {views} views; units {system} ({lengthUnit})."
|
|
130
|
+
+ (topCategories.Count > 0 ? $" Top categories: {string.Join(", ", topCategories)}." : string.Empty);
|
|
131
|
+
|
|
132
|
+
return new ToolOutput(new { project, levels, grids, counts, totals }, compact);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private static string? DisplayUnitName(Units units, ForgeTypeId spec)
|
|
136
|
+
{
|
|
137
|
+
try
|
|
138
|
+
{
|
|
139
|
+
return UnitResolver.ShortName(units.GetFormatOptions(spec).GetUnitTypeId());
|
|
140
|
+
}
|
|
141
|
+
catch
|
|
142
|
+
{
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private static string? FormatLength(Units units, double value)
|
|
148
|
+
{
|
|
149
|
+
try
|
|
150
|
+
{
|
|
151
|
+
return UnitFormatUtils.Format(units, SpecTypeId.Length, value, false);
|
|
152
|
+
}
|
|
153
|
+
catch
|
|
154
|
+
{
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
|
|
4
|
+
namespace RevitBridge.Tools
|
|
5
|
+
{
|
|
6
|
+
/// <summary>
|
|
7
|
+
/// Selection and view-focus management. Selection changes and zoom are pure UI
|
|
8
|
+
/// state. Temporary isolate is view state (not model data), but the Revit API
|
|
9
|
+
/// only toggles it inside an open transaction, so exactly that branch wraps a
|
|
10
|
+
/// small internal one; the tool itself stays a read tool (Write = false).
|
|
11
|
+
/// </summary>
|
|
12
|
+
internal sealed class ManageSelection : ITool
|
|
13
|
+
{
|
|
14
|
+
public string Name => "manage_selection";
|
|
15
|
+
public string Label => "Manage Selection";
|
|
16
|
+
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
|
+
public object ParametersSchema => new
|
|
19
|
+
{
|
|
20
|
+
type = "object",
|
|
21
|
+
properties = new
|
|
22
|
+
{
|
|
23
|
+
action = new
|
|
24
|
+
{
|
|
25
|
+
type = "string",
|
|
26
|
+
@enum = new[] { "get", "set", "add", "remove", "clear", "zoom" },
|
|
27
|
+
description = "What to do with the selection. Default get.",
|
|
28
|
+
},
|
|
29
|
+
element_ids = new
|
|
30
|
+
{
|
|
31
|
+
type = "array",
|
|
32
|
+
items = new { type = "integer" },
|
|
33
|
+
description = "Element ids for set/add/remove/zoom. zoom falls back to the current selection when omitted.",
|
|
34
|
+
},
|
|
35
|
+
isolate_in_view = new
|
|
36
|
+
{
|
|
37
|
+
type = "boolean",
|
|
38
|
+
description = "Temporarily isolate the affected elements in the active view (temporary view state only; resets the temporary isolate when the target set is empty). Default false.",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
required = Array.Empty<string>(),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
public string? PromptSnippet => "Read or change the Revit selection; zoom to or temporarily isolate elements.";
|
|
45
|
+
public IReadOnlyList<string>? PromptGuidelines => new[]
|
|
46
|
+
{
|
|
47
|
+
"Use manage_selection (action 'get') when the user refers to elements they selected in Revit ('what is selected', 'these elements').",
|
|
48
|
+
"To show results in Revit, pass ids from get_elements to manage_selection action 'set' or 'zoom', adding isolate_in_view to focus a crowded view.",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
public object? Execute(JsonElement args, ToolContext context)
|
|
52
|
+
{
|
|
53
|
+
var uiDocument = context.UIApplication?.ActiveUIDocument ?? throw new NoActiveDocumentException();
|
|
54
|
+
var doc = uiDocument.Document;
|
|
55
|
+
|
|
56
|
+
string action = (JsonArgs.GetString(args, "action") ?? "get").Trim().ToLowerInvariant();
|
|
57
|
+
bool isolateInView = JsonArgs.GetBool(args, "isolate_in_view", false);
|
|
58
|
+
var (requestedCount, validIds, notFound) = ResolveElementIds(doc, args);
|
|
59
|
+
|
|
60
|
+
if (action is "set" or "add" or "remove" && requestedCount == 0)
|
|
61
|
+
throw new ArgumentException($"action '{action}' needs element_ids. Use action 'clear' to empty the selection.");
|
|
62
|
+
if (requestedCount > 0 && validIds.Count == 0)
|
|
63
|
+
throw new ArgumentException($"None of the element_ids exist in the document: {string.Join(", ", notFound)}.");
|
|
64
|
+
|
|
65
|
+
var payload = new Dictionary<string, object?> { ["action"] = action };
|
|
66
|
+
var compactParts = new List<string>();
|
|
67
|
+
List<ElementId> isolateTargets;
|
|
68
|
+
|
|
69
|
+
switch (action)
|
|
70
|
+
{
|
|
71
|
+
case "get":
|
|
72
|
+
{
|
|
73
|
+
var selectedIds = uiDocument.Selection.GetElementIds().ToList();
|
|
74
|
+
var rows = selectedIds
|
|
75
|
+
.Select(id => doc.GetElement(id))
|
|
76
|
+
.Where(element => element != null)
|
|
77
|
+
.Select(element => ElementIdentity.Build(doc, element!, ElementIdentity.Fields))
|
|
78
|
+
.ToList();
|
|
79
|
+
payload["count"] = rows.Count;
|
|
80
|
+
payload["elements"] = rows;
|
|
81
|
+
string sample = rows.Count > 0
|
|
82
|
+
? $": {string.Join(", ", rows.Take(5).Select(row => $"'{row["name"]}' ({row["id"]})"))}{(rows.Count > 5 ? $" (+{rows.Count - 5} more)" : string.Empty)}"
|
|
83
|
+
: string.Empty;
|
|
84
|
+
compactParts.Add($"{rows.Count} element(s) selected{sample}");
|
|
85
|
+
isolateTargets = selectedIds;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case "set":
|
|
89
|
+
case "add":
|
|
90
|
+
case "remove":
|
|
91
|
+
{
|
|
92
|
+
var current = uiDocument.Selection.GetElementIds();
|
|
93
|
+
List<ElementId> next = action switch
|
|
94
|
+
{
|
|
95
|
+
"set" => validIds,
|
|
96
|
+
"add" => current.Union(validIds).ToList(),
|
|
97
|
+
_ => current.Except(validIds).ToList(),
|
|
98
|
+
};
|
|
99
|
+
uiDocument.Selection.SetElementIds(next);
|
|
100
|
+
payload["selectedCount"] = next.Count;
|
|
101
|
+
compactParts.Add(action switch
|
|
102
|
+
{
|
|
103
|
+
"set" => $"Selection set to {next.Count} element(s)",
|
|
104
|
+
"add" => $"Added {next.Count - current.Count} element(s); {next.Count} now selected",
|
|
105
|
+
_ => $"Removed {current.Count - next.Count} element(s); {next.Count} still selected",
|
|
106
|
+
});
|
|
107
|
+
isolateTargets = next;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case "clear":
|
|
111
|
+
{
|
|
112
|
+
uiDocument.Selection.SetElementIds(new List<ElementId>());
|
|
113
|
+
payload["selectedCount"] = 0;
|
|
114
|
+
compactParts.Add("Selection cleared");
|
|
115
|
+
isolateTargets = new List<ElementId>();
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "zoom":
|
|
119
|
+
{
|
|
120
|
+
var targets = validIds.Count > 0 ? validIds : uiDocument.Selection.GetElementIds().ToList();
|
|
121
|
+
if (targets.Count == 0)
|
|
122
|
+
throw new ArgumentException("action 'zoom' needs element_ids, or a non-empty current selection to zoom to.");
|
|
123
|
+
uiDocument.ShowElements(targets);
|
|
124
|
+
payload["shownCount"] = targets.Count;
|
|
125
|
+
compactParts.Add($"Zoomed open views to {targets.Count} element(s)");
|
|
126
|
+
isolateTargets = targets;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
default:
|
|
130
|
+
throw new ArgumentException($"Unknown action: {action}. Supported: get, set, add, remove, clear, zoom.");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (notFound.Count > 0)
|
|
134
|
+
{
|
|
135
|
+
payload["not_found"] = notFound;
|
|
136
|
+
compactParts.Add($"ids not found: {string.Join(", ", notFound)}");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (isolateInView)
|
|
140
|
+
{
|
|
141
|
+
var view = uiDocument.ActiveGraphicalView
|
|
142
|
+
?? throw new ArgumentException("isolate_in_view requires an active graphical view in Revit.");
|
|
143
|
+
ApplyTemporaryIsolate(doc, view, isolateTargets);
|
|
144
|
+
payload["viewId"] = view.Id.Value;
|
|
145
|
+
payload["viewName"] = view.Name;
|
|
146
|
+
if (isolateTargets.Count == 0)
|
|
147
|
+
{
|
|
148
|
+
payload["temporaryIsolateReset"] = true;
|
|
149
|
+
compactParts.Add($"temporary isolate reset in view '{view.Name}'");
|
|
150
|
+
}
|
|
151
|
+
else
|
|
152
|
+
{
|
|
153
|
+
payload["isolatedCount"] = isolateTargets.Count;
|
|
154
|
+
compactParts.Add($"{isolateTargets.Count} element(s) temporarily isolated in view '{view.Name}'");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else
|
|
158
|
+
{
|
|
159
|
+
payload["activeViewId"] = doc.ActiveView?.Id.Value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return new ToolOutput(payload, string.Join("; ", compactParts) + ".");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private static (int RequestedCount, List<ElementId> Valid, List<long> NotFound) ResolveElementIds(Document doc, JsonElement args)
|
|
166
|
+
{
|
|
167
|
+
bool present = args.ValueKind == JsonValueKind.Object
|
|
168
|
+
&& args.TryGetProperty("element_ids", out var idsElement)
|
|
169
|
+
&& idsElement.ValueKind != JsonValueKind.Null;
|
|
170
|
+
if (!present)
|
|
171
|
+
return (0, new List<ElementId>(), new List<long>());
|
|
172
|
+
|
|
173
|
+
var requested = JsonArgs.GetLongArray(args, "element_ids");
|
|
174
|
+
var valid = new List<ElementId>(requested.Count);
|
|
175
|
+
var notFound = new List<long>();
|
|
176
|
+
foreach (long id in requested.Distinct())
|
|
177
|
+
{
|
|
178
|
+
var elementId = new ElementId(id);
|
|
179
|
+
if (doc.GetElement(elementId) != null)
|
|
180
|
+
valid.Add(elementId);
|
|
181
|
+
else
|
|
182
|
+
notFound.Add(id);
|
|
183
|
+
}
|
|
184
|
+
return (requested.Count, valid, notFound);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// <summary>
|
|
188
|
+
/// Temporary hide/isolate is view state, not model data, but the Revit API
|
|
189
|
+
/// only changes it inside an open transaction — so exactly this branch wraps
|
|
190
|
+
/// a small one while the tool stays Write = false.
|
|
191
|
+
/// </summary>
|
|
192
|
+
private static void ApplyTemporaryIsolate(Document doc, View view, ICollection<ElementId> elementIds)
|
|
193
|
+
{
|
|
194
|
+
using var transaction = new Transaction(doc, "manage_selection: temporary isolate");
|
|
195
|
+
if (transaction.Start() != TransactionStatus.Started)
|
|
196
|
+
throw new InvalidOperationException("Unable to start the temporary-isolate transaction.");
|
|
197
|
+
try
|
|
198
|
+
{
|
|
199
|
+
if (elementIds.Count == 0)
|
|
200
|
+
view.DisableTemporaryViewMode(TemporaryViewMode.TemporaryHideIsolate);
|
|
201
|
+
else
|
|
202
|
+
view.IsolateElementsTemporary(elementIds);
|
|
203
|
+
if (transaction.Commit() != TransactionStatus.Committed)
|
|
204
|
+
throw new InvalidOperationException("The temporary-isolate transaction failed to commit.");
|
|
205
|
+
}
|
|
206
|
+
catch (Autodesk.Revit.Exceptions.InvalidOperationException ex)
|
|
207
|
+
{
|
|
208
|
+
if (transaction.GetStatus() == TransactionStatus.Started)
|
|
209
|
+
transaction.RollBack();
|
|
210
|
+
throw new ArgumentException($"The active view '{view.Name}' does not support temporary isolate: {ex.Message}");
|
|
211
|
+
}
|
|
212
|
+
catch
|
|
213
|
+
{
|
|
214
|
+
if (transaction.GetStatus() == TransactionStatus.Started)
|
|
215
|
+
transaction.RollBack();
|
|
216
|
+
throw;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|