diff --git a/API_REFERENCE.MD b/API_REFERENCE.MD index 26600e9..c182c7f 100644 --- a/API_REFERENCE.MD +++ b/API_REFERENCE.MD @@ -25,6 +25,7 @@ Schema compatibility notes: - `update_component` accepts the preferred `properties` object and the legacy `json_data` string form. - `create_scene` accepts optional `path` and `open_if_exists`, so agents can create or reopen a deterministic scene asset in one call. - `create_primitive` accepts optional `name`, `parent_id`, `position`, `rotation`, `scale`, and `material_path` for visible non-origin object creation. +- Vector3 fields accept either `{ "x": 0, "y": 1, "z": 0 }` or `[0, 1, 0]`. - `set_transform` accepts `position`, `rotation` / `eulerAngles`, and `scale` / `localScale`. - `create_material` accepts optional `path`, `base_color` / `color`, and `emission_color` so callers can create visible materials in a chosen folder. - `write_file` and `write_files_batch` create missing parent directories after path validation. @@ -168,7 +169,7 @@ Actions: `create`, `open`, `save`, `list`. Aliases: `create_scene`, `open_scene` ### `unity_hierarchy_manager` Actions: `create_empty`, `create_primitive`, `create_hierarchy`, `destroy`, `duplicate`, `rename`, `set_name`, `set_transform`, `set_active`, `set_parent`, `set_sibling_index`. -Aliases: `create`, `create_gameobject`, and `create_game_object` map to `create_empty`. `create_primitive` accepts `name`, `parent_id`, `position`, `rotation`, `scale`, and `material_path`. +Aliases: `create`, `create_gameobject`, and `create_game_object` map to `create_empty`. `create_primitive` accepts `name`, `parent_id`, `position`, `rotation`, `scale`, and `material_path`; Vector3 fields accept either object or array form. ### `unity_component_manager` Actions: `add`, `remove`, `inspect`, `get_schema`, `update_properties`, `set_property`, `set_enabled`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6403406..cdd008a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ All notable public changes to Nexus Unity are documented here. ### Changed - Relicensed Nexus Unity from `GPL-3.0-only` to the `MIT` license to remove copyleft friction for commercial Unity studios. All prior contributors consented to the relicense. +### Fixed +- `create_primitive` now validates parent, transform, and material inputs before creating the GameObject, and Vector3 inputs accept `[x, y, z]` arrays as well as `{x, y, z}` objects. + ## [1.4.2] - 2026-06-13 ### Added diff --git a/DOCUMENTATION.MD b/DOCUMENTATION.MD index 5b985a1..f8fdc85 100644 --- a/DOCUMENTATION.MD +++ b/DOCUMENTATION.MD @@ -129,6 +129,7 @@ The current development line preserves backward compatibility while correcting p - `update_component` should use a `properties` object for new callers; `json_data` remains accepted for existing callers. - `create_scene` accepts optional `path` and `open_if_exists` for deterministic scene assets. - `create_primitive` accepts optional `name`, `parent_id`, `position`, `rotation`, `scale`, and `material_path`. +- Vector3 fields accept either `{ "x": 0, "y": 1, "z": 0 }` or `[0, 1, 0]`. - `set_transform` accepts `position`, `rotation` / `eulerAngles`, and `scale` / `localScale`. - `create_material` accepts an optional explicit `path` plus `base_color` / `color` and `emission_color`, so automation can isolate generated materials in a known project folder and make them visibly distinct. - `write_file` and `write_files_batch` create missing parent directories after path validation. diff --git a/Editor/MCPServerMethods.Core.cs b/Editor/MCPServerMethods.Core.cs index e3842f6..8bfc03b 100644 --- a/Editor/MCPServerMethods.Core.cs +++ b/Editor/MCPServerMethods.Core.cs @@ -63,37 +63,56 @@ private static JToken CreatePrimitive(JToken p) { if (p == null || p["primitive_type"] == null) throw new Exception("primitive_type is required"); if (!Enum.TryParse(typeof(PrimitiveType), p["primitive_type"].ToString(), true, out var type)) throw new Exception("Invalid primitive"); - var go = GameObject.CreatePrimitive((PrimitiveType)type); - Undo.RegisterCreatedObjectUndo(go, "Create Primitive"); string name = p["name"]?.ToString(); - if (!string.IsNullOrWhiteSpace(name)) go.name = name; - + Transform parentTransform = null; if (p["parent_id"] != null) { var parent = MCPServerMethods.IdToObject(MCPServerMethods.ExtractId(p, "parent_id")) as GameObject; if (parent == null) throw new Exception("parent_id does not resolve to a GameObject"); - go.transform.SetParent(parent.transform, false); + parentTransform = parent.transform; } - if (p["position"] != null) go.transform.position = ParseVector3(p["position"], go.transform.position); - JToken rotation = p["rotation"] ?? p["eulerAngles"]; - if (rotation != null) go.transform.eulerAngles = ParseVector3(rotation, go.transform.eulerAngles); - JToken scale = p["scale"] ?? p["localScale"]; - if (scale != null) go.transform.localScale = ParseVector3(scale, go.transform.localScale); + Vector3? position = p["position"] != null ? ParseVector3(p["position"]) : null; + JToken rotationToken = p["rotation"] ?? p["eulerAngles"]; + Vector3? rotation = rotationToken != null ? ParseVector3(rotationToken) : null; + JToken scaleToken = p["scale"] ?? p["localScale"]; + Vector3? scale = scaleToken != null ? ParseVector3(scaleToken) : null; + Material material = null; string materialPath = p["material_path"]?.ToString(); if (!string.IsNullOrWhiteSpace(materialPath)) { materialPath = ValidateAssetPath(materialPath); - var material = AssetDatabase.LoadAssetAtPath(materialPath); + material = AssetDatabase.LoadAssetAtPath(materialPath); if (material == null) throw new Exception($"Material not found at {materialPath}"); - var renderer = go.GetComponent(); - if (renderer != null) renderer.sharedMaterial = material; } - Selection.activeGameObject = go; - return new JObject { ["status"] = "Success", ["data"] = SerializeGameObject(go) }; + GameObject go = null; + try + { + go = GameObject.CreatePrimitive((PrimitiveType)type); + Undo.RegisterCreatedObjectUndo(go, "Create Primitive"); + + if (!string.IsNullOrWhiteSpace(name)) go.name = name; + + if (parentTransform != null) go.transform.SetParent(parentTransform, false); + + if (position.HasValue) go.transform.position = position.Value; + if (rotation.HasValue) go.transform.eulerAngles = rotation.Value; + if (scale.HasValue) go.transform.localScale = scale.Value; + + var renderer = go.GetComponent(); + if (renderer != null && material != null) renderer.sharedMaterial = material; + + Selection.activeGameObject = go; + return new JObject { ["status"] = "Success", ["data"] = SerializeGameObject(go) }; + } + catch + { + if (go != null) UnityEngine.Object.DestroyImmediate(go); + throw; + } } private static JToken AttachScript(JToken p) diff --git a/Editor/MCPServerMethods.Reflection.cs b/Editor/MCPServerMethods.Reflection.cs index e098671..c03ccdc 100644 --- a/Editor/MCPServerMethods.Reflection.cs +++ b/Editor/MCPServerMethods.Reflection.cs @@ -429,6 +429,12 @@ private static JToken FormatResult(MethodInfo method, object result) private static Vector3 ParseVector3(JToken t, Vector3 _defaultValue = default) { if (t == null) return _defaultValue; + if (t is JArray array) + { + if (array.Count != 3) throw new Exception("Vector3 array must have exactly 3 numbers"); + return new Vector3(array[0].Value(), array[1].Value(), array[2].Value()); + } + if (t.Type != JTokenType.Object) throw new Exception("Vector3 must be an object with x/y/z or an array [x,y,z]"); return new Vector3((float)(t["x"] ?? _defaultValue.x), (float)(t["y"] ?? _defaultValue.y), (float)(t["z"] ?? _defaultValue.z)); } diff --git a/Editor/MCPServerMethods.Tools.cs b/Editor/MCPServerMethods.Tools.cs index a4a52b3..1730ee8 100644 --- a/Editor/MCPServerMethods.Tools.cs +++ b/Editor/MCPServerMethods.Tools.cs @@ -397,13 +397,24 @@ private static JObject GetPrimitiveSchema() private static JObject GetVector3Schema() => new JObject { - ["type"] = "object", - ["properties"] = new JObject - { - ["x"] = new JObject { ["type"] = "number" }, - ["y"] = new JObject { ["type"] = "number" }, - ["z"] = new JObject { ["type"] = "number" } - } + ["oneOf"] = new JArray( + new JObject + { + ["type"] = "object", + ["properties"] = new JObject + { + ["x"] = new JObject { ["type"] = "number" }, + ["y"] = new JObject { ["type"] = "number" }, + ["z"] = new JObject { ["type"] = "number" } + } + }, + new JObject + { + ["type"] = "array", + ["items"] = new JObject { ["type"] = "number" }, + ["minItems"] = 3, + ["maxItems"] = 3 + }) }; private static string SanitizeScriptName(string n) => System.Text.RegularExpressions.Regex.Replace(n, @"[^a-zA-Z0-9_]", "_"); diff --git a/Editor/nexus_bridge/schemas.py b/Editor/nexus_bridge/schemas.py index a58f22f..31c865d 100644 --- a/Editor/nexus_bridge/schemas.py +++ b/Editor/nexus_bridge/schemas.py @@ -12,12 +12,22 @@ # --- Shared sub-schemas --- VECTOR3_SCHEMA: JsonObject = { - "type": "object", - "properties": { - "x": {"type": "number"}, - "y": {"type": "number"}, - "z": {"type": "number"}, - }, + "oneOf": [ + { + "type": "object", + "properties": { + "x": {"type": "number"}, + "y": {"type": "number"}, + "z": {"type": "number"}, + }, + }, + { + "type": "array", + "items": {"type": "number"}, + "minItems": 3, + "maxItems": 3, + }, + ], } STATIC_TOOLS: list[ToolDefinition] = [ diff --git a/README.md b/README.md index eb6d36c..7ac9b44 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,7 @@ Current development keeps the public API backward-compatible while tightening sc - `unity_scene_manager` accepts obvious aliases such as `list_scenes`, `create_scene`, `open_scene`, and `save_scene`; invalid manager actions now report the valid action names. - `create_scene` accepts optional `path` and `open_if_exists` so agents can create or reopen a deterministic scene asset in one call. - `create_primitive` accepts optional `name`, `parent_id`, `position`, `rotation`, `scale`, and `material_path`, which lets agents build visible non-origin objects without fragile follow-up calls. +- Vector3 fields accept either `{ "x": 0, "y": 1, "z": 0 }` or `[0, 1, 0]`. - `set_transform` updates position, rotation, and scale. - `create_material` accepts optional `path`, `base_color` / `color`, and `emission_color` so generated materials can be created inside a chosen project folder and made visibly distinct. - `write_file` and `write_files_batch` create missing parent directories after path validation. diff --git a/Tests~/Editor/ConsolidatedManagersTests.cs b/Tests~/Editor/ConsolidatedManagersTests.cs index 3db5af4..2ffa875 100644 --- a/Tests~/Editor/ConsolidatedManagersTests.cs +++ b/Tests~/Editor/ConsolidatedManagersTests.cs @@ -288,6 +288,44 @@ public void UnityHierarchyManager_CreatePrimitiveWithNameAndTransform_ReturnsPla Assert.AreEqual(2f, go.transform.localScale.z, 0.001f); } + [Test] + public void UnityHierarchyManager_CreatePrimitiveWithArrayPosition_ReturnsPlacedObject() { + var res = SimulateBridgeRouting("unity_hierarchy_manager", new JObject { + ["action"] = "create_primitive", + ["primitive_type"] = "Cube", + ["name"] = "ArrayPlacedManagerCube", + ["position"] = new JArray(9, 9, 9) + }); + + Assert.IsNotNull(res["result"], $"Expected result, got error: {res["error"]}"); + var id = res["result"]["data"]?["instance_id"]?.Value(); + Assert.IsTrue(id.HasValue && id.Value != 0); + + var go = EditorUtility.InstanceIDToObject(id.Value) as GameObject; + Assert.IsNotNull(go); + _createdObjects.Add(go); + Assert.AreEqual(9f, go.transform.position.x, 0.001f); + Assert.AreEqual(9f, go.transform.position.y, 0.001f); + Assert.AreEqual(9f, go.transform.position.z, 0.001f); + } + + [Test] + public void UnityHierarchyManager_CreatePrimitiveWithInvalidArrayPosition_DoesNotCreateObject() { + string name = "InvalidArrayCube_" + Guid.NewGuid().ToString("N"); + int before = GameObject.FindObjectsOfType().Count(go => go.name == name); + + var res = SimulateBridgeRouting("unity_hierarchy_manager", new JObject { + ["action"] = "create_primitive", + ["primitive_type"] = "Cube", + ["name"] = name, + ["position"] = new JArray(9, 9) + }); + + Assert.IsNotNull(res["error"], "Expected invalid Vector3 array to fail."); + int after = GameObject.FindObjectsOfType().Count(go => go.name == name); + Assert.AreEqual(before, after); + } + [Test] public void UnityHierarchyManager_RenameAlias_UpdatesGameObjectName() { var go = CreateTestGameObject();