fix(editor): terrain raycast on rough terrain, keyboard shortcuts, context menu

- Raycast AABB now uses actual min/max vertex heights per chunk
  instead of fixed ±200 padding (fixes misses on sculpted terrain)
- Right-click context menu opens correctly (deferred popup via flag
  since ImGui::OpenPopup must be called within ImGui frame)
- Keyboard shortcuts: G=Move, R=Rotate, T=Scale, X/Y=axis lock,
  Escape=deselect, Delete works in any mode for objects/NPCs
- Delete key now removes selected NPC in NPC mode too
This commit is contained in:
Kelsi 2026-05-05 03:55:53 -07:00
parent f38884856f
commit ace6173401
4 changed files with 40 additions and 7 deletions

View file

@ -189,9 +189,27 @@ void EditorApp::processEvents() {
if (event.type == SDL_KEYDOWN) { if (event.type == SDL_KEYDOWN) {
auto sc = event.key.keysym.scancode; auto sc = event.key.keysym.scancode;
if (sc == SDL_SCANCODE_F3) setWireframe(!isWireframe()); if (sc == SDL_SCANCODE_F3) setWireframe(!isWireframe());
if (sc == SDL_SCANCODE_DELETE && mode_ == EditorMode::PlaceObject) { // Transform shortcuts (Blender-style)
objectPlacer_.deleteSelected(); if (objectPlacer_.getSelected()) {
objectsDirty_ = true; if (sc == SDL_SCANCODE_G) startGizmoMode(TransformMode::Move);
if (sc == SDL_SCANCODE_R) startGizmoMode(TransformMode::Rotate);
if (sc == SDL_SCANCODE_T) startGizmoMode(TransformMode::Scale);
if (sc == SDL_SCANCODE_X) setGizmoAxis(TransformAxis::X);
if (sc == SDL_SCANCODE_Y) setGizmoAxis(TransformAxis::Y);
if (sc == SDL_SCANCODE_ESCAPE) {
viewport_.getGizmo().endDrag();
viewport_.getGizmo().setMode(TransformMode::None);
objectPlacer_.clearSelection();
}
}
if (sc == SDL_SCANCODE_DELETE) {
if (objectPlacer_.getSelected()) {
objectPlacer_.deleteSelected();
objectsDirty_ = true;
} else if (npcSpawner_.getSelected()) {
npcSpawner_.removeCreature(npcSpawner_.getSelectedIndex());
objectsDirty_ = true;
}
} }
if (sc == SDL_SCANCODE_Z && (event.key.keysym.mod & KMOD_CTRL)) { if (sc == SDL_SCANCODE_Z && (event.key.keysym.mod & KMOD_CTRL)) {
if (event.key.keysym.mod & KMOD_SHIFT) if (event.key.keysym.mod & KMOD_SHIFT)
@ -242,7 +260,7 @@ void EditorApp::processEvents() {
giz.endDrag(); giz.endDrag();
giz.setMode(TransformMode::None); giz.setMode(TransformMode::None);
} else if (objectPlacer_.getSelected()) { } else if (objectPlacer_.getSelected()) {
ImGui::OpenPopup("ObjectContextMenu"); openContextMenu_ = true;
} else { } else {
camera_.processMouseButton(event.button); camera_.processMouseButton(event.button);
} }

View file

@ -62,6 +62,8 @@ public:
void startGizmoMode(TransformMode mode); void startGizmoMode(TransformMode mode);
void setGizmoAxis(TransformAxis axis); void setGizmoAxis(TransformAxis axis);
TransformGizmo& getGizmo() { return viewport_.getGizmo(); } TransformGizmo& getGizmo() { return viewport_.getGizmo(); }
bool shouldOpenContextMenu() const { return openContextMenu_; }
void clearContextMenuFlag() { openContextMenu_ = false; }
float getWaterHeight() const { return waterHeight_; } float getWaterHeight() const { return waterHeight_; }
void setWaterHeight(float h) { waterHeight_ = h; } void setWaterHeight(float h) { waterHeight_ = h; }
@ -92,6 +94,7 @@ private:
bool imguiInitialized_ = false; bool imguiInitialized_ = false;
bool painting_ = false; bool painting_ = false;
bool objectsDirty_ = false; bool objectsDirty_ = false;
bool openContextMenu_ = false;
size_t lastObjectCount_ = 0; size_t lastObjectCount_ = 0;
EditorMode mode_ = EditorMode::Sculpt; EditorMode mode_ = EditorMode::Sculpt;
float waterHeight_ = 100.0f; float waterHeight_ = 100.0f;

View file

@ -592,6 +592,10 @@ void EditorUI::renderWaterPanel(EditorApp& app) {
} }
void EditorUI::renderContextMenu(EditorApp& app) { void EditorUI::renderContextMenu(EditorApp& app) {
if (app.shouldOpenContextMenu()) {
ImGui::OpenPopup("ObjectContextMenu");
app.clearContextMenuFlag();
}
if (ImGui::BeginPopup("ObjectContextMenu")) { if (ImGui::BeginPopup("ObjectContextMenu")) {
auto* sel = app.getObjectPlacer().getSelected(); auto* sel = app.getObjectPlacer().getSelected();
if (!sel) { ImGui::EndPopup(); return; } if (!sel) { ImGui::EndPopup(); return; }

View file

@ -133,11 +133,19 @@ bool TerrainEditor::raycastTerrain(const rendering::Ray& ray, glm::vec3& hitPos)
const auto& chunk = terrain_->chunks[chunkIdx]; const auto& chunk = terrain_->chunks[chunkIdx];
if (!chunk.hasHeightMap()) continue; if (!chunk.hasHeightMap()) continue;
// Quick AABB check: compute chunk bounds in render space // Quick AABB check using actual vertex extent
glm::vec3 corner0 = chunkVertexWorldPos(chunkIdx, 0); glm::vec3 corner0 = chunkVertexWorldPos(chunkIdx, 0);
glm::vec3 corner1 = chunkVertexWorldPos(chunkIdx, 144); glm::vec3 corner1 = chunkVertexWorldPos(chunkIdx, 144);
glm::vec3 minB = glm::min(corner0, corner1) - glm::vec3(0, 0, 200); glm::vec3 minB = glm::min(corner0, corner1);
glm::vec3 maxB = glm::max(corner0, corner1) + glm::vec3(0, 0, 200); glm::vec3 maxB = glm::max(corner0, corner1);
// Expand Z by actual height range in chunk
float minH = chunk.heightMap.heights[0], maxH = minH;
for (int h = 1; h < 145; h++) {
minH = std::min(minH, chunk.heightMap.heights[h]);
maxH = std::max(maxH, chunk.heightMap.heights[h]);
}
minB.z = chunk.position[2] + minH - 10.0f;
maxB.z = chunk.position[2] + maxH + 10.0f;
// Simple AABB-ray test // Simple AABB-ray test
float tmin = -1e30f, tmax = 1e30f; float tmin = -1e30f, tmax = 1e30f;