unity-mcp/MCPForUnity/Editor/Helpers/PrefabUtilityHelper.cs

229 lines
7.9 KiB
C#
Raw Normal View History

feat: Prefab Feature Updates (#611) * feat: Add prefab read operations (get_info, get_hierarchy, list_prefabs) - Add get_info: retrieve prefab metadata (GUID, type, components, child count, variant info) - Add get_hierarchy: get prefab internal structure with pagination support - Add list_prefabs: search prefabs in project with optional name filtering - Extract PrefabUtilityHelper class for reusable prefab utility methods - Update Python tool descriptions and parameter documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Use correct API to save prefab stage changes Replace PrefabUtility.SaveAsPrefabAsset (for creating new prefabs) with EditorSceneManager.SaveScene to properly save stage modifications. This fixes the issue where component additions were lost after closing the prefab stage. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: improve code quality and error handling - Add pagination constants (DefaultPageSize, MaxPageSize) - Extract SaveAndRefreshStage helper to reduce duplication - Change all user-facing messages to English - Add REQUIRED_PARAMS validation in Python - Split path parameter into prefab_path and folder_path for clarity - Improve error handling with specific exception types Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: Remove list_prefabs action and update related documentation * feat: Enhance prefab management with detailed parameter descriptions and new unlinking option * feat: Simplify prefab creation logic and unify logging for asset replacement * feat: Update SaveStagePrefab method to use SetDirty and SaveAssets for prefab stage saving * feat: Add PrefabUtilityHelper class with utility methods for prefab asset management * feat: Refactor action constants and enhance parameter validation in prefab management * feat: Update ValidateSourceObjectForPrefab method to remove replaceExisting parameter and simplify validation logic * fix: Fix searchInactive parameter and improve prefab management - Fix searchInactive not working correctly for child objects - Improve error message accuracy for object not found - Use Application.dataPath for reliable directory path resolution * feat: Add path validation and security checks for prefab operations * feat: Remove pagination from GetHierarchy method and simplify prefab retrieval * feat: Remove mode parameter from prefab management functions to simplify usage * fix: Improve path validation and replace logic in prefab management * feat: Enhance prefab management by adding nesting depth and parent prefab path retrieval * fix: resolve Unknown pseudo class last-child USS warnings Unity UI Toolkit does not support the :last-child pseudo-class. Replace it with a .section-last class that is applied programmatically to the last section in each .section-stack container. Also moves the Configure All Detected Clients button to the bottom of the Client Configuration section and makes it auto-width. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: improve prefab stage save for automated workflows - Add force parameter to save_open_stage for automated workflows where isDirty may not be correctly set - Use PrefabUtility.SaveAsPrefabAsset for dialog-free saving - Mark prefab stage scene dirty when modifying GameObjects in prefab mode - Skip save when no changes and force=false (prevents false dirty flag) The force parameter ensures reliable saving in CI/automation scenarios where Unity dirty tracking may be inconsistent with programmatic changes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update prefab.py * refactor: remove unnecessary blank line before create function * feat: add info and hierarchy commands to prefab CLI for enhanced prefab management * feat: enhance prefab management with comprehensive CRUD tests and ensure dirty state tracking --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: David Sarno <david@lighthaus.us>
2026-01-26 08:36:29 +08:00
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Provides common utility methods for working with Unity Prefab assets.
/// </summary>
public static class PrefabUtilityHelper
{
/// <summary>
/// Gets the GUID for a prefab asset path.
/// </summary>
/// <param name="assetPath">The Unity asset path (e.g., "Assets/Prefabs/MyPrefab.prefab")</param>
/// <returns>The GUID string, or null if the path is invalid.</returns>
public static string GetPrefabGUID(string assetPath)
{
if (string.IsNullOrEmpty(assetPath))
{
return null;
}
try
{
return AssetDatabase.AssetPathToGUID(assetPath);
}
catch (Exception ex)
{
McpLog.Warn($"Failed to get GUID for asset path '{assetPath}': {ex.Message}");
return null;
}
}
/// <summary>
/// Gets variant information if the prefab is a variant.
/// </summary>
/// <param name="prefabAsset">The prefab GameObject to check.</param>
/// <returns>A tuple containing (isVariant, parentPath, parentGuid).</returns>
public static (bool isVariant, string parentPath, string parentGuid) GetVariantInfo(GameObject prefabAsset)
{
if (prefabAsset == null)
{
return (false, null, null);
}
try
{
PrefabAssetType assetType = PrefabUtility.GetPrefabAssetType(prefabAsset);
if (assetType != PrefabAssetType.Variant)
{
return (false, null, null);
}
GameObject parentAsset = PrefabUtility.GetCorrespondingObjectFromSource(prefabAsset);
if (parentAsset == null)
{
return (true, null, null);
}
string parentPath = AssetDatabase.GetAssetPath(parentAsset);
string parentGuid = GetPrefabGUID(parentPath);
return (true, parentPath, parentGuid);
}
catch (Exception ex)
{
McpLog.Warn($"Failed to get variant info for '{prefabAsset.name}': {ex.Message}");
return (false, null, null);
}
}
/// <summary>
/// Gets the list of component type names on a GameObject.
/// </summary>
/// <param name="obj">The GameObject to inspect.</param>
/// <returns>A list of component type full names.</returns>
public static List<string> GetComponentTypeNames(GameObject obj)
{
var typeNames = new List<string>();
if (obj == null)
{
return typeNames;
}
try
{
var components = obj.GetComponents<Component>();
foreach (var component in components)
{
if (component != null)
{
typeNames.Add(component.GetType().FullName);
}
}
}
catch (Exception ex)
{
McpLog.Warn($"Failed to get component types for '{obj.name}': {ex.Message}");
}
return typeNames;
}
/// <summary>
/// Recursively counts all children in the hierarchy.
/// </summary>
/// <param name="transform">The root transform to count from.</param>
/// <returns>Total number of children in the hierarchy.</returns>
public static int CountChildrenRecursive(Transform transform)
{
if (transform == null)
{
return 0;
}
int count = transform.childCount;
for (int i = 0; i < transform.childCount; i++)
{
count += CountChildrenRecursive(transform.GetChild(i));
}
return count;
}
/// <summary>
/// Gets the source prefab path for a nested prefab instance.
/// </summary>
/// <param name="gameObject">The GameObject to check.</param>
/// <returns>The asset path of the source prefab, or null if not a nested prefab.</returns>
public static string GetNestedPrefabPath(GameObject gameObject)
{
if (gameObject == null || !PrefabUtility.IsAnyPrefabInstanceRoot(gameObject))
{
return null;
}
try
{
var sourcePrefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject);
if (sourcePrefab != null)
{
return AssetDatabase.GetAssetPath(sourcePrefab);
}
}
catch (Exception ex)
{
McpLog.Warn($"Failed to get nested prefab path for '{gameObject.name}': {ex.Message}");
}
return null;
}
/// <summary>
/// Gets the nesting depth of a prefab instance within the prefab hierarchy.
/// Returns 0 for main prefab root, 1 for first-level nested, 2 for second-level, etc.
/// Returns -1 for non-prefab-root objects.
/// </summary>
/// <param name="gameObject">The GameObject to analyze.</param>
/// <param name="mainPrefabRoot">The root transform of the main prefab asset.</param>
/// <returns>Nesting depth (0=main root, 1+=nested), or -1 if not a prefab root.</returns>
public static int GetPrefabNestingDepth(GameObject gameObject, Transform mainPrefabRoot)
{
if (gameObject == null)
return -1;
// Main prefab root
if (gameObject.transform == mainPrefabRoot)
return 0;
// Not a prefab instance root
if (!PrefabUtility.IsAnyPrefabInstanceRoot(gameObject))
return -1;
// Calculate depth by walking up the hierarchy
int depth = 0;
Transform current = gameObject.transform;
while (current != null && current != mainPrefabRoot)
{
if (PrefabUtility.IsAnyPrefabInstanceRoot(current.gameObject))
{
depth++;
}
current = current.parent;
}
return depth;
}
/// <summary>
/// Gets the parent prefab path for a nested prefab instance.
/// Returns null for main prefab root or non-prefab objects.
/// </summary>
/// <param name="gameObject">The GameObject to analyze.</param>
/// <param name="mainPrefabRoot">The root transform of the main prefab asset.</param>
/// <returns>The asset path of the parent prefab, or null if none.</returns>
public static string GetParentPrefabPath(GameObject gameObject, Transform mainPrefabRoot)
{
if (gameObject == null || gameObject.transform == mainPrefabRoot)
return null;
if (!PrefabUtility.IsAnyPrefabInstanceRoot(gameObject))
return null;
// Walk up the hierarchy to find the parent prefab instance
Transform current = gameObject.transform.parent;
while (current != null && current != mainPrefabRoot)
{
if (PrefabUtility.IsAnyPrefabInstanceRoot(current.gameObject))
{
return GetNestedPrefabPath(current.gameObject);
}
current = current.parent;
}
// Parent is the main prefab root - get its asset path
if (mainPrefabRoot != null)
{
return AssetDatabase.GetAssetPath(mainPrefabRoot.gameObject);
}
return null;
}
}
}