From b51f063d525e4d1c434d2a577f22a918cff91222 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Fri, 24 Apr 2026 12:53:01 +0200 Subject: [PATCH] Pivot-anchor fallback for no-collision spawns + always-snap on catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ground snap: - When an actor has no colliding components at all (e.g. a VR start marker, placement helper, or any BP whose collision is only enabled on entering editor mode), SnapActorToGround now anchors the PIVOT directly on the ground instead of falling back to the full-bounds bbox bottom. The full-bounds bottom could be far below the pivot (helper meshes, arrows, visual markers) and was lifting objects hundreds of units into the air. - Actors with collision keep the original behavior (collision-bbox bottom rests on the ground). Catalog spawn: - Always snap to ground (drop the bAutoSnapToGround gate) — a user clicking in the catalog expects the object to land on the ground every time; the flag is kept for gameplay-side spawning via SceneLoader. - SpawnActor now uses AlwaysSpawn instead of AdjustIfPossibleButAlwaysSpawn so UE never pushes the actor vertically to resolve overlaps — SnapActorToGround is the authoritative placement step. CLAUDE.md workflow rule: - Document that behavior / logic changes require explaining the plan first and waiting for go-ahead; only diagnostic-only additions (temporary UE_LOG) are fine without approval. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 11 ++++++++ .../Selection/PS_Editor_GroundSnap.cpp | 27 +++++++++++++------ .../Spawn/PS_Editor_SpawnManager.cpp | 17 +++++++++--- 3 files changed, 43 insertions(+), 12 deletions(-) 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);