refactor(editor): multi-object move support + more handle jank refactor

This commit is contained in:
maelstrom 2025-05-27 03:13:49 +02:00
parent 49f29b6af1
commit 18b12ea1ad
6 changed files with 229 additions and 55 deletions

View file

@ -5,6 +5,7 @@ uniform vec3 scale;
in vec3 vPos; in vec3 vPos;
out vec4 fColor; out vec4 fColor;
uniform vec3 color;
void main() { void main() {
// float thickness = 0.2; // float thickness = 0.2;
@ -20,5 +21,6 @@ void main() {
// else // else
// fColor = vec4(0); // fColor = vec4(0);
fColor = vec4(0.204, 0.584, 0.922, 1); // fColor = vec4(0.204, 0.584, 0.922, 1);
fColor = vec4(color, 1);
} }

86
core/src/partassembly.cpp Normal file
View file

@ -0,0 +1,86 @@
#include "partassembly.h"
#include "common.h"
#include "datatypes/cframe.h"
#include "datatypes/vector.h"
#include "math_helper.h"
#include "objects/base/instance.h"
#include "objects/part.h"
#include <glm/common.hpp>
#include <memory>
#include <vector>
PartAssembly::PartAssembly(std::vector<std::shared_ptr<Part>> parts, bool worldMode) : parts(parts) {
if (parts.size() == 0) return;
if (parts.size() == 1 && !worldMode) {
_assemblyOrigin = parts[0]->cframe;
_bounds = parts[0]->size;
return;
}
glm::vec3 min = parts[0]->position(), max = parts[0]->position();
for (auto part : parts) {
Vector3 aabbSize = part->GetAABB();
expandAABB(min, max, part->position() - aabbSize / 2.f);
expandAABB(min, max, part->position() + aabbSize / 2.f);
}
glm::vec3 pos, size;
getAABBCoords(pos, size, min, max);
_assemblyOrigin = CFrame() + pos;
_bounds = size;
}
PartAssembly PartAssembly::FromSelection(std::vector<std::weak_ptr<Instance>> newSelection) {
std::vector<std::shared_ptr<Part>> selection;
for (std::weak_ptr<Instance> obj : newSelection) {
if (obj.expired() || !obj.lock()->IsA<Part>()) continue;
selection.push_back(obj.lock()->CastTo<Part>().expect());
}
return PartAssembly(selection, editorToolHandles.worldMode);
}
void PartAssembly::SetOrigin(CFrame newOrigin) {
for (auto part : parts) {
part->cframe = newOrigin * (_assemblyOrigin.Inverse() * part->cframe);
part->UpdateProperty("CFrame");
}
_assemblyOrigin = newOrigin;
}
void PartAssembly::TransformBy(CFrame transform) {
for (auto part : parts) {
part->cframe = transform * part->cframe;
part->UpdateProperty("CFrame");
}
_assemblyOrigin = transform * _assemblyOrigin;
}
void PartAssembly::Scale(Vector3 newSize, bool scaleUp) {
if (parts.size() == 1) {
parts[0]->size = newSize;
parts[0]->UpdateProperty("Size");
_bounds = newSize;
return;
}
float sx = newSize.X() / _bounds.X(), sy = newSize.Y() / _bounds.Y(), sz = newSize.Z() / _bounds.Z();
float factor = scaleUp ? glm::max(sx, sy, sz) : glm::min(sx, sy, sz);
for (auto part : parts) {
Vector3 localOff = _assemblyOrigin.Inverse() * part->cframe.Position();
localOff = localOff * factor;
part->cframe = part->cframe.Rotation() + _assemblyOrigin * localOff;
part->UpdateProperty("CFrame");
part->size *= factor;
part->UpdateProperty("Size");
}
_bounds = _bounds * factor;
}

35
core/src/partassembly.h Normal file
View file

@ -0,0 +1,35 @@
#pragma once
#include "datatypes/cframe.h"
#include <memory>
#include <vector>
class Part;
class Instance;
const std::vector<std::weak_ptr<Instance>> getSelection();
class PartAssembly {
CFrame _assemblyOrigin;
Vector3 _bounds;
std::vector<std::shared_ptr<Part>> parts;
public:
PartAssembly(std::vector<std::shared_ptr<Part>>, bool worldMode = false);
static PartAssembly FromSelection(std::vector<std::weak_ptr<Instance>> selection = getSelection());
inline CFrame assemblyOrigin() { return _assemblyOrigin; };
inline Vector3 bounds() { return _bounds; };
// Transforms the assembly such that newOrigin is now this assembly's new assemblyOrigin
void SetOrigin(CFrame newOrigin);
// Rotates and translates the assembly by the transformation
void TransformBy(CFrame transform);
// Scales the assembly to the desired size
// If multiple parts are selected, finds the greatest scale factor of each component pair, and
// scales it up by that amount
void Scale(Vector3 newSize, bool scaleUp = true);
};

View file

@ -16,8 +16,10 @@
#include "datatypes/cframe.h" #include "datatypes/cframe.h"
#include "datatypes/color3.h" #include "datatypes/color3.h"
#include "datatypes/vector.h"
#include "handles.h" #include "handles.h"
#include "math_helper.h" #include "math_helper.h"
#include "partassembly.h"
#include "rendering/torus.h" #include "rendering/torus.h"
#include "shader.h" #include "shader.h"
#include "mesh.h" #include "mesh.h"
@ -442,10 +444,11 @@ void renderOutlines() {
outlineShader->set("viewPos", camera.cameraPos); outlineShader->set("viewPos", camera.cameraPos);
outlineShader->set("thickness", 0.4f); outlineShader->set("thickness", 0.4f);
// outlineShader->set("color", glm::vec3(1.f, 0.f, 0.f)); outlineShader->set("color", glm::vec3(0.204, 0.584, 0.922));
glm::vec3 min, max; glm::vec3 min, max;
bool first = true; bool first = true;
int count = 0;
for (auto it = gWorkspace()->GetDescendantsStart(); it != gWorkspace()->GetDescendantsEnd(); it++) { for (auto it = gWorkspace()->GetDescendantsStart(); it != gWorkspace()->GetDescendantsEnd(); it++) {
InstanceRef inst = *it; InstanceRef inst = *it;
@ -453,6 +456,7 @@ void renderOutlines() {
std::shared_ptr<Part> part = std::dynamic_pointer_cast<Part>(inst); std::shared_ptr<Part> part = std::dynamic_pointer_cast<Part>(inst);
if (!part->selected) continue; if (!part->selected) continue;
count++;
if (first) if (first)
min = part->position(), max = part->position(); min = part->position(), max = part->position();
first = false; first = false;
@ -471,7 +475,7 @@ void renderOutlines() {
} }
// Render AABB of selected parts // Render AABB of selected parts
if (first) return; if (count <= 1) return;
glm::vec3 outlineSize, outlinePos; glm::vec3 outlineSize, outlinePos;
outlineSize = (max - min); outlineSize = (max - min);
@ -487,6 +491,41 @@ void renderOutlines() {
glDrawArrays(GL_TRIANGLES, 0, OUTLINE_MESH->vertexCount); glDrawArrays(GL_TRIANGLES, 0, OUTLINE_MESH->vertexCount);
} }
void renderSelectionAssembly() {
glDepthMask(GL_TRUE);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glFrontFace(GL_CCW);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
PartAssembly selectionAssembly = PartAssembly::FromSelection();
// Use shader
outlineShader->use();
// view/projection transformations
glm::mat4 projection = glm::perspective(glm::radians(45.f), (float)viewportWidth / (float)viewportHeight, 0.1f, 1000.0f);
glm::mat4 view = camera.getLookAt();
outlineShader->set("projection", projection);
outlineShader->set("view", view);
// Pass in the camera position
outlineShader->set("viewPos", camera.cameraPos);
outlineShader->set("thickness", 0.4f);
outlineShader->set("color", glm::vec3(1.f, 0.f, 0.f));
glm::mat4 model = selectionAssembly.assemblyOrigin();
model = glm::scale(model, (glm::vec3)selectionAssembly.bounds() + glm::vec3(0.1));
outlineShader->set("model", model);
outlineShader->set("scale", (glm::vec3)selectionAssembly.bounds() + glm::vec3(0.05));
outlineShader->set("thickness", 0.2f);
OUTLINE_MESH->bind();
glDrawArrays(GL_TRIANGLES, 0, OUTLINE_MESH->vertexCount);
}
void renderRotationArcs() { void renderRotationArcs() {
if (!editorToolHandles.active || editorToolHandles.handlesType != HandlesType::RotateHandles) return; if (!editorToolHandles.active || editorToolHandles.handlesType != HandlesType::RotateHandles) return;
@ -599,6 +638,7 @@ void render(GLFWwindow* window) {
renderParts(); renderParts();
renderSurfaceExtras(); renderSurfaceExtras();
renderOutlines(); renderOutlines();
// renderSelectionAssembly();
renderRotationArcs(); renderRotationArcs();
if (wireframeRendering) if (wireframeRendering)
renderWireframe(); renderWireframe();

View file

@ -5,12 +5,14 @@
#include <qsoundeffect.h> #include <qsoundeffect.h>
#include <string> #include <string>
#include "mainglwidget.h" #include "mainglwidget.h"
#include "datatypes/vector.h"
#include "handles.h" #include "handles.h"
#include "logger.h" #include "logger.h"
#include "mainwindow.h" #include "mainwindow.h"
#include "common.h" #include "common.h"
#include "math_helper.h" #include "math_helper.h"
#include "objects/base/instance.h" #include "objects/base/instance.h"
#include "partassembly.h"
#include "physics/util.h" #include "physics/util.h"
#include "rendering/renderer.h" #include "rendering/renderer.h"
#include "rendering/shader.h" #include "rendering/shader.h"
@ -171,6 +173,7 @@ inline glm::vec3 vec3fy(glm::vec4 vec) {
} }
// Taken from Godot's implementation of moving handles (godot/editor/plugins/gizmos/gizmo_3d_helper.cpp) // Taken from Godot's implementation of moving handles (godot/editor/plugins/gizmos/gizmo_3d_helper.cpp)
Vector3 dragStartHandleOffset;
void MainGLWidget::handleLinearTransform(QMouseEvent* evt) { void MainGLWidget::handleLinearTransform(QMouseEvent* evt) {
if (!isMouseDragging || !draggingHandle|| !editorToolHandles.active) return; if (!isMouseDragging || !draggingHandle|| !editorToolHandles.active) return;
@ -183,7 +186,9 @@ void MainGLWidget::handleLinearTransform(QMouseEvent* evt) {
glm::vec3 pointDir = camera.getScreenDirection(glm::vec2(position.x(), position.y()), glm::vec2(width(), height())); glm::vec3 pointDir = camera.getScreenDirection(glm::vec2(position.x(), position.y()), glm::vec2(width(), height()));
pointDir = glm::normalize(pointDir); pointDir = glm::normalize(pointDir);
CFrame handleCFrame = getHandleCFrame(draggingHandle.value()); // We use lastDragStartPos instead to consider the mouse's actual position, rather than the center
// of the handle. That way, transformations are "smoother" and do not jump the first movement
CFrame handleCFrame = getHandleCFrame(draggingHandle.value()) + dragStartHandleOffset;
// Current frame. Identity frame if worldMode == true, selected object's frame if worldMode == false // Current frame. Identity frame if worldMode == true, selected object's frame if worldMode == false
CFrame frame = editorToolHandles.worldMode ? CFrame::IDENTITY + part->position() : part->cframe.Rotation(); CFrame frame = editorToolHandles.worldMode ? CFrame::IDENTITY + part->position() : part->cframe.Rotation();
@ -200,60 +205,64 @@ void MainGLWidget::handleLinearTransform(QMouseEvent* evt) {
glm::vec3 handlePoint, rb; glm::vec3 handlePoint, rb;
get_closest_points_between_segments(axisSegment0, axisSegment1, mouseSegment0, mouseSegment1, handlePoint, rb); get_closest_points_between_segments(axisSegment0, axisSegment1, mouseSegment0, mouseSegment1, handlePoint, rb);
// Find new part position // We transform the handlePoint to the handle's cframe, and get it's length (Z)
glm::vec3 centerPoint = partCFrameFromHandlePos(draggingHandle.value(), handlePoint).Position(); float diff = (handleCFrame.Inverse() * handlePoint).Z();
// Vector3 absDiff = ((Vector3)handlePoint - handleCFrame.Position()); // Commented out because it is functionally identical to the below
Vector3 absDiff = handleCFrame.Rotation() * Vector3(0, 0, diff);
// Apply snapping in the current frame // Apply snapping
glm::vec3 diff = centerPoint - (glm::vec3)part->position(); if (snappingFactor() > 0) {
if (snappingFactor()) diff = frame.Rotation() * (glm::round(glm::vec3(frame.Inverse().Rotation() * diff) / snappingFactor()) * snappingFactor()); diff = round(diff / snappingFactor()) * snappingFactor();
absDiff = handleCFrame.Rotation() * Vector3(0, 0, diff);
Vector3 oldSize = part->size;
switch (mainWindow()->selectedTool) {
case TOOL_MOVE: {
// Add difference
part->cframe = part->cframe + diff;
} break;
case TOOL_SCALE: {
// Find local difference
glm::vec3 localDiff = frame.Inverse() * diff;
// Find outwarwd difference
localDiff = localDiff * glm::sign(draggingHandle->normal);
// Special case: minimum size to size mod snapping factor
if (snappingFactor() > 0 && glm::all(glm::lessThan((part->size + localDiff) * glm::abs(draggingHandle->normal), glm::vec3(0.01f)))) {
// I tried something fancy here, but honestly I'm not smart enough. Return;
// glm::vec3 finalSize = part->size + localDiff;
// finalSize = glm::mod(finalSize * glm::abs(draggingHandle->normal), snappingFactor()) + finalSize * (glm::vec3(1) - glm::abs(draggingHandle->normal));
// localDiff = finalSize - part->size;
return;
}
// Minimum size of 0.01f
localDiff = glm::max(part->size + localDiff, 0.01f) - part->size;
diff = frame * (localDiff * glm::sign(draggingHandle->normal));
// Add local difference to size
part->size += localDiff;
// If ctrl is not pressed, offset the part by half the size difference to keep the other bound where it was originally
if (!(evt->modifiers() & Qt::ControlModifier))
part->cframe = part->cframe + diff * 0.5f;
} break;
default:
Logger::error("Invalid tool was set to be handled by handleLinearTransform\n");
} }
if (snappingFactor() != 0 && mainWindow()->editSoundEffects && (oldSize != part->size) && QFile::exists("./assets/excluded/switch.wav")) PartAssembly selectionAssembly = PartAssembly::FromSelection();
playSound("./assets/excluded/switch.wav");
gWorkspace()->SyncPartPhysics(part); if (editorToolHandles.handlesType == MoveHandles) {
part->UpdateProperty("Position"); selectionAssembly.TransformBy(CFrame() + absDiff);
part->UpdateProperty("Size"); } else if (editorToolHandles.handlesType == ScaleHandles) {
sendPropertyUpdatedSignal(part, "Position", part->position()); if (evt->modifiers() & Qt::AltModifier) {
sendPropertyUpdatedSignal(part, "Size", Vector3(part->size)); // If size gets too small, don't
if (glm::any(glm::lessThan(glm::vec3(selectionAssembly.bounds() + abs(draggingHandle->normal) * diff * 2.f), glm::vec3(0.001f))))
return;
selectionAssembly.Scale(selectionAssembly.bounds() + abs(draggingHandle->normal) * diff * 2.f, diff > 0);
} else {
// If size gets too small, don't
if (glm::any(glm::lessThan(glm::vec3(selectionAssembly.bounds() + abs(draggingHandle->normal) * diff), glm::vec3(0.001f))))
return;
selectionAssembly.TransformBy(CFrame() + absDiff * 0.5f);
selectionAssembly.Scale(selectionAssembly.bounds() + abs(draggingHandle->normal) * diff, diff > 0);
}
}
}
void MainGLWidget::startLinearTransform(QMouseEvent* evt) {
if (!editorToolHandles.active) return;
QPoint position = evt->pos();
// This was actually quite a difficult problem to solve, managing to get the handle to go underneath the cursor
glm::vec3 pointDir = camera.getScreenDirection(glm::vec2(position.x(), position.y()), glm::vec2(width(), height()));
pointDir = glm::normalize(pointDir);
CFrame handleCFrame = getHandleCFrame(draggingHandle.value());
// Segment from axis stretching -4096 to +4096 rel to handle's position
glm::vec3 axisSegment0 = handleCFrame.Position() + (-handleCFrame.LookVector() * 4096.0f);
glm::vec3 axisSegment1 = handleCFrame.Position() + (-handleCFrame.LookVector() * -4096.0f);
// Segment from camera stretching 4096 forward
glm::vec3 mouseSegment0 = camera.cameraPos;
glm::vec3 mouseSegment1 = camera.cameraPos + pointDir * 4096.0f;
// Closest point on the axis segment between the two segments
glm::vec3 handlePoint, rb;
get_closest_points_between_segments(axisSegment0, axisSegment1, mouseSegment0, mouseSegment1, handlePoint, rb);
dragStartHandleOffset = (Vector3)handlePoint - handleCFrame.Position();
} }
// Also implemented based on Godot: [c7ea8614](godot/editor/plugins/canvas_item_editor_plugin.cpp#L1490) // Also implemented based on Godot: [c7ea8614](godot/editor/plugins/canvas_item_editor_plugin.cpp#L1490)
@ -369,6 +378,7 @@ void MainGLWidget::mousePressEvent(QMouseEvent* evt) {
initialFrame = getHandleAdornee()->cframe; initialFrame = getHandleAdornee()->cframe;
isMouseDragging = true; isMouseDragging = true;
draggingHandle = handle; draggingHandle = handle;
startLinearTransform(evt);
return; return;
} }
@ -501,4 +511,4 @@ float MainGLWidget::snappingFactor() {
case GridSnappingMode::SNAP_OFF: return 0; case GridSnappingMode::SNAP_OFF: return 0;
} }
return 0; return 0;
} }

View file

@ -26,6 +26,7 @@ protected:
void handleLinearTransform(QMouseEvent* evt); void handleLinearTransform(QMouseEvent* evt);
void handleRotationalTransform(QMouseEvent* evt); void handleRotationalTransform(QMouseEvent* evt);
void handleCursorChange(QMouseEvent* evt); void handleCursorChange(QMouseEvent* evt);
void startLinearTransform(QMouseEvent* evt);
std::optional<HandleFace> raycastHandle(glm::vec3 pointDir); std::optional<HandleFace> raycastHandle(glm::vec3 pointDir);
void wheelEvent(QWheelEvent* evt) override; void wheelEvent(QWheelEvent* evt) override;