| 360 | } // namespace |
| 361 | |
| 362 | bool SpawnActorSync( |
| 363 | const FString& BlueprintNameOrPath, |
| 364 | const FString& ActorId, |
| 365 | const FVector& LocationMeters, |
| 366 | const FQuat& RotationQuatXyzw, |
| 367 | const FVector& Scale, |
| 368 | FString& OutActorName, |
| 369 | FString& OutActorPath, |
| 370 | FString& OutBlueprintClassPath, |
| 371 | bool& OutWasExisting, |
| 372 | FString& OutError) |
| 373 | { |
| 374 | OutActorName.Empty(); |
| 375 | OutActorPath.Empty(); |
| 376 | OutBlueprintClassPath.Empty(); |
| 377 | OutWasExisting = false; |
| 378 | OutError.Empty(); |
| 379 | |
| 380 | if (!GEditor) |
| 381 | { |
| 382 | OutError = TEXT("GEditor null"); |
| 383 | return false; |
| 384 | } |
| 385 | UWorld* World = GEditor->GetEditorWorldContext().World(); |
| 386 | if (!World) |
| 387 | { |
| 388 | OutError = TEXT("editor world unavailable"); |
| 389 | return false; |
| 390 | } |
| 391 | |
| 392 | OutBlueprintClassPath = ResolveBpClassPath(BlueprintNameOrPath); |
| 393 | UClass* BPClass = LoadObject<UClass>(nullptr, *OutBlueprintClassPath); |
| 394 | if (!BPClass) |
| 395 | { |
| 396 | OutError = FString::Printf(TEXT("blueprint class not found: %s"), |
| 397 | *OutBlueprintClassPath); |
| 398 | return false; |
| 399 | } |
| 400 | |
| 401 | // MJ -> UE: cm + Y-flip + (handedness baked into MjUtils helpers). |
| 402 | // Accept the conversion routines we already use everywhere else |
| 403 | // so spawn matches imported geometry to within float precision. |
| 404 | double MjPos[3] = {LocationMeters.X, LocationMeters.Y, LocationMeters.Z}; |
| 405 | FVector UELoc = MjUtils::MjToUEPosition(MjPos); |
| 406 | |
| 407 | // Quaternion convention is (w, x, y, z) on the wire when read by |
| 408 | // MjUtils, so re-pack from xyzw. |
| 409 | double MjQuat[4] = { |
| 410 | RotationQuatXyzw.W, |
| 411 | RotationQuatXyzw.X, |
| 412 | RotationQuatXyzw.Y, |
| 413 | RotationQuatXyzw.Z}; |
| 414 | FQuat UEQuat = MjUtils::MjToUERotation(MjQuat); |
| 415 | FRotator UERot = UEQuat.Rotator(); |
| 416 | |
| 417 | // Idempotent path: a non-empty ActorId may already be in the world. |
| 418 | // Update transform on the existing actor instead of duplicating it. |
| 419 | // Empty ActorId always falls through to fresh spawn. |