From 1d6c931b86762a9bdc4f99461c17846330318bd7 Mon Sep 17 00:00:00 2001 From: maelstrom Date: Sun, 12 Oct 2025 19:23:34 +0200 Subject: [PATCH] feat(editor): selection lasso --- core/src/objects/service/workspace.cpp | 15 +++ core/src/objects/service/workspace.h | 2 + core/src/rendering/frustum.cpp | 31 +++++ core/src/rendering/frustum.h | 4 + core/src/rendering/renderer.cpp | 9 ++ editor/mainglwidget.cpp | 169 ++++++++++++++----------- editor/mainglwidget.h | 13 +- 7 files changed, 164 insertions(+), 79 deletions(-) diff --git a/core/src/objects/service/workspace.cpp b/core/src/objects/service/workspace.cpp index 693d6c0..cc11bf2 100644 --- a/core/src/objects/service/workspace.cpp +++ b/core/src/objects/service/workspace.cpp @@ -66,4 +66,19 @@ void Workspace::PhysicsStep(float deltaTime) { parent->Destroy(); } } +} + +std::vector> Workspace::CastFrustum(Frustum frustum) { + std::vector> parts; + + for (auto it = GetDescendantsStart(); it != GetDescendantsEnd(); it++) { + if (!it->IsA()) continue; + std::shared_ptr part = std::dynamic_pointer_cast(*it); + + if (!part->locked && frustum.checkAABB(part->position(), part->GetAABB())) { + parts.push_back(part); + } + } + + return parts; } \ No newline at end of file diff --git a/core/src/objects/service/workspace.h b/core/src/objects/service/workspace.h index 3049413..168cfbe 100644 --- a/core/src/objects/service/workspace.h +++ b/core/src/objects/service/workspace.h @@ -4,6 +4,7 @@ #include "objects/base/service.h" #include "objects/joint/jointinstance.h" #include "physics/world.h" +#include "rendering/frustum.h" #include #include #include @@ -61,4 +62,5 @@ public: void PhysicsStep(float deltaTime); inline std::optional CastRayNearest(glm::vec3 point, glm::vec3 rotation, float maxLength, std::optional filter = std::nullopt, unsigned short categoryMaskBits = 0xFFFF) { return physicsWorld->castRay(point, rotation, maxLength, filter, categoryMaskBits); } + std::vector> CastFrustum(Frustum frustum); }; \ No newline at end of file diff --git a/core/src/rendering/frustum.cpp b/core/src/rendering/frustum.cpp index bc8419d..fcf097b 100644 --- a/core/src/rendering/frustum.cpp +++ b/core/src/rendering/frustum.cpp @@ -7,6 +7,8 @@ // https://stackoverflow.com/q/66227192/16255372 FrustumPlane::FrustumPlane(Vector3 point, Vector3 normal) : normal(normal.Unit()), distance(normal.Unit().Dot(point)) {} +Frustum::Frustum() {} + Frustum::Frustum(const Camera cam, float aspect, float fovY, float zNear, float zFar) { const float halfVSide = zFar * tanf(fovY * 0.5f); const float halfHSide = halfVSide * aspect; @@ -27,6 +29,35 @@ Frustum::Frustum(const Camera cam, float aspect, float fovY, float zNear, float glm::cross(frontMultFar + trueCamUp * halfVSide, camRight) }; } +Frustum Frustum::createSliced(const Camera cam, float width, float height, float left, float right, float top, float bottom, float fovY, float zNear, float zFar) { + Frustum frustum; + + float aspect = width / height; + float halfVSide = zFar * tanf(fovY * 0.5f); + float halfHSide = halfVSide * aspect; + const glm::vec3 frontMultFar = zFar * -cam.cameraFront; + + float leftSide = -halfHSide * (left / width * 2 - 1); + float rightSide = halfHSide * (right / width * 2 - 1); + float topSide = -halfVSide * (top / height * 2 - 1); + float bottomSide = halfVSide * (bottom / height * 2 - 1); + + // Don't forget to normalize!!! + glm::vec3 camRight = glm::normalize(glm::cross(cam.cameraFront, cam.cameraUp)); // Technically this is left, but whatever + glm::vec3 trueCamUp = glm::cross(-cam.cameraFront, camRight); + frustum.near = { cam.cameraPos + zNear * -cam.cameraFront, -cam.cameraFront }; + frustum.far = { cam.cameraPos + frontMultFar, cam.cameraFront }; + frustum.right = { cam.cameraPos, + glm::cross(frontMultFar - camRight * rightSide, trueCamUp) }; + frustum.left = { cam.cameraPos, + glm::cross(trueCamUp,frontMultFar + camRight * leftSide) }; + frustum.top = { cam.cameraPos, + glm::cross(camRight, frontMultFar - trueCamUp * topSide) }; + frustum.bottom = { cam.cameraPos, + glm::cross(frontMultFar + trueCamUp * bottomSide, camRight) }; + return frustum; +} + bool FrustumPlane::checkPointForward(Vector3 point) { return (normal.Dot(point) - distance) > 0; } diff --git a/core/src/rendering/frustum.h b/core/src/rendering/frustum.h index 2c2e737..48477ee 100644 --- a/core/src/rendering/frustum.h +++ b/core/src/rendering/frustum.h @@ -26,6 +26,10 @@ struct Frustum { FrustumPlane bottom; Frustum(const Camera cam, float aspect, float fovY, float zNear, float zFar); + static Frustum createSliced(const Camera cam, float width, float height, float left, float right, float top, float bottom, float fovY, float zNear, float zFar); bool checkPoint(Vector3); bool checkAABB(Vector3 center, Vector3 extents); + +private: + Frustum(); }; \ No newline at end of file diff --git a/core/src/rendering/renderer.cpp b/core/src/rendering/renderer.cpp index f35b082..58d4a14 100644 --- a/core/src/rendering/renderer.cpp +++ b/core/src/rendering/renderer.cpp @@ -666,6 +666,8 @@ void render() { glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnable(GL_DEPTH_TEST); + // For some reason this is unset by QPainter, so we override it here + glEnable(GL_MULTISAMPLE); renderSkyBox(); renderHandles(); @@ -684,6 +686,13 @@ void render() { // renderAABB(); renderTime = tu_clock_micros() - startTime; + + identityShader->use(); + identityShader->set("aColor", glm::vec4(1,0,0,1)); + // Unbinding both is important or else it will mess up QPainter + // https://stackoverflow.com/a/47417780/16255372 + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER,0); } void drawRect(int x, int y, int width, int height, glm::vec4 color) { diff --git a/editor/mainglwidget.cpp b/editor/mainglwidget.cpp index f079544..61fb7f8 100755 --- a/editor/mainglwidget.cpp +++ b/editor/mainglwidget.cpp @@ -1,29 +1,17 @@ #include -#include -#include #include #include #include -#include -#include -#include +#include + #include "./ui_mainwindow.h" -#include "mainglwidget.h" -#include "datatypes/vector.h" -#include "enum/surface.h" -#include "handles.h" -#include "logger.h" -#include "mainwindow.h" #include "common.h" +#include "mainwindow.h" #include "math_helper.h" -#include "objects/base/instance.h" -#include "objects/pvinstance.h" #include "objects/service/selection.h" #include "partassembly.h" #include "rendering/renderer.h" -#include "rendering/shader.h" -#include "datatypes/variant.h" -#include "undohistory.h" +#include "mainglwidget.h" #define PI 3.14159 #define M_mainWindow dynamic_cast(window()) @@ -68,9 +56,16 @@ glm::vec2 secondPoint; extern std::weak_ptr draggingObject; extern std::optional draggingHandle; -extern Shader* shader; void MainGLWidget::paintGL() { + QPainter painter(this); + + painter.beginNativePainting(); ::render(); + painter.endNativePainting(); + + painter.setPen(QColor(200, 200, 200)); + painter.drawRect(selectionLasso); + painter.end(); } bool isMouseRightDragging = false; @@ -326,7 +321,7 @@ std::optional MainGLWidget::raycastHandle(glm::vec3 pointDir) { } void MainGLWidget::handleCursorChange(QMouseEvent* evt) { - if (isMouseRightDragging) return; // Don't change the cursor while it is intentionally blank + if (isMouseRightDragging || selectionLasso != QRect{0,0,0,0}) return; // Don't change the cursor while it is intentionally blank QPoint position = evt->pos(); glm::vec3 pointDir = camera.getScreenDirection(glm::vec2(position.x(), position.y()), glm::vec2(width(), height())); @@ -369,6 +364,85 @@ void MainGLWidget::mouseMoveEvent(QMouseEvent* evt) { default: break; } + + if (selectionLasso != QRect {0,0,0,0}) { + selectionLasso = {selectionLasso.topLeft(), evt->pos()}; + + float left = std::min(selectionLasso.left(), selectionLasso.right()); + float right = std::max(selectionLasso.left(), selectionLasso.right()); + float top = std::min(selectionLasso.top(), selectionLasso.bottom()); + float bottom = std::max(selectionLasso.top(), selectionLasso.bottom()); + + Frustum selectionFrustum = Frustum::createSliced(camera, width(), height(), left, right, top, bottom, glm::radians(45.f), 0.1f, 1000.0f); + + std::vector> castedParts = gWorkspace()->CastFrustum(selectionFrustum); + gDataModel->GetService()->Set(castedParts); + } +} + +bool MainGLWidget::handlePartClick(QMouseEvent* evt) { + QPoint position = evt->pos(); + glm::vec3 pointDir = camera.getScreenDirection(glm::vec2(position.x(), position.y()), glm::vec2(width(), height())); + + // raycast part + std::shared_ptr selection = gDataModel->GetService(); + std::optional rayHit = gWorkspace()->CastRayNearest(camera.cameraPos, pointDir, 50000); + if (!rayHit || !rayHit->hitPart) { selection->Set({}); return false; } + std::shared_ptr part = rayHit->hitPart; + if (part->locked) { selection->Set({}); return false; } + + std::shared_ptr selObject = part; + + // Traverse to the root model + if (~evt->modifiers() & Qt::AltModifier) { + nullable std::shared_ptr nextParent = selObject->GetParent(); + while (nextParent && nextParent->IsA("Model")) { + selObject = std::dynamic_pointer_cast(nextParent); nextParent = selObject->GetParent(); + } + } + + initialAssembly = PartAssembly::FromSelection({selObject}); + initialFrame = initialAssembly.assemblyOrigin(); + initialHitPos = rayHit->worldPoint; + initialHitNormal = rayHit->worldNormal; + + // Handle surface tool + if (mainWindow()->selectedTool >= TOOL_SMOOTH) { + Vector3 localNormal = part->cframe.Inverse().Rotation() * rayHit->worldNormal; + NormalId face = faceFromNormal(localNormal); + SurfaceType surface = SurfaceType(mainWindow()->selectedTool - TOOL_SMOOTH); + std::string surfacePropertyName = EnumType::NormalId.FromValue(face)->Name() + "Surface"; + + // Get old surface and set new surface + EnumItem newSurface = EnumType::SurfaceType.FromValue((int)surface).value(); + EnumItem oldSurface = part->GetProperty(surfacePropertyName).expect().get(); + part->SetProperty(surfacePropertyName, newSurface).expect(); + + M_mainWindow->undoManager.PushState({UndoStatePropertyChanged { part, surfacePropertyName, oldSurface, newSurface }}); + + if (mainWindow()->editSoundEffects && QFile::exists("./assets/excluded/electronicpingshort.wav")) + playSound("./assets/excluded/electronicpingshort.wav"); + + return true; + } + + //part.selected = true; + isMouseDragging = true; + draggingObject = part; + initialTransforms = PartAssembly::FromSelection({part}).GetCurrentTransforms(); + if (evt->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier)) { + auto sel = selection->Get(); + if (std::find(sel.begin(), sel.end(), selObject) == sel.end()) + selection->Add({ selObject }); + else + selection->Remove({ selObject }); + } else { + selection->Set({ selObject }); + } + // Disable bit so that we can ignore the part while raycasting + // part->rigidBody->getCollider(0)->setCollisionCategoryBits(0b10); + + return true; } void MainGLWidget::mousePressEvent(QMouseEvent* evt) { @@ -400,63 +474,9 @@ void MainGLWidget::mousePressEvent(QMouseEvent* evt) { return; } - // raycast part - std::shared_ptr selection = gDataModel->GetService(); - std::optional rayHit = gWorkspace()->CastRayNearest(camera.cameraPos, pointDir, 50000); - if (!rayHit || !rayHit->hitPart) { selection->Set({}); return; } - std::shared_ptr part = rayHit->hitPart; - if (part->locked) { selection->Set({}); return; } + if (handlePartClick(evt)) return; - std::shared_ptr selObject = part; - - // Traverse to the root model - if (~evt->modifiers() & Qt::AltModifier) { - nullable std::shared_ptr nextParent = selObject->GetParent(); - while (nextParent && nextParent->IsA("Model")) { - selObject = std::dynamic_pointer_cast(nextParent); nextParent = selObject->GetParent(); - } - } - - initialAssembly = PartAssembly::FromSelection({selObject}); - initialFrame = initialAssembly.assemblyOrigin(); - initialHitPos = rayHit->worldPoint; - initialHitNormal = rayHit->worldNormal; - - // Handle surface tool - if (mainWindow()->selectedTool >= TOOL_SMOOTH) { - Vector3 localNormal = part->cframe.Inverse().Rotation() * rayHit->worldNormal; - NormalId face = faceFromNormal(localNormal); - SurfaceType surface = SurfaceType(mainWindow()->selectedTool - TOOL_SMOOTH); - std::string surfacePropertyName = EnumType::NormalId.FromValue(face)->Name() + "Surface"; - - // Get old surface and set new surface - EnumItem newSurface = EnumType::SurfaceType.FromValue((int)surface).value(); - EnumItem oldSurface = part->GetProperty(surfacePropertyName).expect().get(); - part->SetProperty(surfacePropertyName, newSurface).expect(); - - M_mainWindow->undoManager.PushState({UndoStatePropertyChanged { part, surfacePropertyName, oldSurface, newSurface }}); - - if (mainWindow()->editSoundEffects && QFile::exists("./assets/excluded/electronicpingshort.wav")) - playSound("./assets/excluded/electronicpingshort.wav"); - - return; - } - - //part.selected = true; - isMouseDragging = true; - draggingObject = part; - initialTransforms = PartAssembly::FromSelection({part}).GetCurrentTransforms(); - if (evt->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier)) { - auto sel = selection->Get(); - if (std::find(sel.begin(), sel.end(), selObject) == sel.end()) - selection->Add({ selObject }); - else - selection->Remove({ selObject }); - } else { - selection->Set({ selObject }); - } - // Disable bit so that we can ignore the part while raycasting - // part->rigidBody->getCollider(0)->setCollisionCategoryBits(0b10); + selectionLasso = {position, QSize {0, 0}}; return; } default: @@ -471,6 +491,7 @@ void MainGLWidget::mouseReleaseEvent(QMouseEvent* evt) { isMouseDragging = false; draggingObject = {}; draggingHandle = std::nullopt; + selectionLasso = {0,0,0,0}; setCursor(Qt::ArrowCursor); if (!initialTransforms.empty()) { diff --git a/editor/mainglwidget.h b/editor/mainglwidget.h index f59dffb..dd00acb 100644 --- a/editor/mainglwidget.h +++ b/editor/mainglwidget.h @@ -1,13 +1,13 @@ #ifndef MAINGLWIDGET_H #define MAINGLWIDGET_H -#include "objects/part/part.h" -#include "qevent.h" -#include -#include +#include #include -#include +#include +#include +#include +class BasePart; class HandleFace; class MainWindow; @@ -28,6 +28,7 @@ protected: void handleLinearTransform(QMouseEvent* evt); void handleRotationalTransform(QMouseEvent* evt); void handleCursorChange(QMouseEvent* evt); + bool handlePartClick(QMouseEvent* evt); void startLinearTransform(QMouseEvent* evt); std::optional raycastHandle(glm::vec3 pointDir); @@ -42,6 +43,8 @@ protected: MainWindow* mainWindow(); float snappingFactor(); + + QRect selectionLasso; }; #endif // MAINGLWIDGET_H