Skip to content

Commit 4d170d0

Browse files
committed
sync: cli-v1.11.0
1 parent 1be41f4 commit 4d170d0

23 files changed

Lines changed: 2175 additions & 573 deletions

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All notable changes to soft-ue-cli will be documented in this file.
44

5+
## [1.11.0] - 2026-04-05
6+
7+
### Added
8+
- `set-viewport-camera` command — programmatically control the editor viewport camera with presets (top, bottom, front, back, left, right, perspective), custom location/rotation, and orthographic zoom
9+
- `level-from-image` skill — populate a UE level from a reference image using existing project assets, with autonomous visual feedback loop and human-in-the-loop refinement
10+
- Batch actor tool reference section in level-from-image skill documentation
11+
12+
### Fixed
13+
- Skills frontmatter parser now skips nested YAML lines, correctly displaying skill descriptions
14+
515
## [1.10.0] - 2026-04-05
616

717
### Added

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ Every command is available via `soft-ue-cli <command>`. Run `soft-ue-cli <comman
229229
| Command | Description |
230230
|---------|-------------|
231231
| `capture-screenshot` | Capture the editor viewport, PIE window, or a specific editor panel |
232+
| `set-viewport-camera` | Set editor viewport camera position, rotation, or preset view (top/front/right/perspective) |
232233

233234
### Logging and Console Variables
234235

@@ -412,6 +413,18 @@ soft-ue-cli skills get blueprint-to-cpp
412413
# ...and generates the .h/.cpp files from the JSON responses
413414
```
414415

416+
### Populate a level from a reference image
417+
418+
```bash
419+
# Get the level-from-image skill instructions
420+
soft-ue-cli skills get level-from-image
421+
# The LLM analyzes the image, searches for matching assets, places them,
422+
# then enters a visual feedback loop:
423+
# soft-ue-cli set-viewport-camera --preset top --ortho-width 8000
424+
# soft-ue-cli capture-screenshot window --output file
425+
# Compares screenshot to reference, auto-corrects, then asks for human feedback
426+
```
427+
415428
### Profile with UE Insights
416429

417430
```bash

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "soft-ue-cli"
7-
version = "1.10.0"
7+
version = "1.11.0"
88
description = "CLI tool for controlling Unreal Engine via soft-ue-bridge plugin"
99
requires-python = ">=3.10"
1010
dependencies = [

soft_ue_cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""soft-ue-cli — CLI interface to the SoftUEBridge UE plugin."""
22

3-
__version__ = "1.10.0"
3+
__version__ = "1.11.0"

soft_ue_cli/__main__.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@ def cmd_spawn_actor(args: argparse.Namespace) -> None:
107107
_print_json(_run_tool("spawn-actor", arguments))
108108

109109

110+
def cmd_set_viewport_camera(args: argparse.Namespace) -> None:
111+
arguments: dict = {}
112+
if args.preset:
113+
arguments["preset"] = args.preset
114+
if args.location:
115+
arguments["location"] = _parse_vector(args.location)
116+
if args.rotation:
117+
arguments["rotation"] = _parse_vector(args.rotation)
118+
if args.ortho_width is not None:
119+
arguments["ortho_width"] = args.ortho_width
120+
_print_json(_run_tool("set-viewport-camera", arguments))
121+
122+
110123
def cmd_batch_spawn_actors(args: argparse.Namespace) -> None:
111124
arguments: dict = {"actors": _parse_json_arg(args.actors, "--actors")}
112125
_print_json(_run_tool("batch-spawn-actors", arguments))
@@ -1273,6 +1286,30 @@ def build_parser() -> argparse.ArgumentParser:
12731286
p_spawn.add_argument("--world", choices=["editor", "pie"], help="Target world: editor (default) or pie")
12741287
p_spawn.set_defaults(func=cmd_spawn_actor)
12751288

1289+
# set-viewport-camera
1290+
p_vpcam = sub.add_parser(
1291+
"set-viewport-camera",
1292+
help="Set the editor viewport camera position, rotation, or view preset.",
1293+
description=(
1294+
"Set the editor viewport camera. Use presets for quick views or\n"
1295+
"specify location/rotation manually.\n\n"
1296+
"Presets: top, bottom, front, back, left, right, perspective\n\n"
1297+
"Examples:\n"
1298+
" soft-ue-cli set-viewport-camera --preset top\n"
1299+
" soft-ue-cli set-viewport-camera --preset top --ortho-width 10000\n"
1300+
" soft-ue-cli set-viewport-camera --location 0,0,2000 --rotation -90,0,0\n"
1301+
" soft-ue-cli set-viewport-camera --preset perspective --location 500,500,500"
1302+
),
1303+
formatter_class=argparse.RawDescriptionHelpFormatter,
1304+
)
1305+
p_vpcam.add_argument("--preset", choices=["top", "bottom", "front", "back", "left", "right", "perspective"],
1306+
help="Camera preset view")
1307+
p_vpcam.add_argument("--location", metavar="X,Y,Z", help="Camera position in cm")
1308+
p_vpcam.add_argument("--rotation", metavar="P,Y,R", help="Camera rotation in degrees")
1309+
p_vpcam.add_argument("--ortho-width", type=float, dest="ortho_width",
1310+
help="Orthographic view width (zoom). Larger = more zoomed out")
1311+
p_vpcam.set_defaults(func=cmd_set_viewport_camera)
1312+
12761313
# batch-spawn-actors
12771314
p_batch_spawn = sub.add_parser(
12781315
"batch-spawn-actors",

soft_ue_cli/plugin_data/SoftUEBridge/Source/SoftUEBridgeEditor/Private/SoftUEBridgeEditorModule.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
#include "Tools/Write/BatchSpawnActorTool.h"
7979
#include "Tools/Write/BatchModifyActorTool.h"
8080
#include "Tools/Write/BatchDeleteActorTool.h"
81+
#include "Tools/Write/SetViewportCameraTool.h"
8182

8283
DEFINE_LOG_CATEGORY(LogSoftUEBridgeEditor);
8384

@@ -161,6 +162,7 @@ void FSoftUEBridgeEditorModule::StartupModule()
161162
Registry.RegisterToolClass<UBatchSpawnActorTool>();
162163
Registry.RegisterToolClass<UBatchModifyActorTool>();
163164
Registry.RegisterToolClass<UBatchDeleteActorTool>();
165+
Registry.RegisterToolClass<USetViewportCameraTool>();
164166

165167
UE_LOG(LogSoftUEBridgeEditor, Log, TEXT("Registered %d editor bridge tools"), Registry.GetToolCount());
166168

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Copyright soft-ue-expert. All Rights Reserved.
2+
3+
#include "Tools/Write/SetViewportCameraTool.h"
4+
#include "SoftUEBridgeEditorModule.h"
5+
#include "Editor.h"
6+
#include "LevelEditorViewport.h"
7+
#include "SLevelViewport.h"
8+
#include "LevelEditor.h"
9+
#include "Modules/ModuleManager.h"
10+
11+
FString USetViewportCameraTool::GetToolDescription() const
12+
{
13+
return TEXT("Set the editor viewport camera position, rotation, and view mode. "
14+
"Supports perspective and orthographic views (Top, Front, Right, etc.). "
15+
"Use 'preset' for quick top-down/front/side views, or set location/rotation manually.");
16+
}
17+
18+
TMap<FString, FBridgeSchemaProperty> USetViewportCameraTool::GetInputSchema() const
19+
{
20+
TMap<FString, FBridgeSchemaProperty> Schema;
21+
22+
FBridgeSchemaProperty Location;
23+
Location.Type = TEXT("array");
24+
Location.Description = TEXT("Camera position [x, y, z]. Ignored for orthographic presets.");
25+
Location.bRequired = false;
26+
Schema.Add(TEXT("location"), Location);
27+
28+
FBridgeSchemaProperty Rotation;
29+
Rotation.Type = TEXT("array");
30+
Rotation.Description = TEXT("Camera rotation [pitch, yaw, roll] in degrees. Ignored for orthographic presets.");
31+
Rotation.bRequired = false;
32+
Schema.Add(TEXT("rotation"), Rotation);
33+
34+
FBridgeSchemaProperty Preset;
35+
Preset.Type = TEXT("string");
36+
Preset.Description = TEXT("Camera preset: 'top', 'bottom', 'front', 'back', 'left', 'right', or 'perspective'. "
37+
"Orthographic presets switch to ortho view. 'perspective' switches back.");
38+
Preset.bRequired = false;
39+
Schema.Add(TEXT("preset"), Preset);
40+
41+
FBridgeSchemaProperty OrthoWidth;
42+
OrthoWidth.Type = TEXT("number");
43+
OrthoWidth.Description = TEXT("Orthographic view width (zoom level). Larger = more zoomed out. Default: 5000.");
44+
OrthoWidth.bRequired = false;
45+
Schema.Add(TEXT("ortho_width"), OrthoWidth);
46+
47+
return Schema;
48+
}
49+
50+
TArray<FString> USetViewportCameraTool::GetRequiredParams() const
51+
{
52+
return {};
53+
}
54+
55+
FBridgeToolResult USetViewportCameraTool::Execute(
56+
const TSharedPtr<FJsonObject>& Arguments,
57+
const FBridgeToolContext& Context)
58+
{
59+
// Get the first level viewport
60+
FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked<FLevelEditorModule>(TEXT("LevelEditor"));
61+
TSharedPtr<ILevelEditor> LevelEditorInstance = LevelEditor.GetFirstLevelEditor();
62+
if (!LevelEditorInstance.IsValid())
63+
{
64+
return FBridgeToolResult::Error(TEXT("No level editor found"));
65+
}
66+
67+
TSharedPtr<SLevelViewport> ActiveViewport = LevelEditorInstance->GetActiveViewportInterface();
68+
if (!ActiveViewport.IsValid())
69+
{
70+
return FBridgeToolResult::Error(TEXT("No active viewport found"));
71+
}
72+
73+
FLevelEditorViewportClient& ViewportClient = ActiveViewport->GetLevelViewportClient();
74+
75+
const FString Preset = GetStringArgOrDefault(Arguments, TEXT("preset"));
76+
const float OrthoWidth = GetFloatArgOrDefault(Arguments, TEXT("ortho_width"), 5000.0f);
77+
78+
// Handle presets
79+
if (!Preset.IsEmpty())
80+
{
81+
if (Preset.Equals(TEXT("top"), ESearchCase::IgnoreCase))
82+
{
83+
ViewportClient.SetViewportType(LVT_OrthoXY);
84+
ViewportClient.SetOrthoZoom(OrthoWidth);
85+
ViewportClient.SetViewRotation(FRotator(-90, 0, 0));
86+
}
87+
else if (Preset.Equals(TEXT("bottom"), ESearchCase::IgnoreCase))
88+
{
89+
ViewportClient.SetViewportType(LVT_OrthoNegativeXY);
90+
ViewportClient.SetOrthoZoom(OrthoWidth);
91+
ViewportClient.SetViewRotation(FRotator(90, 0, 0));
92+
}
93+
else if (Preset.Equals(TEXT("front"), ESearchCase::IgnoreCase))
94+
{
95+
ViewportClient.SetViewportType(LVT_OrthoXZ);
96+
ViewportClient.SetOrthoZoom(OrthoWidth);
97+
ViewportClient.SetViewRotation(FRotator(0, -90, 0));
98+
}
99+
else if (Preset.Equals(TEXT("back"), ESearchCase::IgnoreCase))
100+
{
101+
ViewportClient.SetViewportType(LVT_OrthoNegativeXZ);
102+
ViewportClient.SetOrthoZoom(OrthoWidth);
103+
ViewportClient.SetViewRotation(FRotator(0, 90, 0));
104+
}
105+
else if (Preset.Equals(TEXT("left"), ESearchCase::IgnoreCase))
106+
{
107+
ViewportClient.SetViewportType(LVT_OrthoYZ);
108+
ViewportClient.SetOrthoZoom(OrthoWidth);
109+
ViewportClient.SetViewRotation(FRotator(0, 0, 0));
110+
}
111+
else if (Preset.Equals(TEXT("right"), ESearchCase::IgnoreCase))
112+
{
113+
ViewportClient.SetViewportType(LVT_OrthoNegativeYZ);
114+
ViewportClient.SetOrthoZoom(OrthoWidth);
115+
ViewportClient.SetViewRotation(FRotator(0, 180, 0));
116+
}
117+
else if (Preset.Equals(TEXT("perspective"), ESearchCase::IgnoreCase))
118+
{
119+
ViewportClient.SetViewportType(LVT_Perspective);
120+
}
121+
else
122+
{
123+
return FBridgeToolResult::Error(FString::Printf(
124+
TEXT("Unknown preset '%s'. Use: top, bottom, front, back, left, right, perspective"), *Preset));
125+
}
126+
}
127+
128+
// Apply custom location if provided
129+
const TArray<TSharedPtr<FJsonValue>>* LocArr;
130+
if (Arguments->TryGetArrayField(TEXT("location"), LocArr) && LocArr->Num() >= 3)
131+
{
132+
FVector Location;
133+
Location.X = (*LocArr)[0]->AsNumber();
134+
Location.Y = (*LocArr)[1]->AsNumber();
135+
Location.Z = (*LocArr)[2]->AsNumber();
136+
ViewportClient.SetViewLocation(Location);
137+
}
138+
139+
// Apply custom rotation if provided
140+
const TArray<TSharedPtr<FJsonValue>>* RotArr;
141+
if (Arguments->TryGetArrayField(TEXT("rotation"), RotArr) && RotArr->Num() >= 3)
142+
{
143+
FRotator Rotation;
144+
Rotation.Pitch = (*RotArr)[0]->AsNumber();
145+
Rotation.Yaw = (*RotArr)[1]->AsNumber();
146+
Rotation.Roll = (*RotArr)[2]->AsNumber();
147+
ViewportClient.SetViewRotation(Rotation);
148+
}
149+
150+
// Force viewport redraw
151+
ViewportClient.Invalidate();
152+
153+
// Build result
154+
FVector FinalLoc = ViewportClient.GetViewLocation();
155+
FRotator FinalRot = ViewportClient.GetViewRotation();
156+
157+
TSharedPtr<FJsonObject> Result = MakeShareable(new FJsonObject);
158+
Result->SetBoolField(TEXT("success"), true);
159+
160+
TArray<TSharedPtr<FJsonValue>> LocJson;
161+
LocJson.Add(MakeShared<FJsonValueNumber>(FinalLoc.X));
162+
LocJson.Add(MakeShared<FJsonValueNumber>(FinalLoc.Y));
163+
LocJson.Add(MakeShared<FJsonValueNumber>(FinalLoc.Z));
164+
Result->SetArrayField(TEXT("location"), LocJson);
165+
166+
TArray<TSharedPtr<FJsonValue>> RotJson;
167+
RotJson.Add(MakeShared<FJsonValueNumber>(FinalRot.Pitch));
168+
RotJson.Add(MakeShared<FJsonValueNumber>(FinalRot.Yaw));
169+
RotJson.Add(MakeShared<FJsonValueNumber>(FinalRot.Roll));
170+
Result->SetArrayField(TEXT("rotation"), RotJson);
171+
172+
const bool bOrtho = ViewportClient.IsOrtho();
173+
Result->SetStringField(TEXT("view_type"), bOrtho ? TEXT("orthographic") : TEXT("perspective"));
174+
if (!Preset.IsEmpty())
175+
{
176+
Result->SetStringField(TEXT("preset"), Preset);
177+
}
178+
179+
UE_LOG(LogSoftUEBridgeEditor, Log, TEXT("set-viewport-camera: %s at [%.0f, %.0f, %.0f]"),
180+
bOrtho ? TEXT("ortho") : TEXT("perspective"), FinalLoc.X, FinalLoc.Y, FinalLoc.Z);
181+
return FBridgeToolResult::Json(Result);
182+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright soft-ue-expert. All Rights Reserved.
2+
3+
#pragma once
4+
5+
#include "CoreMinimal.h"
6+
#include "Tools/BridgeToolBase.h"
7+
#include "SetViewportCameraTool.generated.h"
8+
9+
UCLASS()
10+
class SOFTUEBRIDGEEDITOR_API USetViewportCameraTool : public UBridgeToolBase
11+
{
12+
GENERATED_BODY()
13+
14+
public:
15+
virtual FString GetToolName() const override { return TEXT("set-viewport-camera"); }
16+
virtual FString GetToolDescription() const override;
17+
virtual TMap<FString, FBridgeSchemaProperty> GetInputSchema() const override;
18+
virtual TArray<FString> GetRequiredParams() const override;
19+
virtual FBridgeToolResult Execute(
20+
const TSharedPtr<FJsonObject>& Arguments,
21+
const FBridgeToolContext& Context) override;
22+
};

soft_ue_cli/skills/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ def _parse_frontmatter(text: str) -> dict[str, str]:
1616
return {}
1717
fields: dict[str, str] = {}
1818
for line in text[3:end].strip().splitlines():
19+
if line.startswith((" ", "\t", "-")):
20+
continue
1921
if ":" in line:
2022
key, _, value = line.partition(":")
2123
fields[key.strip()] = value.strip()

0 commit comments

Comments
 (0)