Compare commits

..

13 commits

27 changed files with 1424 additions and 1101 deletions

17
.gitignore vendored
View file

@ -1,19 +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
/.gdb_history

View file

@ -1,11 +1,11 @@
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(CMAKE_EXPORT_COMPILE_COMMANDS ON)

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})
find_package(glfw3 REQUIRED)
add_executable(client "src/main.cpp")
target_link_libraries(client PRIVATE openblocks glfw)
target_link_libraries(client PRIVATE ${SDL2_LIBRARIES} openblocks glfw)

View file

@ -1,24 +1,15 @@
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(OpenGL)
find_package(glm CONFIG REQUIRED)
# find_package(assimp REQUIRED)
find_package(ReactPhysics3D REQUIRED)
find_package(pugixml REQUIRED)
file(MAKE_DIRECTORY bin)
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} OpenGL::GL OpenGL::GLU ReactPhysics3D::ReactPhysics3D pugixml::pugixml)
target_link_libraries(openblocks ${GLEW_LIBRARIES} OpenGL::GL ReactPhysics3D::ReactPhysics3D pugixml::pugixml)
target_include_directories(openblocks PUBLIC "src" "../include")

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

@ -10,6 +10,7 @@
// #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)

View file

@ -31,6 +31,7 @@ namespace Data {
~CFrame();
static const CFrame IDENTITY;
static const CFrame YToZ;
virtual const TypeInfo& GetType() const override;
static const TypeInfo TYPE;

View file

@ -67,8 +67,9 @@ 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
rp3d::RigidBody* body = world->createRigidBody(cframe);
body->addCollider(common.createBoxShape(Data::Vector3(.5, .5, .5)), rp3d::Transform::identity());
// 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)) {
@ -80,4 +81,10 @@ std::optional<HandleFace> Handles::RaycastHandle(rp3d::Ray ray) {
}
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

@ -24,12 +24,20 @@ class HandleFace {
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(); };
@ -39,6 +47,7 @@ public:
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>(); };

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;

File diff suppressed because it is too large Load diff

View file

@ -67,6 +67,11 @@ void renderInit(GLFWwindow* window, int width, int height) {
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();
@ -111,12 +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);
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 (inst->name == "camera") model = camera.getLookAt();
if (part->name == "camera") model = camera.getLookAt();
model = glm::scale(model, part->size);
shader->set("model", model);
shader->set("material", Material {
@ -127,14 +160,16 @@ 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();
@ -155,6 +190,7 @@ void renderHandles() {
if (!editorToolHandles->adornee.has_value() || !editorToolHandles->active) return;
glDepthMask(GL_TRUE);
glCullFace(GL_BACK);
// Use shader
handleShader->use();
@ -177,7 +213,7 @@ void renderHandles() {
for (auto face : HandleFace::Faces) {
glm::mat4 model = editorToolHandles->GetCFrameOfHandle(face);
model = glm::scale(model, glm::vec3(1,1,1));
model = glm::scale(model, (glm::vec3)editorToolHandles->HandleSize(face));
handleShader->set("model", model);
handleShader->set("material", Material {
.diffuse = glm::abs(face.normal),
@ -187,11 +223,18 @@ void renderHandles() {
glm::mat3 normalMatrix = glm::mat3(glm::transpose(glm::inverse(model)));
handleShader->set("normalMatrix", normalMatrix);
ARROW_MESH->bind();
glDrawArrays(GL_TRIANGLES, 0, ARROW_MESH->vertexCount);
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));

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)
@ -65,6 +65,16 @@ endif()
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
# explicit, fixed bundle identifier manually though.
@ -86,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

@ -138,7 +138,9 @@ void MainGLWidget::handleHandleDrag(QMouseEvent* evt) {
// Apply snapping in the current frame
glm::vec3 diff = centerPoint - (glm::vec3)editorToolHandles->adornee->lock()->position();
if (snappingFactor()) diff = frame * (glm::round(glm::vec3(frame.Inverse() * diff) / snappingFactor()) * snappingFactor());
// 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;

View file

@ -12,15 +12,20 @@
#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"
@ -36,7 +41,7 @@ MainWindow::MainWindow(QWidget *parent)
timer.start(33, this);
setMouseTracking(true);
ConnectSelectionChangeHandler();
ui->explorerView->buildContextMenu();
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(); });
@ -67,21 +72,124 @@ 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
@ -126,17 +234,6 @@ MainWindow::MainWindow(QWidget *parent)
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()) {
@ -167,6 +264,23 @@ void MainWindow::updateToolbars() {
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,7 @@
#include "qmenu.h"
#include <QMainWindow>
#include <QLineEdit>
#include <qfiledialog.h>
enum SelectedTool {
SELECT,
@ -44,6 +45,7 @@ private:
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>22</height>
<height>30</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
@ -115,6 +115,12 @@
<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"/>
@ -315,6 +321,113 @@
<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,13 +1,16 @@
#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),
model(ExplorerModel(std::dynamic_pointer_cast<Instance>(dataModel))) {
@ -57,8 +60,6 @@ ExplorerView::ExplorerView(QWidget* parent):
this->selectionModel()->select(index, QItemSelectionModel::SelectionFlag::Select);
}
});
buildContextMenu();
}
ExplorerView::~ExplorerView() {
@ -67,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