304 lines
11 KiB
C#
304 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Newtonsoft.Json;
|
|
using Unity.Pipeline.Commands;
|
|
using Unity.Pipeline.Editor.Authoring;
|
|
using Unity.Pipeline.Models;
|
|
using UnityEditor;
|
|
using UnityEditor.Search;
|
|
using UnityEngine;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace Unity.Pipeline.Editor.Commands.Navigation
|
|
{
|
|
/// <summary>
|
|
/// Navigation and targeting commands (CLI-200): read and drive the Editor selection, and run
|
|
/// Unity Search queries — both returning the canonical <see cref="AuthoringResult"/> object
|
|
/// identity (via <see cref="ObjectResolver.Describe"/>) so an agent can reference results in a
|
|
/// follow-up call. All commands are read-only or non-destructive (set_selection only changes the
|
|
/// Editor selection, which carries no undo/safety policy), and run on the main thread.
|
|
/// </summary>
|
|
public static class NavigationCommands
|
|
{
|
|
private const int MaxSearchResults = 200;
|
|
|
|
[CliCommand("get_selection", "Read the current Editor selection as structured object identities.")]
|
|
public static SelectionResult GetSelection()
|
|
{
|
|
return DescribeSelection(null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <para>Set the Editor selection from instance ids and/or asset paths.</para>
|
|
/// <para>
|
|
/// We accept simple <paramref name="instanceIds"/> / <paramref name="paths"/> rather than full
|
|
/// <see cref="ObjectRef"/>[] handles because nested-object parameter schemas (arrays of objects)
|
|
/// are pending CAT-2508. Once that lands this can take an <see cref="ObjectRef"/>[] and route
|
|
/// each through <see cref="ObjectResolver.TryResolve"/> for the full handle surface.
|
|
/// </para>
|
|
/// </summary>
|
|
[CliCommand("set_selection", "Set the Editor selection to the given assets/scene objects.")]
|
|
public static SelectionResult SetSelection(
|
|
[CliArg("instance_ids", "Scene/loaded object instance IDs to select.")] ObjectId[] instanceIds = null,
|
|
[CliArg("paths", "Asset paths to select (e.g. Assets/Foo.prefab).")] string[] paths = null)
|
|
{
|
|
var resolved = new List<Object>();
|
|
var unresolved = new List<string>();
|
|
|
|
if (instanceIds != null)
|
|
{
|
|
foreach (var id in instanceIds)
|
|
{
|
|
var obj = PipelineUtils.IdToObject(id);
|
|
if (obj != null)
|
|
resolved.Add(obj);
|
|
else
|
|
unresolved.Add($"instanceId:{id}");
|
|
}
|
|
}
|
|
|
|
if (paths != null)
|
|
{
|
|
foreach (var path in paths)
|
|
{
|
|
var obj = string.IsNullOrEmpty(path) ? null : AssetDatabase.LoadMainAssetAtPath(path);
|
|
if (obj != null)
|
|
resolved.Add(obj);
|
|
else if (path == null)
|
|
unresolved.Add("<null>");
|
|
else if (path.Length == 0)
|
|
unresolved.Add("<empty>");
|
|
else
|
|
unresolved.Add(path);
|
|
}
|
|
}
|
|
|
|
var hadInputs = (instanceIds != null && instanceIds.Length > 0) ||
|
|
(paths != null && paths.Length > 0);
|
|
|
|
// Inputs were given but none resolved: that's a hard error, not a silent clear.
|
|
if (resolved.Count == 0 && hadInputs)
|
|
throw new ArgumentException($"No objects resolved from the given inputs. Unresolved: {string.Join(", ", unresolved)}");
|
|
|
|
// No inputs at all is a valid request to clear the selection.
|
|
var objects = resolved.ToArray();
|
|
Selection.objects = objects;
|
|
Selection.activeObject = resolved.FirstOrDefault();
|
|
|
|
return DescribeSelection(unresolved.ToArray());
|
|
}
|
|
|
|
[CliCommand("search", "Run a Unity Search query and return structured results.")]
|
|
public static SearchResult Search(
|
|
[CliArg("query", "Unity Search query string, e.g. 't:Material', 'p: my asset', 'h: Main Camera'.", Required = true)] string query,
|
|
[CliArg("limit", "Max results to return (capped 200).")] int limit = 50)
|
|
{
|
|
if (string.IsNullOrEmpty(query))
|
|
throw new ArgumentException("query must be a non-empty Unity Search query string.");
|
|
|
|
var clamped = Mathf.Clamp(limit, 0, MaxSearchResults);
|
|
|
|
// A non-positive limit can't return any rows; skip the potentially expensive request.
|
|
if (clamped <= 0)
|
|
{
|
|
return new SearchResult
|
|
{
|
|
Query = query,
|
|
Count = 0,
|
|
Results = Array.Empty<SearchResultItem>()
|
|
};
|
|
}
|
|
|
|
var results = new List<SearchResultItem>();
|
|
|
|
ISearchList list = null;
|
|
try
|
|
{
|
|
// Synchronous request returns the items already resolved on the main thread.
|
|
list = SearchService.Request(query, SearchFlags.Synchronous);
|
|
|
|
foreach (var item in list)
|
|
{
|
|
if (results.Count >= clamped)
|
|
break;
|
|
if (item == null)
|
|
continue;
|
|
|
|
// One malformed item must not fail the whole call.
|
|
try
|
|
{
|
|
results.Add(MapItem(item));
|
|
}
|
|
catch
|
|
{
|
|
// skip the unmappable item
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new InvalidOperationException($"Unity Search query '{query}' failed: {ex.Message}", ex);
|
|
}
|
|
finally
|
|
{
|
|
// The Synchronous Request overload returns an ISearchList that owns a SearchContext;
|
|
// dispose it when present so we don't leak the context.
|
|
(list as IDisposable)?.Dispose();
|
|
}
|
|
|
|
return new SearchResult
|
|
{
|
|
Query = query,
|
|
Count = results.Count,
|
|
Results = results.ToArray()
|
|
};
|
|
}
|
|
|
|
private static SearchResultItem MapItem(SearchItem item)
|
|
{
|
|
string label;
|
|
try
|
|
{
|
|
label = item.GetLabel(item.context, true);
|
|
}
|
|
catch
|
|
{
|
|
label = null;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(label))
|
|
label = item.label ?? item.id;
|
|
|
|
string description = null;
|
|
try
|
|
{
|
|
description = item.GetDescription(item.context, true);
|
|
}
|
|
catch
|
|
{
|
|
// leave description null
|
|
}
|
|
|
|
string path = null;
|
|
try
|
|
{
|
|
var obj = item.ToObject();
|
|
if (obj != null)
|
|
{
|
|
var assetPath = AssetDatabase.GetAssetPath(obj);
|
|
if (!string.IsNullOrEmpty(assetPath))
|
|
path = assetPath;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// leave path null
|
|
}
|
|
|
|
return new SearchResultItem
|
|
{
|
|
Id = item.id,
|
|
Label = label,
|
|
Description = description,
|
|
Provider = item.provider?.id,
|
|
Path = path
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Snapshot the current <see cref="Selection"/> as canonical identities, optionally tagging on
|
|
/// the <paramref name="unresolved"/> list from a set_selection call.
|
|
/// </summary>
|
|
private static SelectionResult DescribeSelection(string[] unresolved)
|
|
{
|
|
var objects = Selection.objects ?? Array.Empty<Object>();
|
|
var described = objects
|
|
.Select(ObjectResolver.Describe)
|
|
.Where(r => r != null)
|
|
.ToArray();
|
|
|
|
return new SelectionResult
|
|
{
|
|
Count = described.Length,
|
|
Active = ObjectResolver.Describe(Selection.activeObject),
|
|
Objects = described,
|
|
Unresolved = unresolved
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Structured snapshot of the Editor selection: the active object plus every selected object,
|
|
/// each as a canonical <see cref="AuthoringResult"/> identity. <see cref="Unresolved"/> is only
|
|
/// populated by set_selection (inputs that did not resolve to a loaded object/asset).
|
|
/// </summary>
|
|
[Serializable]
|
|
public class SelectionResult
|
|
{
|
|
/// <summary>Number of selected objects (excludes nulls).</summary>
|
|
[JsonProperty("count")]
|
|
public int Count { get; set; }
|
|
|
|
/// <summary>The active selection object, or null when the selection is empty.</summary>
|
|
[JsonProperty("active")]
|
|
public AuthoringResult Active { get; set; }
|
|
|
|
/// <summary>Every selected object as a canonical identity (nulls skipped).</summary>
|
|
[JsonProperty("objects")]
|
|
public AuthoringResult[] Objects { get; set; }
|
|
|
|
/// <summary>Inputs that did not resolve (set_selection only); null for get_selection.</summary>
|
|
[JsonProperty("unresolved")]
|
|
public string[] Unresolved { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Structured result of a Unity Search query: the echoed query, the returned count, and the
|
|
/// mapped <see cref="SearchResultItem"/> rows (capped at the requested limit).
|
|
/// </summary>
|
|
[Serializable]
|
|
public class SearchResult
|
|
{
|
|
/// <summary>The query that was executed (echoed back).</summary>
|
|
[JsonProperty("query")]
|
|
public string Query { get; set; }
|
|
|
|
/// <summary>Number of results returned (after the limit cap).</summary>
|
|
[JsonProperty("count")]
|
|
public int Count { get; set; }
|
|
|
|
/// <summary>The mapped search result rows.</summary>
|
|
[JsonProperty("results")]
|
|
public SearchResultItem[] Results { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// One Unity Search result, flattened to portable fields. <see cref="Path"/> is populated only
|
|
/// when the item resolves to an asset object.
|
|
/// </summary>
|
|
[Serializable]
|
|
public class SearchResultItem
|
|
{
|
|
/// <summary>Provider-specific item id (e.g. an asset path or scene object id).</summary>
|
|
[JsonProperty("id")]
|
|
public string Id { get; set; }
|
|
|
|
/// <summary>Display label for the item.</summary>
|
|
[JsonProperty("label")]
|
|
public string Label { get; set; }
|
|
|
|
/// <summary>Longer description, when the provider supplies one (may be null).</summary>
|
|
[JsonProperty("description")]
|
|
public string Description { get; set; }
|
|
|
|
/// <summary>Id of the search provider that produced this item (e.g. "asset", "scene").</summary>
|
|
[JsonProperty("provider")]
|
|
public string Provider { get; set; }
|
|
|
|
/// <summary>Project-relative asset path, when the item resolves to an asset (else null).</summary>
|
|
[JsonProperty("path")]
|
|
public string Path { get; set; }
|
|
}
|
|
}
|