using System.Text.Json;
using Autodesk.Revit.DB;
namespace RevitBridge.Tools
{
///
/// Selection and view-focus management. Selection changes and zoom are pure UI
/// state. Temporary isolate is view state (not model data), but the Revit API
/// only toggles it inside an open transaction, so exactly that branch wraps a
/// small internal one; the tool itself stays a read tool (Write = false).
///
internal sealed class ManageSelection : ITool
{
public string Name => "manage_selection";
public string Label => "Manage Selection";
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.";
public object ParametersSchema => new
{
type = "object",
properties = new
{
action = new
{
type = "string",
@enum = new[] { "get", "set", "add", "remove", "clear", "zoom" },
description = "What to do with the selection. Default get.",
},
element_ids = new
{
type = "array",
items = new { type = "integer" },
description = "Element ids for set/add/remove/zoom. zoom falls back to the current selection when omitted.",
},
isolate_in_view = new
{
type = "boolean",
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.",
},
},
required = Array.Empty(),
};
public string? PromptSnippet => "Read or change the Revit selection; zoom to or temporarily isolate elements.";
public IReadOnlyList? PromptGuidelines => new[]
{
"Use manage_selection (action 'get') when the user refers to elements they selected in Revit ('what is selected', 'these elements').",
"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.",
};
public object? Execute(JsonElement args, ToolContext context)
{
var uiDocument = context.UIApplication?.ActiveUIDocument ?? throw new NoActiveDocumentException();
var doc = uiDocument.Document;
string action = (JsonArgs.GetString(args, "action") ?? "get").Trim().ToLowerInvariant();
bool isolateInView = JsonArgs.GetBool(args, "isolate_in_view", false);
var (requestedCount, validIds, notFound) = ResolveElementIds(doc, args);
if (action is "set" or "add" or "remove" && requestedCount == 0)
throw new ArgumentException($"action '{action}' needs element_ids. Use action 'clear' to empty the selection.");
if (requestedCount > 0 && validIds.Count == 0)
throw new ArgumentException($"None of the element_ids exist in the document: {string.Join(", ", notFound)}.");
var payload = new Dictionary { ["action"] = action };
var compactParts = new List();
List isolateTargets;
switch (action)
{
case "get":
{
var selectedIds = uiDocument.Selection.GetElementIds().ToList();
var rows = selectedIds
.Select(id => doc.GetElement(id))
.Where(element => element != null)
.Select(element => ElementIdentity.Build(doc, element!, ElementIdentity.Fields))
.ToList();
payload["count"] = rows.Count;
payload["elements"] = rows;
string sample = rows.Count > 0
? $": {string.Join(", ", rows.Take(5).Select(row => $"'{row["name"]}' ({row["id"]})"))}{(rows.Count > 5 ? $" (+{rows.Count - 5} more)" : string.Empty)}"
: string.Empty;
compactParts.Add($"{rows.Count} element(s) selected{sample}");
isolateTargets = selectedIds;
break;
}
case "set":
case "add":
case "remove":
{
var current = uiDocument.Selection.GetElementIds();
List next = action switch
{
"set" => validIds,
"add" => current.Union(validIds).ToList(),
_ => current.Except(validIds).ToList(),
};
uiDocument.Selection.SetElementIds(next);
payload["selectedCount"] = next.Count;
compactParts.Add(action switch
{
"set" => $"Selection set to {next.Count} element(s)",
"add" => $"Added {next.Count - current.Count} element(s); {next.Count} now selected",
_ => $"Removed {current.Count - next.Count} element(s); {next.Count} still selected",
});
isolateTargets = next;
break;
}
case "clear":
{
uiDocument.Selection.SetElementIds(new List());
payload["selectedCount"] = 0;
compactParts.Add("Selection cleared");
isolateTargets = new List();
break;
}
case "zoom":
{
var targets = validIds.Count > 0 ? validIds : uiDocument.Selection.GetElementIds().ToList();
if (targets.Count == 0)
throw new ArgumentException("action 'zoom' needs element_ids, or a non-empty current selection to zoom to.");
uiDocument.ShowElements(targets);
payload["shownCount"] = targets.Count;
compactParts.Add($"Zoomed open views to {targets.Count} element(s)");
isolateTargets = targets;
break;
}
default:
throw new ArgumentException($"Unknown action: {action}. Supported: get, set, add, remove, clear, zoom.");
}
if (notFound.Count > 0)
{
payload["not_found"] = notFound;
compactParts.Add($"ids not found: {string.Join(", ", notFound)}");
}
if (isolateInView)
{
var view = uiDocument.ActiveGraphicalView
?? throw new ArgumentException("isolate_in_view requires an active graphical view in Revit.");
ApplyTemporaryIsolate(doc, view, isolateTargets);
payload["viewId"] = view.Id.Value;
payload["viewName"] = view.Name;
if (isolateTargets.Count == 0)
{
payload["temporaryIsolateReset"] = true;
compactParts.Add($"temporary isolate reset in view '{view.Name}'");
}
else
{
payload["isolatedCount"] = isolateTargets.Count;
compactParts.Add($"{isolateTargets.Count} element(s) temporarily isolated in view '{view.Name}'");
}
}
else
{
payload["activeViewId"] = doc.ActiveView?.Id.Value;
}
return new ToolOutput(payload, string.Join("; ", compactParts) + ".");
}
private static (int RequestedCount, List Valid, List NotFound) ResolveElementIds(Document doc, JsonElement args)
{
bool present = args.ValueKind == JsonValueKind.Object
&& args.TryGetProperty("element_ids", out var idsElement)
&& idsElement.ValueKind != JsonValueKind.Null;
if (!present)
return (0, new List(), new List());
var requested = JsonArgs.GetLongArray(args, "element_ids");
var valid = new List(requested.Count);
var notFound = new List();
foreach (long id in requested.Distinct())
{
var elementId = new ElementId(id);
if (doc.GetElement(elementId) != null)
valid.Add(elementId);
else
notFound.Add(id);
}
return (requested.Count, valid, notFound);
}
///
/// Temporary hide/isolate is view state, not model data, but the Revit API
/// only changes it inside an open transaction — so exactly this branch wraps
/// a small one while the tool stays Write = false.
///
private static void ApplyTemporaryIsolate(Document doc, View view, ICollection elementIds)
{
using var transaction = new Transaction(doc, "manage_selection: temporary isolate");
var failureGuard = FailureGuard.Attach(transaction);
if (transaction.Start() != TransactionStatus.Started)
throw new InvalidOperationException("Unable to start the temporary-isolate transaction.");
try
{
if (elementIds.Count == 0)
view.DisableTemporaryViewMode(TemporaryViewMode.TemporaryHideIsolate);
else
view.IsolateElementsTemporary(elementIds);
if (transaction.Commit() != TransactionStatus.Committed)
throw new InvalidOperationException("The temporary-isolate transaction failed to commit." + failureGuard.DescribeErrors());
}
catch (Autodesk.Revit.Exceptions.InvalidOperationException ex)
{
if (transaction.GetStatus() == TransactionStatus.Started)
transaction.RollBack();
throw new ArgumentException($"The active view '{view.Name}' does not support temporary isolate: {ex.Message}");
}
catch
{
if (transaction.GetStatus() == TransactionStatus.Started)
transaction.RollBack();
throw;
}
}
}
}