diff --git a/CLAUDE.md b/CLAUDE.md index e80b612..747e34e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,6 +83,17 @@ Symptom that the wrong widget was edited: logs you added don't appear in the Out - When fixing a bug, fix the root cause, not the symptom. - If something requires error handling or validation to work reliably, include it without asking. +## Workflow — CRITICAL +- **Do not make behavior / logic changes without first explaining the plan and waiting for the + user's go-ahead.** Explain what the change will be, why it fixes the issue, and what side + effects it could have. Only after the user approves (or asks to proceed), apply the edit. +- Diagnostic-only additions (temporary `UE_LOG`, read-only inspections) are fine without + approval because they can't regress the project. +- Refactors, algorithm changes, comportement changes, new defaults, removed features — all + require approval first, even when they seem obvious. +- When revealing new data (e.g. logs) suggests a fix, STATE the proposed fix in plain words + and wait. Don't pre-emptively write + apply the code change in the same turn. + ## Build - Engine: Unreal Engine 5.5 - Plugin location: `Unreal/Plugins/PS_Editor/` diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_GroundSnap.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_GroundSnap.cpp index bd7cdf8..b301e77 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_GroundSnap.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_GroundSnap.cpp @@ -120,22 +120,33 @@ namespace PS_Editor_GroundSnap return false; } - // Measure the bbox bottom relative to the pivot. Use COLLIDING components only so - // a Character's capsule drives the calculation — not its skeletal mesh (often with - // collision disabled) or attached weapons / child actors whose meshes may extend - // below the capsule and artificially lift the pawn by that overflow. - // Falls back to the full actor bounds if no colliding component is found (e.g. a - // pure-mesh prop without collision — rare but possible). + // Measure the bbox bottom relative to the pivot. + // + // Primary path — colliding components only: a Character's capsule drives the + // calculation, ignoring the skeletal mesh (often with collision disabled) or any + // attached weapons / child actors whose meshes might extend below the capsule and + // artificially lift the pawn by that overflow. Formula: pivot is placed so the + // collision-bbox bottom rests on the ground. + // + // Fallback — no colliding components found: the actor is treated as a pure visual + // marker (VR start zone, placement helper, or a BP that enables collision only + // when entering editor mode and hasn't done so yet). For these, we don't trust + // the full-bounds bbox (it may wrap helper meshes far below the pivot and push + // the object absurdly high). Instead we anchor the PIVOT directly on the ground — + // which is what the designer authored the pivot for on these markers. FVector BoundsOrigin, BoundsExtent; Actor->GetActorBounds(/*bOnlyCollidingComponents*/ true, BoundsOrigin, BoundsExtent); - if (BoundsExtent.IsNearlyZero()) + const bool bUsedFallback = BoundsExtent.IsNearlyZero(); + if (bUsedFallback) { + // Read the full bounds too — only to know where "above the actor" is for the + // ray start position, not to anchor the snap. Actor->GetActorBounds(false, BoundsOrigin, BoundsExtent); } const float ActorZ = Actor->GetActorLocation().Z; const float BBoxBottomZ = BoundsOrigin.Z - BoundsExtent.Z; const float BBoxTopZ = BoundsOrigin.Z + BoundsExtent.Z; - const float PivotAboveBottom = ActorZ - BBoxBottomZ; + const float PivotAboveBottom = bUsedFallback ? 0.0f : (ActorZ - BBoxBottomZ); float ExtraOffset = 0.0f; if (UPS_Editor_SpawnableComponent* SpawnComp = Actor->FindComponentByClass()) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp index 8c05930..9764ead 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp @@ -130,7 +130,13 @@ AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector } FActorSpawnParameters SpawnParams; - SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn; + // AlwaysSpawn (instead of AdjustIfPossibleButAlwaysSpawn) so UE doesn't push the actor + // upward when its bounds overlap the ground at spawn time — our SnapActorToGround call + // below is the authoritative ground placement step, and it uses collision-only bounds + // which correctly ignore non-collidable attached meshes (weapons, child actors). + // With the old override, a character with a weapon dangling below the capsule could + // be shoved high by the auto-adjust, leaving it stuck in the air. + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; AActor* SpawnedActor = PC->GetWorld()->SpawnActor(Entry.ActorClass, WorldLocation, FRotator::ZeroRotator, SpawnParams); if (!SpawnedActor) @@ -160,10 +166,13 @@ AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector SpawnedActors.Add(SpawnedActor); RegisterSpawnedActorId(PC->GetWorld(), SpawnedActor); - // Auto-snap to ground if configured — uses the shared helper so it handles pivot - // positions (characters with pivot at feet) + ignores the fresh actor itself. + // Catalogue spawns are always a deliberate user placement, so always snap to ground. + // Previously gated on SpawnableComponent::bAutoSnapToGround — that flag is preserved + // as the gameplay-spawn hint (for SceneLoader / runtime spawning) but here we want + // consistent placement regardless of BP configuration. If the actor genuinely shouldn't + // touch the ground, the user can move it with the gizmo after spawning. UPS_Editor_SpawnableComponent* SpawnComp = SpawnedActor->FindComponentByClass(); - if (SpawnComp && SpawnComp->bAutoSnapToGround && PC->GetWorld()) + if (PC->GetWorld()) { TArray ExtraIgnored; if (APawn* Pwn = PC->GetPawn()) ExtraIgnored.Add(Pwn);