|
1 | 1 | """Prefab CLI commands.""" |
2 | 2 |
|
| 3 | +import json |
3 | 4 | import sys |
4 | 5 | import click |
5 | 6 | from typing import Optional, Any |
@@ -246,3 +247,132 @@ def create(target: str, path: str, overwrite: bool, include_inactive: bool, unli |
246 | 247 | click.echo(format_output(result, config.format)) |
247 | 248 | if result.get("success"): |
248 | 249 | print_success(f"Created prefab: {path}") |
| 250 | + |
| 251 | + |
| 252 | +def _parse_vector3(value: str) -> list[float]: |
| 253 | + """Parse 'x,y,z' string to list of floats.""" |
| 254 | + parts = value.split(",") |
| 255 | + if len(parts) != 3: |
| 256 | + raise click.BadParameter("Must be 'x,y,z' format") |
| 257 | + try: |
| 258 | + return [float(p.strip()) for p in parts] |
| 259 | + except ValueError as e: |
| 260 | + raise click.BadParameter(f"All components must be numeric, got: '{value}'") from e |
| 261 | + |
| 262 | + |
| 263 | +def _parse_property(prop_str: str) -> tuple[str, str, Any]: |
| 264 | + """Parse 'Component.prop=value' into (component, prop, value).""" |
| 265 | + if "=" not in prop_str: |
| 266 | + raise click.BadParameter("Must be 'Component.prop=value' format") |
| 267 | + comp_prop, val_str = prop_str.split("=", 1) |
| 268 | + if "." not in comp_prop: |
| 269 | + raise click.BadParameter("Must be 'Component.prop=value' format") |
| 270 | + component, prop = comp_prop.rsplit(".", 1) |
| 271 | + if not component.strip() or not prop.strip(): |
| 272 | + raise click.BadParameter(f"Component and property must be non-empty in '{comp_prop}', expected 'Component.prop=value'") |
| 273 | + |
| 274 | + val_str = val_str.strip() |
| 275 | + |
| 276 | + # Parse booleans |
| 277 | + if val_str.lower() == "true": |
| 278 | + parsed_value: Any = True |
| 279 | + elif val_str.lower() == "false": |
| 280 | + parsed_value = False |
| 281 | + # Parse numbers |
| 282 | + elif "." in val_str: |
| 283 | + try: |
| 284 | + parsed_value = float(val_str) |
| 285 | + except ValueError: |
| 286 | + parsed_value = val_str |
| 287 | + else: |
| 288 | + try: |
| 289 | + parsed_value = int(val_str) |
| 290 | + except ValueError: |
| 291 | + parsed_value = val_str |
| 292 | + |
| 293 | + return component.strip(), prop.strip(), parsed_value |
| 294 | + |
| 295 | + |
| 296 | +@prefab.command("modify") |
| 297 | +@click.argument("path") |
| 298 | +@click.option("--target", "-t", help="Target object name/path within prefab (default: root)") |
| 299 | +@click.option("--position", "-p", help="New local position as 'x,y,z'") |
| 300 | +@click.option("--rotation", "-r", help="New local rotation as 'x,y,z'") |
| 301 | +@click.option("--scale", "-s", help="New local scale as 'x,y,z'") |
| 302 | +@click.option("--name", "-n", help="New name for target") |
| 303 | +@click.option("--tag", help="New tag") |
| 304 | +@click.option("--layer", help="New layer") |
| 305 | +@click.option("--active/--inactive", default=None, help="Set active state") |
| 306 | +@click.option("--parent", help="New parent object name/path") |
| 307 | +@click.option("--add-component", multiple=True, help="Component type to add (repeatable)") |
| 308 | +@click.option("--remove-component", multiple=True, help="Component type to remove (repeatable)") |
| 309 | +@click.option("--set-property", multiple=True, help="Property as 'Component.prop=value' (repeatable)") |
| 310 | +@click.option("--delete-child", multiple=True, help="Child name/path to remove (repeatable)") |
| 311 | +@click.option("--create-child", help="JSON object for child creation") |
| 312 | +@handle_unity_errors |
| 313 | +def modify(path: str, target: Optional[str], position: Optional[str], rotation: Optional[str], |
| 314 | + scale: Optional[str], name: Optional[str], tag: Optional[str], layer: Optional[str], |
| 315 | + active: Optional[bool], parent: Optional[str], add_component: tuple, remove_component: tuple, |
| 316 | + set_property: tuple, delete_child: tuple, create_child: Optional[str]): |
| 317 | + """Modify a prefab's contents (headless, no UI). |
| 318 | + |
| 319 | + \b |
| 320 | + Examples: |
| 321 | + unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --delete-child Child1 |
| 322 | + unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --delete-child "Turret/Barrel" --delete-child Bullet |
| 323 | + unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --target Weapon --position "0,1,2" |
| 324 | + unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --set-property "Rigidbody.mass=5" |
| 325 | + unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --create-child '{"name":"Spawn","primitive_type":"Sphere"}' |
| 326 | + """ |
| 327 | + config = get_config() |
| 328 | + |
| 329 | + params: dict[str, Any] = { |
| 330 | + "action": "modify_contents", |
| 331 | + "prefabPath": path, |
| 332 | + } |
| 333 | + |
| 334 | + if target: |
| 335 | + params["target"] = target |
| 336 | + if position: |
| 337 | + params["position"] = _parse_vector3(position) |
| 338 | + if rotation: |
| 339 | + params["rotation"] = _parse_vector3(rotation) |
| 340 | + if scale: |
| 341 | + params["scale"] = _parse_vector3(scale) |
| 342 | + if name: |
| 343 | + params["name"] = name |
| 344 | + if tag: |
| 345 | + params["tag"] = tag |
| 346 | + if layer: |
| 347 | + params["layer"] = layer |
| 348 | + if active is not None: |
| 349 | + params["setActive"] = active |
| 350 | + if parent: |
| 351 | + params["parent"] = parent |
| 352 | + if add_component: |
| 353 | + params["componentsToAdd"] = list(add_component) |
| 354 | + if remove_component: |
| 355 | + params["componentsToRemove"] = list(remove_component) |
| 356 | + if set_property: |
| 357 | + component_properties: dict[str, dict[str, Any]] = {} |
| 358 | + for prop in set_property: |
| 359 | + comp, name_p, val = _parse_property(prop) |
| 360 | + if comp not in component_properties: |
| 361 | + component_properties[comp] = {} |
| 362 | + component_properties[comp][name_p] = val |
| 363 | + params["componentProperties"] = component_properties |
| 364 | + if delete_child: |
| 365 | + params["deleteChild"] = list(delete_child) |
| 366 | + if create_child: |
| 367 | + try: |
| 368 | + parsed = json.loads(create_child) |
| 369 | + except json.JSONDecodeError as e: |
| 370 | + raise click.BadParameter(f"Invalid JSON for --create-child: {e}") from e |
| 371 | + if not isinstance(parsed, dict): |
| 372 | + raise click.BadParameter(f"--create-child must be a JSON object, got {type(parsed).__name__}") |
| 373 | + params["createChild"] = parsed |
| 374 | + |
| 375 | + result = run_command("manage_prefabs", params, config) |
| 376 | + click.echo(format_output(result, config.format)) |
| 377 | + if result.get("success"): |
| 378 | + print_success(f"Modified prefab: {path}") |
0 commit comments