diff --git a/Editor/HarmonyPatches.meta b/Editor/HarmonyPatches.meta
new file mode 100644
index 00000000..0ee1c19c
--- /dev/null
+++ b/Editor/HarmonyPatches.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: cf056e4454ad43ababbfff1dd06ec1d0
+timeCreated: 1708235917
\ No newline at end of file
diff --git a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs
new file mode 100644
index 00000000..6a601d53
--- /dev/null
+++ b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs
@@ -0,0 +1,59 @@
+#region
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Reflection;
+using HarmonyLib;
+using JetBrains.Annotations;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+
+#endregion
+
+namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
+{
+ ///
+ /// Try to prevent various internal objects from showing up in the Prefab Overrides window...
+ ///
+ internal class HideScaleAdjusterFromPrefabOverrideView
+ {
+ internal static Type t_PrefabOverrides;
+ internal static PropertyInfo p_AddedGameObjects, p_ObjectOverrides;
+
+ internal static void Patch(Harmony harmony)
+ {
+ var t_PrefabOverridesTreeView = AccessTools.TypeByName("UnityEditor.PrefabOverridesTreeView");
+ var m_GetPrefabOverrides = AccessTools.Method(t_PrefabOverridesTreeView, "GetPrefabOverrides");
+
+ var m_postfix = AccessTools.Method(typeof(HideScaleAdjusterFromPrefabOverrideView), "Postfix");
+
+ t_PrefabOverrides = AccessTools.TypeByName("UnityEditor.PrefabOverridesTreeView+PrefabOverrides");
+ p_AddedGameObjects = AccessTools.Property(t_PrefabOverrides, "addedGameObjects");
+ p_ObjectOverrides = AccessTools.Property(t_PrefabOverrides, "objectOverrides");
+
+ harmony.Patch(original: m_GetPrefabOverrides, postfix: new HarmonyMethod(m_postfix));
+ }
+
+ [UsedImplicitly]
+ private static void Postfix(GameObject prefabInstance, object __result)
+ {
+ var ignoredObjects = prefabInstance.GetComponentsInChildren()
+ .Select(sar => sar.gameObject)
+ .ToImmutableHashSet();
+ List added = p_AddedGameObjects.GetValue(__result) as List;
+
+ if (added == null) return;
+ added.RemoveAll(obj => ignoredObjects.Contains(obj.instanceGameObject));
+
+ List objectOverrides = p_ObjectOverrides.GetValue(__result) as List;
+ if (objectOverrides == null) return;
+ objectOverrides.RemoveAll(oo =>
+ {
+ var c = oo.instanceObject as Component;
+ return c != null && ignoredObjects.Contains(c.gameObject);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs.meta b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs.meta
new file mode 100644
index 00000000..79b92b42
--- /dev/null
+++ b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 0893522c012a46358e5ecf1df6628b2e
+timeCreated: 1708237029
\ No newline at end of file
diff --git a/Editor/HarmonyPatches/PatchLoader.cs b/Editor/HarmonyPatches/PatchLoader.cs
new file mode 100644
index 00000000..7d0a6f22
--- /dev/null
+++ b/Editor/HarmonyPatches/PatchLoader.cs
@@ -0,0 +1,21 @@
+#region
+
+using HarmonyLib;
+using UnityEditor;
+
+#endregion
+
+namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
+{
+ internal class PatchLoader
+ {
+ [InitializeOnLoadMethod]
+ static void ApplyPatches()
+ {
+ var harmony = new Harmony("nadena.dev.modular_avatar");
+
+ SnoopHeaderRendering.Patch(harmony);
+ HideScaleAdjusterFromPrefabOverrideView.Patch(harmony);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Editor/HarmonyPatches/PatchLoader.cs.meta b/Editor/HarmonyPatches/PatchLoader.cs.meta
new file mode 100644
index 00000000..d82b39f6
--- /dev/null
+++ b/Editor/HarmonyPatches/PatchLoader.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: e92aedf0fa324b8fb1b425a7fc2b1771
+timeCreated: 1708235934
\ No newline at end of file
diff --git a/Editor/HarmonyPatches/SnoopHeaderRendering.cs b/Editor/HarmonyPatches/SnoopHeaderRendering.cs
new file mode 100644
index 00000000..894cd10c
--- /dev/null
+++ b/Editor/HarmonyPatches/SnoopHeaderRendering.cs
@@ -0,0 +1,40 @@
+#region
+
+using HarmonyLib;
+using JetBrains.Annotations;
+using UnityEngine;
+
+#endregion
+
+namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
+{
+ ///
+ /// ScaleAdjusterRenderer toggles off the enabled state of the original mesh just before rendering,
+ /// in order to allow us to effectively replace it at rendering time. We restore this in OnPostRender,
+ /// but GUI rendering can happen before this; as such, snoop GUI events and re-enable the original
+ /// at that time.
+ ///
+ internal class SnoopHeaderRendering
+ {
+ internal static void Patch(Harmony harmony)
+ {
+ var t_orig = AccessTools.TypeByName("UnityEditor.UIElements.EditorElement");
+ var m_orig = AccessTools.Method(t_orig, "HeaderOnGUI");
+
+ var m_prefix = AccessTools.Method(typeof(SnoopHeaderRendering), "Prefix");
+
+ harmony.Patch(original: m_orig, prefix: new HarmonyMethod(m_prefix));
+
+ var t_GUIUtility = typeof(GUIUtility);
+ var m_ProcessEvent = AccessTools.Method(t_GUIUtility, "ProcessEvent");
+
+ harmony.Patch(original: m_ProcessEvent, prefix: new HarmonyMethod(m_prefix));
+ }
+
+ [UsedImplicitly]
+ private static void Prefix()
+ {
+ ScaleAdjusterRenderer.ClearAllOverrides();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Editor/HarmonyPatches/SnoopHeaderRendering.cs.meta b/Editor/HarmonyPatches/SnoopHeaderRendering.cs.meta
new file mode 100644
index 00000000..bf645cc0
--- /dev/null
+++ b/Editor/HarmonyPatches/SnoopHeaderRendering.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: cafb5b7e681644cbbeafbeb12d833f6e
+timeCreated: 1708235926
\ No newline at end of file
diff --git a/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef b/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef
new file mode 100644
index 00000000..e3e391fc
--- /dev/null
+++ b/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef
@@ -0,0 +1,28 @@
+{
+ "name": "nadena.dev.modular-avatar.harmony-patches",
+ "rootNamespace": "",
+ "references": [
+ "nadena.dev.modular-avatar.core",
+ "nadena.dev.modular-avatar.core.editor",
+ "VRC.SDKBase.Editor"
+ ],
+ "includePlatforms": [
+ "Editor"
+ ],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [
+ "VRCSDK_HAS_HARMONY"
+ ],
+ "versionDefines": [
+ {
+ "name": "com.vrchat.base",
+ "expression": "(3.4.999,)",
+ "define": "VRCSDK_HAS_HARMONY"
+ }
+ ],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef.meta b/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef.meta
new file mode 100644
index 00000000..58c24e87
--- /dev/null
+++ b/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 0f9cfa335019051479cc80ce30c95a68
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/PluginDefinition/PluginDefinition.cs b/Editor/PluginDefinition/PluginDefinition.cs
index c8e31e5d..8f99db5a 100644
--- a/Editor/PluginDefinition/PluginDefinition.cs
+++ b/Editor/PluginDefinition/PluginDefinition.cs
@@ -36,6 +36,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
{
seq.Run(ClearEditorOnlyTags.Instance);
seq.Run(MeshSettingsPluginPass.Instance);
+ seq.Run(ScaleAdjusterPass.Instance);
#if MA_VRCSDK3_AVATARS
seq.Run(RenameParametersPluginPass.Instance);
seq.Run(MergeBlendTreePass.Instance);
diff --git a/Editor/ScaleAdjusterPass.cs b/Editor/ScaleAdjusterPass.cs
new file mode 100644
index 00000000..76dd924d
--- /dev/null
+++ b/Editor/ScaleAdjusterPass.cs
@@ -0,0 +1,51 @@
+using System.Collections.Generic;
+using System.Linq;
+using nadena.dev.ndmf;
+using UnityEditor.EditorTools;
+using UnityEngine;
+
+namespace nadena.dev.modular_avatar.core.editor
+{
+ internal class ScaleAdjusterPass : Pass
+ {
+ protected override void Execute(ndmf.BuildContext context)
+ {
+ ScaleAdjusterRenderer.ClearAllOverrides();
+
+ Dictionary boneMappings = new Dictionary();
+ foreach (var component in context.AvatarRootObject.GetComponentsInChildren())
+ {
+ var proxyTransform = component.transform;
+ var parentAdjuster = component.transform.parent?.GetComponent();
+ if (parentAdjuster != null)
+ {
+ UnityEngine.Object.DestroyImmediate(component);
+
+ proxyTransform.localScale = parentAdjuster.Scale;
+ UnityEngine.Object.DestroyImmediate(parentAdjuster);
+
+ boneMappings.Add(proxyTransform.parent, proxyTransform);
+ }
+ }
+
+ foreach (var sar in context.AvatarRootObject.GetComponentsInChildren())
+ {
+ UnityEngine.Object.DestroyImmediate(sar.gameObject);
+ }
+
+ foreach (var smr in context.AvatarRootObject.GetComponentsInChildren())
+ {
+ var bones = smr.bones;
+ for (int i = 0; i < bones.Length; i++)
+ {
+ if (boneMappings.TryGetValue(bones[i], out var newBone))
+ {
+ bones[i] = newBone;
+ }
+ }
+ smr.bones = bones;
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/Editor/ScaleAdjusterPass.cs.meta b/Editor/ScaleAdjusterPass.cs.meta
new file mode 100644
index 00000000..efbc8ddd
--- /dev/null
+++ b/Editor/ScaleAdjusterPass.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 62b78456069047e0949dd1d8c4b25edc
+timeCreated: 1708253304
\ No newline at end of file
diff --git a/Editor/assembly-info.cs b/Editor/assembly-info.cs
index 0502fef3..467a811f 100644
--- a/Editor/assembly-info.cs
+++ b/Editor/assembly-info.cs
@@ -1,4 +1,9 @@
-using System.Runtime.CompilerServices;
+#region
+
+using System.Runtime.CompilerServices;
+
+#endregion
[assembly: InternalsVisibleTo("net.fushizen.xdress")]
-[assembly: InternalsVisibleTo("net.fushizen.xdress.editor")]
\ No newline at end of file
+[assembly: InternalsVisibleTo("net.fushizen.xdress.editor")]
+[assembly: InternalsVisibleTo("nadena.dev.modular-avatar.harmony-patches")]
\ No newline at end of file
diff --git a/Runtime/ModularAvatarScaleAdjuster.cs b/Runtime/ModularAvatarScaleAdjuster.cs
deleted file mode 100644
index be09a58b..00000000
--- a/Runtime/ModularAvatarScaleAdjuster.cs
+++ /dev/null
@@ -1,268 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-#if UNITY_EDITOR
-using UnityEditor;
-#endif
-using UnityEngine;
-
-namespace nadena.dev.modular_avatar.core
-{
- [Serializable]
- internal struct ScalePatch
- {
- public SkinnedMeshRenderer smr;
- public int boneIndex;
-
- public ScalePatch(SkinnedMeshRenderer smr, int boneIndex)
- {
- this.smr = smr;
- this.boneIndex = boneIndex;
- }
-
- public bool Equals(ScalePatch other)
- {
- return smr.Equals(other.smr) && boneIndex == other.boneIndex;
- }
-
- public override bool Equals(object obj)
- {
- return obj is ScalePatch other && Equals(other);
- }
-
- public override int GetHashCode()
- {
- unchecked
- {
- return (smr.GetHashCode() * 397) ^ boneIndex;
- }
- }
- }
-
- [ExecuteInEditMode]
- [DisallowMultipleComponent]
- [AddComponentMenu("Modular Avatar/MA Scale Adjuster")]
- [HelpURL("https://modular-avatar.nadena.dev/docs/reference/scale-adjuster?lang=auto")]
- public sealed class ModularAvatarScaleAdjuster : AvatarTagComponent
- {
- [SerializeField] private Vector3 m_Scale = Vector3.one;
-
- public Vector3 Scale
- {
- get => m_Scale;
- set
- {
- m_Scale = value;
- Update();
- }
- }
-
- [SerializeField] internal Transform scaleProxy;
-
- [SerializeField] private List patches = new List();
-
- private bool initialized = false;
-
-#if UNITY_EDITOR
- private void Update()
- {
- if (this == null) return;
-
- PatchRenderers();
-
- scaleProxy.localScale = m_Scale;
- }
-
- void OnValidate()
- {
- initialized = false;
- EditorApplication.delayCall += Update;
- }
-
- private void PatchRenderers()
- {
- if (initialized || this == null) return;
-
- if (PrefabUtility.IsPartOfPrefabInstance(this))
- {
- // Ensure we're using the same ScaleProxy as the corresponding prefab asset.
- var prefab = PrefabUtility.GetCorrespondingObjectFromSource(this);
- if (this.scaleProxy == null || prefab.scaleProxy == null || prefab.scaleProxy !=
- PrefabUtility.GetCorrespondingObjectFromSource(this.scaleProxy))
- {
- if (prefab.scaleProxy == null && scaleProxy != null)
- {
- // Push our ScaleProxy down into the prefab (this happens after applying the ScaleAdjuster
- // component to a prefab)
- var assetPath = AssetDatabase.GetAssetPath(prefab);
- PrefabUtility.ApplyAddedGameObject(scaleProxy.gameObject, assetPath,
- InteractionMode.AutomatedAction);
- prefab.scaleProxy = PrefabUtility.GetCorrespondingObjectFromSource(this.scaleProxy);
- }
- else
- {
- // Clear any duplicate scaleProxy we have
-
- if (scaleProxy != null) DestroyImmediate(scaleProxy.gameObject);
- }
-
- var so = new SerializedObject(this);
- var sp = so.FindProperty(nameof(scaleProxy));
- PrefabUtility.RevertPropertyOverride(sp, InteractionMode.AutomatedAction);
- so.ApplyModifiedPropertiesWithoutUndo();
-
- // Find the corresponding child
- foreach (Transform t in transform)
- {
- if (PrefabUtility.GetCorrespondingObjectFromSource(t) == prefab.scaleProxy)
- {
- scaleProxy = t;
- break;
- }
- }
- }
- }
-
- if (scaleProxy == null && !PrefabUtility.IsPartOfPrefabAsset(this))
- {
- scaleProxy = new GameObject(gameObject.name + " (Scale Proxy)").transform;
- scaleProxy.SetParent(transform, false);
- scaleProxy.localPosition = Vector3.zero;
- scaleProxy.localRotation = Quaternion.identity;
- scaleProxy.localScale = m_Scale;
- scaleProxy.gameObject.AddComponent();
- PrefabUtility.RecordPrefabInstancePropertyModifications(this);
- }
-
- if (scaleProxy != null)
- {
- scaleProxy.hideFlags = HideFlags.HideInHierarchy;
-
- RewriteBoneReferences(transform, scaleProxy);
- }
-
- initialized = true;
- }
-
- private void RewriteBoneReferences(Transform oldBone, Transform newBone, Transform selfTransform = null)
- {
- if (selfTransform == null) selfTransform = transform;
-
- var prefabNewBone = PrefabUtility.GetCorrespondingObjectFromSource(newBone);
-
- var oldPatches = new HashSet(this.patches);
- var newPatches = new HashSet();
- var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selfTransform);
-
- if (avatarRoot != null)
- {
- foreach (var smr in avatarRoot.GetComponentsInChildren(true))
- {
- var serializedObject = new SerializedObject(smr);
- var bonesArray = serializedObject.FindProperty("m_Bones");
- int boneCount = bonesArray.arraySize;
-
- var parentSmr = PrefabUtility.GetCorrespondingObjectFromSource(smr);
- var parentBones = parentSmr != null ? parentSmr.bones : null;
- var propMods = PrefabUtility.GetPropertyModifications(smr);
-
- bool changed = false;
-
- for (int i = 0; i < boneCount; i++)
- {
- var boneProp = bonesArray.GetArrayElementAtIndex(i);
- var bone = boneProp.objectReferenceValue as Transform;
- if (bone == oldBone || bone == newBone ||
- (bone == null && oldPatches.Contains(new ScalePatch(smr, i))))
- {
- if (parentBones != null && parentBones[i] == prefabNewBone)
- {
- // Remove any prefab overrides for this bone entry
- changed = boneProp.objectReferenceValue != newBone;
- boneProp.objectReferenceValue = newBone;
- serializedObject.ApplyModifiedPropertiesWithoutUndo();
- PrefabUtility.RevertPropertyOverride(boneProp, InteractionMode.AutomatedAction);
- }
- else
- {
- boneProp.objectReferenceValue = newBone;
- changed = true;
- }
-
- newPatches.Add(new ScalePatch(smr, i));
- }
- }
-
- if (changed)
- {
- serializedObject.ApplyModifiedPropertiesWithoutUndo();
-
- ConfigurePrefab();
- }
- }
-
- if (this != null && newPatches != oldPatches)
- {
- this.patches = newPatches.ToList();
- PrefabUtility.RecordPrefabInstancePropertyModifications(this);
- }
- }
- }
-
- private void ConfigurePrefab()
- {
- if (this == null || !PrefabUtility.IsPartOfPrefabInstance(this)) return;
- var source = PrefabUtility.GetCorrespondingObjectFromSource(this);
- var path = AssetDatabase.GetAssetPath(source);
- var root = PrefabUtility.LoadPrefabContents(path);
-
- foreach (var obj in root.GetComponentsInChildren())
- {
- obj.PatchRenderers();
- }
-
- PrefabUtility.SaveAsPrefabAsset(root, path);
- PrefabUtility.UnloadPrefabContents(root);
-
- initialized = false;
- }
-
- protected override void OnDestroy()
- {
- base.OnDestroy();
-
- UnpatchRenderers();
- }
-
- private void UnpatchRenderers()
- {
- var scaleProxy2 = this.scaleProxy;
- var transform2 = this.transform;
-
- EditorApplication.delayCall += () =>
- {
- if (scaleProxy2 == null) return;
-
- if (transform2 != null)
- {
- RewriteBoneReferences(scaleProxy2, transform2, transform2);
- }
-
- try
- {
- DestroyImmediate(scaleProxy2.gameObject);
- }
- catch (InvalidOperationException e)
- {
- // not supported in Unity 2019...
- }
- };
- }
-#else
- private void Update()
- {
- // placeholder to make builds work
- }
-#endif
- }
-}
\ No newline at end of file
diff --git a/Runtime/ScaleAdjuster.meta b/Runtime/ScaleAdjuster.meta
new file mode 100644
index 00000000..15b1d22e
--- /dev/null
+++ b/Runtime/ScaleAdjuster.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 96f10f3195df42bba58b9efb61948b9c
+timeCreated: 1708232523
\ No newline at end of file
diff --git a/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs
new file mode 100644
index 00000000..9171ad21
--- /dev/null
+++ b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs
@@ -0,0 +1,128 @@
+#region
+
+using UnityEngine;
+using UnityEngine.Serialization;
+#if UNITY_EDITOR
+using UnityEditor;
+#endif
+
+#endregion
+
+namespace nadena.dev.modular_avatar.core
+{
+ [ExecuteInEditMode]
+ [DisallowMultipleComponent]
+ [AddComponentMenu("Modular Avatar/MA Scale Adjuster")]
+ [HelpURL("https://modular-avatar.nadena.dev/docs/reference/scale-adjuster?lang=auto")]
+ public sealed class ModularAvatarScaleAdjuster : AvatarTagComponent
+ {
+ private const string ADJUSTER_OBJECT = "MA Scale Adjuster Proxy Renderer";
+ [SerializeField] private Vector3 m_Scale = Vector3.one;
+
+ public Vector3 Scale
+ {
+ get => m_Scale;
+ set
+ {
+ m_Scale = value;
+ Update();
+ }
+ }
+
+ [SerializeField] [FormerlySerializedAs("scaleProxy")]
+ internal Transform legacyScaleProxy;
+
+ internal Transform scaleProxy;
+
+ private bool initialized = false;
+
+#if UNITY_EDITOR
+ void OnValidate()
+ {
+ base.OnValidate();
+ initialized = false;
+ }
+
+ private void Update()
+ {
+ if (scaleProxy == null || initialized == false)
+ {
+ InitializeProxy();
+ }
+
+ if (legacyScaleProxy != null && !PrefabUtility.IsPartOfPrefabAsset(legacyScaleProxy))
+ {
+ DestroyImmediate(legacyScaleProxy.gameObject);
+ legacyScaleProxy = null;
+ }
+
+ scaleProxy.localScale = m_Scale;
+ }
+
+ private void InitializeProxy()
+ {
+ if (scaleProxy == null)
+ {
+ scaleProxy = new GameObject(gameObject.name + " (Scale Proxy)").transform;
+ scaleProxy.SetParent(transform, false);
+ scaleProxy.localPosition = Vector3.zero;
+ scaleProxy.localRotation = Quaternion.identity;
+ scaleProxy.localScale = m_Scale;
+ scaleProxy.gameObject.AddComponent();
+ PrefabUtility.RecordPrefabInstancePropertyModifications(this);
+ }
+
+ ConfigureRenderers();
+
+ initialized = true;
+ }
+
+ private void OnDestroy()
+ {
+ if (scaleProxy != null)
+ {
+ DestroyImmediate(scaleProxy.gameObject);
+ }
+
+ ScaleAdjusterRenderer.InvalidateAll();
+ base.OnDestroy();
+ }
+
+
+ private void ConfigureRenderers()
+ {
+ var avatar = RuntimeUtil.FindAvatarInParents(transform);
+ if (avatar == null) return;
+ foreach (var smr in avatar.GetComponentsInChildren(true))
+ {
+ if (smr.GetComponent() != null) continue;
+
+ var child = smr.transform.Find(ADJUSTER_OBJECT)?.GetComponent();
+ if (child == null)
+ {
+ var childObj = new GameObject(ADJUSTER_OBJECT);
+
+ var childSmr = childObj.AddComponent();
+ EditorUtility.CopySerialized(smr, childSmr);
+
+ child = childObj.AddComponent();
+ child.transform.SetParent(smr.transform, false);
+ child.transform.localPosition = Vector3.zero;
+ child.transform.localRotation = Quaternion.identity;
+ child.transform.localScale = Vector3.one;
+ }
+
+ child.BoneMappings[transform] = scaleProxy;
+ child.ClearBoneCache();
+ }
+ }
+#endif
+
+#if !UNITY_EDITOR
+ private void Update()
+ {
+ // placeholder to make builds work
+ }
+#endif
+ }
+}
\ No newline at end of file
diff --git a/Runtime/ModularAvatarScaleAdjuster.cs.meta b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs.meta
similarity index 100%
rename from Runtime/ModularAvatarScaleAdjuster.cs.meta
rename to Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs.meta
diff --git a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs
new file mode 100644
index 00000000..0716b5de
--- /dev/null
+++ b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs
@@ -0,0 +1,178 @@
+#region
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+using VRC.SDKBase;
+
+#endregion
+
+namespace nadena.dev.modular_avatar.core
+{
+ [ExecuteInEditMode]
+ //[AddComponentMenu("")]
+ [RequireComponent(typeof(SkinnedMeshRenderer))]
+ internal class ScaleAdjusterRenderer : MonoBehaviour, IEditorOnly
+ {
+ private static event Action OnPreInspector;
+ private static int RecreateHierarchyIndexCount = 0;
+
+ #if UNITY_EDITOR
+ [UnityEditor.InitializeOnLoadMethod]
+ static void Setup()
+ {
+ UnityEditor.EditorApplication.hierarchyChanged += InvalidateAll;
+ }
+ #endif
+
+ internal static void InvalidateAll()
+ {
+ RecreateHierarchyIndexCount++;
+ }
+
+ private SkinnedMeshRenderer myRenderer;
+ private SkinnedMeshRenderer parentRenderer;
+
+ private bool wasActive = false;
+ private bool redoBoneMappings = true;
+ private int lastRecreateHierarchyIndex = -1;
+
+ internal Dictionary BoneMappings = new Dictionary();
+
+ #if UNITY_EDITOR
+ private void OnValidate()
+ {
+ if (UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this)) return;
+ redoBoneMappings = true;
+
+ UnityEditor.EditorApplication.delayCall += () =>
+ {
+ if (this == null) return;
+
+#if MODULAR_AVATAR_DEBUG_HIDDEN
+ gameObject.hideFlags = HideFlags.None;
+#else
+ gameObject.hideFlags = HideFlags.HideInHierarchy | HideFlags.DontSaveInBuild;
+#endif
+ if (BoneMappings == null)
+ {
+ BoneMappings = new Dictionary();
+ }
+ };
+ }
+ #endif
+
+ private Transform MapBone(Transform bone)
+ {
+ if (bone == null) return null;
+ if (BoneMappings.TryGetValue(bone, out var newBone) && newBone != null) return newBone;
+ return bone;
+ }
+
+ private void OnDestroy()
+ {
+ ClearOverrides();
+ }
+
+ private void Update()
+ {
+ if (myRenderer == null)
+ {
+ myRenderer = GetComponent();
+ }
+
+ if (parentRenderer == null)
+ {
+ parentRenderer = transform.parent.GetComponent();
+ }
+
+ myRenderer.sharedMaterials = parentRenderer.sharedMaterials;
+ myRenderer.sharedMesh = parentRenderer.sharedMesh;
+ myRenderer.localBounds = parentRenderer.localBounds;
+ if (redoBoneMappings || lastRecreateHierarchyIndex != RecreateHierarchyIndexCount)
+ {
+ var deadBones = BoneMappings.Keys.Where(k => BoneMappings[k] == null)
+ .ToList();
+ deadBones.ForEach(k => { BoneMappings.Remove(k); });
+
+ if (BoneMappings.Count == 0)
+ {
+ DestroyImmediate(gameObject);
+ return;
+ }
+
+ myRenderer.rootBone = MapBone(parentRenderer.rootBone);
+ myRenderer.bones = parentRenderer.bones.Select(MapBone).ToArray();
+ redoBoneMappings = false;
+ lastRecreateHierarchyIndex = RecreateHierarchyIndexCount;
+ }
+
+ myRenderer.quality = parentRenderer.quality;
+ myRenderer.shadowCastingMode = parentRenderer.shadowCastingMode;
+ myRenderer.receiveShadows = parentRenderer.receiveShadows;
+ myRenderer.lightProbeUsage = parentRenderer.lightProbeUsage;
+ myRenderer.reflectionProbeUsage = parentRenderer.reflectionProbeUsage;
+ myRenderer.probeAnchor = parentRenderer.probeAnchor;
+ myRenderer.motionVectorGenerationMode = parentRenderer.motionVectorGenerationMode;
+ myRenderer.allowOcclusionWhenDynamic = parentRenderer.allowOcclusionWhenDynamic;
+
+ var blendShapeCount = myRenderer.sharedMesh.blendShapeCount;
+
+ for (int i = 0; i < blendShapeCount; i++)
+ {
+ myRenderer.SetBlendShapeWeight(i, parentRenderer.GetBlendShapeWeight(i));
+ }
+
+ ClearOverrides();
+
+ myRenderer.enabled = parentRenderer.enabled;
+ }
+
+ public void OnWillRenderObject()
+ {
+ if (myRenderer == null || parentRenderer == null)
+ {
+ return;
+ }
+
+ ClearOverrides();
+
+ if (!parentRenderer.enabled || !parentRenderer.gameObject.activeInHierarchy)
+ {
+ return;
+ }
+
+ parentRenderer.enabled = false;
+ wasActive = true;
+ OnPreInspector += ClearOverrides;
+ }
+
+ private void OnPostRender()
+ {
+ ClearOverrides();
+ }
+
+ private void ClearOverrides()
+ {
+ if (this == null) return;
+
+ if (wasActive && parentRenderer != null)
+ {
+ parentRenderer.enabled = true;
+ wasActive = false;
+ }
+ }
+
+ public void ClearBoneCache()
+ {
+ redoBoneMappings = true;
+ }
+
+ internal static void ClearAllOverrides()
+ {
+ OnPreInspector?.Invoke();
+ OnPreInspector = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs.meta b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs.meta
new file mode 100644
index 00000000..870af8e4
--- /dev/null
+++ b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: c8bc16baa6c345eea5edf47232ee4069
+timeCreated: 1708232586
\ No newline at end of file
diff --git a/Runtime/ScaleProxy.cs b/Runtime/ScaleAdjuster/ScaleProxy.cs
similarity index 75%
rename from Runtime/ScaleProxy.cs
rename to Runtime/ScaleAdjuster/ScaleProxy.cs
index ca972524..40d75685 100644
--- a/Runtime/ScaleProxy.cs
+++ b/Runtime/ScaleAdjuster/ScaleProxy.cs
@@ -1,7 +1,12 @@
-#if UNITY_EDITOR
+
+#region
+
+using UnityEngine;
+#if UNITY_EDITOR
using UnityEditor;
#endif
-using UnityEngine;
+
+#endregion
namespace nadena.dev.modular_avatar.core
{
@@ -11,9 +16,16 @@ namespace nadena.dev.modular_avatar.core
#if UNITY_EDITOR
void OnValidate()
{
+ base.OnDestroy();
EditorApplication.delayCall += DeferredValidate;
}
+ void OnDestroy()
+ {
+ ScaleAdjusterRenderer.InvalidateAll();
+ base.OnDestroy();
+ }
+
private void DeferredValidate()
{
if (this == null) return;
@@ -23,8 +35,16 @@ namespace nadena.dev.modular_avatar.core
gameObject.AddComponent();
}
+ var avatar = ndmf.runtime.RuntimeUtil.FindAvatarInParents(transform);
+ ClearOverrides(avatar);
+
gameObject.hideFlags = HideFlags.HideInHierarchy;
+#if MODULAR_AVATAR_DEBUG_HIDDEN
+ gameObject.hideFlags = HideFlags.None;
+#endif
+ hideFlags = HideFlags.None;
+
var parentObject = transform.parent;
var parentScaleAdjuster =
parentObject != null ? parentObject.GetComponent() : null;
@@ -60,8 +80,19 @@ namespace nadena.dev.modular_avatar.core
while (root.parent != null) root = root.parent;
}
+ ClearOverrides(root);
+
+ DestroyImmediate(gameObject);
+ }
+
+ private void ClearOverrides(Transform root)
+ {
+ // This clears bone overrides that date back to the 1.9.0-rc.2 implementation, to ease rc.2 -> rc.3
+ // migrations. It'll be removed in 1.10.
foreach (var smr in root.GetComponentsInChildren(true))
{
+ if (smr.GetComponent()) continue;
+
var bones = smr.bones;
bool changed = false;
@@ -79,8 +110,6 @@ namespace nadena.dev.modular_avatar.core
smr.bones = bones;
}
}
-
- DestroyImmediate(gameObject);
}
#endif
}
diff --git a/Runtime/ScaleProxy.cs.meta b/Runtime/ScaleAdjuster/ScaleProxy.cs.meta
similarity index 100%
rename from Runtime/ScaleProxy.cs.meta
rename to Runtime/ScaleAdjuster/ScaleProxy.cs.meta
diff --git a/Runtime/TestBehavior.cs.meta b/Runtime/TestBehavior.cs.meta
new file mode 100644
index 00000000..bf95728b
--- /dev/null
+++ b/Runtime/TestBehavior.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: f2c964d1229a4ce698db85bec9043608
+timeCreated: 1708232304
\ No newline at end of file
diff --git a/Runtime/assembly-info.cs b/Runtime/assembly-info.cs
index 5e7b5f6f..0386f743 100644
--- a/Runtime/assembly-info.cs
+++ b/Runtime/assembly-info.cs
@@ -1,6 +1,11 @@
+#region
+
using System.Runtime.CompilerServices;
+#endregion
+
[assembly: InternalsVisibleTo("nadena.dev.modular-avatar.core.editor")]
[assembly: InternalsVisibleTo("net.fushizen.xdress")]
[assembly: InternalsVisibleTo("net.fushizen.xdress.editor")]
-[assembly: InternalsVisibleTo("Tests")]
\ No newline at end of file
+[assembly: InternalsVisibleTo("Tests")]
+[assembly: InternalsVisibleTo("nadena.dev.modular-avatar.harmony-patches")]
\ No newline at end of file
diff --git a/UnitTests~/ComponentSettingsTest.cs b/UnitTests~/ComponentSettingsTest.cs
index 38c05392..90aa6262 100644
--- a/UnitTests~/ComponentSettingsTest.cs
+++ b/UnitTests~/ComponentSettingsTest.cs
@@ -40,6 +40,7 @@ namespace modular_avatar_tests
if (type == typeof(AvatarActivator)) return;
if (type == typeof(TestComponent)) return;
if (type == typeof(ScaleProxy)) return;
+ if (type == typeof(ScaleAdjusterRenderer)) return;
// get icon
var component = (MonoBehaviour) _gameObject.AddComponent(type);
@@ -65,6 +66,7 @@ namespace modular_avatar_tests
if (type == typeof(AvatarActivator)) return;
if (type == typeof(TestComponent)) return;
if (type == typeof(ScaleProxy)) return;
+ if (type == typeof(ScaleAdjusterRenderer)) return;
// get icon
var helpUrl = type.GetCustomAttribute();