// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Editor.Utils { #if UNITY_EDITOR using System; using System.Collections.Generic; using UnityEditor; /// /// Provides allocation-optimized iteration over SerializedObject properties. /// Uses GetIterator() to avoid repeated FindProperty() allocations. /// internal static class SerializedPropertyIterator { private const string ScriptPropertyPath = "m_Script"; /// /// Iterates through all visible properties of a SerializedObject once, /// invoking callbacks for each property based on whether it matches a set of paths to draw. /// This avoids O(n) FindProperty calls by doing a single O(n) iteration. /// /// The serialized object to iterate. /// Set of property paths that should be drawn with the custom drawer. /// Called when m_Script property is found (may be null if not found). /// Called for each property whose path is in pathsToDraw. /// Called for each property whose path is NOT in pathsToDraw. internal static void IterateVisibleProperties( SerializedObject serializedObject, HashSet pathsToDraw, Action scriptPropertyCallback, Action matchedPropertyCallback, Action unmatchedPropertyCallback ) { if (serializedObject == null) { return; } SerializedProperty iterator = serializedObject.GetIterator(); bool enterChildren = true; while (iterator.NextVisible(enterChildren)) { enterChildren = false; string path = iterator.propertyPath; if (string.Equals(path, ScriptPropertyPath, StringComparison.Ordinal)) { scriptPropertyCallback?.Invoke(iterator); continue; } if (pathsToDraw != null && pathsToDraw.Contains(path)) { matchedPropertyCallback?.Invoke(iterator); } else { unmatchedPropertyCallback?.Invoke(iterator); } } } /// /// Iterates through all visible properties and collects copies into a pooled dictionary. /// Use this when you need random access to properties by path. /// Note: The returned properties are copies that remain valid after iteration. /// /// The serialized object to iterate. /// Dictionary to populate with property copies keyed by path. /// If true, excludes the m_Script property. internal static void BuildPropertyLookup( SerializedObject serializedObject, Dictionary propertyLookup, bool excludeScript = true ) { if (serializedObject == null || propertyLookup == null) { return; } propertyLookup.Clear(); SerializedProperty iterator = serializedObject.GetIterator(); bool enterChildren = true; while (iterator.NextVisible(enterChildren)) { enterChildren = false; string path = iterator.propertyPath; if ( excludeScript && string.Equals(path, ScriptPropertyPath, StringComparison.Ordinal) ) { continue; } propertyLookup[path] = iterator.Copy(); } } /// /// Finds a specific property by iterating through visible properties. /// More efficient than FindProperty when you only need one property and are already iterating. /// /// The serialized object to search. /// The path of the property to find. /// A copy of the property if found, null otherwise. internal static SerializedProperty FindPropertyByIteration( SerializedObject serializedObject, string propertyPath ) { if (serializedObject == null || string.IsNullOrEmpty(propertyPath)) { return null; } SerializedProperty iterator = serializedObject.GetIterator(); bool enterChildren = true; while (iterator.NextVisible(enterChildren)) { enterChildren = false; if (string.Equals(iterator.propertyPath, propertyPath, StringComparison.Ordinal)) { return iterator.Copy(); } } return null; } /// /// Draws properties using a single iteration pass. /// Groups are collected first, then drawn during iteration when their anchor property is encountered. /// /// The serialized object. /// Set of property paths that belong to groups (will be skipped in normal iteration). /// Maps anchor property paths to their group definitions. /// Callback to draw a non-grouped property. /// Callback to draw groups anchored at a specific property. /// Callback to draw the script property (or null to skip). internal static void DrawPropertiesWithGroups( SerializedObject serializedObject, HashSet groupedPaths, Dictionary> anchorToGroups, Action drawProperty, Action> drawGroups, Action drawScriptProperty ) { if (serializedObject == null) { return; } SerializedProperty iterator = serializedObject.GetIterator(); bool enterChildren = true; while (iterator.NextVisible(enterChildren)) { enterChildren = false; string path = iterator.propertyPath; if (string.Equals(path, ScriptPropertyPath, StringComparison.Ordinal)) { drawScriptProperty?.Invoke(iterator); continue; } if ( anchorToGroups != null && anchorToGroups.TryGetValue(path, out List groups) ) { drawGroups?.Invoke(groups); continue; } if (groupedPaths != null && groupedPaths.Contains(path)) { continue; } drawProperty?.Invoke(iterator); } } } #endif }