Compare commits

..

25 commits

Author SHA1 Message Date
maelstrom 086a2ca39f chore: remove unused dependencies 2025-03-12 22:50:09 +01:00
maelstrom 7e6261fcf3 chore(run.sh): use out-of-tree build 2025-03-12 02:11:44 +01:00
maelstrom f81964e395 fix(cmake): project not compiling under qt6 due to missing opengl widgets dependency 2025-03-12 02:01:17 +01:00
maelstrom bfaab69daa chore: add warning message when running run.sh without a parameter 2025-03-12 01:22:25 +01:00
maelstrom 75be686e48 fix(handles): glitchy movement while using move tool 2025-03-11 20:47:06 +01:00
maelstrom 45f39c3f49 fix: memory leak involving ExplorerModel 2025-03-11 20:42:50 +01:00
maelstrom 12a4ed76b4 feat(editor): export/import models 2025-03-10 23:03:24 +01:00
maelstrom 186d64f28e feat(clipboard): added copy/cut/paste 2025-03-08 22:16:21 +01:00
maelstrom 940109b0b9 fix(renderer): transparency color + handle anchors not rendering 2025-03-06 22:06:41 +01:00
maelstrom b80eb03f9d feat(renderer): buggy transparency 2025-03-05 23:55:38 +01:00
maelstrom 28ed11fb53 feat(datatype): float parser 2025-03-05 23:31:16 +01:00
maelstrom 23ac7ee634 feat(renderer): backface culling 2025-03-05 23:18:09 +01:00
maelstrom ac89dea966 fix: arrow handles 2025-03-05 22:52:13 +01:00
maelstrom 4c51ee6904 fix: buggy handles 2/2 (scaling and moving implemented) 2025-02-27 22:35:06 +01:00
maelstrom 5081e18ea7 fix: buggy handles 2025-02-27 22:22:59 +01:00
maelstrom 2895124778 feat: arrow handles 2025-02-26 22:31:58 +01:00
maelstrom a2fd9be6e3 fix: selecting items in explorer doesn't update the rest of the editor 2025-02-26 22:31:33 +01:00
maelstrom e22bb74850 feat: spherical handles + rendering limit increase 2025-02-25 23:31:09 +01:00
maelstrom e082428314 fix: fixed framerate for physics. might make it a little more stable 2025-02-25 09:28:31 +01:00
maelstrom a346b1d9a8 refactor: handle inactive when select tool is selected 2025-02-23 16:45:16 +01:00
maelstrom 4bfca68fb0 feat: grid snapping wrt/ scaling and moving 2025-02-23 16:29:38 +01:00
maelstrom 32964df4c3 fix: smoother scaling and moving 2025-02-23 13:41:28 +01:00
maelstrom fd1037d76a feat: scale caging 2025-02-18 22:56:06 +01:00
maelstrom 64fa46d496 feat: basic moving and resizing support 2025-02-18 22:50:16 +01:00
maelstrom 81d172900b chore: move core module into its own dedicated directory 2025-02-13 23:43:30 +01:00
83 changed files with 2506 additions and 242 deletions

11
.clangd
View file

@ -1,11 +0,0 @@
CompileFlags: # Tweak the parse settings, example directory given to show format
Add:
- "--include-directory=../../src"
- "--include-directory=../src"
- "--include-directory=./editor_autogen/include"
- "--include-directory=../.."
- "--include-directory=/usr/include/qt6/QtWidgets"
- "--include-directory=/usr/include/qt6/QtOpenGLWidgets"
- "--include-directory=/usr/include/qt6"
- "--include-directory=/usr/include/qt6/QtGui"
- "--include-directory=/usr/include/qt6/QtCore"

22
.gitignore vendored
View file

@ -1,12 +1,14 @@
bin/
lib/
CMakeFiles/
cmake_install.cmake
CMakeCache.txt
Makefile
/bin/
/lib/
/build/
# Qt
.lupdate/
*.pro.user*
CMakeLists.txt.user*
*_autogen/
/*.pro.user*
/CMakeLists.txt.user*
# Clangd
/compile_commands.json
/.cache
# Gdb
/.gdb_history

View file

@ -1,50 +1,14 @@
cmake_minimum_required(VERSION 3.5.0)
cmake_minimum_required(VERSION 3.31..)
set(CMAKE_CXX_STANDARD 17)
project(openblocks VERSION 0.1.0)
set(OpenGL_GL_PREFERENCE "GLVND")
set( CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin )
set( CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib )
set( CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/lib )
set( CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib )
SET(BASEPATH "${CMAKE_SOURCE_DIR}")
include_directories("${BASEPATH}")
include_directories("src")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(OpenGL REQUIRED COMPONENTS OpenGL)
find_package(SDL2 REQUIRED)
include_directories(${SDL2_INCLUDE_DIRS})
find_package(GLEW REQUIRED)
include_directories(${GLEW_INCLUDE_DIRS})
find_package(GLUT REQUIRED)
include_directories(${GLUT_INCLUDE_DIRS})
find_package(glfw3 REQUIRED)
find_package(OpenGL)
find_package(glm CONFIG REQUIRED)
find_package(assimp REQUIRED)
find_package(ReactPhysics3D REQUIRED)
find_package(pugixml REQUIRED)
# PkgConfig packages
# find_package(PkgConfig REQUIRED)
# pkg_check_modules(PUGIXML REQUIRED pugixml)
file(MAKE_DIRECTORY bin)
include_directories("include")
file(GLOB_RECURSE SOURCES "src/*.cpp" "src/*.h")
add_library(openblocks ${SOURCES})
set_target_properties(openblocks PROPERTIES OUTPUT_NAME "openblocks")
target_link_libraries(openblocks ${SDL2_LIBRARIES} ${GLEW_LIBRARIES} ${GLUT_LIBRARIES} ${PUGIXML_LIBRARIES} OpenGL::GL OpenGL::GLU glfw glm::glm assimp ReactPhysics3D::ReactPhysics3D pugixml::pugixml)
# add_executable(client "client/src/main.cpp" $<TARGET_OBJECTS:openblocks>)
# include_directories("src")
# target_link_libraries(client ${SDL2_LIBRARIES} ${GLEW_LIBRARIES} ${GLUT_LIBRARIES} OpenGL::GL OpenGL::GLU glfw glm::glm assimp ReactPhysics3D::ReactPhysics3D)
add_subdirectory("client")
add_subdirectory("editor")
add_subdirectory(core)
add_subdirectory(client)
add_subdirectory(editor)

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

109
assets/shaders/handle.fs Normal file
View file

@ -0,0 +1,109 @@
#version 330 core
// Implements the Phong lighting model with respect to materials and lighting materials
// Structs
struct Material {
vec3 diffuse;
vec3 specular;
float shininess;
};
struct DirLight {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
struct PointLight {
vec3 position;
// vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
// I/O
in vec3 vPos;
in vec3 vNormal;
in vec2 vTexCoords;
out vec4 FragColor;
#define NR_POINT_LIGHTS 4
uniform vec3 viewPos;
uniform PointLight pointLights[NR_POINT_LIGHTS];
uniform int numPointLights;
uniform DirLight sunLight;
uniform Material material;
// Functions
vec3 calculateDirectionalLight(DirLight light);
vec3 calculatePointLight(PointLight light);
// Main
void main() {
vec3 result = vec3(0.0);
result += calculateDirectionalLight(sunLight);
for (int i = 0; i < numPointLights; i++) {
result += calculatePointLight(pointLights[i]);
}
FragColor = vec4(result, 1);
}
vec3 calculateDirectionalLight(DirLight light) {
// Calculate diffuse
vec3 norm = normalize(vNormal);
vec3 lightDir = normalize(-light.direction);
float diff = max(dot(norm, lightDir), 0.0);
// Calculate specular
vec3 viewDir = normalize(viewPos - vPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 ambient = light.ambient * material.diffuse;
vec3 diffuse = light.diffuse * diff * material.diffuse;
vec3 specular = light.specular * spec * material.specular;
return (ambient + diffuse + specular);
}
vec3 calculatePointLight(PointLight light) {
// Calculate ambient light
// Calculate diffuse light
vec3 norm = normalize(vNormal);
vec3 lightDir = normalize(light.position - vPos);
float diff = max(dot(norm, lightDir), 0.0);
// Calculate specular
vec3 viewDir = normalize(viewPos - vPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// Calculate attenuation
float distance = length(light.position - vPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
vec3 ambient = light.ambient * material.diffuse;
vec3 diffuse = light.diffuse * diff * material.diffuse;
vec3 specular = light.specular * spec * material.specular;
return (ambient + diffuse + specular) * attenuation;
}

20
assets/shaders/handle.vs Normal file
View file

@ -0,0 +1,20 @@
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
out vec3 vPos;
out vec3 vNormal;
out vec2 vTexCoords;
uniform mat4 model;
uniform mat3 normalMatrix;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
vPos = vec3(model * vec4(aPos, 1.0));
vNormal = normalMatrix * aNormal;
}

View file

@ -0,0 +1,13 @@
#version 330 core
// I/O
out vec4 FragColor;
uniform vec3 aColor;
// Main
void main() {
FragColor = vec4(aColor, 1);
}

View file

@ -0,0 +1,8 @@
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 vPos;
void main()
{
gl_Position = vec4(aPos, 1.0);
}

View file

@ -53,6 +53,7 @@ uniform int numPointLights;
uniform DirLight sunLight;
uniform Material material;
uniform sampler2DArray studs;
uniform float transparency;
// Functions
@ -72,7 +73,7 @@ void main() {
}
vec4 studPx = texture(studs, vec3(vTexCoords, vSurfaceZ));
FragColor = vec4(mix(result, vec3(studPx), studPx.w), 1);
FragColor = vec4(mix(result, vec3(studPx), studPx.w), 1) * (1-transparency);
}
vec3 calculateDirectionalLight(DirLight light) {

View file

@ -1,4 +1,7 @@
find_package(SDL2 REQUIRED)
include_directories(${SDL2_INCLUDE_DIRS})
add_executable(client "src/main.cpp" $<TARGET_OBJECTS:openblocks>)
include_directories("../src")
target_link_libraries(client ${SDL2_LIBRARIES} ${GLEW_LIBRARIES} ${GLUT_LIBRARIES} OpenGL::GL OpenGL::GLU glfw glm::glm assimp ReactPhysics3D::ReactPhysics3D pugixml::pugixml)
find_package(glfw3 REQUIRED)
add_executable(client "src/main.cpp")
target_link_libraries(client PRIVATE ${SDL2_LIBRARIES} openblocks glfw)

15
core/CMakeLists.txt Normal file
View file

@ -0,0 +1,15 @@
find_package(OpenGL REQUIRED COMPONENTS OpenGL)
find_package(GLEW REQUIRED)
include_directories(${GLEW_INCLUDE_DIRS})
find_package(OpenGL)
find_package(glm CONFIG REQUIRED)
find_package(ReactPhysics3D REQUIRED)
find_package(pugixml REQUIRED)
file(GLOB_RECURSE SOURCES "src/*.cpp" "src/*.h")
add_library(openblocks ${SOURCES})
set_target_properties(openblocks PROPERTIES OUTPUT_NAME "openblocks")
target_link_libraries(openblocks ${GLEW_LIBRARIES} OpenGL::GL ReactPhysics3D::ReactPhysics3D pugixml::pugixml)
target_include_directories(openblocks PUBLIC "src" "../include")

View file

@ -8,6 +8,7 @@ Camera camera(glm::vec3(0.0, 0.0, 3.0));
std::shared_ptr<DataModel> dataModel = DataModel::New();
std::optional<HierarchyPreUpdateHandler> hierarchyPreUpdateHandler;
std::optional<HierarchyPostUpdateHandler> hierarchyPostUpdateHandler;
std::shared_ptr<Handles> editorToolHandles = Handles::New();
std::vector<InstanceRefWeak> currentSelection;

View file

@ -1,5 +1,6 @@
#pragma once
#include "objects/base/instance.h"
#include "objects/handles.h"
#include "objects/workspace.h"
#include "camera.h"
#include <functional>
@ -18,6 +19,7 @@ extern std::shared_ptr<DataModel> dataModel;
inline std::shared_ptr<Workspace> workspace() { return std::dynamic_pointer_cast<Workspace>(dataModel->services["Workspace"]); }
extern std::optional<HierarchyPreUpdateHandler> hierarchyPreUpdateHandler;
extern std::optional<HierarchyPostUpdateHandler> hierarchyPostUpdateHandler;
extern std::shared_ptr<Handles> editorToolHandles;
void setSelection(std::vector<InstanceRefWeak> newSelection, bool fromExplorer = false);
const std::vector<InstanceRefWeak> getSelection();

View file

@ -4,7 +4,7 @@
#define IMPL_WRAPPER_CLASS(CLASS_NAME, WRAPPED_TYPE, TYPE_NAME) Data::CLASS_NAME::CLASS_NAME(WRAPPED_TYPE in) : value(in) {} \
Data::CLASS_NAME::~CLASS_NAME() = default; \
Data::CLASS_NAME::operator const WRAPPED_TYPE() const { return value; } \
const Data::TypeInfo Data::CLASS_NAME::TYPE = { .name = TYPE_NAME, .deserializer = &Data::CLASS_NAME::Deserialize }; \
const Data::TypeInfo Data::CLASS_NAME::TYPE = { .name = TYPE_NAME, .deserializer = &Data::CLASS_NAME::Deserialize, .fromString = &Data::CLASS_NAME::FromString }; \
const Data::TypeInfo& Data::CLASS_NAME::GetType() const { return Data::CLASS_NAME::TYPE; }; \
void Data::CLASS_NAME::Serialize(pugi::xml_node* node) const { node->text().set(std::string(this->ToString())); }
@ -45,6 +45,11 @@ Data::Variant Data::Bool::Deserialize(pugi::xml_node* node) {
return Data::Bool(node->text().as_bool());
}
Data::Variant Data::Bool::FromString(std::string string) {
return Data::Bool(string[0] == 't' || string[0] == 'T' || string[0] == '1' || string[0] == 'y' || string[0] == 'Y');
}
const Data::String Data::Int::ToString() const {
return Data::String(std::to_string(value));
}
@ -53,6 +58,11 @@ Data::Variant Data::Int::Deserialize(pugi::xml_node* node) {
return Data::Int(node->text().as_int());
}
Data::Variant Data::Int::FromString(std::string string) {
return Data::Int(std::stoi(string));
}
const Data::String Data::Float::ToString() const {
return Data::String(std::to_string(value));
}
@ -61,10 +71,19 @@ Data::Variant Data::Float::Deserialize(pugi::xml_node* node) {
return Data::Float(node->text().as_float());
}
Data::Variant Data::Float::FromString(std::string string) {
return Data::Float(std::stof(string));
}
const Data::String Data::String::ToString() const {
return *this;
}
Data::Variant Data::String::Deserialize(pugi::xml_node* node) {
return Data::String(node->text().as_string());
}
Data::Variant Data::String::FromString(std::string string) {
return Data::String(string);
}

View file

@ -16,15 +16,18 @@ public: \
virtual const Data::String ToString() const override; \
virtual void Serialize(pugi::xml_node* node) const override; \
static Data::Variant Deserialize(pugi::xml_node* node); \
static Data::Variant FromString(std::string); \
};
namespace Data {
class Variant;
typedef std::function<Data::Variant(pugi::xml_node*)> Deserializer;
typedef std::function<Data::Variant(std::string)> FromString;
struct TypeInfo {
std::string name;
Deserializer deserializer;
FromString fromString;
TypeInfo(const TypeInfo&) = delete;
};

View file

@ -2,11 +2,16 @@
#include "datatypes/vector.h"
#include "physics/util.h"
#include <glm/ext/matrix_transform.hpp>
#include <glm/gtc/matrix_inverse.hpp>
#include <glm/matrix.hpp>
#include <reactphysics3d/mathematics/Transform.h>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/euler_angles.hpp>
// #include "meta.h" // IWYU pragma: keep
const Data::CFrame Data::CFrame::IDENTITY(glm::vec3(0, 0, 0), glm::mat3(1.f));
const Data::CFrame Data::CFrame::YToZ(glm::vec3(0, 0, 0), glm::mat3(glm::vec3(1, 0, 0), glm::vec3(0, 0, 1), glm::vec3(0, 1, 0)));
Data::CFrame::CFrame(float x, float y, float z, float R00, float R01, float R02, float R10, float R11, float R12, float R20, float R21, float R22)
: translation(x, y, z)
, rotation({
@ -33,17 +38,13 @@ Data::CFrame::CFrame(const rp::Transform& transform) : Data::CFrame::CFrame(rpTo
}
glm::mat3 lookAt(Data::Vector3 position, Data::Vector3 lookAt, Data::Vector3 up) {
// https://github.com/sgorsten/glm/issues/29#issuecomment-743989030
// https://github.com/sgorsten/linalg/issues/29#issuecomment-743989030
Data::Vector3 f = (lookAt - position).Unit(); // Forward/Look
Data::Vector3 u = up.Unit(); // Up
Data::Vector3 s = f.Cross(u).Unit(); // Right
u = s.Cross(u);
u = s.Cross(f);
return {
{ s.X(), u.X(), -f.X() },
{ s.Y(), u.Y(), -f.Y() },
{ s.Z(), u.Z(), -f.Z() },
};
return { s, u, f };
}
Data::CFrame::CFrame(Data::Vector3 position, Data::Vector3 lookAt, Data::Vector3 up)
@ -85,8 +86,21 @@ Data::CFrame Data::CFrame::FromEulerAnglesXYZ(Data::Vector3 vector) {
return Data::CFrame(Data::Vector3::ZERO, glm::column(mat, 2), (Data::Vector3)glm::column(mat, 1)); // Getting LookAt (3rd) and Up (2nd) vectors
}
Data::CFrame Data::CFrame::Inverse() const {
return CFrame { -translation * glm::transpose(glm::inverse(rotation)), glm::inverse(rotation) };
}
// Operators
Data::CFrame Data::CFrame::operator *(Data::CFrame otherFrame) const {
return CFrame { this->translation + this->rotation * otherFrame.translation, this->rotation * otherFrame.rotation };
}
Data::Vector3 Data::CFrame::operator *(Data::Vector3 vector) const {
return this->translation + this->rotation * vector;
}
Data::CFrame Data::CFrame::operator +(Data::Vector3 vector) const {
return CFrame { this->translation + glm::vec3(vector), this->rotation };
}

View file

@ -7,6 +7,8 @@
#include <glm/ext/vector_float3.hpp>
#include <glm/fwd.hpp>
#include <glm/gtc/matrix_access.hpp>
#include <glm/gtc/matrix_inverse.hpp>
#include <glm/matrix.hpp>
#include <reactphysics3d/mathematics/Transform.h>
#include <reactphysics3d/reactphysics3d.h>
@ -28,6 +30,9 @@ namespace Data {
CFrame(Data::Vector3 position, Data::Vector3 lookAt, Data::Vector3 up = Data::Vector3(0, 1, 0));
~CFrame();
static const CFrame IDENTITY;
static const CFrame YToZ;
virtual const TypeInfo& GetType() const override;
static const TypeInfo TYPE;
@ -41,18 +46,21 @@ namespace Data {
//inline static CFrame identity() { }
inline Vector3 Position() const { return translation; }
inline CFrame Rotation() const { return CFrame { glm::vec3(0, 0, 0), rotation }; }
CFrame Inverse() const;
inline float X() const { return translation.x; }
inline float Y() const { return translation.y; }
inline float Z() const { return translation.z; }
inline Vector3 RightVector() { return glm::column(rotation, 0); }
inline Vector3 UpVector() { return glm::column(rotation, 1); }
inline Vector3 LookVector() { return glm::column(rotation, 2); }
inline Vector3 LookVector() { return -glm::column(rotation, 2); }
Vector3 ToEulerAnglesXYZ();
static CFrame FromEulerAnglesXYZ(Data::Vector3);
// Operators
Data::CFrame operator *(Data::CFrame) const;
Data::Vector3 operator *(Data::Vector3) const;
Data::CFrame operator +(Data::Vector3) const;
Data::CFrame operator -(Data::Vector3) const;
};

View file

@ -25,6 +25,14 @@ Data::Vector3::operator glm::vec3() const { return vector; };
Data::Vector3::operator rp::Vector3() const { return rp::Vector3(X(), Y(), Z()); };
// Operators
Data::Vector3 Data::Vector3::operator *(float scale) const {
return Data::Vector3(this->X() * scale, this->Y() * scale, this->Z() * scale);
}
Data::Vector3 Data::Vector3::operator /(float scale) const {
return Data::Vector3(this->X() / scale, this->Y() / scale, this->Z() / scale);
}
Data::Vector3 Data::Vector3::operator +(Data::Vector3 other) const {
return Data::Vector3(this->X() + other.X(), this->Y() + other.Y(), this->Z() + other.Z());
}

View file

@ -40,6 +40,8 @@ namespace Data {
float Dot(Data::Vector3) const;
// Operators
Data::Vector3 operator *(float) const;
Data::Vector3 operator /(float) const;
Data::Vector3 operator +(Data::Vector3) const;
Data::Vector3 operator -(Data::Vector3) const;
Data::Vector3 operator -() const;

103
core/src/math_helper.cpp Normal file
View file

@ -0,0 +1,103 @@
#include "math_helper.h"
#define CMP_EPSILON 0.00001
// After a long time researching, I was able to use and adapt Godot's implementation of movable handles (godot/editor/plugins/gizmos/gizmo_3d_helper.cpp)
// All thanks goes to them and David Eberly for his algorithm.
void get_closest_points_between_segments(const glm::vec3 &p_p0, const glm::vec3 &p_p1, const glm::vec3 &p_q0, const glm::vec3 &p_q1, glm::vec3 &r_ps, glm::vec3 &r_qt) {
// Based on David Eberly's Computation of Distance Between Line Segments algorithm.
glm::vec3 p = p_p1 - p_p0;
glm::vec3 q = p_q1 - p_q0;
glm::vec3 r = p_p0 - p_q0;
float a = glm::dot(p, p);
float b = glm::dot(p, q);
float c = glm::dot(q, q);
float d = glm::dot(p, r);
float e = glm::dot(q, r);
float s = 0.0f;
float t = 0.0f;
float det = a * c - b * b;
if (det > CMP_EPSILON) {
// Non-parallel segments
float bte = b * e;
float ctd = c * d;
if (bte <= ctd) {
// s <= 0.0f
if (e <= 0.0f) {
// t <= 0.0f
s = (-d >= a ? 1 : (-d > 0.0f ? -d / a : 0.0f));
t = 0.0f;
} else if (e < c) {
// 0.0f < t < 1
s = 0.0f;
t = e / c;
} else {
// t >= 1
s = (b - d >= a ? 1 : (b - d > 0.0f ? (b - d) / a : 0.0f));
t = 1;
}
} else {
// s > 0.0f
s = bte - ctd;
if (s >= det) {
// s >= 1
if (b + e <= 0.0f) {
// t <= 0.0f
s = (-d <= 0.0f ? 0.0f : (-d < a ? -d / a : 1));
t = 0.0f;
} else if (b + e < c) {
// 0.0f < t < 1
s = 1;
t = (b + e) / c;
} else {
// t >= 1
s = (b - d <= 0.0f ? 0.0f : (b - d < a ? (b - d) / a : 1));
t = 1;
}
} else {
// 0.0f < s < 1
float ate = a * e;
float btd = b * d;
if (ate <= btd) {
// t <= 0.0f
s = (-d <= 0.0f ? 0.0f : (-d >= a ? 1 : -d / a));
t = 0.0f;
} else {
// t > 0.0f
t = ate - btd;
if (t >= det) {
// t >= 1
s = (b - d <= 0.0f ? 0.0f : (b - d >= a ? 1 : (b - d) / a));
t = 1;
} else {
// 0.0f < t < 1
s /= det;
t /= det;
}
}
}
}
} else {
// Parallel segments
if (e <= 0.0f) {
s = (-d <= 0.0f ? 0.0f : (-d >= a ? 1 : -d / a));
t = 0.0f;
} else if (e >= c) {
s = (b - d <= 0.0f ? 0.0f : (b - d >= a ? 1 : (b - d) / a));
t = 1;
} else {
s = 0.0f;
t = e / c;
}
}
r_ps = (1 - s) * p_p0 + s * p_p1;
r_qt = (1 - t) * p_q0 + t * p_q1;
}

5
core/src/math_helper.h Normal file
View file

@ -0,0 +1,5 @@
#pragma once
#include <glm/glm.hpp>
// From godot/editor/plugins/gizmos/gizmo_3d_helper.h
void get_closest_points_between_segments(const glm::vec3 &p_p0, const glm::vec3 &p_p1, const glm::vec3 &p_q0, const glm::vec3 &p_q1, glm::vec3 &r_ps, glm::vec3 &r_qt);

View file

@ -10,7 +10,8 @@
#include <variant>
#include <map>
#include <vector>
#include <../include/expected.hpp>
// #include <../../include/expected.hpp>
#include <expected.hpp>
#include <pugixml.hpp>
#include "member.h"

View file

@ -0,0 +1,90 @@
#include "handles.h"
#include "common.h"
#include "datatypes/cframe.h"
#include "datatypes/vector.h"
#include <optional>
#include <reactphysics3d/collision/RaycastInfo.h>
#include <reactphysics3d/engine/PhysicsCommon.h>
#include <reactphysics3d/engine/PhysicsWorld.h>
#include <reactphysics3d/mathematics/Transform.h>
HandleFace HandleFace::XPos(0, glm::vec3(1,0,0));
HandleFace HandleFace::XNeg(1, glm::vec3(-1,0,0));
HandleFace HandleFace::YPos(2, glm::vec3(0,1,0));
HandleFace HandleFace::YNeg(3, glm::vec3(0,-1,0));
HandleFace HandleFace::ZPos(4, glm::vec3(0,0,1));
HandleFace HandleFace::ZNeg(5, glm::vec3(0,0,-1));
std::array<HandleFace, 6> HandleFace::Faces { HandleFace::XPos, HandleFace::XNeg, HandleFace::YPos, HandleFace::YNeg, HandleFace::ZPos, HandleFace::ZNeg };
// Shitty solution
static rp3d::PhysicsCommon common;
static rp3d::PhysicsWorld* world = common.createPhysicsWorld();
const InstanceType Handles::TYPE = {
.super = &Instance::TYPE,
.className = "Handles",
// .constructor = &Workspace::Create,
// .explorerIcon = "",
};
const InstanceType* Handles::GetClass() {
return &TYPE;
}
Handles::Handles(): Instance(&TYPE) {
}
Data::CFrame Handles::GetCFrameOfHandle(HandleFace face) {
if (!adornee.has_value() || adornee->expired()) return Data::CFrame(glm::vec3(0,0,0), (Data::Vector3)glm::vec3(0,0,0));
Data::CFrame localFrame = worldMode ? Data::CFrame::IDENTITY + adornee->lock()->position() : adornee->lock()->cframe;
// We don't want this to align with local * face.normal, or else we have problems.
glm::vec3 upAxis(0, 0, 1);
if (glm::abs(glm::dot(glm::vec3(localFrame.Rotation() * face.normal), upAxis)) > 0.9999f)
upAxis = glm::vec3(0, 1, 0);
Data::Vector3 handlePos = localFrame * ((2.f + adornee->lock()->size * 0.5f) * face.normal);
Data::CFrame cframe(handlePos, handlePos + localFrame.Rotation() * face.normal, upAxis);
return cframe;
}
Data::CFrame Handles::PartCFrameFromHandlePos(HandleFace face, Data::Vector3 newPos) {
if (!adornee.has_value() || adornee->expired()) return Data::CFrame(glm::vec3(0,0,0), (Data::Vector3)glm::vec3(0,0,0));
Data::CFrame localFrame = worldMode ? Data::CFrame::IDENTITY + adornee->lock()->position() : adornee->lock()->cframe;
Data::CFrame inverseFrame = localFrame.Inverse();
Data::Vector3 handlePos = localFrame * ((2.f + adornee->lock()->size * 0.5f) * face.normal);
// glm::vec3 localPos = inverseFrame * newPos;
glm::vec3 newPartPos = newPos - localFrame.Rotation() * ((2.f + adornee->lock()->size * 0.5f) * face.normal);
return adornee->lock()->cframe.Rotation() + newPartPos;
}
std::optional<HandleFace> Handles::RaycastHandle(rp3d::Ray ray) {
for (HandleFace face : HandleFace::Faces) {
Data::CFrame cframe = GetCFrameOfHandle(face);
// Implement manual detection via boxes instead of... this shit
// This code also hardly works, and is not good at all... Hooo nope.
rp3d::RigidBody* body = world->createRigidBody(Data::CFrame::IDENTITY + cframe.Position());
body->addCollider(common.createBoxShape(cframe.Rotation() * Data::Vector3(HandleSize(face) / 2.f)), rp3d::Transform::identity());
rp3d::RaycastInfo info;
if (body->raycast(ray, info)) {
world->destroyRigidBody(body);
return face;
}
world->destroyRigidBody(body);
}
return std::nullopt;
}
Data::Vector3 Handles::HandleSize(HandleFace face) {
if (handlesType == HandlesType::MoveHandles)
return glm::vec3(0.5f, 0.5f, 2.f);
return glm::vec3(1,1,1);
}

View file

@ -0,0 +1,55 @@
#pragma once
#include "base.h"
#include "datatypes/cframe.h"
#include "objects/base/service.h"
#include "objects/part.h"
#include <array>
#include <memory>
#include <reactphysics3d/body/RigidBody.h>
class HandleFace {
HandleFace(int index, glm::vec3 normal) : index(index), normal(normal){}
public:
int index;
glm::vec3 normal;
static HandleFace XPos;
static HandleFace XNeg;
static HandleFace YPos;
static HandleFace YNeg;
static HandleFace ZPos;
static HandleFace ZNeg;
static std::array<HandleFace, 6> Faces;
};
enum HandlesType {
MoveHandles,
ScaleHandles,
RotateHandles,
};
class Handles : public Instance {
public:
const static InstanceType TYPE;
bool active;
std::optional<std::weak_ptr<Part>> adornee;
HandlesType handlesType;
// inline std::optional<std::weak_ptr<Part>> GetAdornee() { return adornee; }
// inline void SetAdornee(std::optional<std::weak_ptr<Part>> newAdornee) { this->adornee = newAdornee; updateAdornee(); };
Handles();
// World-space handles vs local-space handles
bool worldMode = false;
Data::CFrame GetCFrameOfHandle(HandleFace face);
Data::CFrame PartCFrameFromHandlePos(HandleFace face, Data::Vector3 newPos);
Data::Vector3 HandleSize(HandleFace face);
std::optional<HandleFace> RaycastHandle(rp3d::Ray ray);
static inline std::shared_ptr<Handles> New() { return std::make_shared<Handles>(); };
virtual const InstanceType* GetClass() override;
};

View file

@ -69,33 +69,37 @@ Part::Part(PartConstructParams params): Instance(&TYPE), cframe(Data::CFrame(par
.type = &Data::Bool::TYPE,
.codec = fieldCodecOf<Data::Bool, bool>(),
.updateCallback = memberFunctionOf(&Part::onUpdated, this)
} }, { "Position", {
}}, { "Position", {
.backingField = &cframe,
.type = &Data::Vector3::TYPE,
.codec = cframePositionCodec(),
.updateCallback = memberFunctionOf(&Part::onUpdated, this),
.flags = PropertyFlags::PROP_NOSAVE
} }, { "Rotation", {
}}, { "Rotation", {
.backingField = &cframe,
.type = &Data::Vector3::TYPE,
.codec = cframeRotationCodec(),
.updateCallback = memberFunctionOf(&Part::onUpdated, this),
.flags = PropertyFlags::PROP_NOSAVE
} }, { "CFrame", {
}}, { "CFrame", {
.backingField = &cframe,
.type = &Data::CFrame::TYPE,
.codec = fieldCodecOf<Data::CFrame>(),
.updateCallback = memberFunctionOf(&Part::onUpdated, this),
} }, { "Size", {
}}, { "Size", {
.backingField = &size,
.type = &Data::Vector3::TYPE,
.codec = fieldCodecOf<Data::Vector3, glm::vec3>(),
.updateCallback = memberFunctionOf(&Part::onUpdated, this)
} }, { "Color", {
}}, { "Color", {
.backingField = &color,
.type = &Data::Color3::TYPE,
.codec = fieldCodecOf<Data::Color3>(),
} }
}}, { "Transparency", {
.backingField = &transparency,
.type = &Data::Float::TYPE,
.codec = fieldCodecOf<Data::Float, float>(),
}}
}
});
}

View file

@ -32,6 +32,7 @@ public:
Data::CFrame cframe;
glm::vec3 size;
Data::Color3 color;
float transparency = 0.f;
bool selected = false;
bool anchored = false;

View file

@ -6,6 +6,7 @@
#include <reactphysics3d/collision/shapes/BoxShape.h>
#include <reactphysics3d/collision/shapes/CollisionShape.h>
#include <reactphysics3d/components/RigidBodyComponents.h>
#include <reactphysics3d/configuration.h>
#include <reactphysics3d/engine/EventListener.h>
#include <reactphysics3d/engine/PhysicsCommon.h>
#include <reactphysics3d/mathematics/Quaternion.h>
@ -39,6 +40,9 @@ void simulationInit() {
world = physicsCommon->createPhysicsWorld();
world->setGravity(rp::Vector3(0, -196.2, 0));
// world->setContactsPositionCorrectionTechnique(rp3d::ContactsPositionCorrectionTechnique::BAUMGARTE_CONTACTS);
world->setNbIterationsPositionSolver(2000);
world->setNbIterationsVelocitySolver(2000);
world->setEventListener(&eventListener);
}
@ -69,7 +73,7 @@ void syncPartPhysics(std::shared_ptr<Part> part) {
void physicsStep(float deltaTime) {
// Step the simulation a few steps
world->update(deltaTime / 2);
world->update(std::min(deltaTime / 2, (1/60.f)));
// Naive implementation. Parts are only considered so if they are just under Workspace
// TODO: Add list of tracked parts in workspace based on their ancestry using inWorkspace property of Instance

File diff suppressed because it is too large Load diff

View file

@ -2,5 +2,7 @@
#include "mesh.h"
extern Mesh* CUBE_MESH;
extern Mesh* SPHERE_MESH;
extern Mesh* ARROW_MESH;
void initMeshes();

View file

@ -3,7 +3,7 @@
#include "mesh.h"
Mesh::Mesh(int vertexCount, float *vertices) {
Mesh::Mesh(int vertexCount, float *vertices) : vertexCount(vertexCount) {
// Generate buffers
glGenBuffers(1, &VBO);
glGenVertexArrays(1, &VAO);

View file

@ -4,6 +4,8 @@ class Mesh {
unsigned int VBO, VAO;
public:
int vertexCount;
Mesh(int vertexCount, float* vertices);
~Mesh();
void bind();

View file

@ -5,11 +5,13 @@
#include <glm/ext.hpp>
#include <glm/ext/matrix_float4x4.hpp>
#include <glm/ext/matrix_transform.hpp>
#include <glm/ext/vector_float3.hpp>
#include <glm/glm.hpp>
#include <glm/trigonometric.hpp>
#include <memory>
#include <vector>
#include "datatypes/cframe.h"
#include "physics/util.h"
#include "shader.h"
#include "mesh.h"
@ -23,8 +25,10 @@
#include "renderer.h"
Shader *shader = NULL;
Shader *skyboxShader = NULL;
Shader* shader = NULL;
Shader* skyboxShader = NULL;
Shader* handleShader = NULL;
Shader* identityShader = NULL;
extern Camera camera;
Skybox* skyboxTexture = NULL;
Texture3D* studsTexture = NULL;
@ -54,13 +58,20 @@ void renderInit(GLFWwindow* window, int width, int height) {
studsTexture = new Texture3D("assets/textures/studs.png", 128, 128, 6, GL_RGBA);
// Compile shader
// Compile shaders
shader = new Shader("assets/shaders/phong.vs", "assets/shaders/phong.fs");
skyboxShader = new Shader("assets/shaders/skybox.vs", "assets/shaders/skybox.fs");
handleShader = new Shader("assets/shaders/handle.vs", "assets/shaders/handle.fs");
identityShader = new Shader("assets/shaders/identity.vs", "assets/shaders/identity.fs");
}
void renderParts() {
glDepthMask(GL_TRUE);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glFrontFace(GL_CW);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Use shader
shader->use();
@ -68,7 +79,7 @@ void renderParts() {
// shader->set("lightColor", glm::vec3(1.0f, 1.0f, 1.0f));
// view/projection transformations
glm::mat4 projection = glm::perspective(glm::radians(45.f), (float)viewportWidth / (float)viewportHeight, 0.1f, 100.0f);
glm::mat4 projection = glm::perspective(glm::radians(45.f), (float)viewportWidth / (float)viewportHeight, 0.1f, 1000.0f);
glm::mat4 view = camera.getLookAt();
shader->set("projection", projection);
shader->set("view", view);
@ -105,27 +116,40 @@ void renderParts() {
// Pass in the camera position
shader->set("viewPos", camera.cameraPos);
// TODO: Same as todo in src/physics/simulation.cpp
// Sort by nearest
std::map<float, std::shared_ptr<Part>> sorted;
for (InstanceRef inst : workspace()->GetChildren()) {
if (inst->GetClass()->className != "Part") continue;
std::shared_ptr<Part> part = std::dynamic_pointer_cast<Part>(inst);
if (part->transparency > 0.00001) {
float distance = glm::length(glm::vec3(Data::Vector3(camera.cameraPos) - part->position()));
sorted[distance] = part;
} else {
glm::mat4 model = part->cframe;
if (part->name == "camera") model = camera.getLookAt();
model = glm::scale(model, part->size);
shader->set("model", model);
shader->set("material", Material {
.diffuse = part->color,
.specular = glm::vec3(0.5f, 0.5f, 0.5f),
.shininess = 16.0f,
});
glm::mat3 normalMatrix = glm::mat3(glm::transpose(glm::inverse(model)));
shader->set("normalMatrix", normalMatrix);
shader->set("texScale", part->size);
shader->set("transparency", part->transparency);
// if (inst->name == "Target") printf("(%f,%f,%f):(%f,%f,%f;%f,%f,%f;%f,%f,%f)\n",
// part->cframe.X(),
// part->cframe.Y(),
// part->cframe.Z(),
// part->cframe.RightVector().X(),
// part->cframe.UpVector().X(),
// part->cframe.LookVector().X(),
// part->cframe.RightVector().Y(),
// part->cframe.UpVector().Y(),
// part->cframe.LookVector().Y(),
// part->cframe.RightVector().Z(),
// part->cframe.UpVector().Z(),
// part->cframe.LookVector().Z()
// );
CUBE_MESH->bind();
glDrawArrays(GL_TRIANGLES, 0, CUBE_MESH->vertexCount);
}
}
// TODO: Same as todo in src/physics/simulation.cpp
// According to LearnOpenGL, std::map automatically sorts its contents.
for (std::map<float, std::shared_ptr<Part>>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); it++) {
std::shared_ptr<Part> part = it->second;
glm::mat4 model = part->cframe;
if (part->name == "camera") model = camera.getLookAt();
model = glm::scale(model, part->size);
shader->set("model", model);
shader->set("material", Material {
@ -136,18 +160,20 @@ void renderParts() {
glm::mat3 normalMatrix = glm::mat3(glm::transpose(glm::inverse(model)));
shader->set("normalMatrix", normalMatrix);
shader->set("texScale", part->size);
shader->set("transparency", part->transparency);
CUBE_MESH->bind();
glDrawArrays(GL_TRIANGLES, 0, 36);
glDrawArrays(GL_TRIANGLES, 0, CUBE_MESH->vertexCount);
}
}
void renderSkyBox() {
glDepthMask(GL_FALSE);
glCullFace(GL_FRONT);
skyboxShader->use();
glm::mat4 projection = glm::perspective(glm::radians(45.f), (float)viewportWidth / (float)viewportHeight, 0.1f, 100.0f);
glm::mat4 projection = glm::perspective(glm::radians(45.f), (float)viewportWidth / (float)viewportHeight, 0.1f, 1000.0f);
// Remove translation component of view, making us always at (0, 0, 0)
glm::mat4 view = glm::mat4(glm::mat3(camera.getLookAt()));
@ -160,11 +186,82 @@ void renderSkyBox() {
glDrawArrays(GL_TRIANGLES, 0, 36);
}
void renderHandles() {
if (!editorToolHandles->adornee.has_value() || !editorToolHandles->active) return;
glDepthMask(GL_TRUE);
glCullFace(GL_BACK);
// Use shader
handleShader->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();
handleShader->set("projection", projection);
handleShader->set("view", view);
handleShader->set("sunLight", DirLight {
.direction = glm::vec3(-0.2f, -1.0f, -0.3f),
.ambient = glm::vec3(0.2f, 0.2f, 0.2f),
.diffuse = glm::vec3(0.5f, 0.5f, 0.5f),
.specular = glm::vec3(1.0f, 1.0f, 1.0f),
});
handleShader->set("numPointLights", 0);
// Pass in the camera position
handleShader->set("viewPos", camera.cameraPos);
for (auto face : HandleFace::Faces) {
glm::mat4 model = editorToolHandles->GetCFrameOfHandle(face);
model = glm::scale(model, (glm::vec3)editorToolHandles->HandleSize(face));
handleShader->set("model", model);
handleShader->set("material", Material {
.diffuse = glm::abs(face.normal),
.specular = glm::vec3(0.5f, 0.5f, 0.5f),
.shininess = 16.0f,
});
glm::mat3 normalMatrix = glm::mat3(glm::transpose(glm::inverse(model)));
handleShader->set("normalMatrix", normalMatrix);
if (editorToolHandles->handlesType == HandlesType::MoveHandles) {
ARROW_MESH->bind();
glDrawArrays(GL_TRIANGLES, 0, ARROW_MESH->vertexCount);
} else {
SPHERE_MESH->bind();
glDrawArrays(GL_TRIANGLES, 0, SPHERE_MESH->vertexCount);
}
}
// 2d square overlay
glDisable(GL_CULL_FACE);
identityShader->use();
identityShader->set("aColor", glm::vec3(0.f, 1.f, 1.f));
for (auto face : HandleFace::Faces) {
Data::CFrame cframe = editorToolHandles->GetCFrameOfHandle(face);
glm::vec4 screenPos = projection * view * glm::vec4((glm::vec3)cframe.Position(), 1.0f);
glm::vec3 ndcCoords = screenPos / screenPos.w;
float rad = 5;
float xRad = rad * 1/viewportWidth;
float yRad = rad * 1/viewportHeight;
glBegin(GL_QUADS);
glVertex3f(ndcCoords.x - xRad, ndcCoords.y - yRad, 0);
glVertex3f(ndcCoords.x + xRad, ndcCoords.y - yRad, 0);
glVertex3f(ndcCoords.x + xRad, ndcCoords.y + yRad, 0);
glVertex3f(ndcCoords.x - xRad, ndcCoords.y + yRad, 0);
glEnd();
}
}
void render(GLFWwindow* window) {
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
renderSkyBox();
renderHandles();
renderParts();
}

View file

@ -1,10 +1,9 @@
glm
opengl
assimp
sdl2
opengl (Linux: glvnd, Windows: [built-in/none])
glfw
glut
glew
glm
sdl2
stb
qt6
reactphysics3d
pugixml

View file

@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.16)
cmake_minimum_required(VERSION 3.31..)
project(editor VERSION 0.1 LANGUAGES CXX)
@ -14,8 +14,6 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets LinguistTools)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets LinguistTools)
include_directories("../src")
set(TS_FILES editor_en_US.ts)
set(PROJECT_SOURCES
@ -40,7 +38,6 @@ if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
qt_add_executable(editor
MANUAL_FINALIZATION
${PROJECT_SOURCES}
$<TARGET_OBJECTS:openblocks>
)
# Define target properties for Android with Qt 6 as:
# set_property(TARGET editor APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
@ -59,14 +56,24 @@ else()
add_executable(editor
${PROJECT_SOURCES}
mainglwidget.h mainglwidget.cpp
$<TARGET_OBJECTS:openblocks>
)
endif()
qt5_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES})
endif()
target_link_libraries(editor PRIVATE Qt${QT_VERSION_MAJOR}::Widgets ${SDL2_LIBRARIES} ${GLEW_LIBRARIES} ${GLUT_LIBRARIES} OpenGL::GL OpenGL::GLU glfw glm::glm assimp ReactPhysics3D::ReactPhysics3D pugixml::pugixml)
target_include_directories(editor PUBLIC "../core/src" "../include")
target_link_libraries(editor PRIVATE openblocks Qt${QT_VERSION_MAJOR}::Widgets)
# Qt6 does not include QOpenGLWidgets as part of Widgets base anymore, so
# we have to include it manually
if (${QT_VERSION} GREATER_EQUAL 6)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS OpenGL OpenGLWidgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS OpenGL OpenGLWidgets)
target_include_directories(editor PUBLIC Qt6::OpenGL Qt6::OpenGLWidgets)
target_link_libraries(editor PRIVATE Qt6::OpenGL Qt6::OpenGLWidgets)
endif()
# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
# If you are developing for iOS or macOS you should consider setting an
@ -89,6 +96,8 @@ install(TARGETS editor
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(FILES $<TARGET_RUNTIME_DLLS:editor> TYPE BIN)
if(QT_VERSION_MAJOR EQUAL 6)
qt_finalize_executable(editor)
endif()

View file

@ -1,10 +1 @@
#pragma once
enum SelectedTool {
SELECT,
MOVE,
SCALE,
ROTATE,
};
extern SelectedTool selectedTool;
#pragma once

View file

@ -2,15 +2,22 @@
#include <chrono>
#include <QMouseEvent>
#include <glm/common.hpp>
#include <glm/ext/matrix_projection.hpp>
#include <glm/ext/matrix_transform.hpp>
#include <glm/ext/vector_float3.hpp>
#include <glm/geometric.hpp>
#include <glm/gtc/round.hpp>
#include <glm/matrix.hpp>
#include <memory>
#include <optional>
#include <reactphysics3d/collision/RaycastInfo.h>
#include <vector>
#include "datatypes/cframe.h"
#include "editorcommon.h"
#include "mainwindow.h"
#include "objects/handles.h"
#include "physics/util.h"
#include "qcursor.h"
#include "qevent.h"
@ -21,8 +28,11 @@
#include "camera.h"
#include "common.h"
#include "rendering/shader.h"
#include "mainglwidget.h"
#include "../core/src/rendering/defaultmeshes.h"
#include "math_helper.h"
MainGLWidget::MainGLWidget(QWidget* parent): QOpenGLWidget(parent) {
setFocusPolicy(Qt::FocusPolicy::ClickFocus);
@ -45,6 +55,12 @@ void MainGLWidget::resizeGL(int w, int h) {
setViewport(w, h);
}
glm::vec2 firstPoint;
glm::vec2 secondPoint;
extern std::optional<std::weak_ptr<Part>> draggingObject;
extern std::optional<HandleFace> draggingHandle;
extern Shader* shader;
void MainGLWidget::paintGL() {
::render(NULL);
}
@ -62,8 +78,9 @@ void MainGLWidget::handleCameraRotate(QMouseEvent* evt) {
bool isMouseDragging = false;
std::optional<std::weak_ptr<Part>> draggingObject;
std::optional<HandleFace> draggingHandle;
void MainGLWidget::handleObjectDrag(QMouseEvent* evt) {
if (!isMouseDragging) return;
if (!isMouseDragging || !draggingObject) return;
QPoint position = evt->pos();
@ -79,17 +96,109 @@ void MainGLWidget::handleObjectDrag(QMouseEvent* evt) {
syncPartPhysics(draggingObject->lock());
}
inline glm::vec3 vec3fy(glm::vec4 vec) {
return vec / vec.w;
}
QPoint lastPoint;
void MainGLWidget::handleHandleDrag(QMouseEvent* evt) {
QPoint cLastPoint = lastPoint;
lastPoint = evt->pos();
if (!isMouseDragging || !draggingHandle || !editorToolHandles->adornee || !editorToolHandles->active) return;
QPoint position = evt->pos();
auto part = editorToolHandles->adornee->lock();
// 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);
Data::CFrame handleCFrame = editorToolHandles->GetCFrameOfHandle(draggingHandle.value());
// Current frame. Identity frame if worldMode == true, selected object's frame if worldMode == false
Data::CFrame frame = editorToolHandles->worldMode ? Data::CFrame::IDENTITY + part->position() : part->cframe.Rotation();
// 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);
// Find new part position
glm::vec3 centerPoint = editorToolHandles->PartCFrameFromHandlePos(draggingHandle.value(), handlePoint).Position();
// Apply snapping in the current frame
glm::vec3 diff = centerPoint - (glm::vec3)editorToolHandles->adornee->lock()->position();
// printf("\n=======\nPre-snap: (%f, %f, %f)\n", diff.x, diff.y, diff.z);
if (snappingFactor()) diff = frame.Rotation() * (glm::round(glm::vec3(frame.Inverse().Rotation() * diff) / snappingFactor()) * snappingFactor());
// printf("Post-snap: (%f, %f, %f)\n", diff.x, diff.y, diff.z);
switch (mainWindow()->selectedTool) {
case SelectedTool::SELECT: break;
case SelectedTool::MOVE: {
// Add difference
editorToolHandles->adornee->lock()->cframe = editorToolHandles->adornee->lock()->cframe + diff;
} break;
case SelectedTool::SCALE: {
// Find local difference
glm::vec3 localDiff = frame.Inverse() * diff;
// Find outwarwd difference
localDiff = 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;
case SelectedTool::ROTATE: {
// TODO: Implement rotation
} break;
}
syncPartPhysics(std::dynamic_pointer_cast<Part>(editorToolHandles->adornee->lock()));
}
std::optional<HandleFace> MainGLWidget::raycastHandle(glm::vec3 pointDir) {
if (!editorToolHandles->adornee.has_value() || !editorToolHandles->active) return std::nullopt;
return editorToolHandles->RaycastHandle(rp3d::Ray(glmToRp(camera.cameraPos), glmToRp(glm::normalize(pointDir)) * 50000));
}
void MainGLWidget::handleCursorChange(QMouseEvent* evt) {
QPoint position = evt->pos();
glm::vec3 pointDir = camera.getScreenDirection(glm::vec2(position.x(), position.y()), glm::vec2(width(), height()));
if (raycastHandle(pointDir)) {
setCursor(Qt::OpenHandCursor);
return;
};
std::optional<const RaycastResult> rayHit = castRayNearest(camera.cameraPos, pointDir, 50000);
setCursor((rayHit && partFromBody(rayHit->body)->name != "Baseplate") ? Qt::OpenHandCursor : Qt::ArrowCursor);
if (rayHit && partFromBody(rayHit->body)->name != "Baseplate") {
setCursor(Qt::OpenHandCursor);
return;
}
setCursor(Qt::ArrowCursor);
}
void MainGLWidget::mouseMoveEvent(QMouseEvent* evt) {
handleCameraRotate(evt);
handleObjectDrag(evt);
handleHandleDrag(evt);
handleCursorChange(evt);
}
@ -105,6 +214,15 @@ void MainGLWidget::mousePressEvent(QMouseEvent* evt) {
QPoint position = evt->pos();
glm::vec3 pointDir = camera.getScreenDirection(glm::vec2(position.x(), position.y()), glm::vec2(width(), height()));
// raycast handles
auto handle = raycastHandle(pointDir);
if (handle.has_value()) {
isMouseDragging = true;
draggingHandle = handle;
return;
}
// raycast part
std::optional<const RaycastResult> rayHit = castRayNearest(camera.cameraPos, pointDir, 50000);
if (!rayHit || !partFromBody(rayHit->body)) return;
std::shared_ptr<Part> part = partFromBody(rayHit->body);
@ -128,6 +246,7 @@ void MainGLWidget::mouseReleaseEvent(QMouseEvent* evt) {
isMouseRightDragging = false;
isMouseDragging = false;
draggingObject = std::nullopt;
draggingHandle = std::nullopt;
}
static int moveZ = 0;
@ -166,4 +285,17 @@ void MainGLWidget::keyPressEvent(QKeyEvent* evt) {
void MainGLWidget::keyReleaseEvent(QKeyEvent* evt) {
if (evt->key() == Qt::Key_W || evt->key() == Qt::Key_S) moveZ = 0;
else if (evt->key() == Qt::Key_A || evt->key() == Qt::Key_D) moveX = 0;
}
MainWindow* MainGLWidget::mainWindow() {
return dynamic_cast<MainWindow*>(window());
}
float MainGLWidget::snappingFactor() {
switch (mainWindow()->snappingMode) {
case GridSnappingMode::SNAP_1_STUD: return 1;
case GridSnappingMode::SNAP_05_STUDS: return 0.5;
case GridSnappingMode::SNAP_OFF: return 0;
}
return 0;
}

View file

@ -1,12 +1,15 @@
#ifndef MAINGLWIDGET_H
#define MAINGLWIDGET_H
#include "mainwindow.h"
#include "objects/part.h"
#include "qevent.h"
#include <QOpenGLWidget>
#include <QWidget>
#include <memory>
class HandleFace;
class MainGLWidget : public QOpenGLWidget {
public:
MainGLWidget(QWidget *parent = nullptr);
@ -20,13 +23,18 @@ protected:
void handleCameraRotate(QMouseEvent* evt);
void handleObjectDrag(QMouseEvent* evt);
void handleHandleDrag(QMouseEvent* evt);
void handleCursorChange(QMouseEvent* evt);
std::optional<HandleFace> raycastHandle(glm::vec3 pointDir);
void mouseMoveEvent(QMouseEvent* evt) override;
void mousePressEvent(QMouseEvent* evt) override;
void mouseReleaseEvent(QMouseEvent* evt) override;
void keyPressEvent(QKeyEvent* evt) override;
void keyReleaseEvent(QKeyEvent* evt) override;
MainWindow* mainWindow();
float snappingFactor();
};
#endif // MAINGLWIDGET_H

View file

@ -10,21 +10,25 @@
#include <QWidget>
#include <QTreeView>
#include <QAbstractItemView>
#include <memory>
#include <optional>
#include <qglobal.h>
#include <qwindowdefs.h>
#include <sstream>
#include "common.h"
#include "editorcommon.h"
#include "objects/base/instance.h"
#include "objects/datamodel.h"
#include "objects/handles.h"
#include "physics/simulation.h"
#include "objects/part.h"
#include "qfiledialog.h"
#include "qitemselectionmodel.h"
#include "qclipboard.h"
#include "qmimedata.h"
#include "qobject.h"
#include "qsysinfo.h"
SelectedTool selectedTool;
bool simulationPlaying = false;
MainWindow::MainWindow(QWidget *parent)
@ -37,13 +41,20 @@ MainWindow::MainWindow(QWidget *parent)
timer.start(33, this);
setMouseTracking(true);
ConnectSelectionChangeHandler();
ui->explorerView->buildContextMenu();
connect(ui->actionToolSelect, &QAction::triggered, this, [&]() { selectedTool = SelectedTool::SELECT; updateSelectedTool(); });
connect(ui->actionToolMove, &QAction::triggered, this, [&](bool state) { selectedTool = state ? SelectedTool::MOVE : SelectedTool::SELECT; updateSelectedTool(); });
connect(ui->actionToolScale, &QAction::triggered, this, [&](bool state) { selectedTool = state ? SelectedTool::SCALE : SelectedTool::SELECT; updateSelectedTool(); });
connect(ui->actionToolRotate, &QAction::triggered, this, [&](bool state) { selectedTool = state ? SelectedTool::ROTATE : SelectedTool::SELECT; updateSelectedTool(); });
connect(ui->actionToolSelect, &QAction::triggered, this, [&]() { selectedTool = SelectedTool::SELECT; updateToolbars(); });
connect(ui->actionToolMove, &QAction::triggered, this, [&](bool state) { selectedTool = state ? SelectedTool::MOVE : SelectedTool::SELECT; updateToolbars(); });
connect(ui->actionToolScale, &QAction::triggered, this, [&](bool state) { selectedTool = state ? SelectedTool::SCALE : SelectedTool::SELECT; updateToolbars(); });
connect(ui->actionToolRotate, &QAction::triggered, this, [&](bool state) { selectedTool = state ? SelectedTool::ROTATE : SelectedTool::SELECT; updateToolbars(); });
ui->actionToolSelect->setChecked(true);
selectedTool = SelectedTool::SELECT;
connect(ui->actionGridSnap1, &QAction::triggered, this, [&]() { snappingMode = GridSnappingMode::SNAP_1_STUD; updateToolbars(); });
connect(ui->actionGridSnap05, &QAction::triggered, this, [&]() { snappingMode = GridSnappingMode::SNAP_05_STUDS; updateToolbars(); });
connect(ui->actionGridSnapOff, &QAction::triggered, this, [&]() { snappingMode = GridSnappingMode::SNAP_OFF; updateToolbars(); });
ui->actionGridSnap1->setChecked(true);
snappingMode = GridSnappingMode::SNAP_1_STUD;
connect(ui->actionToggleSimulation, &QAction::triggered, this, [&]() {
simulationPlaying = !simulationPlaying;
@ -61,23 +72,144 @@ MainWindow::MainWindow(QWidget *parent)
connect(ui->actionSave, &QAction::triggered, this, [&]() {
std::optional<std::string> path;
if (!dataModel->HasFile())
path = QFileDialog::getSaveFileName(this, QString::fromStdString("Save " + dataModel->name), "", "*.obl").toStdString();
path = openFileDialog("Openblocks Level (*.obl)", ".obl", QFileDialog::AcceptSave, QString::fromStdString("Save " + dataModel->name));
if (path == "") return;
dataModel->SaveToFile(path);
});
connect(ui->actionOpen, &QAction::triggered, this, [&]() {
std::string path = QFileDialog::getOpenFileName(this, "Load file", "", "*.obl").toStdString();
if (path == "") return;
std::shared_ptr<DataModel> newModel = DataModel::LoadFromFile(path);
std::optional<std::string> path = openFileDialog("Openblocks Level (*.obl)", ".obl", QFileDialog::AcceptOpen);
if (!path) return;
std::shared_ptr<DataModel> newModel = DataModel::LoadFromFile(path.value());
dataModel = newModel;
delete ui->explorerView->selectionModel();
ui->explorerView->reset();
ui->explorerView->setModel(new ExplorerModel(dataModel));
ConnectSelectionChangeHandler();
ui->explorerView->updateRoot(newModel);
});
connect(ui->actionDelete, &QAction::triggered, this, [&]() {
for (InstanceRefWeak inst : getSelection()) {
if (inst.expired()) continue;
inst.lock()->SetParent(std::nullopt);
}
setSelection(std::vector<InstanceRefWeak> {});
});
connect(ui->actionCopy, &QAction::triggered, this, [&]() {
pugi::xml_document rootDoc;
for (InstanceRefWeak inst : getSelection()) {
if (inst.expired()) continue;
inst.lock()->Serialize(&rootDoc);
}
std::ostringstream encoded;
rootDoc.save(encoded);
QMimeData* mimeData = new QMimeData;
mimeData->setData("application/xml", QByteArray::fromStdString(encoded.str()));
QApplication::clipboard()->setMimeData(mimeData);
});
connect(ui->actionCut, &QAction::triggered, this, [&]() {
pugi::xml_document rootDoc;
for (InstanceRefWeak inst : getSelection()) {
if (inst.expired()) continue;
inst.lock()->Serialize(&rootDoc);
inst.lock()->SetParent(std::nullopt);
}
std::ostringstream encoded;
rootDoc.save(encoded);
QMimeData* mimeData = new QMimeData;
mimeData->setData("application/xml", QByteArray::fromStdString(encoded.str()));
QApplication::clipboard()->setMimeData(mimeData);
});
connect(ui->actionPaste, &QAction::triggered, this, [&]() {
const QMimeData* mimeData = QApplication::clipboard()->mimeData();
if (!mimeData || !mimeData->hasFormat("application/xml")) return;
QByteArray bytes = mimeData->data("application/xml");
std::string encoded = bytes.toStdString();
pugi::xml_document rootDoc;
rootDoc.load_string(encoded.c_str());
for (pugi::xml_node instNode : rootDoc.children()) {
InstanceRef inst = Instance::Deserialize(&instNode);
workspace()->AddChild(inst);
}
});
connect(ui->actionPasteInto, &QAction::triggered, this, [&]() {
if (getSelection().size() != 1 || getSelection()[0].expired()) return;
InstanceRef selectedParent = getSelection()[0].lock();
const QMimeData* mimeData = QApplication::clipboard()->mimeData();
if (!mimeData || !mimeData->hasFormat("application/xml")) return;
QByteArray bytes = mimeData->data("application/xml");
std::string encoded = bytes.toStdString();
pugi::xml_document rootDoc;
rootDoc.load_string(encoded.c_str());
for (pugi::xml_node instNode : rootDoc.children()) {
InstanceRef inst = Instance::Deserialize(&instNode);
selectedParent->AddChild(inst);
}
});
connect(ui->actionSaveModel, &QAction::triggered, this, [&]() {
std::optional<std::string> path = openFileDialog("Openblocks Model (*.obm)", ".obm", QFileDialog::AcceptSave);
if (!path) return;
std::ofstream outStream(path.value());
// Serialized XML for exporting
pugi::xml_document modelDoc;
pugi::xml_node modelRoot = modelDoc.append_child("openblocks");
for (InstanceRefWeak inst : getSelection()) {
if (inst.expired()) continue;
inst.lock()->Serialize(&modelRoot);
}
modelDoc.save(outStream);
});
connect(ui->actionInsertModel, &QAction::triggered, this, [&]() {
if (getSelection().size() != 1 || getSelection()[0].expired()) return;
InstanceRef selectedParent = getSelection()[0].lock();
std::optional<std::string> path = openFileDialog("Openblocks Model (*.obm)", ".obm", QFileDialog::AcceptOpen);
if (!path) return;
std::ifstream inStream(path.value());
pugi::xml_document modelDoc;
modelDoc.load(inStream);
for (pugi::xml_node instNode : modelDoc.child("openblocks").children("Item")) {
InstanceRef inst = Instance::Deserialize(&instNode);
selectedParent->AddChild(inst);
}
});
// Update handles
addSelectionListener([&](auto oldSelection, auto newSelection, bool fromExplorer) {
editorToolHandles->adornee = std::nullopt;
if (newSelection.size() == 0) return;
InstanceRef inst = newSelection[0].lock();
if (inst->GetClass() != &Part::TYPE) return;
editorToolHandles->adornee = std::dynamic_pointer_cast<Part>(inst);
});
// Update properties
addSelectionListener([&](auto oldSelection, auto newSelection, bool fromExplorer) {
if (newSelection.size() == 0) return;
if (newSelection.size() > 1)
ui->propertiesView->setSelected(std::nullopt);
ui->propertiesView->setSelected(newSelection[0].lock());
});
// ui->explorerView->Init(ui);
simulationInit();
@ -95,24 +227,13 @@ MainWindow::MainWindow(QWidget *parent)
workspace()->AddChild(ui->mainWidget->lastPart = Part::New({
.position = glm::vec3(0),
.rotation = glm::vec3(0),
.rotation = glm::vec3(0.5, 2, 1),
.size = glm::vec3(4, 1.2, 2),
.color = glm::vec3(0.639216f, 0.635294f, 0.647059f),
}));
syncPartPhysics(ui->mainWidget->lastPart);
}
void MainWindow::ConnectSelectionChangeHandler() {
connect(ui->explorerView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [&](const QItemSelection &selected, const QItemSelection &deselected) {
if (selected.count() == 0) return;
std::optional<InstanceRef> inst = selected.count() == 0 ? std::nullopt
: std::make_optional(((Instance*)selected.indexes()[0].internalPointer())->shared_from_this());
ui->propertiesView->setSelected(inst);
});
}
static std::chrono::time_point lastTime = std::chrono::steady_clock::now();
void MainWindow::timerEvent(QTimerEvent* evt) {
if (evt->timerId() != timer.timerId()) {
@ -129,11 +250,37 @@ void MainWindow::timerEvent(QTimerEvent* evt) {
ui->mainWidget->updateCycle();
}
void MainWindow::updateSelectedTool() {
void MainWindow::updateToolbars() {
ui->actionToolSelect->setChecked(selectedTool == SelectedTool::SELECT);
ui->actionToolMove->setChecked(selectedTool == SelectedTool::MOVE);
ui->actionToolScale->setChecked(selectedTool == SelectedTool::SCALE);
ui->actionToolRotate->setChecked(selectedTool == SelectedTool::ROTATE);
ui->actionGridSnap1->setChecked(snappingMode == GridSnappingMode::SNAP_1_STUD);
ui->actionGridSnap05->setChecked(snappingMode == GridSnappingMode::SNAP_05_STUDS);
ui->actionGridSnapOff->setChecked(snappingMode == GridSnappingMode::SNAP_OFF);
// editorToolHandles->worldMode = false;
if (selectedTool == SelectedTool::MOVE) editorToolHandles->worldMode = true;
if (selectedTool == SelectedTool::SCALE) editorToolHandles->worldMode = false;
editorToolHandles->active = selectedTool != SelectedTool::SELECT;
editorToolHandles->handlesType =
selectedTool == SelectedTool::MOVE ? HandlesType::MoveHandles
: selectedTool == SelectedTool::SCALE ? HandlesType::ScaleHandles
: selectedTool == SelectedTool::ROTATE ? HandlesType::RotateHandles
: HandlesType::ScaleHandles;
}
std::optional<std::string> MainWindow::openFileDialog(QString filter, QString defaultExtension, QFileDialog::AcceptMode acceptMode, QString title) {
QFileDialog dialog(this);
if (title != "") dialog.setWindowTitle(title);
dialog.setNameFilters(QStringList { filter, "All Files (*)" });
dialog.setDefaultSuffix(defaultExtension);
dialog.setAcceptMode(acceptMode);
if (!dialog.exec())
return std::nullopt;
return dialog.selectedFiles().front().toStdString();
}
MainWindow::~MainWindow()

View file

@ -7,6 +7,20 @@
#include "qmenu.h"
#include <QMainWindow>
#include <QLineEdit>
#include <qfiledialog.h>
enum SelectedTool {
SELECT,
MOVE,
SCALE,
ROTATE,
};
enum GridSnappingMode {
SNAP_1_STUD,
SNAP_05_STUDS,
SNAP_OFF,
};
QT_BEGIN_NAMESPACE
namespace Ui {
@ -22,12 +36,16 @@ public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
SelectedTool selectedTool;
GridSnappingMode snappingMode;
Ui::MainWindow *ui;
private:
QBasicTimer timer;
void updateSelectedTool();
void updateToolbars();
void timerEvent(QTimerEvent*) override;
void ConnectSelectionChangeHandler();
std::optional<std::string> openFileDialog(QString filter, QString defaultExtension, QFileDialog::AcceptMode acceptMode, QString title = "");
};
#endif // MAINWINDOW_H

View file

@ -41,7 +41,7 @@
<x>0</x>
<y>0</y>
<width>1027</width>
<height>29</height>
<height>30</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
@ -115,6 +115,16 @@
<addaction name="actionToolScale"/>
<addaction name="actionToolRotate"/>
<addaction name="separator"/>
<addaction name="actionDelete"/>
<addaction name="actionCopy"/>
<addaction name="actionCut"/>
<addaction name="actionPaste"/>
<addaction name="actionPasteInto"/>
<addaction name="separator"/>
<addaction name="actionGridSnap1"/>
<addaction name="actionGridSnap05"/>
<addaction name="actionGridSnapOff"/>
<addaction name="separator"/>
<addaction name="actionToggleSimulation"/>
</widget>
<action name="actionAddPart">
@ -257,6 +267,167 @@
<string>F5</string>
</property>
</action>
<action name="actionGridSnap1">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset>
<normaloff>assets/icons/editor/snap1.png</normaloff>assets/icons/editor/snap1.png</iconset>
</property>
<property name="text">
<string>1-Stud Snapping</string>
</property>
<property name="toolTip">
<string>Set grid snapping to 1 stud</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionGridSnap05">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset>
<normaloff>assets/icons/editor/snap05.png</normaloff>assets/icons/editor/snap05.png</iconset>
</property>
<property name="text">
<string>1/2-Stud Snapping</string>
</property>
<property name="toolTip">
<string>Set grid snapping to 1/2 studs</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionGridSnapOff">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset>
<normaloff>assets/icons/editor/snapoff.png</normaloff>assets/icons/editor/snapoff.png</iconset>
</property>
<property name="text">
<string>No Grid Snapping</string>
</property>
<property name="toolTip">
<string>Turn grid snapping off</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionCopy">
<property name="icon">
<iconset theme="edit-copy"/>
</property>
<property name="text">
<string>Copy</string>
</property>
<property name="toolTip">
<string>Copy objects to clipboard</string>
</property>
<property name="shortcut">
<string>Ctrl+C</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionCut">
<property name="icon">
<iconset theme="edit-cut"/>
</property>
<property name="text">
<string>Cut</string>
</property>
<property name="toolTip">
<string>Cut objects into clipboard</string>
</property>
<property name="shortcut">
<string>Ctrl+X</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionPaste">
<property name="icon">
<iconset theme="edit-paste"/>
</property>
<property name="text">
<string>Paste</string>
</property>
<property name="toolTip">
<string>Paste objects from clipboard</string>
</property>
<property name="shortcut">
<string>Ctrl+V</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionPasteInto">
<property name="icon">
<iconset theme="edit-paste"/>
</property>
<property name="text">
<string>Paste Into</string>
</property>
<property name="toolTip">
<string>Paste objects from clipboard into selected object</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+V</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionDelete">
<property name="icon">
<iconset theme="edit-delete"/>
</property>
<property name="text">
<string>Delete Object</string>
</property>
<property name="toolTip">
<string>Delete selected objects</string>
</property>
<property name="shortcut">
<string>Del</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionSaveModel">
<property name="text">
<string>Save Model to File</string>
</property>
<property name="toolTip">
<string>Saves objects to file as XML model</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionInsertModel">
<property name="text">
<string>Insert Model from File</string>
</property>
<property name="toolTip">
<string>Insert model from XML file</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View file

@ -234,6 +234,12 @@ bool ExplorerModel::dropMimeData(const QMimeData *data, Qt::DropAction action, i
return true;
}
void ExplorerModel::updateRoot(InstanceRef newRoot) {
beginResetModel();
rootItem = newRoot;
endResetModel();
}
QMimeData* ExplorerModel::mimeData(const QModelIndexList& indexes) const {
// application/x-openblocks-instance-pointers
DragDropSlot* slot = new DragDropSlot();

View file

@ -1,18 +1,12 @@
#pragma once
#include "objects/base/instance.h"
#include "objects/part.h"
#include "qabstractitemmodel.h"
#include "qevent.h"
#include "qmenu.h"
#include "qnamespace.h"
#include "qtreeview.h"
#include <QOpenGLWidget>
#include <QWidget>
#include <memory>
// #ifndef EXPLORERMODEL_H
// #define EXPLORERMODEL_H
class ExplorerModel : public QAbstractItemModel {
Q_OBJECT
@ -42,6 +36,8 @@ public:
Qt::DropActions supportedDropActions() const override;
InstanceRef fromIndex(const QModelIndex index) const;
QModelIndex ObjectToIndex(InstanceRef item);
void updateRoot(InstanceRef newRoot);
private:
InstanceRef rootItem;
QModelIndex toIndex(InstanceRef item);

View file

@ -1,11 +1,15 @@
#include "explorerview.h"
#include "explorermodel.h"
#include "mainwindow.h"
#include "../ui_mainwindow.h"
#include "common.h"
#include "objects/base/instance.h"
#include "objects/workspace.h"
#include "qabstractitemmodel.h"
#include "qaction.h"
#include "qnamespace.h"
#include <qaction.h>
#include <qnamespace.h>
#include <qitemselectionmodel.h>
#define M_mainWindow dynamic_cast<MainWindow*>(window())
ExplorerView::ExplorerView(QWidget* parent):
QTreeView(parent),
@ -32,7 +36,23 @@ ExplorerView::ExplorerView(QWidget* parent):
contextMenu.exec(this->viewport()->mapToGlobal(point));
});
connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, [&](const QItemSelection &selected, const QItemSelection &deselected) {
std::vector<InstanceRefWeak> selectedInstances;
selectedInstances.reserve(selected.count()); // This doesn't reserve everything, but should enhance things anyway
for (auto range : selected) {
for (auto index : range.indexes()) {
selectedInstances.push_back(reinterpret_cast<Instance*>(index.internalPointer())->weak_from_this());
}
}
::setSelection(selectedInstances, true);
});
addSelectionListener([&](auto oldSelection, auto newSelection, bool fromExplorer) {
// It's from us, ignore it.
if (fromExplorer) return;
this->clearSelection();
for (InstanceRefWeak inst : newSelection) {
if (inst.expired()) continue;
@ -40,8 +60,6 @@ ExplorerView::ExplorerView(QWidget* parent):
this->selectionModel()->select(index, QItemSelectionModel::SelectionFlag::Select);
}
});
buildContextMenu();
}
ExplorerView::~ExplorerView() {
@ -50,19 +68,23 @@ ExplorerView::~ExplorerView() {
void ExplorerView::keyPressEvent(QKeyEvent* event) {
switch (event->key()) {
case Qt::Key_Delete:
actionDelete->trigger();
M_mainWindow->ui->actionDelete->trigger();
break;
}
}
void ExplorerView::buildContextMenu() {
// This will leak memory. Anyway...
contextMenu.addAction(this->actionDelete = new QAction(QIcon("assets/icons/editor/delete"), "Delete"));
contextMenu.addAction(M_mainWindow->ui->actionDelete);
contextMenu.addSeparator();
contextMenu.addAction(M_mainWindow->ui->actionCopy);
contextMenu.addAction(M_mainWindow->ui->actionCut);
contextMenu.addAction(M_mainWindow->ui->actionPaste);
contextMenu.addAction(M_mainWindow->ui->actionPasteInto);
contextMenu.addSeparator();
contextMenu.addAction(M_mainWindow->ui->actionSaveModel);
contextMenu.addAction(M_mainWindow->ui->actionInsertModel);
}
connect(actionDelete, &QAction::triggered, this, [&]() {
QModelIndexList selectedIndexes = this->selectionModel()->selectedIndexes();
for (QModelIndex index : selectedIndexes) {
model.fromIndex(index)->SetParent(std::nullopt);
}
});
void ExplorerView::updateRoot(InstanceRef newRoot) {
model.updateRoot(newRoot);
}

View file

@ -20,18 +20,10 @@ public:
void keyPressEvent(QKeyEvent*) override;
// void dropEvent(QDropEvent*) override;
void buildContextMenu();
void updateRoot(InstanceRef newRoot);
private:
ExplorerModel model;
QMenu contextMenu;
// TODO: Move these to a separate top-level namespace so these can be
// accessed from multiple locations
QAction* actionDelete;
QAction* actionCopy;
QAction* actionCut;
QAction* actionPaste;
QAction* actionPasteInto;
QAction* actionSelectChildren;
void buildContextMenu();
};

View file

@ -56,10 +56,10 @@ bool PropertiesModel::setData(const QModelIndex &index, const QVariant &value, i
switch (role) {
case Qt::EditRole:
if (meta.type != &Data::String::TYPE)
if (!meta.type->fromString)
return false;
selectedItem->SetPropertyValue(propertyName, value.toString().toStdString());
selectedItem->SetPropertyValue(propertyName, meta.type->fromString(value.toString().toStdString()));
return true;
case Qt::CheckStateRole:
if (meta.type != &Data::Bool::TYPE)

4
run.sh
View file

@ -1,7 +1,9 @@
if [ $# -eq 0 ] || ([ "$1" != "editor" ] && [ "$1" != "client" ]); then echo "Argument missing, must be 'client' or 'editor'"; exit; fi
[ "$2" = "-debug" ] && CMAKE_OPTS=-DCMAKE_BUILD_TYPE=Debug
[ "$2" = "-release" ] && CMAKE_OPTS=-DCMAKE_BUILD_TYPE=Release
[ "$2" = "-reldbg" ] && CMAKE_OPTS=-DCMAKE_BUILD_TYPE=RelWithDebInfo
[ "$3" = "-gdb" ] && PRE_COMMAND="gdb -ex run "
cmake $CMAKE_OPTS . && cmake --build . && $PRE_COMMAND ./bin/$1
cmake -Bbuild $CMAKE_OPTS . && (cd build; cmake --build .; cd ..) && $PRE_COMMAND ./build/bin/$1

View file

@ -1,47 +0,0 @@
#include "defaultmeshes.h"
Mesh* CUBE_MESH;
void initMeshes() {
static float vertices[] = {
// positions // normals // texture coords
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
};
CUBE_MESH = new Mesh(36, vertices);
}

78
tools/genmesh.py Executable file
View file

@ -0,0 +1,78 @@
#!/usr/bin/python3
# Default mesh generator
# Input from wavefront obj file
# Usage: ./genmesh.py mesh.obj > mesh.cpp
import sys
HEADER = """
static float CUBE_VERTICES[] = {
// positions // normals // texture coords
"""
FOOTER = """
};"""
file = open(sys.argv[1], "r")
vert_coords = []
vert_norms = []
vert_uvs = []
out_vertices = []
min_coords: tuple[float, float, float] | None = None
max_coords: tuple[float, float, float] | None = None
def normalize(x, y, z):
assert min_coords
assert max_coords
return ((x-max_coords[0])/(max_coords[0]-min_coords[0])+0.5, (y-max_coords[1])/(max_coords[1]-min_coords[1])+0.5, (z-max_coords[2])/(max_coords[2]-min_coords[2])+0.5)
for line in file:
if line.startswith('v '):
coords = line.split(' ')[1:]
coords = (float(coords[0]), float(coords[1]), float(coords[2]))
vert_coords.append(coords)
if not min_coords: min_coords = coords
if not max_coords: max_coords = coords
if coords[0] > max_coords[0]: max_coords = (coords[0], max_coords[1], max_coords[2])
if coords[1] > max_coords[1]: max_coords = (max_coords[0], coords[1], max_coords[2])
if coords[2] > max_coords[2]: max_coords = (max_coords[0], max_coords[1], coords[2])
if coords[0] < min_coords[0]: min_coords = (coords[0], min_coords[1], min_coords[2])
if coords[1] < min_coords[1]: min_coords = (min_coords[0], coords[1], min_coords[2])
if coords[2] < min_coords[2]: min_coords = (min_coords[0], min_coords[1], coords[2])
if line.startswith('vn '):
coords = line.split(' ')[1:]
vert_norms.append((float(coords[0]), float(coords[1]), float(coords[2])))
if line.startswith('vt '):
coords = line.split(' ')[1:]
vert_uvs.append((float(coords[0]), float(coords[1])))
if line.startswith('f '):
verts = line.split(' ')[1:]
for vert in verts:
coords, uv, normal = vert.split('/')
coords, uv, normal = int(coords), int(uv), int(normal)
coords, uv, normal = vert_coords[coords-1], vert_uvs[uv-1], vert_norms[normal-1]
coords = normalize(*coords)
# for coord in [*normal]:
# if coord > greatest_coord: greatest_coord = coord
# if coord < least_coord: least_coord = coord
out_vertices.append((coords, normal, uv))
print(HEADER)
for coords, normal, uv in out_vertices:
print(f"\t{coords[0]}, {coords[1]}, {coords[2]},\t{normal[0]}, {normal[1]}, {normal[2]},\t{uv[0]}, {uv[1]},")
print(FOOTER)