From 1a8b02a13c9a1a326385f81a656dc8f49d57fff4 Mon Sep 17 00:00:00 2001
From: bd_ <bd_@nadena.dev>
Date: Mon, 10 Mar 2025 19:11:45 -0700
Subject: [PATCH] feat: World Scale Object

---
 CHANGELOG-PRERELEASE-jp.md                    |  1 +
 CHANGELOG-PRERELEASE.md                       |  1 +
 CHANGELOG-jp.md                               |  1 +
 CHANGELOG.md                                  |  1 +
 Editor/Inspector/WorldScaleObjectEditor.cs    | 12 ++++
 .../Inspector/WorldScaleObjectEditor.cs.meta  |  3 +
 Editor/PluginDefinition/PluginDefinition.cs   |  1 +
 Editor/WorldScaleObjectPass.cs                | 46 +++++++++++++++
 Editor/WorldScaleObjectPass.cs.meta           |  3 +
 ...dena.dev.modular-avatar.core.editor.asmdef |  3 +-
 ...ena.dev.modular-avatar.core.editor.asmdef~ | 59 -------------------
 Runtime/ModularAvatarWorldScaleObject.cs      | 12 ++++
 Runtime/ModularAvatarWorldScaleObject.cs.meta | 11 ++++
 UnitTests~/WorldScaleObject.meta              |  3 +
 .../WorldScaleObject/WorldScaleObjectTest.cs  | 58 ++++++++++++++++++
 .../WorldScaleObjectTest.cs.meta              |  3 +
 docs~/docs/reference/world-scale-object.md    | 17 ++++++
 .../current/reference/world-scale.object.md   | 14 +++++
 18 files changed, 189 insertions(+), 60 deletions(-)
 create mode 100644 Editor/Inspector/WorldScaleObjectEditor.cs
 create mode 100644 Editor/Inspector/WorldScaleObjectEditor.cs.meta
 create mode 100644 Editor/WorldScaleObjectPass.cs
 create mode 100644 Editor/WorldScaleObjectPass.cs.meta
 delete mode 100644 Editor/nadena.dev.modular-avatar.core.editor.asmdef~
 create mode 100644 Runtime/ModularAvatarWorldScaleObject.cs
 create mode 100644 Runtime/ModularAvatarWorldScaleObject.cs.meta
 create mode 100644 UnitTests~/WorldScaleObject.meta
 create mode 100644 UnitTests~/WorldScaleObject/WorldScaleObjectTest.cs
 create mode 100644 UnitTests~/WorldScaleObject/WorldScaleObjectTest.cs.meta
 create mode 100644 docs~/docs/reference/world-scale-object.md
 create mode 100644 docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/world-scale.object.md

diff --git a/CHANGELOG-PRERELEASE-jp.md b/CHANGELOG-PRERELEASE-jp.md
index 4150c88e..15e1ea14 100644
--- a/CHANGELOG-PRERELEASE-jp.md
+++ b/CHANGELOG-PRERELEASE-jp.md
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## [Unreleased]
 
 ### Added
+- [World Scale Object](https://m-a.nadena.dev/dev/ja/docs/reference/world-scale-object)を追加
 
 ### Fixed
 
diff --git a/CHANGELOG-PRERELEASE.md b/CHANGELOG-PRERELEASE.md
index a01e411c..b50b3230 100644
--- a/CHANGELOG-PRERELEASE.md
+++ b/CHANGELOG-PRERELEASE.md
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## [Unreleased]
 
 ### Added
+- Added [World Scale Object](https://m-a.nadena.dev/dev/docs/reference/world-scale-object)
 
 ### Fixed
 
diff --git a/CHANGELOG-jp.md b/CHANGELOG-jp.md
index cf49b41e..c27f5c80 100644
--- a/CHANGELOG-jp.md
+++ b/CHANGELOG-jp.md
@@ -10,6 +10,7 @@ Modular Avatarの主な変更点をこのファイルで記録しています。
 
 ### Added
 - CHANGELOGファイルを追加
+- [World Scale Object](https://m-a.nadena.dev/ja/docs/reference/world-scale-object)を追加
 
 ### Fixed
 - [#1460] パラメーターアセットをMA Parametersにインポートするとき、ローカルのみのパラメーターが間違ってアニメーターのみ扱いになる問題を修正
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66fd48e0..4ebea092 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 - Added CHANGELOG files
+- Added [World Scale Object](https://m-a.nadena.dev/docs/reference/world-scale-object)
 
 ### Fixed
 - [#1460] When importing parameter assets in MA Parameters, "local only" parameters were incorrectly treated as
diff --git a/Editor/Inspector/WorldScaleObjectEditor.cs b/Editor/Inspector/WorldScaleObjectEditor.cs
new file mode 100644
index 00000000..c878e376
--- /dev/null
+++ b/Editor/Inspector/WorldScaleObjectEditor.cs
@@ -0,0 +1,12 @@
+using UnityEditor;
+
+namespace nadena.dev.modular_avatar.core.editor
+{
+    [CustomEditor(typeof(ModularAvatarWorldScaleObject))]
+    internal class WorldScaleObjectEditor : MAEditorBase
+    {
+        protected override void OnInnerInspectorGUI()
+        {
+        }
+    }
+}
\ No newline at end of file
diff --git a/Editor/Inspector/WorldScaleObjectEditor.cs.meta b/Editor/Inspector/WorldScaleObjectEditor.cs.meta
new file mode 100644
index 00000000..71c01d8f
--- /dev/null
+++ b/Editor/Inspector/WorldScaleObjectEditor.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: e9b8b83586074bd7a6441b4cd7539dc9
+timeCreated: 1741658287
\ No newline at end of file
diff --git a/Editor/PluginDefinition/PluginDefinition.cs b/Editor/PluginDefinition/PluginDefinition.cs
index 330f5df2..b951fe19 100644
--- a/Editor/PluginDefinition/PluginDefinition.cs
+++ b/Editor/PluginDefinition/PluginDefinition.cs
@@ -84,6 +84,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
                     seq.Run("World Fixed Object",
                         ctx => new WorldFixedObjectProcessor().Process(ctx)
                     );
+                    seq.Run(WorldScaleObjectPass.Instance);
                     
                     seq.Run(ReplaceObjectPluginPass.Instance);
                     
diff --git a/Editor/WorldScaleObjectPass.cs b/Editor/WorldScaleObjectPass.cs
new file mode 100644
index 00000000..2632b73f
--- /dev/null
+++ b/Editor/WorldScaleObjectPass.cs
@@ -0,0 +1,46 @@
+using nadena.dev.modular_avatar.editor.ErrorReporting;
+using nadena.dev.ndmf;
+using UnityEditor;
+using UnityEngine;
+#if VRC_SDK_VRCSDK3
+using VRC.Dynamics;
+using VRC.SDK3.Dynamics.Constraint.Components;
+
+#else
+using UnityEngine.Animations;
+#endif
+
+namespace nadena.dev.modular_avatar.core.editor
+{
+    internal class WorldScaleObjectPass : Pass<WorldScaleObjectPass>
+    {
+        protected override void Execute(ndmf.BuildContext context)
+        {
+            var fixedPrefab =
+                AssetDatabase.LoadAssetAtPath<GameObject>(
+                    "Packages/nadena.dev.modular-avatar/Assets/FixedPrefab.prefab"
+                );
+            var targets = context.AvatarRootTransform.GetComponentsInChildren<ModularAvatarWorldScaleObject>(true);
+
+            foreach (var target in targets)
+            {
+                BuildReport.ReportingObject(target, () =>
+                {
+#if MA_VRCSDK3_AVATARS
+                    var c = target.gameObject.AddComponent<VRCScaleConstraint>();
+                    c.Sources.Add(new VRCConstraintSource(fixedPrefab.transform, 1));
+                    c.Locked = true;
+                    c.IsActive = true;
+#else
+                    var c = target.gameObject.AddComponent<ScaleConstraint>();
+                    c.AddSource(new ConstraintSource() {sourceTransform = fixedPrefab.transform, weight = 1});
+                    c.locked = true;
+                    c.constraintActive = true;
+#endif
+
+                    Object.DestroyImmediate(target);
+                });
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Editor/WorldScaleObjectPass.cs.meta b/Editor/WorldScaleObjectPass.cs.meta
new file mode 100644
index 00000000..b54b6c3d
--- /dev/null
+++ b/Editor/WorldScaleObjectPass.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 19a3b07a9eeb413887792469f344c34d
+timeCreated: 1741657804
\ No newline at end of file
diff --git a/Editor/nadena.dev.modular-avatar.core.editor.asmdef b/Editor/nadena.dev.modular-avatar.core.editor.asmdef
index 6372b236..6d0af71a 100644
--- a/Editor/nadena.dev.modular-avatar.core.editor.asmdef
+++ b/Editor/nadena.dev.modular-avatar.core.editor.asmdef
@@ -29,7 +29,8 @@
         "VRC.SDK3.Dynamics.Contact.Editor.dll",
         "VRC.SDK3.Dynamics.PhysBone.dll",
         "VRC.SDK3.Dynamics.PhysBone.Editor.dll",
-        "VRCCore-Editor.dll"
+      "VRCCore-Editor.dll",
+      "VRC.SDK3.Dynamics.Constraint.dll"
     ],
     "autoReferenced": false,
     "defineConstraints": [],
diff --git a/Editor/nadena.dev.modular-avatar.core.editor.asmdef~ b/Editor/nadena.dev.modular-avatar.core.editor.asmdef~
deleted file mode 100644
index 6372b236..00000000
--- a/Editor/nadena.dev.modular-avatar.core.editor.asmdef~
+++ /dev/null
@@ -1,59 +0,0 @@
-{
-    "name": "nadena.dev.modular-avatar.core.editor",
-    "rootNamespace": "",
-    "references": [
-        "nadena.dev.modular-avatar.core",
-        "VRC.SDK3A",
-        "VRC.SDKBase",
-        "nadena.dev.ndmf",
-        "nadena.dev.ndmf.vrchat",
-        "nadena.dev.ndmf.runtime",
-        "VRC.SDK3A.Editor",
-        "Unity.Burst"
-    ],
-    "includePlatforms": [
-        "Editor"
-    ],
-    "excludePlatforms": [],
-    "allowUnsafeCode": false,
-    "overrideReferences": true,
-    "precompiledReferences": [
-        "Newtonsoft.Json.dll",
-        "System.Collections.Immutable.dll",
-        "VRCSDKBase.dll",
-        "VRCSDKBase-Editor.dll",
-        "VRCSDK3A.dll",
-        "VRCSDK3A-Editor.dll",
-        "VRC.Dynamics.dll",
-        "VRC.SDK3.Dynamics.Contact.dll",
-        "VRC.SDK3.Dynamics.Contact.Editor.dll",
-        "VRC.SDK3.Dynamics.PhysBone.dll",
-        "VRC.SDK3.Dynamics.PhysBone.Editor.dll",
-        "VRCCore-Editor.dll"
-    ],
-    "autoReferenced": false,
-    "defineConstraints": [],
-    "versionDefines": [
-        {
-            "name": "com.anatawa12.avatar-optimizer",
-            "expression": "(,1.5.0-rc.8)",
-            "define": "LEGACY_AVATAR_OPTIMIZER"
-        },
-        {
-            "name": "com.vrchat.avatars",
-            "expression": "",
-            "define": "MA_VRCSDK3_AVATARS"
-        },
-        {
-            "name": "com.vrchat.avatars",
-            "expression": "3.5.2",
-            "define": "MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER"
-        },
-        {
-            "name": "com.vrchat.avatars",
-            "expression": "3.7.0-beta.2",
-            "define": "MA_VRCSDK3_AVATARS_3_7_0_OR_NEWER"
-        }
-    ],
-    "noEngineReferences": false
-}
\ No newline at end of file
diff --git a/Runtime/ModularAvatarWorldScaleObject.cs b/Runtime/ModularAvatarWorldScaleObject.cs
new file mode 100644
index 00000000..ace07e8e
--- /dev/null
+++ b/Runtime/ModularAvatarWorldScaleObject.cs
@@ -0,0 +1,12 @@
+using UnityEngine;
+
+namespace nadena.dev.modular_avatar.core
+{
+    [AddComponentMenu("Modular Avatar/MA World Scale Object")]
+    [DisallowMultipleComponent]
+    [HelpURL("https://modular-avatar.nadena.dev/docs/reference/world-scale-object?lang=auto")]
+    public class ModularAvatarWorldScaleObject : AvatarTagComponent
+    {
+        // no configuration
+    }
+}
\ No newline at end of file
diff --git a/Runtime/ModularAvatarWorldScaleObject.cs.meta b/Runtime/ModularAvatarWorldScaleObject.cs.meta
new file mode 100644
index 00000000..ea8152be
--- /dev/null
+++ b/Runtime/ModularAvatarWorldScaleObject.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e113c01563a14226b5e863befe6fe769
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: a8edd5bd1a0a64a40aa99cc09fb5f198, type: 3}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/UnitTests~/WorldScaleObject.meta b/UnitTests~/WorldScaleObject.meta
new file mode 100644
index 00000000..89d3214a
--- /dev/null
+++ b/UnitTests~/WorldScaleObject.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 2ce1741d17804d6c94f296ef930afba8
+timeCreated: 1741658553
\ No newline at end of file
diff --git a/UnitTests~/WorldScaleObject/WorldScaleObjectTest.cs b/UnitTests~/WorldScaleObject/WorldScaleObjectTest.cs
new file mode 100644
index 00000000..8ef96a5a
--- /dev/null
+++ b/UnitTests~/WorldScaleObject/WorldScaleObjectTest.cs
@@ -0,0 +1,58 @@
+using modular_avatar_tests;
+using nadena.dev.modular_avatar.core;
+using nadena.dev.modular_avatar.core.editor;
+using NUnit.Framework;
+using UnityEngine;
+using UnityEngine.Animations;
+
+#if MA_VRCSDK3_AVATARS
+using VRC.SDK3.Dynamics.Constraint.Components;
+#endif
+
+namespace UnitTests.WorldScaleObject
+{
+    public class WorldScaleObjectTest : TestBase
+    {
+        [Test]
+        public void TestWSO()
+        {
+            var root = CreateRoot("root");
+            var child = CreateChild(root, "child");
+            var wso = child.AddComponent<ModularAvatarWorldScaleObject>();
+            
+            AvatarProcessor.ProcessAvatar(root);
+            
+            Assert.IsTrue(wso == null);
+            
+            #if MA_VRCSDK3_AVATARS
+            AssertVRCScaleConstraintPresent(child);
+            #else
+            AssertScaleConstraintPresent(child);
+            #endif
+        }
+
+        #if MA_VRCSDK3_AVATARS
+        private void AssertVRCScaleConstraintPresent(GameObject child)
+        {
+            var scaleConstraint = child.GetComponent<VRCScaleConstraint>();
+            Assert.IsNotNull(scaleConstraint);
+            Assert.AreEqual(1, scaleConstraint.Sources.Count);
+            Assert.AreEqual("FixedPrefab", scaleConstraint.Sources[0].SourceTransform.gameObject.name);
+            Assert.AreEqual(1, scaleConstraint.Sources[0].Weight);
+            Assert.AreEqual(true, scaleConstraint.Locked);
+            Assert.AreEqual(true, scaleConstraint.IsActive);
+        }
+        #endif
+
+        private void AssertScaleConstraintPresent(GameObject child)
+        {
+            var scaleConstraint = child.GetComponent<ScaleConstraint>();
+            Assert.IsNotNull(scaleConstraint);
+            Assert.AreEqual(1, scaleConstraint.sourceCount);
+            Assert.AreEqual("FixedPrefab", scaleConstraint.GetSource(0).sourceTransform.gameObject.name);
+            Assert.AreEqual(1, scaleConstraint.GetSource(0).weight);
+            Assert.AreEqual(true, scaleConstraint.locked);
+            Assert.AreEqual(true, scaleConstraint.constraintActive);
+        }
+    }
+}
\ No newline at end of file
diff --git a/UnitTests~/WorldScaleObject/WorldScaleObjectTest.cs.meta b/UnitTests~/WorldScaleObject/WorldScaleObjectTest.cs.meta
new file mode 100644
index 00000000..dba68e96
--- /dev/null
+++ b/UnitTests~/WorldScaleObject/WorldScaleObjectTest.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 41ec840868664e28bd0546a3cc639692
+timeCreated: 1741658559
\ No newline at end of file
diff --git a/docs~/docs/reference/world-scale-object.md b/docs~/docs/reference/world-scale-object.md
new file mode 100644
index 00000000..236735ab
--- /dev/null
+++ b/docs~/docs/reference/world-scale-object.md
@@ -0,0 +1,17 @@
+# World Scale Object
+
+This component can be used to force a game object to have the same scale as the world, regardless of the current avatar
+scale. It will attach a (VRC) Scale Constraint to the game object, and set the constraint to scale to 1,1,1 scale relative
+to the world.
+
+## When should I use it?
+
+When you want to have a game object scale with the world, rather than the avatar. This can be useful in certain complex
+constraint gimmicks.
+
+## Setting up World Scale Object
+
+Simply attach the `World Scale Object` component to a GameObject. There are no configuration options to set.
+
+Note that `World Scale Object` currently is not previewed in the Unity Editor, but will work correctly in-game or in
+play mode.
\ No newline at end of file
diff --git a/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/world-scale.object.md b/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/world-scale.object.md
new file mode 100644
index 00000000..cc695562
--- /dev/null
+++ b/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/world-scale.object.md
@@ -0,0 +1,14 @@
+# World Scale Object
+
+このコンポーネントは、現在のアバタースケールに関係なく、ゲームオブジェクトをワールドと同じスケールにするために使用できます。
+ゲームオブジェクトにScaleConstraintまたはVRCScaleConstraintをアタッチし、1,1,1スケールに設定して、世界に対してスケールを固定します。
+
+## いつ使うべきか?
+
+アバターではなく、ワールドと一緒にスケールするゲームオブジェクトが必要な場合に使用します。特定の複雑なコンストレイントギミックなどで便利です。
+
+## World Scale Objectの設定
+
+単純に`World Scale Object`コンポーネントをゲームオブジェクトにアタッチするだけです。詳細設定はありません。
+
+なお、`World Scale Object`は現在Unityエディターでプレビューされませんが、ゲーム内またはプレイモードで正常に動作します。
\ No newline at end of file