Initial Commit
This commit is contained in:
commit
6088fc29ed
978 changed files with 353647 additions and 0 deletions
62
src/BookmarkManager.cpp
Normal file
62
src/BookmarkManager.cpp
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
#include "BookmarkManager.hpp"
|
||||
#include "Helpers/Networking/NetLibrary.hpp"
|
||||
#include "World.hpp"
|
||||
#include "Server/CommandList.hpp"
|
||||
#include "MainProgram.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
BookmarkManager::BookmarkManager(World& w):
|
||||
world(w)
|
||||
{}
|
||||
|
||||
void BookmarkManager::init_client_callbacks() {
|
||||
world.con.client_add_recv_callback(CLIENT_NEW_BOOKMARK, [&](cereal::PortableBinaryInputArchive& message) {
|
||||
std::string bName;
|
||||
message(bName);
|
||||
auto& b = bookmarks[bName];
|
||||
message(b);
|
||||
changed = true;
|
||||
});
|
||||
world.con.client_add_recv_callback(CLIENT_REMOVE_BOOKMARK, [&](cereal::PortableBinaryInputArchive& message) {
|
||||
std::string name;
|
||||
message(name);
|
||||
bookmarks.erase(name);
|
||||
changed = true;
|
||||
});
|
||||
}
|
||||
|
||||
void BookmarkManager::add_bookmark(const std::string& name) {
|
||||
Bookmark b{world.drawData.cam.c, world.main.window.size.cast<int32_t>()};
|
||||
bookmarks[name] = b;
|
||||
world.con.client_send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_NEW_BOOKMARK, name, b);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
void BookmarkManager::remove_bookmark(const std::string& name) {
|
||||
bookmarks.erase(name);
|
||||
world.con.client_send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_REMOVE_BOOKMARK, name);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
void BookmarkManager::jump_to_bookmark(const std::string& name) {
|
||||
auto it = bookmarks.find(name);
|
||||
if(it != bookmarks.end()) {
|
||||
auto& b = it->second;
|
||||
world.drawData.cam.smooth_move_to(world, b.coords, b.windowSize.cast<float>());
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::string>& BookmarkManager::sorted_names() {
|
||||
if(changed) {
|
||||
sortedNames.clear();
|
||||
for(auto& [k, v] : bookmarks)
|
||||
sortedNames.emplace_back(k);
|
||||
std::sort(sortedNames.begin(), sortedNames.end());
|
||||
changed = false;
|
||||
}
|
||||
return sortedNames;
|
||||
}
|
||||
|
||||
const std::unordered_map<std::string, Bookmark>& BookmarkManager::bookmark_list() {
|
||||
return bookmarks;
|
||||
}
|
||||
34
src/BookmarkManager.hpp
Normal file
34
src/BookmarkManager.hpp
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#pragma once
|
||||
#include <Helpers/Serializers.hpp>
|
||||
#include <cereal/types/string.hpp>
|
||||
#include "SharedTypes.hpp"
|
||||
#include "CoordSpaceHelper.hpp"
|
||||
|
||||
struct Bookmark {
|
||||
CoordSpaceHelper coords;
|
||||
Vector<int32_t, 2> windowSize;
|
||||
template <class Archive> void serialize(Archive& a) {
|
||||
a(coords, windowSize);
|
||||
}
|
||||
};
|
||||
|
||||
class World;
|
||||
|
||||
class BookmarkManager {
|
||||
public:
|
||||
BookmarkManager(World& w);
|
||||
void init_client_callbacks();
|
||||
void add_bookmark(const std::string& name);
|
||||
void remove_bookmark(const std::string& name);
|
||||
void jump_to_bookmark(const std::string& name);
|
||||
const std::unordered_map<std::string, Bookmark>& bookmark_list();
|
||||
template <typename Archive> void serialize(Archive& a) {
|
||||
a(bookmarks);
|
||||
}
|
||||
const std::vector<std::string>& sorted_names();
|
||||
private:
|
||||
bool changed = true;
|
||||
std::vector<std::string> sortedNames;
|
||||
std::unordered_map<std::string, Bookmark> bookmarks;
|
||||
World& world;
|
||||
};
|
||||
156
src/CollabList.hpp
Normal file
156
src/CollabList.hpp
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
#pragma once
|
||||
#include "SharedTypes.hpp"
|
||||
|
||||
template <typename T, typename IDType> class CollabList {
|
||||
public:
|
||||
CollabList(const std::function<IDType()>& initGetNewIDFunc):
|
||||
getNewIDFunc(initGetNewIDFunc)
|
||||
{}
|
||||
|
||||
struct ObjectInfo {
|
||||
bool syncIgnore;
|
||||
IDType id;
|
||||
T obj;
|
||||
};
|
||||
|
||||
const std::vector<std::shared_ptr<ObjectInfo>>& client_list() const { return clientSideList; }
|
||||
std::vector<std::shared_ptr<ObjectInfo>>& client_list() { return clientSideList; }
|
||||
const std::vector<std::shared_ptr<ObjectInfo>>& server_list() const { return serverSideList; }
|
||||
|
||||
std::function<void()> updateCallback;
|
||||
|
||||
void set_server_from_client_list() {
|
||||
serverSideList = clientSideList;
|
||||
for(auto& obj : serverSideList)
|
||||
obj->syncIgnore = false;
|
||||
}
|
||||
|
||||
T* get_item_by_id(IDType id) {
|
||||
auto clientFound = std::find_if(clientSideList.begin(), clientSideList.end(), [&](const auto& obj) {
|
||||
return (obj->id == id);
|
||||
});
|
||||
if(clientFound != clientSideList.end())
|
||||
return &((*clientFound)->obj);
|
||||
auto serverFound = std::find_if(serverSideList.begin(), serverSideList.end(), [&](const auto& obj) {
|
||||
return (obj->id == id);
|
||||
});
|
||||
if(serverFound != serverSideList.end())
|
||||
return &((*serverFound)->obj);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
IDType get_id(const T& item) {
|
||||
auto clientFound = std::find_if(clientSideList.begin(), clientSideList.end(), [&](const auto& obj) {
|
||||
return (obj->obj == item);
|
||||
});
|
||||
if(clientFound != clientSideList.end())
|
||||
return (*clientFound)->id;
|
||||
return {0, 0};
|
||||
}
|
||||
|
||||
void client_erase_if(const std::function<bool(uint64_t, const std::shared_ptr<ObjectInfo>&)>& func) {
|
||||
for(int64_t i = 0; i < (int64_t)clientSideList.size(); i++) {
|
||||
if(func((uint64_t)i, clientSideList[i])) {
|
||||
clientSideList.erase(clientSideList.begin() + i);
|
||||
i--;
|
||||
if(updateCallback)
|
||||
updateCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IDType client_insert(uint64_t pos, const T& item) {
|
||||
auto newObj(std::make_shared<ObjectInfo>());
|
||||
newObj->syncIgnore = true;
|
||||
newObj->id = getNewIDFunc();
|
||||
newObj->obj = item;
|
||||
uint64_t minPos = std::min<uint64_t>(static_cast<uint64_t>(clientSideList.size()), pos);
|
||||
clientSideList.insert(clientSideList.begin() + minPos, newObj);
|
||||
if(updateCallback)
|
||||
updateCallback();
|
||||
return newObj->id;
|
||||
}
|
||||
void client_erase(const T& item, IDType& erasedID) {
|
||||
std::erase_if(clientSideList, [&](const auto& obj) {
|
||||
if(obj->obj == item) {
|
||||
erasedID = obj->id;
|
||||
obj->syncIgnore = true;
|
||||
if(updateCallback)
|
||||
updateCallback();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
bool server_insert(uint64_t pos, IDType id, const T& item) {
|
||||
auto foundInClient = std::find_if(clientSideList.begin(), clientSideList.end(), [&](const auto& clientObj) {
|
||||
return clientObj->id == id;
|
||||
});
|
||||
if(foundInClient != clientSideList.end()) {
|
||||
std::shared_ptr<ObjectInfo>& o = *foundInClient;
|
||||
serverSideList.insert(serverSideList.begin() + pos, o);
|
||||
o->syncIgnore = false;
|
||||
sync();
|
||||
return false;
|
||||
}
|
||||
auto newObj(std::make_shared<ObjectInfo>());
|
||||
newObj->syncIgnore = false;
|
||||
newObj->id = id;
|
||||
newObj->obj = item;
|
||||
serverSideList.insert(serverSideList.begin() + pos, newObj);
|
||||
uint64_t minPos = std::min<uint64_t>(pos, clientSideList.size());
|
||||
clientSideList.insert(clientSideList.begin() + minPos, newObj);
|
||||
sync();
|
||||
return true;
|
||||
}
|
||||
void server_erase(IDType id) {
|
||||
std::erase_if(serverSideList, [&](const auto& obj) {
|
||||
if(obj->id == id) {
|
||||
obj->syncIgnore = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
std::erase_if(clientSideList, [&](const auto& obj) {
|
||||
if(obj->id == id) {
|
||||
obj->syncIgnore = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
sync();
|
||||
}
|
||||
void init_emplace_back(IDType id, const T& item) {
|
||||
auto newObj(std::make_shared<ObjectInfo>());
|
||||
newObj->syncIgnore = false;
|
||||
newObj->id = id;
|
||||
newObj->obj = item;
|
||||
serverSideList.emplace_back(newObj);
|
||||
clientSideList.emplace_back(newObj);
|
||||
if(updateCallback)
|
||||
updateCallback();
|
||||
}
|
||||
private:
|
||||
void sync() {
|
||||
std::vector<std::pair<uint64_t, std::shared_ptr<ObjectInfo>>> ignoredToReinsert;
|
||||
for(uint64_t i = 0; i < clientSideList.size(); i++)
|
||||
if(clientSideList[i]->syncIgnore)
|
||||
ignoredToReinsert.emplace_back(std::pair<uint64_t, std::shared_ptr<ObjectInfo>>{i, clientSideList[i]});
|
||||
clientSideList = serverSideList;
|
||||
std::erase_if(clientSideList, [&](const auto& obj) {
|
||||
return obj->syncIgnore;
|
||||
});
|
||||
for(auto& p : ignoredToReinsert) {
|
||||
if(p.first > clientSideList.size())
|
||||
clientSideList.emplace_back(p.second);
|
||||
else
|
||||
clientSideList.insert(clientSideList.begin() + p.first, p.second);
|
||||
}
|
||||
if(updateCallback)
|
||||
updateCallback();
|
||||
}
|
||||
std::function<IDType()> getNewIDFunc;
|
||||
std::vector<std::shared_ptr<ObjectInfo>> clientSideList;
|
||||
std::vector<std::shared_ptr<ObjectInfo>> serverSideList;
|
||||
};
|
||||
498
src/CollabTextBox/CollabTextBox.cpp
Normal file
498
src/CollabTextBox/CollabTextBox.cpp
Normal file
|
|
@ -0,0 +1,498 @@
|
|||
// This file has been copied from the Skia library, and may be slightly modified. Please check the NOTICE file for more details
|
||||
|
||||
#include "CollabTextBox.hpp"
|
||||
#include "CollabTextBoxShaper.hpp"
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include <include/core/SkPath.h>
|
||||
#include <src/base/SkUTF.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cassert>
|
||||
#include <utility>
|
||||
|
||||
using namespace CollabTextBox;
|
||||
|
||||
static inline SkRect offset(SkRect r, SkIPoint p) {
|
||||
return r.makeOffset((float)p.x(), (float)p.y());
|
||||
}
|
||||
|
||||
static constexpr SkRect kUnsetRect{-FLT_MAX, -FLT_MAX, -FLT_MAX, -FLT_MAX};
|
||||
|
||||
//static bool valid_utf8(const char* ptr, size_t size) { return SkUTF::CountUTF8(ptr, size) >= 0; }
|
||||
|
||||
// Kind of like Python's readlines(), but without any allocation.
|
||||
// Calls f() on each line.
|
||||
// F is [](const char*, size_t) -> void
|
||||
template <typename F>
|
||||
static void readlines(const void* data, size_t size, F f) {
|
||||
const char* start = (const char*)data;
|
||||
const char* end = start + size;
|
||||
const char* ptr = start;
|
||||
while (ptr < end) {
|
||||
while (*ptr++ != '\n' && ptr < end) {}
|
||||
size_t len = ptr - start;
|
||||
assert(len > 0);
|
||||
f(start, len);
|
||||
start = ptr;
|
||||
}
|
||||
}
|
||||
|
||||
static std::string remove_newline(const char* str, size_t len) {
|
||||
return assert((str != nullptr) || (len == 0)), std::string(str, (len > 0 && str[len - 1] == '\n') ? len - 1 : len);
|
||||
}
|
||||
|
||||
void Editor::markDirty(TextParagraph* line) {
|
||||
line->fBlob = nullptr;
|
||||
line->fShaped = false;
|
||||
line->fWordBoundaries = std::vector<bool>();
|
||||
}
|
||||
|
||||
void Editor::setFont(SkFont font) {
|
||||
if (font != fFont) {
|
||||
fFont = std::move(font);
|
||||
fNeedsReshape = true;
|
||||
for (auto& l : fLines) { this->markDirty(&l); }
|
||||
}
|
||||
}
|
||||
|
||||
void Editor::setFontMgr(sk_sp<SkFontMgr> fontMgr) {
|
||||
if(fontMgr != fFontMgr) {
|
||||
fFontMgr = fontMgr;
|
||||
fNeedsReshape = true;
|
||||
for (auto& l : fLines) { this->markDirty(&l); }
|
||||
}
|
||||
}
|
||||
|
||||
void Editor::setWidth(int w) {
|
||||
if (fWidth != w) {
|
||||
fWidth = w;
|
||||
fNeedsReshape = true;
|
||||
for (auto& l : fLines) { this->markDirty(&l); }
|
||||
}
|
||||
}
|
||||
static SkPoint to_point(SkIPoint p) { return {(float)p.x(), (float)p.y()}; }
|
||||
|
||||
TextPosition Editor::getPosition(SkIPoint xy) {
|
||||
// Clamp input coordinates to stop function from hanging
|
||||
xy.fX = std::clamp(xy.fX, 0, fWidth - 1);
|
||||
xy.fY = std::clamp(xy.fY, 0, fHeight - 1);
|
||||
|
||||
TextPosition approximatePosition{0, 0};
|
||||
this->reshapeAll();
|
||||
for (size_t j = 0; j < fLines.size(); ++j) {
|
||||
const TextParagraph& line = fLines[j];
|
||||
SkIRect lineRect = {0,
|
||||
line.fOrigin.y(),
|
||||
fWidth,
|
||||
j + 1 < fLines.size() ? fLines[j + 1].fOrigin.y() : INT_MAX};
|
||||
if (const SkTextBlob* b = line.fBlob.get()) {
|
||||
SkIRect r = b->bounds().roundOut();
|
||||
r.offset(line.fOrigin);
|
||||
lineRect.join(r);
|
||||
}
|
||||
if (!lineRect.contains(xy.x(), xy.y())) {
|
||||
continue;
|
||||
}
|
||||
SkPoint pt = to_point(xy - line.fOrigin);
|
||||
const std::vector<SkRect>& pos = line.fCursorPos;
|
||||
for (size_t i = 0; i < pos.size(); ++i) {
|
||||
if (pos[i] != kUnsetRect && pos[i].contains(pt.x(), pt.y())) {
|
||||
return TextPosition{i, j};
|
||||
}
|
||||
}
|
||||
approximatePosition = {xy.x() <= line.fOrigin.x() ? 0 : line.fText.size(), j};
|
||||
}
|
||||
return approximatePosition;
|
||||
}
|
||||
|
||||
bool CollabTextBox::is_utf8_continuation(char v) {
|
||||
return ((unsigned char)v & 0b11000000) ==
|
||||
0b10000000;
|
||||
}
|
||||
|
||||
const char* CollabTextBox::next_utf8(const char* p, const char* end) {
|
||||
if (p < end) {
|
||||
do {
|
||||
++p;
|
||||
} while (p < end && is_utf8_continuation(*p));
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
const char* CollabTextBox::align_utf8(const char* p, const char* begin) {
|
||||
while (p > begin && is_utf8_continuation(*p)) {
|
||||
--p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
const char* CollabTextBox::prev_utf8(const char* p, const char* begin) {
|
||||
return p > begin ? align_utf8(p - 1, begin) : begin;
|
||||
}
|
||||
|
||||
SkRect Editor::getLocation(TextPosition cursor) {
|
||||
this->reshapeAll();
|
||||
cursor = this->move(Movement::kNowhere, cursor);
|
||||
if (fLines.size() > 0) {
|
||||
const TextParagraph& cLine = fLines[cursor.fParagraphIndex];
|
||||
SkRect pos = {0, 0, 0, 0};
|
||||
if (cursor.fTextByteIndex < cLine.fCursorPos.size()) {
|
||||
pos = cLine.fCursorPos[cursor.fTextByteIndex];
|
||||
}
|
||||
pos.fRight = pos.fLeft + 1;
|
||||
pos.fLeft -= 1;
|
||||
return offset(pos, cLine.fOrigin);
|
||||
}
|
||||
return SkRect{0, 0, 0, 0};
|
||||
}
|
||||
|
||||
static size_t count_char(const std::string& string, char value) {
|
||||
return std::ranges::count(string, value);
|
||||
}
|
||||
|
||||
TextPosition Editor::insert(TextPosition pos, std::string_view utf8Text) {
|
||||
if (/*!valid_utf8(utf8Text, byteLen) ||*/ utf8Text.size() == 0) { // dont check for valid strings for now
|
||||
return pos;
|
||||
}
|
||||
pos = this->move(Movement::kNowhere, pos);
|
||||
fNeedsReshape = true;
|
||||
if (pos.fParagraphIndex < fLines.size()) {
|
||||
fLines[pos.fParagraphIndex].fText.insert(pos.fTextByteIndex, utf8Text.data(), utf8Text.size());
|
||||
this->markDirty(&fLines[pos.fParagraphIndex]);
|
||||
} else {
|
||||
assert(pos.fParagraphIndex == fLines.size());
|
||||
assert(pos.fTextByteIndex == 0);
|
||||
fLines.push_back(TextParagraph(utf8Text));
|
||||
}
|
||||
pos = TextPosition{pos.fTextByteIndex + utf8Text.size(), pos.fParagraphIndex};
|
||||
size_t newlinecount = count_char(fLines[pos.fParagraphIndex].fText, '\n');
|
||||
if (newlinecount > 0) {
|
||||
std::string src = fLines[pos.fParagraphIndex].fText;
|
||||
std::vector<TextParagraph>::const_iterator next = fLines.begin() + pos.fParagraphIndex + 1;
|
||||
fLines.insert(next, newlinecount, TextParagraph());
|
||||
TextParagraph* line = &fLines[pos.fParagraphIndex];
|
||||
readlines(src.c_str(), src.size(), [&line](const char* str, size_t l) {
|
||||
(line++)->fText = remove_newline(str, l);
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate the TextPosition post insert properly
|
||||
pos.fParagraphIndex = pos.fParagraphIndex + newlinecount;
|
||||
size_t lastNewline = utf8Text.find_last_of('\n');
|
||||
if(std::string_view::npos != lastNewline)
|
||||
pos.fTextByteIndex = utf8Text.size() - lastNewline - 1;
|
||||
if(utf8Text == "\n") { // Special case where the string is just a newline
|
||||
pos.fTextByteIndex = 0;
|
||||
pos.fParagraphIndex--;
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
TextPosition Editor::remove(TextPosition pos1, TextPosition pos2) {
|
||||
pos1 = this->move(Movement::kNowhere, pos1);
|
||||
pos2 = this->move(Movement::kNowhere, pos2);
|
||||
auto cmp = [](const TextPosition& u, const TextPosition& v) { return u < v; };
|
||||
TextPosition start = std::min(pos1, pos2, cmp);
|
||||
TextPosition end = std::max(pos1, pos2, cmp);
|
||||
if (start == end || start.fParagraphIndex == fLines.size()) {
|
||||
return start;
|
||||
}
|
||||
fNeedsReshape = true;
|
||||
if (start.fParagraphIndex == end.fParagraphIndex) {
|
||||
assert(end.fTextByteIndex > start.fTextByteIndex);
|
||||
fLines[start.fParagraphIndex].fText.erase(start.fTextByteIndex, end.fTextByteIndex - start.fTextByteIndex);
|
||||
this->markDirty(&fLines[start.fParagraphIndex]);
|
||||
} else {
|
||||
assert(end.fParagraphIndex < fLines.size());
|
||||
auto& line = fLines[start.fParagraphIndex];
|
||||
line.fText.erase(start.fTextByteIndex, line.fText.size() - start.fTextByteIndex);
|
||||
line.fText.insert(start.fTextByteIndex, fLines[end.fParagraphIndex].fText.substr(end.fTextByteIndex));
|
||||
this->markDirty(&line);
|
||||
fLines.erase(fLines.begin() + start.fParagraphIndex + 1,
|
||||
fLines.begin() + end.fParagraphIndex + 1);
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
std::string Editor::get_string() const {
|
||||
std::string toRet;
|
||||
for(const TextParagraph& p : fLines) {
|
||||
toRet += p.fText;
|
||||
toRet += '\n';
|
||||
}
|
||||
if(!toRet.empty())
|
||||
toRet.pop_back();
|
||||
return toRet;
|
||||
}
|
||||
|
||||
std::string Editor::copy(TextPosition pos1, TextPosition pos2) const {
|
||||
pos1 = this->move(Movement::kNowhere, pos1);
|
||||
pos2 = this->move(Movement::kNowhere, pos2);
|
||||
auto cmp = [](const TextPosition& u, const TextPosition& v) { return u < v; };
|
||||
TextPosition start = std::min(pos1, pos2, cmp);
|
||||
TextPosition end = std::max(pos1, pos2, cmp);
|
||||
if(start == end || start.fParagraphIndex == fLines.size())
|
||||
return {};
|
||||
if(start.fParagraphIndex == end.fParagraphIndex) {
|
||||
assert(end.fTextByteIndex > start.fTextByteIndex);
|
||||
auto& str = fLines[start.fParagraphIndex].fText;
|
||||
return str.substr(start.fTextByteIndex, end.fTextByteIndex - start.fTextByteIndex);
|
||||
}
|
||||
assert(end.fParagraphIndex < fLines.size());
|
||||
const std::vector<TextParagraph>::const_iterator firstP = fLines.begin() + start.fParagraphIndex;
|
||||
const std::vector<TextParagraph>::const_iterator lastP = fLines.begin() + end.fParagraphIndex;
|
||||
const auto& first = firstP->fText;
|
||||
const auto& last = lastP->fText;
|
||||
|
||||
std::string toRet;
|
||||
toRet += first.substr(start.fTextByteIndex);
|
||||
for (auto line = firstP + 1; line < lastP; ++line) {
|
||||
toRet += '\n';
|
||||
toRet += line->fText;
|
||||
}
|
||||
toRet += '\n';
|
||||
toRet += last.substr(0, end.fTextByteIndex);
|
||||
return toRet;
|
||||
}
|
||||
|
||||
static inline const char* begin(const std::string& s) { return s.c_str(); }
|
||||
|
||||
static inline const char* end(const std::string& s) { return s.c_str() + s.size(); }
|
||||
|
||||
static size_t align_column(const std::string& str, size_t p) {
|
||||
if(p >= str.size())
|
||||
return str.size();
|
||||
return align_utf8(begin(str) + p, begin(str)) - begin(str);
|
||||
}
|
||||
|
||||
// returns smallest i such that list[i] > value. value > list[i-1]
|
||||
// Use a binary search since list is monotonic
|
||||
template <typename T>
|
||||
static size_t find_first_larger(const std::vector<T>& list, T value) {
|
||||
return (size_t)(std::upper_bound(list.begin(), list.end(), value) - list.begin());
|
||||
}
|
||||
|
||||
static size_t find_closest_x(const std::vector<SkRect>& bounds, float x, size_t b, size_t e) {
|
||||
if (b >= e) {
|
||||
return b;
|
||||
}
|
||||
assert(e <= bounds.size());
|
||||
size_t best_index = b;
|
||||
float best_diff = ::fabsf(bounds[best_index].x() - x);
|
||||
for (size_t i = b + 1; i < e; ++i) {
|
||||
float d = ::fabsf(bounds[i].x() - x);
|
||||
if (d < best_diff) {
|
||||
best_diff = d;
|
||||
best_index = i;
|
||||
}
|
||||
}
|
||||
return best_index;
|
||||
}
|
||||
|
||||
TextPosition Editor::move(Movement move, TextPosition pos) const {
|
||||
if (fLines.empty()) {
|
||||
return {0, 0};
|
||||
}
|
||||
// First thing: fix possible bad input values.
|
||||
if (pos.fParagraphIndex >= fLines.size()) {
|
||||
pos.fParagraphIndex = fLines.size() - 1;
|
||||
pos.fTextByteIndex = fLines[pos.fParagraphIndex].fText.size();
|
||||
} else {
|
||||
pos.fTextByteIndex = align_column(fLines[pos.fParagraphIndex].fText, pos.fTextByteIndex);
|
||||
}
|
||||
|
||||
assert(pos.fParagraphIndex < fLines.size());
|
||||
assert(pos.fTextByteIndex <= fLines[pos.fParagraphIndex].fText.size());
|
||||
|
||||
assert(pos.fTextByteIndex == fLines[pos.fParagraphIndex].fText.size() ||
|
||||
!is_utf8_continuation(fLines[pos.fParagraphIndex].fText.begin()[pos.fTextByteIndex]));
|
||||
|
||||
switch (move) {
|
||||
case Movement::kNowhere:
|
||||
break;
|
||||
case Movement::kLeft:
|
||||
if (0 == pos.fTextByteIndex) {
|
||||
if (pos.fParagraphIndex > 0) {
|
||||
--pos.fParagraphIndex;
|
||||
pos.fTextByteIndex = fLines[pos.fParagraphIndex].fText.size();
|
||||
}
|
||||
} else {
|
||||
const auto& str = fLines[pos.fParagraphIndex].fText;
|
||||
pos.fTextByteIndex =
|
||||
prev_utf8(begin(str) + pos.fTextByteIndex, begin(str)) - begin(str);
|
||||
}
|
||||
break;
|
||||
case Movement::kRight:
|
||||
if (fLines[pos.fParagraphIndex].fText.size() == pos.fTextByteIndex) {
|
||||
if (pos.fParagraphIndex + 1 < fLines.size()) {
|
||||
++pos.fParagraphIndex;
|
||||
pos.fTextByteIndex = 0;
|
||||
}
|
||||
} else {
|
||||
const auto& str = fLines[pos.fParagraphIndex].fText;
|
||||
pos.fTextByteIndex =
|
||||
next_utf8(begin(str) + pos.fTextByteIndex, end(str)) - begin(str);
|
||||
}
|
||||
break;
|
||||
case Movement::kHome:
|
||||
{
|
||||
const std::vector<size_t>& list = fLines[pos.fParagraphIndex].fLineEndOffsets;
|
||||
size_t f = find_first_larger(list, pos.fTextByteIndex);
|
||||
pos.fTextByteIndex = f > 0 ? list[f - 1] : 0;
|
||||
}
|
||||
break;
|
||||
case Movement::kEnd:
|
||||
{
|
||||
const std::vector<size_t>& list = fLines[pos.fParagraphIndex].fLineEndOffsets;
|
||||
size_t f = find_first_larger(list, pos.fTextByteIndex);
|
||||
if (f < list.size()) {
|
||||
pos.fTextByteIndex = list[f] > 0 ? list[f] - 1 : 0;
|
||||
} else {
|
||||
pos.fTextByteIndex = fLines[pos.fParagraphIndex].fText.size();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Movement::kUp:
|
||||
{
|
||||
assert(pos.fTextByteIndex < fLines[pos.fParagraphIndex].fCursorPos.size());
|
||||
float x = fLines[pos.fParagraphIndex].fCursorPos[pos.fTextByteIndex].left();
|
||||
const std::vector<size_t>& list = fLines[pos.fParagraphIndex].fLineEndOffsets;
|
||||
size_t f = find_first_larger(list, pos.fTextByteIndex);
|
||||
// list[f] > value. value > list[f-1]
|
||||
if (f > 0) {
|
||||
// not the first line in paragraph.
|
||||
pos.fTextByteIndex = find_closest_x(fLines[pos.fParagraphIndex].fCursorPos, x,
|
||||
(f == 1) ? 0 : list[f - 2],
|
||||
list[f - 1]);
|
||||
} else if (pos.fParagraphIndex > 0) {
|
||||
--pos.fParagraphIndex;
|
||||
const auto& newLine = fLines[pos.fParagraphIndex];
|
||||
size_t r = newLine.fLineEndOffsets.size();
|
||||
if (r > 0) {
|
||||
pos.fTextByteIndex = find_closest_x(newLine.fCursorPos, x,
|
||||
newLine.fLineEndOffsets[r - 1],
|
||||
newLine.fCursorPos.size());
|
||||
} else {
|
||||
pos.fTextByteIndex = find_closest_x(newLine.fCursorPos, x, 0,
|
||||
newLine.fCursorPos.size());
|
||||
}
|
||||
}
|
||||
pos.fTextByteIndex =
|
||||
align_column(fLines[pos.fParagraphIndex].fText, pos.fTextByteIndex);
|
||||
}
|
||||
break;
|
||||
case Movement::kDown:
|
||||
{
|
||||
const std::vector<size_t>& list = fLines[pos.fParagraphIndex].fLineEndOffsets;
|
||||
float x = fLines[pos.fParagraphIndex].fCursorPos[pos.fTextByteIndex].left();
|
||||
|
||||
size_t f = find_first_larger(list, pos.fTextByteIndex);
|
||||
if (f < list.size()) {
|
||||
const auto& bounds = fLines[pos.fParagraphIndex].fCursorPos;
|
||||
pos.fTextByteIndex = find_closest_x(bounds, x, list[f],
|
||||
f + 1 < list.size() ? list[f + 1]
|
||||
: bounds.size());
|
||||
} else if (pos.fParagraphIndex + 1 < fLines.size()) {
|
||||
++pos.fParagraphIndex;
|
||||
const auto& bounds = fLines[pos.fParagraphIndex].fCursorPos;
|
||||
const std::vector<size_t>& l2 = fLines[pos.fParagraphIndex].fLineEndOffsets;
|
||||
pos.fTextByteIndex = find_closest_x(bounds, x, 0,
|
||||
l2.size() > 0 ? l2[0] : bounds.size());
|
||||
} else {
|
||||
pos.fTextByteIndex = fLines[pos.fParagraphIndex].fText.size();
|
||||
}
|
||||
pos.fTextByteIndex =
|
||||
align_column(fLines[pos.fParagraphIndex].fText, pos.fTextByteIndex);
|
||||
}
|
||||
break;
|
||||
case Movement::kWordLeft:
|
||||
{
|
||||
if (pos.fTextByteIndex == 0) {
|
||||
pos = this->move(Movement::kLeft, pos);
|
||||
break;
|
||||
}
|
||||
const std::vector<bool>& words = fLines[pos.fParagraphIndex].fWordBoundaries;
|
||||
assert(words.size() == fLines[pos.fParagraphIndex].fText.size());
|
||||
do {
|
||||
--pos.fTextByteIndex;
|
||||
} while (pos.fTextByteIndex > 0 && !words[pos.fTextByteIndex]);
|
||||
}
|
||||
break;
|
||||
case Movement::kWordRight:
|
||||
{
|
||||
const std::string& text = fLines[pos.fParagraphIndex].fText;
|
||||
if (pos.fTextByteIndex == text.size()) {
|
||||
pos = this->move(Movement::kRight, pos);
|
||||
break;
|
||||
}
|
||||
const std::vector<bool>& words = fLines[pos.fParagraphIndex].fWordBoundaries;
|
||||
assert(words.size() == text.size());
|
||||
do {
|
||||
++pos.fTextByteIndex;
|
||||
} while (pos.fTextByteIndex < text.size() && !words[pos.fTextByteIndex]);
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
void Editor::paint(SkCanvas* c, PaintOpts options) {
|
||||
this->reshapeAll();
|
||||
if(!c)
|
||||
return;
|
||||
|
||||
c->drawPaint(SkPaint(options.fBackgroundColor));
|
||||
|
||||
if(options.showCursor) {
|
||||
auto& cur = options.cursor;
|
||||
SkPaint selection = SkPaint(SkColor4f{options.cursorColor[0], options.cursorColor[1], options.cursorColor[2], 0.3f});
|
||||
auto cmp = [](const TextPosition& u, const TextPosition& v) { return u < v; };
|
||||
for(TextPosition pos = std::min(cur.selectionBeginPos, cur.selectionEndPos, cmp), end = std::max(cur.selectionBeginPos, cur.selectionEndPos, cmp);
|
||||
pos < end;
|
||||
pos = this->move(Movement::kRight, pos))
|
||||
{
|
||||
assert(pos.fParagraphIndex < fLines.size());
|
||||
const TextParagraph& l = fLines[pos.fParagraphIndex];
|
||||
c->drawRect(offset(l.fCursorPos[pos.fTextByteIndex], l.fOrigin), selection);
|
||||
}
|
||||
|
||||
if(fLines.size() > 0)
|
||||
c->drawRect(Editor::getLocation(cur.pos), SkPaint(options.cursorColor));
|
||||
}
|
||||
|
||||
SkPaint foreground = SkPaint(options.fForegroundColor);
|
||||
for(const TextParagraph& line : fLines) {
|
||||
if(line.fBlob)
|
||||
c->drawTextBlob(line.fBlob.get(), line.fOrigin.x(), line.fOrigin.y(), foreground);
|
||||
}
|
||||
}
|
||||
|
||||
void Editor::reshapeAll() {
|
||||
if(fNeedsReshape && fFontMgr) { // fFontMgr must not be null for text to be reshaped
|
||||
if(fLines.empty())
|
||||
fLines.push_back(TextParagraph());
|
||||
float shape_width = (float)(fWidth);
|
||||
for(TextParagraph& line : fLines) {
|
||||
if(!line.fShaped) {
|
||||
ShapeResult result = shape(line.fText.c_str(), line.fText.size(), fFont, fFontMgr, fLocale, shape_width);
|
||||
line.fBlob = std::move(result.blob);
|
||||
line.fLineEndOffsets = std::move(result.lineBreakOffsets);
|
||||
line.fCursorPos = std::move(result.glyphBounds);
|
||||
line.fWordBoundaries = std::move(result.wordBreaks);
|
||||
line.fHeight = result.verticalAdvance;
|
||||
line.fShaped = true;
|
||||
}
|
||||
}
|
||||
int y = 0;
|
||||
for (TextParagraph& line : fLines) {
|
||||
line.fOrigin = {0, y};
|
||||
y += line.fHeight;
|
||||
}
|
||||
fHeight = y;
|
||||
fNeedsReshape = false;
|
||||
}
|
||||
}
|
||||
|
||||
125
src/CollabTextBox/CollabTextBox.hpp
Normal file
125
src/CollabTextBox/CollabTextBox.hpp
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// This file has been copied from the Skia library, and may be slightly modified. Please check the NOTICE file for more details
|
||||
|
||||
#pragma once
|
||||
#include "CollabTextBoxCursor.hpp"
|
||||
#include <include/core/SkColor.h>
|
||||
#include <include/core/SkFont.h>
|
||||
#include <include/core/SkFontMgr.h>
|
||||
#include <include/core/SkRefCnt.h>
|
||||
#include <include/core/SkTextBlob.h>
|
||||
|
||||
#include <vector>
|
||||
#include <string_view>
|
||||
#include <string>
|
||||
#include <Eigen/Dense>
|
||||
|
||||
class SkCanvas;
|
||||
class SkShaper;
|
||||
|
||||
using namespace Eigen;
|
||||
|
||||
namespace CollabTextBox {
|
||||
struct TextParagraph {
|
||||
TextParagraph(std::string_view t) : fText(t) {}
|
||||
TextParagraph() {}
|
||||
|
||||
std::string fText;
|
||||
sk_sp<const SkTextBlob> fBlob;
|
||||
std::vector<SkRect> fCursorPos;
|
||||
std::vector<size_t> fLineEndOffsets;
|
||||
std::vector<bool> fWordBoundaries;
|
||||
SkIPoint fOrigin = {0, 0};
|
||||
int fHeight = 0;
|
||||
bool fShaped = false;
|
||||
};
|
||||
|
||||
enum class Movement {
|
||||
kNowhere,
|
||||
kLeft,
|
||||
kUp,
|
||||
kRight,
|
||||
kDown,
|
||||
kHome,
|
||||
kEnd,
|
||||
kWordLeft,
|
||||
kWordRight,
|
||||
};
|
||||
|
||||
bool is_utf8_continuation(char v);
|
||||
const char* next_utf8(const char* p, const char* end);
|
||||
const char* align_utf8(const char* p, const char* begin);
|
||||
const char* prev_utf8(const char* p, const char* begin);
|
||||
|
||||
class Editor {
|
||||
public:
|
||||
// total height in canvas display units.
|
||||
int getHeight() const { return fHeight; }
|
||||
|
||||
// set display width in canvas display units
|
||||
void setWidth(int w); // may force re-shape
|
||||
|
||||
// get/set current font (used for shaping and displaying text)
|
||||
const SkFont& font() const { return fFont; }
|
||||
void setFont(SkFont font);
|
||||
|
||||
void setFontMgr(sk_sp<SkFontMgr> fontMgr);
|
||||
|
||||
struct Text {
|
||||
const std::vector<TextParagraph>& fLines;
|
||||
struct Iterator {
|
||||
std::vector<TextParagraph>::const_iterator fPtr;
|
||||
std::string_view operator*() { return std::string_view(fPtr->fText); }
|
||||
void operator++() { ++fPtr; }
|
||||
bool operator!=(const Iterator& other) const { return fPtr != other.fPtr; }
|
||||
};
|
||||
Iterator begin() const { return Iterator{fLines.begin()}; }
|
||||
Iterator end() const { return Iterator{fLines.end()}; }
|
||||
};
|
||||
// Loop over all the lines of text. The lines are not '\0'- or '\n'-terminated.
|
||||
// For example, to dump the entire file to standard output:
|
||||
// for (SkPlainTextEditor::StringView str : editor.text()) {
|
||||
// std::cout.write(str.data, str.size) << '\n';
|
||||
// }
|
||||
Text text() const { return Text{fLines}; }
|
||||
std::string get_string() const;
|
||||
|
||||
// get size of line in canvas display units.
|
||||
int lineHeight(size_t index) const { return fLines[index].fHeight; }
|
||||
|
||||
TextPosition move(Movement move, TextPosition pos) const;
|
||||
TextPosition getPosition(SkIPoint);
|
||||
SkRect getLocation(TextPosition);
|
||||
// insert into current text.
|
||||
TextPosition insert(TextPosition, std::string_view utf8Text);
|
||||
// remove text between two positions
|
||||
TextPosition remove(TextPosition, TextPosition);
|
||||
|
||||
std::string copy(TextPosition pos1, TextPosition pos2) const;
|
||||
size_t lineCount() const { return fLines.size(); }
|
||||
std::string_view line(size_t i) const {
|
||||
return i < fLines.size() ? std::string_view(fLines[i].fText) : std::string_view();
|
||||
}
|
||||
|
||||
struct PaintOpts {
|
||||
SkColor4f fBackgroundColor = {1, 1, 1, 1};
|
||||
SkColor4f fForegroundColor = {0, 0, 0, 1};
|
||||
SkColor4f cursorColor = {1, 0, 0, 1};
|
||||
bool showCursor = false;
|
||||
Cursor cursor;
|
||||
};
|
||||
void paint(SkCanvas* canvas, PaintOpts options);
|
||||
std::vector<TextParagraph> fLines;
|
||||
private:
|
||||
int fWidth = 0;
|
||||
int fHeight = 0;
|
||||
SkFont fFont;
|
||||
sk_sp<SkFontMgr> fFontMgr;
|
||||
bool fNeedsReshape = false;
|
||||
const char* fLocale = "en"; // TODO: make this setable
|
||||
|
||||
void markDirty(TextParagraph*);
|
||||
void reshapeAll();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
33
src/CollabTextBox/CollabTextBoxCursor.hpp
Normal file
33
src/CollabTextBox/CollabTextBoxCursor.hpp
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// This file has been copied from the Skia library, and may be slightly modified. Please check the NOTICE file for more details
|
||||
|
||||
#pragma once
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
|
||||
namespace CollabTextBox {
|
||||
struct TextPosition {
|
||||
// Set to uint32_t values because theyll be saved and sent over networks. Size has to be consistent
|
||||
size_t fTextByteIndex = std::numeric_limits<uint32_t>::max();
|
||||
size_t fParagraphIndex = std::numeric_limits<uint32_t>::max();
|
||||
template <typename Archive> void serialize(Archive& a) {
|
||||
a((uint32_t)fTextByteIndex, (uint32_t)fParagraphIndex);
|
||||
}
|
||||
bool operator==(const TextPosition& o) const = default;
|
||||
};
|
||||
|
||||
static inline bool operator<(const CollabTextBox::TextPosition& u,
|
||||
const CollabTextBox::TextPosition& v) {
|
||||
return u.fParagraphIndex < v.fParagraphIndex ||
|
||||
(u.fParagraphIndex == v.fParagraphIndex && u.fTextByteIndex < v.fTextByteIndex);
|
||||
}
|
||||
|
||||
struct Cursor {
|
||||
TextPosition pos{0, 0};
|
||||
TextPosition selectionBeginPos{0, 0};
|
||||
TextPosition selectionEndPos{0, 0};
|
||||
template <typename Archive> void serialize(Archive& a) {
|
||||
a(pos, selectionBeginPos, selectionEndPos);
|
||||
}
|
||||
bool operator==(const Cursor& o) const = default;
|
||||
};
|
||||
}
|
||||
365
src/CollabTextBox/CollabTextBoxShaper.cpp
Normal file
365
src/CollabTextBox/CollabTextBoxShaper.cpp
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
// This file has been copied from the Skia library, and may be slightly modified. Please check the NOTICE file for more details
|
||||
|
||||
#include "CollabTextBoxShaper.hpp"
|
||||
|
||||
#include <include/core/SkFont.h>
|
||||
#include <include/core/SkFontMetrics.h>
|
||||
#include <include/core/SkFontMgr.h>
|
||||
#include <include/core/SkPoint.h>
|
||||
#include <include/core/SkRefCnt.h>
|
||||
#include <include/core/SkScalar.h>
|
||||
#include <include/core/SkString.h>
|
||||
#include <include/core/SkTextBlob.h>
|
||||
#include <include/core/SkTypes.h>
|
||||
#include <include/private/base/SkTFitsIn.h>
|
||||
#include <modules/skshaper/include/SkShaper.h>
|
||||
#include <src/base/SkUTF.h>
|
||||
|
||||
#include <modules/skshaper/include/SkShaper_harfbuzz.h>
|
||||
#include <modules/skshaper/include/SkShaper_skunicode.h>
|
||||
#include <modules/skshaper/include/SkShaper_factory.h>
|
||||
|
||||
#include <modules/skunicode/include/SkUnicode.h>
|
||||
|
||||
#include <modules/skunicode/include/SkUnicode_icu.h>
|
||||
|
||||
#include <cfloat>
|
||||
#include <climits>
|
||||
#include <cstring>
|
||||
|
||||
sk_sp<SkUnicode> get_unicode_2() {
|
||||
if(auto unicode = SkUnicodes::ICU::Make()) {
|
||||
return unicode;
|
||||
}
|
||||
SkDEBUGFAIL("Cannot make SkUnicode");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
namespace {
|
||||
class RunHandler final : public SkShaper::RunHandler {
|
||||
public:
|
||||
RunHandler(const char* utf8Text, size_t) : fUtf8Text(utf8Text) {}
|
||||
using RunCallback = void (*)(void* context,
|
||||
const char* utf8Text,
|
||||
size_t utf8TextBytes,
|
||||
size_t glyphCount,
|
||||
const SkGlyphID* glyphs,
|
||||
const SkPoint* positions,
|
||||
const uint32_t* clusters,
|
||||
const SkFont& font);
|
||||
void setRunCallback(RunCallback f, void* context) {
|
||||
fCallbackContext = context;
|
||||
fCallbackFunction = f;
|
||||
}
|
||||
|
||||
sk_sp<SkTextBlob> makeBlob();
|
||||
SkPoint endPoint() const { return fOffset; }
|
||||
SkPoint finalPosition() const { return fCurrentPosition; }
|
||||
|
||||
void beginLine() override;
|
||||
void runInfo(const RunInfo&) override;
|
||||
void commitRunInfo() override;
|
||||
SkShaper::RunHandler::Buffer runBuffer(const RunInfo&) override;
|
||||
void commitRunBuffer(const RunInfo&) override;
|
||||
void commitLine() override;
|
||||
|
||||
const std::vector<size_t>& lineEndOffsets() const { return fLineEndOffsets; }
|
||||
|
||||
SkRect finalRect(const SkFont& font) const {
|
||||
if (0 == fMaxRunAscent || 0 == fMaxRunDescent) {
|
||||
SkFontMetrics metrics;
|
||||
font.getMetrics(&metrics);
|
||||
return {fCurrentPosition.x(),
|
||||
fCurrentPosition.y(),
|
||||
fCurrentPosition.x() + font.getSize(),
|
||||
fCurrentPosition.y() + metrics.fDescent - metrics.fAscent};
|
||||
} else {
|
||||
return {fCurrentPosition.x(),
|
||||
fCurrentPosition.y() + fMaxRunAscent,
|
||||
fCurrentPosition.x() + font.getSize(),
|
||||
fCurrentPosition.y() + fMaxRunDescent};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
SkTextBlobBuilder fBuilder;
|
||||
std::vector<size_t> fLineEndOffsets;
|
||||
const SkGlyphID* fCurrentGlyphs = nullptr;
|
||||
const SkPoint* fCurrentPoints = nullptr;
|
||||
void* fCallbackContext = nullptr;
|
||||
RunCallback fCallbackFunction = nullptr;
|
||||
char const * const fUtf8Text;
|
||||
size_t fTextOffset = 0;
|
||||
uint32_t* fClusters = nullptr;
|
||||
int fClusterOffset = 0;
|
||||
int fGlyphCount = 0;
|
||||
SkScalar fMaxRunAscent = 0;
|
||||
SkScalar fMaxRunDescent = 0;
|
||||
SkScalar fMaxRunLeading = 0;
|
||||
SkPoint fCurrentPosition = {0, 0};
|
||||
SkPoint fOffset = {0, 0};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void RunHandler::beginLine() {
|
||||
fCurrentPosition = fOffset;
|
||||
fMaxRunAscent = 0;
|
||||
fMaxRunDescent = 0;
|
||||
fMaxRunLeading = 0;
|
||||
}
|
||||
|
||||
void RunHandler::runInfo(const SkShaper::RunHandler::RunInfo& info) {
|
||||
SkFontMetrics metrics;
|
||||
info.fFont.getMetrics(&metrics);
|
||||
fMaxRunAscent = std::min(fMaxRunAscent, metrics.fAscent);
|
||||
fMaxRunDescent = std::max(fMaxRunDescent, metrics.fDescent);
|
||||
fMaxRunLeading = std::max(fMaxRunLeading, metrics.fLeading);
|
||||
}
|
||||
|
||||
void RunHandler::commitRunInfo() {
|
||||
fCurrentPosition.fY -= fMaxRunAscent;
|
||||
}
|
||||
|
||||
SkShaper::RunHandler::Buffer RunHandler::runBuffer(const RunInfo& info) {
|
||||
int glyphCount = SkTFitsIn<int>(info.glyphCount) ? info.glyphCount : INT_MAX;
|
||||
int utf8RangeSize = SkTFitsIn<int>(info.utf8Range.size()) ? info.utf8Range.size() : INT_MAX;
|
||||
|
||||
const auto& runBuffer = fBuilder.allocRunTextPos(info.fFont, glyphCount, utf8RangeSize);
|
||||
fCurrentGlyphs = runBuffer.glyphs;
|
||||
fCurrentPoints = runBuffer.points();
|
||||
|
||||
if (runBuffer.utf8text && fUtf8Text) {
|
||||
memcpy(runBuffer.utf8text, fUtf8Text + info.utf8Range.begin(), utf8RangeSize);
|
||||
}
|
||||
fClusters = runBuffer.clusters;
|
||||
fGlyphCount = glyphCount;
|
||||
fClusterOffset = info.utf8Range.begin();
|
||||
|
||||
return {runBuffer.glyphs,
|
||||
runBuffer.points(),
|
||||
nullptr,
|
||||
runBuffer.clusters,
|
||||
fCurrentPosition};
|
||||
}
|
||||
|
||||
void RunHandler::commitRunBuffer(const RunInfo& info) {
|
||||
// for (size_t i = 0; i < info.glyphCount; ++i) {
|
||||
// SkASSERT(fClusters[i] >= info.utf8Range.begin());
|
||||
// // this fails for khmer example.
|
||||
// SkASSERT(fClusters[i] < info.utf8Range.end());
|
||||
// }
|
||||
if (fCallbackFunction) {
|
||||
fCallbackFunction(fCallbackContext,
|
||||
fUtf8Text,
|
||||
info.utf8Range.end(),
|
||||
info.glyphCount,
|
||||
fCurrentGlyphs,
|
||||
fCurrentPoints,
|
||||
fClusters,
|
||||
info.fFont);
|
||||
}
|
||||
SkASSERT(0 <= fClusterOffset);
|
||||
for (int i = 0; i < fGlyphCount; ++i) {
|
||||
SkASSERT(fClusters[i] >= (unsigned)fClusterOffset);
|
||||
fClusters[i] -= fClusterOffset;
|
||||
}
|
||||
fCurrentPosition += info.fAdvance;
|
||||
fTextOffset = std::max(fTextOffset, info.utf8Range.end());
|
||||
}
|
||||
|
||||
void RunHandler::commitLine() {
|
||||
if (fLineEndOffsets.empty() || fTextOffset > fLineEndOffsets.back()) {
|
||||
// Ensure that fLineEndOffsets is monotonic.
|
||||
fLineEndOffsets.push_back(fTextOffset);
|
||||
}
|
||||
fOffset += { 0, fMaxRunDescent + fMaxRunLeading - fMaxRunAscent };
|
||||
}
|
||||
|
||||
sk_sp<SkTextBlob> RunHandler::makeBlob() {
|
||||
return fBuilder.make();
|
||||
}
|
||||
|
||||
static SkRect selection_box(const SkFontMetrics& metrics,
|
||||
float advance,
|
||||
SkPoint pos) {
|
||||
if (fabsf(advance) < 1.0f) {
|
||||
advance = copysignf(1.0f, advance);
|
||||
}
|
||||
return SkRect{pos.x(),
|
||||
pos.y() + metrics.fAscent,
|
||||
pos.x() + advance,
|
||||
pos.y() + metrics.fDescent}.makeSorted();
|
||||
}
|
||||
|
||||
static void set_character_bounds(void* context,
|
||||
const char* utf8Text,
|
||||
size_t utf8TextBytes,
|
||||
size_t glyphCount,
|
||||
const SkGlyphID* glyphs,
|
||||
const SkPoint* positions,
|
||||
const uint32_t* clusters,
|
||||
const SkFont& font)
|
||||
{
|
||||
SkASSERT(context);
|
||||
SkASSERT(glyphCount > 0);
|
||||
SkRect* cursors = (SkRect*)context;
|
||||
|
||||
SkFontMetrics metrics;
|
||||
font.getMetrics(&metrics);
|
||||
std::unique_ptr<float[]> advances(new float[glyphCount]);
|
||||
font.getWidths(glyphs, glyphCount, advances.get());
|
||||
|
||||
// Loop over each cluster in this run.
|
||||
size_t clusterStart = 0;
|
||||
for (size_t glyphIndex = 0; glyphIndex < glyphCount; ++glyphIndex) {
|
||||
if (glyphIndex + 1 < glyphCount // more glyphs
|
||||
&& clusters[glyphIndex] == clusters[glyphIndex + 1]) {
|
||||
continue; // multi-glyph cluster
|
||||
}
|
||||
unsigned textBegin = clusters[glyphIndex];
|
||||
unsigned textEnd = utf8TextBytes;
|
||||
for (size_t i = 0; i < glyphCount; ++i) {
|
||||
if (clusters[i] >= textEnd) {
|
||||
textEnd = clusters[i] + 1;
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < glyphCount; ++i) {
|
||||
if (clusters[i] > textBegin && clusters[i] < textEnd) {
|
||||
textEnd = clusters[i];
|
||||
if (textEnd == textBegin + 1) { break; }
|
||||
}
|
||||
}
|
||||
SkASSERT(glyphIndex + 1 > clusterStart);
|
||||
unsigned clusterGlyphCount = glyphIndex + 1 - clusterStart;
|
||||
const SkPoint* clusterGlyphPositions = &positions[clusterStart];
|
||||
const float* clusterAdvances = &advances[clusterStart];
|
||||
clusterStart = glyphIndex + 1; // for next loop
|
||||
|
||||
SkRect clusterBox = selection_box(metrics, clusterAdvances[0], clusterGlyphPositions[0]);
|
||||
for (unsigned i = 1; i < clusterGlyphCount; ++i) { // multiple glyphs
|
||||
clusterBox.join(selection_box(metrics, clusterAdvances[i], clusterGlyphPositions[i]));
|
||||
}
|
||||
if (textBegin + 1 == textEnd) { // single byte, fast path.
|
||||
cursors[textBegin] = clusterBox;
|
||||
continue;
|
||||
}
|
||||
int textCount = textEnd - textBegin;
|
||||
int codePointCount = SkUTF::CountUTF8(utf8Text + textBegin, textCount);
|
||||
if (codePointCount == 1) { // single codepoint, fast path.
|
||||
cursors[textBegin] = clusterBox;
|
||||
continue;
|
||||
}
|
||||
|
||||
float width = clusterBox.width() / codePointCount;
|
||||
SkASSERT(width > 0);
|
||||
const char* ptr = utf8Text + textBegin;
|
||||
const char* end = utf8Text + textEnd;
|
||||
float x = clusterBox.left();
|
||||
while (ptr < end) { // for each codepoint in cluster
|
||||
const char* nextPtr = ptr;
|
||||
SkUTF::NextUTF8(&nextPtr, end);
|
||||
int firstIndex = ptr - utf8Text;
|
||||
float nextX = x + width;
|
||||
cursors[firstIndex] = SkRect{x, clusterBox.top(), nextX, clusterBox.bottom()};
|
||||
x = nextX;
|
||||
ptr = nextPtr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollabTextBox::ShapeResult CollabTextBox::shape(const char* utf8Text,
|
||||
size_t textByteLen,
|
||||
const SkFont& font,
|
||||
sk_sp<SkFontMgr> fontMgr,
|
||||
const char* locale,
|
||||
float width)
|
||||
{
|
||||
ShapeResult result;
|
||||
if (SkUTF::CountUTF8(utf8Text, textByteLen) < 0) {
|
||||
utf8Text = nullptr;
|
||||
textByteLen = 0;
|
||||
}
|
||||
std::unique_ptr<SkShaper> shaper = nullptr;
|
||||
auto unicode = get_unicode_2();
|
||||
|
||||
// USING PRIMITIVE WRAPPER INSTEAD OF HARFBUZZ, BECAUSE HARFBUZZ HAS ISSUES ON EMSCRIPTEN
|
||||
// WE MIGHT HAVE TO CHANGE THIS BACK LATER WHEN THERE'S A FIX
|
||||
//shaper = SkShapers::HB::ShaperDrivenWrapper(unicode, fontMgr);
|
||||
shaper = SkShaper::MakePrimitive();
|
||||
|
||||
SkASSERT(shaper);
|
||||
|
||||
float height = font.getSpacing();
|
||||
RunHandler runHandler(utf8Text, textByteLen);
|
||||
if (textByteLen) {
|
||||
result.glyphBounds.resize(textByteLen);
|
||||
for (SkRect& c : result.glyphBounds) {
|
||||
c = SkRect{-FLT_MAX, -FLT_MAX, -FLT_MAX, -FLT_MAX};
|
||||
}
|
||||
runHandler.setRunCallback(set_character_bounds, result.glyphBounds.data());
|
||||
|
||||
static constexpr uint8_t kBidiLevelLTR = 0;
|
||||
std::unique_ptr<SkShaper::BiDiRunIterator> bidi = nullptr;
|
||||
bidi = SkShapers::unicode::BidiRunIterator(
|
||||
unicode, utf8Text, textByteLen, kBidiLevelLTR);
|
||||
//if (!bidi) {
|
||||
// throw std::runtime_error("[CollabTextBox] SkShaper unicode or harfbuzz not available");
|
||||
// bidi = std::make_unique<SkShaper::TrivialBiDiRunIterator>(kBidiLevelLTR, textByteLen);
|
||||
//}
|
||||
SkASSERT(bidi);
|
||||
|
||||
std::unique_ptr<SkShaper::LanguageRunIterator> language =
|
||||
SkShaper::MakeStdLanguageRunIterator(utf8Text, textByteLen);
|
||||
SkASSERT(language);
|
||||
|
||||
std::unique_ptr<SkShaper::ScriptRunIterator> script =
|
||||
SkShaper::MakeScriptRunIterator(utf8Text, textByteLen, SkSetFourByteTag('Z','z','z','z'));
|
||||
SkASSERT(script);
|
||||
|
||||
std::unique_ptr<SkShaper::FontRunIterator> fontRuns =
|
||||
SkShaper::MakeFontMgrRunIterator(utf8Text, textByteLen, font, fontMgr);
|
||||
SkASSERT(fontRuns);
|
||||
|
||||
shaper->shape(utf8Text,
|
||||
textByteLen,
|
||||
*fontRuns,
|
||||
*bidi,
|
||||
*script,
|
||||
*language,
|
||||
nullptr,
|
||||
0,
|
||||
width,
|
||||
&runHandler);
|
||||
if (runHandler.lineEndOffsets().size() > 1) {
|
||||
result.lineBreakOffsets = runHandler.lineEndOffsets();
|
||||
SkASSERT(result.lineBreakOffsets.size() > 0);
|
||||
result.lineBreakOffsets.pop_back();
|
||||
}
|
||||
height = std::max(height, runHandler.endPoint().y());
|
||||
result.blob = runHandler.makeBlob();
|
||||
}
|
||||
result.glyphBounds.push_back(runHandler.finalRect(font));
|
||||
result.verticalAdvance = (int)ceilf(height);
|
||||
result.wordBreaks = get_utf8_word_boundaries(utf8Text, textByteLen, locale);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<bool> CollabTextBox::get_utf8_word_boundaries(const char* begin, std::size_t byteCount, const char* locale) {
|
||||
auto unicode = get_unicode_2();
|
||||
if (nullptr == unicode) {
|
||||
return {};
|
||||
}
|
||||
std::vector<SkUnicode::Position> positions;
|
||||
if (!unicode->getWords(begin, byteCount, locale, &positions) || byteCount == 0) {
|
||||
return {};
|
||||
}
|
||||
std::vector<bool> result;
|
||||
result.resize(byteCount);
|
||||
for (auto& pos : positions) {
|
||||
if(pos < result.size()) // Sometimes the position to cut at is out of bounds
|
||||
result[pos] = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
21
src/CollabTextBox/CollabTextBoxShaper.hpp
Normal file
21
src/CollabTextBox/CollabTextBoxShaper.hpp
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// This file has been copied from the Skia library, and may be slightly modified. Please check the NOTICE file for more details
|
||||
|
||||
#pragma once
|
||||
#include <include/core/SkFont.h>
|
||||
#include <include/core/SkTextBlob.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace CollabTextBox {
|
||||
struct ShapeResult {
|
||||
sk_sp<SkTextBlob> blob;
|
||||
std::vector<size_t> lineBreakOffsets;
|
||||
std::vector<SkRect> glyphBounds;
|
||||
std::vector<bool> wordBreaks;
|
||||
int verticalAdvance;
|
||||
};
|
||||
|
||||
ShapeResult shape(const char* ut8text, size_t textByteLen, const SkFont& font, sk_sp<SkFontMgr> fontMgr, const char* locale, float width);
|
||||
std::vector<bool> get_utf8_word_boundaries(const char* begin, size_t byteCount, const char* locale);
|
||||
};
|
||||
40
src/ConnectionManager.cpp
Normal file
40
src/ConnectionManager.cpp
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#include "ConnectionManager.hpp"
|
||||
#include "Server/CommandList.hpp"
|
||||
#include <chrono>
|
||||
|
||||
void ConnectionManager::init_local_p2p(World& initWorld, const std::string& serverLocalID) {
|
||||
localServer = std::make_unique<MainServer>(initWorld, serverLocalID);
|
||||
client = std::make_shared<NetClient>(*localServer->netServer);
|
||||
lastKeepAliveTime = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
void ConnectionManager::connect_p2p(const std::string& serverFullID) {
|
||||
client = std::make_shared<NetClient>(serverFullID);
|
||||
lastKeepAliveTime = std::chrono::steady_clock::now();
|
||||
NetLibrary::register_client(client);
|
||||
}
|
||||
|
||||
void ConnectionManager::update() {
|
||||
if(localServer)
|
||||
localServer->update();
|
||||
if(client) {
|
||||
client->update();
|
||||
if(std::chrono::steady_clock::now() - lastKeepAliveTime > std::chrono::seconds(2)) {
|
||||
client->send_items_to_server(UNRELIABLE_COMMAND_CHANNEL, SERVER_KEEP_ALIVE);
|
||||
lastKeepAliveTime = std::chrono::steady_clock::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectionManager::client_add_recv_callback(uint32_t commandID, const NetClientRecvCallback &callback) {
|
||||
if(client)
|
||||
client->add_recv_callback(commandID, callback);
|
||||
}
|
||||
|
||||
bool ConnectionManager::is_client_disconnected() {
|
||||
return client && client->is_disconnected();
|
||||
}
|
||||
|
||||
bool ConnectionManager::is_host_disconnected() {
|
||||
return localServer && localServer->netServer->is_disconnected();
|
||||
}
|
||||
21
src/ConnectionManager.hpp
Normal file
21
src/ConnectionManager.hpp
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#pragma once
|
||||
#include "Server/MainServer.hpp"
|
||||
#include <Helpers/Networking/NetClient.hpp>
|
||||
|
||||
class ConnectionManager {
|
||||
public:
|
||||
void init_local_p2p(World& initWorld, const std::string& serverLocalID);
|
||||
void connect_p2p(const std::string& serverFullID);
|
||||
void update();
|
||||
void client_add_recv_callback(uint32_t commandID, const NetClientRecvCallback &callback);
|
||||
template <typename... Args> void client_send_items_to_server(const std::string& channel, Args&&... items) {
|
||||
if(client)
|
||||
client->send_items_to_server(channel, items...);
|
||||
}
|
||||
bool is_client_disconnected();
|
||||
bool is_host_disconnected();
|
||||
private:
|
||||
std::shared_ptr<NetClient> client;
|
||||
std::unique_ptr<MainServer> localServer;
|
||||
std::chrono::steady_clock::time_point lastKeepAliveTime;
|
||||
};
|
||||
150
src/CoordSpaceHelper.cpp
Normal file
150
src/CoordSpaceHelper.cpp
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
#include "CoordSpaceHelper.hpp"
|
||||
#include "MainProgram.hpp"
|
||||
#include <Helpers/MathExtras.hpp>
|
||||
#include "World.hpp"
|
||||
#include "MainProgram.hpp"
|
||||
|
||||
CoordSpaceHelper::CoordSpaceHelper() { }
|
||||
|
||||
CoordSpaceHelper::CoordSpaceHelper(const WorldVec& initPos, const WorldScalar& initInverseScale, double initRotation):
|
||||
pos(initPos),
|
||||
inverseScale(initInverseScale),
|
||||
rotation(initRotation)
|
||||
{}
|
||||
|
||||
Vector2f CoordSpaceHelper::to_space(const WorldVec& coord) const {
|
||||
return (Rotation2D<double>(-rotation) * ((coord - pos) / inverseScale).cast<double>()).cast<float>();
|
||||
}
|
||||
|
||||
WorldVec CoordSpaceHelper::from_space(Vector2f coord) const {
|
||||
return ((Rotation2D<double>(rotation) * coord.cast<double>()).cast<WorldScalar>() * inverseScale + pos);
|
||||
}
|
||||
|
||||
WorldScalar CoordSpaceHelper::scalar_from_space(float coord) const {
|
||||
return WorldScalar(coord) * inverseScale;
|
||||
}
|
||||
|
||||
float CoordSpaceHelper::scalar_to_space(const WorldScalar& coord) const {
|
||||
return static_cast<float>(coord / inverseScale);
|
||||
}
|
||||
|
||||
WorldVec CoordSpaceHelper::dir_from_space(Vector2f coord) const {
|
||||
return (Rotation2D<double>(rotation) * coord.cast<double>()).cast<WorldScalar>() * inverseScale;
|
||||
}
|
||||
|
||||
Vector2f CoordSpaceHelper::dir_to_space(const WorldVec& coord) const {
|
||||
return (Rotation2D<double>(-rotation) * (coord / inverseScale).cast<double>()).cast<float>();
|
||||
}
|
||||
|
||||
Vector2f CoordSpaceHelper::normalized_dir_to_space(const WorldVec& coord) const {
|
||||
return (Rotation2D<double>(-rotation) * coord.cast<double>()).cast<float>();
|
||||
}
|
||||
|
||||
void CoordSpaceHelper::scale_about_double(const WorldVec& scalePos, double scaleAmount) {
|
||||
if(scaleAmount > 1.0) {
|
||||
WorldScalar worldScale(scaleAmount);
|
||||
inverseScale /= worldScale;
|
||||
WorldVec mVec = pos - scalePos;
|
||||
pos = scalePos + mVec / worldScale;
|
||||
}
|
||||
else {
|
||||
WorldScalar worldScale(1.0 / scaleAmount);
|
||||
inverseScale *= worldScale;
|
||||
WorldVec mVec = pos - scalePos;
|
||||
pos = scalePos + mVec * worldScale;
|
||||
}
|
||||
}
|
||||
|
||||
void CoordSpaceHelper::scale_about(const WorldVec& scalePos, const WorldScalar& scaleAmount, bool flipScale) {
|
||||
if(!flipScale) {
|
||||
inverseScale /= scaleAmount;
|
||||
WorldVec mVec = pos - scalePos;
|
||||
pos = scalePos + mVec / scaleAmount;
|
||||
}
|
||||
else {
|
||||
inverseScale *= scaleAmount;
|
||||
WorldVec mVec = pos - scalePos;
|
||||
pos = scalePos + mVec * scaleAmount;
|
||||
}
|
||||
}
|
||||
|
||||
void CoordSpaceHelper::rotate_about(const WorldVec& rotatePos, double angle) {
|
||||
WorldVec a = pos - rotatePos;
|
||||
FixedPoint::Number<boost::multiprecision::cpp_int, 15> s = FixedPoint::Number<boost::multiprecision::cpp_int, 15>(std::sin(angle));
|
||||
FixedPoint::Number<boost::multiprecision::cpp_int, 15> c = FixedPoint::Number<boost::multiprecision::cpp_int, 15>(std::cos(angle));
|
||||
set_rotation(rotation + angle);
|
||||
pos = {
|
||||
rotatePos.x() + a.x().multiply_different_precision(c) - a.y().multiply_different_precision(s),
|
||||
rotatePos.y() + a.x().multiply_different_precision(s) + a.y().multiply_different_precision(c)
|
||||
};
|
||||
}
|
||||
|
||||
void CoordSpaceHelper::translate(const WorldVec& translation) {
|
||||
pos += translation;
|
||||
}
|
||||
|
||||
void CoordSpaceHelper::scale(const WorldScalar& scaleAmount) {
|
||||
inverseScale /= scaleAmount;
|
||||
}
|
||||
|
||||
void CoordSpaceHelper::set_rotation(double newRotation) {
|
||||
rotation = newRotation;
|
||||
// using circular fmod caused inaccuracies, so these loops might be a better solution
|
||||
while(rotation >= 2.0 * std::numbers::pi)
|
||||
rotation -= 2.0 * std::numbers::pi;
|
||||
while (rotation < 0.0)
|
||||
rotation += 2.0 * std::numbers::pi;
|
||||
}
|
||||
|
||||
std::vector<Vector2f> CoordSpaceHelper::to_space(const std::vector<WorldVec>& coord) const {
|
||||
std::vector<Vector2f> toRet(coord.size());
|
||||
for(size_t i = 0; i < coord.size(); i++)
|
||||
toRet[i] = to_space(coord[i]);
|
||||
return toRet;
|
||||
}
|
||||
|
||||
std::vector<WorldVec> CoordSpaceHelper::from_space(const std::vector<Vector2f>& coord) const {
|
||||
std::vector<WorldVec> toRet(coord.size());
|
||||
for(size_t i = 0; i < coord.size(); i++)
|
||||
toRet[i] = from_space(coord[i]);
|
||||
return toRet;
|
||||
}
|
||||
|
||||
Vector2f CoordSpaceHelper::get_mouse_pos(const World& w) const {
|
||||
return to_space(w.drawData.cam.c.from_space(w.main.input.mouse.pos));
|
||||
}
|
||||
|
||||
//SkMatrix CoordSpaceHelper::get_sk_matrix(const DrawData& drawData) const {
|
||||
// //WorldTransform newTransform = drawData.cam.transform * inverseTransform;
|
||||
// //const auto& newMat = newTransform.matrix();
|
||||
// //return SkMatrix::MakeAll(newMat(0, 0), newMat(0, 1), newMat(0, 2),
|
||||
// // newMat(1, 0), newMat(1, 1), newMat(1, 2),
|
||||
// // 0.0f, 0.0f, 1.0f);
|
||||
// return SkMatrix::I();
|
||||
//}
|
||||
//
|
||||
//SkMatrix CoordSpaceHelper::get_sk_matrix_temp(const WorldTransform& tempTransform, const DrawData& drawData) const {
|
||||
// //WorldTransform newTransform = drawData.cam.transform * tempTransform * inverseTransform;
|
||||
// //const auto& newMat = newTransform.matrix();
|
||||
// //return SkMatrix::MakeAll(newMat(0, 0), newMat(0, 1), newMat(0, 2),
|
||||
// // newMat(1, 0), newMat(1, 1), newMat(1, 2),
|
||||
// // 0.0f, 0.0f, 1.0f);
|
||||
// return SkMatrix::I();
|
||||
//}
|
||||
//
|
||||
|
||||
void CoordSpaceHelper::transform_sk_canvas(SkCanvas* canvas, const DrawData& drawData) const {
|
||||
Vector2f translateSpace = -to_space(drawData.cam.c.pos);
|
||||
// NOTE: Using this commented code will lead to more accurate results with small scales, but that isn't really required in this case since that stuff just wouldn't be rendered
|
||||
//
|
||||
//double aS;
|
||||
//if(inverseScale > drawData.cam.c.inverseScale)
|
||||
// aS = static_cast<double>(inverseScale / drawData.cam.c.inverseScale);
|
||||
//else
|
||||
// aS = 1.0 / static_cast<double>(drawData.cam.c.inverseScale / inverseScale);
|
||||
double aS = static_cast<double>(inverseScale / drawData.cam.c.inverseScale);
|
||||
|
||||
canvas->scale(aS, aS);
|
||||
canvas->rotate((rotation - drawData.cam.c.rotation) * 180.0 / std::numbers::pi);
|
||||
canvas->translate(translateSpace.x(), translateSpace.y());
|
||||
}
|
||||
97
src/CoordSpaceHelper.hpp
Normal file
97
src/CoordSpaceHelper.hpp
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#pragma once
|
||||
#include "SharedTypes.hpp"
|
||||
#include <Helpers/Serializers.hpp>
|
||||
#include <array>
|
||||
#include <Helpers/SCollision.hpp>
|
||||
|
||||
#ifndef IS_SERVER
|
||||
#include "include/core/SkMatrix.h"
|
||||
#include "include/core/SkCanvas.h"
|
||||
#endif
|
||||
|
||||
class World;
|
||||
struct DrawData;
|
||||
|
||||
class CoordSpaceHelper {
|
||||
public:
|
||||
template <typename Archive> void save(Archive& a) const {
|
||||
a(pos, inverseScale, rotation);
|
||||
}
|
||||
template <typename Archive> void load(Archive& a) {
|
||||
a(pos, inverseScale, rotation);
|
||||
}
|
||||
|
||||
CoordSpaceHelper();
|
||||
CoordSpaceHelper(const WorldVec& initPos, const WorldScalar& initInverseScale, double initRotation);
|
||||
|
||||
Vector2f to_space(const WorldVec& coord) const;
|
||||
WorldVec from_space(Vector2f coord) const;
|
||||
WorldScalar scalar_from_space(float coord) const;
|
||||
float scalar_to_space(const WorldScalar& coord) const;
|
||||
WorldVec dir_from_space(Vector2f coord) const;
|
||||
Vector2f dir_to_space(const WorldVec& coord) const;
|
||||
Vector2f normalized_dir_to_space(const WorldVec& coord) const;
|
||||
|
||||
template <typename S, typename T> S collider_to_world(const T& collider) const {
|
||||
return collider.template transform<WorldScalar>(
|
||||
[&coords = *this](const Vector2f& a) {
|
||||
return coords.from_space(a);
|
||||
},
|
||||
[&coords = *this](const float& a) {
|
||||
return coords.scalar_from_space(a);
|
||||
});
|
||||
}
|
||||
|
||||
template <typename T> T collider_to_new_coords(const CoordSpaceHelper newCoords, const T& collider) const {
|
||||
return collider.template transform<float>(
|
||||
[&coords = *this, &newCoords](const Vector2f& a) {
|
||||
return newCoords.to_space(coords.from_space(a));
|
||||
},
|
||||
[&coords = *this, &newCoords](const float& a) {
|
||||
return newCoords.scalar_to_space(coords.scalar_from_space(a));
|
||||
});
|
||||
}
|
||||
|
||||
template <typename S, typename T> S world_collider_to_coords(const T& collider) const {
|
||||
return collider.template transform<float>(
|
||||
[&coords = *this](const WorldVec& a) {
|
||||
return coords.to_space(a);
|
||||
},
|
||||
[&coords = *this](const WorldScalar& a) {
|
||||
return coords.scalar_to_space(a);
|
||||
});
|
||||
}
|
||||
|
||||
//float scalar_to_space(WorldScalar coord) const;
|
||||
std::vector<Vector2f> to_space(const std::vector<WorldVec>& coord) const;
|
||||
std::vector<WorldVec> from_space(const std::vector<Vector2f>& coord) const;
|
||||
|
||||
void translate(const WorldVec& translation);
|
||||
void scale(const WorldScalar& scaleAmount);
|
||||
void rotate_about(const WorldVec& rotatePos, double angle);
|
||||
void scale_about_double(const WorldVec& scalePos, double scaleAmount);
|
||||
void scale_about(const WorldVec& scalePos, const WorldScalar& scaleAmount, bool flipScale);
|
||||
void set_rotation(double newRotation);
|
||||
|
||||
template <size_t S> std::array<Vector2f, S> to_space(const std::array<WorldVec, S>& coord) const {
|
||||
std::array<Vector2f, S> toRet;
|
||||
for(size_t i = 0; i < coord.size(); i++)
|
||||
toRet[i] = to_space(coord[i]);
|
||||
return toRet;
|
||||
}
|
||||
template <size_t S> std::array<WorldVec, S> from_space(const std::array<Vector2f, S>& coord) const {
|
||||
std::array<WorldVec, S> toRet;
|
||||
for(size_t i = 0; i < coord.size(); i++)
|
||||
toRet[i] = from_space(coord[i]);
|
||||
return toRet;
|
||||
}
|
||||
|
||||
Vector2f get_mouse_pos(const World& w) const;
|
||||
//SkMatrix get_sk_matrix_temp(const WorldTransform& tempTransform, const DrawData& drawData) const;
|
||||
//SkMatrix get_sk_matrix(const DrawData& drawData) const;
|
||||
void transform_sk_canvas(SkCanvas* canvas, const DrawData& drawData) const;
|
||||
|
||||
WorldVec pos{0, 0};
|
||||
WorldScalar inverseScale{1};
|
||||
double rotation = 0.0;
|
||||
};
|
||||
174
src/DrawCamera.cpp
Executable file
174
src/DrawCamera.cpp
Executable file
|
|
@ -0,0 +1,174 @@
|
|||
#include "DrawCamera.hpp"
|
||||
#include "Helpers/FixedPoint.hpp"
|
||||
#include "Helpers/MathExtras.hpp"
|
||||
#include "MainProgram.hpp"
|
||||
#include "World.hpp"
|
||||
#include "InputManager.hpp"
|
||||
#include <iostream>
|
||||
|
||||
DrawCamera::DrawCamera():
|
||||
c({0, 0}, WorldScalar(1000000), 0.0)
|
||||
{}
|
||||
|
||||
void DrawCamera::set_based_on_properties(World& w, const WorldVec& newPos, const WorldScalar& newZoom, double newRotate) {
|
||||
c.inverseScale = newZoom;
|
||||
c.pos = newPos;
|
||||
c.set_rotation(newRotate);
|
||||
|
||||
if(changed)
|
||||
changed();
|
||||
|
||||
set_viewing_area(w.main.window.size.cast<float>());
|
||||
}
|
||||
|
||||
void DrawCamera::set_based_on_center(World& w, const WorldVec& newPos, const WorldScalar& newZoom, double newRotate) {
|
||||
c.inverseScale = newZoom;
|
||||
c.set_rotation(newRotate);
|
||||
c.pos = newPos - c.dir_from_space(w.main.window.size.cast<float>() * 0.5f);
|
||||
|
||||
if(changed)
|
||||
changed();
|
||||
|
||||
set_viewing_area(w.main.window.size.cast<float>());
|
||||
}
|
||||
|
||||
void DrawCamera::set_viewing_area(Vector2f viewingAreaNew) {
|
||||
if(viewingArea != viewingAreaNew) {
|
||||
viewingArea = viewingAreaNew;
|
||||
if(changed)
|
||||
changed();
|
||||
}
|
||||
float maxDim = std::max(viewingAreaNew.x(), viewingAreaNew.y());
|
||||
WorldScalar a = WorldScalar(maxDim * 0.5f) * c.inverseScale;
|
||||
WorldVec center = c.from_space(viewingAreaNew * 0.5f);
|
||||
viewingAreaGenerousCollider = SCollision::AABB<WorldScalar>(center - WorldVec{a, a}, center + WorldVec{a, a});
|
||||
}
|
||||
|
||||
void DrawCamera::smooth_move_to(World& w, const CoordSpaceHelper& newCoords, Vector2f windowSize, bool instantJump) {
|
||||
smoothMove.start = c;
|
||||
smoothMove.end = newCoords;
|
||||
smoothMove.endWindowSize = windowSize;
|
||||
smoothMove.startCenter = c.pos + c.dir_from_space(w.main.window.size.cast<float>() * 0.5f);
|
||||
smoothMove.endCenter = newCoords.pos + newCoords.dir_from_space(windowSize * 0.5f);
|
||||
|
||||
float a1(w.main.window.size.x() / windowSize.x());
|
||||
float a2(w.main.window.size.y() / windowSize.y());
|
||||
smoothMove.endUniformZoom = std::max(smoothMove.end.inverseScale.multiply_double((a1 < a2) ? a1 : a2), WorldScalar(1));
|
||||
|
||||
smoothMove.occurring = true;
|
||||
smoothMove.moveTime = (instantJump || w.main.toolbar.jumpTransitionTime <= 0.01f) ? w.main.toolbar.jumpTransitionTime : 0.0f;
|
||||
}
|
||||
|
||||
void DrawCamera::update_main(World& w) {
|
||||
if(smoothMove.occurring) {
|
||||
BezierEasing zoomAnim{w.main.toolbar.jumpTransitionEasing};
|
||||
float smoothTime = smooth_two_way_time(smoothMove.moveTime, w.main.deltaTime, true, w.main.toolbar.jumpTransitionTime);
|
||||
float lerpTime;
|
||||
float rotationLerpTime = zoomAnim(smoothTime);
|
||||
lerpTime = zoomAnim(smoothTime);
|
||||
|
||||
WorldVec mVec = smoothMove.startCenter - smoothMove.endCenter;
|
||||
WorldScalar startLog2 = FixedPoint::log2(smoothMove.start.inverseScale);
|
||||
WorldScalar endLog2 = FixedPoint::log2(smoothMove.endUniformZoom);
|
||||
WorldVec windowCenter = smoothMove.endCenter;
|
||||
if(FixedPoint::abs(startLog2 - endLog2) < WorldScalar(3)) {
|
||||
c.inverseScale = FixedPoint::lerp_double(smoothMove.start.inverseScale, smoothMove.endUniformZoom, lerpTime);
|
||||
if(lerpTime != 1.0)
|
||||
windowCenter += WorldVec{mVec.x() / WorldScalar(1.0 / (1.0 - lerpTime)), mVec.y() / WorldScalar(1.0 / (1.0 - lerpTime))};
|
||||
c.pos = windowCenter - (w.main.window.size.cast<float>() * 0.5f).cast<WorldScalar>() * c.inverseScale;
|
||||
}
|
||||
else {
|
||||
c.inverseScale = FixedPoint::exp2(FixedPoint::lerp_double(startLog2, endLog2, lerpTime));
|
||||
if(smoothMove.start.inverseScale < smoothMove.endUniformZoom) {
|
||||
if(smoothMove.start.inverseScale > c.inverseScale)
|
||||
c.inverseScale = smoothMove.start.inverseScale;
|
||||
else if(smoothMove.endUniformZoom < c.inverseScale)
|
||||
c.inverseScale = smoothMove.endUniformZoom;
|
||||
}
|
||||
else {
|
||||
if(smoothMove.start.inverseScale < c.inverseScale)
|
||||
c.inverseScale = smoothMove.start.inverseScale;
|
||||
else if(smoothMove.endUniformZoom > c.inverseScale)
|
||||
c.inverseScale = smoothMove.endUniformZoom;
|
||||
}
|
||||
WorldScalar denominator = (smoothMove.endUniformZoom - smoothMove.start.inverseScale);
|
||||
windowCenter += WorldVec{mVec.x() - ((mVec.x() * c.inverseScale) / denominator) + ((mVec.x() * smoothMove.start.inverseScale) / denominator),
|
||||
mVec.y() - ((mVec.y() * c.inverseScale) / denominator) + ((mVec.y() * smoothMove.start.inverseScale) / denominator)};
|
||||
c.pos = windowCenter - (w.main.window.size.cast<float>() * 0.5f).cast<WorldScalar>() * c.inverseScale;
|
||||
}
|
||||
|
||||
c.set_rotation(0.0);
|
||||
double midRotate = Rotation2D(smoothMove.start.rotation).slerp(rotationLerpTime, Rotation2D(smoothMove.end.rotation)).smallestPositiveAngle();
|
||||
c.rotate_about(windowCenter, midRotate);
|
||||
|
||||
if(smoothTime >= 1.0f) {
|
||||
c.inverseScale = smoothMove.endUniformZoom;
|
||||
c.pos = smoothMove.endCenter - (w.main.window.size.cast<float>() * 0.5f).cast<WorldScalar>() * c.inverseScale;
|
||||
c.set_rotation(0.0);
|
||||
c.rotate_about(smoothMove.endCenter, smoothMove.end.rotation);
|
||||
smoothMove.occurring = false;
|
||||
}
|
||||
|
||||
if(changed)
|
||||
changed();
|
||||
}
|
||||
else {
|
||||
bool middleClickDown = w.main.input.mouse.middleDown || w.main.input.pen.buttons[w.main.toolbar.tabletOptions.middleClickButton].held;
|
||||
bool newIsMiddleClickZooming = middleClickDown && w.main.input.key(InputManager::KEY_ZOOM_CTRL).held;
|
||||
if(newIsMiddleClickZooming && !isMiddleClickZooming) {
|
||||
startZoomMousePos = w.get_mouse_world_pos();
|
||||
startZoomVal = c.inverseScale;
|
||||
startZoomCameraPos = c.pos;
|
||||
if(changed)
|
||||
changed();
|
||||
}
|
||||
isMiddleClickZooming = newIsMiddleClickZooming;
|
||||
if(isMiddleClickZooming) {
|
||||
WorldScalar zoomFactor(std::pow(1.0 + w.main.toolbar.dragZoomSpeed, w.main.input.mouse.move.y()));
|
||||
c.scale(zoomFactor);
|
||||
if(c.inverseScale < WorldScalar(1))
|
||||
c.inverseScale = WorldScalar(1);
|
||||
else {
|
||||
WorldVec mVec = startZoomCameraPos - startZoomMousePos;
|
||||
WorldScalar mX = c.inverseScale / startZoomVal;
|
||||
c.pos = startZoomMousePos + mVec * mX;
|
||||
}
|
||||
if(changed)
|
||||
changed();
|
||||
}
|
||||
else if(middleClickDown) {
|
||||
c.pos -= c.dir_from_space(w.main.input.mouse.move);
|
||||
if(changed)
|
||||
changed();
|
||||
}
|
||||
if(w.main.input.mouse.scrollAmount.y() && !w.main.toolbar.io->hoverObstructed) {
|
||||
// Ignore the value of scrollAmount, since that leads to problems on macOS
|
||||
|
||||
WorldScalar zoomFactor(1.0 + w.main.toolbar.scrollZoomSpeed);
|
||||
if(w.main.input.mouse.scrollAmount.y() < 0.0)
|
||||
zoomFactor = WorldScalar(1) / zoomFactor;
|
||||
|
||||
c.scale(zoomFactor);
|
||||
if(c.inverseScale < WorldScalar(1))
|
||||
c.inverseScale = WorldScalar(1);
|
||||
else {
|
||||
WorldVec mVec = c.pos - w.get_mouse_world_pos();
|
||||
c.pos = w.get_mouse_world_pos() + mVec / zoomFactor;
|
||||
}
|
||||
if(changed)
|
||||
changed();
|
||||
}
|
||||
if(w.main.input.key(InputManager::KEY_CAMERA_ROTATE_COUNTERCLOCKWISE).held && !w.main.input.text.get_accepting_input()) {
|
||||
c.rotate_about(c.from_space(w.main.window.size.cast<float>() * 0.5f), -w.main.deltaTime);
|
||||
if(changed)
|
||||
changed();
|
||||
}
|
||||
if(w.main.input.key(InputManager::KEY_CAMERA_ROTATE_CLOCKWISE).held && !w.main.input.text.get_accepting_input()) {
|
||||
c.rotate_about(c.from_space(w.main.window.size.cast<float>() * 0.5f), w.main.deltaTime);
|
||||
if(changed)
|
||||
changed();
|
||||
}
|
||||
}
|
||||
|
||||
set_viewing_area(w.main.window.size.cast<float>());
|
||||
}
|
||||
45
src/DrawCamera.hpp
Normal file
45
src/DrawCamera.hpp
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#pragma once
|
||||
#include <Eigen/Dense>
|
||||
#include <Helpers/ConvertVec.hpp>
|
||||
#include "SharedTypes.hpp"
|
||||
#include <Helpers/SCollision.hpp>
|
||||
#include "CoordSpaceHelper.hpp"
|
||||
|
||||
#include "include/core/SkMatrix.h"
|
||||
|
||||
using namespace Eigen;
|
||||
|
||||
class World;
|
||||
|
||||
class DrawCamera {
|
||||
public:
|
||||
DrawCamera();
|
||||
|
||||
CoordSpaceHelper c;
|
||||
Vector2f viewingArea{-1, -1};
|
||||
SCollision::AABB<WorldScalar> viewingAreaGenerousCollider;
|
||||
|
||||
std::function<void()> changed;
|
||||
|
||||
void set_viewing_area(Vector2f viewingAreaNew);
|
||||
void smooth_move_to(World& w, const CoordSpaceHelper& c, Vector2f windowSize, bool instantJump = false);
|
||||
void set_based_on_properties(World& w, const WorldVec& newPos, const WorldScalar& newZoom, double newRotate);
|
||||
void set_based_on_center(World& w, const WorldVec& newPos, const WorldScalar& newZoom, double newRotate);
|
||||
void update_main(World& main);
|
||||
private:
|
||||
struct SmoothMove {
|
||||
CoordSpaceHelper start;
|
||||
WorldVec startCenter;
|
||||
WorldVec endCenter;
|
||||
WorldScalar endUniformZoom;
|
||||
CoordSpaceHelper end;
|
||||
Vector2f endWindowSize;
|
||||
bool occurring = false;
|
||||
float moveTime;
|
||||
} smoothMove;
|
||||
|
||||
WorldScalar startZoomVal;
|
||||
WorldVec startZoomMousePos;
|
||||
WorldVec startZoomCameraPos;
|
||||
bool isMiddleClickZooming = false;
|
||||
};
|
||||
36
src/DrawCollision.cpp
Normal file
36
src/DrawCollision.cpp
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#include "DrawCollision.hpp"
|
||||
#include <include/core/SkPaint.h>
|
||||
#include <include/core/SkPath.h>
|
||||
|
||||
void draw_collider(SkCanvas* canvas, const DrawData& drawData, const SCollision::AABB<float>& aabb, const SkPaint& p) {
|
||||
SkRect r = SkRect::MakeLTRB(aabb.min.x(), aabb.min.y(), aabb.max.x(), aabb.max.y());
|
||||
canvas->drawRect(r, p);
|
||||
}
|
||||
|
||||
void draw_collider(SkCanvas* canvas, const DrawData& drawData, const SCollision::Circle<float>& circle, const SkPaint& p) {
|
||||
canvas->drawCircle(circle.pos.x(), circle.pos.y(), circle.radius, p);
|
||||
}
|
||||
|
||||
void draw_collider(SkCanvas* canvas, const DrawData& drawData, const SCollision::Triangle<float>& triangle, const SkPaint& p) {
|
||||
SkPath path;
|
||||
path.moveTo(convert_vec2<SkPoint>(triangle.p[0]));
|
||||
path.lineTo(convert_vec2<SkPoint>(triangle.p[1]));
|
||||
path.lineTo(convert_vec2<SkPoint>(triangle.p[2]));
|
||||
path.close();
|
||||
canvas->drawPath(path, p);
|
||||
}
|
||||
|
||||
void draw_collider(SkCanvas* canvas, const DrawData& drawData, const SCollision::ColliderCollection<float>& collection, const SkPaint& p) {
|
||||
for(auto& aabb : collection.aabb)
|
||||
draw_collider(canvas, drawData, aabb, p);
|
||||
for(auto& circle : collection.circle)
|
||||
draw_collider(canvas, drawData, circle, p);
|
||||
for(auto& triangle : collection.triangle)
|
||||
draw_collider(canvas, drawData, triangle, p);
|
||||
}
|
||||
|
||||
void draw_collider(SkCanvas* canvas, const DrawData& drawData, const SCollision::BVHContainer<float>& bvh, const SkPaint& p) {
|
||||
for(auto& child : bvh.children)
|
||||
draw_collider(canvas, drawData, child, p);
|
||||
draw_collider(canvas, drawData, bvh.objects, p);
|
||||
}
|
||||
12
src/DrawCollision.hpp
Normal file
12
src/DrawCollision.hpp
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
#include <Helpers/SCollision.hpp>
|
||||
#include "DrawData.hpp"
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include <include/core/SkPaint.h>
|
||||
|
||||
void draw_collider(SkCanvas* canvas, const DrawData& drawData, const SCollision::AABB<float>& aabb, const SkPaint& p);
|
||||
void draw_collider(SkCanvas* canvas, const DrawData& drawData, const SCollision::Circle<float>& circle, const SkPaint& p);
|
||||
void draw_collider(SkCanvas* canvas, const DrawData& drawData, const SCollision::Triangle<float>& triangle, const SkPaint& p);
|
||||
void draw_collider(SkCanvas* canvas, const DrawData& drawData, const SCollision::ColliderCollection<float>& collection, const SkPaint& p);
|
||||
void draw_collider(SkCanvas* canvas, const DrawData& drawData, const SCollision::BVHContainer<float>& bvh, const SkPaint& p);
|
||||
|
||||
399
src/DrawComponents/DrawBrushStroke.cpp
Normal file
399
src/DrawComponents/DrawBrushStroke.cpp
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
#include "DrawBrushStroke.hpp"
|
||||
#include <Helpers/Serializers.hpp>
|
||||
#include <cereal/types/vector.hpp>
|
||||
#include <limits>
|
||||
|
||||
#ifndef IS_SERVER
|
||||
#include <Eigen/Geometry>
|
||||
#include "Helpers/MathExtras.hpp"
|
||||
#include "../TimePoint.hpp"
|
||||
#include <include/core/SkVertices.h>
|
||||
#include "../DrawCollision.hpp"
|
||||
#endif
|
||||
|
||||
#define AABB_PRECHECK_BVH_LEVELS 2
|
||||
|
||||
void DrawBrushStroke::save(cereal::PortableBinaryOutputArchive& a) const {
|
||||
a(d.points, d.color, d.hasRoundCaps);
|
||||
}
|
||||
|
||||
void DrawBrushStroke::load(cereal::PortableBinaryInputArchive& a) {
|
||||
a(d.points, d.color, d.hasRoundCaps);
|
||||
}
|
||||
|
||||
DrawComponentType DrawBrushStroke::get_type() const {
|
||||
return DRAWCOMPONENT_BRUSHSTROKE;
|
||||
}
|
||||
|
||||
#ifndef IS_SERVER
|
||||
|
||||
std::shared_ptr<DrawComponent> DrawBrushStroke::copy() const {
|
||||
auto a = std::make_shared<DrawBrushStroke>();
|
||||
a->d = d;
|
||||
a->coords = coords;
|
||||
return a;
|
||||
}
|
||||
|
||||
void DrawBrushStroke::update_from_delayed_ptr() {
|
||||
std::shared_ptr<DrawBrushStroke> newPtr = std::static_pointer_cast<DrawBrushStroke>(delayedUpdatePtr);
|
||||
d = newPtr->d;
|
||||
}
|
||||
|
||||
Vector2f two_dim_vec_slerp(const Vector2f& v1, const Vector2f& v2) {
|
||||
Vector2f v1Norm = v1.normalized();
|
||||
Vector2f v2Norm = v2.normalized();
|
||||
Vector2f avg = (v1Norm + v2Norm) * 0.5f;
|
||||
if(std::fabs(avg.x()) < 0.00001f && std::fabs(avg.y()) < 0.00001f)
|
||||
return perpendicular_vec2(v1Norm).normalized();
|
||||
else
|
||||
return avg.normalized();
|
||||
}
|
||||
|
||||
void DrawBrushStroke::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
if(drawSetupData.shouldDraw) {
|
||||
//bool deepPrecheck = false;
|
||||
//for(auto& aabb : precheckAABBLevels) {
|
||||
// if(SCollision::collide(drawData.cam.viewingAreaGenerousCollider, aabb)) {
|
||||
// deepPrecheck = true;
|
||||
// break;
|
||||
// }
|
||||
//}
|
||||
//if(!deepPrecheck && !precheckAABBLevels.empty())
|
||||
// return;
|
||||
|
||||
SkPaint paint;
|
||||
if(d.color.w() == 1.0) // Fully opaque, dont bother with blending
|
||||
canvas->save();
|
||||
else
|
||||
canvas->saveLayerAlphaf(nullptr, d.color.w());
|
||||
paint.setColor4f(SkColor4f{d.color.x(), d.color.y(), d.color.z(), 1.0f});
|
||||
canvas_do_calculated_transform(canvas);
|
||||
canvas->drawVertices(vertices, SkBlendMode::kClear, paint);
|
||||
//SkPaint p2(SkColor4f{1, 0, 0, 1});
|
||||
//p2.setStroke(true);
|
||||
//SkPaint p3(SkColor4f{0, 1, 0, 1});
|
||||
//SkPaint p4(SkColor4f{0, 0, 1, 1});
|
||||
//p3.setStroke(true);
|
||||
//p4.setStroke(true);
|
||||
//for(auto& precheckAABBLevel : precheckAABBLevels)
|
||||
// draw_collider(canvas, drawData, precheckAABBLevel, p4);
|
||||
//draw_collider(canvas, drawData, collisionTree, p3);
|
||||
canvas->restore();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawBrushStroke::initialize_draw_data(DrawingProgram& drawP) {
|
||||
std::vector<SkPoint> vertexData;
|
||||
create_triangles([&](Vector2f a, Vector2f b, Vector2f c) {
|
||||
vertexData.emplace_back(a.x(), a.y());
|
||||
vertexData.emplace_back(b.x(), b.y());
|
||||
vertexData.emplace_back(c.x(), c.y());
|
||||
return false;
|
||||
});
|
||||
vertices_to_draw_data(vertexData);
|
||||
}
|
||||
|
||||
std::vector<size_t> DrawBrushStroke::get_wedge_indices(const std::vector<DrawBrushStrokePoint>& points) {
|
||||
if(points.size() < 2)
|
||||
return {};
|
||||
std::vector<size_t> toRet;
|
||||
toRet.emplace_back(0);
|
||||
for(size_t i = 1; i < points.size() - 1; i++) {
|
||||
Vector2f v1Norm = (points[i - 1].pos - points[i].pos).normalized();
|
||||
Vector2f v2Norm = (points[i + 1].pos - points[i].pos).normalized();
|
||||
if(v1Norm.dot(v2Norm) > -0.6f)
|
||||
toRet.emplace_back(i);
|
||||
}
|
||||
toRet.emplace_back(points.size() - 1);
|
||||
return toRet;
|
||||
}
|
||||
|
||||
std::vector<DrawBrushStrokePoint> DrawBrushStroke::smooth_points(size_t beginIndex, size_t endIndex) {
|
||||
std::vector<DrawBrushStrokePoint>& points = d.points;
|
||||
|
||||
size_t pointsSize = endIndex - beginIndex + 1;
|
||||
if(pointsSize < 2)
|
||||
return {points[beginIndex], points[endIndex]};
|
||||
|
||||
std::vector<DrawBrushStrokePoint> toRet;
|
||||
|
||||
DrawBrushStrokePoint p0 = points[beginIndex];
|
||||
p0.pos = p0.pos + p0.width * (p0.pos - points[beginIndex + 1].pos).normalized() * DRAW_MINIMUM_LIMIT;
|
||||
DrawBrushStrokePoint p1 = points[beginIndex];
|
||||
DrawBrushStrokePoint p2 = points[beginIndex + 1];
|
||||
DrawBrushStrokePoint p3;
|
||||
|
||||
for(size_t i = beginIndex; i < endIndex; i++) {
|
||||
|
||||
if(i == endIndex - 1) {
|
||||
p3 = points[i + 1];
|
||||
p3.pos = p3.pos + p3.width * (p3.pos - points[i].pos).normalized() * DRAW_MINIMUM_LIMIT;
|
||||
}
|
||||
else
|
||||
p3 = points[i + 2];
|
||||
|
||||
toRet.emplace_back(p1);
|
||||
|
||||
const unsigned numOfDivisions = 5; // How smooth the stroke should be
|
||||
|
||||
float tMove = 1.0 / static_cast<float>(numOfDivisions);
|
||||
for(unsigned d = 1; d < numOfDivisions; d++) {
|
||||
float t = tMove * static_cast<float>(d);
|
||||
DrawBrushStrokePoint pToAdd;
|
||||
pToAdd.pos = catmull_rom(p0.pos, p1.pos, p2.pos, p3.pos, t);
|
||||
pToAdd.width = std::lerp(p1.width, p2.width, t);
|
||||
if(!(std::isnan(pToAdd.pos.x()) || std::isnan(pToAdd.pos.y()))) // Don't add NAN values
|
||||
toRet.emplace_back(pToAdd);
|
||||
}
|
||||
p0 = p1;
|
||||
p1 = p2;
|
||||
p2 = p3;
|
||||
}
|
||||
toRet.emplace_back(points[endIndex]);
|
||||
|
||||
return toRet;
|
||||
}
|
||||
|
||||
std::vector<DrawBrushStrokePoint> DrawBrushStroke::smooth_points_avg(size_t beginIndex, size_t endIndex) {
|
||||
std::vector<DrawBrushStrokePoint>& points = d.points;
|
||||
|
||||
size_t pointsSize = endIndex - beginIndex + 1;
|
||||
if(pointsSize < 2)
|
||||
return {points[beginIndex], points[endIndex]};
|
||||
|
||||
std::vector<DrawBrushStrokePoint> toRet;
|
||||
|
||||
for(size_t i = beginIndex; i < endIndex; i++) {
|
||||
const DrawBrushStrokePoint& p1 = points[i];
|
||||
const DrawBrushStrokePoint& p2 = points[i + 1];
|
||||
|
||||
const unsigned numOfDivisions = 5; // How smooth the stroke should be
|
||||
|
||||
float tMove = 1.0 / static_cast<float>(numOfDivisions);
|
||||
for(unsigned d = 0; d < numOfDivisions; d++) {
|
||||
float t = tMove * static_cast<float>(d);
|
||||
DrawBrushStrokePoint pToAdd;
|
||||
pToAdd.pos = lerp_vec(p1.pos, p2.pos, t);
|
||||
pToAdd.width = std::lerp(p1.width, p2.width, t);
|
||||
if(!(std::isnan(pToAdd.pos.x()) || std::isnan(pToAdd.pos.y()))) // Don't add NAN values
|
||||
toRet.emplace_back(pToAdd);
|
||||
}
|
||||
}
|
||||
|
||||
toRet.emplace_back(points[endIndex]);
|
||||
|
||||
return toRet;
|
||||
}
|
||||
|
||||
void DrawBrushStroke::create_triangles(const std::function<bool(Vector2f, Vector2f, Vector2f)>& passTriangleFunc) {
|
||||
const int ARC_SMOOTHNESS = 10;
|
||||
const int CIRCLE_SMOOTHNESS = 20;
|
||||
std::vector<DrawBrushStrokePoint>& pointsN = d.points;
|
||||
|
||||
if(pointsN.size() == 1 && d.hasRoundCaps) {
|
||||
std::vector<Vector2f> circlePoints = gen_circle_points(pointsN.front().pos, pointsN.front().width / 2.0f, CIRCLE_SMOOTHNESS);
|
||||
for(size_t i = 0; i < circlePoints.size(); i++)
|
||||
if(passTriangleFunc(pointsN.front().pos, circlePoints[i], circlePoints[(i + 1) % circlePoints.size()])) return;
|
||||
}
|
||||
|
||||
if(pointsN.size() < 2)
|
||||
return;
|
||||
|
||||
std::vector<DrawBrushStrokePoint> points = smooth_points(0, pointsN.size() - 1);
|
||||
std::vector<size_t> wedgeIndices = get_wedge_indices(points);
|
||||
for(size_t i = 0; i < wedgeIndices.size() - 1; i++) {
|
||||
size_t pointsBegin = wedgeIndices[i];
|
||||
size_t pointsEnd = wedgeIndices[i + 1];
|
||||
|
||||
std::array<Vector2f, 3> vertArray;
|
||||
size_t newIndex = 0;
|
||||
|
||||
Vector2f perpPrev = perpendicular_vec2((points[pointsBegin + 1].pos - points[pointsBegin].pos).eval());
|
||||
perpPrev.normalize();
|
||||
|
||||
vertArray[0] = points[pointsBegin].pos + perpPrev * points[pointsBegin].width * 0.5f;
|
||||
vertArray[1] = points[pointsBegin].pos - perpPrev * points[pointsBegin].width * 0.5f;
|
||||
newIndex = 2;
|
||||
|
||||
if(d.hasRoundCaps && pointsBegin == 0) {
|
||||
Vector2f arcDirStart = (convert_vec2<Vector2f>(vertArray[0]) - points[0].pos).normalized();
|
||||
Vector2f arcDirEnd = (convert_vec2<Vector2f>(vertArray[1]) - points[0].pos).normalized();
|
||||
Vector2f arcDirCenter = (points[0].pos - points[1].pos).normalized();
|
||||
std::vector<Vector2f> arcPoints = arc_vec<float>(points[0].pos, arcDirStart, arcDirEnd, arcDirCenter, points[0].width * 0.5f, ARC_SMOOTHNESS);
|
||||
arcPoints.emplace(arcPoints.begin(), convert_vec2<Vector2f>(vertArray[0]));
|
||||
arcPoints.emplace_back(convert_vec2<Vector2f>(vertArray[1]));
|
||||
for(size_t i = 0; i < arcPoints.size() - 1; i++)
|
||||
if(passTriangleFunc(points[0].pos, arcPoints[i], arcPoints[i + 1])) return;
|
||||
}
|
||||
|
||||
for(size_t j = pointsBegin + 1; j < pointsEnd; j++) {
|
||||
Vector2f v1 = points[j - 1].pos - points[j].pos;
|
||||
Vector2f v2 = points[j].pos - points[j + 1].pos;
|
||||
Vector2f perp = perpendicular_vec2((v1 + v2).eval()).normalized();
|
||||
if(std::isnan(perp.dot(perpPrev))) {
|
||||
std::cout << "NAN FOUND" << std::endl;
|
||||
std::cout << v1 << std::endl;
|
||||
std::cout << v2 << std::endl;
|
||||
std::cout << perp << std::endl;
|
||||
std::cout << perpPrev << std::endl;
|
||||
throw 1;
|
||||
}
|
||||
if(perp.dot(perpPrev) < 0.0)
|
||||
perp = -perp;
|
||||
|
||||
vertArray[newIndex] = points[j].pos + perp * 0.5f * points[j].width;
|
||||
newIndex = (newIndex + 1) % 3;
|
||||
if(passTriangleFunc(vertArray[0], vertArray[1], vertArray[2])) return;
|
||||
|
||||
vertArray[newIndex] = points[j].pos - perp * 0.5f * points[j].width;
|
||||
newIndex = (newIndex + 1) % 3;
|
||||
if(passTriangleFunc(vertArray[0], vertArray[1], vertArray[2])) return;
|
||||
|
||||
perpPrev = perp;
|
||||
}
|
||||
|
||||
Vector2f rectDir = points[pointsEnd - 1].pos - points[pointsEnd].pos;
|
||||
rectDir.normalize();
|
||||
Vector2f perp = perpendicular_vec2(rectDir);
|
||||
if(perp.dot(perpPrev) < 0.0)
|
||||
perp = -perp;
|
||||
vertArray[newIndex] = points[pointsEnd].pos + perp * points[pointsEnd].width * 0.5;
|
||||
newIndex = (newIndex + 1) % 3;
|
||||
if(passTriangleFunc(vertArray[0], vertArray[1], vertArray[2])) return;
|
||||
|
||||
vertArray[newIndex] = points[pointsEnd].pos - perp * points[pointsEnd].width * 0.5;
|
||||
newIndex = (newIndex + 1) % 3;
|
||||
if(passTriangleFunc(vertArray[0], vertArray[1], vertArray[2])) return;
|
||||
|
||||
// Render wedge
|
||||
if(pointsEnd != points.size() - 1) {
|
||||
// Find 4 points involved in intersection
|
||||
Vector2f currentP1 = points[pointsEnd].pos - perp * points[pointsEnd].width * 0.5;
|
||||
Vector2f currentP2 = points[pointsEnd].pos + perp * points[pointsEnd].width * 0.5;
|
||||
Vector2f nextRectDir = points[pointsEnd + 1].pos - points[pointsEnd].pos;
|
||||
nextRectDir.normalize();
|
||||
Vector2f nextPerp = perpendicular_vec2(nextRectDir);
|
||||
Vector2f nextP1 = points[pointsEnd].pos - nextPerp * points[pointsEnd].width * 0.5;
|
||||
Vector2f nextP2 = points[pointsEnd].pos + nextPerp * points[pointsEnd].width * 0.5;
|
||||
|
||||
// Find center of wedge
|
||||
Vector2f wedgeCenter = line_line_intersection(currentP1, currentP2, nextP1, nextP2);
|
||||
|
||||
Vector2f wedgeP1 = ((currentP1 - currentP2).dot(nextRectDir)) <= 0.0 ? currentP1 : currentP2;
|
||||
Vector2f wedgeP2 = ((nextP1 - nextP2).dot(rectDir)) <= 0.0 ? nextP1 : nextP2;
|
||||
|
||||
float arcRadius;
|
||||
|
||||
std::vector<Vector2f> arcPoints;
|
||||
|
||||
if(wedgeCenter.x() == std::numeric_limits<float>::max()) {
|
||||
wedgeCenter = (currentP1 + currentP2) * 0.5;
|
||||
wedgeP1 = currentP1;
|
||||
wedgeP2 = currentP2;
|
||||
arcRadius = points[pointsEnd].width * 0.5;
|
||||
arcPoints = arc_vec(wedgeCenter, (wedgeP1 - wedgeCenter).normalized(), (wedgeP2 - wedgeCenter).normalized(), (-rectDir).eval(), arcRadius, ARC_SMOOTHNESS);
|
||||
}
|
||||
else {
|
||||
arcRadius = (wedgeCenter - wedgeP1).norm();
|
||||
arcPoints = arc_vec(wedgeCenter, (wedgeP1 - wedgeCenter).normalized(), (wedgeP2 - wedgeCenter).normalized(), lerp_vec((-rectDir).eval(), (-nextRectDir).eval(), 0.5).normalized(), arcRadius, ARC_SMOOTHNESS);
|
||||
}
|
||||
|
||||
arcPoints.emplace(arcPoints.begin(), wedgeP1);
|
||||
arcPoints.emplace_back(wedgeP2);
|
||||
for(size_t i = 0; i < arcPoints.size() - 1; i++)
|
||||
if(passTriangleFunc(wedgeCenter, arcPoints[i], arcPoints[i + 1])) return;
|
||||
}
|
||||
else if(d.hasRoundCaps) { // It's the last point, cap it off
|
||||
Vector2f arcDirStart = (convert_vec2<Vector2f>(vertArray[(newIndex + 1) % 3]) - points.back().pos).normalized();
|
||||
Vector2f arcDirEnd = (convert_vec2<Vector2f>(vertArray[(newIndex + 2) % 3]) - points.back().pos).normalized();
|
||||
Vector2f arcDirCenter = -rectDir;
|
||||
std::vector<Vector2f> arcPoints = arc_vec<float>(points.back().pos, arcDirStart, arcDirEnd, arcDirCenter, points.back().width * 0.5f, ARC_SMOOTHNESS);
|
||||
arcPoints.emplace(arcPoints.begin(), convert_vec2<Vector2f>(vertArray[(newIndex + 1) % 3]));
|
||||
arcPoints.emplace_back(convert_vec2<Vector2f>(vertArray[(newIndex + 2) % 3]));
|
||||
for(size_t i = 0; i < arcPoints.size() - 1; i++)
|
||||
if(passTriangleFunc(points.back().pos, arcPoints[i], arcPoints[i + 1])) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawBrushStroke::create_collider(bool colliderAllocated) {
|
||||
using namespace SCollision;
|
||||
ColliderCollection<float> strokeObjects;
|
||||
create_triangles([&](Vector2f a, Vector2f b, Vector2f c) {
|
||||
strokeObjects.triangle.emplace_back(a, b, c);
|
||||
return false;
|
||||
});
|
||||
strokeObjects.recalculate_bounds();
|
||||
collisionTree.clear();
|
||||
collisionTree.calculate_bvh_recursive(strokeObjects);
|
||||
precheckAABBLevels.clear();
|
||||
add_precheck_aabb_level(0, collisionTree.children);
|
||||
if(!colliderAllocated) {
|
||||
collisionTree.objects.aabb.clear();
|
||||
collisionTree.objects.circle.clear();
|
||||
collisionTree.objects.triangle.clear();
|
||||
collisionTree.children.clear();
|
||||
}
|
||||
calculate_world_bounds();
|
||||
}
|
||||
|
||||
void DrawBrushStroke::add_precheck_aabb_level(size_t level, const std::vector<SCollision::BVHContainer<float>>& levelArray) {
|
||||
for(const auto& a : levelArray) {
|
||||
if(level == AABB_PRECHECK_BVH_LEVELS || a.children.empty())
|
||||
precheckAABBLevels.emplace_back(a.objects.bounds);
|
||||
else
|
||||
add_precheck_aabb_level(level + 1, a.children);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawBrushStroke::vertices_to_draw_data(const std::vector<SkPoint>& vertexData) {
|
||||
if((d.points.size() == 1 && d.hasRoundCaps) || d.points.size() >= 2)
|
||||
vertices = SkVertices::MakeCopy(SkVertices::kTriangles_VertexMode, vertexData.size(), vertexData.data(), nullptr, nullptr);
|
||||
else
|
||||
vertices = SkVertices::MakeCopy(SkVertices::kTriangles_VertexMode, 0, nullptr, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void DrawBrushStroke::update(DrawingProgram& drawP) {
|
||||
}
|
||||
|
||||
bool DrawBrushStroke::collides_within_coords(const SCollision::ColliderCollection<float>& checkAgainst, bool colliderAllocated) {
|
||||
if(colliderAllocated)
|
||||
return collisionTree.is_collide(checkAgainst);
|
||||
|
||||
if(!SCollision::collide(checkAgainst, collisionTree.objects.bounds))
|
||||
return false;
|
||||
|
||||
bool deepPrecheck = false;
|
||||
for(auto& aabb : precheckAABBLevels) {
|
||||
if(SCollision::collide(checkAgainst, aabb)) {
|
||||
deepPrecheck = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!deepPrecheck && !precheckAABBLevels.empty())
|
||||
return false;
|
||||
|
||||
bool toRet = false;
|
||||
create_triangles([&](Vector2f a, Vector2f b, Vector2f c) {
|
||||
toRet = SCollision::collide(checkAgainst, SCollision::Triangle<float>{a, b, c});
|
||||
return toRet;
|
||||
});
|
||||
return toRet;
|
||||
}
|
||||
|
||||
void DrawBrushStroke::free_collider() {
|
||||
collisionTree.objects.aabb.clear();
|
||||
collisionTree.objects.circle.clear();
|
||||
collisionTree.objects.triangle.clear();
|
||||
collisionTree.children.clear();
|
||||
}
|
||||
|
||||
void DrawBrushStroke::allocate_collider() {
|
||||
create_collider(true);
|
||||
}
|
||||
|
||||
SCollision::AABB<float> DrawBrushStroke::get_obj_coord_bounds() const {
|
||||
return collisionTree.objects.bounds;
|
||||
}
|
||||
|
||||
#endif
|
||||
62
src/DrawComponents/DrawBrushStroke.hpp
Normal file
62
src/DrawComponents/DrawBrushStroke.hpp
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
#pragma once
|
||||
#include "../SharedTypes.hpp"
|
||||
#include "DrawComponent.hpp"
|
||||
#include "Helpers/SCollision.hpp"
|
||||
#include <Helpers/Serializers.hpp>
|
||||
|
||||
#ifndef IS_SERVER
|
||||
#include <include/core/SkPath.h>
|
||||
#endif
|
||||
|
||||
#define DRAW_MINIMUM_LIMIT 0.75
|
||||
|
||||
|
||||
struct DrawBrushStrokePoint {
|
||||
Vector2f pos;
|
||||
float width;
|
||||
|
||||
template <typename Archive> void serialize(Archive& a) {
|
||||
a(pos, width);
|
||||
}
|
||||
};
|
||||
|
||||
class DrawBrushStroke : public DrawComponent {
|
||||
public:
|
||||
virtual DrawComponentType get_type() const override;
|
||||
virtual void save(cereal::PortableBinaryOutputArchive& a) const override;
|
||||
virtual void load(cereal::PortableBinaryInputArchive& a) override;
|
||||
|
||||
struct Data {
|
||||
std::vector<DrawBrushStrokePoint> points;
|
||||
Vector4f color;
|
||||
bool hasRoundCaps;
|
||||
} d;
|
||||
|
||||
#ifndef IS_SERVER
|
||||
virtual std::shared_ptr<DrawComponent> copy() const override;
|
||||
virtual void draw(SkCanvas* canvas, const DrawData& drawData) override;
|
||||
virtual void initialize_draw_data(DrawingProgram& drawP) override;
|
||||
virtual void update(DrawingProgram& drawP) override;
|
||||
virtual bool collides_within_coords(const SCollision::ColliderCollection<float>& checkAgainst, bool colliderAllocated) override;
|
||||
virtual void create_collider(bool colliderAllocated) override;
|
||||
virtual void update_from_delayed_ptr() override;
|
||||
|
||||
virtual void free_collider() override;
|
||||
virtual void allocate_collider() override;
|
||||
|
||||
virtual SCollision::AABB<float> get_obj_coord_bounds() const override;
|
||||
|
||||
sk_sp<SkVertices> vertices;
|
||||
SCollision::BVHContainer<float> collisionTree;
|
||||
|
||||
private:
|
||||
void add_precheck_aabb_level(size_t level, const std::vector<SCollision::BVHContainer<float>>& levelArray);
|
||||
std::vector<SCollision::AABB<float>> precheckAABBLevels;
|
||||
void create_triangles(const std::function<bool(Vector2f, Vector2f, Vector2f)>& passTriangleFunc);
|
||||
void vertices_to_draw_data(const std::vector<SkPoint>& vertexData);
|
||||
std::vector<size_t> get_wedge_indices(const std::vector<DrawBrushStrokePoint>& points);
|
||||
std::vector<DrawBrushStrokePoint> smooth_points(size_t beginIndex, size_t endIndex);
|
||||
std::vector<DrawBrushStrokePoint> smooth_points_avg(size_t beginIndex, size_t endIndex);
|
||||
#endif
|
||||
};
|
||||
|
||||
208
src/DrawComponents/DrawComponent.cpp
Normal file
208
src/DrawComponents/DrawComponent.cpp
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
#include "DrawComponent.hpp"
|
||||
#include "DrawBrushStroke.hpp"
|
||||
#include "DrawEllipse.hpp"
|
||||
#include "DrawRectangle.hpp"
|
||||
#include "DrawTextBox.hpp"
|
||||
#include "DrawImage.hpp"
|
||||
#include "Helpers/Networking/NetLibrary.hpp"
|
||||
#include "../SharedTypes.hpp"
|
||||
#include "../Server/CommandList.hpp"
|
||||
#include <chrono>
|
||||
|
||||
#ifndef IS_SERVER
|
||||
#include "../MainProgram.hpp"
|
||||
#endif
|
||||
|
||||
std::shared_ptr<DrawComponent> DrawComponent::allocate_comp_type(DrawComponentType type) {
|
||||
switch(type) {
|
||||
case DRAWCOMPONENT_BRUSHSTROKE:
|
||||
return std::make_shared<DrawBrushStroke>();
|
||||
case DRAWCOMPONENT_RECTANGLE:
|
||||
return std::make_shared<DrawRectangle>();
|
||||
case DRAWCOMPONENT_ELLIPSE:
|
||||
return std::make_shared<DrawEllipse>();
|
||||
case DRAWCOMPONENT_TEXTBOX:
|
||||
return std::make_shared<DrawTextBox>();
|
||||
case DRAWCOMPONENT_IMAGE:
|
||||
return std::make_shared<DrawImage>();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void DrawComponent::server_send_place(MainServer& server, ServerClientID id, uint64_t placement) {
|
||||
server.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_PLACE_COMPONENT, placement, get_type(), id, this->coords, *this);
|
||||
}
|
||||
|
||||
void DrawComponent::server_send_erase(MainServer& server, ServerClientID id) {
|
||||
server.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_ERASE_COMPONENT, id);
|
||||
}
|
||||
|
||||
void DrawComponent::server_send_update_temp(MainServer& server, ServerClientID id) {
|
||||
server.netServer->send_items_to_all_clients(UNRELIABLE_COMMAND_CHANNEL, CLIENT_UPDATE_COMPONENT, true, id, *this);
|
||||
tempServerUpdateTimer = std::chrono::steady_clock::now();
|
||||
serverIsTempUpdate = true;
|
||||
}
|
||||
|
||||
void DrawComponent::server_send_update_final(MainServer& server, ServerClientID id) {
|
||||
server.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_UPDATE_COMPONENT, false, id, *this);
|
||||
serverIsTempUpdate = false;
|
||||
}
|
||||
|
||||
void DrawComponent::server_send_transform_temp(MainServer& server, ServerClientID id) {
|
||||
server.netServer->send_items_to_all_clients(UNRELIABLE_COMMAND_CHANNEL, CLIENT_TRANSFORM_COMPONENT, true, id, this->coords);
|
||||
tempServerUpdateTimer = std::chrono::steady_clock::now();
|
||||
serverIsTempUpdate = true;
|
||||
}
|
||||
|
||||
void DrawComponent::server_send_transform_final(MainServer& server, ServerClientID id) {
|
||||
server.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_TRANSFORM_COMPONENT, false, id, this->coords);
|
||||
serverIsTempUpdate = false;
|
||||
}
|
||||
|
||||
void DrawComponent::server_update(MainServer& server, ServerClientID id) {
|
||||
if(serverIsTempUpdate) {
|
||||
float timeSince = std::chrono::duration_cast<std::chrono::duration<float>>(std::chrono::steady_clock::now() - tempServerUpdateTimer).count();
|
||||
if(timeSince >= 1.0f) {
|
||||
server_send_update_final(server, id);
|
||||
serverIsTempUpdate = false;
|
||||
}
|
||||
}
|
||||
if(serverIsTempTransform) {
|
||||
float timeSince = std::chrono::duration_cast<std::chrono::duration<float>>(std::chrono::steady_clock::now() - tempServerTransformTimer).count();
|
||||
if(timeSince >= 1.0f) {
|
||||
server_send_transform_final(server, id);
|
||||
serverIsTempTransform = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawComponent::get_used_resources(std::unordered_set<ServerClientID>& v) const {
|
||||
}
|
||||
|
||||
void DrawComponent::remap_resource_ids(std::unordered_map<ServerClientID, ServerClientID>& newIDs) {
|
||||
}
|
||||
|
||||
#ifndef IS_SERVER
|
||||
|
||||
bool DrawComponent::bounds_draw_check(const DrawData& drawData) const {
|
||||
bool a = !drawData.clampDrawBetween || ((drawData.clampDrawMaximum >= coords.inverseScale) && (drawData.clampDrawMinimum < coords.inverseScale));
|
||||
return a;
|
||||
}
|
||||
|
||||
void DrawComponent::check_timers(DrawingProgram& drawP) {
|
||||
if(delayedCoordinateSpace) {
|
||||
float transformDur = std::chrono::duration_cast<std::chrono::duration<float>>(std::chrono::steady_clock::now() - lastTransformTime).count();
|
||||
if(transformDur > CLIENT_DRAWCOMP_DELAY_TIMER_DURATION) {
|
||||
coords = *delayedCoordinateSpace;
|
||||
delayedCoordinateSpace = nullptr;
|
||||
finalize_update(drawP.colliderAllocated);
|
||||
}
|
||||
}
|
||||
if(delayedUpdatePtr) {
|
||||
float updateDur = std::chrono::duration_cast<std::chrono::duration<float>>(std::chrono::steady_clock::now() - lastUpdateTime).count();
|
||||
if(updateDur > CLIENT_DRAWCOMP_DELAY_TIMER_DURATION) {
|
||||
update_from_delayed_ptr();
|
||||
delayedUpdatePtr = nullptr;
|
||||
final_update(drawP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawComponent::temp_update(DrawingProgram& drawP) {
|
||||
initialize_draw_data(drawP);
|
||||
updateDraw = true;
|
||||
}
|
||||
|
||||
void DrawComponent::final_update(DrawingProgram& drawP) {
|
||||
initialize_draw_data(drawP);
|
||||
finalize_update(drawP.colliderAllocated);
|
||||
updateDraw = true;
|
||||
}
|
||||
|
||||
void DrawComponent::client_send_place(DrawingProgram& drawP, uint64_t placement) {
|
||||
drawP.world.con.client_send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_PLACE_COMPONENT, placement, get_type(), this->coords, *this);
|
||||
}
|
||||
|
||||
void DrawComponent::client_send_erase(DrawingProgram& drawP, ServerClientID id) {
|
||||
drawP.world.con.client_send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_ERASE_COMPONENT, id);
|
||||
}
|
||||
|
||||
void DrawComponent::client_send_update_temp(DrawingProgram& drawP, ServerClientID id) {
|
||||
drawP.world.con.client_send_items_to_server(UNRELIABLE_COMMAND_CHANNEL, SERVER_UPDATE_COMPONENT, true, id, *this);
|
||||
}
|
||||
|
||||
void DrawComponent::client_send_update_final(DrawingProgram& drawP, ServerClientID id) {
|
||||
drawP.world.con.client_send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_UPDATE_COMPONENT, false, id, *this);
|
||||
}
|
||||
|
||||
void DrawComponent::client_send_transform_temp(DrawingProgram& drawP, ServerClientID id) {
|
||||
drawP.world.con.client_send_items_to_server(UNRELIABLE_COMMAND_CHANNEL, SERVER_TRANSFORM_COMPONENT, true, id, this->coords);
|
||||
}
|
||||
|
||||
void DrawComponent::client_send_transform_final(DrawingProgram& drawP, ServerClientID id) {
|
||||
drawP.world.con.client_send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_TRANSFORM_COMPONENT, false, id, this->coords);
|
||||
}
|
||||
|
||||
void DrawComponent::finalize_update(bool colliderAllocated) {
|
||||
create_collider(colliderAllocated);
|
||||
updateDraw = true;
|
||||
}
|
||||
|
||||
bool DrawComponent::collides_with_world_coords(const CoordSpaceHelper& camCoords, const SCollision::ColliderCollection<WorldScalar>& checkAgainstWorld, bool colliderAllocated) {
|
||||
return collides_with(camCoords, checkAgainstWorld, camCoords.world_collider_to_coords<SCollision::ColliderCollection<float>>(checkAgainstWorld), colliderAllocated);
|
||||
}
|
||||
|
||||
bool DrawComponent::collides_with_cam_coords(const CoordSpaceHelper& camCoords, const SCollision::ColliderCollection<float>& checkAgainstCam, bool colliderAllocated) {
|
||||
return collides_with(camCoords, camCoords.collider_to_world<SCollision::ColliderCollection<WorldScalar>>(checkAgainstCam), checkAgainstCam, colliderAllocated);
|
||||
}
|
||||
|
||||
// We could just send one of the checkAgainst colliders, since they represent the same thing, but sending both saves on redundant transformations, since we can save both of the versions on the executor side
|
||||
bool DrawComponent::collides_with(const CoordSpaceHelper& camCoords, const SCollision::ColliderCollection<WorldScalar>& checkAgainstWorld, const SCollision::ColliderCollection<float>& checkAgainstCam, bool colliderAllocated) {
|
||||
if((camCoords.inverseScale << DRAWCOMP_MAX_SHIFT_BEFORE_DISAPPEAR) < coords.inverseScale) // Object is too large, just dismiss the collision
|
||||
return false;
|
||||
else if((camCoords.inverseScale >> DRAWCOMP_MIN_SHIFT_BEFORE_DISAPPEAR) >= coords.inverseScale) {
|
||||
if(!worldAABB)
|
||||
return false;
|
||||
return SCollision::collide(checkAgainstCam, camCoords.to_space(worldAABB->min));
|
||||
}
|
||||
else {
|
||||
auto c = checkAgainstWorld.transform<float>(
|
||||
[&coords = coords](const WorldVec& a) {
|
||||
return coords.to_space(a);
|
||||
},
|
||||
[&coords = coords](const WorldScalar& a) {
|
||||
return coords.scalar_to_space(a);
|
||||
});
|
||||
return collides_within_coords(c, colliderAllocated);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawComponent::canvas_do_calculated_transform(SkCanvas* canvas) {
|
||||
canvas->scale(drawSetupData.transformData.scale, drawSetupData.transformData.scale);
|
||||
canvas->rotate(drawSetupData.transformData.rotation);
|
||||
canvas->translate(drawSetupData.transformData.translation.x(), drawSetupData.transformData.translation.y());
|
||||
}
|
||||
|
||||
void DrawComponent::calculate_draw_transform(const DrawData& drawData) {
|
||||
drawSetupData.shouldDraw = !drawData.clampDrawBetween || ((drawData.clampDrawMaximum >= coords.inverseScale) && (drawData.clampDrawMinimum < coords.inverseScale));
|
||||
if(drawSetupData.shouldDraw) {
|
||||
drawSetupData.shouldDraw = updateDraw || !worldAABB || SCollision::collide(*worldAABB, drawData.cam.viewingAreaGenerousCollider);
|
||||
if(drawSetupData.shouldDraw) {
|
||||
drawSetupData.transformData.translation = -coords.to_space(drawData.cam.c.pos);
|
||||
drawSetupData.transformData.rotation = (coords.rotation - drawData.cam.c.rotation) * 180.0 / std::numbers::pi;
|
||||
drawSetupData.transformData.scale = static_cast<float>(coords.inverseScale / drawData.cam.c.inverseScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawComponent::calculate_world_bounds() {
|
||||
worldAABB = std::optional<SCollision::AABB<WorldScalar>>(coords.collider_to_world<SCollision::AABB<WorldScalar>, SCollision::AABB<float>>(get_obj_coord_bounds()));
|
||||
}
|
||||
|
||||
std::optional<SCollision::AABB<WorldScalar>> DrawComponent::get_world_bounds() const {
|
||||
return worldAABB;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
DrawComponent::~DrawComponent() { }
|
||||
109
src/DrawComponents/DrawComponent.hpp
Normal file
109
src/DrawComponents/DrawComponent.hpp
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
#pragma once
|
||||
#include "Helpers/SCollision.hpp"
|
||||
#include <Helpers/Hashes.hpp>
|
||||
#include "cereal/archives/portable_binary.hpp"
|
||||
#include "../SharedTypes.hpp"
|
||||
#include "../CoordSpaceHelper.hpp"
|
||||
#include "../Server/MainServer.hpp"
|
||||
|
||||
#ifndef IS_SERVER
|
||||
#include "../DrawData.hpp"
|
||||
#include <chrono>
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include <include/core/SkVertices.h>
|
||||
#endif
|
||||
|
||||
#define CLIENT_DRAWCOMP_DELAY_TIMER_DURATION 5.0f
|
||||
|
||||
#define DRAWCOMP_MAX_SHIFT_BEFORE_DISAPPEAR 20
|
||||
#define DRAWCOMP_MIN_SHIFT_BEFORE_DISAPPEAR 9
|
||||
|
||||
class DrawingProgram;
|
||||
|
||||
class DrawComponent {
|
||||
public:
|
||||
static std::shared_ptr<DrawComponent> allocate_comp_type(DrawComponentType type);
|
||||
virtual void save(cereal::PortableBinaryOutputArchive& a) const = 0;
|
||||
virtual void load(cereal::PortableBinaryInputArchive& a) = 0;
|
||||
virtual DrawComponentType get_type() const = 0;
|
||||
virtual void get_used_resources(std::unordered_set<ServerClientID>& v) const;
|
||||
virtual void remap_resource_ids(std::unordered_map<ServerClientID, ServerClientID>& newIDs);
|
||||
virtual ~DrawComponent();
|
||||
|
||||
// NOTE: Permanantly altering the coordinates during transformation can lead to loss of detail for smaller objects
|
||||
CoordSpaceHelper coords;
|
||||
|
||||
struct DrawSetupData {
|
||||
struct TransformDrawData {
|
||||
Vector2f translation;
|
||||
float rotation;
|
||||
float scale;
|
||||
} transformData;
|
||||
bool shouldDraw = true;
|
||||
} drawSetupData;
|
||||
|
||||
void canvas_do_calculated_transform(SkCanvas* canvas);
|
||||
void calculate_draw_transform(const DrawData& drawData);
|
||||
|
||||
void server_send_place(MainServer& server, ServerClientID id, uint64_t placement);
|
||||
static void server_send_erase(MainServer& server, ServerClientID id);
|
||||
void server_send_update_temp(MainServer& server, ServerClientID id);
|
||||
void server_send_update_final(MainServer& server, ServerClientID id);
|
||||
void server_send_transform_temp(MainServer& server, ServerClientID id);
|
||||
void server_send_transform_final(MainServer& server, ServerClientID id);
|
||||
|
||||
std::chrono::time_point<std::chrono::steady_clock> tempServerUpdateTimer;
|
||||
bool serverIsTempUpdate = false;
|
||||
|
||||
std::chrono::time_point<std::chrono::steady_clock> tempServerTransformTimer;
|
||||
bool serverIsTempTransform = false;
|
||||
|
||||
void server_update(MainServer& server, ServerClientID id);
|
||||
|
||||
#ifndef IS_SERVER
|
||||
bool selected = false;
|
||||
bool updateDraw = true;
|
||||
|
||||
bool globalCollisionCheck = false;
|
||||
|
||||
std::chrono::time_point<std::chrono::steady_clock> lastTransformTime;
|
||||
std::chrono::time_point<std::chrono::steady_clock> lastUpdateTime;
|
||||
|
||||
std::shared_ptr<DrawComponent> delayedUpdatePtr = nullptr;
|
||||
std::shared_ptr<CoordSpaceHelper> delayedCoordinateSpace = nullptr;
|
||||
|
||||
bool bounds_draw_check(const DrawData& drawData) const;
|
||||
|
||||
void check_timers(DrawingProgram& drawP);
|
||||
void client_send_place(DrawingProgram& drawP, uint64_t placement);
|
||||
static void client_send_erase(DrawingProgram& drawP, ServerClientID id);
|
||||
void client_send_update_temp(DrawingProgram& drawP, ServerClientID id);
|
||||
void client_send_update_final(DrawingProgram& drawP, ServerClientID id);
|
||||
void client_send_transform_temp(DrawingProgram& drawP, ServerClientID id);
|
||||
void client_send_transform_final(DrawingProgram& drawP, ServerClientID id);
|
||||
|
||||
std::optional<SCollision::AABB<WorldScalar>> worldAABB;
|
||||
|
||||
std::optional<SCollision::AABB<WorldScalar>> get_world_bounds() const;
|
||||
void calculate_world_bounds();
|
||||
virtual SCollision::AABB<float> get_obj_coord_bounds() const = 0;
|
||||
|
||||
virtual std::shared_ptr<DrawComponent> copy() const = 0;
|
||||
virtual void create_collider(bool colliderAllocated) = 0;
|
||||
virtual void draw(SkCanvas* canvas, const DrawData& drawData) = 0;
|
||||
void temp_update(DrawingProgram& drawP);
|
||||
void final_update(DrawingProgram& drawP);
|
||||
virtual void initialize_draw_data(DrawingProgram& drawP) = 0;
|
||||
virtual void finalize_update(bool colliderAllocated);
|
||||
virtual void update(DrawingProgram& drawP) = 0;
|
||||
virtual bool collides_within_coords(const SCollision::ColliderCollection<float>& checkAgainst, bool colliderAllocated) = 0;
|
||||
bool collides_with_world_coords(const CoordSpaceHelper& camCoords, const SCollision::ColliderCollection<WorldScalar>& checkAgainstWorld, bool colliderAllocated);
|
||||
bool collides_with_cam_coords(const CoordSpaceHelper& camCoords, const SCollision::ColliderCollection<float>& checkAgainstCam, bool colliderAllocated);
|
||||
bool collides_with(const CoordSpaceHelper& camCoords, const SCollision::ColliderCollection<WorldScalar>& checkAgainstWorld, const SCollision::ColliderCollection<float>& checkAgainstCam, bool colliderAllocated);
|
||||
virtual void update_from_delayed_ptr() = 0;
|
||||
|
||||
virtual void free_collider() = 0;
|
||||
virtual void allocate_collider() = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
141
src/DrawComponents/DrawEllipse.cpp
Normal file
141
src/DrawComponents/DrawEllipse.cpp
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
#include "DrawEllipse.hpp"
|
||||
#include "Helpers/ConvertVec.hpp"
|
||||
#include "Helpers/SCollision.hpp"
|
||||
|
||||
#ifndef IS_SERVER
|
||||
#include "../DrawCollision.hpp"
|
||||
#endif
|
||||
|
||||
DrawComponentType DrawEllipse::get_type() const {
|
||||
return DRAWCOMPONENT_ELLIPSE;
|
||||
}
|
||||
|
||||
void DrawEllipse::save(cereal::PortableBinaryOutputArchive& a) const {
|
||||
a(d.strokeColor, d.fillColor, d.strokeWidth, d.p1, d.p2, d.fillStrokeMode);
|
||||
}
|
||||
|
||||
void DrawEllipse::load(cereal::PortableBinaryInputArchive& a) {
|
||||
a(d.strokeColor, d.fillColor, d.strokeWidth, d.p1, d.p2, d.fillStrokeMode);
|
||||
}
|
||||
|
||||
#ifndef IS_SERVER
|
||||
std::shared_ptr<DrawComponent> DrawEllipse::copy() const {
|
||||
auto a = std::make_shared<DrawEllipse>();
|
||||
a->d = d;
|
||||
a->coords = coords;
|
||||
return a;
|
||||
}
|
||||
|
||||
void DrawEllipse::update_from_delayed_ptr() {
|
||||
std::shared_ptr<DrawEllipse> newPtr = std::static_pointer_cast<DrawEllipse>(delayedUpdatePtr);
|
||||
d = newPtr->d;
|
||||
}
|
||||
|
||||
void DrawEllipse::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
if(drawSetupData.shouldDraw) {
|
||||
canvas->save();
|
||||
canvas_do_calculated_transform(canvas);
|
||||
if(d.fillStrokeMode == 0 || d.fillStrokeMode == 2) {
|
||||
SkPaint p;
|
||||
p.setStyle(SkPaint::kFill_Style);
|
||||
p.setColor4f(convert_vec4<SkColor4f>(d.fillColor));
|
||||
canvas->drawPath(ellipsePath, p);
|
||||
}
|
||||
if(d.fillStrokeMode == 1 || d.fillStrokeMode == 2) {
|
||||
SkPaint p;
|
||||
p.setStyle(SkPaint::kStroke_Style);
|
||||
p.setColor4f(convert_vec4<SkColor4f>(d.strokeColor));
|
||||
p.setStrokeWidth(d.strokeWidth);
|
||||
canvas->drawPath(ellipsePath, p);
|
||||
}
|
||||
canvas->restore();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawEllipse::create_draw_data() {
|
||||
ellipsePath = SkPath();
|
||||
SkRect newRect = SkRect::MakeLTRB(d.p1.x(), d.p1.y(), d.p2.x(), d.p2.y());
|
||||
ellipsePath.addOval(newRect);
|
||||
}
|
||||
|
||||
void DrawEllipse::initialize_draw_data(DrawingProgram& drawP) {
|
||||
create_draw_data();
|
||||
}
|
||||
|
||||
void DrawEllipse::update(DrawingProgram& drawP) {
|
||||
}
|
||||
|
||||
bool DrawEllipse::collides_within_coords(const SCollision::ColliderCollection<float>& checkAgainst, bool colliderAllocated) {
|
||||
return collisionTree.is_collide(checkAgainst);
|
||||
}
|
||||
|
||||
void DrawEllipse::create_collider(bool colliderAllocated) {
|
||||
using namespace SCollision;
|
||||
ColliderCollection<float> strokeObjects;
|
||||
|
||||
if(d.fillStrokeMode == 0) {
|
||||
Vector2f ellipseCenter = (d.p1 + d.p2) * 0.5;
|
||||
float a = ellipseCenter.x() - d.p1.x();
|
||||
float b = ellipseCenter.y() - d.p1.y();
|
||||
Vector2f prevPoint = ellipseCenter + Vector2f{a * std::cos(0), b * std::sin(0)};
|
||||
unsigned numOfSegments = 20;
|
||||
float segmentStep = 1.0 / numOfSegments;
|
||||
float t = 0.0;
|
||||
for(unsigned i = 0; i <= numOfSegments; i++) {
|
||||
Vector2f nextPoint = ellipseCenter + Vector2f{a * std::cos(t), b * std::sin(t)};
|
||||
strokeObjects.triangle.emplace_back(ellipseCenter, prevPoint, nextPoint);
|
||||
prevPoint = nextPoint;
|
||||
t += segmentStep * std::numbers::pi * 2.0;
|
||||
}
|
||||
}
|
||||
else if(d.fillStrokeMode == 1) {
|
||||
std::vector<Vector2f> points;
|
||||
|
||||
Vector2f ellipseCenter = (d.p1 + d.p2) * 0.5;
|
||||
float a = ellipseCenter.x() - d.p1.x();
|
||||
float b = ellipseCenter.y() - d.p1.y();
|
||||
unsigned numOfSegments = 40;
|
||||
float segmentStep = 1.0 / numOfSegments;
|
||||
float t = 0.0;
|
||||
for(unsigned i = 0; i < numOfSegments; i++) {
|
||||
points.emplace_back(ellipseCenter + Vector2f{a * std::cos(t), b * std::sin(t)});
|
||||
t += segmentStep * std::numbers::pi * 2.0;
|
||||
}
|
||||
generate_polyline(strokeObjects, points, d.strokeWidth, true);
|
||||
|
||||
}
|
||||
else if(d.fillStrokeMode == 2) {
|
||||
float strokeRadius = d.strokeWidth * 0.5f;
|
||||
Vector2f wNewP1 = d.p1 - Vector2f{strokeRadius, strokeRadius};
|
||||
Vector2f wNewP2 = d.p2 + Vector2f{strokeRadius, strokeRadius};
|
||||
Vector2f ellipseCenter = (wNewP1 + wNewP2) * 0.5;
|
||||
float a = ellipseCenter.x() - wNewP1.x();
|
||||
float b = ellipseCenter.y() - wNewP1.y();
|
||||
Vector2f prevPoint = ellipseCenter + Vector2f{a * std::cos(0), b * std::sin(0)};
|
||||
unsigned numOfSegments = 20;
|
||||
float segmentStep = 1.0 / numOfSegments;
|
||||
float t = 0.0;
|
||||
for(unsigned i = 0; i <= numOfSegments; i++) {
|
||||
Vector2f nextPoint = ellipseCenter + Vector2f{a * std::cos(t), b * std::sin(t)};
|
||||
strokeObjects.triangle.emplace_back(ellipseCenter, prevPoint, nextPoint);
|
||||
prevPoint = nextPoint;
|
||||
t += segmentStep * std::numbers::pi * 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
collisionTree.clear();
|
||||
collisionTree.calculate_bvh_recursive(strokeObjects);
|
||||
calculate_world_bounds();
|
||||
}
|
||||
|
||||
void DrawEllipse::free_collider() {
|
||||
}
|
||||
|
||||
void DrawEllipse::allocate_collider() {
|
||||
}
|
||||
|
||||
SCollision::AABB<float> DrawEllipse::get_obj_coord_bounds() const {
|
||||
return collisionTree.objects.bounds;
|
||||
}
|
||||
|
||||
#endif
|
||||
49
src/DrawComponents/DrawEllipse.hpp
Normal file
49
src/DrawComponents/DrawEllipse.hpp
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#pragma once
|
||||
#include "../SharedTypes.hpp"
|
||||
#include "DrawComponent.hpp"
|
||||
#include "../CoordSpaceHelper.hpp"
|
||||
|
||||
#ifndef IS_SERVER
|
||||
#include <include/core/SkPath.h>
|
||||
#endif
|
||||
|
||||
class DrawEllipse : public DrawComponent {
|
||||
public:
|
||||
virtual void save(cereal::PortableBinaryOutputArchive& a) const override;
|
||||
virtual void load(cereal::PortableBinaryInputArchive& a) override;
|
||||
virtual DrawComponentType get_type() const override;
|
||||
|
||||
// User input data
|
||||
struct Data {
|
||||
Vector4f strokeColor;
|
||||
Vector4f fillColor;
|
||||
float strokeWidth;
|
||||
Vector2f p1;
|
||||
Vector2f p2;
|
||||
uint8_t fillStrokeMode;
|
||||
bool operator==(const Data& o) const = default;
|
||||
} d;
|
||||
|
||||
#ifndef IS_SERVER
|
||||
virtual std::shared_ptr<DrawComponent> copy() const override;
|
||||
virtual void draw(SkCanvas* canvas, const DrawData& drawData) override;
|
||||
virtual void initialize_draw_data(DrawingProgram& drawP) override;
|
||||
virtual void update(DrawingProgram& drawP) override;
|
||||
virtual bool collides_within_coords(const SCollision::ColliderCollection<float>& checkAgainst, bool colliderAllocated) override;
|
||||
virtual void update_from_delayed_ptr() override;
|
||||
void create_draw_data();
|
||||
virtual void create_collider(bool colliderAllocated) override;
|
||||
|
||||
virtual void free_collider() override;
|
||||
virtual void allocate_collider() override;
|
||||
|
||||
virtual SCollision::AABB<float> get_obj_coord_bounds() const override;
|
||||
|
||||
SkPath ellipsePath;
|
||||
|
||||
// Cache
|
||||
SCollision::BVHContainer<float> collisionTree;
|
||||
private:
|
||||
void create_triangles(const std::function<bool(Vector2f, Vector2f, Vector2f)>& passTriangleFunc);
|
||||
#endif
|
||||
};
|
||||
102
src/DrawComponents/DrawImage.cpp
Normal file
102
src/DrawComponents/DrawImage.cpp
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
#include "DrawImage.hpp"
|
||||
#include "Helpers/ConvertVec.hpp"
|
||||
#include "Helpers/MathExtras.hpp"
|
||||
#include "Helpers/SCollision.hpp"
|
||||
#include "../SharedTypes.hpp"
|
||||
#include "../World.hpp"
|
||||
|
||||
#ifndef IS_SERVER
|
||||
#include <include/core/SkSamplingOptions.h>
|
||||
#include "../DrawCollision.hpp"
|
||||
#endif
|
||||
|
||||
DrawComponentType DrawImage::get_type() const {
|
||||
return DRAWCOMPONENT_IMAGE;
|
||||
}
|
||||
|
||||
void DrawImage::save(cereal::PortableBinaryOutputArchive& a) const {
|
||||
a(d.p1, d.p2, d.imageID);
|
||||
}
|
||||
|
||||
void DrawImage::load(cereal::PortableBinaryInputArchive& a) {
|
||||
a(d.p1, d.p2, d.imageID);
|
||||
}
|
||||
|
||||
void DrawImage::get_used_resources(std::unordered_set<ServerClientID>& v) const {
|
||||
v.emplace(d.imageID);
|
||||
}
|
||||
|
||||
void DrawImage::remap_resource_ids(std::unordered_map<ServerClientID, ServerClientID>& newIDs) {
|
||||
auto it = newIDs.find(d.imageID);
|
||||
if(it != newIDs.end())
|
||||
d.imageID = it->second;
|
||||
}
|
||||
|
||||
#ifndef IS_SERVER
|
||||
std::shared_ptr<DrawComponent> DrawImage::copy() const {
|
||||
auto a = std::make_shared<DrawImage>();
|
||||
a->d = d;
|
||||
a->coords = coords;
|
||||
return a;
|
||||
}
|
||||
|
||||
void DrawImage::update_from_delayed_ptr() {
|
||||
std::shared_ptr<DrawImage> newPtr = std::static_pointer_cast<DrawImage>(delayedUpdatePtr);
|
||||
d = newPtr->d;
|
||||
}
|
||||
|
||||
void DrawImage::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
if(drawSetupData.shouldDraw) {
|
||||
canvas->save();
|
||||
canvas_do_calculated_transform(canvas);
|
||||
std::shared_ptr<ResourceDisplay> display = drawData.rMan->get_display_data(d.imageID);
|
||||
if(display)
|
||||
display->draw(canvas, drawData, imRect);
|
||||
else
|
||||
canvas->drawRect(imRect, SkPaint({0.5f, 0.5f, 0.5f, 0.5f}));
|
||||
canvas->restore();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawImage::update(DrawingProgram& drawP) {
|
||||
std::shared_ptr<ResourceDisplay> display = drawP.world.drawData.rMan->get_display_data(d.imageID);
|
||||
if(display) {
|
||||
if(display->update_draw())// && bounds_draw_check(drawP.world.drawData, collisionTree))
|
||||
updateDraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
void DrawImage::initialize_draw_data(DrawingProgram& drawP) {
|
||||
create_draw_data();
|
||||
}
|
||||
|
||||
bool DrawImage::collides_within_coords(const SCollision::ColliderCollection<float>& checkAgainst, bool colliderAllocated) {
|
||||
return collisionTree.is_collide(checkAgainst);
|
||||
}
|
||||
|
||||
void DrawImage::create_draw_data() {
|
||||
imRect = SkRect::MakeLTRB(d.p1.x(), d.p1.y(), d.p2.x(), d.p2.y());
|
||||
}
|
||||
|
||||
void DrawImage::create_collider(bool colliderAllocated) {
|
||||
using namespace SCollision;
|
||||
ColliderCollection<float> strokeObjects;
|
||||
std::array<Vector2f, 4> newT = triangle_from_rect_points(d.p1, d.p2);
|
||||
strokeObjects.triangle.emplace_back(newT[0], newT[1], newT[2]);
|
||||
strokeObjects.triangle.emplace_back(newT[2], newT[3], newT[0]);
|
||||
collisionTree.clear();
|
||||
collisionTree.calculate_bvh_recursive(strokeObjects);
|
||||
calculate_world_bounds();
|
||||
}
|
||||
|
||||
void DrawImage::free_collider() {
|
||||
}
|
||||
|
||||
void DrawImage::allocate_collider() {
|
||||
}
|
||||
|
||||
SCollision::AABB<float> DrawImage::get_obj_coord_bounds() const {
|
||||
return collisionTree.objects.bounds;
|
||||
}
|
||||
|
||||
#endif
|
||||
46
src/DrawComponents/DrawImage.hpp
Normal file
46
src/DrawComponents/DrawImage.hpp
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#pragma once
|
||||
#include "../SharedTypes.hpp"
|
||||
#include "DrawComponent.hpp"
|
||||
#include "../CoordSpaceHelper.hpp"
|
||||
|
||||
#ifndef IS_SERVER
|
||||
#include <include/core/SkPath.h>
|
||||
#endif
|
||||
|
||||
class DrawImage : public DrawComponent {
|
||||
public:
|
||||
virtual void save(cereal::PortableBinaryOutputArchive& a) const override;
|
||||
virtual void load(cereal::PortableBinaryInputArchive& a) override;
|
||||
virtual DrawComponentType get_type() const override;
|
||||
virtual void get_used_resources(std::unordered_set<ServerClientID>& v) const override;
|
||||
virtual void remap_resource_ids(std::unordered_map<ServerClientID, ServerClientID>& newIDs) override;
|
||||
|
||||
// User input data
|
||||
struct Data {
|
||||
Vector2f p1;
|
||||
Vector2f p2;
|
||||
|
||||
ServerClientID imageID;
|
||||
} d;
|
||||
|
||||
#ifndef IS_SERVER
|
||||
virtual std::shared_ptr<DrawComponent> copy() const override;
|
||||
virtual void draw(SkCanvas* canvas, const DrawData& drawData) override;
|
||||
virtual void initialize_draw_data(DrawingProgram& drawP) override;
|
||||
virtual void update(DrawingProgram& drawP) override;
|
||||
virtual bool collides_within_coords(const SCollision::ColliderCollection<float>& checkAgainst, bool colliderAllocated) override;
|
||||
void create_draw_data();
|
||||
virtual void create_collider(bool colliderAllocated) override;
|
||||
virtual void update_from_delayed_ptr() override;
|
||||
|
||||
virtual void free_collider() override;
|
||||
virtual void allocate_collider() override;
|
||||
|
||||
virtual SCollision::AABB<float> get_obj_coord_bounds() const override;
|
||||
|
||||
SkRect imRect;
|
||||
|
||||
SCollision::BVHContainer<float> collisionTree;
|
||||
#endif
|
||||
};
|
||||
|
||||
124
src/DrawComponents/DrawRectangle.cpp
Normal file
124
src/DrawComponents/DrawRectangle.cpp
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
#include "DrawRectangle.hpp"
|
||||
#include "Helpers/ConvertVec.hpp"
|
||||
#include "Helpers/MathExtras.hpp"
|
||||
#include "Helpers/SCollision.hpp"
|
||||
#include <include/core/SkPaint.h>
|
||||
#include "../SharedTypes.hpp"
|
||||
|
||||
#ifndef IS_SERVER
|
||||
#include "../DrawCollision.hpp"
|
||||
#endif
|
||||
|
||||
DrawComponentType DrawRectangle::get_type() const {
|
||||
return DRAWCOMPONENT_RECTANGLE;
|
||||
}
|
||||
|
||||
void DrawRectangle::save(cereal::PortableBinaryOutputArchive& a) const {
|
||||
a(d.strokeColor, d.fillColor, d.cornerRadius, d.strokeWidth, d.p1, d.p2, d.fillStrokeMode);
|
||||
}
|
||||
|
||||
void DrawRectangle::load(cereal::PortableBinaryInputArchive& a) {
|
||||
a(d.strokeColor, d.fillColor, d.cornerRadius, d.strokeWidth, d.p1, d.p2, d.fillStrokeMode);
|
||||
}
|
||||
|
||||
#ifndef IS_SERVER
|
||||
std::shared_ptr<DrawComponent> DrawRectangle::copy() const {
|
||||
auto a = std::make_shared<DrawRectangle>();
|
||||
a->d = d;
|
||||
a->coords = coords;
|
||||
return a;
|
||||
}
|
||||
|
||||
void DrawRectangle::update_from_delayed_ptr() {
|
||||
std::shared_ptr<DrawRectangle> newPtr = std::static_pointer_cast<DrawRectangle>(delayedUpdatePtr);
|
||||
d = newPtr->d;
|
||||
}
|
||||
|
||||
void DrawRectangle::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
if(drawSetupData.shouldDraw) {
|
||||
canvas->save();
|
||||
canvas_do_calculated_transform(canvas);
|
||||
if(d.fillStrokeMode == 0 || d.fillStrokeMode == 2) {
|
||||
SkPaint p;
|
||||
p.setStyle(SkPaint::kFill_Style);
|
||||
p.setStrokeCap(SkPaint::kRound_Cap);
|
||||
p.setColor4f(convert_vec4<SkColor4f>(d.fillColor));
|
||||
canvas->drawPath(rectPath, p);
|
||||
}
|
||||
if(d.fillStrokeMode == 1 || d.fillStrokeMode == 2) {
|
||||
SkPaint p;
|
||||
p.setStyle(SkPaint::kStroke_Style);
|
||||
p.setStrokeCap(SkPaint::kRound_Cap);
|
||||
p.setColor4f(convert_vec4<SkColor4f>(d.strokeColor));
|
||||
p.setStrokeWidth(d.strokeWidth);
|
||||
canvas->drawPath(rectPath, p);
|
||||
}
|
||||
canvas->restore();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawRectangle::update(DrawingProgram& drawP) {
|
||||
}
|
||||
|
||||
void DrawRectangle::initialize_draw_data(DrawingProgram& drawP) {
|
||||
create_draw_data();
|
||||
}
|
||||
|
||||
bool DrawRectangle::collides_within_coords(const SCollision::ColliderCollection<float>& checkAgainst, bool colliderAllocated) {
|
||||
return collisionTree.is_collide(checkAgainst);
|
||||
}
|
||||
|
||||
void DrawRectangle::create_draw_data() {
|
||||
rectPath = SkPath();
|
||||
if(d.p1.x() == d.p2.x() || d.p1.y() == d.p2.y()) {
|
||||
rectPath.moveTo(convert_vec2<SkPoint>(d.p1));
|
||||
rectPath.lineTo(convert_vec2<SkPoint>(d.p2));
|
||||
}
|
||||
else {
|
||||
SkRect newRect = SkRect::MakeLTRB(d.p1.x(), d.p1.y(), d.p2.x(), d.p2.y());
|
||||
rectPath.addRoundRect(newRect, d.cornerRadius, d.cornerRadius);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawRectangle::create_collider(bool colliderAllocated) {
|
||||
using namespace SCollision;
|
||||
ColliderCollection<float> strokeObjects;
|
||||
if(d.fillStrokeMode == 0) {
|
||||
std::array<Vector2f, 4> newT = triangle_from_rect_points(d.p1, d.p2);
|
||||
strokeObjects.triangle.emplace_back(newT[0], newT[1], newT[2]);
|
||||
strokeObjects.triangle.emplace_back(newT[2], newT[3], newT[0]);
|
||||
}
|
||||
else if(d.fillStrokeMode == 1) {
|
||||
if(d.p1.x() == d.p2.x() || d.p1.y() == d.p2.y()) {
|
||||
std::vector<Vector2f> points = {d.p1, d.p2};
|
||||
generate_polyline(strokeObjects, points, d.strokeWidth, true);
|
||||
}
|
||||
else {
|
||||
std::array<Vector2f, 4> pointArr = triangle_from_rect_points(d.p1, d.p2);
|
||||
std::vector<Vector2f> points(pointArr.begin(), pointArr.end());
|
||||
generate_polyline(strokeObjects, points, d.strokeWidth, true);
|
||||
}
|
||||
}
|
||||
else if(d.fillStrokeMode == 2) {
|
||||
float strokeRadius = d.strokeWidth * 0.5f;
|
||||
std::array<Vector2f, 4> newT = triangle_from_rect_points((d.p1 - Vector2f{strokeRadius, strokeRadius}).eval(), (d.p2 + Vector2f{strokeRadius, strokeRadius}).eval());
|
||||
strokeObjects.triangle.emplace_back(newT[0], newT[1], newT[2]);
|
||||
strokeObjects.triangle.emplace_back(newT[2], newT[3], newT[0]);
|
||||
}
|
||||
|
||||
collisionTree.clear();
|
||||
collisionTree.calculate_bvh_recursive(strokeObjects);
|
||||
calculate_world_bounds();
|
||||
}
|
||||
|
||||
void DrawRectangle::free_collider() {
|
||||
}
|
||||
|
||||
void DrawRectangle::allocate_collider() {
|
||||
}
|
||||
|
||||
SCollision::AABB<float> DrawRectangle::get_obj_coord_bounds() const {
|
||||
return collisionTree.objects.bounds;
|
||||
}
|
||||
|
||||
#endif
|
||||
48
src/DrawComponents/DrawRectangle.hpp
Normal file
48
src/DrawComponents/DrawRectangle.hpp
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#pragma once
|
||||
#include "../SharedTypes.hpp"
|
||||
#include "DrawComponent.hpp"
|
||||
#include "../CoordSpaceHelper.hpp"
|
||||
|
||||
#ifndef IS_SERVER
|
||||
#include <include/core/SkPath.h>
|
||||
#endif
|
||||
|
||||
class DrawRectangle : public DrawComponent {
|
||||
public:
|
||||
virtual void save(cereal::PortableBinaryOutputArchive& a) const override;
|
||||
virtual void load(cereal::PortableBinaryInputArchive& a) override;
|
||||
virtual DrawComponentType get_type() const override;
|
||||
|
||||
// User input data
|
||||
struct Data {
|
||||
Vector4f strokeColor;
|
||||
Vector4f fillColor;
|
||||
float cornerRadius;
|
||||
float strokeWidth;
|
||||
Vector2f p1;
|
||||
Vector2f p2;
|
||||
uint8_t fillStrokeMode;
|
||||
bool operator==(const Data& o) const = default;
|
||||
} d;
|
||||
|
||||
#ifndef IS_SERVER
|
||||
virtual std::shared_ptr<DrawComponent> copy() const override;
|
||||
virtual void draw(SkCanvas* canvas, const DrawData& drawData) override;
|
||||
virtual void initialize_draw_data(DrawingProgram& drawP) override;
|
||||
virtual void update(DrawingProgram& drawP) override;
|
||||
virtual bool collides_within_coords(const SCollision::ColliderCollection<float>& checkAgainst, bool colliderAllocated) override;
|
||||
void create_draw_data();
|
||||
virtual void create_collider(bool colliderAllocated) override;
|
||||
virtual void update_from_delayed_ptr() override;
|
||||
|
||||
virtual void free_collider() override;
|
||||
virtual void allocate_collider() override;
|
||||
|
||||
virtual SCollision::AABB<float> get_obj_coord_bounds() const override;
|
||||
|
||||
SkPath rectPath;
|
||||
|
||||
SCollision::BVHContainer<float> collisionTree;
|
||||
#endif
|
||||
};
|
||||
|
||||
134
src/DrawComponents/DrawTextBox.cpp
Normal file
134
src/DrawComponents/DrawTextBox.cpp
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
#include "DrawTextBox.hpp"
|
||||
#include "Helpers/ConvertVec.hpp"
|
||||
#include <cereal/types/string.hpp>
|
||||
|
||||
#ifndef IS_SERVER
|
||||
#include "../MainProgram.hpp"
|
||||
#include "Helpers/SCollision.hpp"
|
||||
#include "../DrawingProgram/DrawingProgram.hpp"
|
||||
#include "../DrawCollision.hpp"
|
||||
#endif
|
||||
|
||||
DrawComponentType DrawTextBox::get_type() const {
|
||||
return DRAWCOMPONENT_TEXTBOX;
|
||||
}
|
||||
|
||||
void DrawTextBox::save(cereal::PortableBinaryOutputArchive& a) const {
|
||||
a(d.editing, d.p1, d.p2, d.textColor, d.textSize, d.cursor, d.currentText);
|
||||
}
|
||||
|
||||
void DrawTextBox::load(cereal::PortableBinaryInputArchive& a) {
|
||||
a(d.editing, d.p1, d.p2, d.textColor, d.textSize, d.cursor, d.currentText);
|
||||
#ifndef IS_SERVER
|
||||
textboxUpdate = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef IS_SERVER
|
||||
std::shared_ptr<DrawComponent> DrawTextBox::copy() const {
|
||||
auto a = std::make_shared<DrawTextBox>();
|
||||
a->d = d;
|
||||
a->coords = coords;
|
||||
a->textboxUpdate = true;
|
||||
return a;
|
||||
}
|
||||
|
||||
void DrawTextBox::update_from_delayed_ptr() {
|
||||
std::shared_ptr<DrawTextBox> newPtr = std::static_pointer_cast<DrawTextBox>(delayedUpdatePtr);
|
||||
d = newPtr->d;
|
||||
textboxUpdate = true;
|
||||
}
|
||||
|
||||
void DrawTextBox::init_text_box(DrawingProgram& drawP) {
|
||||
textBox.setFontMgr(drawP.world.main.fonts.mgr);
|
||||
SkFont f(drawP.world.main.fonts.map["Roboto"], d.textSize);
|
||||
f.setLinearMetrics(true);
|
||||
f.setHinting(SkFontHinting::kNormal);
|
||||
//font.setForceAutoHinting(true);
|
||||
f.setSubpixel(true);
|
||||
f.setBaselineSnap(true);
|
||||
f.setEdging(SkFont::Edging::kSubpixelAntiAlias);
|
||||
textBox.setFont(f);
|
||||
textBox.setWidth(d.p2.x() - d.p1.x());
|
||||
textBox.fLines.clear();
|
||||
textBox.insert({0, 0}, d.currentText);
|
||||
}
|
||||
|
||||
void DrawTextBox::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
if(drawSetupData.shouldDraw) {
|
||||
canvas->save();
|
||||
canvas_do_calculated_transform(canvas);
|
||||
SkRect clipR = SkRect::MakeLTRB(d.p1.x(), d.p1.y(), d.p2.x(), d.p2.y());
|
||||
if(d.editing) {
|
||||
SkPaint p;
|
||||
p.setStyle(SkPaint::kStroke_Style);
|
||||
p.setStrokeWidth(0.0f);
|
||||
p.setColor4f({0.5f, 0.5f, 0.5f, 1.0f});
|
||||
canvas->drawRect(clipR, p);
|
||||
}
|
||||
canvas->clipRect(clipR);
|
||||
canvas->translate(d.p1.x(), d.p1.y());
|
||||
|
||||
CollabTextBox::Editor::PaintOpts paintOpts;
|
||||
paintOpts.fForegroundColor = convert_vec4<SkColor4f>(d.textColor);
|
||||
paintOpts.fBackgroundColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
paintOpts.cursorColor = {0.2f, 0.2f, 0.7f, 1.0f};
|
||||
paintOpts.showCursor = d.editing;
|
||||
paintOpts.cursor = d.cursor;
|
||||
|
||||
textBox.paint(canvas, paintOpts);
|
||||
canvas->restore();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawTextBox::update(DrawingProgram& drawP) {
|
||||
if(textboxUpdate)
|
||||
set_textbox_string(d.currentText);
|
||||
}
|
||||
|
||||
void DrawTextBox::set_textbox_string(const std::string& str) {
|
||||
d.currentText = str;
|
||||
textBox.fLines.clear();
|
||||
textBox.insert({0, 0}, d.currentText);
|
||||
textboxUpdate = false;
|
||||
}
|
||||
|
||||
void DrawTextBox::update_contained_string() {
|
||||
d.currentText = textBox.get_string();
|
||||
updateDraw = true;
|
||||
}
|
||||
|
||||
Vector2f DrawTextBox::get_mouse_pos(DrawingProgram& drawP) {
|
||||
return coords.get_mouse_pos(drawP.world) - d.p1;
|
||||
}
|
||||
|
||||
void DrawTextBox::initialize_draw_data(DrawingProgram& drawP) {
|
||||
init_text_box(drawP);
|
||||
}
|
||||
|
||||
bool DrawTextBox::collides_within_coords(const SCollision::ColliderCollection<float>& checkAgainst, bool colliderAllocated) {
|
||||
return collisionTree.is_collide(checkAgainst);
|
||||
}
|
||||
|
||||
void DrawTextBox::create_collider(bool colliderAllocated) {
|
||||
using namespace SCollision;
|
||||
ColliderCollection<float> strokeObjects;
|
||||
std::array<Vector2f, 4> newT = triangle_from_rect_points(d.p1, d.p2);
|
||||
strokeObjects.triangle.emplace_back(newT[0], newT[1], newT[2]);
|
||||
strokeObjects.triangle.emplace_back(newT[2], newT[3], newT[0]);
|
||||
collisionTree.clear();
|
||||
collisionTree.calculate_bvh_recursive(strokeObjects);
|
||||
calculate_world_bounds();
|
||||
}
|
||||
|
||||
void DrawTextBox::free_collider() {
|
||||
}
|
||||
|
||||
void DrawTextBox::allocate_collider() {
|
||||
}
|
||||
|
||||
SCollision::AABB<float> DrawTextBox::get_obj_coord_bounds() const {
|
||||
return collisionTree.objects.bounds;
|
||||
}
|
||||
|
||||
#endif
|
||||
52
src/DrawComponents/DrawTextBox.hpp
Normal file
52
src/DrawComponents/DrawTextBox.hpp
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#pragma once
|
||||
#include "../SharedTypes.hpp"
|
||||
#include "DrawComponent.hpp"
|
||||
#include "../CollabTextBox/CollabTextBoxCursor.hpp"
|
||||
#include "../CoordSpaceHelper.hpp"
|
||||
|
||||
#ifndef IS_SERVER
|
||||
#include <include/core/SkPath.h>
|
||||
#include "../CollabTextBox/CollabTextBox.hpp"
|
||||
#endif
|
||||
|
||||
class DrawTextBox : public DrawComponent {
|
||||
public:
|
||||
virtual DrawComponentType get_type() const override;
|
||||
virtual void save(cereal::PortableBinaryOutputArchive& a) const override;
|
||||
virtual void load(cereal::PortableBinaryInputArchive& a) override;
|
||||
|
||||
// User input data
|
||||
struct Data {
|
||||
Vector4f textColor;
|
||||
float textSize = -100000.0f;
|
||||
Vector2f p1;
|
||||
Vector2f p2;
|
||||
std::string currentText;
|
||||
CollabTextBox::Cursor cursor;
|
||||
bool editing = false;
|
||||
bool operator==(const Data& o) const = default;
|
||||
} d;
|
||||
|
||||
#ifndef IS_SERVER
|
||||
virtual std::shared_ptr<DrawComponent> copy() const override;
|
||||
virtual void draw(SkCanvas* canvas, const DrawData& drawData) override;
|
||||
virtual void initialize_draw_data(DrawingProgram& drawP) override;
|
||||
virtual bool collides_within_coords(const SCollision::ColliderCollection<float>& checkAgainst, bool colliderAllocated) override;
|
||||
virtual void update(DrawingProgram& drawP) override;
|
||||
virtual void create_collider(bool colliderAllocated) override;
|
||||
virtual void update_from_delayed_ptr() override;
|
||||
void init_text_box(DrawingProgram& drawP);
|
||||
void update_contained_string();
|
||||
void set_textbox_string(const std::string& str);
|
||||
Vector2f get_mouse_pos(DrawingProgram& drawP);
|
||||
|
||||
virtual void free_collider() override;
|
||||
virtual void allocate_collider() override;
|
||||
|
||||
virtual SCollision::AABB<float> get_obj_coord_bounds() const override;
|
||||
|
||||
bool textboxUpdate = false;
|
||||
CollabTextBox::Editor textBox;
|
||||
SCollision::BVHContainer<float> collisionTree;
|
||||
#endif
|
||||
};
|
||||
1
src/DrawData.cpp
Normal file
1
src/DrawData.cpp
Normal file
|
|
@ -0,0 +1 @@
|
|||
#include "DrawData.hpp"
|
||||
19
src/DrawData.hpp
Normal file
19
src/DrawData.hpp
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
#include "DrawCamera.hpp"
|
||||
#include "ResourceManager.hpp"
|
||||
#include "FontData.hpp"
|
||||
|
||||
using namespace Eigen;
|
||||
|
||||
class Toolbar;
|
||||
class MainProgram;
|
||||
|
||||
struct DrawData {
|
||||
DrawCamera cam;
|
||||
ResourceManager* rMan;
|
||||
MainProgram* main;
|
||||
bool dontUseDrawProgCache = false;
|
||||
bool clampDrawBetween = true;
|
||||
WorldScalar clampDrawMinimum;
|
||||
WorldScalar clampDrawMaximum;
|
||||
};
|
||||
196
src/DrawingProgram/BrushTool.cpp
Normal file
196
src/DrawingProgram/BrushTool.cpp
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
#include "BrushTool.hpp"
|
||||
#include <Helpers/ConvertVec.hpp>
|
||||
#include "../TimePoint.hpp"
|
||||
#include "../DrawComponents/DrawBrushStroke.hpp"
|
||||
#include "../GUIStuff/GUIManager.hpp"
|
||||
#include "../SharedTypes.hpp"
|
||||
#include "../Server/CommandList.hpp"
|
||||
#include "DrawingProgram.hpp"
|
||||
#include "../MainProgram.hpp"
|
||||
#include <chrono>
|
||||
|
||||
#define VEL_SMOOTH_MIN 0.6
|
||||
#define VEL_SMOOTH_MAX 1.0
|
||||
#define MINIMUM_DISTANCE_TO_NEXT_POINT 0.002f
|
||||
|
||||
BrushTool::BrushTool(DrawingProgram& initDrawP):
|
||||
drawP(initDrawP)
|
||||
{
|
||||
controls.intermediateItem = std::make_shared<DrawBrushStroke>();
|
||||
}
|
||||
|
||||
void BrushTool::reset_tool() {
|
||||
commit_stroke();
|
||||
controls.isDrawing = false;
|
||||
}
|
||||
|
||||
bool BrushTool::extensive_point_checking(const Vector2f& newPoint) {
|
||||
auto& points = controls.intermediateItem->d.points;
|
||||
if(points.size() >= 1 && (newPoint - points[points.size() - 1].pos).norm() < MINIMUM_DISTANCE_TO_NEXT_POINT)
|
||||
return false;
|
||||
if(points.size() >= 2 && (newPoint - points[points.size() - 2].pos).norm() < MINIMUM_DISTANCE_TO_NEXT_POINT)
|
||||
return false;
|
||||
if(points.size() >= 3 && (newPoint - points[points.size() - 3].pos).norm() < MINIMUM_DISTANCE_TO_NEXT_POINT)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BrushTool::extensive_point_checking_back(const Vector2f& newPoint) {
|
||||
auto& points = controls.intermediateItem->d.points;
|
||||
if(points.size() >= 2 && (newPoint - points[points.size() - 2].pos).norm() < MINIMUM_DISTANCE_TO_NEXT_POINT)
|
||||
return false;
|
||||
if(points.size() >= 3 && (newPoint - points[points.size() - 3].pos).norm() < MINIMUM_DISTANCE_TO_NEXT_POINT)
|
||||
return false;
|
||||
if(points.size() >= 4 && (newPoint - points[points.size() - 4].pos).norm() < MINIMUM_DISTANCE_TO_NEXT_POINT)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void BrushTool::tool_update() {
|
||||
if(drawP.controls.cursorHoveringOverCanvas)
|
||||
drawP.world.main.input.hideCursor = true;
|
||||
|
||||
if(!controls.isDrawing)
|
||||
controls.penSmoothingData.clear();
|
||||
|
||||
float averageScreenSize = (drawP.world.main.window.size.x() + drawP.world.main.window.size.y()) / 2.0f;
|
||||
float minVel = 0.2f * averageScreenSize;
|
||||
float maxVel = 3.5f * averageScreenSize;
|
||||
|
||||
while(!controls.velocitySmoothingData.empty() && (std::chrono::steady_clock::now() - controls.velocitySmoothingData.front().t) > std::chrono::milliseconds(200))
|
||||
controls.velocitySmoothingData.pop_front();
|
||||
controls.velocitySmoothingData.emplace_back(
|
||||
drawP.world.main.input.mouse.move.norm() / drawP.world.main.deltaTime,
|
||||
std::chrono::steady_clock::now()
|
||||
);
|
||||
|
||||
if(drawP.world.main.input.pen.isDown && drawP.controls.leftClickHeld && drawP.world.main.toolbar.tabletOptions.pressureAffectsBrushWidth) {
|
||||
while(!controls.penSmoothingData.empty() && (std::chrono::steady_clock::now() - controls.penSmoothingData.front().t) > std::chrono::milliseconds(static_cast<int>(drawP.world.main.toolbar.tabletOptions.smoothingSamplingTime * 1000.0f)))
|
||||
controls.penSmoothingData.pop_front();
|
||||
controls.penSmoothingData.emplace_back(
|
||||
drawP.world.main.input.pen.pressure,
|
||||
std::chrono::steady_clock::now()
|
||||
);
|
||||
float averagePressure = 0.0f;
|
||||
for(auto& [width, t] : controls.penSmoothingData)
|
||||
averagePressure += width;
|
||||
averagePressure /= controls.penSmoothingData.size();
|
||||
controls.penWidth = averagePressure;
|
||||
}
|
||||
else if(drawP.world.main.toolbar.velocityAffectsBrushWidth) {
|
||||
float averageVel = 0.0f;
|
||||
for(auto& [width, t] : controls.velocitySmoothingData)
|
||||
averageVel += width;
|
||||
averageVel /= controls.velocitySmoothingData.size();
|
||||
controls.penWidth = std::clamp<float>((averageVel - minVel) / (maxVel - minVel), VEL_SMOOTH_MIN, VEL_SMOOTH_MAX);
|
||||
}
|
||||
else
|
||||
controls.penWidth = 1.0f;
|
||||
|
||||
bool isPenDown = false;
|
||||
if(drawP.world.main.input.pen.isDown && drawP.controls.leftClickHeld)
|
||||
isPenDown = controls.penWidth != 0.0;
|
||||
else if(drawP.controls.leftClickHeld)
|
||||
isPenDown = true;
|
||||
|
||||
if(isPenDown) {
|
||||
if(!controls.isDrawing) {
|
||||
controls.isDrawing = true;
|
||||
controls.intermediateItem = std::make_shared<DrawBrushStroke>();
|
||||
DrawBrushStrokePoint p;
|
||||
p.pos = drawP.world.main.input.mouse.pos;
|
||||
p.width = drawP.controls.relativeWidth * controls.penWidth;
|
||||
controls.prevPointUnaltered = p.pos;
|
||||
controls.intermediateItem->d.points.emplace_back(p);
|
||||
controls.intermediateItem->d.color = drawP.controls.foregroundColor;
|
||||
controls.intermediateItem->d.hasRoundCaps = controls.hasRoundCaps;
|
||||
controls.intermediateItem->coords = drawP.world.drawData.cam.c;
|
||||
controls.intermediateItem->lastUpdateTime = std::chrono::steady_clock::now();
|
||||
controls.intermediateItem->temp_update(drawP);
|
||||
uint64_t placement = drawP.components.client_list().size();
|
||||
controls.intermediateID = drawP.components.client_insert(placement, controls.intermediateItem);
|
||||
controls.intermediateItem->client_send_place(drawP, placement);
|
||||
// NOTE: The data isnt finallized at this point, so a BVH isn't generated for an object that you undo while youre drawing (this isnt just for brush strokes)
|
||||
// The fix for now is to generate a BVH for an object when you redo it
|
||||
drawP.add_undo_place_component(placement, controls.intermediateItem);
|
||||
controls.addedTemporaryPoint = false;
|
||||
}
|
||||
else {
|
||||
controls.intermediateItem->lastUpdateTime = std::chrono::steady_clock::now();
|
||||
|
||||
DrawBrushStrokePoint p;
|
||||
p.pos = controls.intermediateItem->coords.get_mouse_pos(drawP.world);
|
||||
p.width = drawP.controls.relativeWidth * controls.penWidth;
|
||||
|
||||
if(!controls.addedTemporaryPoint) {
|
||||
if(extensive_point_checking(p.pos)) {
|
||||
controls.intermediateItem->d.points.emplace_back(p);
|
||||
controls.addedTemporaryPoint = true;
|
||||
}
|
||||
else
|
||||
controls.intermediateItem->d.points.back().width = std::max(controls.intermediateItem->d.points.back().width, p.width);
|
||||
}
|
||||
|
||||
if(controls.addedTemporaryPoint) {
|
||||
const DrawBrushStrokePoint& prevP = controls.intermediateItem->d.points[controls.intermediateItem->d.points.size() - 2];
|
||||
float distToPrev = (p.pos - prevP.pos).norm();
|
||||
if(extensive_point_checking_back(p.pos)) {
|
||||
controls.intermediateItem->d.points.back().pos = p.pos;
|
||||
controls.intermediateItem->d.points.back().width = std::max(controls.intermediateItem->d.points.back().width, p.width);
|
||||
controls.intermediateItem->d.points[controls.intermediateItem->d.points.size() - 2].width = std::max(controls.intermediateItem->d.points[controls.intermediateItem->d.points.size() - 2].width, p.width);
|
||||
}
|
||||
if((!controls.drawingMinimumRelativeToSize && distToPrev >= 10.0) || (controls.drawingMinimumRelativeToSize && distToPrev >= drawP.controls.relativeWidth * DRAW_MINIMUM_LIMIT)) {
|
||||
controls.intermediateItem->d.points.back() = p;
|
||||
controls.addedTemporaryPoint = false;
|
||||
controls.intermediateItem->client_send_update_temp(drawP, controls.intermediateID);
|
||||
|
||||
if(controls.midwayInterpolation) {
|
||||
if(controls.intermediateItem->d.points.size() != 2) // Don't interpolate the first point
|
||||
controls.intermediateItem->d.points[controls.intermediateItem->d.points.size() - 2].pos = (controls.prevPointUnaltered + p.pos) * 0.5;
|
||||
controls.prevPointUnaltered = p.pos;
|
||||
}
|
||||
}
|
||||
controls.intermediateItem->temp_update(drawP);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(controls.isDrawing) {
|
||||
controls.isDrawing = false;
|
||||
commit_stroke();
|
||||
}
|
||||
}
|
||||
|
||||
void BrushTool::gui_toolbox() {
|
||||
Toolbar& t = drawP.world.main.toolbar;
|
||||
t.gui.push_id("brush tool");
|
||||
t.gui.text_label_centered("Brush");
|
||||
t.gui.checkbox_field("hasroundcaps", "Round Caps", &controls.hasRoundCaps);
|
||||
t.gui.slider_scalar_field("relwidth", "Size", &drawP.controls.relativeWidth, 3.0f, 40.0f);
|
||||
//t.gui.checkbox_field("drawminrel", "Drawing Min Relative To Size", &controls.drawingMinimumRelativeToSize);
|
||||
//t.gui.checkbox_field("midwayinterp", "Midway Interpolation", &controls.midwayInterpolation);
|
||||
t.gui.pop_id();
|
||||
}
|
||||
|
||||
void BrushTool::commit_stroke() {
|
||||
if(controls.intermediateItem) {
|
||||
controls.intermediateItem->client_send_update_final(drawP, controls.intermediateID);
|
||||
controls.intermediateItem->final_update(drawP);
|
||||
controls.intermediateItem = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void BrushTool::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
if(!drawData.main->toolbar.io->hoverObstructed) {
|
||||
SkPaint linePaint;
|
||||
linePaint.setColor4f(drawP.world.main.canvasTheme.toolFrontColor);
|
||||
linePaint.setStroke(true);
|
||||
linePaint.setStrokeWidth(0.0f);
|
||||
float width;
|
||||
if(controls.isDrawing)
|
||||
width = drawP.controls.relativeWidth * controls.penWidth * 0.5f;
|
||||
else
|
||||
width = drawP.controls.relativeWidth * 0.5f;
|
||||
Vector2f pos = drawData.main->input.mouse.pos;
|
||||
canvas->drawCircle(pos.x(), pos.y(), width, linePaint);
|
||||
}
|
||||
}
|
||||
47
src/DrawingProgram/BrushTool.hpp
Normal file
47
src/DrawingProgram/BrushTool.hpp
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#pragma once
|
||||
#include "../SharedTypes.hpp"
|
||||
#include <Helpers/Hashes.hpp>
|
||||
#include <include/core/SkPath.h>
|
||||
#include <include/core/SkVertices.h>
|
||||
#include <Helpers/SCollision.hpp>
|
||||
#include <deque>
|
||||
#include <any>
|
||||
#include <include/core/SkCanvas.h>
|
||||
|
||||
class DrawingProgram;
|
||||
class DrawBrushStroke;
|
||||
struct DrawData;
|
||||
|
||||
class BrushTool {
|
||||
public:
|
||||
BrushTool(DrawingProgram& initDrawP);
|
||||
void gui_toolbox();
|
||||
void tool_update();
|
||||
void reset_tool();
|
||||
void draw(SkCanvas* canvas, const DrawData& drawData);
|
||||
private:
|
||||
void commit_stroke();
|
||||
bool extensive_point_checking_back(const Vector2f& newPoint);
|
||||
bool extensive_point_checking(const Vector2f& newPoint);
|
||||
|
||||
struct BrushToolControls {
|
||||
float penWidth = 1.0f;
|
||||
bool addedTemporaryPoint = false;
|
||||
struct SmoothingPoint {
|
||||
float val;
|
||||
std::chrono::steady_clock::time_point t;
|
||||
};
|
||||
std::deque<SmoothingPoint> velocitySmoothingData;
|
||||
std::deque<SmoothingPoint> penSmoothingData;
|
||||
|
||||
std::shared_ptr<DrawBrushStroke> intermediateItem;
|
||||
ServerClientID intermediateID;
|
||||
bool isDrawing = false;
|
||||
bool hasRoundCaps = true;
|
||||
bool drawingMinimumRelativeToSize = true;
|
||||
bool midwayInterpolation = true;
|
||||
Vector2f prevPointUnaltered = {0, 0};
|
||||
} controls;
|
||||
|
||||
DrawingProgram& drawP;
|
||||
};
|
||||
578
src/DrawingProgram/DrawingProgram.cpp
Normal file
578
src/DrawingProgram/DrawingProgram.cpp
Normal file
|
|
@ -0,0 +1,578 @@
|
|||
#include "DrawingProgram.hpp"
|
||||
#include "Helpers/Hashes.hpp"
|
||||
#include "Helpers/MathExtras.hpp"
|
||||
#include "Helpers/SCollision.hpp"
|
||||
#include <include/core/SkPaint.h>
|
||||
#include <include/core/SkVertices.h>
|
||||
#include "../DrawCamera.hpp"
|
||||
#include "../DrawComponents/DrawComponent.hpp"
|
||||
#include "EllipseDrawTool.hpp"
|
||||
#include "InkDropperTool.hpp"
|
||||
#include "../InputManager.hpp"
|
||||
#include "../SharedTypes.hpp"
|
||||
#include "../Server/CommandList.hpp"
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <Helpers/ConvertVec.hpp>
|
||||
#include <cereal/types/vector.hpp>
|
||||
#include <iostream>
|
||||
#include "../DrawComponents/DrawImage.hpp"
|
||||
#include <include/core/SkImage.h>
|
||||
#include <include/core/SkSurface.h>
|
||||
#include "../World.hpp"
|
||||
#include "../MainProgram.hpp"
|
||||
#include <Helpers/Logger.hpp>
|
||||
#include <execution>
|
||||
#ifdef __APPLE__
|
||||
#define POOLSTL_STD_SUPPLEMENT
|
||||
#include <poolstl.hpp>
|
||||
#endif
|
||||
|
||||
DrawingProgram::DrawingProgram(World& initWorld):
|
||||
world(initWorld),
|
||||
components([&](){ return world.get_new_id(); }),
|
||||
brushTool(*this),
|
||||
eraserTool(*this),
|
||||
rectSelectTool(*this),
|
||||
rectDrawTool(*this),
|
||||
ellipseDrawTool(*this),
|
||||
textBoxTool(*this),
|
||||
inkDropperTool(*this),
|
||||
screenshotTool(*this),
|
||||
editTool(*this),
|
||||
imageTool(*this)
|
||||
{
|
||||
components.updateCallback = [&]() {
|
||||
if(&world == world.main.world.get())
|
||||
world.main.drawProgCache.refresh = true;
|
||||
};
|
||||
}
|
||||
|
||||
void DrawingProgram::init_client_callbacks() {
|
||||
world.con.client_add_recv_callback(CLIENT_UPDATE_COMPONENT, [&](cereal::PortableBinaryInputArchive& message) {
|
||||
bool isTemp;
|
||||
ServerClientID id;
|
||||
message(isTemp, id);
|
||||
std::shared_ptr<DrawComponent>* compPtr = components.get_item_by_id(id);
|
||||
if(!compPtr)
|
||||
return;
|
||||
std::shared_ptr<DrawComponent>& comp = *compPtr;
|
||||
float dur = std::chrono::duration_cast<std::chrono::duration<float>>(std::chrono::steady_clock::now() - comp->lastUpdateTime).count();
|
||||
if(dur > CLIENT_DRAWCOMP_DELAY_TIMER_DURATION){
|
||||
comp->delayedUpdatePtr = nullptr;
|
||||
message(*comp);
|
||||
if(isTemp)
|
||||
comp->temp_update(*this);
|
||||
else
|
||||
comp->final_update(*this);
|
||||
}
|
||||
else {
|
||||
comp->delayedUpdatePtr = DrawComponent::allocate_comp_type(comp->get_type());
|
||||
message(*comp->delayedUpdatePtr);
|
||||
}
|
||||
});
|
||||
world.con.client_add_recv_callback(CLIENT_TRANSFORM_COMPONENT, [&](cereal::PortableBinaryInputArchive& message) {
|
||||
bool isTemp;
|
||||
ServerClientID id;
|
||||
message(isTemp, id);
|
||||
std::shared_ptr<DrawComponent>* compPtr = components.get_item_by_id(id);
|
||||
if(!compPtr)
|
||||
return;
|
||||
std::shared_ptr<DrawComponent>& comp = *compPtr;
|
||||
float dur = std::chrono::duration_cast<std::chrono::duration<float>>(std::chrono::steady_clock::now() - comp->lastTransformTime).count();
|
||||
if(dur > CLIENT_DRAWCOMP_DELAY_TIMER_DURATION){
|
||||
comp->delayedCoordinateSpace = nullptr;
|
||||
comp->updateDraw = true;
|
||||
message(comp->coords);
|
||||
if(!isTemp)
|
||||
comp->finalize_update(colliderAllocated); // No need to update draw data during transformation
|
||||
}
|
||||
else {
|
||||
comp->delayedCoordinateSpace = std::make_shared<CoordSpaceHelper>();
|
||||
message(*comp->delayedCoordinateSpace);
|
||||
}
|
||||
});
|
||||
world.con.client_add_recv_callback(CLIENT_PLACE_COMPONENT, [&](cereal::PortableBinaryInputArchive& message) {
|
||||
uint64_t insertPosition;
|
||||
DrawComponentType type;
|
||||
message(insertPosition, type);
|
||||
ServerClientID newObjectID;
|
||||
auto newObj = DrawComponent::allocate_comp_type(type);
|
||||
message(newObjectID, newObj->coords, *newObj);
|
||||
bool didntExistPreviously = components.server_insert(insertPosition, newObjectID, newObj);
|
||||
if(didntExistPreviously)
|
||||
newObj->final_update(*this);
|
||||
});
|
||||
world.con.client_add_recv_callback(CLIENT_ERASE_COMPONENT, [&](cereal::PortableBinaryInputArchive& message) {
|
||||
ServerClientID cToRemove;
|
||||
message(cToRemove);
|
||||
components.server_erase(cToRemove);
|
||||
});
|
||||
}
|
||||
|
||||
void DrawingProgram::free_collider_memory() {
|
||||
for(auto& c : components.client_list())
|
||||
c->obj->free_collider();
|
||||
colliderAllocated = false;
|
||||
}
|
||||
|
||||
void DrawingProgram::allocate_collider_memory() {
|
||||
for(auto& c : components.client_list())
|
||||
c->obj->allocate_collider();
|
||||
colliderAllocated = true;
|
||||
}
|
||||
|
||||
void DrawingProgram::check_all_collisions_base(const SCollision::ColliderCollection<WorldScalar>& checkAgainstWorld, const SCollision::ColliderCollection<float>& checkAgainstCam) {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
std::for_each(components.client_list().begin(), components.client_list().end(), [&](auto& c) {
|
||||
c->obj->globalCollisionCheck = c->obj->collides_with(world.drawData.cam.c, checkAgainstWorld, checkAgainstCam, colliderAllocated);
|
||||
});
|
||||
#else
|
||||
std::for_each(std::execution::par_unseq, components.client_list().begin(), components.client_list().end(), [&](auto& c) {
|
||||
c->obj->globalCollisionCheck = c->obj->collides_with(world.drawData.cam.c, checkAgainstWorld, checkAgainstCam, colliderAllocated);
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void DrawingProgram::check_all_collisions(const SCollision::ColliderCollection<WorldScalar>& checkAgainst) {
|
||||
check_all_collisions_base(checkAgainst, world.drawData.cam.c.world_collider_to_coords<SCollision::ColliderCollection<float>>(checkAgainst));
|
||||
}
|
||||
|
||||
void DrawingProgram::check_all_collisions_transform(const SCollision::ColliderCollection<float>& checkAgainst) {
|
||||
check_all_collisions_base(world.drawData.cam.c.collider_to_world<SCollision::ColliderCollection<WorldScalar>, SCollision::ColliderCollection<float>>(checkAgainst), checkAgainst);
|
||||
}
|
||||
|
||||
std::unordered_set<ServerClientID> DrawingProgram::get_used_resources() {
|
||||
std::unordered_set<ServerClientID> toRet;
|
||||
for(auto& c : components.client_list())
|
||||
c->obj->get_used_resources(toRet);
|
||||
return toRet;
|
||||
}
|
||||
|
||||
void DrawingProgram::toolbar_gui() {
|
||||
Toolbar& t = world.main.toolbar;
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_FIT(0), .height = CLAY_SIZING_FIT(0)},
|
||||
.padding = CLAY_PADDING_ALL(t.io->theme->padding1),
|
||||
.childGap = t.io->theme->childGap1,
|
||||
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP},
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||
},
|
||||
.backgroundColor = convert_vec4<Clay_Color>(t.io->theme->backColor1),
|
||||
.cornerRadius = CLAY_CORNER_RADIUS(t.io->theme->windowCorners1),
|
||||
.border = {.color = convert_vec4<Clay_Color>(t.io->theme->backColor2), .width = CLAY_BORDER_OUTSIDE(t.io->theme->windowBorders1)}
|
||||
}) {
|
||||
t.gui.obstructing_window();
|
||||
if(t.gui.svg_icon_button("Brush Toolbar Button", "data/icons/brush.svg", controls.selectedTool == TOOL_BRUSH)) { controls.selectedTool = TOOL_BRUSH; }
|
||||
if(t.gui.svg_icon_button("Eraser Toolbar Button", "data/icons/eraser.svg", controls.selectedTool == TOOL_ERASER)) { controls.selectedTool = TOOL_ERASER; }
|
||||
if(t.gui.svg_icon_button("Text Toolbar Button", "data/icons/text.svg", controls.selectedTool == TOOL_TEXTBOX)) { controls.selectedTool = TOOL_TEXTBOX; }
|
||||
if(t.gui.svg_icon_button("Ellipse Toolbar Button", "data/icons/circle.svg", controls.selectedTool == TOOL_ELLIPSE)) { controls.selectedTool = TOOL_ELLIPSE; }
|
||||
if(t.gui.svg_icon_button("Rect Toolbar Button", "data/icons/rectangle.svg", controls.selectedTool == TOOL_RECTANGLE)) { controls.selectedTool = TOOL_RECTANGLE; }
|
||||
if(t.gui.svg_icon_button("RectSelect Toolbar Button", "data/icons/rectselect.svg", controls.selectedTool == TOOL_RECTSELECT)) { controls.selectedTool = TOOL_RECTSELECT; }
|
||||
if(t.gui.svg_icon_button("Edit Toolbar Button", "data/icons/cursor.svg", controls.selectedTool == TOOL_EDIT)) { controls.selectedTool = TOOL_EDIT; }
|
||||
if(t.gui.svg_icon_button("Inkdropper Toolbar Button", "data/icons/eyedropper.svg", controls.selectedTool == TOOL_INKDROPPER)) { controls.selectedTool = TOOL_INKDROPPER; }
|
||||
if(t.gui.svg_icon_button("Screenshot Toolbar Button", "data/icons/camera.svg", controls.selectedTool == TOOL_SCREENSHOT)) { controls.selectedTool = TOOL_SCREENSHOT; }
|
||||
CLAY({.layout = {.sizing = {.width = CLAY_SIZING_FIXED(40), .height = CLAY_SIZING_FIXED(40)}}}) {
|
||||
if(t.gui.color_button("Foreground Color", &controls.foregroundColor, &controls.foregroundColor == t.colorLeft)) {
|
||||
t.color_selector_left(&controls.foregroundColor == t.colorLeft ? nullptr : &controls.foregroundColor);
|
||||
}
|
||||
}
|
||||
CLAY({.layout = {.sizing = {.width = CLAY_SIZING_FIXED(40), .height = CLAY_SIZING_FIXED(40)}}}) {
|
||||
if(t.gui.color_button("Background Color", &controls.backgroundColor, &controls.backgroundColor == t.colorLeft))
|
||||
t.color_selector_left(&controls.backgroundColor == t.colorLeft ? nullptr : &controls.backgroundColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawingProgram::tool_options_gui() {
|
||||
Toolbar& t = world.main.toolbar;
|
||||
float minGUIWidth = controls.selectedTool == TOOL_SCREENSHOT ? 300 : 200;
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_FIT(minGUIWidth), .height = CLAY_SIZING_FIT(0)},
|
||||
.padding = CLAY_PADDING_ALL(t.io->theme->padding1),
|
||||
.childGap = t.io->theme->childGap1,
|
||||
.childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP},
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||
},
|
||||
.backgroundColor = convert_vec4<Clay_Color>(t.io->theme->backColor1),
|
||||
.cornerRadius = CLAY_CORNER_RADIUS(t.io->theme->windowCorners1),
|
||||
.border = {.color = convert_vec4<Clay_Color>(t.io->theme->backColor2), .width = CLAY_BORDER_OUTSIDE(t.io->theme->windowBorders1)}
|
||||
}) {
|
||||
t.gui.obstructing_window();
|
||||
switch(controls.selectedTool) {
|
||||
case TOOL_BRUSH:
|
||||
brushTool.gui_toolbox();
|
||||
break;
|
||||
case TOOL_ERASER:
|
||||
eraserTool.gui_toolbox();
|
||||
break;
|
||||
case TOOL_RECTSELECT:
|
||||
rectSelectTool.gui_toolbox();
|
||||
break;
|
||||
case TOOL_RECTANGLE:
|
||||
rectDrawTool.gui_toolbox();
|
||||
break;
|
||||
case TOOL_ELLIPSE:
|
||||
ellipseDrawTool.gui_toolbox();
|
||||
break;
|
||||
case TOOL_TEXTBOX:
|
||||
textBoxTool.gui_toolbox();
|
||||
break;
|
||||
case TOOL_INKDROPPER:
|
||||
inkDropperTool.gui_toolbox();
|
||||
break;
|
||||
case TOOL_SCREENSHOT:
|
||||
screenshotTool.gui_toolbox();
|
||||
break;
|
||||
case TOOL_EDIT:
|
||||
editTool.gui_toolbox();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
void DrawingProgram::update() {
|
||||
if(controls.selectedTool == TOOL_BRUSH && world.main.input.pen.isEraser && !temporaryEraser) {
|
||||
controls.selectedTool = TOOL_ERASER;
|
||||
temporaryEraser = true;
|
||||
}
|
||||
else if(controls.selectedTool == TOOL_ERASER && !world.main.input.pen.isEraser && temporaryEraser) {
|
||||
controls.selectedTool = TOOL_BRUSH;
|
||||
temporaryEraser = false;
|
||||
}
|
||||
|
||||
if(controls.selectedTool != controls.previousSelected) {
|
||||
controls.previousSelected = controls.selectedTool;
|
||||
reset_tools();
|
||||
}
|
||||
|
||||
controls.cursorHoveringOverCanvas = !world.main.toolbar.io->hoverObstructed;
|
||||
controls.leftClick = controls.cursorHoveringOverCanvas && world.main.input.mouse.leftClicks;
|
||||
if(controls.leftClick) {
|
||||
controls.leftClickHeld = true;
|
||||
world.main.toolbar.paintPopupLocation = std::nullopt;
|
||||
}
|
||||
if(controls.leftClickHeld && !world.main.input.mouse.leftDown) {
|
||||
controls.leftClick = false;
|
||||
controls.leftClickHeld = false;
|
||||
controls.leftClickReleased = true;
|
||||
}
|
||||
|
||||
if(controls.cursorHoveringOverCanvas && (world.main.input.mouse.rightClicks || world.main.input.pen.buttons[world.main.toolbar.tabletOptions.rightClickButton].pressed)) {
|
||||
if(world.main.toolbar.paintPopupLocation)
|
||||
world.main.toolbar.paintPopupLocation = std::nullopt;
|
||||
else
|
||||
world.main.toolbar.paintPopupLocation = world.main.input.mouse.pos / world.main.window.scale;
|
||||
}
|
||||
|
||||
controls.previousMouseWorldPos = controls.currentMouseWorldPos;
|
||||
controls.currentMouseWorldPos = world.get_mouse_world_pos();
|
||||
|
||||
drag_drop_update();
|
||||
|
||||
if(world.main.input.key(InputManager::KEY_DRAW_TOOL_BRUSH).pressed)
|
||||
controls.selectedTool = TOOL_BRUSH;
|
||||
else if(world.main.input.key(InputManager::KEY_DRAW_TOOL_ERASER).pressed)
|
||||
controls.selectedTool = TOOL_ERASER;
|
||||
else if(world.main.input.key(InputManager::KEY_DRAW_TOOL_RECTSELECT).pressed)
|
||||
controls.selectedTool = TOOL_RECTSELECT;
|
||||
else if(world.main.input.key(InputManager::KEY_DRAW_TOOL_RECTANGLE).pressed)
|
||||
controls.selectedTool = TOOL_RECTANGLE;
|
||||
else if(world.main.input.key(InputManager::KEY_DRAW_TOOL_ELLIPSE).pressed)
|
||||
controls.selectedTool = TOOL_ELLIPSE;
|
||||
else if(world.main.input.key(InputManager::KEY_DRAW_TOOL_TEXTBOX).pressed)
|
||||
controls.selectedTool = TOOL_TEXTBOX;
|
||||
else if(world.main.input.key(InputManager::KEY_DRAW_TOOL_INKDROPPER).pressed)
|
||||
controls.selectedTool = TOOL_INKDROPPER;
|
||||
else if(world.main.input.key(InputManager::KEY_DRAW_TOOL_SCREENSHOT).pressed)
|
||||
controls.selectedTool = TOOL_SCREENSHOT;
|
||||
else if(world.main.input.key(InputManager::KEY_DRAW_TOOL_EDIT).pressed)
|
||||
controls.selectedTool = TOOL_EDIT;
|
||||
else if(world.main.input.key(InputManager::KEY_PASTE).pressed)
|
||||
rectSelectTool.paste_clipboard();
|
||||
|
||||
switch(controls.selectedTool) {
|
||||
case TOOL_BRUSH:
|
||||
brushTool.tool_update();
|
||||
break;
|
||||
case TOOL_ERASER:
|
||||
eraserTool.tool_update();
|
||||
break;
|
||||
case TOOL_RECTSELECT:
|
||||
rectSelectTool.tool_update();
|
||||
break;
|
||||
case TOOL_RECTANGLE:
|
||||
rectDrawTool.tool_update();
|
||||
break;
|
||||
case TOOL_ELLIPSE:
|
||||
ellipseDrawTool.tool_update();
|
||||
break;
|
||||
case TOOL_TEXTBOX:
|
||||
textBoxTool.tool_update();
|
||||
break;
|
||||
case TOOL_INKDROPPER:
|
||||
inkDropperTool.tool_update();
|
||||
break;
|
||||
case TOOL_SCREENSHOT:
|
||||
screenshotTool.tool_update();
|
||||
break;
|
||||
case TOOL_EDIT:
|
||||
editTool.tool_update();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
};
|
||||
|
||||
if(controls.leftClickReleased)
|
||||
controls.leftClickReleased = false;
|
||||
|
||||
for(auto& c : components.client_list()) {
|
||||
c->obj->check_timers(*this);
|
||||
c->obj->update(*this);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawingProgram::drag_drop_update() {
|
||||
if(controls.cursorHoveringOverCanvas) {
|
||||
for(auto& droppedItem : world.main.input.droppedItems) {
|
||||
if(droppedItem.isFile && std::filesystem::is_regular_file(droppedItem.droppedData)) {
|
||||
ServerClientID imageID = world.rMan.add_resource_file(droppedItem.droppedData);
|
||||
std::shared_ptr<ResourceDisplay> display = world.rMan.get_display_data(imageID);
|
||||
Vector2f imTrueDim = display->get_dimensions();
|
||||
auto img(std::make_shared<DrawImage>());
|
||||
img->coords = world.drawData.cam.c;
|
||||
float imWidth = imTrueDim.x() / (imTrueDim.x() + imTrueDim.y());
|
||||
float imHeight = imTrueDim.y() / (imTrueDim.x() + imTrueDim.y());
|
||||
Vector2f imDim = Vector2f{world.main.window.size.x() * imWidth, world.main.window.size.x() * imHeight} * display->get_dimension_scale();
|
||||
Vector2f dropPos = world.main.input.mouse.pos;
|
||||
// dropPos = droppedItem.pos; // Doesnt work properly with emscripten
|
||||
img->d.p1 = dropPos - imDim;
|
||||
img->d.p2 = dropPos + imDim;
|
||||
img->d.imageID = imageID;
|
||||
img->initialize_draw_data(*this);
|
||||
img->finalize_update(colliderAllocated);
|
||||
uint64_t placement = components.client_list().size();
|
||||
components.client_insert(placement, img);
|
||||
img->client_send_place(*this, placement);
|
||||
add_undo_place_component(placement, img);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawingProgram::reset_tools() {
|
||||
rectSelectTool.reset_tool();
|
||||
textBoxTool.reset_tool();
|
||||
brushTool.reset_tool();
|
||||
rectDrawTool.reset_tool();
|
||||
ellipseDrawTool.reset_tool();
|
||||
editTool.reset_tool();
|
||||
reset_selection();
|
||||
}
|
||||
|
||||
void DrawingProgram::initialize_draw_data(cereal::PortableBinaryInputArchive& a) {
|
||||
uint64_t compCount;
|
||||
a(compCount);
|
||||
for(uint64_t i = 0; i < compCount; i++) {
|
||||
DrawComponentType t;
|
||||
ServerClientID id;
|
||||
a(t, id);
|
||||
auto newComp = DrawComponent::allocate_comp_type(t);
|
||||
a(newComp->coords, *newComp);
|
||||
components.init_emplace_back(id, newComp);
|
||||
newComp->initialize_draw_data(*this);
|
||||
newComp->finalize_update(colliderAllocated);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawingProgram::reset_selection() {
|
||||
for(auto& c : components.client_list())
|
||||
c->obj->selected = false;
|
||||
}
|
||||
|
||||
void DrawingProgram::add_undo_place_component(uint64_t placement, const std::shared_ptr<DrawComponent>& comp) {
|
||||
world.undo.push(UndoManager::UndoRedoPair{
|
||||
[&, comp]() {
|
||||
ServerClientID compID;
|
||||
components.client_erase(comp, compID);
|
||||
if(compID == ServerClientID{0, 0})
|
||||
return false;
|
||||
DrawComponent::client_send_erase(*this, compID);
|
||||
reset_tools();
|
||||
return true;
|
||||
},
|
||||
[&, comp, placement]() {
|
||||
if(components.get_id(comp) != ServerClientID{0, 0})
|
||||
return false;
|
||||
components.client_insert(placement, comp);
|
||||
comp->initialize_draw_data(*this); // Method to recover after an undo that happened while drawing a component
|
||||
comp->finalize_update(colliderAllocated);
|
||||
comp->client_send_place(*this, placement);
|
||||
reset_tools();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DrawingProgram::add_undo_place_components(uint64_t placement, const std::vector<std::shared_ptr<DrawComponent>>& comps) {
|
||||
world.undo.push(UndoManager::UndoRedoPair{
|
||||
[&, comps]() {
|
||||
std::vector<ServerClientID> compIDs;
|
||||
for(auto& c : comps) {
|
||||
ServerClientID compID;
|
||||
components.client_erase(c, compID);
|
||||
compIDs.emplace_back(compID);
|
||||
}
|
||||
bool toRet = true;
|
||||
for(auto& compID : compIDs) {
|
||||
if(compID == ServerClientID{0, 0})
|
||||
toRet = false;
|
||||
else
|
||||
DrawComponent::client_send_erase(*this, compID);
|
||||
}
|
||||
reset_tools();
|
||||
return toRet;
|
||||
},
|
||||
[&, comps, placement]() {
|
||||
for(auto& c : comps)
|
||||
if(components.get_id(c) != ServerClientID{0, 0})
|
||||
return false;
|
||||
for(auto& c : comps | std::views::reverse) {
|
||||
components.client_insert(placement, c);
|
||||
c->initialize_draw_data(*this); // Method to recover after an undo that happened while drawing a component
|
||||
c->finalize_update(colliderAllocated);
|
||||
c->client_send_place(*this, placement);
|
||||
}
|
||||
reset_tools();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
ClientPortionID DrawingProgram::get_max_id(ServerPortionID serverID) {
|
||||
ClientPortionID maxClientID = 0;
|
||||
auto& cList = components.client_list();
|
||||
for(auto& comp : cList) {
|
||||
if(comp->id.first == serverID)
|
||||
maxClientID = std::max(maxClientID, comp->id.second);
|
||||
}
|
||||
return maxClientID;
|
||||
}
|
||||
|
||||
void DrawingProgram::draw_drag_circle(SkCanvas* canvas, const Vector2f& sPos, const SkColor4f& c, const DrawData& drawData, float radiusMultiplier) {
|
||||
float constantRadius = DRAG_POINT_RADIUS * radiusMultiplier;
|
||||
float constantThickness = constantRadius * 0.2f;
|
||||
canvas->drawCircle(sPos.x(), sPos.y(), constantRadius, SkPaint(c));
|
||||
SkPaint paintOutline(SkColor4f{0.95f, 0.95f, 0.95f, 1.0f});
|
||||
paintOutline.setStroke(true);
|
||||
paintOutline.setStrokeWidth(constantThickness);
|
||||
canvas->drawCircle(sPos.x(), sPos.y(), constantRadius, paintOutline);
|
||||
}
|
||||
|
||||
void DrawingProgram::write_to_file(cereal::PortableBinaryOutputArchive& a) {
|
||||
auto& cList = components.client_list();
|
||||
a((uint64_t)cList.size());
|
||||
for(size_t i = 0; i < cList.size(); i++) {
|
||||
a(cList[i]->obj->get_type(), cList[i]->id, cList[i]->obj->coords, *cList[i]->obj);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawingProgram::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
if(drawData.dontUseDrawProgCache) {
|
||||
for(auto& c : components.client_list()) {
|
||||
c->obj->calculate_draw_transform(drawData);
|
||||
c->obj->draw(canvas, drawData);
|
||||
}
|
||||
}
|
||||
else if(world.main.drawProgCache.refresh)
|
||||
draw_cache(canvas, drawData, nullptr);
|
||||
else {
|
||||
bool caughtFirstUpdated = false;
|
||||
for(auto& c : components.client_list()) {
|
||||
if(caughtFirstUpdated) {
|
||||
c->obj->calculate_draw_transform(drawData);
|
||||
c->obj->draw(canvas, drawData);
|
||||
}
|
||||
else if(c->obj == world.main.drawProgCache.firstCompUpdate) {
|
||||
canvas->drawImage(world.main.drawProgCache.surface->makeTemporaryImage(), 0, 0);
|
||||
c->obj->calculate_draw_transform(drawData);
|
||||
c->obj->draw(canvas, drawData);
|
||||
caughtFirstUpdated = true;
|
||||
}
|
||||
else if(c->obj->updateDraw) {
|
||||
draw_cache(canvas, drawData, c->obj);
|
||||
c->obj->calculate_draw_transform(drawData);
|
||||
c->obj->draw(canvas, drawData);
|
||||
caughtFirstUpdated = true;
|
||||
}
|
||||
c->obj->updateDraw = false;
|
||||
}
|
||||
if(!caughtFirstUpdated)
|
||||
canvas->drawImage(world.main.drawProgCache.surface->makeTemporaryImage(), 0, 0);
|
||||
}
|
||||
|
||||
switch(controls.selectedTool) {
|
||||
case TOOL_BRUSH:
|
||||
brushTool.draw(canvas, drawData);
|
||||
break;
|
||||
case TOOL_ERASER:
|
||||
eraserTool.draw(canvas, drawData);
|
||||
break;
|
||||
case TOOL_RECTSELECT:
|
||||
rectSelectTool.draw(canvas, drawData);
|
||||
break;
|
||||
case TOOL_RECTANGLE:
|
||||
rectDrawTool.draw(canvas, drawData);
|
||||
break;
|
||||
case TOOL_ELLIPSE:
|
||||
ellipseDrawTool.draw(canvas, drawData);
|
||||
break;
|
||||
case TOOL_TEXTBOX:
|
||||
textBoxTool.draw(canvas, drawData);
|
||||
break;
|
||||
case TOOL_INKDROPPER:
|
||||
inkDropperTool.draw(canvas, drawData);
|
||||
break;
|
||||
case TOOL_SCREENSHOT:
|
||||
screenshotTool.draw(canvas, drawData);
|
||||
break;
|
||||
case TOOL_EDIT:
|
||||
editTool.draw(canvas, drawData);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Vector4f* DrawingProgram::get_foreground_color_ptr() {
|
||||
return &controls.foregroundColor;
|
||||
}
|
||||
|
||||
void DrawingProgram::draw_cache(SkCanvas* canvas, const DrawData& drawData, const std::shared_ptr<DrawComponent>& lastComp) {
|
||||
SkCanvas* cacheCanvas = world.main.drawProgCache.canvas;
|
||||
cacheCanvas->clear(SkColor4f{0, 0, 0, 0});
|
||||
#ifdef __EMSCRIPTEN__
|
||||
std::for_each(components.client_list().begin(), components.client_list().end(), [&](auto& c) {
|
||||
c->obj->calculate_draw_transform(drawData);
|
||||
});
|
||||
#else
|
||||
std::for_each(std::execution::par_unseq, components.client_list().begin(), components.client_list().end(), [&](auto& c) {
|
||||
c->obj->calculate_draw_transform(drawData);
|
||||
});
|
||||
#endif
|
||||
for(auto& c : components.client_list()) {
|
||||
if(c->obj == lastComp)
|
||||
break;
|
||||
c->obj->draw(cacheCanvas, drawData);
|
||||
}
|
||||
canvas->drawImage(world.main.drawProgCache.surface->makeTemporaryImage(), 0, 0);
|
||||
world.main.drawProgCache.firstCompUpdate = lastComp;
|
||||
world.main.drawProgCache.refresh = false;
|
||||
}
|
||||
121
src/DrawingProgram/DrawingProgram.hpp
Normal file
121
src/DrawingProgram/DrawingProgram.hpp
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
#pragma once
|
||||
#include "../DrawData.hpp"
|
||||
#include "cereal/archives/portable_binary.hpp"
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include <include/core/SkPath.h>
|
||||
#include <include/core/SkVertices.h>
|
||||
#include <Helpers/SCollision.hpp>
|
||||
#include <Helpers/Hashes.hpp>
|
||||
#include <list>
|
||||
#include <Helpers/Random.hpp>
|
||||
#include "BrushTool.hpp"
|
||||
#include "EraserTool.hpp"
|
||||
#include "RectSelectTool.hpp"
|
||||
#include "RectDrawTool.hpp"
|
||||
#include "EllipseDrawTool.hpp"
|
||||
#include "TextBoxTool.hpp"
|
||||
#include "InkDropperTool.hpp"
|
||||
#include "ScreenshotTool.hpp"
|
||||
#include "EditTool.hpp"
|
||||
#include "ImageTool.hpp"
|
||||
#include "../CollabList.hpp"
|
||||
|
||||
class World;
|
||||
|
||||
#define DRAG_POINT_RADIUS 10.0f
|
||||
|
||||
class DrawingProgram {
|
||||
public:
|
||||
DrawingProgram(World& initWorld);
|
||||
void init_client_callbacks();
|
||||
void toolbar_gui();
|
||||
void tool_options_gui();
|
||||
void update();
|
||||
void draw(SkCanvas* canvas, const DrawData& drawData);
|
||||
void initialize_draw_data(cereal::PortableBinaryInputArchive& a);
|
||||
std::unordered_set<ServerClientID> get_used_resources();
|
||||
ClientPortionID get_max_id(ServerPortionID serverID);
|
||||
void write_to_file(cereal::PortableBinaryOutputArchive& a);
|
||||
World& world;
|
||||
|
||||
void free_collider_memory();
|
||||
void allocate_collider_memory();
|
||||
|
||||
bool colliderAllocated = false;
|
||||
|
||||
CollabList<std::shared_ptr<DrawComponent>, ServerClientID> components;
|
||||
|
||||
Vector4f* get_foreground_color_ptr();
|
||||
private:
|
||||
void draw_cache(SkCanvas* canvas, const DrawData& drawData, const std::shared_ptr<DrawComponent>& lastComp);
|
||||
|
||||
void check_all_collisions_base(const SCollision::ColliderCollection<WorldScalar>& checkAgainstWorld, const SCollision::ColliderCollection<float>& checkAgainstCam);
|
||||
void check_all_collisions(const SCollision::ColliderCollection<WorldScalar>& checkAgainst);
|
||||
void check_all_collisions_transform(const SCollision::ColliderCollection<float>& checkAgainst);
|
||||
|
||||
void add_variable_width_line(SkPath& p, const DrawData& drawData, const WorldVec& p1, const WorldVec& p2, float w1, float w2);
|
||||
void reset_selection();
|
||||
void reset_tools();
|
||||
|
||||
void drag_drop_update();
|
||||
|
||||
void draw_drag_circle(SkCanvas* canvas, const Vector2f& pos, const SkColor4f& c, const DrawData& drawData, float radiusMultiplier = 1.0f);
|
||||
|
||||
void add_undo_place_component(uint64_t placement, const std::shared_ptr<DrawComponent>& comp);
|
||||
void add_undo_place_components(uint64_t placement, const std::vector<std::shared_ptr<DrawComponent>>& comps);
|
||||
|
||||
BrushTool brushTool;
|
||||
EraserTool eraserTool;
|
||||
RectSelectTool rectSelectTool;
|
||||
RectDrawTool rectDrawTool;
|
||||
EllipseDrawTool ellipseDrawTool;
|
||||
TextBoxTool textBoxTool;
|
||||
InkDropperTool inkDropperTool;
|
||||
ScreenshotTool screenshotTool;
|
||||
EditTool editTool;
|
||||
ImageTool imageTool;
|
||||
|
||||
bool temporaryEraser = false;
|
||||
|
||||
enum Tool : int {
|
||||
TOOL_BRUSH = 0,
|
||||
TOOL_ERASER,
|
||||
TOOL_RECTSELECT,
|
||||
TOOL_RECTANGLE,
|
||||
TOOL_ELLIPSE,
|
||||
TOOL_TEXTBOX,
|
||||
TOOL_INKDROPPER,
|
||||
TOOL_SCREENSHOT,
|
||||
TOOL_EDIT
|
||||
};
|
||||
|
||||
struct GlobalControls {
|
||||
float relativeWidth = 15.0f;
|
||||
|
||||
Vector4f foregroundColor{0.9f, 0.9f, 0.9f, 1.0f};
|
||||
Vector4f backgroundColor{0.0f, 0.0f, 0.0f, 1.0f};
|
||||
int previousSelected = 0;
|
||||
WorldVec previousMouseWorldPos = {0, 0};
|
||||
WorldVec currentMouseWorldPos = {0, 0};
|
||||
int selectedTool = 0;
|
||||
bool leftClick = false;
|
||||
bool leftClickHeld = false;
|
||||
bool leftClickReleased = false;
|
||||
bool cursorHoveringOverCanvas = false;
|
||||
|
||||
int colorEditing = 0;
|
||||
} controls;
|
||||
|
||||
uint32_t nextID = 0;
|
||||
|
||||
friend class InkDropperTool;
|
||||
friend class RectDrawTool;
|
||||
friend class EllipseDrawTool;
|
||||
friend class BrushTool;
|
||||
friend class EraserTool;
|
||||
friend class RectSelectTool;
|
||||
friend class TextBoxTool;
|
||||
friend class ScreenshotTool;
|
||||
friend class EditTool;
|
||||
friend class ImageTool;
|
||||
};
|
||||
218
src/DrawingProgram/EditTool.cpp
Normal file
218
src/DrawingProgram/EditTool.cpp
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
#include "EditTool.hpp"
|
||||
#include <chrono>
|
||||
#include "DrawingProgram.hpp"
|
||||
#include "../Server/CommandList.hpp"
|
||||
#include "../MainProgram.hpp"
|
||||
#include "../DrawData.hpp"
|
||||
#include "Helpers/MathExtras.hpp"
|
||||
#include "Helpers/SCollision.hpp"
|
||||
#include "Helpers/Serializers.hpp"
|
||||
#include "../DrawComponents/DrawRectangle.hpp"
|
||||
#include "../DrawComponents/DrawTextBox.hpp"
|
||||
#include "../InputManager.hpp"
|
||||
#include "../SharedTypes.hpp"
|
||||
#include <cereal/types/vector.hpp>
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
|
||||
EditTool::EditTool(DrawingProgram& initDrawP):
|
||||
drawP(initDrawP)
|
||||
{
|
||||
}
|
||||
|
||||
void EditTool::gui_toolbox() {
|
||||
Toolbar& t = drawP.world.main.toolbar;
|
||||
if(controls.isEditing) {
|
||||
std::shared_ptr<DrawComponent> a = controls.compToEdit.lock();
|
||||
bool editHappened = false;
|
||||
if(a) {
|
||||
switch(a->get_type()) {
|
||||
case DRAWCOMPONENT_TEXTBOX: {
|
||||
editHappened = drawP.textBoxTool.edit_gui(std::static_pointer_cast<DrawTextBox>(a));
|
||||
break;
|
||||
}
|
||||
case DRAWCOMPONENT_ELLIPSE: {
|
||||
editHappened = drawP.ellipseDrawTool.edit_gui(std::static_pointer_cast<DrawEllipse>(a));
|
||||
break;
|
||||
}
|
||||
case DRAWCOMPONENT_RECTANGLE: {
|
||||
editHappened = drawP.rectDrawTool.edit_gui(std::static_pointer_cast<DrawRectangle>(a));
|
||||
break;
|
||||
}
|
||||
case DRAWCOMPONENT_IMAGE: {
|
||||
editHappened = drawP.imageTool.edit_gui(std::static_pointer_cast<DrawImage>(a));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(editHappened) {
|
||||
a->temp_update(drawP);
|
||||
a->client_send_update_temp(drawP, drawP.components.get_id(a));
|
||||
}
|
||||
}
|
||||
else {
|
||||
t.gui.push_id("edit tool");
|
||||
t.gui.text_label_centered("Edit");
|
||||
t.gui.pop_id();
|
||||
}
|
||||
}
|
||||
|
||||
void EditTool::add_point_handle(const HandleData& handle) {
|
||||
controls.pointHandles.emplace_back(handle);
|
||||
}
|
||||
|
||||
void EditTool::reset_tool() {
|
||||
auto a = controls.compToEdit.lock();
|
||||
if(a) {
|
||||
switch(a->get_type()) {
|
||||
case DRAWCOMPONENT_TEXTBOX:
|
||||
drawP.textBoxTool.commit_edit_updates(std::static_pointer_cast<DrawTextBox>(a), controls.prevData);
|
||||
break;
|
||||
case DRAWCOMPONENT_ELLIPSE:
|
||||
drawP.ellipseDrawTool.commit_edit_updates(std::static_pointer_cast<DrawEllipse>(a), controls.prevData);
|
||||
break;
|
||||
case DRAWCOMPONENT_RECTANGLE:
|
||||
drawP.rectDrawTool.commit_edit_updates(std::static_pointer_cast<DrawRectangle>(a), controls.prevData);
|
||||
break;
|
||||
case DRAWCOMPONENT_IMAGE:
|
||||
drawP.imageTool.commit_edit_updates(std::static_pointer_cast<DrawImage>(a), controls.prevData);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
a->final_update(drawP);
|
||||
a->client_send_update_final(drawP, drawP.components.get_id(a));
|
||||
}
|
||||
controls.compToEdit.reset();
|
||||
controls.pointHandles.clear();
|
||||
controls.pointDragging = nullptr;
|
||||
controls.isEditing = false;
|
||||
}
|
||||
|
||||
void EditTool::edit_start(const std::shared_ptr<DrawComponent>& comp) {
|
||||
controls.isEditing = true;
|
||||
switch(comp->get_type()) {
|
||||
case DRAWCOMPONENT_TEXTBOX: {
|
||||
drawP.textBoxTool.edit_start(std::static_pointer_cast<DrawTextBox>(comp), controls.prevData);
|
||||
break;
|
||||
}
|
||||
case DRAWCOMPONENT_ELLIPSE: {
|
||||
drawP.ellipseDrawTool.edit_start(std::static_pointer_cast<DrawEllipse>(comp), controls.prevData);
|
||||
break;
|
||||
}
|
||||
case DRAWCOMPONENT_RECTANGLE: {
|
||||
drawP.rectDrawTool.edit_start(std::static_pointer_cast<DrawRectangle>(comp), controls.prevData);
|
||||
break;
|
||||
}
|
||||
case DRAWCOMPONENT_IMAGE: {
|
||||
drawP.imageTool.edit_start(std::static_pointer_cast<DrawImage>(comp), controls.prevData);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
controls.isEditing = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(controls.isEditing) {
|
||||
controls.compToEdit = comp;
|
||||
comp->lastUpdateTime = std::chrono::steady_clock::now();
|
||||
}
|
||||
}
|
||||
|
||||
void EditTool::tool_update() {
|
||||
using namespace SCollision;
|
||||
ColliderCollection<float> ci;
|
||||
ci.circle.emplace_back(Circle<float>{drawP.world.main.input.mouse.pos, 1.0f});
|
||||
ci.recalculate_bounds();
|
||||
|
||||
if(!controls.isEditing) {
|
||||
if(drawP.controls.leftClick) {
|
||||
drawP.check_all_collisions_transform(ci);
|
||||
for(auto& comp : drawP.components.client_list() | std::views::reverse) {
|
||||
if(comp->obj->globalCollisionCheck) {
|
||||
edit_start(comp->obj);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
std::shared_ptr<DrawComponent> a = controls.compToEdit.lock();
|
||||
a->updateDraw = true;
|
||||
if(a) {
|
||||
bool isMovingPoint = false;
|
||||
bool clickedAway = false;
|
||||
if(!controls.pointDragging && drawP.controls.leftClick) {
|
||||
for(HandleData& h : controls.pointHandles) {
|
||||
if(SCollision::collide(ci, Circle<float>(drawP.world.drawData.cam.c.to_space(a->coords.from_space(*h.p)), DRAG_POINT_RADIUS))) {
|
||||
controls.pointDragging = &h;
|
||||
isMovingPoint = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!isMovingPoint && !a->collides_with_cam_coords(drawP.world.drawData.cam.c, ci, drawP.colliderAllocated))
|
||||
clickedAway = true;
|
||||
}
|
||||
else if(controls.pointDragging) {
|
||||
if(drawP.controls.leftClickHeld) {
|
||||
Vector2f newPos = a->coords.get_mouse_pos(drawP.world);
|
||||
if(newPos != *controls.pointDragging->p) {
|
||||
if(controls.pointDragging->min)
|
||||
newPos = cwise_vec_max(*controls.pointDragging->min, newPos);
|
||||
if(controls.pointDragging->max)
|
||||
newPos = cwise_vec_min(*controls.pointDragging->max, newPos);
|
||||
*controls.pointDragging->p = newPos;
|
||||
a->temp_update(drawP);
|
||||
a->client_send_update_temp(drawP, drawP.components.get_id(a));
|
||||
}
|
||||
isMovingPoint = true;
|
||||
}
|
||||
else
|
||||
controls.pointDragging = nullptr;
|
||||
}
|
||||
|
||||
a->lastUpdateTime = std::chrono::steady_clock::now();
|
||||
|
||||
bool shouldNotReset;
|
||||
switch(a->get_type()) {
|
||||
case DRAWCOMPONENT_TEXTBOX: {
|
||||
shouldNotReset = drawP.textBoxTool.edit_update(std::static_pointer_cast<DrawTextBox>(a));
|
||||
break;
|
||||
}
|
||||
case DRAWCOMPONENT_ELLIPSE: {
|
||||
shouldNotReset = drawP.ellipseDrawTool.edit_update(std::static_pointer_cast<DrawEllipse>(a));
|
||||
break;
|
||||
}
|
||||
case DRAWCOMPONENT_RECTANGLE: {
|
||||
shouldNotReset = drawP.rectDrawTool.edit_update(std::static_pointer_cast<DrawRectangle>(a));
|
||||
break;
|
||||
}
|
||||
case DRAWCOMPONENT_IMAGE: {
|
||||
shouldNotReset = drawP.imageTool.edit_update(std::static_pointer_cast<DrawImage>(a));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
shouldNotReset = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!shouldNotReset || clickedAway)
|
||||
reset_tool();
|
||||
}
|
||||
else
|
||||
reset_tool();
|
||||
}
|
||||
}
|
||||
|
||||
void EditTool::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
if(controls.isEditing) {
|
||||
std::shared_ptr<DrawComponent> a = controls.compToEdit.lock();
|
||||
if(a) {
|
||||
for(HandleData& h : controls.pointHandles)
|
||||
drawP.draw_drag_circle(canvas, drawData.cam.c.to_space((a->coords.from_space(*h.p))), {0.1f, 0.9f, 0.9f, 1.0f}, drawData);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/DrawingProgram/EditTool.hpp
Normal file
36
src/DrawingProgram/EditTool.hpp
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include "../DrawData.hpp"
|
||||
#include "../DrawComponents/DrawEllipse.hpp"
|
||||
#include <Helpers/SCollision.hpp>
|
||||
#include <any>
|
||||
|
||||
class DrawingProgram;
|
||||
|
||||
class EditTool {
|
||||
public:
|
||||
struct HandleData {
|
||||
Vector2f* p;
|
||||
Vector2f* min;
|
||||
Vector2f* max;
|
||||
};
|
||||
|
||||
EditTool(DrawingProgram& initDrawP);
|
||||
void gui_toolbox();
|
||||
void tool_update();
|
||||
void reset_tool();
|
||||
void draw(SkCanvas* canvas, const DrawData& drawData);
|
||||
void add_point_handle(const HandleData& handle);
|
||||
void edit_start(const std::shared_ptr<DrawComponent>& comp);
|
||||
|
||||
struct EditControls {
|
||||
std::vector<HandleData> pointHandles;
|
||||
std::weak_ptr<DrawComponent> compToEdit;
|
||||
bool isEditing = false;
|
||||
HandleData* pointDragging = nullptr;
|
||||
std::any prevData;
|
||||
} controls;
|
||||
private:
|
||||
|
||||
DrawingProgram& drawP;
|
||||
};
|
||||
156
src/DrawingProgram/EllipseDrawTool.cpp
Normal file
156
src/DrawingProgram/EllipseDrawTool.cpp
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
#include "EllipseDrawTool.hpp"
|
||||
#include <chrono>
|
||||
#include "DrawingProgram.hpp"
|
||||
#include "../Server/CommandList.hpp"
|
||||
#include "../MainProgram.hpp"
|
||||
#include "../DrawData.hpp"
|
||||
#include "Helpers/SCollision.hpp"
|
||||
#include "Helpers/Serializers.hpp"
|
||||
#include "../InputManager.hpp"
|
||||
#include "../SharedTypes.hpp"
|
||||
#include <cereal/types/vector.hpp>
|
||||
|
||||
EllipseDrawTool::EllipseDrawTool(DrawingProgram& initDrawP):
|
||||
drawP(initDrawP)
|
||||
{
|
||||
controls.intermediateItem = std::make_shared<DrawEllipse>();
|
||||
}
|
||||
|
||||
void EllipseDrawTool::gui_toolbox() {
|
||||
Toolbar& t = drawP.world.main.toolbar;
|
||||
t.gui.push_id("ellipse draw tool");
|
||||
t.gui.text_label_centered("Draw Ellipse");
|
||||
if(t.gui.radio_button_field("fillonly", "Fill only", controls.fillStrokeMode == 0)) controls.fillStrokeMode = 0;
|
||||
if(t.gui.radio_button_field("outlineonly", "Outline only", controls.fillStrokeMode == 1)) controls.fillStrokeMode = 1;
|
||||
if(t.gui.radio_button_field("filloutline", "Fill and Outline", controls.fillStrokeMode == 2)) controls.fillStrokeMode = 2;
|
||||
if(controls.fillStrokeMode == 1 || controls.fillStrokeMode == 2)
|
||||
t.gui.slider_scalar_field("relstrokewidth", "Outline Size", &drawP.controls.relativeWidth, 3.0f, 40.0f);
|
||||
t.gui.pop_id();
|
||||
}
|
||||
|
||||
bool EllipseDrawTool::edit_gui(const std::shared_ptr<DrawEllipse>& a) {
|
||||
DrawEllipse::Data oldData = a->d;
|
||||
Toolbar& t = drawP.world.main.toolbar;
|
||||
t.gui.push_id("edit tool ellipse");
|
||||
t.gui.text_label_centered("Edit Ellipse");
|
||||
if(t.gui.radio_button_field("fillonly", "Fill only", a->d.fillStrokeMode == 0)) a->d.fillStrokeMode = 0;
|
||||
if(t.gui.radio_button_field("outlineonly", "Outline only", a->d.fillStrokeMode == 1)) a->d.fillStrokeMode = 1;
|
||||
if(t.gui.radio_button_field("filloutline", "Fill and Outline", a->d.fillStrokeMode == 2)) a->d.fillStrokeMode = 2;
|
||||
if(a->d.fillStrokeMode == 0 || a->d.fillStrokeMode == 2) {
|
||||
t.gui.left_to_right_line_layout([&]() {
|
||||
CLAY({.layout = {.sizing = {.width = CLAY_SIZING_FIXED(40), .height = CLAY_SIZING_FIXED(40)}}}) {
|
||||
if(t.gui.color_button("Fill Color", &a->d.fillColor, &a->d.fillColor == t.colorRight))
|
||||
t.color_selector_right(&a->d.fillColor == t.colorRight ? nullptr : &a->d.fillColor);
|
||||
}
|
||||
t.gui.text_label("Fill Color");
|
||||
});
|
||||
}
|
||||
if(a->d.fillStrokeMode == 1 || a->d.fillStrokeMode == 2) {
|
||||
t.gui.slider_scalar_field("relstrokewidth", "Outline Size", &a->d.strokeWidth, 3.0f, 40.0f);
|
||||
t.gui.left_to_right_line_layout([&]() {
|
||||
CLAY({.layout = {.sizing = {.width = CLAY_SIZING_FIXED(40), .height = CLAY_SIZING_FIXED(40)}}}) {
|
||||
if(t.gui.color_button("Outline Color", &a->d.strokeColor, &a->d.strokeColor == t.colorRight))
|
||||
t.color_selector_right(&a->d.strokeColor == t.colorRight ? nullptr : &a->d.strokeColor);
|
||||
}
|
||||
t.gui.text_label("Outline Color");
|
||||
});
|
||||
}
|
||||
t.gui.pop_id();
|
||||
return a->d != oldData;
|
||||
}
|
||||
|
||||
void EllipseDrawTool::edit_start(const std::shared_ptr<DrawEllipse>& a, std::any& prevData) {
|
||||
prevData = a->d;
|
||||
drawP.editTool.add_point_handle({&a->d.p1, nullptr, &a->d.p2});
|
||||
drawP.editTool.add_point_handle({&a->d.p2, &a->d.p1, nullptr});
|
||||
}
|
||||
|
||||
void EllipseDrawTool::commit_edit_updates(const std::shared_ptr<DrawEllipse>& a, std::any& prevData) {
|
||||
DrawEllipse::Data pData = std::any_cast<DrawEllipse::Data>(prevData);
|
||||
DrawEllipse::Data cData = a->d;
|
||||
drawP.world.undo.push(UndoManager::UndoRedoPair{
|
||||
[&, a, pData]() {
|
||||
a->d = pData;
|
||||
ServerClientID compID = drawP.components.get_id(a);
|
||||
if(compID != ServerClientID{0, 0})
|
||||
a->client_send_update_final(drawP, compID);
|
||||
a->final_update(drawP);
|
||||
drawP.reset_tools();
|
||||
return true;
|
||||
},
|
||||
[&, a, cData]() {
|
||||
ServerClientID compID = drawP.components.get_id(a);
|
||||
a->d = cData;
|
||||
if(compID != ServerClientID{0, 0})
|
||||
a->client_send_update_final(drawP, compID);
|
||||
a->final_update(drawP);
|
||||
drawP.reset_tools();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void EllipseDrawTool::reset_tool() {
|
||||
commit();
|
||||
controls.drawStage = 0;
|
||||
}
|
||||
|
||||
bool EllipseDrawTool::edit_update(const std::shared_ptr<DrawEllipse>& a) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void EllipseDrawTool::tool_update() {
|
||||
switch(controls.drawStage) {
|
||||
case 0: {
|
||||
if(drawP.controls.leftClick) {
|
||||
controls.startAt = drawP.world.main.input.mouse.pos;
|
||||
controls.intermediateItem = std::make_shared<DrawEllipse>();
|
||||
controls.intermediateItem->d.strokeColor = drawP.controls.foregroundColor;
|
||||
controls.intermediateItem->d.fillColor = drawP.controls.backgroundColor;
|
||||
controls.intermediateItem->d.strokeWidth = drawP.controls.relativeWidth;
|
||||
controls.intermediateItem->d.p1 = controls.startAt;
|
||||
controls.intermediateItem->d.p2 = controls.startAt;
|
||||
controls.intermediateItem->d.fillStrokeMode = static_cast<uint8_t>(controls.fillStrokeMode);
|
||||
controls.intermediateItem->coords = drawP.world.drawData.cam.c;
|
||||
controls.intermediateItem->lastUpdateTime = std::chrono::steady_clock::now();
|
||||
uint64_t placement = drawP.components.client_list().size();
|
||||
drawP.components.client_insert(placement, controls.intermediateItem);
|
||||
controls.intermediateItem->client_send_place(drawP, placement);
|
||||
drawP.add_undo_place_component(placement, controls.intermediateItem);
|
||||
controls.intermediateItem->temp_update(drawP);
|
||||
controls.drawStage = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
if(drawP.controls.leftClickHeld) {
|
||||
controls.intermediateItem->lastUpdateTime = std::chrono::steady_clock::now();
|
||||
Vector2f newPos = controls.intermediateItem->coords.get_mouse_pos(drawP.world);
|
||||
if(drawP.world.main.input.key(InputManager::KEY_EQUAL_DIMENSIONS).held) {
|
||||
float height = std::fabs(controls.startAt.y() - newPos.y());
|
||||
newPos.x() = controls.startAt.x() + (((newPos.x() - controls.startAt.x()) < 0.0f ? -1.0f : 1.0f) * height);
|
||||
}
|
||||
controls.intermediateItem->d.p1 = cwise_vec_min(controls.startAt, newPos);
|
||||
controls.intermediateItem->d.p2 = cwise_vec_max(controls.startAt, newPos);
|
||||
controls.intermediateItem->client_send_update_temp(drawP, drawP.components.get_id(controls.intermediateItem));
|
||||
controls.intermediateItem->temp_update(drawP);
|
||||
}
|
||||
else {
|
||||
commit();
|
||||
controls.drawStage = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EllipseDrawTool::commit() {
|
||||
if(controls.intermediateItem) {
|
||||
controls.intermediateItem->client_send_update_final(drawP, drawP.components.get_id(controls.intermediateItem));
|
||||
controls.intermediateItem->final_update(drawP);
|
||||
controls.intermediateItem = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void EllipseDrawTool::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
}
|
||||
33
src/DrawingProgram/EllipseDrawTool.hpp
Normal file
33
src/DrawingProgram/EllipseDrawTool.hpp
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include "../DrawData.hpp"
|
||||
#include "../DrawComponents/DrawEllipse.hpp"
|
||||
#include <any>
|
||||
#include <Helpers/SCollision.hpp>
|
||||
|
||||
class DrawingProgram;
|
||||
|
||||
class EllipseDrawTool {
|
||||
public:
|
||||
EllipseDrawTool(DrawingProgram& initDrawP);
|
||||
void gui_toolbox();
|
||||
void tool_update();
|
||||
void reset_tool();
|
||||
void draw(SkCanvas* canvas, const DrawData& drawData);
|
||||
|
||||
void edit_start(const std::shared_ptr<DrawEllipse>& a, std::any& prevData);
|
||||
void commit_edit_updates(const std::shared_ptr<DrawEllipse>& a, std::any& prevData);
|
||||
bool edit_update(const std::shared_ptr<DrawEllipse>& a);
|
||||
bool edit_gui(const std::shared_ptr<DrawEllipse>& a);
|
||||
private:
|
||||
void commit();
|
||||
|
||||
struct EllipseDrawControls {
|
||||
Vector2f startAt;
|
||||
int fillStrokeMode = 1;
|
||||
int drawStage = 0;
|
||||
std::shared_ptr<DrawEllipse> intermediateItem;
|
||||
} controls;
|
||||
|
||||
DrawingProgram& drawP;
|
||||
};
|
||||
89
src/DrawingProgram/EraserTool.cpp
Normal file
89
src/DrawingProgram/EraserTool.cpp
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#include "EraserTool.hpp"
|
||||
#include "DrawingProgram.hpp"
|
||||
#include "../Server/CommandList.hpp"
|
||||
#include "../MainProgram.hpp"
|
||||
#include "../DrawData.hpp"
|
||||
#include "../SharedTypes.hpp"
|
||||
#include <ranges>
|
||||
#include "../DrawCollision.hpp"
|
||||
|
||||
EraserTool::EraserTool(DrawingProgram& initDrawP):
|
||||
drawP(initDrawP)
|
||||
{
|
||||
}
|
||||
|
||||
void EraserTool::gui_toolbox() {
|
||||
Toolbar& t = drawP.world.main.toolbar;
|
||||
t.gui.push_id("eraser tool");
|
||||
t.gui.text_label_centered("Eraser");
|
||||
t.gui.slider_scalar_field("relwidth", "Size", &drawP.controls.relativeWidth, 3.0f, 40.0f);
|
||||
t.gui.pop_id();
|
||||
}
|
||||
|
||||
void EraserTool::tool_update() {
|
||||
if(drawP.controls.cursorHoveringOverCanvas)
|
||||
drawP.world.main.input.hideCursor = true;
|
||||
|
||||
if(drawP.controls.leftClickHeld) {
|
||||
const Vector2f& prevMousePos = drawP.controls.leftClick ? drawP.world.main.input.mouse.pos : drawP.world.main.input.mouse.lastPos;
|
||||
SCollision::ColliderCollection<float> cC;
|
||||
SCollision::generate_wide_line(cC, prevMousePos, drawP.world.main.input.mouse.pos, drawP.controls.relativeWidth * 2.0f, true);
|
||||
drawP.check_all_collisions_transform(cC);
|
||||
drawP.components.client_erase_if([&](uint64_t oldPlacement, const auto& c) {
|
||||
if(c->obj->globalCollisionCheck) {
|
||||
erasedComponents.emplace_back(oldPlacement, c->obj);
|
||||
DrawComponent::client_send_erase(drawP, c->id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
else if(!erasedComponents.empty()) {
|
||||
auto erasedCompVal = erasedComponents;
|
||||
drawP.world.undo.push(UndoManager::UndoRedoPair{
|
||||
[&, erasedCompVal]() {
|
||||
for(auto& comp : erasedCompVal)
|
||||
if(drawP.components.get_id(comp.second) != ServerClientID{0, 0})
|
||||
return false;
|
||||
|
||||
for(auto& comp : erasedCompVal | std::views::reverse) {
|
||||
drawP.components.client_insert(comp.first, comp.second);
|
||||
comp.second->client_send_place(drawP, comp.first);
|
||||
}
|
||||
drawP.reset_tools();
|
||||
return true;
|
||||
},
|
||||
[&, erasedCompVal]() {
|
||||
for(auto& comp : erasedCompVal)
|
||||
if(drawP.components.get_id(comp.second) == ServerClientID{0, 0})
|
||||
return false;
|
||||
|
||||
for(auto& comp : erasedCompVal) {
|
||||
ServerClientID compID;
|
||||
drawP.components.client_erase(comp.second, compID);
|
||||
DrawComponent::client_send_erase(drawP, compID);
|
||||
}
|
||||
drawP.reset_tools();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
erasedComponents.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void EraserTool::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
if(!drawData.main->toolbar.io->hoverObstructed) {
|
||||
SkColor4f c = drawP.world.main.canvasTheme.toolFrontColor;
|
||||
c.fA = 0.5f;
|
||||
|
||||
SkPaint linePaint;
|
||||
linePaint.setColor4f(c);
|
||||
linePaint.setStyle(SkPaint::kStroke_Style);
|
||||
linePaint.setStrokeCap(SkPaint::kRound_Cap);
|
||||
linePaint.setStrokeWidth(drawP.controls.relativeWidth * 2.0f);
|
||||
SkPath erasePath;
|
||||
erasePath.moveTo(convert_vec2<SkPoint>(drawData.main->input.mouse.lastPos));
|
||||
erasePath.lineTo(convert_vec2<SkPoint>(drawData.main->input.mouse.pos));
|
||||
canvas->drawPath(erasePath, linePaint);
|
||||
}
|
||||
}
|
||||
19
src/DrawingProgram/EraserTool.hpp
Normal file
19
src/DrawingProgram/EraserTool.hpp
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include "../DrawData.hpp"
|
||||
#include "../DrawComponents/DrawComponent.hpp"
|
||||
|
||||
class DrawingProgram;
|
||||
|
||||
class EraserTool {
|
||||
public:
|
||||
EraserTool(DrawingProgram& initDrawP);
|
||||
void gui_toolbox();
|
||||
void tool_update();
|
||||
void draw(SkCanvas* canvas, const DrawData& drawData);
|
||||
private:
|
||||
|
||||
std::vector<std::pair<uint64_t, std::shared_ptr<DrawComponent>>> erasedComponents; // <placement order, object>
|
||||
|
||||
DrawingProgram& drawP;
|
||||
};
|
||||
54
src/DrawingProgram/ImageTool.cpp
Normal file
54
src/DrawingProgram/ImageTool.cpp
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#include "ImageTool.hpp"
|
||||
#include "DrawingProgram.hpp"
|
||||
#include "../World.hpp"
|
||||
#include "../Toolbar.hpp"
|
||||
#include "../MainProgram.hpp"
|
||||
#include "../ResourceManager.hpp"
|
||||
#include <fstream>
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <EmscriptenHelpers/emscripten_browser_file.h>
|
||||
#endif
|
||||
|
||||
ImageTool::ImageTool(DrawingProgram& initDrawP):
|
||||
drawP(initDrawP)
|
||||
{}
|
||||
|
||||
void ImageTool::edit_start(const std::shared_ptr<DrawImage>& a, std::any& prevData) {
|
||||
}
|
||||
|
||||
void ImageTool::commit_edit_updates(const std::shared_ptr<DrawImage>& a, std::any& prevData) {
|
||||
}
|
||||
|
||||
bool ImageTool::edit_update(const std::shared_ptr<DrawImage>& a) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImageTool::edit_gui(const std::shared_ptr<DrawImage>& a) {
|
||||
Toolbar& t = drawP.world.main.toolbar;
|
||||
t.gui.push_id("edit tool image");
|
||||
t.gui.text_label_centered("File Properties");
|
||||
std::optional<ResourceData> resourceData = drawP.world.rMan.get_resource(a->d.imageID);
|
||||
if(resourceData) {
|
||||
t.gui.text_label("Name: " + resourceData->name);
|
||||
if(t.gui.text_button_wide("file download", "Download file")) {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
emscripten_browser_file::download(
|
||||
resourceData->name,
|
||||
"application/octet-binary",
|
||||
*resourceData->data
|
||||
);
|
||||
#endif
|
||||
t.open_file_selector("Download File", {{"Any File", "*"}}, [resourceData](const std::filesystem::path& p, const auto& e) {
|
||||
std::ofstream f(p, std::ios::binary);
|
||||
if(f.is_open()) {
|
||||
f << *resourceData->data;
|
||||
f.close();
|
||||
}
|
||||
}, resourceData->name, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
t.gui.text_label_centered("Loading resource...");
|
||||
t.gui.pop_id();
|
||||
return false;
|
||||
}
|
||||
23
src/DrawingProgram/ImageTool.hpp
Normal file
23
src/DrawingProgram/ImageTool.hpp
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include "../DrawData.hpp"
|
||||
#include "../DrawComponents/DrawRectangle.hpp"
|
||||
#include "../DrawComponents/DrawImage.hpp"
|
||||
#include <Helpers/SCollision.hpp>
|
||||
#include <any>
|
||||
|
||||
class DrawingProgram;
|
||||
|
||||
class ImageTool {
|
||||
public:
|
||||
ImageTool(DrawingProgram& initDrawP);
|
||||
void edit_start(const std::shared_ptr<DrawImage>& a, std::any& prevData);
|
||||
void commit_edit_updates(const std::shared_ptr<DrawImage>& a, std::any& prevData);
|
||||
bool edit_update(const std::shared_ptr<DrawImage>& a);
|
||||
bool edit_gui(const std::shared_ptr<DrawImage>& a);
|
||||
private:
|
||||
struct ImageControls {
|
||||
} controls;
|
||||
|
||||
DrawingProgram& drawP;
|
||||
};
|
||||
77
src/DrawingProgram/InkDropperTool.cpp
Normal file
77
src/DrawingProgram/InkDropperTool.cpp
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#include "InkDropperTool.hpp"
|
||||
#include "DrawingProgram.hpp"
|
||||
#include "../Server/CommandList.hpp"
|
||||
#include "../MainProgram.hpp"
|
||||
#include "../DrawData.hpp"
|
||||
#include <include/core/SkAlphaType.h>
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include <include/core/SkColor.h>
|
||||
#include <include/core/SkColorType.h>
|
||||
#include <include/core/SkRRect.h>
|
||||
#include <include/core/SkRect.h>
|
||||
#include <include/core/SkColorSpace.h>
|
||||
#include <include/core/SkStream.h>
|
||||
#include <include/core/SkSurface.h>
|
||||
#ifdef USE_SKIA_BACKEND_GRAPHITE
|
||||
#include <include/gpu/graphite/Surface.h>
|
||||
#elif USE_SKIA_BACKEND_GANESH
|
||||
#include <include/gpu/ganesh/GrDirectContext.h>
|
||||
#include <include/gpu/ganesh/SkSurfaceGanesh.h>
|
||||
#endif
|
||||
#include <Helpers/Logger.hpp>
|
||||
|
||||
InkDropperTool::InkDropperTool(DrawingProgram& initDrawP):
|
||||
drawP(initDrawP)
|
||||
{
|
||||
}
|
||||
|
||||
void InkDropperTool::gui_toolbox() {
|
||||
Toolbar& t = drawP.world.main.toolbar;
|
||||
t.gui.push_id("Color select tool");
|
||||
t.gui.text_label_centered("Color Select");
|
||||
t.gui.pop_id();
|
||||
}
|
||||
|
||||
void InkDropperTool::tool_update() {
|
||||
if(drawP.controls.leftClick) {
|
||||
SkSurfaceProps props;
|
||||
SkImageInfo imgInfo = SkImageInfo::MakeN32Premul(drawP.world.main.window.size.x(), drawP.world.main.window.size.y());
|
||||
#ifdef USE_SKIA_BACKEND_GRAPHITE
|
||||
sk_sp<SkSurface> surface = SkSurfaces::RenderTarget(drawP.world.main.window.recorder(), imgInfo, skgpu::Mipmapped::kNo, &props);
|
||||
#elif USE_SKIA_BACKEND_GANESH
|
||||
sk_sp<SkSurface> surface = SkSurfaces::RenderTarget(drawP.world.main.window.ctx.get(), skgpu::Budgeted::kNo, imgInfo, 0, &props);
|
||||
#endif
|
||||
if(!surface) {
|
||||
Logger::get().log("INFO", "[SkSurfaces::WrapBackendRenderTarget] Ink Dropper Tool could not make surface");
|
||||
return;
|
||||
}
|
||||
SkCanvas* inkDropperCanvas = surface->getCanvas();
|
||||
if(!inkDropperCanvas) {
|
||||
Logger::get().log("INFO", "Ink Dropper Tool could not make canvas");
|
||||
return;
|
||||
}
|
||||
int xPos = std::clamp<int>(drawP.world.main.input.mouse.pos.x(), 0, drawP.world.main.window.size.x() - 1);
|
||||
int yPos = std::clamp<int>(drawP.world.main.input.mouse.pos.y(), 0, drawP.world.main.window.size.y() - 1);
|
||||
|
||||
inkDropperCanvas->save();
|
||||
drawP.world.drawData.dontUseDrawProgCache = true;
|
||||
drawP.world.main.draw(inkDropperCanvas);
|
||||
drawP.world.drawData.dontUseDrawProgCache = false;
|
||||
inkDropperCanvas->restore();
|
||||
//drawP.world.main.window.ctx->flush();
|
||||
|
||||
Vector4<uint8_t> readData;
|
||||
SkImageInfo readDataInfo = SkImageInfo::Make(1, 1, kRGBA_8888_SkColorType, kUnpremul_SkAlphaType);
|
||||
#ifdef USE_SKIA_BACKEND_GRAPHITE
|
||||
// NOT WORKING IN GRAPHITE YET
|
||||
//surface->makeImageSnapshot()->readPixels(drawP.world.main.window.ctx.get(), readDataInfo, (void*)readData.data(), 4, xPos, yPos);
|
||||
#elif USE_SKIA_BACKEND_GANESH
|
||||
surface->makeImageSnapshot()->readPixels(drawP.world.main.window.ctx.get(), readDataInfo, (void*)readData.data(), 4, xPos, yPos);
|
||||
#endif
|
||||
|
||||
drawP.controls.foregroundColor = readData.cast<float>() / 255.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void InkDropperTool::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
}
|
||||
17
src/DrawingProgram/InkDropperTool.hpp
Normal file
17
src/DrawingProgram/InkDropperTool.hpp
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#pragma once
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include "../DrawData.hpp"
|
||||
#include <Helpers/SCollision.hpp>
|
||||
|
||||
class DrawingProgram;
|
||||
|
||||
class InkDropperTool {
|
||||
public:
|
||||
InkDropperTool(DrawingProgram& initDrawP);
|
||||
void gui_toolbox();
|
||||
void tool_update();
|
||||
void draw(SkCanvas* canvas, const DrawData& drawData);
|
||||
private:
|
||||
|
||||
DrawingProgram& drawP;
|
||||
};
|
||||
157
src/DrawingProgram/RectDrawTool.cpp
Normal file
157
src/DrawingProgram/RectDrawTool.cpp
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
#include "RectDrawTool.hpp"
|
||||
#include "DrawingProgram.hpp"
|
||||
#include "../Server/CommandList.hpp"
|
||||
#include "../MainProgram.hpp"
|
||||
#include "../DrawData.hpp"
|
||||
#include "Helpers/SCollision.hpp"
|
||||
#include "Helpers/Serializers.hpp"
|
||||
#include "../SharedTypes.hpp"
|
||||
#include <cereal/types/vector.hpp>
|
||||
|
||||
RectDrawTool::RectDrawTool(DrawingProgram& initDrawP):
|
||||
drawP(initDrawP)
|
||||
{
|
||||
controls.intermediateItem = std::make_shared<DrawRectangle>();
|
||||
}
|
||||
|
||||
void RectDrawTool::gui_toolbox() {
|
||||
Toolbar& t = drawP.world.main.toolbar;
|
||||
t.gui.push_id("rect draw tool");
|
||||
t.gui.text_label_centered("Draw Rectangle");
|
||||
t.gui.slider_scalar_field("relradiuswidth", "Corner Radius", &controls.relativeRadiusWidth, 0.0f, 40.0f);
|
||||
if(t.gui.radio_button_field("fillonly", "Fill only", controls.fillStrokeMode == 0)) controls.fillStrokeMode = 0;
|
||||
if(t.gui.radio_button_field("outlineonly", "Outline only", controls.fillStrokeMode == 1)) controls.fillStrokeMode = 1;
|
||||
if(t.gui.radio_button_field("filloutline", "Fill and Outline", controls.fillStrokeMode == 2)) controls.fillStrokeMode = 2;
|
||||
if(controls.fillStrokeMode == 1 || controls.fillStrokeMode == 2)
|
||||
t.gui.slider_scalar_field("relstrokewidth", "Outline Size", &drawP.controls.relativeWidth, 3.0f, 40.0f);
|
||||
t.gui.pop_id();
|
||||
}
|
||||
|
||||
bool RectDrawTool::edit_gui(const std::shared_ptr<DrawRectangle>& a) {
|
||||
DrawRectangle::Data oldData = a->d;
|
||||
Toolbar& t = drawP.world.main.toolbar;
|
||||
t.gui.push_id("edit tool rectangle");
|
||||
t.gui.text_label_centered("Edit Rectangle");
|
||||
t.gui.slider_scalar_field("relradiuswidth", "Corner Radius", &a->d.cornerRadius, 0.0f, 40.0f);
|
||||
if(t.gui.radio_button_field("fillonly", "Fill only", a->d.fillStrokeMode == 0)) a->d.fillStrokeMode = 0;
|
||||
if(t.gui.radio_button_field("outlineonly", "Outline only", a->d.fillStrokeMode == 1)) a->d.fillStrokeMode = 1;
|
||||
if(t.gui.radio_button_field("filloutline", "Fill and Outline", a->d.fillStrokeMode == 2)) a->d.fillStrokeMode = 2;
|
||||
if(a->d.fillStrokeMode == 0 || a->d.fillStrokeMode == 2) {
|
||||
t.gui.left_to_right_line_layout([&]() {
|
||||
CLAY({.layout = {.sizing = {.width = CLAY_SIZING_FIXED(40), .height = CLAY_SIZING_FIXED(40)}}}) {
|
||||
if(t.gui.color_button("Fill Color", &a->d.fillColor, &a->d.fillColor == t.colorRight))
|
||||
t.color_selector_right(&a->d.fillColor == t.colorRight ? nullptr : &a->d.fillColor);
|
||||
}
|
||||
t.gui.text_label("Fill Color");
|
||||
});
|
||||
}
|
||||
if(a->d.fillStrokeMode == 1 || a->d.fillStrokeMode == 2) {
|
||||
t.gui.slider_scalar_field("relstrokewidth", "Outline Size", &a->d.strokeWidth, 3.0f, 40.0f);
|
||||
t.gui.left_to_right_line_layout([&]() {
|
||||
CLAY({.layout = {.sizing = {.width = CLAY_SIZING_FIXED(40), .height = CLAY_SIZING_FIXED(40)}}}) {
|
||||
if(t.gui.color_button("Outline Color", &a->d.strokeColor, &a->d.strokeColor == t.colorRight))
|
||||
t.color_selector_right(&a->d.strokeColor == t.colorRight ? nullptr : &a->d.strokeColor);
|
||||
}
|
||||
t.gui.text_label("Outline Color");
|
||||
});
|
||||
}
|
||||
t.gui.pop_id();
|
||||
return a->d != oldData;
|
||||
}
|
||||
|
||||
void RectDrawTool::reset_tool() {
|
||||
commit_rectangle();
|
||||
controls.drawStage = 0;
|
||||
}
|
||||
|
||||
void RectDrawTool::edit_start(const std::shared_ptr<DrawRectangle>& a, std::any& prevData) {
|
||||
prevData = a->d;
|
||||
drawP.editTool.add_point_handle({&a->d.p1, nullptr, &a->d.p2});
|
||||
drawP.editTool.add_point_handle({&a->d.p2, &a->d.p1, nullptr});
|
||||
}
|
||||
|
||||
void RectDrawTool::commit_edit_updates(const std::shared_ptr<DrawRectangle>& a, std::any& prevData) {
|
||||
DrawRectangle::Data pData = std::any_cast<DrawRectangle::Data>(prevData);
|
||||
DrawRectangle::Data cData = a->d;
|
||||
drawP.world.undo.push(UndoManager::UndoRedoPair{
|
||||
[&, a, pData]() {
|
||||
a->d = pData;
|
||||
ServerClientID compID = drawP.components.get_id(a);
|
||||
if(compID != ServerClientID{0, 0})
|
||||
a->client_send_update_final(drawP, compID);
|
||||
a->final_update(drawP);
|
||||
drawP.reset_tools();
|
||||
return true;
|
||||
},
|
||||
[&, a, cData]() {
|
||||
ServerClientID compID = drawP.components.get_id(a);
|
||||
a->d = cData;
|
||||
if(compID != ServerClientID{0, 0})
|
||||
a->client_send_update_final(drawP, compID);
|
||||
a->final_update(drawP);
|
||||
drawP.reset_tools();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool RectDrawTool::edit_update(const std::shared_ptr<DrawRectangle>& a) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void RectDrawTool::tool_update() {
|
||||
switch(controls.drawStage) {
|
||||
case 0: {
|
||||
if(drawP.controls.leftClick) {
|
||||
controls.startAt = drawP.world.main.input.mouse.pos;
|
||||
controls.intermediateItem = std::make_shared<DrawRectangle>();
|
||||
controls.intermediateItem->d.strokeColor = drawP.controls.foregroundColor;
|
||||
controls.intermediateItem->d.fillColor = drawP.controls.backgroundColor;
|
||||
controls.intermediateItem->d.cornerRadius = controls.relativeRadiusWidth;
|
||||
controls.intermediateItem->d.strokeWidth = drawP.controls.relativeWidth;
|
||||
controls.intermediateItem->d.p1 = controls.startAt;
|
||||
controls.intermediateItem->d.p2 = controls.startAt;
|
||||
controls.intermediateItem->d.fillStrokeMode = static_cast<uint8_t>(controls.fillStrokeMode);
|
||||
controls.intermediateItem->coords = drawP.world.drawData.cam.c;
|
||||
controls.intermediateItem->lastUpdateTime = std::chrono::steady_clock::now();
|
||||
controls.intermediateItem->temp_update(drawP);
|
||||
uint64_t placement = drawP.components.client_list().size();
|
||||
controls.intermediateID = drawP.components.client_insert(placement, controls.intermediateItem);
|
||||
controls.intermediateItem->client_send_place(drawP, placement);
|
||||
drawP.add_undo_place_component(placement, controls.intermediateItem);
|
||||
controls.drawStage = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
if(drawP.controls.leftClickHeld) {
|
||||
controls.intermediateItem->lastUpdateTime = std::chrono::steady_clock::now();
|
||||
Vector2f newPos = controls.intermediateItem->coords.get_mouse_pos(drawP.world);
|
||||
if(drawP.world.main.input.key(InputManager::KEY_EQUAL_DIMENSIONS).held) {
|
||||
float height = std::fabs(controls.startAt.y() - newPos.y());
|
||||
newPos.x() = controls.startAt.x() + (((newPos.x() - controls.startAt.x()) < 0.0f ? -1.0f : 1.0f) * height);
|
||||
}
|
||||
controls.intermediateItem->d.p1 = cwise_vec_min(controls.startAt, newPos);
|
||||
controls.intermediateItem->d.p2 = cwise_vec_max(controls.startAt, newPos);
|
||||
controls.intermediateItem->client_send_update_temp(drawP, controls.intermediateID);
|
||||
controls.intermediateItem->temp_update(drawP);
|
||||
}
|
||||
else {
|
||||
commit_rectangle();
|
||||
controls.drawStage = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RectDrawTool::commit_rectangle() {
|
||||
if(controls.intermediateItem) {
|
||||
controls.intermediateItem->client_send_update_final(drawP, controls.intermediateID);
|
||||
controls.intermediateItem->final_update(drawP);
|
||||
controls.intermediateItem = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void RectDrawTool::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
}
|
||||
35
src/DrawingProgram/RectDrawTool.hpp
Normal file
35
src/DrawingProgram/RectDrawTool.hpp
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#pragma once
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include "../DrawData.hpp"
|
||||
#include "../DrawComponents/DrawRectangle.hpp"
|
||||
#include <Helpers/SCollision.hpp>
|
||||
#include <any>
|
||||
|
||||
class DrawingProgram;
|
||||
|
||||
class RectDrawTool {
|
||||
public:
|
||||
RectDrawTool(DrawingProgram& initDrawP);
|
||||
void gui_toolbox();
|
||||
void tool_update();
|
||||
void reset_tool();
|
||||
void draw(SkCanvas* canvas, const DrawData& drawData);
|
||||
|
||||
void edit_start(const std::shared_ptr<DrawRectangle>& a, std::any& prevData);
|
||||
void commit_edit_updates(const std::shared_ptr<DrawRectangle>& a, std::any& prevData);
|
||||
bool edit_update(const std::shared_ptr<DrawRectangle>& a);
|
||||
bool edit_gui(const std::shared_ptr<DrawRectangle>& a);
|
||||
private:
|
||||
void commit_rectangle();
|
||||
|
||||
struct RectDrawControls {
|
||||
Vector2f startAt;
|
||||
float relativeRadiusWidth = 10.0f;
|
||||
int fillStrokeMode = 1;
|
||||
int drawStage = 0;
|
||||
std::shared_ptr<DrawRectangle> intermediateItem;
|
||||
ServerClientID intermediateID;
|
||||
} controls;
|
||||
|
||||
DrawingProgram& drawP;
|
||||
};
|
||||
398
src/DrawingProgram/RectSelectTool.cpp
Normal file
398
src/DrawingProgram/RectSelectTool.cpp
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
#include "RectSelectTool.hpp"
|
||||
#include <chrono>
|
||||
#include "DrawingProgram.hpp"
|
||||
#include "../Server/CommandList.hpp"
|
||||
#include "../MainProgram.hpp"
|
||||
#include "../DrawData.hpp"
|
||||
#include "Helpers/FixedPoint.hpp"
|
||||
#include "Helpers/MathExtras.hpp"
|
||||
#include "Helpers/SCollision.hpp"
|
||||
#include "Helpers/Serializers.hpp"
|
||||
#include "../CoordSpaceHelper.hpp"
|
||||
#include <cereal/types/vector.hpp>
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
|
||||
#define ROTATION_POINT_RADIUS_MULTIPLIER 0.7
|
||||
#define ROTATION_POINTS_DISTANCE 20.0
|
||||
|
||||
RectSelectTool::RectSelectTool(DrawingProgram& initDrawP):
|
||||
drawP(initDrawP)
|
||||
{
|
||||
}
|
||||
|
||||
void RectSelectTool::gui_toolbox() {
|
||||
drawP.world.main.toolbar.gui.text_label_centered("Rectangle Select");
|
||||
}
|
||||
|
||||
void RectSelectTool::reset_tool() {
|
||||
if(controls.selectionMode == 3)
|
||||
commit_transformation();
|
||||
controls = RectSelectControls();
|
||||
}
|
||||
|
||||
void RectSelectTool::tool_update() {
|
||||
switch(controls.selectionMode) {
|
||||
case 0: {
|
||||
if(drawP.controls.leftClick) {
|
||||
reset_tool();
|
||||
controls.coords = drawP.world.drawData.cam.c;
|
||||
controls.selectStartAt = controls.coords.get_mouse_pos(drawP.world);
|
||||
controls.rotationPointAngle = 0.0;
|
||||
controls.selectionMode = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
using namespace SCollision;
|
||||
Vector2f mPos = controls.coords.get_mouse_pos(drawP.world);
|
||||
Vector2f newP1 = cwise_vec_min(controls.selectStartAt, mPos);
|
||||
Vector2f newP2 = cwise_vec_max(controls.selectStartAt, mPos);
|
||||
controls.newT = triangle_from_rect_points(newP1, newP2);
|
||||
if(!drawP.controls.leftClickHeld) {
|
||||
ColliderCollection<float> selectionCollection;
|
||||
selectionCollection.triangle.emplace_back(controls.newT[0], controls.newT[1], controls.newT[2]);
|
||||
selectionCollection.triangle.emplace_back(controls.newT[2], controls.newT[3], controls.newT[0]);
|
||||
drawP.check_all_collisions_transform(selectionCollection);
|
||||
for(auto& c : drawP.components.client_list())
|
||||
c->obj->selected = c->obj->globalCollisionCheck;
|
||||
controls.selectionMode = create_selection_aabb() ? 2 : 0;
|
||||
controls.rotationCenter = controls.finalSelectionAABB.center();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
Vector2f mPos = drawP.world.main.input.mouse.pos;
|
||||
bool copyHit = drawP.world.main.input.key(InputManager::KEY_COPY).pressed;
|
||||
bool cutHit = drawP.world.main.input.key(InputManager::KEY_CUT).pressed;
|
||||
bool deleteHit = drawP.world.main.input.key(InputManager::KEY_DRAW_DELETE).pressed;
|
||||
bool unselectHit = drawP.world.main.input.key(InputManager::KEY_DRAW_UNSELECT).pressed;
|
||||
|
||||
if(drawP.controls.leftClick) {
|
||||
if(SCollision::collide(mPos, controls.scaleSelectionCircle)) {
|
||||
controls.scaleStartAt = controls.finalSelectionAABB.max;
|
||||
controls.scaleCenterPoint = controls.finalSelectionAABB.center();
|
||||
set_selected_items();
|
||||
controls.selectionMode = 4;
|
||||
}
|
||||
else if(SCollision::collide(mPos, controls.rotationCenterCircle)) {
|
||||
controls.moveStartAt = controls.rotationCenter;
|
||||
controls.selectionMode = 5;
|
||||
}
|
||||
else if(SCollision::collide(mPos, controls.rotationPointCircle)) {
|
||||
set_selected_items();
|
||||
controls.selectionMode = 6;
|
||||
}
|
||||
else if(SCollision::collide(drawP.controls.currentMouseWorldPos, controls.finalSelectionAABB)) {
|
||||
controls.moveStartAt = drawP.controls.currentMouseWorldPos;
|
||||
set_selected_items();
|
||||
controls.selectionMode = 3;
|
||||
}
|
||||
else {
|
||||
controls.selectionMode = 0;
|
||||
drawP.reset_selection();
|
||||
}
|
||||
}
|
||||
else if(deleteHit || cutHit) {
|
||||
selection_to_clipboard();
|
||||
std::vector<std::pair<uint64_t, std::shared_ptr<DrawComponent>>> erasedCompVal; // <placement order, object>
|
||||
drawP.components.client_erase_if([&](uint64_t oldPlacement, const auto& c) {
|
||||
if(c->obj->selected) {
|
||||
erasedCompVal.emplace_back(std::pair<uint64_t, std::shared_ptr<DrawComponent>>{oldPlacement, c->obj});
|
||||
DrawComponent::client_send_erase(drawP, c->id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
drawP.world.undo.push(UndoManager::UndoRedoPair{
|
||||
[&, erasedCompVal]() {
|
||||
for(auto& comp : erasedCompVal)
|
||||
if(drawP.components.get_id(comp.second) != ServerClientID{0, 0})
|
||||
return false;
|
||||
|
||||
for(auto& comp : erasedCompVal | std::views::reverse) {
|
||||
drawP.components.client_insert(comp.first, comp.second);
|
||||
comp.second->client_send_place(drawP, comp.first);
|
||||
}
|
||||
drawP.reset_tools();
|
||||
return true;
|
||||
},
|
||||
[&, erasedCompVal]() {
|
||||
for(auto& comp : erasedCompVal)
|
||||
if(drawP.components.get_id(comp.second) == ServerClientID{0, 0})
|
||||
return false;
|
||||
|
||||
for(auto& comp : erasedCompVal) {
|
||||
ServerClientID compID;
|
||||
drawP.components.client_erase(comp.second, compID);
|
||||
DrawComponent::client_send_erase(drawP, compID);
|
||||
}
|
||||
drawP.reset_tools();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
controls.selectionMode = 0;
|
||||
}
|
||||
else if(copyHit)
|
||||
selection_to_clipboard();
|
||||
else if(unselectHit) {
|
||||
drawP.reset_selection();
|
||||
controls.selectionMode = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
WorldVec translationVec = drawP.controls.currentMouseWorldPos - controls.moveStartAt;
|
||||
if(drawP.controls.leftClickHeld) {
|
||||
for(auto& item : controls.selectedItems) {
|
||||
auto newCoords = item.oldCoords;
|
||||
newCoords.translate(translationVec);
|
||||
item.comp->coords = newCoords;
|
||||
item.comp->updateDraw = true;
|
||||
item.comp->client_send_transform_temp(drawP, item.id);
|
||||
item.comp->lastTransformTime = std::chrono::steady_clock::now();
|
||||
}
|
||||
}
|
||||
else {
|
||||
commit_transformation();
|
||||
controls.selectionMode = create_selection_aabb() ? 2 : 0;
|
||||
controls.rotationCenter = controls.finalSelectionAABB.center();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
WorldVec centerToScaleStart = controls.scaleStartAt - controls.scaleCenterPoint;
|
||||
Vector2f centerToScaleStartInCamSpace = drawP.world.drawData.cam.c.normalized_dir_to_space(centerToScaleStart.normalized());
|
||||
Vector2f scaleCenterPointInCamSpace = drawP.world.drawData.cam.c.to_space(controls.scaleCenterPoint);
|
||||
controls.scaleCurrentPoint = drawP.world.drawData.cam.c.from_space(project_point_on_line(drawP.world.main.input.mouse.pos, scaleCenterPointInCamSpace, (scaleCenterPointInCamSpace + centerToScaleStartInCamSpace).eval()));
|
||||
|
||||
WorldVec centerToScaleCurrent = controls.scaleCurrentPoint - controls.scaleCenterPoint;
|
||||
|
||||
WorldScalar centerToScaleStartNorm = centerToScaleStart.norm();
|
||||
WorldScalar centerToScaleCurrentNorm = centerToScaleCurrent.norm();
|
||||
bool isAnyNumberZero = centerToScaleCurrentNorm == WorldScalar(0) || centerToScaleStartNorm == WorldScalar(0);
|
||||
if(!isAnyNumberZero) {
|
||||
bool flipScale = centerToScaleStartNorm < centerToScaleCurrentNorm;
|
||||
WorldScalar scaleAmount;
|
||||
if(flipScale)
|
||||
scaleAmount = centerToScaleCurrentNorm / centerToScaleStartNorm;
|
||||
else
|
||||
scaleAmount = centerToScaleStartNorm / centerToScaleCurrentNorm;
|
||||
|
||||
if(drawP.controls.leftClickHeld) {
|
||||
for(auto& item : controls.selectedItems) {
|
||||
auto newCoords = item.oldCoords;
|
||||
newCoords.scale_about(controls.scaleCenterPoint, scaleAmount, flipScale);
|
||||
item.comp->coords = newCoords;
|
||||
item.comp->client_send_transform_temp(drawP, item.id);
|
||||
item.comp->updateDraw = true;
|
||||
item.comp->lastTransformTime = std::chrono::steady_clock::now();
|
||||
}
|
||||
}
|
||||
else {
|
||||
commit_transformation();
|
||||
controls.selectionMode = create_selection_aabb() ? 2 : 0;
|
||||
controls.rotationCenter = controls.finalSelectionAABB.center();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
controls.rotationCenter = drawP.controls.currentMouseWorldPos;
|
||||
if(!drawP.controls.leftClickHeld) {
|
||||
controls.selectionMode = 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 6: {
|
||||
Vector2f rotationPointDiff = drawP.world.main.input.mouse.pos - drawP.world.drawData.cam.c.to_space(controls.rotationCenter);
|
||||
controls.rotationPointAngle = std::atan2(rotationPointDiff.y(), rotationPointDiff.x());
|
||||
if(drawP.controls.leftClickHeld) {
|
||||
for(auto& item : controls.selectedItems) {
|
||||
auto newCoords = item.oldCoords;
|
||||
newCoords.rotate_about(controls.rotationCenter, controls.rotationPointAngle);
|
||||
item.comp->coords = newCoords;
|
||||
item.comp->updateDraw = true;
|
||||
item.comp->client_send_transform_temp(drawP, item.id);
|
||||
item.comp->lastTransformTime = std::chrono::steady_clock::now();
|
||||
}
|
||||
}
|
||||
else {
|
||||
commit_transformation();
|
||||
controls.rotationPointAngle = 0.0;
|
||||
controls.selectionMode = create_selection_aabb() ? 2 : 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
update_drag_circles();
|
||||
}
|
||||
|
||||
void RectSelectTool::paste_clipboard() {
|
||||
auto& clipboard = drawP.world.main.clipboard;
|
||||
if(!clipboard.components.empty()) {
|
||||
drawP.reset_tools();
|
||||
drawP.controls.selectedTool = DrawingProgram::TOOL_RECTSELECT;
|
||||
drawP.controls.previousSelected = DrawingProgram::TOOL_RECTSELECT;
|
||||
uint64_t allPlacement = drawP.components.client_list().size();
|
||||
std::vector<std::shared_ptr<DrawComponent>> placedComponents;
|
||||
WorldVec mousePos = drawP.world.get_mouse_world_pos();
|
||||
WorldVec moveVec = clipboard.pos - mousePos;
|
||||
std::unordered_map<ServerClientID, ServerClientID> resourceRemapIDs;
|
||||
for(auto& r : clipboard.resources)
|
||||
resourceRemapIDs[r.first] = drawP.world.rMan.add_resource(r.second);
|
||||
for(auto& c : clipboard.components) {
|
||||
uint64_t placement = drawP.components.client_list().size();
|
||||
std::shared_ptr<DrawComponent> newC = c->copy();
|
||||
newC->remap_resource_ids(resourceRemapIDs);
|
||||
newC->coords.translate(-moveVec);
|
||||
newC->selected = true;
|
||||
|
||||
drawP.components.client_insert(placement, newC);
|
||||
newC->final_update(drawP);
|
||||
newC->client_send_place(drawP, placement);
|
||||
placedComponents.emplace_back(newC);
|
||||
}
|
||||
set_selected_items();
|
||||
create_selection_aabb();
|
||||
controls.rotationCenter = controls.finalSelectionAABB.center();
|
||||
controls.selectionMode = 2;
|
||||
drawP.add_undo_place_components(allPlacement, placedComponents);
|
||||
}
|
||||
}
|
||||
|
||||
void RectSelectTool::selection_to_clipboard() {
|
||||
auto& clipboard = drawP.world.main.clipboard;
|
||||
clipboard.components.clear();
|
||||
std::unordered_set<ServerClientID> resourceSet;
|
||||
for(auto& c : drawP.components.client_list()) {
|
||||
if(c->obj->selected) {
|
||||
c->obj->get_used_resources(resourceSet);
|
||||
clipboard.components.emplace_back(c->obj->copy());
|
||||
}
|
||||
}
|
||||
drawP.world.rMan.copy_resource_set_to_map(resourceSet, clipboard.resources);
|
||||
clipboard.pos = controls.finalSelectionAABB.center();
|
||||
}
|
||||
|
||||
Vector2f RectSelectTool::get_rotation_point_pos_from_angle(double angle) {
|
||||
return Vector2f{drawP.world.drawData.cam.c.to_space(controls.rotationCenter) + ROTATION_POINTS_DISTANCE * Vector2f{std::cos(angle), std::sin(angle)}};
|
||||
}
|
||||
|
||||
void RectSelectTool::update_drag_circles() {
|
||||
controls.scaleSelectionCircle = SCollision::Circle<float>(drawP.world.drawData.cam.c.to_space(controls.scaleCurrentPoint), DRAG_POINT_RADIUS);
|
||||
controls.rotationCenterCircle = SCollision::Circle<float>(drawP.world.drawData.cam.c.to_space(controls.rotationCenter), DRAG_POINT_RADIUS);
|
||||
controls.rotationPoint = get_rotation_point_pos_from_angle(controls.rotationPointAngle);
|
||||
controls.rotationPointCircle = SCollision::Circle<float>(controls.rotationPoint, ROTATION_POINT_RADIUS_MULTIPLIER * DRAG_POINT_RADIUS);
|
||||
}
|
||||
|
||||
bool RectSelectTool::create_selection_aabb() {
|
||||
controls.coords = drawP.world.drawData.cam.c;
|
||||
|
||||
controls.finalSelectionAABB = SCollision::AABB<WorldScalar>();
|
||||
bool first = true;
|
||||
for(auto& c : drawP.components.client_list()) {
|
||||
if(c->obj->selected && c->obj->get_world_bounds()) {
|
||||
if(first) {
|
||||
controls.finalSelectionAABB = *c->obj->get_world_bounds();
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
controls.finalSelectionAABB.include_aabb_in_bounds(*c->obj->get_world_bounds());
|
||||
}
|
||||
}
|
||||
|
||||
controls.scaleCurrentPoint = controls.finalSelectionAABB.max;
|
||||
|
||||
return !first;
|
||||
}
|
||||
|
||||
void RectSelectTool::set_selected_items() {
|
||||
controls.selectedItems.clear();
|
||||
for(auto& c : drawP.components.client_list()) {
|
||||
if(c->obj->selected) {
|
||||
controls.selectedItems.emplace_back(c->obj, c->id, c->obj->coords);
|
||||
c->obj->lastTransformTime = std::chrono::steady_clock::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RectSelectTool::commit_transformation() {
|
||||
std::vector<CoordSpaceHelper> finalCoords;
|
||||
for(auto& item : controls.selectedItems) {
|
||||
item.comp->client_send_transform_final(drawP, item.id);
|
||||
item.comp->finalize_update(drawP.colliderAllocated);
|
||||
finalCoords.emplace_back(item.comp->coords);
|
||||
}
|
||||
|
||||
auto sItems = controls.selectedItems;
|
||||
|
||||
drawP.world.undo.push(UndoManager::UndoRedoPair{
|
||||
[&, sItems, finalCoords]() {
|
||||
for(auto& item : sItems) {
|
||||
item.comp->coords = item.oldCoords;
|
||||
item.comp->client_send_transform_final(drawP, drawP.components.get_id(item.comp));
|
||||
item.comp->finalize_update(drawP.colliderAllocated);
|
||||
}
|
||||
drawP.reset_tools();
|
||||
return true;
|
||||
},
|
||||
[&, sItems, finalCoords]() {
|
||||
for(auto [item, finalCoord] : std::views::zip(sItems, finalCoords)) {
|
||||
item.comp->coords = finalCoord;
|
||||
item.comp->client_send_transform_final(drawP, drawP.components.get_id(item.comp));
|
||||
item.comp->finalize_update(drawP.colliderAllocated);
|
||||
}
|
||||
drawP.reset_tools();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void RectSelectTool::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
if(controls.selectionMode != 0) {
|
||||
canvas->save();
|
||||
controls.coords.transform_sk_canvas(canvas, drawData);
|
||||
|
||||
SkPaint selectionPaint;
|
||||
selectionPaint.setColor4f({0.3f, 0.6f, 0.9f, 0.4f});
|
||||
|
||||
if(controls.selectionMode == 1) {
|
||||
SkRect r = SkRect::MakeLTRB(controls.newT[0].x(), controls.newT[0].y(), controls.newT[2].x(), controls.newT[2].y());
|
||||
canvas->drawRect(r, selectionPaint);
|
||||
canvas->restore();
|
||||
}
|
||||
if(controls.selectionMode == 2 || controls.selectionMode == 5) {
|
||||
auto triArray = triangle_from_rect_points(controls.finalSelectionAABB.min, controls.finalSelectionAABB.max);
|
||||
std::array<Vector2f, 4> triArray2;
|
||||
triArray2[0] = controls.coords.to_space(controls.finalSelectionAABB.min);
|
||||
triArray2[1] = controls.coords.to_space(controls.finalSelectionAABB.top_right());
|
||||
triArray2[2] = controls.coords.to_space(controls.finalSelectionAABB.bottom_left());
|
||||
triArray2[3] = controls.coords.to_space(controls.finalSelectionAABB.max);
|
||||
SkPath a;
|
||||
a.moveTo(triArray2[0].x(), triArray2[0].y());
|
||||
a.lineTo(triArray2[1].x(), triArray2[1].y());
|
||||
a.lineTo(triArray2[2].x(), triArray2[2].y());
|
||||
a.close();
|
||||
a.moveTo(triArray2[1].x(), triArray2[1].y());
|
||||
a.lineTo(triArray2[2].x(), triArray2[2].y());
|
||||
a.lineTo(triArray2[3].x(), triArray2[3].y());
|
||||
a.close();
|
||||
canvas->drawPath(a, selectionPaint);
|
||||
// SkPaint rotateCirclePaint(SkColor4f{0.9f, 0.5f, 0.1f, 1.0f});
|
||||
}
|
||||
|
||||
canvas->restore();
|
||||
|
||||
if(controls.selectionMode == 6 || controls.selectionMode == 5 || controls.selectionMode == 2) {
|
||||
drawP.draw_drag_circle(canvas, controls.rotationPoint, {0.9f, 0.5f, 0.1f, 1.0f}, drawData, ROTATION_POINT_RADIUS_MULTIPLIER);
|
||||
drawP.draw_drag_circle(canvas, drawData.cam.c.to_space(controls.rotationCenter), {0.9f, 0.5f, 0.1f, 1.0f}, drawData);
|
||||
}
|
||||
if(controls.selectionMode == 4 || controls.selectionMode == 2) {
|
||||
drawP.draw_drag_circle(canvas, drawData.cam.c.to_space(controls.scaleCurrentPoint), {0.1f, 0.9f, 0.9f, 1.0f}, drawData);
|
||||
if(controls.selectionMode == 4)
|
||||
drawP.draw_drag_circle(canvas, drawData.cam.c.to_space(controls.scaleCenterPoint), {0.0f, 0.0f, 0.0f, 0.0f}, drawData);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/DrawingProgram/RectSelectTool.hpp
Normal file
58
src/DrawingProgram/RectSelectTool.hpp
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#pragma once
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include "../DrawData.hpp"
|
||||
#include <Helpers/SCollision.hpp>
|
||||
#include "../CoordSpaceHelper.hpp"
|
||||
#include "../DrawComponents/DrawComponent.hpp"
|
||||
|
||||
class DrawingProgram;
|
||||
|
||||
class RectSelectTool {
|
||||
public:
|
||||
RectSelectTool(DrawingProgram& initDrawP);
|
||||
void gui_toolbox();
|
||||
void tool_update();
|
||||
void draw(SkCanvas* canvas, const DrawData& drawData);
|
||||
void reset_tool();
|
||||
void paste_clipboard();
|
||||
private:
|
||||
struct RectSelectControls {
|
||||
int selectionMode = 0;
|
||||
Vector2f selectStartAt;
|
||||
CoordSpaceHelper coords;
|
||||
|
||||
WorldVec moveStartAt;
|
||||
SCollision::AABB<WorldScalar> finalSelectionAABB;
|
||||
std::array<Vector2f, 4> newT;
|
||||
|
||||
WorldVec scaleStartAt;
|
||||
WorldVec scaleCenterPoint;
|
||||
WorldVec scaleCurrentPoint;
|
||||
|
||||
WorldVec rotationCenter;
|
||||
|
||||
// Temporary, per frame colliders for handles
|
||||
Vector2f rotationPoint;
|
||||
SCollision::Circle<float> scaleSelectionCircle;
|
||||
SCollision::Circle<float> rotationCenterCircle;
|
||||
SCollision::Circle<float> rotationPointCircle;
|
||||
|
||||
double rotationPointAngle;
|
||||
|
||||
struct SelectedItemInfo {
|
||||
std::shared_ptr<DrawComponent> comp;
|
||||
ServerClientID id;
|
||||
CoordSpaceHelper oldCoords;
|
||||
};
|
||||
std::vector<SelectedItemInfo> selectedItems;
|
||||
} controls;
|
||||
|
||||
void selection_to_clipboard();
|
||||
Vector2f get_rotation_point_pos_from_angle(double angle);
|
||||
void update_drag_circles();
|
||||
void set_selected_items();
|
||||
bool create_selection_aabb();
|
||||
void commit_transformation();
|
||||
|
||||
DrawingProgram& drawP;
|
||||
};
|
||||
356
src/DrawingProgram/ScreenshotTool.cpp
Normal file
356
src/DrawingProgram/ScreenshotTool.cpp
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
#include "ScreenshotTool.hpp"
|
||||
#include "DrawingProgram.hpp"
|
||||
#include "../Server/CommandList.hpp"
|
||||
#include "../MainProgram.hpp"
|
||||
#include "../DrawData.hpp"
|
||||
#include "Helpers/SCollision.hpp"
|
||||
#include <include/core/SkAlphaType.h>
|
||||
#include <include/core/SkColorType.h>
|
||||
#include <include/core/SkPaint.h>
|
||||
#include <include/core/SkPathTypes.h>
|
||||
#include <include/core/SkStream.h>
|
||||
#include <include/core/SkSurface.h>
|
||||
#include <include/core/SkImage.h>
|
||||
#include <include/core/SkData.h>
|
||||
#include <include/encode/SkPngEncoder.h>
|
||||
#include <include/encode/SkWebpEncoder.h>
|
||||
#include <include/encode/SkJpegEncoder.h>
|
||||
#include <include/gpu/GpuTypes.h>
|
||||
#include "../FileHelpers.hpp"
|
||||
#include <filesystem>
|
||||
#ifdef USE_SKIA_BACKEND_GRAPHITE
|
||||
#include <include/gpu/graphite/Surface.h>
|
||||
#elif USE_SKIA_BACKEND_GANESH
|
||||
#include <include/gpu/ganesh/GrDirectContext.h>
|
||||
#include <include/gpu/ganesh/SkSurfaceGanesh.h>
|
||||
#endif
|
||||
#include "../GridManager.hpp"
|
||||
#include <Helpers/Logger.hpp>
|
||||
|
||||
#include "../MainProgram.hpp"
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <EmscriptenHelpers/emscripten_browser_file.h>
|
||||
#endif
|
||||
|
||||
#define SECTION_SIZE 4000
|
||||
#define AA_LEVEL 4
|
||||
|
||||
ScreenshotTool::ScreenshotTool(DrawingProgram& initDrawP):
|
||||
drawP(initDrawP)
|
||||
{
|
||||
}
|
||||
|
||||
void ScreenshotTool::gui_toolbox() {
|
||||
Toolbar& t = drawP.world.main.toolbar;
|
||||
t.gui.push_id("screenshot tool");
|
||||
t.gui.text_label_centered("Screenshot");
|
||||
auto oldImgSize = controls.imageSize;
|
||||
if(controls.selectionMode == 2) {
|
||||
t.gui.input_scalar_fields("Image Size", "Image Size", &controls.imageSize, 2, 0, 999999999);
|
||||
t.gui.left_to_right_line_layout([&]() {
|
||||
t.gui.text_label("Image Type");
|
||||
t.gui.dropdown_select("image type select", &controls.selectedType, controls.typeSelections);
|
||||
});
|
||||
t.gui.checkbox_field("Display Grid", "Display Grid", &controls.displayGrid);
|
||||
if(controls.selectedType != 0)
|
||||
t.gui.checkbox_field("Transparent Background", "Transparent Background", &controls.transparentBackground);
|
||||
if(controls.imageSize.x() != oldImgSize.x())
|
||||
controls.imageSize.y() = ((double)controls.imageSize.x() / oldImgSize.x()) * controls.imageSize.y();
|
||||
else if(controls.imageSize.y() != oldImgSize.y())
|
||||
controls.imageSize.x() = ((double)controls.imageSize.y() / oldImgSize.y()) * controls.imageSize.x();
|
||||
if(t.gui.text_button_wide("Take Screenshot", "Take Screenshot")) {
|
||||
controls.selectionMode = 0;
|
||||
#ifdef __EMSCRIPTEN__
|
||||
take_screenshot("a" + controls.typeSelections[controls.selectedType]);
|
||||
#else
|
||||
// We can't actually use the extension from the callback, so we have to set the extension of choice beforehand
|
||||
Toolbar::ExtensionFilter setExtensionFilter;
|
||||
if(controls.selectedType == 0)
|
||||
setExtensionFilter = {"JPEG", "jpg;jpeg"};
|
||||
else if(controls.selectedType == 1)
|
||||
setExtensionFilter = {"PNG", "png"};
|
||||
else if(controls.selectedType == 2)
|
||||
setExtensionFilter = {"WebP", "webp"};
|
||||
t.open_file_selector("Export", {setExtensionFilter}, [setExtensionFilter, w = make_weak_ptr(drawP.world.main.world)](const std::filesystem::path& p, const auto& e) {
|
||||
auto world = w.lock();
|
||||
if(world) {
|
||||
world->drawProg.screenshotTool.controls.screenshotSavePath = force_extension_on_path(p, setExtensionFilter.extensions);
|
||||
world->drawProg.screenshotTool.controls.setToTakeScreenshot = true;
|
||||
}
|
||||
}, "screenshot", true);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
t.gui.pop_id();
|
||||
}
|
||||
|
||||
void ScreenshotTool::take_screenshot(const std::filesystem::path& filePath) {
|
||||
if(controls.imageSize.x() <= 0 || controls.imageSize.y() <= 0) {
|
||||
std::cout << "[ScreenshotTool::take_screenshot] Image size is 0 or negative" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::string ext = filePath.extension().string();
|
||||
unsigned extType = 0;
|
||||
if(ext == ".jpg" || ext == ".jpeg")
|
||||
extType = 0;
|
||||
else if(ext == ".png")
|
||||
extType = 1;
|
||||
else if(ext == ".webp")
|
||||
extType = 2;
|
||||
|
||||
size_t imageByteSize = (size_t)controls.imageSize.x() * (size_t)controls.imageSize.y() * 4;
|
||||
size_t imageRowSize = 4 * controls.imageSize.x();
|
||||
|
||||
SkImageInfo finalImgInfo = SkImageInfo::Make(controls.imageSize.x(), controls.imageSize.y(), kRGBA_8888_SkColorType, kUnpremul_SkAlphaType);
|
||||
std::vector<uint8_t> finalImgRawData(imageByteSize);
|
||||
SkPixmap finalImgData(finalImgInfo, finalImgRawData.data(), imageRowSize);
|
||||
|
||||
SkImageInfo imgInfo = SkImageInfo::MakeN32Premul(drawP.world.main.window.size.x(), drawP.world.main.window.size.y());
|
||||
#ifdef USE_SKIA_BACKEND_GRAPHITE
|
||||
sk_sp<SkSurface> surface = SkSurfaces::RenderTarget(drawP.world.main.window.recorder(), imgInfo, skgpu::Mipmapped::kNo, &drawP.world.main.window.defaultMSAASurfaceProps);
|
||||
#elif USE_SKIA_BACKEND_GANESH
|
||||
sk_sp<SkSurface> surface = SkSurfaces::RenderTarget(drawP.world.main.window.ctx.get(), skgpu::Budgeted::kNo, imgInfo, drawP.world.main.window.defaultMSAASampleCount, &drawP.world.main.window.defaultMSAASurfaceProps);
|
||||
#endif
|
||||
if(!surface) {
|
||||
Logger::get().log("INFO", "[SkSurfaces::WrapBackendRenderTarget] Screenshot Tool could not make surface");
|
||||
return;
|
||||
}
|
||||
SkCanvas* screenshotCanvas = surface->getCanvas();
|
||||
if(!screenshotCanvas) {
|
||||
Logger::get().log("INFO", "Screenshot Tool could not make canvas");
|
||||
return;
|
||||
}
|
||||
|
||||
for(int i = 0; i < controls.imageSize.x(); i += drawP.world.main.window.size.x())
|
||||
for(int j = 0; j < controls.imageSize.y(); j += drawP.world.main.window.size.y())
|
||||
take_screenshot_area_hw(surface, screenshotCanvas, finalImgRawData.data(), controls.imageSize, Vector2i{i, j}, Vector2i{std::min(drawP.world.main.window.size.x(), controls.imageSize.x() - i), std::min(drawP.world.main.window.size.y(), controls.imageSize.y() - j)}, drawP.world.main.window.size, extType != 0 && controls.transparentBackground);
|
||||
|
||||
bool success = false;
|
||||
#ifdef __EMSCRIPTEN__
|
||||
SkDynamicMemoryWStream out;
|
||||
#else
|
||||
SkFILEWStream out((const char*)filePath.c_str());
|
||||
#endif
|
||||
|
||||
if(extType == 0)
|
||||
success = SkJpegEncoder::Encode(&out, finalImgData, {});
|
||||
else if(extType == 1)
|
||||
success = SkPngEncoder::Encode(&out, finalImgData, {});
|
||||
else if(extType == 2)
|
||||
success = SkWebpEncoder::Encode(&out, finalImgData, {});
|
||||
if(!success) {
|
||||
Logger::get().log("INFO", "[ScreenshotTool::take_screenshot] Could not encode and write screenshot");
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
if(success) {
|
||||
std::string mimeTypeArray[] = {"image/jpeg", "image/png", "image/webp"};
|
||||
auto skData = out.detachAsData();
|
||||
emscripten_browser_file::download(
|
||||
"screenshot" + controls.typeSelections[extType],
|
||||
mimeTypeArray[extType],
|
||||
std::string_view((const char*)skData->bytes(), skData->size())
|
||||
);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ScreenshotTool::take_screenshot_area_hw(const sk_sp<SkSurface>& surface, SkCanvas* canvas, void* fullImgRawData, const Vector2i& fullImageSize, const Vector2i& sectionImagePos, const Vector2i& sectionImageSize, const Vector2i& canvasSize, bool transparentBackground) {
|
||||
//std::cout << "[ScreenshotTool::take_screenshot_area_hw] Screenshot Pos: " << "[" << sectionImagePos.x() << " " << sectionImagePos.y() << "]" << std::endl;
|
||||
|
||||
float secRectX1 = controls.rectX1 + (controls.rectX2 - controls.rectX1) * (sectionImagePos.x() / (double)fullImageSize.x());
|
||||
float secRectX2 = controls.rectX1 + (controls.rectX2 - controls.rectX1) * ((sectionImagePos.x() + canvasSize.x()) / (double)fullImageSize.x());
|
||||
float secRectY1 = controls.rectY1 + (controls.rectY2 - controls.rectY1) * (sectionImagePos.y() / (double)fullImageSize.y());
|
||||
float secRectY2 = controls.rectY1 + (controls.rectY2 - controls.rectY1) * ((sectionImagePos.y() + canvasSize.y()) / (double)fullImageSize.y());
|
||||
|
||||
DrawData d = drawP.world.drawData;
|
||||
WorldVec topLeft = controls.coords.from_space({secRectX1, secRectY1});
|
||||
WorldVec topRight = controls.coords.from_space({secRectX2, secRectY1});
|
||||
WorldVec bottomLeft = controls.coords.from_space({secRectX1, secRectY2});
|
||||
WorldVec bottomRight = controls.coords.from_space({secRectX2, secRectY2});
|
||||
WorldVec camCenter = (topLeft + bottomRight) / WorldScalar(2);
|
||||
|
||||
WorldVec vectorZoom;
|
||||
WorldScalar distX = (camCenter - (topLeft + bottomLeft) * WorldScalar(0.5)).norm();
|
||||
WorldScalar distY = (camCenter - (topLeft + topRight) * WorldScalar(0.5)).norm();
|
||||
vectorZoom.x() = distX / WorldScalar(canvasSize.x() * 0.5);
|
||||
vectorZoom.y() = distY / WorldScalar(canvasSize.y() * 0.5);
|
||||
WorldScalar newInverseScale = (vectorZoom.x() + vectorZoom.y()) * WorldScalar(0.5);
|
||||
|
||||
drawP.world.drawData.cam.set_based_on_properties(drawP.world, topLeft, newInverseScale, controls.coords.rotation);
|
||||
|
||||
GridManager::GridType oldGridType = drawP.world.main.grid.gridType;
|
||||
drawP.world.main.takingScreenshot = true;
|
||||
drawP.world.main.transparentBackground = transparentBackground;
|
||||
if(!controls.displayGrid)
|
||||
drawP.world.main.grid.gridType = GridManager::GRIDTYPE_NONE;
|
||||
drawP.world.drawData.dontUseDrawProgCache = true;
|
||||
|
||||
drawP.world.main.draw(canvas);
|
||||
drawP.world.main.takingScreenshot = false;
|
||||
drawP.world.main.transparentBackground = false;
|
||||
drawP.world.main.grid.gridType = oldGridType;
|
||||
drawP.world.drawData = d;
|
||||
drawP.world.drawData.cam.changed();
|
||||
|
||||
SkImageInfo aaImgInfo = SkImageInfo::Make(sectionImageSize.x(), sectionImageSize.y(), kRGBA_8888_SkColorType, kPremul_SkAlphaType);
|
||||
void* fullImgRawDataStartPt = (uint8_t*)fullImgRawData + 4 * (size_t)sectionImagePos.x() + 4 * (size_t)fullImageSize.x() * (size_t)sectionImagePos.y();
|
||||
SkPixmap aaImgData(aaImgInfo, fullImgRawDataStartPt, fullImageSize.x() * 4);
|
||||
if(!surface->readPixels(aaImgData, 0, 0))
|
||||
throw std::runtime_error("[ScreenshotTool::take_screenshot_area_hw] Error copy pixmap");
|
||||
}
|
||||
|
||||
void ScreenshotTool::reset_tool() {
|
||||
controls.selectionMode = 0;
|
||||
}
|
||||
|
||||
void ScreenshotTool::tool_update() {
|
||||
if(controls.setToTakeScreenshot) {
|
||||
controls.setToTakeScreenshot = false;
|
||||
take_screenshot(controls.screenshotSavePath);
|
||||
}
|
||||
switch(controls.selectionMode) {
|
||||
case 0: {
|
||||
if(drawP.controls.leftClick) {
|
||||
controls.rectX1 = controls.rectX2 = drawP.world.main.input.mouse.pos.x();
|
||||
controls.rectY1 = controls.rectY2 = drawP.world.main.input.mouse.pos.y();
|
||||
controls.coords = drawP.world.drawData.cam.c;
|
||||
controls.dragType = 0;
|
||||
controls.selectionMode = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
Vector2f curPos = controls.coords.get_mouse_pos(drawP.world);
|
||||
float oldX1 = controls.rectX1;
|
||||
float oldX2 = controls.rectX2;
|
||||
float oldY1 = controls.rectY1;
|
||||
float oldY2 = controls.rectY2;
|
||||
switch(controls.dragType) {
|
||||
case 0:
|
||||
controls.rectX2 = curPos.x();
|
||||
controls.rectY2 = curPos.y();
|
||||
break;
|
||||
case 1:
|
||||
controls.rectY2 = curPos.y();
|
||||
break;
|
||||
case 2:
|
||||
controls.rectX1 = curPos.x();
|
||||
controls.rectY2 = curPos.y();
|
||||
break;
|
||||
case 3:
|
||||
controls.rectX1 = curPos.x();
|
||||
break;
|
||||
case 4:
|
||||
controls.rectX1 = curPos.x();
|
||||
controls.rectY1 = curPos.y();
|
||||
break;
|
||||
case 5:
|
||||
controls.rectY1 = curPos.y();
|
||||
break;
|
||||
case 6:
|
||||
controls.rectX2 = curPos.x();
|
||||
controls.rectY1 = curPos.y();
|
||||
break;
|
||||
case 7:
|
||||
controls.rectX2 = curPos.x();
|
||||
break;
|
||||
}
|
||||
if(std::fabs(controls.rectX1 - controls.rectX2) > 1e10 || std::fabs(controls.rectY1 - controls.rectY2) > 1e10) { // Limit before screenshot tool starts breaking
|
||||
controls.rectX1 = oldX1;
|
||||
controls.rectX2 = oldX2;
|
||||
controls.rectY1 = oldY1;
|
||||
controls.rectY2 = oldY2;
|
||||
}
|
||||
if((drawP.world.drawData.cam.c.inverseScale << 8) < controls.coords.inverseScale || (drawP.world.drawData.cam.c.inverseScale >> 15) > controls.coords.inverseScale) {
|
||||
controls.selectionMode = 0;
|
||||
break;
|
||||
}
|
||||
if(!drawP.controls.leftClickHeld) {
|
||||
controls.selectionMode = 2;
|
||||
commit_rect();
|
||||
controls.imageSize.x() = 1000.0;
|
||||
controls.imageSize.y() = controls.imageSize.x() * (controls.rectY2 - controls.rectY1) / (controls.rectX2 - controls.rectX1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
commit_rect();
|
||||
controls.dragType = -1;
|
||||
|
||||
if((drawP.world.drawData.cam.c.inverseScale << 8) < controls.coords.inverseScale || (drawP.world.drawData.cam.c.inverseScale >> 15) > controls.coords.inverseScale) {
|
||||
controls.selectionMode = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if(drawP.controls.cursorHoveringOverCanvas)
|
||||
for(int i = 0; i < 8; i++)
|
||||
if(SCollision::collide(controls.circles[i], drawP.world.main.input.mouse.pos))
|
||||
controls.dragType = i;
|
||||
|
||||
if(drawP.controls.leftClick) {
|
||||
if(controls.dragType == -1)
|
||||
controls.selectionMode = 0;
|
||||
else
|
||||
controls.selectionMode = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenshotTool::commit_rect() {
|
||||
float x1 = controls.rectX1;
|
||||
float x2 = controls.rectX2;
|
||||
float y1 = controls.rectY1;
|
||||
float y2 = controls.rectY2;
|
||||
controls.rectX1 = std::min(x1, x2);
|
||||
controls.rectX2 = std::max(x1, x2);
|
||||
controls.rectY1 = std::min(y1, y2);
|
||||
controls.rectY2 = std::max(y1, y2);
|
||||
|
||||
controls.circles[0] = {drawP.world.drawData.cam.c.to_space(controls.coords.from_space({controls.rectX2 , controls.rectY2})), DRAG_POINT_RADIUS};
|
||||
controls.circles[1] = {drawP.world.drawData.cam.c.to_space(controls.coords.from_space({(controls.rectX1 + controls.rectX2) * 0.5, controls.rectY2})), DRAG_POINT_RADIUS};
|
||||
controls.circles[2] = {drawP.world.drawData.cam.c.to_space(controls.coords.from_space({controls.rectX1 , controls.rectY2})), DRAG_POINT_RADIUS};
|
||||
controls.circles[3] = {drawP.world.drawData.cam.c.to_space(controls.coords.from_space({controls.rectX1, (controls.rectY1 + controls.rectY2) * 0.5})), DRAG_POINT_RADIUS};
|
||||
controls.circles[4] = {drawP.world.drawData.cam.c.to_space(controls.coords.from_space({controls.rectX1 , controls.rectY1})), DRAG_POINT_RADIUS};
|
||||
controls.circles[5] = {drawP.world.drawData.cam.c.to_space(controls.coords.from_space({(controls.rectX1 + controls.rectX2) * 0.5, controls.rectY1})), DRAG_POINT_RADIUS};
|
||||
controls.circles[6] = {drawP.world.drawData.cam.c.to_space(controls.coords.from_space({controls.rectX2 , controls.rectY1})), DRAG_POINT_RADIUS};
|
||||
controls.circles[7] = {drawP.world.drawData.cam.c.to_space(controls.coords.from_space({controls.rectX2, (controls.rectY1 + controls.rectY2) * 0.5})), DRAG_POINT_RADIUS};
|
||||
}
|
||||
|
||||
void ScreenshotTool::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
if(controls.selectionMode == 1 || controls.selectionMode == 2) {
|
||||
canvas->save();
|
||||
controls.coords.transform_sk_canvas(canvas, drawData);
|
||||
float x1 = std::min(controls.rectX1, controls.rectX2);
|
||||
float x2 = std::max(controls.rectX1, controls.rectX2);
|
||||
float y1 = std::min(controls.rectY1, controls.rectY2);
|
||||
float y2 = std::max(controls.rectY1, controls.rectY2);
|
||||
SkPoint p1{x1, y1};
|
||||
SkPoint p2{x2, y2};
|
||||
SkPath path1;
|
||||
path1.addRect(p1.x(), p1.y(), p2.x(), p2.y());
|
||||
path1.setFillType(SkPathFillType::kInverseWinding);
|
||||
SkPaint paint1;
|
||||
paint1.setColor4f({0.0f, 0.0f, 0.0f, 0.3f});
|
||||
canvas->drawPath(path1, paint1);
|
||||
|
||||
path1.setFillType(SkPathFillType::kWinding);
|
||||
SkPaint paint2;
|
||||
paint2.setColor4f({0.0f, 0.0f, 0.0f, 0.9f});
|
||||
paint2.setStyle(SkPaint::kStroke_Style);
|
||||
canvas->drawPath(path1, paint2);
|
||||
|
||||
canvas->restore();
|
||||
|
||||
if(controls.selectionMode == 2) {
|
||||
paint2.setStyle(SkPaint::kFill_Style);
|
||||
for(auto& circ : controls.circles)
|
||||
drawP.draw_drag_circle(canvas, circ.pos, {0.1f, 0.9f, 0.9f, 1.0f}, drawData, 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/DrawingProgram/ScreenshotTool.hpp
Normal file
56
src/DrawingProgram/ScreenshotTool.hpp
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
#pragma once
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include "../DrawData.hpp"
|
||||
#include <Helpers/SCollision.hpp>
|
||||
#include <filesystem>
|
||||
#include "../InputManager.hpp"
|
||||
#include "../CoordSpaceHelper.hpp"
|
||||
|
||||
class DrawingProgram;
|
||||
|
||||
class ScreenshotTool {
|
||||
public:
|
||||
ScreenshotTool(DrawingProgram& initDrawP);
|
||||
void gui_toolbox();
|
||||
void tool_update();
|
||||
void draw(SkCanvas* canvas, const DrawData& drawData);
|
||||
void reset_tool();
|
||||
private:
|
||||
void commit_rect();
|
||||
void take_screenshot(const std::filesystem::path& filePath);
|
||||
void take_screenshot_area_hw(const sk_sp<SkSurface>& surface, SkCanvas* canvas, void* fullImgRawData, const Vector2i& fullImageSize, const Vector2i& sectionImagePos, const Vector2i& sectionImageSize, const Vector2i& canvasSize, bool transparentBackground);
|
||||
|
||||
//static constexpr std::array<InputManager::SystemCursorType, 8> cursors{
|
||||
// InputManager::SystemCursorType::SE_RESIZE,
|
||||
// InputManager::SystemCursorType::S_RESIZE,
|
||||
// InputManager::SystemCursorType::SW_RESIZE,
|
||||
// InputManager::SystemCursorType::W_RESIZE,
|
||||
// InputManager::SystemCursorType::NW_RESIZE,
|
||||
// InputManager::SystemCursorType::N_RESIZE,
|
||||
// InputManager::SystemCursorType::NE_RESIZE,
|
||||
// InputManager::SystemCursorType::E_RESIZE
|
||||
//};
|
||||
|
||||
struct ScreenshotControls {
|
||||
CoordSpaceHelper coords;
|
||||
float rectX1;
|
||||
float rectX2;
|
||||
float rectY1;
|
||||
float rectY2;
|
||||
int selectionMode = 0;
|
||||
int dragType = 0;
|
||||
Vector2i imageSize = {0, 0};
|
||||
std::array<SCollision::Circle<float>, 8> circles;
|
||||
|
||||
bool displayGrid = true;
|
||||
bool transparentBackground = false;
|
||||
|
||||
size_t selectedType = 0;
|
||||
std::vector<std::string> typeSelections = {".jpg", ".png", ".webp"};
|
||||
|
||||
std::atomic<bool> setToTakeScreenshot = false;
|
||||
std::filesystem::path screenshotSavePath;
|
||||
} controls;
|
||||
|
||||
DrawingProgram& drawP;
|
||||
};
|
||||
292
src/DrawingProgram/TextBoxTool.cpp
Normal file
292
src/DrawingProgram/TextBoxTool.cpp
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
#include "TextBoxTool.hpp"
|
||||
#include <chrono>
|
||||
#include "DrawingProgram.hpp"
|
||||
#include "../Server/CommandList.hpp"
|
||||
#include "../MainProgram.hpp"
|
||||
#include "../DrawData.hpp"
|
||||
#include "Helpers/MathExtras.hpp"
|
||||
#include "Helpers/SCollision.hpp"
|
||||
#include "Helpers/Serializers.hpp"
|
||||
#include "../CollabTextBox/CollabTextBox.hpp"
|
||||
#include "../SharedTypes.hpp"
|
||||
#include <cereal/types/vector.hpp>
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
|
||||
TextBoxTool::TextBoxTool(DrawingProgram& initDrawP):
|
||||
drawP(initDrawP)
|
||||
{
|
||||
}
|
||||
|
||||
void TextBoxTool::gui_toolbox() {
|
||||
Toolbar& t = drawP.world.main.toolbar;
|
||||
t.gui.push_id("textbox tool");
|
||||
t.gui.text_label_centered("Textbox");
|
||||
t.gui.slider_scalar_field("Text Relative Size", "Text Size", &controls.textRelativeSize, 5.0f, 100.0f);
|
||||
t.gui.pop_id();
|
||||
}
|
||||
|
||||
void TextBoxTool::reset_tool() {
|
||||
commit();
|
||||
controls.drawStage = 0;
|
||||
}
|
||||
|
||||
bool TextBoxTool::edit_gui(const std::shared_ptr<DrawTextBox>& a) {
|
||||
DrawTextBox::Data oldData = a->d;
|
||||
Toolbar& t = drawP.world.main.toolbar;
|
||||
t.gui.push_id("edit tool text");
|
||||
t.gui.text_label_centered("Edit Text");
|
||||
t.gui.slider_scalar_field("Text Relative Size", "Text Size", &a->d.textSize, 5.0f, 100.0f);
|
||||
t.gui.left_to_right_line_layout([&]() {
|
||||
CLAY({.layout = {.sizing = {.width = CLAY_SIZING_FIXED(40), .height = CLAY_SIZING_FIXED(40)}}}) {
|
||||
if(t.gui.color_button("Text Color", &a->d.textColor, &a->d.textColor == t.colorRight))
|
||||
t.color_selector_right(&a->d.textColor == t.colorRight ? nullptr : &a->d.textColor);
|
||||
}
|
||||
t.gui.text_label("Text Color");
|
||||
});
|
||||
t.gui.pop_id();
|
||||
return a->d != oldData;
|
||||
}
|
||||
|
||||
void TextBoxTool::commit_edit_updates(const std::shared_ptr<DrawTextBox>& a, std::any& prevData) {
|
||||
a->d.editing = false;
|
||||
DrawTextBox::Data pData = std::any_cast<DrawTextBox::Data>(prevData);
|
||||
DrawTextBox::Data cData = a->d;
|
||||
drawP.world.undo.push(UndoManager::UndoRedoPair{
|
||||
[&, a, pData]() {
|
||||
a->d = pData;
|
||||
a->set_textbox_string(pData.currentText);
|
||||
a->d.editing = false;
|
||||
ServerClientID compID = drawP.components.get_id(a);
|
||||
if(compID != ServerClientID{0, 0})
|
||||
a->client_send_update_final(drawP, compID);
|
||||
a->final_update(drawP);
|
||||
drawP.reset_tools();
|
||||
return true;
|
||||
},
|
||||
[&, a, cData]() {
|
||||
ServerClientID compID = drawP.components.get_id(a);
|
||||
a->d = cData;
|
||||
a->set_textbox_string(cData.currentText);
|
||||
a->d.editing = false;
|
||||
if(compID != ServerClientID{0, 0})
|
||||
a->client_send_update_final(drawP, compID);
|
||||
a->final_update(drawP);
|
||||
drawP.reset_tools();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void TextBoxTool::edit_start(const std::shared_ptr<DrawTextBox>& a, std::any& prevData) {
|
||||
auto& cur = a->d.cursor;
|
||||
auto& textbox = a->textBox;
|
||||
Vector2f textSelectPos = a->get_mouse_pos(drawP);
|
||||
SkIPoint p = convert_vec2<SkIPoint>(textSelectPos.cast<int32_t>());
|
||||
cur.pos = cur.selectionBeginPos = cur.selectionEndPos = textbox.getPosition(p);
|
||||
prevData = a->d;
|
||||
a->d.editing = true;
|
||||
a->client_send_update_temp(drawP, drawP.components.get_id(a));
|
||||
drawP.editTool.add_point_handle({&a->d.p1, nullptr, &a->d.p2});
|
||||
drawP.editTool.add_point_handle({&a->d.p2, &a->d.p1, nullptr});
|
||||
}
|
||||
|
||||
bool TextBoxTool::edit_update(const std::shared_ptr<DrawTextBox>& a) {
|
||||
SCollision::ColliderCollection<float> mousePointCollection;
|
||||
mousePointCollection.circle.emplace_back(drawP.world.main.input.mouse.pos, 1.0f);
|
||||
mousePointCollection.recalculate_bounds();
|
||||
|
||||
a->textBox.setWidth(std::abs(a->d.p2.x() - a->d.p1.x()));
|
||||
a->create_collider(drawP.colliderAllocated);
|
||||
|
||||
InputManager& input = drawP.world.main.input;
|
||||
auto& textbox = a->textBox;
|
||||
auto& cur = a->d.cursor;
|
||||
|
||||
bool moved = false;
|
||||
bool movedHeld = false;
|
||||
|
||||
bool collidesWithBox = false;
|
||||
if(a->collides_with_cam_coords(drawP.world.drawData.cam.c, mousePointCollection, drawP.colliderAllocated)) {
|
||||
//drawP.world.main.input.cursorIcon = InputManager::SystemCursorType::TEXT;
|
||||
collidesWithBox = true;
|
||||
}
|
||||
|
||||
if(drawP.controls.leftClick) {
|
||||
if(collidesWithBox) {
|
||||
Vector2f textSelectPos = a->get_mouse_pos(drawP);
|
||||
SkIPoint p = convert_vec2<SkIPoint>(textSelectPos.cast<int32_t>());
|
||||
cur.pos = cur.selectionBeginPos = textbox.getPosition(p);
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
else if(drawP.controls.leftClickHeld) {
|
||||
if(collidesWithBox) {
|
||||
Vector2f textSelectPos = a->get_mouse_pos(drawP);
|
||||
SkIPoint p = convert_vec2<SkIPoint>(textSelectPos.cast<int32_t>());
|
||||
cur.pos = cur.selectionBeginPos = textbox.getPosition(p);
|
||||
}
|
||||
movedHeld = true;
|
||||
}
|
||||
|
||||
|
||||
if(input.key(InputManager::KEY_DRAW_UNSELECT).repeat)
|
||||
reset_tool();
|
||||
|
||||
if(input.key(InputManager::KEY_TEXT_LEFT).repeat)
|
||||
cur.pos = cur.selectionBeginPos = textbox.move(CollabTextBox::Movement::kLeft, cur.selectionBeginPos);
|
||||
if(input.key(InputManager::KEY_TEXT_RIGHT).repeat)
|
||||
cur.pos = cur.selectionBeginPos = textbox.move(CollabTextBox::Movement::kRight, cur.selectionBeginPos);
|
||||
if(input.key(InputManager::KEY_TEXT_UP).repeat)
|
||||
cur.pos = cur.selectionBeginPos = textbox.move(CollabTextBox::Movement::kUp, cur.selectionBeginPos);
|
||||
if(input.key(InputManager::KEY_TEXT_DOWN).repeat)
|
||||
cur.pos = cur.selectionBeginPos = textbox.move(CollabTextBox::Movement::kDown, cur.selectionBeginPos);
|
||||
if(input.key(InputManager::KEY_TEXT_HOME).repeat)
|
||||
cur.pos = cur.selectionBeginPos = textbox.move(CollabTextBox::Movement::kHome, cur.selectionBeginPos);
|
||||
|
||||
moved = moved || input.key(InputManager::KEY_TEXT_DOWN).repeat || input.key(InputManager::KEY_TEXT_UP).repeat || input.key(InputManager::KEY_TEXT_RIGHT).repeat || input.key(InputManager::KEY_TEXT_LEFT).repeat || input.key(InputManager::KEY_TEXT_HOME).repeat;
|
||||
if(moved && !input.key(InputManager::KEY_TEXT_SHIFT).held && !movedHeld)
|
||||
cur.selectionEndPos = cur.selectionBeginPos;
|
||||
|
||||
if(moved || movedHeld)
|
||||
update_textbox_network(a);
|
||||
|
||||
if(input.key(InputManager::KEY_TEXT_BACKSPACE).repeat) {
|
||||
edit_text([&]() {
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
else
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.pos, textbox.move(CollabTextBox::Movement::kLeft, cur.pos));
|
||||
}, a);
|
||||
}
|
||||
if(input.key(InputManager::KEY_TEXT_DELETE).repeat) {
|
||||
edit_text([&]() {
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
else
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.pos, textbox.move(CollabTextBox::Movement::kRight, cur.pos));
|
||||
}, a);
|
||||
}
|
||||
if(input.get_clipboard_paste_happened()) {
|
||||
edit_text([&]() {
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
std::string clipboard = input.get_clipboard_str();
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.insert(cur.pos, clipboard);
|
||||
}, a);
|
||||
}
|
||||
if(input.key(InputManager::KEY_TEXT_ENTER).repeat) {
|
||||
edit_text([&]() {
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
cur.pos = textbox.insert(cur.pos, "\n");
|
||||
cur.pos.fParagraphIndex++;
|
||||
cur.pos.fTextByteIndex = 0;
|
||||
cur.selectionBeginPos = cur.selectionEndPos = cur.pos;
|
||||
}, a);
|
||||
}
|
||||
if(input.key(InputManager::KEY_TEXT_COPY).repeat) {
|
||||
std::string clipboardData = textbox.copy(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
input.set_clipboard_str(clipboardData);
|
||||
}
|
||||
if(input.key(InputManager::KEY_TEXT_CUT).repeat) {
|
||||
edit_text([&]() {
|
||||
std::string clipboardData = textbox.copy(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
input.set_clipboard_str(clipboardData);
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
}, a);
|
||||
}
|
||||
if(input.key(InputManager::KEY_TEXT_SELECTALL).repeat) {
|
||||
cur.selectionEndPos.fParagraphIndex = textbox.lineCount() == 0 ? 0 : textbox.lineCount() - 1;
|
||||
cur.selectionEndPos.fTextByteIndex = textbox.line(cur.selectionEndPos.fParagraphIndex).size();
|
||||
cur.pos = cur.selectionEndPos;
|
||||
cur.selectionBeginPos.fTextByteIndex = 0;
|
||||
cur.selectionBeginPos.fParagraphIndex = 0;
|
||||
update_textbox_network(a);
|
||||
}
|
||||
if(!input.text.newInput.empty()) {
|
||||
edit_text([&]() {
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.insert(cur.pos, input.text.newInput);
|
||||
}, a);
|
||||
}
|
||||
|
||||
input.text.set_accepting_input();
|
||||
return true;
|
||||
}
|
||||
|
||||
void TextBoxTool::tool_update() {
|
||||
switch(controls.drawStage) {
|
||||
case 0: {
|
||||
if(drawP.controls.leftClick) {
|
||||
controls.intermediateItem = std::make_shared<DrawTextBox>();
|
||||
controls.startAt = drawP.world.main.input.mouse.pos;
|
||||
controls.endAt = controls.startAt;
|
||||
controls.intermediateItem->d.p1 = controls.intermediateItem->d.p2 = controls.startAt;
|
||||
controls.intermediateItem->d.textColor = drawP.controls.foregroundColor;
|
||||
controls.intermediateItem->d.textSize = controls.textRelativeSize;
|
||||
controls.intermediateItem->d.editing = true;
|
||||
controls.intermediateItem->coords = drawP.world.drawData.cam.c;
|
||||
controls.intermediateItem->lastUpdateTime = std::chrono::steady_clock::now();
|
||||
uint64_t placement = drawP.components.client_list().size();
|
||||
drawP.components.client_insert(placement, controls.intermediateItem);
|
||||
controls.intermediateItem->client_send_place(drawP, placement);
|
||||
drawP.add_undo_place_component(placement, controls.intermediateItem);
|
||||
controls.drawStage = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
Vector2f newPos = controls.intermediateItem->coords.get_mouse_pos(drawP.world);
|
||||
if(drawP.world.main.input.key(InputManager::KEY_EQUAL_DIMENSIONS).held) {
|
||||
float height = std::fabs(controls.startAt.y() - newPos.y());
|
||||
newPos.x() = controls.startAt.x() + (((newPos.x() - controls.startAt.x()) < 0.0f ? -1.0f : 1.0f) * height);
|
||||
}
|
||||
controls.endAt = newPos;
|
||||
controls.intermediateItem->d.p1 = cwise_vec_min(controls.endAt, controls.startAt);
|
||||
controls.intermediateItem->d.p2 = cwise_vec_max(controls.endAt, controls.startAt);
|
||||
controls.intermediateItem->lastUpdateTime = std::chrono::steady_clock::now();
|
||||
if(!drawP.controls.leftClickHeld) {
|
||||
auto i = controls.intermediateItem;
|
||||
commit();
|
||||
drawP.reset_tools();
|
||||
drawP.controls.selectedTool = DrawingProgram::TOOL_EDIT;
|
||||
drawP.controls.previousSelected = DrawingProgram::TOOL_EDIT;
|
||||
drawP.editTool.edit_start(i);
|
||||
}
|
||||
else {
|
||||
controls.intermediateItem->client_send_update_temp(drawP, drawP.components.get_id(controls.intermediateItem));
|
||||
controls.intermediateItem->temp_update(drawP);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Passing a function in, just in case we have to do something before editing the text
|
||||
void TextBoxTool::edit_text(std::function<void()> toRun, const std::shared_ptr<DrawTextBox>& textBox) {
|
||||
toRun();
|
||||
textBox->update_contained_string();
|
||||
update_textbox_network(textBox);
|
||||
}
|
||||
|
||||
void TextBoxTool::update_textbox_network(const std::shared_ptr<DrawTextBox>& textBox) {
|
||||
textBox->client_send_update_temp(drawP, drawP.components.get_id(textBox));
|
||||
textBox->updateDraw = true;
|
||||
}
|
||||
|
||||
void TextBoxTool::commit() {
|
||||
if(controls.intermediateItem) {
|
||||
controls.intermediateItem->client_send_update_final(drawP, drawP.components.get_id(controls.intermediateItem));
|
||||
controls.intermediateItem->final_update(drawP);
|
||||
//controls.intermediateItem->currentlyUpdating = true; // It remains true, as we are about to start writing into the text box we just placed
|
||||
controls.intermediateItem = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void TextBoxTool::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
if(controls.intermediateItem)
|
||||
controls.intermediateItem->draw(canvas, drawData);
|
||||
}
|
||||
36
src/DrawingProgram/TextBoxTool.hpp
Normal file
36
src/DrawingProgram/TextBoxTool.hpp
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include "../DrawData.hpp"
|
||||
#include "../DrawComponents/DrawTextBox.hpp"
|
||||
#include <Helpers/SCollision.hpp>
|
||||
#include <any>
|
||||
|
||||
class DrawingProgram;
|
||||
|
||||
class TextBoxTool {
|
||||
public:
|
||||
TextBoxTool(DrawingProgram& initDrawP);
|
||||
void gui_toolbox();
|
||||
void tool_update();
|
||||
void draw(SkCanvas* canvas, const DrawData& drawData);
|
||||
void reset_tool();
|
||||
|
||||
void edit_start(const std::shared_ptr<DrawTextBox>& a, std::any& prevData);
|
||||
void commit_edit_updates(const std::shared_ptr<DrawTextBox>& a, std::any& prevData);
|
||||
bool edit_update(const std::shared_ptr<DrawTextBox>& a);
|
||||
bool edit_gui(const std::shared_ptr<DrawTextBox>& a);
|
||||
private:
|
||||
void edit_text(std::function<void()> toRun, const std::shared_ptr<DrawTextBox>& textBox);
|
||||
void update_textbox_network(const std::shared_ptr<DrawTextBox>& textBox);
|
||||
void commit();
|
||||
|
||||
struct TextBoxControls {
|
||||
float textRelativeSize = 20.0f;
|
||||
Vector2f startAt;
|
||||
Vector2f endAt;
|
||||
int drawStage = 0;
|
||||
std::shared_ptr<DrawTextBox> intermediateItem;
|
||||
} controls;
|
||||
|
||||
DrawingProgram& drawP;
|
||||
};
|
||||
24
src/FileHelpers.cpp
Normal file
24
src/FileHelpers.cpp
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#include "FileHelpers.hpp"
|
||||
#include <Helpers/StringHelpers.hpp>
|
||||
#include <algorithm>
|
||||
|
||||
std::filesystem::path force_extension_on_path(std::filesystem::path p, const std::string& extensions) {
|
||||
std::vector<std::string> extensionList = split_string_by_token(extensions, ";");
|
||||
for(std::string& s : extensionList) {
|
||||
std::transform(s.begin(), s.end(), s.begin(), ::tolower);
|
||||
s.insert(0, ".");
|
||||
}
|
||||
|
||||
|
||||
std::string fExtension;
|
||||
if(p.has_extension()) {
|
||||
fExtension = p.extension().string();
|
||||
std::transform(fExtension.begin(), fExtension.end(), fExtension.begin(), ::tolower);
|
||||
}
|
||||
|
||||
if(std::ranges::contains(extensionList, fExtension))
|
||||
return p;
|
||||
|
||||
p += extensionList[0];
|
||||
return p;
|
||||
}
|
||||
4
src/FileHelpers.hpp
Normal file
4
src/FileHelpers.hpp
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#pragma once
|
||||
#include <filesystem>
|
||||
|
||||
std::filesystem::path force_extension_on_path(std::filesystem::path p, const std::string& extensions);
|
||||
22
src/FontData.cpp
Normal file
22
src/FontData.cpp
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#include "FontData.hpp"
|
||||
//#include <include/ports/SkFontMgr_directory.h>
|
||||
#include <include/ports/SkFontMgr_empty.h>
|
||||
#include <include/core/SkFontMetrics.h>
|
||||
#include <include/core/SkTextBlob.h>
|
||||
#include <iostream>
|
||||
|
||||
FontData::FontData():
|
||||
mgr(SkFontMgr_New_Custom_Empty())
|
||||
{
|
||||
map["Roboto"] = mgr->makeFromFile("data/fonts/Roboto-variable.ttf");
|
||||
}
|
||||
|
||||
FontData::~FontData() {
|
||||
}
|
||||
|
||||
Vector2f get_str_font_bounds(const SkFont& font, const std::string& str) {
|
||||
SkFontMetrics metrics;
|
||||
font.getMetrics(&metrics);
|
||||
float nextText = font.measureText(str.data(), str.length(), SkTextEncoding::kUTF8, nullptr);
|
||||
return Vector2f{nextText, - metrics.fAscent + metrics.fDescent};
|
||||
}
|
||||
17
src/FontData.hpp
Normal file
17
src/FontData.hpp
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#pragma once
|
||||
#include <include/core/SkFontMgr.h>
|
||||
#include <include/core/SkTypeface.h>
|
||||
#include <include/core/SkFont.h>
|
||||
#include <unordered_map>
|
||||
#include <Eigen/Dense>
|
||||
|
||||
using namespace Eigen;
|
||||
|
||||
struct FontData {
|
||||
FontData();
|
||||
sk_sp<SkFontMgr> mgr;
|
||||
std::unordered_map<std::string, sk_sp<SkTypeface>> map;
|
||||
~FontData();
|
||||
};
|
||||
|
||||
Vector2f get_str_font_bounds(const SkFont& font, const std::string& str);
|
||||
2
src/GUIStuff/ClayImplement.cpp
Normal file
2
src/GUIStuff/ClayImplement.cpp
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
#define CLAY_IMPLEMENTATION
|
||||
#include <clay.h>
|
||||
90
src/GUIStuff/Elements/CheckBox.cpp
Normal file
90
src/GUIStuff/Elements/CheckBox.cpp
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
#include "CheckBox.hpp"
|
||||
#include <include/core/SkPath.h>
|
||||
#include <iostream>
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
void CheckBox::update(UpdateInputData& io, bool newIsTicked, const std::function<void()>& elemUpdate) {
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_FIXED(20), .height = CLAY_SIZING_FIXED(20)}
|
||||
},
|
||||
.custom = { .customData = this }
|
||||
}) {
|
||||
selection.update(Clay_Hovered(), io.mouse.leftClick, io.mouse.leftHeld);
|
||||
if(isTicked != newIsTicked) {
|
||||
hoverAnimation2 = CHECKBOX_ANIMATION_TIME;
|
||||
isTicked = newIsTicked;
|
||||
}
|
||||
if(elemUpdate)
|
||||
elemUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void CheckBox::clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) {
|
||||
auto bb = get_bb(command);
|
||||
|
||||
canvas->save();
|
||||
canvas->translate(bb.min.x(), bb.min.y());
|
||||
canvas->scale(bb.width(), bb.height());
|
||||
canvas->translate(0.5f, 0.5f);
|
||||
|
||||
SkPaint p;
|
||||
if(isTicked) {
|
||||
SkRect checkBox = SkRect::MakeLTRB(-0.5f, -0.5f, 0.5f, 0.5f);
|
||||
p.setColor4f(convert_vec4<SkColor4f>(io.theme->fillColor1));
|
||||
p.setStyle(SkPaint::kFill_Style);
|
||||
canvas->drawRoundRect(checkBox, 0.25f, 0.25f, p);
|
||||
}
|
||||
else {
|
||||
SkRect checkBox = SkRect::MakeLTRB(-0.45f, -0.45f, 0.45f, 0.45f);
|
||||
p.setColor4f(convert_vec4<SkColor4f>(selection.hovered ? io.theme->fillColor1 : io.theme->backColor3));
|
||||
p.setStyle(SkPaint::kStroke_Style);
|
||||
p.setStrokeWidth(0.15f);
|
||||
canvas->drawRoundRect(checkBox, 0.25f, 0.25f, p);
|
||||
}
|
||||
|
||||
|
||||
if(isTicked) {
|
||||
SkPaint checkP;
|
||||
checkP.setColor4f(io.theme->backColor2);
|
||||
checkP.setStyle(SkPaint::kFill_Style);
|
||||
checkP.setStrokeWidth(0.12);
|
||||
checkP.setStrokeCap(SkPaint::kRound_Cap);
|
||||
checkP.setStrokeJoin(SkPaint::kRound_Join);
|
||||
|
||||
SkPath checkPath;
|
||||
Vector2f checkP1{(4.5/17.0) - 0.5, (8.5/17.0) - 0.5};
|
||||
Vector2f checkP2{(7.5/17.0) - 0.5, (12.0/17.0) - 0.5};
|
||||
Vector2f checkP3{(12.5/17.0) - 0.5, (6.0/17.0) - 0.5};
|
||||
|
||||
Vector2f rectP1{-0.2f, -0.2f};
|
||||
Vector2f rectP2{-0.2f, 0.2f};
|
||||
Vector2f rectP3{ 0.2f, 0.2f};
|
||||
Vector2f rectP4{ 0.2f, -0.2f};
|
||||
|
||||
static BezierEasing anim{0.445, -0.733, 0.575, 1.627};
|
||||
|
||||
float lerpTime2 = anim(smooth_two_way_time(hoverAnimation2, io.deltaTime, selection.hovered, CHECKBOX_ANIMATION_TIME));
|
||||
|
||||
std::array<Vector2f, 5> points;
|
||||
|
||||
points[0] = lerp_vec(checkP1, rectP1, lerpTime2);
|
||||
points[1] = lerp_vec(checkP2, rectP2, lerpTime2);
|
||||
points[2] = lerp_vec(checkP3, rectP3, lerpTime2);
|
||||
points[3] = lerp_vec(checkP2, rectP4, lerpTime2);
|
||||
points[4] = points[0];
|
||||
|
||||
checkPath.moveTo(points[0].x(), points[0].y());
|
||||
for(unsigned i = 1; i < 5; i++)
|
||||
checkPath.lineTo(points[i].x(), points[i].y());
|
||||
checkPath.close();
|
||||
|
||||
canvas->drawPath(checkPath, checkP);
|
||||
checkP.setStyle(SkPaint::kStroke_Style);
|
||||
canvas->drawPath(checkPath, checkP);
|
||||
}
|
||||
canvas->restore();
|
||||
}
|
||||
|
||||
}
|
||||
17
src/GUIStuff/Elements/CheckBox.hpp
Normal file
17
src/GUIStuff/Elements/CheckBox.hpp
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#pragma once
|
||||
#include "Element.hpp"
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
class CheckBox : public Element {
|
||||
public:
|
||||
void update(UpdateInputData& io, bool newIsTicked, const std::function<void()>& elemUpdate);
|
||||
virtual void clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) override;
|
||||
SelectionHelper selection;
|
||||
private:
|
||||
static constexpr float CHECKBOX_ANIMATION_TIME = 0.3;
|
||||
bool isTicked = false;
|
||||
float hoverAnimation2 = 0.0;
|
||||
};
|
||||
|
||||
}
|
||||
285
src/GUIStuff/Elements/ColorPicker.hpp
Normal file
285
src/GUIStuff/Elements/ColorPicker.hpp
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
#pragma once
|
||||
#include "Element.hpp"
|
||||
#include <include/core/SkColor.h>
|
||||
#include <include/core/SkPath.h>
|
||||
#include <include/effects/SkRuntimeEffect.h>
|
||||
#include <iostream>
|
||||
#include <Helpers/SCollision.hpp>
|
||||
#include <Helpers/HsvRgb.hpp>
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
template <typename T> class ColorPicker : public Element {
|
||||
public:
|
||||
void update(UpdateInputData& io, T* newData, bool newSelectAlpha, const std::function<void()>& elemUpdate) {
|
||||
data = newData;
|
||||
selectAlpha = newSelectAlpha;
|
||||
|
||||
if(!data)
|
||||
return;
|
||||
|
||||
force_update_colorpicker();
|
||||
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(bb.width()) }
|
||||
},
|
||||
.custom = { .customData = this }
|
||||
}) {
|
||||
selection.update(Clay_Hovered(), io.mouse.leftClick, io.mouse.leftHeld);
|
||||
if(selection.clicked && data) {
|
||||
float svSelectionAreaSize = get_sv_selection_area_size();
|
||||
modifyingSv = SCollision::collide(SCollision::AABB<float>(bb.min, bb.min + Vector2f{svSelectionAreaSize, svSelectionAreaSize}), io.mouse.pos);
|
||||
Vector2f hueBarPos = get_hue_bar_pos();
|
||||
Vector2f hueBarDim = get_hue_bar_dim();
|
||||
modifyingHue = SCollision::collide(SCollision::AABB<float>(hueBarPos, hueBarPos + hueBarDim), io.mouse.pos);
|
||||
if(selectAlpha) {
|
||||
Vector2f alphaBarPos = get_alpha_bar_pos();
|
||||
Vector2f alphaBarDim = get_alpha_bar_dim();
|
||||
modifyingAlpha = SCollision::collide(SCollision::AABB<float>(alphaBarPos, alphaBarPos + alphaBarDim), io.mouse.pos);
|
||||
}
|
||||
}
|
||||
if(selection.held && modifyingSv) {
|
||||
float svSelectionAreaSize = bb.width() - (BAR_GAP + BAR_WIDTH);
|
||||
Vector2f newSv = cwise_vec_clamp<Vector2f>((io.mouse.pos - bb.min) / svSelectionAreaSize, Vector2f{0.0f, 0.0f}, Vector2f{1.0f, 1.0f});
|
||||
savedHsv.y() = newSv.x();
|
||||
savedHsv.z() = 1.0f - newSv.y();
|
||||
set_hsv(savedHsv);
|
||||
}
|
||||
if(selection.held && modifyingHue) {
|
||||
Vector2f huePos = get_hue_bar_pos();
|
||||
Vector2f hueDim = get_hue_bar_dim();
|
||||
savedHsv.x() = (1.0f - std::clamp((io.mouse.pos.y() - huePos.y()) / hueDim.y(), 0.0f, 1.0f)) * 360.0f;
|
||||
set_hsv(savedHsv);
|
||||
}
|
||||
if(selection.held && modifyingAlpha) {
|
||||
Vector2f alphaPos = get_alpha_bar_pos();
|
||||
Vector2f alphaDim = get_alpha_bar_dim();
|
||||
(*data)[3] = std::clamp((io.mouse.pos.x() - alphaPos.x()) / alphaDim.x(), 0.0f, 1.0f);
|
||||
oldData = *data;
|
||||
}
|
||||
if(!selection.held) {
|
||||
modifyingSv = false;
|
||||
modifyingHue = false;
|
||||
modifyingAlpha = false;
|
||||
}
|
||||
if(elemUpdate)
|
||||
elemUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
virtual void clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) {
|
||||
if(!data)
|
||||
return;
|
||||
|
||||
bb = get_bb(command);
|
||||
|
||||
canvas->save();
|
||||
canvas->translate(bb.min.x(), bb.min.y());
|
||||
float svSelectionAreaSize = get_sv_selection_area_size();
|
||||
canvas->scale(svSelectionAreaSize, svSelectionAreaSize);
|
||||
canvas->clipRect(SkRect::MakeXYWH(0.0f, 0.0f, 1.0f, 1.0f));
|
||||
|
||||
SkPaint svSelectionAreaPaint;
|
||||
svSelectionAreaPaint.setShader(get_sv_selection_shader(savedHsv.x() / 360.0f));
|
||||
canvas->drawPaint(svSelectionAreaPaint);
|
||||
|
||||
SkPaint selectionLinePaint({1.0f, 1.0f, 1.0f, 1.0f});
|
||||
selectionLinePaint.setStrokeWidth(2.0f / svSelectionAreaSize);
|
||||
canvas->drawLine(0.0f, 1.0f - savedHsv.z(), 1.0f, 1.0f - savedHsv.z(), selectionLinePaint);
|
||||
canvas->drawLine(savedHsv.y(), 0.0f, savedHsv.y(), 1.0f, selectionLinePaint);
|
||||
|
||||
canvas->restore();
|
||||
|
||||
float normalizedHue = savedHsv.x() / 360.0f;
|
||||
|
||||
Vector2f hueBarPos = get_hue_bar_pos();
|
||||
Vector2f hueBarDim = get_hue_bar_dim();
|
||||
canvas->save();
|
||||
canvas->translate(hueBarPos.x(), hueBarPos.y());
|
||||
canvas->scale(hueBarDim.x(), hueBarDim.y());
|
||||
canvas->clipRect(SkRect::MakeXYWH(0.0f, 0.0f, 1.0f, 1.0f));
|
||||
|
||||
SkPaint hueBarPaint;
|
||||
hueBarPaint.setShader(get_hue_shader());
|
||||
canvas->drawPaint(hueBarPaint);
|
||||
canvas->drawLine(0.0f, 1.0f - normalizedHue, 1.0f, 1.0f - normalizedHue, selectionLinePaint);
|
||||
|
||||
canvas->restore();
|
||||
|
||||
Vector2f alphaBarPos = get_alpha_bar_pos();
|
||||
Vector2f alphaBarDim = get_alpha_bar_dim();
|
||||
canvas->save();
|
||||
canvas->translate(alphaBarPos.x(), alphaBarPos.y());
|
||||
canvas->clipRect(SkRect::MakeXYWH(0.0f, 0.0f, alphaBarDim.x(), alphaBarDim.y()));
|
||||
|
||||
SkPaint alphaBarPaint;
|
||||
if(selectAlpha)
|
||||
alphaBarPaint.setShader(get_alpha_bar_shader({(*data)[0], (*data)[1], (*data)[2]}, alphaBarDim.x()));
|
||||
else
|
||||
alphaBarPaint.setColor4f(SkColor4f{(*data)[0], (*data)[1], (*data)[2], 1.0f});
|
||||
canvas->drawPaint(alphaBarPaint);
|
||||
|
||||
if(selectAlpha) {
|
||||
canvas->scale(alphaBarDim.x(), alphaBarDim.y());
|
||||
canvas->drawLine((*data)[3], 0.0f, (*data)[3], 1.0f, selectionLinePaint);
|
||||
}
|
||||
|
||||
canvas->restore();
|
||||
}
|
||||
private:
|
||||
void force_update_colorpicker() {
|
||||
if(data && (*data == oldData))
|
||||
return;
|
||||
|
||||
if(data) {
|
||||
savedHsv = rgb_to_hsv<Vector3f>(*data);
|
||||
oldData = *data;
|
||||
}
|
||||
else
|
||||
oldData = T{0, 0, 0, 0};
|
||||
}
|
||||
|
||||
float get_sv_selection_area_size() {
|
||||
return bb.width() - (BAR_GAP + BAR_WIDTH);
|
||||
}
|
||||
|
||||
Vector2f get_hue_bar_pos() {
|
||||
return {bb.min.x() + get_sv_selection_area_size() + BAR_GAP, bb.min.y()};
|
||||
}
|
||||
|
||||
Vector2f get_hue_bar_dim() {
|
||||
return {BAR_WIDTH, get_sv_selection_area_size()};
|
||||
}
|
||||
|
||||
Vector2f get_alpha_bar_pos() {
|
||||
return {bb.min.x(), bb.min.y() + get_sv_selection_area_size() + BAR_GAP};
|
||||
}
|
||||
|
||||
Vector2f get_alpha_bar_dim() {
|
||||
return {bb.width(), BAR_WIDTH};
|
||||
}
|
||||
|
||||
void set_hsv(const Vector3f& hsv) {
|
||||
Vector3f a = hsv_to_rgb<Vector3f>(hsv);
|
||||
(*data)[0] = a.x();
|
||||
(*data)[1] = a.y();
|
||||
(*data)[2] = a.z();
|
||||
oldData = *data;
|
||||
}
|
||||
|
||||
static constexpr float BAR_WIDTH = 30.0;
|
||||
static constexpr float BAR_GAP = 5.0;
|
||||
|
||||
// HSV to RGB func from: https://github.com/hughsk/glsl-hsv2rgb?tab=readme-ov-file
|
||||
static constexpr const char* hueBarSkSl =
|
||||
R"V(
|
||||
vec3 hsv2rgb(vec3 c) {
|
||||
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
||||
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
||||
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
||||
}
|
||||
|
||||
vec4 main(vec2 fragCoord) {
|
||||
vec3 a = hsv2rgb(vec3(1.0 - fragCoord.y, 1.0, 1.0));
|
||||
return vec4(a.xyz, 1.0);
|
||||
})V";
|
||||
|
||||
static constexpr const char* svSelectionAreaSkSl =
|
||||
R"V(uniform float hue;
|
||||
vec3 hsv2rgb(vec3 c) {
|
||||
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
||||
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
||||
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
||||
}
|
||||
|
||||
vec4 main(vec2 fragCoord) {
|
||||
vec3 a = hsv2rgb(vec3(hue, fragCoord.x, 1.0 - fragCoord.y));
|
||||
return vec4(a.xyz, 1.0);
|
||||
})V";
|
||||
|
||||
static constexpr const char* alphaBarSksl =
|
||||
R"V(uniform vec3 mainColor;
|
||||
uniform float horizontalResolution;
|
||||
|
||||
vec4 main(vec2 fragcoord) {
|
||||
vec4 alphaBoard1 = vec4(0.2, 0.2, 0.2, 1.0);
|
||||
vec4 alphaBoard2 = vec4(0.8, 0.8, 0.8, 1.0);
|
||||
|
||||
vec2 flooredCoords = floor(fragcoord / 10);
|
||||
bool isDark = mod(flooredCoords.x + flooredCoords.y, 2.0) > 0.5;
|
||||
|
||||
return mix(isDark ? alphaBoard2 : alphaBoard1, vec4(mainColor, 1.0), fragcoord.x / horizontalResolution);
|
||||
}
|
||||
)V";
|
||||
|
||||
static sk_sp<SkShader> hueBarShader;
|
||||
static sk_sp<SkRuntimeEffect> svSelectionAreaEffect;
|
||||
static sk_sp<SkRuntimeEffect> alphaBarEffect;
|
||||
|
||||
static sk_sp<SkShader> get_hue_shader();
|
||||
static sk_sp<SkShader> get_sv_selection_shader(float hue);
|
||||
static sk_sp<SkShader> get_alpha_bar_shader(const Vector3f& mainColor, float horizontalResolution);
|
||||
|
||||
SCollision::AABB<float> bb;
|
||||
SelectionHelper selection;
|
||||
T* data = nullptr;
|
||||
T oldData;
|
||||
Vector3f savedHsv; // We save the HSV so that conversion doesnt ruin the UI
|
||||
bool selectAlpha;
|
||||
bool modifyingSv = false;
|
||||
bool modifyingHue = false;
|
||||
bool modifyingAlpha = false;
|
||||
};
|
||||
|
||||
template <typename T> sk_sp<SkShader> ColorPicker<T>::hueBarShader;
|
||||
template <typename T> sk_sp<SkRuntimeEffect> ColorPicker<T>::svSelectionAreaEffect;
|
||||
template <typename T> sk_sp<SkRuntimeEffect> ColorPicker<T>::alphaBarEffect;
|
||||
|
||||
template <typename T> sk_sp<SkShader> ColorPicker<T>::get_hue_shader(){
|
||||
if(!hueBarShader) {
|
||||
auto [effect, err] = SkRuntimeEffect::MakeForShader(SkString(hueBarSkSl));
|
||||
if(!err.isEmpty()) {
|
||||
std::cout << "[ColorPicker::get_hue_shader] Hue shader construction error\n";
|
||||
std::cout << err.c_str() << std::endl;
|
||||
}
|
||||
else
|
||||
hueBarShader = effect->makeShader(nullptr, {nullptr, 0});
|
||||
}
|
||||
return hueBarShader;
|
||||
}
|
||||
|
||||
template <typename T> sk_sp<SkShader> ColorPicker<T>::get_sv_selection_shader(float hue) {
|
||||
if(!svSelectionAreaEffect) {
|
||||
auto [effect, err] = SkRuntimeEffect::MakeForShader(SkString(svSelectionAreaSkSl));
|
||||
if(!err.isEmpty()) {
|
||||
std::cout << "[ColorPicker::get_sv_selection_shader] SV Selection shader construction error\n";
|
||||
std::cout << err.c_str() << std::endl;
|
||||
return nullptr;
|
||||
}
|
||||
else
|
||||
svSelectionAreaEffect = effect;
|
||||
}
|
||||
SkRuntimeShaderBuilder builder(svSelectionAreaEffect);
|
||||
builder.uniform("hue") = hue;
|
||||
return builder.makeShader();
|
||||
}
|
||||
|
||||
template <typename T> sk_sp<SkShader> ColorPicker<T>::get_alpha_bar_shader(const Vector3f& mainColor, float horizontalResolution) {
|
||||
if(!alphaBarEffect) {
|
||||
auto [effect, err] = SkRuntimeEffect::MakeForShader(SkString(alphaBarSksl));
|
||||
if(!err.isEmpty()) {
|
||||
std::cout << "[ColorPicker::get_alpha_bar_shader] Alpha bar shader construction error\n";
|
||||
std::cout << err.c_str() << std::endl;
|
||||
return nullptr;
|
||||
}
|
||||
else
|
||||
alphaBarEffect = effect;
|
||||
}
|
||||
SkRuntimeShaderBuilder builder(alphaBarEffect);
|
||||
builder.uniform("mainColor") = SkV3{mainColor.x(), mainColor.y(), mainColor.z()};
|
||||
builder.uniform("horizontalResolution") = horizontalResolution;
|
||||
return builder.makeShader();
|
||||
}
|
||||
|
||||
}
|
||||
63
src/GUIStuff/Elements/Element.cpp
Normal file
63
src/GUIStuff/Elements/Element.cpp
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
#include "Element.hpp"
|
||||
|
||||
namespace GUIStuff {
|
||||
SkFont get_setup_skfont() {
|
||||
SkFont font;
|
||||
font.setLinearMetrics(true);
|
||||
font.setHinting(SkFontHinting::kNormal);
|
||||
//font.setForceAutoHinting(true);
|
||||
font.setSubpixel(true);
|
||||
font.setBaselineSnap(true);
|
||||
font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
|
||||
//paint.setAntiAlias(true);
|
||||
return font;
|
||||
}
|
||||
|
||||
SkFont UpdateInputData::get_font(float fSize) const {
|
||||
SkFont f = get_setup_skfont();
|
||||
f.setTypeface(textTypeface);
|
||||
f.setSize(fSize);
|
||||
return f;
|
||||
}
|
||||
|
||||
std::shared_ptr<Theme> get_default_dark_mode() {
|
||||
std::shared_ptr<Theme> theme(std::make_shared<Theme>());
|
||||
theme->fillColor1 = {0.1f, 0.4f, 0.8f, 1.0f};
|
||||
theme->fillColor2 = {0.4f, 0.5f, 0.9f, 1.0f};
|
||||
theme->fillColor3 = {1.0f, 0.0f, 0.0f, 1.0f};
|
||||
theme->fillColor4 = {1.0f, 1.0f, 0.0f, 1.0f};
|
||||
theme->backColor1 = {0.0f, 0.0f, 0.0f, 0.9f};
|
||||
theme->backColor2 = {0.15f, 0.15f, 0.15f, 1.0f};
|
||||
theme->backColor3 = {0.22f, 0.22f, 0.22f, 1.0f};
|
||||
theme->frontColor1 = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||
theme->frontColor2 = {0.8f, 0.8f, 0.8f, 1.0f};
|
||||
return theme;
|
||||
}
|
||||
|
||||
SCollision::AABB<float> Element::get_bb(Clay_RenderCommand* command) {
|
||||
return {{command->boundingBox.x, command->boundingBox.y}, {command->boundingBox.x + command->boundingBox.width, command->boundingBox.y + command->boundingBox.height}};
|
||||
}
|
||||
|
||||
void SelectionHelper::update(bool isHovering, bool isLeftClick, bool isLeftHeld) {
|
||||
hovered = isHovering;
|
||||
|
||||
clicked = false;
|
||||
justUnselected = false;
|
||||
bool oldSelected = selected;
|
||||
|
||||
if(isLeftClick) {
|
||||
if(hovered) {
|
||||
selected = true;
|
||||
clicked = true;
|
||||
held = true;
|
||||
}
|
||||
else
|
||||
selected = false;
|
||||
}
|
||||
else if(!isLeftHeld)
|
||||
held = false;
|
||||
|
||||
if(!selected && oldSelected)
|
||||
justUnselected = true;
|
||||
}
|
||||
}
|
||||
138
src/GUIStuff/Elements/Element.hpp
Normal file
138
src/GUIStuff/Elements/Element.hpp
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
#pragma once
|
||||
#include "Helpers/ConvertVec.hpp"
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include "../../TimePoint.hpp"
|
||||
#include <Eigen/Dense>
|
||||
#include <include/core/SkFont.h>
|
||||
#include <include/core/SkFontMgr.h>
|
||||
#include <modules/svg/include/SkSVGDOM.h>
|
||||
#include <Helpers/SCollision.hpp>
|
||||
#include <Helpers/Serializers.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using namespace Eigen;
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
// Taken from https://github.com/TimothyHoytBSME/ClayMan (rewritten to be a separate struct, and use templates to change size)
|
||||
template <size_t S> class StringArena {
|
||||
public:
|
||||
StringArena() {
|
||||
stringArena = std::vector<char>(S);
|
||||
}
|
||||
void reset() {
|
||||
stringArenaIndex = 0;
|
||||
}
|
||||
const char* insert_string_into_arena(const std::string& str) {
|
||||
size_t strSize = str.size();
|
||||
if(stringArenaIndex + strSize + 1 > S)
|
||||
throw std::overflow_error("[StringArena::insert_string_into_arena] Not enough space to insert string");
|
||||
char* startPtr = &stringArena[stringArenaIndex];
|
||||
for(size_t i = 0; i < strSize; i++)
|
||||
stringArena[stringArenaIndex++] = str[i];
|
||||
stringArena[stringArenaIndex++] = ' ';
|
||||
return startPtr;
|
||||
}
|
||||
Clay_String std_str_to_clay_str(const std::string& str) {
|
||||
return Clay_String{.length = static_cast<int32_t>(str.size()), .chars = insert_string_into_arena(str)};
|
||||
}
|
||||
private:
|
||||
std::vector<char> stringArena;
|
||||
size_t stringArenaIndex;
|
||||
};
|
||||
|
||||
typedef StringArena<4000000> DefaultStringArena;
|
||||
|
||||
SkFont get_setup_skfont();
|
||||
|
||||
struct Theme {
|
||||
SkColor4f fillColor1 = {0.5f, 0.6f, 1.0f, 1.0f};
|
||||
SkColor4f fillColor2 = {0.4f, 0.5f, 0.9f, 1.0f};
|
||||
SkColor4f fillColor3 = {1.0f, 0.0f, 0.0f, 1.0f};
|
||||
SkColor4f fillColor4 = {1.0f, 1.0f, 0.0f, 1.0f};
|
||||
SkColor4f backColor1 = {1.0f, 1.0f, 1.0f, 0.9f};
|
||||
SkColor4f backColor2 = {0.95f, 0.95f, 0.95f, 1.0f};
|
||||
SkColor4f backColor3 = {0.9f, 0.9f, 0.9f, 1.0f};
|
||||
SkColor4f frontColor1 = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
SkColor4f frontColor2 = {0.1f, 0.1f, 0.1f, 1.0f};
|
||||
|
||||
float hoverExpandTime = 0.1f;
|
||||
uint16_t childGap1 = 10;
|
||||
uint16_t padding1 = 10;
|
||||
float windowCorners1 = 10;
|
||||
uint16_t windowBorders1 = 2;
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Theme, fillColor1, fillColor2, fillColor3, fillColor4, backColor1, backColor2, backColor3, frontColor1, frontColor2, hoverExpandTime, childGap1, padding1, windowCorners1, windowBorders1)
|
||||
};
|
||||
|
||||
std::shared_ptr<Theme> get_default_dark_mode();
|
||||
|
||||
struct UpdateInputData {
|
||||
struct {
|
||||
int leftClick = 0;
|
||||
int rightClick = 0;
|
||||
bool leftDoubleClick = false;
|
||||
bool leftHeld = false;
|
||||
bool rightHeld = false;
|
||||
Vector2f globalPos{0, 0};
|
||||
Vector2f pos{0, 0}; // Position relative to window
|
||||
Vector2f scroll;
|
||||
} mouse;
|
||||
|
||||
struct {
|
||||
bool left = false;
|
||||
bool right = false;
|
||||
bool up = false;
|
||||
bool down = false;
|
||||
bool leftShift = false;
|
||||
bool leftCtrl = false;
|
||||
bool home = false;
|
||||
bool del = false;
|
||||
bool backspace = false;
|
||||
bool enter = false;
|
||||
bool selectAll = false;
|
||||
bool copy = false;
|
||||
bool paste = false;
|
||||
bool cut = false;
|
||||
} key;
|
||||
|
||||
struct {
|
||||
std::function<std::string()> textInFunc;
|
||||
std::optional<std::string> textOut;
|
||||
} clipboard;
|
||||
|
||||
SkFont get_font(float fSize) const;
|
||||
|
||||
uint16_t fontSize = 20;
|
||||
sk_sp<SkTypeface> textTypeface;
|
||||
|
||||
std::string textInput;
|
||||
bool hoverObstructed = false;
|
||||
bool acceptingTextInput = false;
|
||||
float deltaTime = 0.0f;
|
||||
sk_sp<SkFontMgr> textFontMgr;
|
||||
DefaultStringArena* strArena;
|
||||
std::shared_ptr<Theme> theme;
|
||||
std::unordered_map<std::string, sk_sp<SkSVGDOM>> svgData;
|
||||
};
|
||||
|
||||
class SelectionHelper {
|
||||
public:
|
||||
void update(bool isHovering, bool isLeftClick, bool isLeftHeld);
|
||||
bool held = false;
|
||||
bool hovered = false;
|
||||
bool selected = false;
|
||||
bool clicked = false;
|
||||
bool justUnselected = false;
|
||||
};
|
||||
|
||||
class Element {
|
||||
public:
|
||||
virtual void clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) = 0;
|
||||
virtual ~Element() = default;
|
||||
|
||||
protected:
|
||||
static SCollision::AABB<float> get_bb(Clay_RenderCommand* command);
|
||||
};
|
||||
|
||||
}
|
||||
79
src/GUIStuff/Elements/MovableTabList.cpp
Normal file
79
src/GUIStuff/Elements/MovableTabList.cpp
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
#include "MovableTabList.hpp"
|
||||
#include "Helpers/ConvertVec.hpp"
|
||||
#include <iostream>
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
void MovableTabList::update(UpdateInputData& io, const std::vector<std::pair<std::string, std::string>>& tabNames, size_t& selectedTab, std::optional<size_t>& closedTab, const std::function<void()>& elemUpdate) {
|
||||
size_t currentSelectedTab = selectedTab;
|
||||
if(tabNames != oldTabNames) {
|
||||
buttons = std::vector<SelectableButton>(tabNames.size());
|
||||
closeButtons = std::vector<SelectableButton>(tabNames.size());
|
||||
closeIcons = std::vector<SVGIcon>(tabNames.size());
|
||||
tabIcons = std::vector<SVGIcon>(tabNames.size());
|
||||
oldTabNames = tabNames;
|
||||
}
|
||||
|
||||
CLAY({.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(45)},
|
||||
.padding = CLAY_PADDING_ALL(2),
|
||||
.childGap = 8,
|
||||
.childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER },
|
||||
.layoutDirection = CLAY_LEFT_TO_RIGHT
|
||||
},
|
||||
.clip = {.horizontal = true, .childOffset = Clay_GetScrollOffset()}
|
||||
}) {
|
||||
selection.update(Clay_Hovered(), io.mouse.leftClick, io.mouse.leftHeld);
|
||||
if(elemUpdate)
|
||||
elemUpdate();
|
||||
for(size_t i = 0; i < buttons.size(); i++) {
|
||||
CLAY({.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_FIT(250), .height = CLAY_SIZING_GROW(0)},
|
||||
}
|
||||
}) {
|
||||
buttons[i].update(io, true, true, [&](SelectionHelper& s, bool iS) {
|
||||
CLAY({.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)},
|
||||
.padding = CLAY_PADDING_ALL(4),
|
||||
.childGap = 4,
|
||||
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
|
||||
.layoutDirection = CLAY_LEFT_TO_RIGHT
|
||||
}
|
||||
}) {
|
||||
if(!tabNames[i].first.empty()) {
|
||||
CLAY({.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_FIXED(25), .height = CLAY_SIZING_FIXED(25)},
|
||||
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
|
||||
.layoutDirection = CLAY_LEFT_TO_RIGHT
|
||||
}
|
||||
}) {
|
||||
tabIcons[i].update(io, tabNames[i].first, iS, nullptr);
|
||||
}
|
||||
}
|
||||
CLAY_TEXT(io.strArena->std_str_to_clay_str(tabNames[i].second), CLAY_TEXT_CONFIG({.textColor = convert_vec4<Clay_Color>(io.theme->frontColor1), .fontSize = io.fontSize }));
|
||||
if(s.clicked)
|
||||
selectedTab = i;
|
||||
CLAY({.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}}}) {}
|
||||
CLAY({.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_FIXED(25), .height = CLAY_SIZING_FIXED(25)},
|
||||
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
|
||||
.layoutDirection = CLAY_LEFT_TO_RIGHT
|
||||
}
|
||||
}) {
|
||||
closeButtons[i].update(io, false, false, [&](SelectionHelper& s, bool iS) {
|
||||
if(s.clicked)
|
||||
closedTab = i;
|
||||
closeIcons[i].update(io, "data/icons/close.svg", s.held || s.hovered, nullptr);
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
}, currentSelectedTab == i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MovableTabList::clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) {
|
||||
}
|
||||
|
||||
}
|
||||
21
src/GUIStuff/Elements/MovableTabList.hpp
Normal file
21
src/GUIStuff/Elements/MovableTabList.hpp
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#pragma once
|
||||
#include "Element.hpp"
|
||||
#include "SelectableButton.hpp"
|
||||
#include "SVGIcon.hpp"
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
class MovableTabList : public Element {
|
||||
public:
|
||||
void update(UpdateInputData& io, const std::vector<std::pair<std::string, std::string>>& tabNames, size_t& selectedTab, std::optional<size_t>& closedTab, const std::function<void()>& elemUpdate);
|
||||
virtual void clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) override;
|
||||
SelectionHelper selection;
|
||||
private:
|
||||
std::vector<std::pair<std::string, std::string>> oldTabNames;
|
||||
std::vector<SelectableButton> buttons;
|
||||
std::vector<SelectableButton> closeButtons;
|
||||
std::vector<SVGIcon> closeIcons;
|
||||
std::vector<SVGIcon> tabIcons;
|
||||
};
|
||||
|
||||
}
|
||||
91
src/GUIStuff/Elements/NumberSlider.hpp
Normal file
91
src/GUIStuff/Elements/NumberSlider.hpp
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#pragma once
|
||||
#include "Element.hpp"
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
template <typename T> class NumberSlider : public Element {
|
||||
public:
|
||||
void update(UpdateInputData& io, T* dataPtr, T minNew, T maxNew, const std::function<void()>& elemUpdate) {
|
||||
data = dataPtr;
|
||||
min = minNew;
|
||||
max = maxNew;
|
||||
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(100), .height = CLAY_SIZING_FIXED(15)}
|
||||
},
|
||||
.custom = { .customData = this }
|
||||
}) {
|
||||
selection.update(Clay_Hovered(), io.mouse.leftClick, io.mouse.leftHeld);
|
||||
if(selection.held && data) {
|
||||
float fracPosOnSlider = (io.mouse.pos.x() - bb.min.x()) / bb.width();
|
||||
*data = std::clamp<T>(std::lerp<double>(min, max, fracPosOnSlider), min, max);
|
||||
}
|
||||
if(elemUpdate)
|
||||
elemUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
virtual void clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) override {
|
||||
if(!data)
|
||||
return;
|
||||
|
||||
bb = get_bb(command);
|
||||
|
||||
canvas->save();
|
||||
canvas->translate(bb.min.x(), bb.min.y());
|
||||
|
||||
float lerpTimeHover = smooth_two_way_time(hoverAnimation, io.deltaTime, selection.hovered, io.theme->hoverExpandTime);
|
||||
|
||||
//static BezierEasing easeHeight(0.75, 0.25, 0.25, 0.75);
|
||||
static BezierEasing easeHeight(0.68, -1.55, 0.265, 2.55);
|
||||
|
||||
float lerpTimeHeld = easeHeight(smooth_two_way_time(holdAnimation, io.deltaTime, selection.held, 0.3));
|
||||
|
||||
float holderRadius = lerp_vec(4.0, 5.0, lerpTimeHover);
|
||||
float holderHeight = lerp_vec(4.0, 10.0, lerpTimeHeld);
|
||||
|
||||
const float yChange = bb.height() * 0.5f - holderRadius * 0.5f;
|
||||
|
||||
float holderPos = lerp_time<float>(*data, max, min) * bb.width();
|
||||
|
||||
SkRect barFull = SkRect::MakeXYWH(0.0f, yChange, holderPos, holderRadius);
|
||||
SkRect barEmpty = SkRect::MakeXYWH(holderPos, yChange, bb.width() - holderPos, holderRadius);
|
||||
|
||||
SkPaint barFullP;
|
||||
barFullP.setColor(convert_vec4<SkColor4f>(io.theme->fillColor1));
|
||||
canvas->drawRoundRect(barFull, 5.0f, 5.0f, barFullP);
|
||||
|
||||
SkPaint barEmptyP;
|
||||
barEmptyP.setColor(convert_vec4<SkColor4f>(io.theme->backColor2));
|
||||
canvas->drawRoundRect(barEmpty, 5.0f, 5.0f, barEmptyP);
|
||||
|
||||
canvas->translate(holderPos, bb.height() * 0.5f);
|
||||
|
||||
SkRect holderRect = SkRect::MakeLTRB(-holderRadius, -holderHeight, holderRadius, holderHeight);
|
||||
SkPaint holderBorderP;
|
||||
holderBorderP.setStyle(SkPaint::kStroke_Style);
|
||||
holderBorderP.setStrokeWidth(3.0f);
|
||||
holderBorderP.setColor(convert_vec4<SkColor4f>(io.theme->fillColor1));
|
||||
canvas->drawRoundRect(holderRect, holderRadius, holderRadius, holderBorderP);
|
||||
|
||||
SkPaint holderP;
|
||||
holderP.setColor(convert_vec4<SkColor4f>(io.theme->backColor2));
|
||||
canvas->drawRoundRect(holderRect, holderRadius, holderRadius, holderP);
|
||||
|
||||
canvas->restore();
|
||||
}
|
||||
|
||||
private:
|
||||
SCollision::AABB<float> bb;
|
||||
SelectionHelper selection;
|
||||
|
||||
T* data = nullptr;
|
||||
T min = 0.0;
|
||||
T max = 1.0;
|
||||
|
||||
float hoverAnimation = 0.0;
|
||||
float holdAnimation = 0.0;
|
||||
};
|
||||
|
||||
}
|
||||
193
src/GUIStuff/Elements/PaintCircleMenu.cpp
Normal file
193
src/GUIStuff/Elements/PaintCircleMenu.cpp
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
#include "PaintCircleMenu.hpp"
|
||||
#include "Helpers/ConvertVec.hpp"
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
constexpr float ROTATE_START = 125.0f;
|
||||
constexpr float PALETTE_START = 100.0f;
|
||||
constexpr float CIRCLE_END = 150.0f;
|
||||
constexpr double ROTATE_BAR_SNAP_DISTANCE = 0.2;
|
||||
constexpr double ROTATE_BAR_SNAP_DISTRIBUTION = std::numbers::pi * 0.25;
|
||||
|
||||
void PaintCircleMenu::update(UpdateInputData& io, const Data& data, const std::function<void()>& elemUpdate) {
|
||||
CLAY({.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}
|
||||
},
|
||||
.custom = { .customData = this }
|
||||
}) {
|
||||
bool isHoveredNoObstruction = Clay_Hovered() && !io.hoverObstructed;
|
||||
Vector2f vecFromCenter = (io.mouse.pos - bb.center()).normalized();
|
||||
float distFromCenter = vec_distance(io.mouse.pos, bb.center());
|
||||
bool rotateHovered = false;
|
||||
if(data.newRotationAngle) {
|
||||
rotateBarSelect.update(isHoveredNoObstruction && distFromCenter > ROTATE_START && distFromCenter < CIRCLE_END, io.mouse.leftClick, io.mouse.leftHeld);
|
||||
if(rotateBarSelect.held) {
|
||||
double& newRotationAngle = *data.newRotationAngle;
|
||||
newRotationAngle = std::atan2(vecFromCenter.y(), -vecFromCenter.x()) + std::numbers::pi;
|
||||
if(rotateBarSelect.hovered) { // If we're hovering over the rotation bar, we should try snapping to specific angles
|
||||
for(double snapPos = 0.0; snapPos < std::numbers::pi * 2.0 + 0.001; snapPos += ROTATE_BAR_SNAP_DISTRIBUTION) {
|
||||
if(std::fabs(newRotationAngle - snapPos) < ROTATE_BAR_SNAP_DISTANCE) {
|
||||
newRotationAngle = snapPos;
|
||||
if(newRotationAngle >= std::numbers::pi * 2.0 - 0.001) // Set 2pi back to 0
|
||||
newRotationAngle = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
*data.newRotationAngle = data.currentRotationAngle;
|
||||
rotateHovered = rotateBarSelect.hovered;
|
||||
}
|
||||
bool colorHovered = false;
|
||||
if(!data.palette.empty() && data.selectedColor) {
|
||||
colorBarSelect.update(isHoveredNoObstruction && distFromCenter > PALETTE_START && distFromCenter < ROTATE_START, io.mouse.leftClick, io.mouse.leftHeld);
|
||||
if(colorBarSelect.hovered) {
|
||||
Vector4f& selectedColor = *data.selectedColor;
|
||||
double selectionAngle = std::atan2(-vecFromCenter.y(), -vecFromCenter.x()) + std::numbers::pi;
|
||||
colorSelectionIndex = (selectionAngle / (std::numbers::pi * 2.0)) * data.palette.size();
|
||||
colorSelectionIndex = std::clamp<unsigned>(colorSelectionIndex, 0, data.palette.size() - 1);
|
||||
if(colorBarSelect.clicked) {
|
||||
selectedColor.x() = data.palette[colorSelectionIndex].x();
|
||||
selectedColor.y() = data.palette[colorSelectionIndex].y();
|
||||
selectedColor.z() = data.palette[colorSelectionIndex].z();
|
||||
}
|
||||
}
|
||||
colorHovered = colorBarSelect.hovered;
|
||||
}
|
||||
if(rotateHovered || colorHovered)
|
||||
io.hoverObstructed = true;
|
||||
if(elemUpdate)
|
||||
elemUpdate();
|
||||
}
|
||||
|
||||
d = data;
|
||||
}
|
||||
|
||||
void PaintCircleMenu::clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) {
|
||||
bb = get_bb(command);
|
||||
canvas->save();
|
||||
canvas->translate(bb.center().x(), bb.center().y());
|
||||
|
||||
draw_rotate_bar(canvas, io);
|
||||
draw_palette_bar(canvas, io);
|
||||
|
||||
canvas->restore();
|
||||
}
|
||||
|
||||
void PaintCircleMenu::draw_rotate_bar(SkCanvas* canvas, UpdateInputData& io) {
|
||||
if(!d.newRotationAngle)
|
||||
return;
|
||||
|
||||
double rotationAngle = d.currentRotationAngle;
|
||||
float rotateBarMiddleRadius = (CIRCLE_END + ROTATE_START) / 2.0f;
|
||||
SkPaint rotateBarFill(io.theme->backColor2);
|
||||
rotateBarFill.setStroke(true);
|
||||
rotateBarFill.setStrokeWidth(CIRCLE_END - ROTATE_START);
|
||||
canvas->drawCircle(0.0f, 0.0f, rotateBarMiddleRadius, rotateBarFill);
|
||||
|
||||
if(rotateBarSelect.hovered) {
|
||||
for(double snapPos = 0.0; snapPos < std::numbers::pi * 2.0; snapPos += ROTATE_BAR_SNAP_DISTRIBUTION) {
|
||||
Vector2f lineDir{std::cos(snapPos), -std::sin(snapPos)};
|
||||
SkPaint p(snapPos == 0.0 ? io.theme->fillColor1 : io.theme->frontColor2);
|
||||
p.setStrokeWidth(2.0f);
|
||||
canvas->drawLine(lineDir.x() * ROTATE_START, lineDir.y() * ROTATE_START, lineDir.x() * CIRCLE_END, lineDir.y() * CIRCLE_END, p);
|
||||
}
|
||||
}
|
||||
|
||||
SkPaint rotateBarHolderFill;
|
||||
if(rotateBarSelect.held)
|
||||
rotateBarHolderFill.setColor4f(io.theme->fillColor1);
|
||||
else if(rotateBarSelect.hovered)
|
||||
rotateBarHolderFill.setColor4f(io.theme->fillColor2);
|
||||
else
|
||||
rotateBarHolderFill.setColor4f(io.theme->frontColor2);
|
||||
float rotateBarHolderRadius = (CIRCLE_END - ROTATE_START) / 2.0f;
|
||||
Vector2f rotateBarHolderPos{std::cos(rotationAngle) * rotateBarMiddleRadius, -std::sin(rotationAngle) * rotateBarMiddleRadius};
|
||||
canvas->drawCircle(rotateBarHolderPos.x(), rotateBarHolderPos.y(), rotateBarHolderRadius, rotateBarHolderFill);
|
||||
SkPaint rotateBarHolderOutline(io.theme->frontColor1);
|
||||
rotateBarHolderOutline.setStroke(true);
|
||||
rotateBarHolderOutline.setStrokeWidth(2.0f);
|
||||
canvas->drawCircle(rotateBarHolderPos.x(), rotateBarHolderPos.y(), rotateBarHolderRadius, rotateBarHolderOutline);
|
||||
|
||||
SkPaint rotateBarOutline(io.theme->frontColor2);
|
||||
rotateBarOutline.setStroke(true);
|
||||
rotateBarOutline.setStrokeWidth(2.0f);
|
||||
canvas->drawCircle(0.0f, 0.0f, ROTATE_START, rotateBarOutline);
|
||||
canvas->drawCircle(0.0f, 0.0f, CIRCLE_END, rotateBarOutline);
|
||||
}
|
||||
|
||||
void PaintCircleMenu::draw_palette_bar(SkCanvas* canvas, UpdateInputData& io) {
|
||||
if(d.palette.empty() || !d.selectedColor)
|
||||
return;
|
||||
|
||||
float middleRadius = (PALETTE_START + ROTATE_START) / 2.0f;
|
||||
|
||||
double colorDistribution = std::numbers::pi * 2.0 / d.palette.size();
|
||||
SkRect colorOval = SkRect::MakeLTRB(-middleRadius, -middleRadius, middleRadius, middleRadius);
|
||||
for(unsigned i = 0; i < d.palette.size(); i++) {
|
||||
double angleStart = i * colorDistribution;
|
||||
double angleEnd = (i + 1) * colorDistribution;
|
||||
|
||||
SkPaint colorP(SkColor4f{d.palette[i].x(), d.palette[i].y(), d.palette[i].z(), 1.0f});
|
||||
colorP.setStroke(true);
|
||||
colorP.setStrokeWidth(ROTATE_START - PALETTE_START);
|
||||
canvas->drawArc(colorOval, angleStart * 180.0 / std::numbers::pi, (angleEnd - angleStart) * 180.0 / std::numbers::pi, false, colorP);
|
||||
|
||||
Vector2f lineDir1{std::cos(angleStart), std::sin(angleStart)};
|
||||
SkPaint lineP(io.theme->frontColor2);
|
||||
lineP.setStrokeWidth(2.0f);
|
||||
lineP.setStroke(true);
|
||||
canvas->drawLine(lineDir1.x() * PALETTE_START, lineDir1.y() * PALETTE_START, lineDir1.x() * ROTATE_START, lineDir1.y() * ROTATE_START, lineP);
|
||||
}
|
||||
|
||||
SkPaint barOutline(io.theme->frontColor2);
|
||||
barOutline.setStroke(true);
|
||||
barOutline.setStrokeWidth(2.0f);
|
||||
canvas->drawCircle(0.0f, 0.0f, PALETTE_START, barOutline);
|
||||
canvas->drawCircle(0.0f, 0.0f, ROTATE_START, barOutline);
|
||||
|
||||
SkRect startOval = SkRect::MakeLTRB(-PALETTE_START, -PALETTE_START, PALETTE_START, PALETTE_START);
|
||||
SkRect endOval = SkRect::MakeLTRB(-ROTATE_START, -ROTATE_START, ROTATE_START, ROTATE_START);
|
||||
if(colorBarSelect.hovered) {
|
||||
SkPath selectionPath;
|
||||
double angleStart = colorSelectionIndex * colorDistribution;
|
||||
double angleEnd = (colorSelectionIndex + 1) * colorDistribution;
|
||||
Vector2f lineDir1{std::cos(angleStart), std::sin(angleStart)};
|
||||
Vector2f lineDir2{std::cos(angleEnd), std::sin(angleEnd)};
|
||||
SkPaint lineP(io.theme->fillColor2);
|
||||
lineP.setStrokeWidth(5.0f);
|
||||
lineP.setStroke(true);
|
||||
lineP.setStrokeJoin(SkPaint::kRound_Join);
|
||||
selectionPath.moveTo(lineDir1.x() * PALETTE_START, lineDir1.y() * PALETTE_START);
|
||||
selectionPath.lineTo(lineDir1.x() * ROTATE_START, lineDir1.y() * ROTATE_START);
|
||||
selectionPath.arcTo(endOval, angleStart * 180.0 / std::numbers::pi, (angleEnd - angleStart) * 180.0 / std::numbers::pi, false);
|
||||
selectionPath.lineTo(lineDir2.x() * PALETTE_START, lineDir2.y() * PALETTE_START);
|
||||
selectionPath.arcTo(startOval, angleEnd * 180.0 / std::numbers::pi, (angleStart - angleEnd) * 180.0 / std::numbers::pi, false);
|
||||
selectionPath.close();
|
||||
canvas->drawPath(selectionPath, lineP);
|
||||
}
|
||||
for(unsigned i = 0; i < d.palette.size(); i++) {
|
||||
if(d.palette[i].x() == d.selectedColor->x() && d.palette[i].y() == d.selectedColor->y() && d.palette[i].z() == d.selectedColor->z()) {
|
||||
SkPath selectionPath;
|
||||
double angleStart = i * colorDistribution;
|
||||
double angleEnd = (i + 1) * colorDistribution;
|
||||
Vector2f lineDir1{std::cos(angleStart), std::sin(angleStart)};
|
||||
Vector2f lineDir2{std::cos(angleEnd), std::sin(angleEnd)};
|
||||
SkPaint lineP(io.theme->fillColor1);
|
||||
lineP.setStrokeWidth(5.0f);
|
||||
lineP.setStroke(true);
|
||||
lineP.setStrokeJoin(SkPaint::kRound_Join);
|
||||
selectionPath.moveTo(lineDir1.x() * PALETTE_START, lineDir1.y() * PALETTE_START);
|
||||
selectionPath.lineTo(lineDir1.x() * ROTATE_START, lineDir1.y() * ROTATE_START);
|
||||
selectionPath.arcTo(endOval, angleStart * 180.0 / std::numbers::pi, (angleEnd - angleStart) * 180.0 / std::numbers::pi, false);
|
||||
selectionPath.lineTo(lineDir2.x() * PALETTE_START, lineDir2.y() * PALETTE_START);
|
||||
selectionPath.arcTo(startOval, angleEnd * 180.0 / std::numbers::pi, (angleStart - angleEnd) * 180.0 / std::numbers::pi, false);
|
||||
selectionPath.close();
|
||||
canvas->drawPath(selectionPath, lineP);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
26
src/GUIStuff/Elements/PaintCircleMenu.hpp
Normal file
26
src/GUIStuff/Elements/PaintCircleMenu.hpp
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#pragma once
|
||||
#include "Element.hpp"
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
class PaintCircleMenu : public Element {
|
||||
public:
|
||||
struct Data {
|
||||
double currentRotationAngle = 0.0;
|
||||
double* newRotationAngle = nullptr;
|
||||
Vector4f* selectedColor = nullptr;
|
||||
std::vector<Vector3f> palette;
|
||||
};
|
||||
void update(UpdateInputData& io, const Data& data, const std::function<void()>& elemUpdate);
|
||||
virtual void clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) override;
|
||||
private:
|
||||
void draw_rotate_bar(SkCanvas* canvas, UpdateInputData& io);
|
||||
void draw_palette_bar(SkCanvas* canvas, UpdateInputData& io);
|
||||
Data d;
|
||||
SCollision::AABB<float> bb;
|
||||
SelectionHelper rotateBarSelect;
|
||||
SelectionHelper colorBarSelect;
|
||||
unsigned colorSelectionIndex = 0;
|
||||
};
|
||||
|
||||
}
|
||||
57
src/GUIStuff/Elements/RadioButton.cpp
Normal file
57
src/GUIStuff/Elements/RadioButton.cpp
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#include "RadioButton.hpp"
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
void RadioButton::update(UpdateInputData& io, bool newIsTicked, const std::function<void()>& elemUpdate) {
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_FIXED(20), .height = CLAY_SIZING_FIXED(20)}
|
||||
},
|
||||
.custom = { .customData = this }
|
||||
}) {
|
||||
selection.update(Clay_Hovered(), io.mouse.leftClick, io.mouse.leftHeld);
|
||||
if(isTicked != newIsTicked) {
|
||||
hoverAnimation2 = RADIOBUTTON_ANIMATION_TIME;
|
||||
isTicked = newIsTicked;
|
||||
}
|
||||
if(elemUpdate)
|
||||
elemUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void RadioButton::clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) {
|
||||
auto bb = get_bb(command);
|
||||
|
||||
canvas->save();
|
||||
canvas->translate(bb.min.x(), bb.min.y());
|
||||
canvas->scale(bb.width(), bb.height());
|
||||
canvas->translate(0.5f, 0.5f);
|
||||
|
||||
if(isTicked) {
|
||||
SkPaint p;
|
||||
p.setColor4f(convert_vec4<SkColor4f>(io.theme->fillColor1));
|
||||
p.setStyle(SkPaint::kFill_Style);
|
||||
canvas->drawCircle(0.0f, 0.0f, 0.5f, p);
|
||||
|
||||
SkPaint innerCircleP;
|
||||
innerCircleP.setColor4f(convert_vec4<SkColor4f>(io.theme->backColor2));
|
||||
innerCircleP.setStyle(SkPaint::kFill_Style);
|
||||
|
||||
static BezierEasing easeRadius(0.68, -2.55, 0.265, 3.55);
|
||||
float lerpTime2 = easeRadius(smooth_two_way_time(hoverAnimation2, io.deltaTime, selection.hovered, RADIOBUTTON_ANIMATION_TIME));
|
||||
float innerCircleRadius = lerp_vec(0.3f, 0.2f, lerpTime2);
|
||||
|
||||
canvas->drawCircle(0.0f, 0.0f, innerCircleRadius, innerCircleP);
|
||||
}
|
||||
else {
|
||||
SkPaint p;
|
||||
p.setColor4f(convert_vec4<SkColor4f>(selection.hovered ? io.theme->fillColor1 : io.theme->backColor3));
|
||||
p.setStyle(SkPaint::kStroke_Style);
|
||||
p.setStrokeWidth(0.15f);
|
||||
canvas->drawCircle(0.0f, 0.0f, 0.5f, p);
|
||||
}
|
||||
|
||||
canvas->restore();
|
||||
}
|
||||
|
||||
}
|
||||
19
src/GUIStuff/Elements/RadioButton.hpp
Normal file
19
src/GUIStuff/Elements/RadioButton.hpp
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
#include "Element.hpp"
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
class RadioButton : public Element {
|
||||
public:
|
||||
void update(UpdateInputData& io, bool newIsTicked, const std::function<void()>& elemUpdate);
|
||||
virtual void clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) override;
|
||||
SelectionHelper selection;
|
||||
private:
|
||||
static constexpr float RADIOBUTTON_ANIMATION_TIME = 0.3;
|
||||
|
||||
bool isTicked = false;
|
||||
|
||||
float hoverAnimation2 = 0.0;
|
||||
};
|
||||
|
||||
}
|
||||
67
src/GUIStuff/Elements/SVGIcon.cpp
Normal file
67
src/GUIStuff/Elements/SVGIcon.cpp
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#include "SVGIcon.hpp"
|
||||
#include "Helpers/ConvertVec.hpp"
|
||||
#include <modules/svg/include/SkSVGNode.h>
|
||||
#include <include/core/SkStream.h>
|
||||
#include <include/core/SkColorFilter.h>
|
||||
#include <include/core/SkColorSpace.h>
|
||||
#include <iostream>
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
void SVGIcon::update(UpdateInputData& io, const std::string& newSvgPath, bool newIsHighlighted, const std::function<void()>& elemUpdate) {
|
||||
auto findSVGData = io.svgData.find(newSvgPath);
|
||||
if(findSVGData == io.svgData.end()) {
|
||||
auto stream = SkStream::MakeFromFile(newSvgPath.c_str());
|
||||
if(!stream) {
|
||||
std::cout << "[SVGIcon::update] Could not open file " << newSvgPath << std::endl;
|
||||
io.svgData[newSvgPath] = nullptr;
|
||||
svgDom = nullptr;
|
||||
}
|
||||
auto newSvgDom = SkSVGDOM::Builder().make(*stream);
|
||||
if(!newSvgDom) {
|
||||
io.svgData[newSvgPath] = nullptr;
|
||||
svgDom = nullptr;
|
||||
std::cout << "[SVGIcon::update] Could not parse SVG " << newSvgPath << std::endl;
|
||||
}
|
||||
else {
|
||||
io.svgData[newSvgPath] = newSvgDom;
|
||||
svgDom = newSvgDom;
|
||||
}
|
||||
}
|
||||
else
|
||||
svgDom = findSVGData->second;
|
||||
|
||||
highlighted = newIsHighlighted;
|
||||
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0) }
|
||||
},
|
||||
.custom = { .customData = this }
|
||||
}) {
|
||||
if(elemUpdate)
|
||||
elemUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void SVGIcon::clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) {
|
||||
if(!svgDom)
|
||||
return;
|
||||
|
||||
auto bb = get_bb(command);
|
||||
|
||||
canvas->save();
|
||||
canvas->translate(bb.min.x(), bb.min.y());
|
||||
SkRect r = SkRect::MakeXYWH(0.0f, 0.0f, bb.width(), bb.height());
|
||||
canvas->clipRect(r);
|
||||
SkPaint blendingPaint;
|
||||
blendingPaint.setColorFilter(SkColorFilters::Blend(highlighted ? io.theme->frontColor1 : io.theme->frontColor2, SkColorSpace::MakeSRGB(), SkBlendMode::kSrcIn));
|
||||
canvas->saveLayer(r, &blendingPaint);
|
||||
canvas->scale(bb.width() / svgDom->containerSize().width(), bb.height() / svgDom->containerSize().height());
|
||||
svgDom->render(canvas);
|
||||
canvas->restore();
|
||||
|
||||
canvas->restore();
|
||||
}
|
||||
|
||||
}
|
||||
15
src/GUIStuff/Elements/SVGIcon.hpp
Normal file
15
src/GUIStuff/Elements/SVGIcon.hpp
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#pragma once
|
||||
#include "Element.hpp"
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
class SVGIcon : public Element {
|
||||
public:
|
||||
void update(UpdateInputData& io, const std::string& newSvgPath, bool newIsHighlighted, const std::function<void()>& elemUpdate);
|
||||
virtual void clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) override;
|
||||
private:
|
||||
bool highlighted;
|
||||
sk_sp<SkSVGDOM> svgDom;
|
||||
};
|
||||
|
||||
}
|
||||
55
src/GUIStuff/Elements/SelectableButton.cpp
Normal file
55
src/GUIStuff/Elements/SelectableButton.cpp
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#include "SelectableButton.hpp"
|
||||
#include "Helpers/ConvertVec.hpp"
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
void SelectableButton::update(UpdateInputData& io, bool hasBorders, bool hasBorderPadding, const std::function<void(SelectionHelper&, bool)>& elemUpdate, bool isSelected) {
|
||||
if(hasBorders) {
|
||||
SkColor4f borderColor;
|
||||
if(selection.held)
|
||||
borderColor = io.theme->fillColor1;
|
||||
else if(isSelected)
|
||||
borderColor = io.theme->fillColor2;
|
||||
else if(selection.hovered)
|
||||
borderColor = io.theme->fillColor2;
|
||||
else
|
||||
borderColor = io.theme->backColor3;
|
||||
|
||||
SkColor4f backgroundColor;
|
||||
if(selection.held)
|
||||
backgroundColor = io.theme->backColor3;
|
||||
else if(isSelected)
|
||||
backgroundColor = io.theme->backColor2;
|
||||
else if(selection.hovered)
|
||||
backgroundColor = io.theme->backColor2;
|
||||
else
|
||||
backgroundColor = io.theme->backColor1;
|
||||
|
||||
CLAY({.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)},
|
||||
.padding = hasBorderPadding ? CLAY_PADDING_ALL(4) : CLAY_PADDING_ALL(0),
|
||||
.childGap = 4,
|
||||
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||
},
|
||||
.backgroundColor = convert_vec4<Clay_Color>(backgroundColor),
|
||||
.cornerRadius = CLAY_CORNER_RADIUS(4),
|
||||
.border = {
|
||||
.color = convert_vec4<Clay_Color>(borderColor),
|
||||
.width = CLAY_BORDER_OUTSIDE(4)
|
||||
}
|
||||
}) {
|
||||
selection.update(Clay_Hovered(), io.mouse.leftClick, io.mouse.leftHeld);
|
||||
elemUpdate(selection, isSelected);
|
||||
}
|
||||
}
|
||||
else {
|
||||
selection.update(Clay_Hovered(), io.mouse.leftClick, io.mouse.leftHeld);
|
||||
elemUpdate(selection, isSelected);
|
||||
}
|
||||
}
|
||||
|
||||
void SelectableButton::clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) {
|
||||
}
|
||||
|
||||
}
|
||||
13
src/GUIStuff/Elements/SelectableButton.hpp
Normal file
13
src/GUIStuff/Elements/SelectableButton.hpp
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
#include "Element.hpp"
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
class SelectableButton : public Element {
|
||||
public:
|
||||
void update(UpdateInputData& io, bool hasBorders, bool hasBorderPadding, const std::function<void(SelectionHelper&, bool)>& elemUpdate, bool isSelected);
|
||||
virtual void clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) override;
|
||||
SelectionHelper selection;
|
||||
};
|
||||
|
||||
}
|
||||
195
src/GUIStuff/Elements/TextBox.hpp
Normal file
195
src/GUIStuff/Elements/TextBox.hpp
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
#pragma once
|
||||
#include "Element.hpp"
|
||||
#include "../../CollabTextBox/CollabTextBox.hpp"
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
template <typename T> class TextBox : public Element {
|
||||
public:
|
||||
void update(UpdateInputData& io, T* newData, const std::function<std::optional<T>(const std::string&)>& newFromStr, const std::function<std::string(const T&)> newToStr, bool newSingleLine, const std::function<void()>& elemUpdate) {
|
||||
data = newData;
|
||||
fromStr = newFromStr;
|
||||
toStr = newToStr;
|
||||
singleLine = newSingleLine;
|
||||
force_update_textbox(false);
|
||||
|
||||
textbox.setFont(SkFont(io.textTypeface, io.fontSize));
|
||||
textbox.setFontMgr(io.textFontMgr);
|
||||
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(static_cast<float>(io.fontSize * 2)), .height = CLAY_SIZING_GROW(0)}
|
||||
},
|
||||
.custom = { .customData = this }
|
||||
}) {
|
||||
selection.update(Clay_Hovered(), io.mouse.leftClick, io.mouse.leftHeld);
|
||||
if(data && selection.selected) {
|
||||
bool moved = io.key.left || io.key.right || io.key.up || io.key.down || io.key.home;
|
||||
bool movedHeld = false;
|
||||
|
||||
if(io.mouse.leftClick) {
|
||||
Vector2f textSelectPos = io.mouse.pos - bb.min;
|
||||
SkIPoint p = convert_vec2<SkIPoint>(textSelectPos.cast<int32_t>());
|
||||
cur.pos = cur.selectionBeginPos = textbox.getPosition(p);
|
||||
moved = true;
|
||||
}
|
||||
else if(io.mouse.leftHeld) {
|
||||
Vector2f textSelectPos = io.mouse.pos - bb.min;
|
||||
SkIPoint p = convert_vec2<SkIPoint>(textSelectPos.cast<int32_t>());
|
||||
cur.pos = cur.selectionBeginPos = textbox.getPosition(p);
|
||||
movedHeld = true;
|
||||
}
|
||||
|
||||
if(io.key.left)
|
||||
cur.pos = cur.selectionBeginPos = textbox.move(io.key.leftCtrl ? CollabTextBox::Movement::kWordLeft : CollabTextBox::Movement::kLeft, cur.selectionBeginPos);
|
||||
if(io.key.right)
|
||||
cur.pos = cur.selectionBeginPos = textbox.move(io.key.leftCtrl ? CollabTextBox::Movement::kWordRight : CollabTextBox::Movement::kRight, cur.selectionBeginPos);
|
||||
if(io.key.up && !singleLine)
|
||||
cur.pos = cur.selectionBeginPos = textbox.move(CollabTextBox::Movement::kUp, cur.selectionBeginPos);
|
||||
if(io.key.down && !singleLine)
|
||||
cur.pos = cur.selectionBeginPos = textbox.move(CollabTextBox::Movement::kDown, cur.selectionBeginPos);
|
||||
if(io.key.home)
|
||||
cur.pos = cur.selectionBeginPos = textbox.move(CollabTextBox::Movement::kHome, cur.selectionBeginPos);
|
||||
|
||||
if(moved && !io.key.leftShift && !movedHeld)
|
||||
cur.selectionEndPos = cur.selectionBeginPos;
|
||||
|
||||
if(io.key.backspace) {
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
else
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.pos, textbox.move(CollabTextBox::Movement::kLeft, cur.pos));
|
||||
}
|
||||
if(io.key.del) {
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
else
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.pos, textbox.move(CollabTextBox::Movement::kRight, cur.pos));
|
||||
}
|
||||
if(io.key.paste) {
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
if(singleLine) {
|
||||
std::string removedNewlines = io.clipboard.textInFunc();
|
||||
std::erase(removedNewlines, '\n');
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.insert(cur.pos, removedNewlines);
|
||||
}
|
||||
else
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.insert(cur.pos, io.clipboard.textInFunc());
|
||||
}
|
||||
if(io.key.enter) {
|
||||
if(singleLine) {
|
||||
std::optional<T> dataToAssign = fromStr(textbox.get_string());
|
||||
if(dataToAssign)
|
||||
*data = dataToAssign.value();
|
||||
force_update_textbox(true);
|
||||
}
|
||||
else {
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
cur.pos = textbox.insert(cur.pos, "\n");
|
||||
cur.pos.fParagraphIndex++;
|
||||
cur.pos.fTextByteIndex = 0;
|
||||
cur.selectionBeginPos = cur.selectionEndPos = cur.pos;
|
||||
}
|
||||
}
|
||||
if(io.key.copy) {
|
||||
io.clipboard.textOut = textbox.copy(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
}
|
||||
if(io.key.cut) {
|
||||
io.clipboard.textOut = textbox.copy(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
}
|
||||
if(io.key.selectAll) {
|
||||
cur.selectionEndPos.fParagraphIndex = textbox.lineCount() == 0 ? 0 : textbox.lineCount() - 1;
|
||||
cur.selectionEndPos.fTextByteIndex = textbox.line(cur.selectionEndPos.fParagraphIndex).size();
|
||||
cur.pos = cur.selectionEndPos;
|
||||
cur.selectionBeginPos.fTextByteIndex = 0;
|
||||
cur.selectionBeginPos.fParagraphIndex = 0;
|
||||
}
|
||||
if(!io.textInput.empty()) {
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = textbox.insert(cur.pos, io.textInput);
|
||||
}
|
||||
|
||||
io.acceptingTextInput = true;
|
||||
}
|
||||
if(data && selection.justUnselected) {
|
||||
std::optional<T> dataToAssign = fromStr(textbox.get_string());
|
||||
if(dataToAssign)
|
||||
*data = dataToAssign.value();
|
||||
force_update_textbox(true);
|
||||
}
|
||||
if(elemUpdate)
|
||||
elemUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
virtual void clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) override {
|
||||
if(!data)
|
||||
return;
|
||||
|
||||
bb = get_bb(command);
|
||||
|
||||
canvas->save();
|
||||
SkRect r = SkRect::MakeXYWH(bb.min.x(), bb.min.y(), bb.width(), bb.height());
|
||||
|
||||
canvas->drawRect(r, SkPaint(io.theme->backColor2));
|
||||
SkPaint outline(selection.selected ? io.theme->fillColor1 : io.theme->backColor3);
|
||||
outline.setStroke(true);
|
||||
outline.setStrokeWidth(2.0f);
|
||||
canvas->drawRoundRect(r, 2.0f, 2.0f, outline);
|
||||
|
||||
canvas->clipRect(SkRect::MakeXYWH(bb.min.x(), bb.min.y(), bb.width(), bb.height()));
|
||||
|
||||
canvas->translate(bb.min.x(), bb.min.y());
|
||||
|
||||
textbox.setFont(SkFont(io.textTypeface, io.fontSize));
|
||||
textbox.setFontMgr(io.textFontMgr);
|
||||
textbox.setWidth(singleLine ? 99999999 : bb.width());
|
||||
|
||||
CollabTextBox::Editor::PaintOpts paintOpts;
|
||||
paintOpts.fForegroundColor = io.theme->frontColor1;
|
||||
paintOpts.fBackgroundColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
paintOpts.cursorColor = io.theme->fillColor1;
|
||||
paintOpts.showCursor = selection.selected;
|
||||
paintOpts.cursor = cur;
|
||||
|
||||
textbox.paint(canvas, paintOpts);
|
||||
|
||||
canvas->restore();
|
||||
}
|
||||
|
||||
SelectionHelper selection;
|
||||
private:
|
||||
void force_update_textbox(bool reallyForce) {
|
||||
if(data && (*data == oldData) && !reallyForce)
|
||||
return;
|
||||
|
||||
textbox = CollabTextBox::Editor();
|
||||
cur = CollabTextBox::Cursor();
|
||||
if(data) {
|
||||
cur.pos = cur.selectionBeginPos = cur.selectionEndPos = textbox.insert({0, 0}, toStr(*data));
|
||||
oldData = *data;
|
||||
}
|
||||
else {}
|
||||
//oldData = T();
|
||||
}
|
||||
|
||||
T* data = nullptr;
|
||||
T oldData;
|
||||
|
||||
bool singleLine = true;
|
||||
|
||||
std::function<std::optional<T>(const std::string&)> fromStr;
|
||||
std::function<std::string(const T&)> toStr;
|
||||
|
||||
CollabTextBox::Editor textbox;
|
||||
CollabTextBox::Cursor cur;
|
||||
|
||||
SCollision::AABB<float> bb;
|
||||
};
|
||||
|
||||
}
|
||||
702
src/GUIStuff/GUIManager.cpp
Normal file
702
src/GUIStuff/GUIManager.cpp
Normal file
|
|
@ -0,0 +1,702 @@
|
|||
#include "GUIManager.hpp"
|
||||
#include <include/core/SkPaint.h>
|
||||
#include <include/core/SkRRect.h>
|
||||
#include <include/core/SkFont.h>
|
||||
#include <include/core/SkFontMetrics.h>
|
||||
#include <include/core/SkPath.h>
|
||||
#include "Elements/SVGIcon.hpp"
|
||||
#include "Elements/SelectableButton.hpp"
|
||||
#include "Elements/MovableTabList.hpp"
|
||||
#include "Elements/Element.hpp"
|
||||
#include <Helpers/ConvertVec.hpp>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
GUIManager::GUIManager()
|
||||
{
|
||||
Clay_SetMaxMeasureTextCacheWordCount(100000);
|
||||
clayArena = Clay_CreateArenaWithCapacityAndMemory(Clay_MinMemorySize(), malloc(Clay_MinMemorySize()));
|
||||
clayInstance = Clay_Initialize(clayArena, Clay_Dimensions(1.0f, 1.0f), (Clay_ErrorHandler)clay_error_handler);
|
||||
Clay_SetMeasureTextFunction(clay_skia_measure_text, this);
|
||||
//Clay_SetDebugModeEnabled(true);
|
||||
}
|
||||
|
||||
Clay_Dimensions GUIManager::clay_skia_measure_text(Clay_StringSlice str, Clay_TextElementConfig* config, void* userData) {
|
||||
GUIManager* window = static_cast<GUIManager*>(userData);
|
||||
SkFont font = window->io->get_font(config->fontSize);
|
||||
SkFontMetrics metrics;
|
||||
font.getMetrics(&metrics);
|
||||
float nextText = font.measureText(str.chars, str.length, SkTextEncoding::kUTF8, nullptr);
|
||||
return Clay_Dimensions(nextText, - metrics.fAscent + metrics.fDescent);
|
||||
}
|
||||
|
||||
void GUIManager::begin() {
|
||||
io->mouse.pos = io->mouse.globalPos - windowPos;
|
||||
io->strArena = &strArena;
|
||||
Clay_SetLayoutDimensions(Clay_Dimensions(windowSize.x(), windowSize.y()));
|
||||
Clay_SetPointerState(Clay_Vector2((float)io->mouse.pos.x(), (float)io->mouse.pos.y()), io->mouse.leftHeld);
|
||||
Clay_UpdateScrollContainers(false, Clay_Vector2(io->mouse.scroll.y(), io->mouse.scroll.y()), io->deltaTime * 2.0f);
|
||||
Clay_SetCurrentContext(clayInstance);
|
||||
|
||||
strArena.reset();
|
||||
|
||||
Clay_BeginLayout();
|
||||
}
|
||||
|
||||
void GUIManager::end() {
|
||||
renderCommands = Clay_EndLayout();
|
||||
if(!idStack.empty())
|
||||
throw std::runtime_error("[GUIManager::end] ID Stack is not empty on end (push_id and pop_id calls not equal)");
|
||||
}
|
||||
|
||||
void GUIManager::draw(SkCanvas* canvas) {
|
||||
canvas->save();
|
||||
canvas->translate(windowPos.x(), windowPos.y());
|
||||
|
||||
for(size_t i = 0; i < static_cast<size_t>(renderCommands.length); i++) {
|
||||
Clay_RenderCommand* command = Clay_RenderCommandArray_Get(&renderCommands, i);
|
||||
Clay_BoundingBox bb = command->boundingBox;
|
||||
switch(command->commandType) {
|
||||
case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: {
|
||||
Clay_RectangleRenderData* config = &command->renderData.rectangle;
|
||||
SkVector radii[4] = {
|
||||
{config->cornerRadius.topLeft, config->cornerRadius.topLeft},
|
||||
{config->cornerRadius.topRight, config->cornerRadius.topRight},
|
||||
{config->cornerRadius.bottomRight, config->cornerRadius.bottomRight},
|
||||
{config->cornerRadius.bottomLeft, config->cornerRadius.bottomLeft}
|
||||
};
|
||||
|
||||
SkRRect rrect;
|
||||
rrect.setRectRadii(
|
||||
SkRect::MakeXYWH(bb.x, bb.y, bb.width, bb.height),
|
||||
radii
|
||||
);
|
||||
|
||||
SkPaint paint;
|
||||
paint.setStyle(SkPaint::kFill_Style);
|
||||
paint.setColor4f(convert_vec4<SkColor4f>(config->backgroundColor));
|
||||
canvas->drawRRect(rrect, paint);
|
||||
|
||||
break;
|
||||
}
|
||||
case CLAY_RENDER_COMMAND_TYPE_TEXT: {
|
||||
Clay_TextRenderData* config = &command->renderData.text;
|
||||
SkPaint paint;
|
||||
paint.setColor4f(convert_vec4<SkColor4f>(config->textColor));
|
||||
SkFont f = io->get_font(config->fontSize);
|
||||
SkFontMetrics metrics;
|
||||
f.getMetrics(&metrics);
|
||||
canvas->drawSimpleText(config->stringContents.chars, config->stringContents.length, SkTextEncoding::kUTF8, bb.x, (bb.y + bb.height - metrics.fDescent), io->get_font(config->fontSize), paint);
|
||||
break;
|
||||
}
|
||||
case CLAY_RENDER_COMMAND_TYPE_BORDER: {
|
||||
Clay_BorderRenderData* config = &command->renderData.border;
|
||||
|
||||
SkPaint p;
|
||||
p.setColor4f(convert_vec4<SkColor4f>(config->color));
|
||||
p.setStyle(SkPaint::kStroke_Style);
|
||||
|
||||
float halfLineWidth = 0.0f;
|
||||
// Top Left corner
|
||||
if (config->cornerRadius.topLeft > 0.0f) {
|
||||
SkPath path;
|
||||
float lineWidth = config->width.top;
|
||||
p.setStrokeWidth(lineWidth);
|
||||
path.moveTo(bb.x + halfLineWidth, bb.y + config->cornerRadius.topLeft + halfLineWidth);
|
||||
path.arcTo((bb.x + halfLineWidth), (bb.y + halfLineWidth), (bb.x + config->cornerRadius.topLeft + halfLineWidth), (bb.y + halfLineWidth), config->cornerRadius.topLeft);
|
||||
canvas->drawPath(path, p);
|
||||
}
|
||||
// Top border
|
||||
if (config->width.top > 0.0f) {
|
||||
SkPath path;
|
||||
float lineWidth = config->width.top;
|
||||
p.setStrokeWidth(lineWidth);
|
||||
path.moveTo((bb.x + config->cornerRadius.topLeft + halfLineWidth), (bb.y + halfLineWidth));
|
||||
path.lineTo((bb.x + bb.width - config->cornerRadius.topRight - halfLineWidth), (bb.y + halfLineWidth));
|
||||
canvas->drawPath(path, p);
|
||||
}
|
||||
// Top Right Corner
|
||||
if (config->cornerRadius.topRight > 0.0f) {
|
||||
SkPath path;
|
||||
float lineWidth = config->width.top;
|
||||
p.setStrokeWidth(lineWidth);
|
||||
path.moveTo((bb.x + bb.width - config->cornerRadius.topRight - halfLineWidth), (bb.y + halfLineWidth));
|
||||
path.arcTo((bb.x + bb.width - halfLineWidth), (bb.y + halfLineWidth), (bb.x + bb.width - halfLineWidth), (bb.y + config->cornerRadius.topRight + halfLineWidth), config->cornerRadius.topRight);
|
||||
canvas->drawPath(path, p);
|
||||
}
|
||||
// Right border
|
||||
if (config->width.right > 0.0f) {
|
||||
SkPath path;
|
||||
float lineWidth = config->width.right;
|
||||
p.setStrokeWidth(lineWidth);
|
||||
path.moveTo((bb.x + bb.width - halfLineWidth), (bb.y + config->cornerRadius.topRight + halfLineWidth));
|
||||
path.lineTo((bb.x + bb.width - halfLineWidth), (bb.y + bb.height - config->cornerRadius.bottomRight - halfLineWidth));
|
||||
canvas->drawPath(path, p);
|
||||
}
|
||||
// Bottom Right Corner
|
||||
if (config->cornerRadius.bottomRight > 0.0f) {
|
||||
SkPath path;
|
||||
float lineWidth = config->width.bottom;
|
||||
p.setStrokeWidth(lineWidth);
|
||||
path.moveTo((bb.x + bb.width - halfLineWidth), (bb.y + bb.height - config->cornerRadius.bottomRight - halfLineWidth));
|
||||
path.arcTo((bb.x + bb.width - halfLineWidth), (bb.y + bb.height - halfLineWidth), (bb.x + bb.width - config->cornerRadius.bottomRight - halfLineWidth), (bb.y + bb.height - halfLineWidth), config->cornerRadius.bottomRight);
|
||||
canvas->drawPath(path, p);
|
||||
}
|
||||
// Bottom Border
|
||||
if (config->width.bottom > 0.0f) {
|
||||
SkPath path;
|
||||
float lineWidth = config->width.bottom;
|
||||
p.setStrokeWidth(lineWidth);
|
||||
path.moveTo((bb.x + config->cornerRadius.bottomLeft + halfLineWidth), (bb.y + bb.height - halfLineWidth));
|
||||
path.lineTo((bb.x + bb.width - config->cornerRadius.bottomRight - halfLineWidth), (bb.y + bb.height - halfLineWidth));
|
||||
canvas->drawPath(path, p);
|
||||
}
|
||||
// Bottom Left Corner
|
||||
if (config->cornerRadius.bottomLeft > 0.0f) {
|
||||
SkPath path;
|
||||
float lineWidth = config->width.bottom;
|
||||
p.setStrokeWidth(lineWidth);
|
||||
path.moveTo((bb.x + config->cornerRadius.bottomLeft + halfLineWidth), (bb.y + bb.height - halfLineWidth));
|
||||
path.arcTo((bb.x + halfLineWidth), (bb.y + bb.height - halfLineWidth), (bb.x + halfLineWidth), (bb.y + bb.height - config->cornerRadius.bottomLeft - halfLineWidth), config->cornerRadius.bottomLeft);
|
||||
canvas->drawPath(path, p);
|
||||
}
|
||||
// Left Border
|
||||
if (config->width.left > 0.0f) {
|
||||
SkPath path;
|
||||
float lineWidth = config->width.left;
|
||||
p.setStrokeWidth(lineWidth);
|
||||
path.moveTo((bb.x + halfLineWidth), (bb.y + bb.height - config->cornerRadius.bottomLeft - halfLineWidth));
|
||||
path.lineTo((bb.x + halfLineWidth), (bb.y + config->cornerRadius.topRight + halfLineWidth));
|
||||
canvas->drawPath(path, p);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: {
|
||||
canvas->save();
|
||||
SkRect clipRect = SkRect::MakeXYWH(bb.x, bb.y, bb.width, bb.height);
|
||||
canvas->clipRect(clipRect);
|
||||
break;
|
||||
}
|
||||
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: {
|
||||
canvas->restore();
|
||||
break;
|
||||
}
|
||||
case CLAY_RENDER_COMMAND_TYPE_CUSTOM: {
|
||||
Element* customElement = (Element*)command->renderData.custom.customData;
|
||||
customElement->clay_draw(canvas, *io, command);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
canvas->restore();
|
||||
}
|
||||
|
||||
void GUIManager::top_to_bottom_window_popup_layout(Clay_SizingAxis x, Clay_SizingAxis y, const std::function<void()>& elemUpdate) {
|
||||
CLAY({.layout = {
|
||||
.sizing = {.width = x, .height = y },
|
||||
.padding = CLAY_PADDING_ALL(io->theme->padding1),
|
||||
.childGap = io->theme->childGap1,
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||
},
|
||||
.backgroundColor = convert_vec4<Clay_Color>(io->theme->backColor1),
|
||||
.cornerRadius = CLAY_CORNER_RADIUS(10),
|
||||
.floating = { .attachTo = CLAY_ATTACH_TO_PARENT },
|
||||
.border = {.color = convert_vec4<Clay_Color>(io->theme->backColor2), .width = CLAY_BORDER_OUTSIDE(2)}
|
||||
}) {
|
||||
if(Clay_Hovered())
|
||||
io->hoverObstructed = true;
|
||||
elemUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void GUIManager::left_to_right_layout(Clay_SizingAxis x, Clay_SizingAxis y, const std::function<void()>& elemUpdate) {
|
||||
CLAY({.layout = {
|
||||
.sizing = {.width = x, .height = y },
|
||||
.childGap = io->theme->childGap1,
|
||||
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER},
|
||||
.layoutDirection = CLAY_LEFT_TO_RIGHT,
|
||||
}
|
||||
}) {
|
||||
elemUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void GUIManager::left_to_right_line_layout(const std::function<void()>& elemUpdate) {
|
||||
left_to_right_layout(CLAY_SIZING_GROW(0), CLAY_SIZING_FIT(0), elemUpdate);
|
||||
}
|
||||
|
||||
void GUIManager::text_label_size(const std::string& val, float modifier) {
|
||||
CLAY_TEXT(strArena.std_str_to_clay_str(val), CLAY_TEXT_CONFIG({.textColor = convert_vec4<Clay_Color>(io->theme->frontColor1), .fontSize = static_cast<uint16_t>(io->fontSize * modifier)}));
|
||||
}
|
||||
|
||||
void GUIManager::text_label_color(const std::string& val, const SkColor4f& color) {
|
||||
CLAY_TEXT(strArena.std_str_to_clay_str(val), CLAY_TEXT_CONFIG({.textColor = convert_vec4<Clay_Color>(color), .fontSize = io->fontSize }));
|
||||
}
|
||||
|
||||
void GUIManager::text_label(const std::string& val) {
|
||||
CLAY_TEXT(strArena.std_str_to_clay_str(val), CLAY_TEXT_CONFIG({.textColor = convert_vec4<Clay_Color>(io->theme->frontColor1), .fontSize = io->fontSize }));
|
||||
}
|
||||
|
||||
void GUIManager::text_label_centered(const std::string& val) {
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) },
|
||||
.childAlignment = {.x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER},
|
||||
}
|
||||
}) {
|
||||
CLAY_TEXT(strArena.std_str_to_clay_str(val), CLAY_TEXT_CONFIG({.textColor = convert_vec4<Clay_Color>(io->theme->frontColor1), .fontSize = io->fontSize }));
|
||||
}
|
||||
}
|
||||
|
||||
bool GUIManager::radio_button(const std::string& id, bool val, const std::function<void()>& elemUpdate) {
|
||||
push_id(id);
|
||||
RadioButton* e = insert_element<RadioButton>();
|
||||
e->update(*io, val, elemUpdate);
|
||||
pop_id();
|
||||
return e->selection.clicked;
|
||||
}
|
||||
|
||||
void GUIManager::checkbox(const std::string& id, bool* val, const std::function<void()>& elemUpdate) {
|
||||
push_id(id);
|
||||
CheckBox* e = insert_element<CheckBox>();
|
||||
e->update(*io, *val, elemUpdate);
|
||||
if(e->selection.clicked)
|
||||
*val = !(*val);
|
||||
pop_id();
|
||||
}
|
||||
|
||||
void GUIManager::input_text_field(const std::string& id, const std::string& name, std::string* val, const std::function<void()>& elemUpdate) {
|
||||
left_to_right_line_layout([&]() {
|
||||
text_label(name);
|
||||
input_text(id, val, elemUpdate);
|
||||
});
|
||||
}
|
||||
|
||||
void GUIManager::input_path_field(const std::string& id, const std::string& name, std::filesystem::path* val, std::filesystem::file_type fileTypeRestriction, const std::function<void()>& elemUpdate) {
|
||||
left_to_right_line_layout([&]() {
|
||||
text_label(name);
|
||||
input_path(id, val, fileTypeRestriction, elemUpdate);
|
||||
});
|
||||
}
|
||||
|
||||
void GUIManager::checkbox_field(const std::string& id, const std::string& name, bool* val, const std::function<void()>& elemUpdate) {
|
||||
left_to_right_line_layout([&]() {
|
||||
checkbox(id, val, elemUpdate);
|
||||
text_label(name);
|
||||
});
|
||||
}
|
||||
|
||||
bool GUIManager::radio_button_field(const std::string& id, const std::string& name, bool val, const std::function<void()>& elemUpdate) {
|
||||
bool toRet;
|
||||
left_to_right_line_layout([&]() {
|
||||
toRet = radio_button(id, val, elemUpdate);
|
||||
text_label(name);
|
||||
});
|
||||
return toRet;
|
||||
}
|
||||
|
||||
void GUIManager::input_color_component_255(const std::string& id, float* val, const std::function<void()>& elemUpdate) {
|
||||
push_id(id);
|
||||
insert_element<TextBox<float>>()->update(*io, val,
|
||||
[&](const std::string& str) {
|
||||
int roundTo255;
|
||||
std::stringstream ss;
|
||||
ss << str;
|
||||
ss >> roundTo255;
|
||||
return std::clamp<float>(static_cast<float>(roundTo255) / 255.0f, 0.0f, 1.0f);
|
||||
},
|
||||
[&](const float& colorFloat) {
|
||||
int convertTo255 = colorFloat * 255;
|
||||
std::stringstream ss;
|
||||
ss << convertTo255;
|
||||
return ss.str();
|
||||
}, true, elemUpdate);
|
||||
pop_id();
|
||||
}
|
||||
|
||||
void GUIManager::input_text(const std::string& id, std::string* val, const std::function<void()>& elemUpdate) {
|
||||
push_id(id);
|
||||
insert_element<TextBox<std::string>>()->update(*io, val,
|
||||
[&](const std::string& str) {
|
||||
return str;
|
||||
},
|
||||
[&](const std::string& str) {
|
||||
return str;
|
||||
}, true, elemUpdate);
|
||||
pop_id();
|
||||
}
|
||||
|
||||
bool GUIManager::selectable_button(const std::string& id, const std::function<void(SelectionHelper&, bool)>& elemUpdate, bool hasBorder, bool hasBorderPadding, bool isSelected) {
|
||||
push_id(id);
|
||||
SelectableButton* e = insert_element<SelectableButton>();
|
||||
e->update(*io, hasBorder, hasBorderPadding, elemUpdate, isSelected);
|
||||
bool toRet = e->selection.clicked;
|
||||
pop_id();
|
||||
return toRet;
|
||||
}
|
||||
|
||||
void GUIManager::svg_icon(const std::string& id, const std::string& svgPath, bool isHighlighted, const std::function<void()>& elemUpdate) {
|
||||
push_id(id);
|
||||
insert_element<SVGIcon>()->update(*io, svgPath, isHighlighted, elemUpdate);
|
||||
pop_id();
|
||||
}
|
||||
|
||||
bool GUIManager::svg_icon_button(const std::string& id, const std::string& svgPath, bool isSelected, float size, const std::function<void()>& elemUpdate) {
|
||||
bool toRet = false;
|
||||
push_id(id);
|
||||
CLAY ({.layout = {.sizing = {.width = CLAY_SIZING_FIXED(size), .height = CLAY_SIZING_FIXED(size) } } }) {
|
||||
toRet = selectable_button("0", [&](SelectionHelper& s, bool is) {
|
||||
svg_icon("1", svgPath, is || s.held || s.hovered);
|
||||
if(elemUpdate)
|
||||
elemUpdate();
|
||||
}, true, true, isSelected);
|
||||
}
|
||||
pop_id();
|
||||
return toRet;
|
||||
}
|
||||
|
||||
bool GUIManager::text_button_sized(const std::string& id, const std::string& text, Clay_SizingAxis x, Clay_SizingAxis y, bool isSelected, const std::function<void()>& elemUpdate) {
|
||||
bool toRet = false;
|
||||
CLAY ({.layout = {.sizing = {.width = x, .height = y } } }) {
|
||||
toRet = selectable_button(id, [&](SelectionHelper& s, bool iS) {
|
||||
text_label(text);
|
||||
if(elemUpdate)
|
||||
elemUpdate();
|
||||
}, true, true, isSelected);
|
||||
}
|
||||
return toRet;
|
||||
}
|
||||
|
||||
bool GUIManager::text_button(const std::string& id, const std::string& text, bool isSelected, const std::function<void()>& elemUpdate) {
|
||||
bool toRet = false;
|
||||
CLAY ({.layout = {.sizing = {.width = CLAY_SIZING_FIT(0), .height = CLAY_SIZING_FIT(0) } } }) {
|
||||
toRet = selectable_button(id, [&](SelectionHelper& s, bool iS) {
|
||||
text_label(text);
|
||||
if(elemUpdate)
|
||||
elemUpdate();
|
||||
}, true, true, isSelected);
|
||||
}
|
||||
return toRet;
|
||||
}
|
||||
|
||||
bool GUIManager::text_button_wide(const std::string& id, const std::string& text, bool isSelected, const std::function<void()>& elemUpdate) {
|
||||
bool toRet = false;
|
||||
CLAY ({.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) } } }) {
|
||||
toRet = selectable_button(id, [&](SelectionHelper& s, bool iS) {
|
||||
text_label(text);
|
||||
if(elemUpdate)
|
||||
elemUpdate();
|
||||
}, true, true, isSelected);
|
||||
}
|
||||
return toRet;
|
||||
}
|
||||
|
||||
void GUIManager::input_scalar(const std::string& id, uint8_t* val, uint8_t min, uint8_t max, int decimalPrecision, const std::function<void()>& elemUpdate) {
|
||||
push_id(id);
|
||||
insert_element<TextBox<uint8_t>>()->update(*io, val,
|
||||
[&](const std::string& a) {
|
||||
if(a.empty())
|
||||
return min;
|
||||
uint32_t toRet;
|
||||
std::stringstream ss;
|
||||
ss << a;
|
||||
ss >> toRet;
|
||||
if(ss.fail())
|
||||
return min;
|
||||
return std::clamp<uint8_t>(toRet, min, max);
|
||||
},
|
||||
[&](const uint8_t& a) {
|
||||
return std::to_string(static_cast<uint32_t>(a));
|
||||
}, true, elemUpdate);
|
||||
pop_id();
|
||||
}
|
||||
|
||||
void GUIManager::input_path(const std::string& id, std::filesystem::path* val, std::filesystem::file_type fileTypeRestriction, const std::function<void()>& elemUpdate) {
|
||||
push_id(id);
|
||||
insert_element<TextBox<std::filesystem::path>>()->update(*io, val,
|
||||
[&](const std::string& a) {
|
||||
std::filesystem::path toRet = std::filesystem::path(a);
|
||||
if(fileTypeRestriction != std::filesystem::file_type::none && std::filesystem::status(toRet).type() != fileTypeRestriction)
|
||||
return std::optional<std::filesystem::path>(std::nullopt);
|
||||
return std::optional<std::filesystem::path>(toRet);
|
||||
},
|
||||
[&](const std::filesystem::path& a) {
|
||||
return a.string();
|
||||
}, true, elemUpdate);
|
||||
pop_id();
|
||||
}
|
||||
|
||||
void GUIManager::tab_list(const std::string& id, const std::vector<std::pair<std::string, std::string>>& tabNames, size_t& selectedTab, std::optional<size_t>& closedTab, const std::function<void()>& elemUpdate) {
|
||||
push_id(id);
|
||||
insert_element<MovableTabList>()->update(*io, tabNames, selectedTab, closedTab, elemUpdate);
|
||||
pop_id();
|
||||
}
|
||||
|
||||
void GUIManager::dropdown_select(const std::string& id, size_t* val, const std::vector<std::string>& selections, float width, const std::function<void()>& hoverboxElemUpdate) {
|
||||
push_id(id);
|
||||
bool& dropDownOpen = insert_any_with_id(0, false);
|
||||
left_to_right_layout(CLAY_SIZING_FIXED(width), CLAY_SIZING_FIT(0), [&]() {
|
||||
bool click = selectable_button(id, [&](SelectionHelper& s, bool iS) {
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0) },
|
||||
.padding = {.left = 4, .right = 4},
|
||||
.childGap = io->theme->childGap1,
|
||||
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER},
|
||||
.layoutDirection = CLAY_LEFT_TO_RIGHT,
|
||||
}
|
||||
}) {
|
||||
text_label(selections[*val]);
|
||||
CLAY({.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}}}) {}
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_FIT(20), .height = CLAY_SIZING_FIT(20)}
|
||||
}
|
||||
}) {
|
||||
svg_icon("dropico", "data/icons/droparrow.svg", dropDownOpen);
|
||||
}
|
||||
}
|
||||
}, true, false, dropDownOpen);
|
||||
if(dropDownOpen) {
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_FIXED(width), .height = CLAY_SIZING_FIT(0)},
|
||||
.padding = {.left = 2, .right = 2},
|
||||
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP},
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||
},
|
||||
.cornerRadius = CLAY_CORNER_RADIUS(4),
|
||||
.floating = {
|
||||
.offset = {
|
||||
.y = 4
|
||||
},
|
||||
.attachPoints = {
|
||||
.element = CLAY_ATTACH_POINT_LEFT_TOP,
|
||||
.parent = CLAY_ATTACH_POINT_LEFT_BOTTOM
|
||||
},
|
||||
.attachTo = CLAY_ATTACH_TO_PARENT
|
||||
},
|
||||
.border = {
|
||||
.color = convert_vec4<Clay_Color>(io->theme->backColor3),
|
||||
.width = CLAY_BORDER_OUTSIDE(4)
|
||||
}
|
||||
}) {
|
||||
obstructing_window();
|
||||
for(size_t i = 0; i < selections.size(); i++) {
|
||||
bool selectedEntry = *val == i;
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0)},
|
||||
},
|
||||
}) {
|
||||
SkColor4f entryColor;
|
||||
if(selectedEntry)
|
||||
entryColor = io->theme->backColor2;
|
||||
else if(Clay_Hovered())
|
||||
entryColor = io->theme->backColor3;
|
||||
else
|
||||
entryColor = io->theme->backColor1;
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0)},
|
||||
},
|
||||
.backgroundColor = convert_vec4<Clay_Color>(entryColor)
|
||||
}) {
|
||||
text_label(selections[i]);
|
||||
if(io->mouse.leftClick && Clay_Hovered()) {
|
||||
*val = i;
|
||||
dropDownOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(hoverboxElemUpdate)
|
||||
hoverboxElemUpdate();
|
||||
}
|
||||
}
|
||||
if(click)
|
||||
dropDownOpen = !dropDownOpen;
|
||||
});
|
||||
pop_id();
|
||||
}
|
||||
|
||||
void GUIManager::scroll_bar_area(const std::string& uniqueId, const std::function<void(float, float, float)>& elemUpdate) {
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)},
|
||||
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP},
|
||||
.layoutDirection = CLAY_LEFT_TO_RIGHT
|
||||
}
|
||||
}) {
|
||||
struct ScrollAreaData {
|
||||
float currentScrollPos = 0.0f;
|
||||
bool isMoving = false;
|
||||
float contentDimensions = 100.0f;
|
||||
float containerDimensions = 100.0f;
|
||||
};
|
||||
push_id(uniqueId);
|
||||
ScrollAreaData& sD = insert_any(ScrollAreaData{});
|
||||
pop_id();
|
||||
|
||||
Clay_ElementId clayID = Clay_GetElementId(strArena.std_str_to_clay_str(uniqueId));
|
||||
|
||||
CLAY({
|
||||
.id = clayID,
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)},
|
||||
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP},
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||
},
|
||||
.clip = {.vertical = true, .childOffset = Clay_GetScrollOffset()}
|
||||
}) {
|
||||
elemUpdate(sD.contentDimensions, sD.containerDimensions, sD.currentScrollPos);
|
||||
}
|
||||
|
||||
// Make sure you are accessing this data after the element has been created
|
||||
Clay_ScrollContainerData scrollData = Clay_GetScrollContainerData(clayID);
|
||||
Clay_BoundingBox scrollAreaBB = Clay_GetElementData(clayID).boundingBox;
|
||||
|
||||
sD.contentDimensions = scrollData.contentDimensions.height;
|
||||
sD.containerDimensions = scrollData.scrollContainerDimensions.height;
|
||||
|
||||
if(scrollData.contentDimensions.height > scrollData.scrollContainerDimensions.height) {
|
||||
float sAreaDim = scrollData.scrollContainerDimensions.height;
|
||||
float contDim = scrollData.contentDimensions.height;
|
||||
float scrollerSize = (sAreaDim / contDim) * sAreaDim;
|
||||
float scrollPosMax = (contDim - sAreaDim) + 3.0f; // NOTE: Adding + 3.0f to prevent bug with scroller that pushes everything under it when its at the bottom of the scroll area
|
||||
float scrollerPos = std::fabs(scrollData.scrollPosition->y / scrollPosMax);
|
||||
float areaAboveScrollerSize = scrollerPos * (sAreaDim - scrollerSize);
|
||||
|
||||
if(Clay_Hovered())
|
||||
sD.currentScrollPos += io->mouse.scroll.y() * io->deltaTime * 3000.0f;
|
||||
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_FIXED(12), .height = CLAY_SIZING_GROW(0)},
|
||||
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP},
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||
},
|
||||
.backgroundColor = convert_vec4<Clay_Color>(io->theme->backColor1)
|
||||
}) {
|
||||
SkColor4f scrollerColor;
|
||||
if(sD.isMoving)
|
||||
scrollerColor = io->theme->fillColor1;
|
||||
else if(Clay_Hovered())
|
||||
scrollerColor = io->theme->fillColor2;
|
||||
else
|
||||
scrollerColor = io->theme->backColor3;
|
||||
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(areaAboveScrollerSize)}
|
||||
}
|
||||
}) {}
|
||||
CLAY({
|
||||
.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(scrollerSize)}},
|
||||
.backgroundColor = convert_vec4<Clay_Color>(scrollerColor),
|
||||
.cornerRadius = CLAY_CORNER_RADIUS(3),
|
||||
}) {
|
||||
}
|
||||
CLAY({ .layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}}}) {}
|
||||
if(Clay_Hovered() && io->mouse.leftClick)
|
||||
sD.isMoving = true;
|
||||
if(!io->mouse.leftHeld)
|
||||
sD.isMoving = false;
|
||||
if(sD.isMoving)
|
||||
sD.currentScrollPos = ((io->mouse.pos.y() - scrollAreaBB.y) / scrollAreaBB.height) * (-scrollPosMax);
|
||||
sD.currentScrollPos = std::clamp(sD.currentScrollPos, -scrollPosMax, 0.0f);
|
||||
scrollData.scrollPosition->y = sD.currentScrollPos;
|
||||
}
|
||||
}
|
||||
else
|
||||
sD.currentScrollPos = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void GUIManager::scroll_bar_many_entries_area(const std::string& uniqueId, float entryHeight, size_t entryCount, const std::function<void(size_t, bool)>& entryUpdate, const std::function<void(float, float, float)>& elemUpdate) {
|
||||
push_id(uniqueId);
|
||||
scroll_bar_area(uniqueId, [&](float scrollContentHeight, float containerHeight, float scrollAmount) {
|
||||
if(elemUpdate)
|
||||
elemUpdate(scrollContentHeight, containerHeight, scrollAmount);
|
||||
|
||||
bool listHovered = Clay_Hovered();
|
||||
|
||||
scrollAmount = std::fabs(scrollAmount);
|
||||
float entryHeight = 25.0f;
|
||||
size_t startPoint = scrollAmount / entryHeight;
|
||||
size_t elementsContainable = (containerHeight / entryHeight) + 1;
|
||||
size_t endPoint = std::min(entryCount, startPoint + elementsContainable);
|
||||
|
||||
if(startPoint != 0) {
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(startPoint * entryHeight)},
|
||||
}
|
||||
}) { }
|
||||
}
|
||||
|
||||
for(size_t i = startPoint; i < endPoint; i++) {
|
||||
push_id(i);
|
||||
entryUpdate(i, listHovered);
|
||||
pop_id();
|
||||
}
|
||||
|
||||
if(endPoint != entryCount) {
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED((entryCount - endPoint) * entryHeight)},
|
||||
}
|
||||
}) { }
|
||||
}
|
||||
});
|
||||
pop_id();
|
||||
}
|
||||
|
||||
void GUIManager::paint_circle_popup_menu(const std::string& id, const Vector2f& centerPos, const PaintCircleMenu::Data& val, const std::function<void()>& elemUpdate) {
|
||||
float size = 300.0f;
|
||||
push_id(id);
|
||||
CLAY({.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_FIXED(size), .height = CLAY_SIZING_FIXED(size)}
|
||||
},
|
||||
.floating = {
|
||||
.offset = {centerPos.x() - size * 0.5f, centerPos.y() - size * 0.5f},
|
||||
.zIndex = -1,
|
||||
.attachTo = CLAY_ATTACH_TO_ROOT
|
||||
}
|
||||
}) {
|
||||
PaintCircleMenu* e = insert_element<PaintCircleMenu>();
|
||||
e->update(*io, val, elemUpdate);
|
||||
}
|
||||
pop_id();
|
||||
}
|
||||
|
||||
void GUIManager::obstructing_window() {
|
||||
if(Clay_Hovered())
|
||||
io->hoverObstructed = true;
|
||||
}
|
||||
|
||||
void GUIManager::clay_error_handler(Clay_ErrorData errorData) {
|
||||
std::cout << "[Clay Error] " << errorData.errorText.chars << std::endl;
|
||||
}
|
||||
|
||||
void GUIManager::push_id(int64_t id) {
|
||||
idStack.emplace_back(id);
|
||||
}
|
||||
|
||||
void GUIManager::push_id(const std::string& id) {
|
||||
idStack.emplace_back(id);
|
||||
}
|
||||
|
||||
void GUIManager::pop_id() {
|
||||
idStack.pop_back();
|
||||
}
|
||||
|
||||
}
|
||||
291
src/GUIStuff/GUIManager.hpp
Normal file
291
src/GUIStuff/GUIManager.hpp
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
#pragma once
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include <Eigen/Dense>
|
||||
#include "Elements/Element.hpp"
|
||||
#include "Elements/NumberSlider.hpp"
|
||||
#include "Elements/CheckBox.hpp"
|
||||
#include "Elements/PaintCircleMenu.hpp"
|
||||
#include "Elements/RadioButton.hpp"
|
||||
#include "Elements/ColorPicker.hpp"
|
||||
#include "Elements/TextBox.hpp"
|
||||
#include <any>
|
||||
#include "GUIManagerID.hpp"
|
||||
#include <filesystem>
|
||||
#include <type_traits>
|
||||
#include <clay.h>
|
||||
|
||||
using namespace Eigen;
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
class GUIManager {
|
||||
public:
|
||||
struct ElementContainer {
|
||||
std::unique_ptr<Element> elem;
|
||||
std::any extra;
|
||||
};
|
||||
|
||||
GUIManager();
|
||||
void begin();
|
||||
void end();
|
||||
void draw(SkCanvas* canvas);
|
||||
|
||||
Vector2f windowPos = Vector2f{0.0f, 0.0f};
|
||||
Vector2f windowSize = Vector2f{0.0f, 0.0f};
|
||||
std::shared_ptr<UpdateInputData> io;
|
||||
|
||||
GUIManagerIDStack idStack;
|
||||
std::unordered_map<GUIManagerIDStack, ElementContainer> elements;
|
||||
|
||||
Vector2f screen_pos_to_window_pos(const Vector2f& screenPos);
|
||||
Vector2f get_mouse_pos();
|
||||
|
||||
void input_text_field(const std::string& id, const std::string& name, std::string* val, const std::function<void()>& elemUpdate = nullptr);
|
||||
|
||||
void text_label_color(const std::string& val, const SkColor4f& color);
|
||||
void text_label_size(const std::string& val, float modifier);
|
||||
void text_label(const std::string& val);
|
||||
void text_label_centered(const std::string& val);
|
||||
bool text_button(const std::string& id, const std::string& text, bool isSelected = false, const std::function<void()>& elemUpdate = nullptr);
|
||||
bool text_button_wide(const std::string& id, const std::string& text, bool isSelected = false, const std::function<void()>& elemUpdate = nullptr);
|
||||
bool text_button_sized(const std::string& id, const std::string& text, Clay_SizingAxis x, Clay_SizingAxis y, bool isSelected = false, const std::function<void()>& elemUpdate = nullptr);
|
||||
|
||||
void svg_icon(const std::string& id, const std::string& svgPath, bool isHighlighted = false, const std::function<void()>& elemUpdate = nullptr);
|
||||
bool svg_icon_button(const std::string& id, const std::string& svgPath, bool isSelected = false, float size = 40.0f, const std::function<void()>& elemUpdate = nullptr);
|
||||
|
||||
void top_to_bottom_window_popup_layout(Clay_SizingAxis x, Clay_SizingAxis y, const std::function<void()>& elemUpdate);
|
||||
void left_to_right_layout(Clay_SizingAxis x, Clay_SizingAxis y, const std::function<void()>& elemUpdate);
|
||||
void left_to_right_line_layout(const std::function<void()>& elemUpdate);
|
||||
|
||||
bool selectable_button(const std::string& id, const std::function<void(SelectionHelper&, bool)>& elemUpdate, bool hasBorder, bool hasBorderPadding, bool isSelected);
|
||||
|
||||
void push_id(int64_t id);
|
||||
void push_id(const std::string& id);
|
||||
void pop_id();
|
||||
|
||||
void scroll_bar_area(const std::string& uniqueId, const std::function<void(float scrollContentHeight, float containerHeight, float scrollAmount)>& elemUpdate);
|
||||
void scroll_bar_many_entries_area(const std::string& uniqueId, float entryHeight, size_t entryCount, const std::function<void(size_t elementIndex, bool listHovered)>& entryUpdate, const std::function<void(float scrollContentHeight, float containerHeight, float scrollAmount)>& elemUpdate = nullptr);
|
||||
|
||||
void input_color_component_255(const std::string& id, float* val, const std::function<void()>& elemUpdate = nullptr);
|
||||
void input_text(const std::string& id, std::string* val, const std::function<void()>& elemUpdate = nullptr);
|
||||
|
||||
bool radio_button(const std::string& id, bool val, const std::function<void()>& elemUpdate = nullptr);
|
||||
bool radio_button_field(const std::string& id, const std::string& name, bool val, const std::function<void()>& elemUpdate = nullptr);
|
||||
|
||||
void checkbox_field(const std::string& id, const std::string& name, bool* val, const std::function<void()>& elemUpdate = nullptr);
|
||||
void checkbox(const std::string& id, bool* val, const std::function<void()>& elemUpdate = nullptr);
|
||||
|
||||
void tab_list(const std::string& id, const std::vector<std::pair<std::string, std::string>>& tabNames, size_t& selectedTab, std::optional<size_t>& closedTab, const std::function<void()>& elemUpdate = nullptr);
|
||||
|
||||
void input_path(const std::string& id, std::filesystem::path* val, std::filesystem::file_type fileTypeRestriction, const std::function<void()>& elemUpdate = nullptr);
|
||||
void input_path_field(const std::string& id, const std::string& name, std::filesystem::path* val, std::filesystem::file_type fileTypeRestriction, const std::function<void()>& elemUpdate = nullptr);
|
||||
|
||||
void dropdown_select(const std::string& id, size_t* val, const std::vector<std::string>& selections, float width = 200.0f, const std::function<void()>& hoverboxElemUpdate = nullptr);
|
||||
|
||||
void paint_circle_popup_menu(const std::string& id, const Vector2f& centerPos, const PaintCircleMenu::Data& val, const std::function<void()>& elemUpdate = nullptr);
|
||||
|
||||
void obstructing_window();
|
||||
|
||||
template <typename NewElement> NewElement* insert_element() {
|
||||
auto [it, inserted] = elements.emplace(idStack, ElementContainer());
|
||||
if(inserted)
|
||||
it->second = {std::make_unique<NewElement>()};
|
||||
return static_cast<NewElement*>(it->second.elem.get());
|
||||
}
|
||||
|
||||
template <typename T> T& insert_any(const T& def) {
|
||||
auto [it, inserted] = elements.emplace(idStack, ElementContainer());
|
||||
if(inserted)
|
||||
it->second.extra = def;
|
||||
return std::any_cast<T&>(it->second.extra);
|
||||
}
|
||||
|
||||
template <typename T> T& insert_any_with_id(int64_t id, const T& def) {
|
||||
push_id(id);
|
||||
T& toRet = insert_any(def);
|
||||
pop_id();
|
||||
return toRet;
|
||||
}
|
||||
|
||||
template <typename T> void input_generic(const std::string& id, T* val, const std::function<std::optional<T>(const std::string&)>& fromStr, const std::function<std::string(const T&)>& toStr, bool singleLine, const std::function<void()>& elemUpdate = nullptr) {
|
||||
push_id(id);
|
||||
insert_element<TextBox<T>>()->update(*io, val, fromStr, toStr, singleLine, elemUpdate);
|
||||
pop_id();
|
||||
}
|
||||
|
||||
template <typename T> void input_scalar(const std::string& id, T* val, T min, T max, int decimalPrecision = 0, const std::function<void()>& elemUpdate = nullptr) {
|
||||
input_generic<T>(id, val,
|
||||
[&](const std::string& a) {
|
||||
if(a.empty())
|
||||
return min;
|
||||
T toRet;
|
||||
std::stringstream ss;
|
||||
ss << a;
|
||||
ss >> toRet;
|
||||
if(ss.fail())
|
||||
return min;
|
||||
return std::clamp(toRet, min, max);
|
||||
},
|
||||
[&](const T& a) {
|
||||
std::stringstream ss;
|
||||
if(std::is_floating_point<T>()) {
|
||||
ss.precision(decimalPrecision);
|
||||
ss << std::fixed << *val;
|
||||
}
|
||||
else
|
||||
ss << *val;
|
||||
return ss.str();
|
||||
}, true, elemUpdate);
|
||||
}
|
||||
|
||||
void input_scalar(const std::string& id, uint8_t* val, uint8_t min, uint8_t max, int decimalPrecision, const std::function<void()>& elemUpdate);
|
||||
|
||||
template <typename TContainer, typename T> void input_scalar_fields(const std::string& id, const std::string& name, TContainer* val, size_t elemCount, T min, T max, int decimalPrecision = 0, const std::function<void()>& elemUpdate = nullptr) {
|
||||
push_id(id);
|
||||
left_to_right_line_layout([&]() {
|
||||
text_label(name);
|
||||
for(size_t i = 0; i < elemCount; i++)
|
||||
input_scalar<T>(std::to_string(i), &(*val)[i], min, max, decimalPrecision, elemUpdate);
|
||||
});
|
||||
pop_id();
|
||||
}
|
||||
|
||||
template <typename T> void input_color_hex(const std::string& id, T* val, bool selectAlpha, const std::function<void()>& elemUpdate = nullptr) {
|
||||
input_generic<T>(id, val,
|
||||
[&](const std::string& str) {
|
||||
T def = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
unsigned startIndex = 0;
|
||||
if(str.empty())
|
||||
return def;
|
||||
|
||||
if(str[0] == '#')
|
||||
startIndex++;
|
||||
|
||||
for(unsigned i = 0; i < (selectAlpha ? 4 : 3); i++) {
|
||||
int byteColor;
|
||||
std::stringstream ss;
|
||||
size_t startingAt = startIndex + i * 2;
|
||||
if(startingAt + 1 >= str.size())
|
||||
break;
|
||||
ss << str.substr(startingAt, 2);
|
||||
ss >> std::hex >> byteColor;
|
||||
if(ss.fail())
|
||||
break;
|
||||
def[i] = byteColor / 255.0f;
|
||||
}
|
||||
return def;
|
||||
},
|
||||
[&](const T& color) {
|
||||
std::stringstream ss;
|
||||
ss << "#" << std::setfill('0') << std::setw(2) << std::hex;
|
||||
for(size_t i = 0; i < (selectAlpha ? 4 : 3); i++) {
|
||||
int convertTo255 = static_cast<int>(std::clamp<float>(color[i], 0, 1) * 255);
|
||||
ss << std::setfill('0') << std::setw(2) << std::hex << convertTo255;
|
||||
}
|
||||
return ss.str();
|
||||
},
|
||||
true, elemUpdate);
|
||||
}
|
||||
|
||||
template <typename T> void color_picker(const std::string& id, T* val, bool selectAlpha, const std::function<void()>& elemUpdate = nullptr) {
|
||||
push_id(id);
|
||||
insert_element<ColorPicker<T>>()->update(*io, val, selectAlpha, elemUpdate);
|
||||
pop_id();
|
||||
}
|
||||
|
||||
template <typename T> void slider_scalar(const std::string& id, T* val, T min, T max, const std::function<void()>& elemUpdate = nullptr) {
|
||||
push_id(id);
|
||||
insert_element<NumberSlider<T>>()->update(*io, val, min, max, elemUpdate);
|
||||
pop_id();
|
||||
}
|
||||
|
||||
template <typename T> void input_scalar_field(const std::string& id, const std::string& name, T* val, T min, T max, int decimalPrecision = 0, const std::function<void()>& elemUpdate = nullptr) {
|
||||
left_to_right_line_layout([&]() {
|
||||
text_label(name);
|
||||
input_scalar(id, val, min, max, decimalPrecision, elemUpdate);
|
||||
});
|
||||
}
|
||||
|
||||
template <typename T> void slider_scalar_field(const std::string& id, const std::string& name, T* val, T min, T max, int decimalPrecision = 0, const std::function<void()>& elemUpdate = nullptr) {
|
||||
push_id(id);
|
||||
left_to_right_line_layout([&]() {
|
||||
text_label(name);
|
||||
input_scalar("0", val, min, max, decimalPrecision, elemUpdate);
|
||||
});
|
||||
slider_scalar("1", val, min, max, elemUpdate);
|
||||
pop_id();
|
||||
}
|
||||
|
||||
template <typename T> bool color_button(const std::string& id, T* val, bool isSelected = false, const std::function<void()>& elemUpdate = nullptr) {
|
||||
return selectable_button(id, [&](SelectionHelper& s, bool iS) {
|
||||
CLAY({.layout = {
|
||||
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)},
|
||||
},
|
||||
.backgroundColor = convert_vec4<Clay_Color>(*val),
|
||||
.cornerRadius = CLAY_CORNER_RADIUS(5),
|
||||
}) {}
|
||||
if(elemUpdate)
|
||||
elemUpdate();
|
||||
}, true, false, isSelected);
|
||||
}
|
||||
|
||||
template <typename T> void color_picker_button(const std::string& id, T* val, bool selectAlpha, const std::function<void()>& elemUpdate = nullptr) {
|
||||
push_id(id);
|
||||
bool& isOpen = insert_any_with_id<bool>(0, false);
|
||||
bool clicked;
|
||||
CLAY ({.layout = {.sizing = {.width = CLAY_SIZING_FIXED(40), .height = CLAY_SIZING_FIXED(40) } } }) {
|
||||
clicked = color_button("0", val, isOpen, elemUpdate);
|
||||
}
|
||||
if(clicked)
|
||||
isOpen = true;
|
||||
if(isOpen) {
|
||||
top_to_bottom_window_popup_layout(CLAY_SIZING_FIT(300), CLAY_SIZING_FIT(0), [&]() {
|
||||
color_picker_items("c", val, selectAlpha);
|
||||
if(io->mouse.leftClick && !Clay_Hovered() && !clicked)
|
||||
isOpen = false;
|
||||
});
|
||||
}
|
||||
pop_id();
|
||||
}
|
||||
|
||||
template <typename T> void color_picker_items(const std::string& id, T* val, bool selectAlpha) {
|
||||
push_id(id);
|
||||
color_picker("c", val, selectAlpha);
|
||||
left_to_right_line_layout([&]() {
|
||||
text_label("R");
|
||||
input_color_component_255("r", &(*val)[0]);
|
||||
text_label("G");
|
||||
input_color_component_255("g", &(*val)[1]);
|
||||
text_label("B");
|
||||
input_color_component_255("b", &(*val)[2]);
|
||||
if(selectAlpha) {
|
||||
text_label("A");
|
||||
input_color_component_255("a", &(*val)[3]);
|
||||
}
|
||||
});
|
||||
left_to_right_line_layout([&]() {
|
||||
text_label("Hex");
|
||||
input_color_hex("h", val, selectAlpha);
|
||||
});
|
||||
pop_id();
|
||||
}
|
||||
|
||||
template <typename T> void color_picker_button_field(const std::string& id, const std::string& name, T* val, bool selectAlpha, const std::function<void()>& elemUpdate = nullptr) {
|
||||
left_to_right_line_layout([&]() {
|
||||
color_picker_button(id, val, selectAlpha, elemUpdate);
|
||||
text_label(name);
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
static void clay_error_handler(Clay_ErrorData errorData);
|
||||
static Clay_Dimensions clay_skia_measure_text(Clay_StringSlice str, Clay_TextElementConfig* config, void* userData);
|
||||
|
||||
DefaultStringArena strArena;
|
||||
|
||||
Clay_Context* clayInstance;
|
||||
Clay_Arena clayArena;
|
||||
Clay_RenderCommandArray renderCommands;
|
||||
};
|
||||
|
||||
}
|
||||
19
src/GUIStuff/GUIManagerID.cpp
Normal file
19
src/GUIStuff/GUIManagerID.cpp
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#include "GUIManagerID.hpp"
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
GUIManagerID::GUIManagerID(int64_t numID):
|
||||
idIsStr(false),
|
||||
idNum(numID)
|
||||
{}
|
||||
|
||||
GUIManagerID::GUIManagerID(const std::string& strID):
|
||||
idIsStr(true),
|
||||
idStr(strID)
|
||||
{}
|
||||
|
||||
bool GUIManagerID::operator==(const GUIManagerID& other) const {
|
||||
return idIsStr == other.idIsStr && idStr == other.idStr && idNum == other.idNum;
|
||||
}
|
||||
|
||||
}
|
||||
40
src/GUIStuff/GUIManagerID.hpp
Normal file
40
src/GUIStuff/GUIManagerID.hpp
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#pragma once
|
||||
#include <stack>
|
||||
#include <Helpers/Hashes.hpp>
|
||||
|
||||
namespace GUIStuff {
|
||||
|
||||
struct GUIManagerID {
|
||||
GUIManagerID() = default;
|
||||
GUIManagerID(int64_t numID);
|
||||
GUIManagerID(const std::string& strID);
|
||||
bool operator==(const GUIManagerID& other) const;
|
||||
|
||||
bool idIsStr;
|
||||
std::string idStr;
|
||||
int64_t idNum = 0;
|
||||
};
|
||||
|
||||
typedef std::vector<GUIManagerID> GUIManagerIDStack;
|
||||
|
||||
}
|
||||
|
||||
template <> struct std::hash<GUIStuff::GUIManagerID> {
|
||||
size_t operator()(const GUIStuff::GUIManagerID& v) const
|
||||
{
|
||||
if(v.idIsStr)
|
||||
return std::hash<std::string>{}(v.idStr);
|
||||
else
|
||||
return std::hash<uint64_t>{}(v.idNum);
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct std::hash<GUIStuff::GUIManagerIDStack> {
|
||||
size_t operator()(const GUIStuff::GUIManagerIDStack& v) const
|
||||
{
|
||||
uint64_t h = 0;
|
||||
for(const auto& i : v)
|
||||
hash_combine(h, i);
|
||||
return h;
|
||||
}
|
||||
};
|
||||
99
src/GridManager.cpp
Normal file
99
src/GridManager.cpp
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#include "GridManager.hpp"
|
||||
#include "MainProgram.hpp"
|
||||
#include <include/core/SkPaint.h>
|
||||
#include <include/core/SkPathTypes.h>
|
||||
#include <Helpers/MathExtras.hpp>
|
||||
|
||||
#define GRID_CACHE_SIZE 10.0f
|
||||
#define GRID_CACHE_SIZE_INT 10
|
||||
#define GRID_DOT_SIZE 0.5f
|
||||
|
||||
const char* squareDotGridSkSl =
|
||||
R"V(half4 main(float2 fragCoord) {
|
||||
float m1 = step(mod(fragCoord.x + 0.25, 10.0), 0.5);
|
||||
float m2 = step(mod(fragCoord.y + 0.25, 10.0), 0.5);
|
||||
float fin = m1 * m2;
|
||||
return half4(fin);
|
||||
})V";
|
||||
|
||||
const char* circleDotGridSkSl =
|
||||
R"V(half4 main(float2 fragCoord) {
|
||||
float2 modCoord = mod(fragCoord + float2(5.0), float2(10.0));
|
||||
float fin = step(distance(float2(5.0), modCoord), 0.25);
|
||||
|
||||
return half4(fin);
|
||||
})V";
|
||||
|
||||
GridManager::GridManager(MainProgram& initMain):
|
||||
main(initMain)
|
||||
{
|
||||
auto [effect, err] = SkRuntimeEffect::MakeForShader(SkString(circleDotGridSkSl));
|
||||
if(!err.isEmpty()) {
|
||||
std::cout << "[GridManager::GridManager] Circle dot shader construction error\n";
|
||||
std::cout << err.c_str() << std::endl;
|
||||
}
|
||||
else
|
||||
circleDotShader = effect->makeShader(nullptr, {nullptr, 0});
|
||||
|
||||
auto [effect2, err2] = SkRuntimeEffect::MakeForShader(SkString(squareDotGridSkSl));
|
||||
if(!err2.isEmpty()) {
|
||||
std::cout << "[GridManager::GridManager] Square dot shader construction error\n";
|
||||
std::cout << err2.c_str() << std::endl;
|
||||
}
|
||||
else
|
||||
squareDotShader = effect2->makeShader(nullptr, {nullptr, 0});
|
||||
}
|
||||
|
||||
void GridManager::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
if(gridType == GRIDTYPE_NONE)
|
||||
return;
|
||||
if(oldWindowSize != main.window.size.cast<float>()) {
|
||||
gridPath = SkPath();
|
||||
float maxDim = std::max(main.window.size.x(), main.window.size.y()) * 2.0f;
|
||||
for(float i = 0.0f; i < maxDim; i += GRID_CACHE_SIZE) {
|
||||
gridPath.moveTo(i, -maxDim);
|
||||
gridPath.lineTo(i, maxDim);
|
||||
gridPath.moveTo(-maxDim, i);
|
||||
gridPath.lineTo(maxDim, i);
|
||||
}
|
||||
for(float i = 0.0f; i > -maxDim; i -= GRID_CACHE_SIZE) {
|
||||
gridPath.moveTo(i, -maxDim);
|
||||
gridPath.lineTo(i, maxDim);
|
||||
gridPath.moveTo(-maxDim, i);
|
||||
gridPath.lineTo(maxDim, i);
|
||||
}
|
||||
oldWindowSize = main.window.size.cast<float>();
|
||||
}
|
||||
float alteredZoom = static_cast<float>(((size / WorldScalar(GRID_CACHE_SIZE_INT)) / drawData.cam.c.inverseScale));
|
||||
if(alteredZoom > 0.5f) {
|
||||
Vector2f fracCamPos;
|
||||
|
||||
fracCamPos.x() = static_cast<float>((drawData.cam.c.pos.x() % size) / drawData.cam.c.inverseScale);
|
||||
fracCamPos.y() = static_cast<float>((drawData.cam.c.pos.y() % size) / drawData.cam.c.inverseScale);
|
||||
|
||||
SkPaint linePaint;
|
||||
if(gridType == GRIDTYPE_CIRCLEDOT || gridType == GRIDTYPE_SQUAREDOT) {
|
||||
linePaint.setShader(gridType == 1 ? circleDotShader : squareDotShader);
|
||||
linePaint.setAlphaf(std::clamp(alteredZoom / 20.0f, 0.0f, 0.4f));
|
||||
}
|
||||
else if(gridType == GRIDTYPE_LINE) {
|
||||
linePaint.setColor4f({1.0f, 1.0f, 1.0f, 1.0f});
|
||||
linePaint.setStyle(SkPaint::kStroke_Style);
|
||||
linePaint.setStrokeWidth(0.0f);
|
||||
linePaint.setAlphaf(std::clamp(alteredZoom / 35.0f, 0.0f, 0.3f));
|
||||
}
|
||||
|
||||
canvas->save();
|
||||
|
||||
canvas->rotate(-drawData.cam.c.rotation * 180.0 / std::numbers::pi);
|
||||
canvas->translate(-fracCamPos.x(), -fracCamPos.y());
|
||||
canvas->scale(alteredZoom, alteredZoom);
|
||||
|
||||
if(gridType == GRIDTYPE_CIRCLEDOT || gridType == GRIDTYPE_SQUAREDOT)
|
||||
canvas->drawPaint(linePaint);
|
||||
else if(gridType == GRIDTYPE_LINE)
|
||||
canvas->drawPath(gridPath, linePaint);
|
||||
|
||||
canvas->restore();
|
||||
}
|
||||
}
|
||||
41
src/GridManager.hpp
Normal file
41
src/GridManager.hpp
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
#include <Eigen/Dense>
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include "DrawData.hpp"
|
||||
#include <include/core/SkPath.h>
|
||||
#include <include/effects/SkRuntimeEffect.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using namespace Eigen;
|
||||
|
||||
class MainProgram;
|
||||
|
||||
class GridManager {
|
||||
public:
|
||||
GridManager(MainProgram& initMain);
|
||||
void draw(SkCanvas* canvas, const DrawData& drawData);
|
||||
|
||||
enum GridType : unsigned {
|
||||
GRIDTYPE_NONE = 0,
|
||||
GRIDTYPE_CIRCLEDOT,
|
||||
GRIDTYPE_SQUAREDOT,
|
||||
GRIDTYPE_LINE
|
||||
};
|
||||
|
||||
GridType gridType = GRIDTYPE_CIRCLEDOT;
|
||||
private:
|
||||
Vector2f oldWindowSize{0.0f, 0.0f};
|
||||
WorldScalar size{50000000};
|
||||
SkPath gridPath;
|
||||
sk_sp<SkShader> circleDotShader;
|
||||
sk_sp<SkShader> squareDotShader;
|
||||
sk_sp<SkShader> lineShader;
|
||||
MainProgram& main;
|
||||
};
|
||||
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM(GridManager::GridType, {
|
||||
{GridManager::GRIDTYPE_NONE, "None"},
|
||||
{GridManager::GRIDTYPE_CIRCLEDOT, "Circle Dot"},
|
||||
{GridManager::GRIDTYPE_SQUAREDOT, "Square Dot"},
|
||||
{GridManager::GRIDTYPE_LINE, "Line"}
|
||||
})
|
||||
379
src/InputManager.cpp
Normal file
379
src/InputManager.cpp
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
#include "InputManager.hpp"
|
||||
|
||||
#include <SDL3/SDL_clipboard.h>
|
||||
#include <SDL3/SDL_keycode.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <EmscriptenHelpers/emscripten_browser_clipboard.h>
|
||||
|
||||
extern "C" {
|
||||
int isAcceptingInputEmscripten = 0;
|
||||
EMSCRIPTEN_KEEPALIVE inline int is_text_input_happening() {
|
||||
return isAcceptingInputEmscripten;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
InputManager::InputManager() {
|
||||
frame_reset({0, 0});
|
||||
|
||||
defaultKeyAssignments[{0, SDLK_W}] = KEY_CAMERA_ROTATE_COUNTERCLOCKWISE;
|
||||
defaultKeyAssignments[{0, SDLK_Q}] = KEY_CAMERA_ROTATE_CLOCKWISE;
|
||||
defaultKeyAssignments[{0, SDLK_DELETE}] = KEY_DRAW_DELETE;
|
||||
defaultKeyAssignments[{0, SDLK_ESCAPE}] = KEY_DRAW_UNSELECT;
|
||||
defaultKeyAssignments[{CTRL_MOD, SDLK_Z}] = KEY_UNDO;
|
||||
defaultKeyAssignments[{CTRL_MOD, SDLK_R}] = KEY_REDO;
|
||||
defaultKeyAssignments[{0, SDLK_TAB}] = KEY_NOGUI;
|
||||
defaultKeyAssignments[{0, SDLK_F11}] = KEY_FULLSCREEN;
|
||||
defaultKeyAssignments[{CTRL_MOD, SDLK_S}] = KEY_SAVE;
|
||||
defaultKeyAssignments[{CTRL_MOD | SDL_KMOD_SHIFT, SDLK_S}] = KEY_SAVE_AS;
|
||||
defaultKeyAssignments[{CTRL_MOD, SDLK_C}] = KEY_COPY;
|
||||
defaultKeyAssignments[{CTRL_MOD, SDLK_X}] = KEY_CUT;
|
||||
defaultKeyAssignments[{CTRL_MOD, SDLK_V}] = KEY_PASTE;
|
||||
defaultKeyAssignments[{0, SDLK_B}] = KEY_DRAW_TOOL_BRUSH;
|
||||
defaultKeyAssignments[{0, SDLK_E}] = KEY_DRAW_TOOL_ERASER;
|
||||
defaultKeyAssignments[{0, SDLK_T}] = KEY_DRAW_TOOL_TEXTBOX;
|
||||
defaultKeyAssignments[{0, SDLK_R}] = KEY_DRAW_TOOL_RECTANGLE;
|
||||
defaultKeyAssignments[{0, SDLK_C}] = KEY_DRAW_TOOL_ELLIPSE;
|
||||
defaultKeyAssignments[{0, SDLK_X}] = KEY_DRAW_TOOL_RECTSELECT;
|
||||
defaultKeyAssignments[{0, SDLK_I}] = KEY_DRAW_TOOL_INKDROPPER;
|
||||
defaultKeyAssignments[{0, SDLK_S}] = KEY_DRAW_TOOL_EDIT;
|
||||
defaultKeyAssignments[{0, SDLK_P}] = KEY_DRAW_TOOL_SCREENSHOT;
|
||||
defaultKeyAssignments[{0, SDLK_F1}] = KEY_OPEN_CHAT;
|
||||
defaultKeyAssignments[{0, SDLK_F3}] = KEY_SHOW_METRICS;
|
||||
defaultKeyAssignments[{0, SDLK_F2}] = KEY_SHOW_PLAYER_LIST;
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// Without this, SDL eats the CTRL-V event that initiates the paste event
|
||||
// https://github.com/pthom/hello_imgui/issues/3#issuecomment-1564536870
|
||||
EM_ASM({
|
||||
window.addEventListener('keydown', function(event){
|
||||
if (event.ctrlKey && event.key == 'v') {
|
||||
if(Module["ccall"]('is_text_input_happening', 'number', [], []) === 1)
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
}, true);
|
||||
});
|
||||
|
||||
emscripten_browser_clipboard::paste([](std::string&& pasteData, void* callbackData){
|
||||
InputManager* inMan = (InputManager*)callbackData;
|
||||
inMan->clipboardPasteEventHappened = true;
|
||||
inMan->clipboardPasteEventData = pasteData;
|
||||
}, this);
|
||||
#endif
|
||||
|
||||
keyAssignments = defaultKeyAssignments;
|
||||
}
|
||||
|
||||
void InputManager::text_input_silence_everything() {
|
||||
text.set_accepting_input();
|
||||
for(auto& p : keys) {
|
||||
p.pressed = false;
|
||||
p.repeat = false;
|
||||
p.held = false;
|
||||
}
|
||||
text.newInput.clear();
|
||||
}
|
||||
|
||||
void InputManager::Text::set_accepting_input() {
|
||||
acceptingInputNew = true;
|
||||
}
|
||||
|
||||
bool InputManager::Text::get_accepting_input() {
|
||||
return acceptingInput;
|
||||
}
|
||||
|
||||
bool InputManager::get_clipboard_paste_happened() {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
return clipboardPasteEventHappened;
|
||||
#else
|
||||
return key(KEY_TEXT_PASTE).repeat;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string InputManager::get_clipboard_str() {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
return clipboardPasteEventData;
|
||||
#else
|
||||
char* data = SDL_GetClipboardText();
|
||||
std::string toRet(data);
|
||||
SDL_free(data);
|
||||
return toRet;
|
||||
#endif
|
||||
}
|
||||
|
||||
void InputManager::set_clipboard_str(std::string_view s) {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
emscripten_browser_clipboard::copy(std::string(s));
|
||||
#else
|
||||
SDL_SetClipboardText(s.data());
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string InputManager::key_assignment_to_str(const Vector2ui32& k) {
|
||||
std::string toRet;
|
||||
if(k.x() & SDL_KMOD_CTRL)
|
||||
toRet += "CTRL+";
|
||||
if(k.x() & SDL_KMOD_ALT)
|
||||
toRet += "ALT+";
|
||||
if(k.x() & SDL_KMOD_SHIFT)
|
||||
toRet += "SHIFT+";
|
||||
if(k.x() & SDL_KMOD_GUI)
|
||||
toRet += "META+";
|
||||
toRet += SDL_GetKeyName(k.y());
|
||||
return toRet;
|
||||
}
|
||||
|
||||
Vector2ui32 InputManager::key_assignment_from_str(const std::string& s) {
|
||||
Vector2ui32 toRet = {0, 0};
|
||||
if(s.find("CTRL+") != std::string::npos)
|
||||
toRet.x() |= SDL_KMOD_CTRL;
|
||||
if(s.find("ALT+") != std::string::npos)
|
||||
toRet.x() |= SDL_KMOD_ALT;
|
||||
if(s.find("SHIFT+") != std::string::npos)
|
||||
toRet.x() |= SDL_KMOD_SHIFT;
|
||||
if(s.find("META+") != std::string::npos)
|
||||
toRet.x() |= SDL_KMOD_GUI;
|
||||
size_t startInd = s.find_last_of("+");
|
||||
if(startInd == std::string::npos)
|
||||
startInd = 0;
|
||||
else
|
||||
startInd++;
|
||||
toRet.y() = SDL_GetKeyFromName(s.substr(startInd).c_str());
|
||||
if(toRet.y() == 0)
|
||||
toRet.x() = 0;
|
||||
return toRet;
|
||||
}
|
||||
|
||||
const InputManager::KeyData& InputManager::key(KeyCode kCode) {
|
||||
return keys[kCode];
|
||||
}
|
||||
|
||||
void InputManager::set_key_down(const SDL_KeyboardEvent& e, KeyCode kCode) {
|
||||
auto& k = keys[kCode];
|
||||
if(!k.held || (std::chrono::steady_clock::now() - k.lastPressTime) > std::chrono::milliseconds(500)) // Timer works around some systems that emit a keypress twice (a repeat and a press event)
|
||||
k.repeat = true;
|
||||
if(!k.held) { // We use if statements instead of setting booleans so that values arent forced to false
|
||||
k.pressed = true;
|
||||
k.lastPressTime = std::chrono::steady_clock::now();
|
||||
}
|
||||
k.held = true;
|
||||
}
|
||||
|
||||
void InputManager::set_key_up(const SDL_KeyboardEvent& e, KeyCode kCode) {
|
||||
keys[kCode].held = false;
|
||||
}
|
||||
|
||||
void InputManager::set_pen_button_down(const SDL_PenButtonEvent& e) {
|
||||
pen.buttons[e.button].held = true;
|
||||
pen.buttons[e.button].pressed = true;
|
||||
}
|
||||
|
||||
void InputManager::set_pen_button_up(const SDL_PenButtonEvent& e) {
|
||||
pen.buttons[e.button].held = false;
|
||||
}
|
||||
|
||||
uint32_t InputManager::make_generic_key_mod(SDL_Keymod m) {
|
||||
uint32_t toRet = 0;
|
||||
if(m & SDL_KMOD_CTRL)
|
||||
toRet |= SDL_KMOD_CTRL;
|
||||
if(m & SDL_KMOD_ALT)
|
||||
toRet |= SDL_KMOD_ALT;
|
||||
if(m & SDL_KMOD_GUI)
|
||||
toRet |= SDL_KMOD_GUI;
|
||||
if(m & SDL_KMOD_SHIFT)
|
||||
toRet |= SDL_KMOD_SHIFT;
|
||||
return toRet;
|
||||
}
|
||||
|
||||
void InputManager::backend_key_down_update(const SDL_KeyboardEvent& e) {
|
||||
auto kPress = e.key;
|
||||
auto kMod = e.mod;
|
||||
|
||||
if(kPress != SDLK_LSHIFT && kPress != SDLK_RSHIFT && kPress != SDLK_LCTRL && kPress != SDLK_RCTRL &&
|
||||
kPress != SDLK_LGUI && kPress != SDLK_RGUI &&
|
||||
kPress != SDLK_LALT && kPress != SDLK_RALT) {
|
||||
lastPressedKeybind = {make_generic_key_mod(kMod), kPress};
|
||||
}
|
||||
|
||||
// e.key != SDLK_LMETA && e.key != SDLK_RMETA &&
|
||||
|
||||
if(stopKeyInput)
|
||||
return;
|
||||
|
||||
switch(kPress) {
|
||||
case SDLK_UP:
|
||||
set_key_down(e, KEY_TEXT_UP);
|
||||
break;
|
||||
case SDLK_DOWN:
|
||||
set_key_down(e, KEY_TEXT_DOWN);
|
||||
break;
|
||||
case SDLK_LEFT:
|
||||
set_key_down(e, KEY_TEXT_LEFT);
|
||||
break;
|
||||
case SDLK_RIGHT:
|
||||
set_key_down(e, KEY_TEXT_RIGHT);
|
||||
break;
|
||||
case SDLK_BACKSPACE:
|
||||
set_key_down(e, KEY_TEXT_BACKSPACE);
|
||||
break;
|
||||
case SDLK_DELETE:
|
||||
set_key_down(e, KEY_TEXT_DELETE);
|
||||
break;
|
||||
case SDLK_HOME:
|
||||
set_key_down(e, KEY_TEXT_HOME);
|
||||
break;
|
||||
case SDLK_LSHIFT:
|
||||
set_key_down(e, KEY_ZOOM_SHIFT);
|
||||
set_key_down(e, KEY_TEXT_SHIFT);
|
||||
set_key_down(e, KEY_EQUAL_DIMENSIONS);
|
||||
break;
|
||||
case SDLK_LCTRL:
|
||||
set_key_down(e, KEY_TEXT_CTRL);
|
||||
set_key_down(e, KEY_ZOOM_CTRL);
|
||||
break;
|
||||
case SDLK_C:
|
||||
// Use either Ctrl or Meta (command for Mac) keys. We do either instead of checking, since checking can get complicated on Emscripten
|
||||
if((kMod & SDL_KMOD_GUI) || (kMod & SDL_KMOD_CTRL))
|
||||
set_key_down(e, KEY_TEXT_COPY);
|
||||
break;
|
||||
case SDLK_X:
|
||||
if((kMod & SDL_KMOD_GUI) || (kMod & SDL_KMOD_CTRL))
|
||||
set_key_down(e, KEY_TEXT_CUT);
|
||||
break;
|
||||
case SDLK_V:
|
||||
if((kMod & SDL_KMOD_GUI) || (kMod & SDL_KMOD_CTRL))
|
||||
set_key_down(e, KEY_TEXT_PASTE);
|
||||
break;
|
||||
case SDLK_A:
|
||||
if((kMod & SDL_KMOD_GUI) || (kMod & SDL_KMOD_CTRL))
|
||||
set_key_down(e, KEY_TEXT_SELECTALL);
|
||||
break;
|
||||
case SDLK_RETURN:
|
||||
set_key_down(e, KEY_TEXT_ENTER);
|
||||
break;
|
||||
case SDLK_ESCAPE:
|
||||
set_key_down(e, KEY_TEXT_ESCAPE);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if(text.get_accepting_input())
|
||||
return;
|
||||
|
||||
auto f = keyAssignments.find({make_generic_key_mod(kMod), kPress});
|
||||
if(f != keyAssignments.end())
|
||||
set_key_down(e, f->second);
|
||||
}
|
||||
|
||||
void InputManager::backend_key_up_update(const SDL_KeyboardEvent& e) {
|
||||
auto kPress = e.key;
|
||||
//auto kMod = e.mod;
|
||||
|
||||
switch(kPress) {
|
||||
case SDLK_UP:
|
||||
set_key_up(e, KEY_TEXT_UP);
|
||||
break;
|
||||
case SDLK_DOWN:
|
||||
set_key_up(e, KEY_TEXT_DOWN);
|
||||
break;
|
||||
case SDLK_LEFT:
|
||||
set_key_up(e, KEY_TEXT_LEFT);
|
||||
break;
|
||||
case SDLK_RIGHT:
|
||||
set_key_up(e, KEY_TEXT_RIGHT);
|
||||
break;
|
||||
case SDLK_BACKSPACE:
|
||||
set_key_up(e, KEY_TEXT_BACKSPACE);
|
||||
break;
|
||||
case SDLK_DELETE:
|
||||
set_key_up(e, KEY_TEXT_DELETE);
|
||||
break;
|
||||
case SDLK_HOME:
|
||||
set_key_up(e, KEY_TEXT_HOME);
|
||||
break;
|
||||
case SDLK_LSHIFT:
|
||||
set_key_up(e, KEY_ZOOM_SHIFT);
|
||||
set_key_up(e, KEY_TEXT_SHIFT);
|
||||
set_key_up(e, KEY_EQUAL_DIMENSIONS);
|
||||
break;
|
||||
case SDLK_LCTRL:
|
||||
set_key_up(e, KEY_TEXT_CTRL);
|
||||
set_key_up(e, KEY_ZOOM_CTRL);
|
||||
break;
|
||||
case SDLK_C:
|
||||
set_key_up(e, KEY_TEXT_COPY);
|
||||
break;
|
||||
case SDLK_X:
|
||||
set_key_up(e, KEY_TEXT_CUT);
|
||||
break;
|
||||
case SDLK_V:
|
||||
set_key_up(e, KEY_TEXT_PASTE);
|
||||
break;
|
||||
case SDLK_A:
|
||||
set_key_up(e, KEY_TEXT_SELECTALL);
|
||||
break;
|
||||
case SDLK_RETURN:
|
||||
set_key_up(e, KEY_TEXT_ENTER);
|
||||
break;
|
||||
case SDLK_ESCAPE:
|
||||
set_key_up(e, KEY_TEXT_ESCAPE);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
for(auto& p : keyAssignments) {
|
||||
if(p.first.y() == kPress) // Dont try matching modifiers when setting key to go up
|
||||
set_key_up(e, p.second);
|
||||
}
|
||||
}
|
||||
|
||||
void InputManager::stop_key_input() {
|
||||
stopKeyInput = 2;
|
||||
}
|
||||
|
||||
void InputManager::Mouse::set_pos(const Vector2f& newPos) {
|
||||
pos = newPos;
|
||||
move = newPos - lastPos;
|
||||
}
|
||||
|
||||
void InputManager::frame_reset(const Vector2i& windowSize) {
|
||||
mouse.lastPos = mouse.pos;
|
||||
mouse.move = Vector2f({0.0f, 0.0f});
|
||||
mouse.scrollAmount = Vector2f({0.0f, 0.0f});
|
||||
mouse.leftClicks = 0;
|
||||
mouse.rightClicks = 0;
|
||||
mouse.middleClicks = 0;
|
||||
text.acceptingInput = text.acceptingInputNew;
|
||||
text.acceptingInputNew = false;
|
||||
text.newInput.clear();
|
||||
droppedItems.clear();
|
||||
|
||||
cursorIcon = SystemCursorType::DEFAULT;
|
||||
|
||||
for(auto& p : keys) {
|
||||
p.pressed = false;
|
||||
p.repeat = false;
|
||||
}
|
||||
|
||||
for(auto& p : pen.buttons)
|
||||
p.pressed = false;
|
||||
|
||||
lastPressedKeybind = std::nullopt;
|
||||
if(stopKeyInput)
|
||||
stopKeyInput--;
|
||||
|
||||
toggleFullscreen = false;
|
||||
hideCursor = false;
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
clipboardPasteEventHappened = false;
|
||||
clipboardPasteEventData.clear();
|
||||
isAcceptingInputEmscripten = text.acceptingInput ? 1 : 0;
|
||||
#endif
|
||||
}
|
||||
220
src/InputManager.hpp
Normal file
220
src/InputManager.hpp
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
#pragma once
|
||||
#include <Eigen/Dense>
|
||||
#include <array>
|
||||
#include "TimePoint.hpp"
|
||||
#include <Helpers/Hashes.hpp>
|
||||
#include <unordered_map>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <queue>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <SDL3/SDL_keycode.h>
|
||||
|
||||
using namespace Eigen;
|
||||
|
||||
struct InputManager {
|
||||
InputManager();
|
||||
|
||||
void frame_reset(const Vector2i& windowSize);
|
||||
|
||||
// Just renamed SDL3 cursor types, but if we need to change to a different backend we can
|
||||
// translate this enum to different commands in main.cpp
|
||||
enum class SystemCursorType : unsigned {
|
||||
DEFAULT = 0,
|
||||
TEXT,
|
||||
WAIT,
|
||||
CROSSHAIR,
|
||||
PROGRESS,
|
||||
NWSE_RESIZE,
|
||||
NESW_RESIZE,
|
||||
EW_RESIZE,
|
||||
NS_RESIZE,
|
||||
MOVE,
|
||||
NOT_ALLOWED,
|
||||
POINTER,
|
||||
NW_RESIZE,
|
||||
N_RESIZE,
|
||||
NE_RESIZE,
|
||||
E_RESIZE,
|
||||
SE_RESIZE,
|
||||
S_RESIZE,
|
||||
SW_RESIZE,
|
||||
W_RESIZE
|
||||
} cursorIcon;
|
||||
bool hideCursor = false;
|
||||
|
||||
bool toggleFullscreen = false;
|
||||
|
||||
struct KeyData {
|
||||
bool pressed = false;
|
||||
bool held = false;
|
||||
bool repeat = false;
|
||||
std::chrono::steady_clock::time_point lastPressTime;
|
||||
};
|
||||
|
||||
struct Mouse {
|
||||
void set_pos(const Vector2f& newPos);
|
||||
Vector2f scrollAmount = {0, 0};
|
||||
Vector2f pos = {0, 0};
|
||||
Vector2f previousPos = {0, 0};
|
||||
Vector2f move = {0, 0};
|
||||
bool leftDown = false;
|
||||
bool rightDown = false;
|
||||
bool middleDown = false;
|
||||
uint8_t leftClicks = 0;
|
||||
uint8_t rightClicks = 0;
|
||||
uint8_t middleClicks = 0;
|
||||
Vector2f lastPos = {0, 0};
|
||||
friend struct InputManager;
|
||||
} mouse;
|
||||
|
||||
struct Pen {
|
||||
bool inProximity = false;
|
||||
bool isDown = false;
|
||||
bool isEraser = false;
|
||||
float pressure = 0.0f;
|
||||
std::array<KeyData, 256> buttons;
|
||||
} pen;
|
||||
|
||||
struct Text {
|
||||
std::string newInput;
|
||||
bool lastAcceptingTextInputVal = false;
|
||||
|
||||
void set_accepting_input();
|
||||
bool get_accepting_input();
|
||||
private:
|
||||
bool acceptingInput = false;
|
||||
bool acceptingInputNew = false;
|
||||
friend struct InputManager;
|
||||
} text;
|
||||
|
||||
struct DroppedItem {
|
||||
bool isFile;
|
||||
Vector2f pos;
|
||||
std::string droppedData;
|
||||
};
|
||||
|
||||
#ifdef __APPLE__
|
||||
SDL_Keymod CTRL_MOD = SDL_KMOD_GUI;
|
||||
#else
|
||||
SDL_Keymod CTRL_MOD = SDL_KMOD_CTRL;
|
||||
#endif
|
||||
|
||||
int stopKeyInput = 0;
|
||||
std::optional<Vector2ui32> lastPressedKeybind;
|
||||
|
||||
std::vector<DroppedItem> droppedItems;
|
||||
|
||||
void text_input_silence_everything();
|
||||
|
||||
enum KeyCodeEnum : unsigned {
|
||||
// Assignable
|
||||
KEY_CAMERA_ROTATE_CLOCKWISE = 0,
|
||||
KEY_CAMERA_ROTATE_COUNTERCLOCKWISE,
|
||||
KEY_DRAW_DELETE,
|
||||
KEY_DRAW_UNSELECT,
|
||||
KEY_UNDO,
|
||||
KEY_REDO,
|
||||
KEY_NOGUI,
|
||||
KEY_FULLSCREEN,
|
||||
KEY_SAVE,
|
||||
KEY_SAVE_AS,
|
||||
KEY_COPY,
|
||||
KEY_CUT,
|
||||
KEY_PASTE,
|
||||
KEY_DRAW_TOOL_BRUSH,
|
||||
KEY_DRAW_TOOL_ERASER,
|
||||
KEY_DRAW_TOOL_TEXTBOX,
|
||||
KEY_DRAW_TOOL_RECTANGLE,
|
||||
KEY_DRAW_TOOL_ELLIPSE,
|
||||
KEY_DRAW_TOOL_RECTSELECT,
|
||||
KEY_DRAW_TOOL_EDIT,
|
||||
KEY_DRAW_TOOL_INKDROPPER,
|
||||
KEY_DRAW_TOOL_SCREENSHOT,
|
||||
KEY_SHOW_METRICS,
|
||||
KEY_OPEN_CHAT,
|
||||
KEY_SHOW_PLAYER_LIST,
|
||||
|
||||
KEY_ASSIGNABLE_COUNT, // Not a real key
|
||||
|
||||
// Unassignable
|
||||
KEY_TEXT_DOWN,
|
||||
KEY_TEXT_UP,
|
||||
KEY_TEXT_LEFT,
|
||||
KEY_TEXT_RIGHT,
|
||||
KEY_TEXT_BACKSPACE,
|
||||
KEY_TEXT_DELETE,
|
||||
KEY_TEXT_HOME,
|
||||
KEY_TEXT_SHIFT,
|
||||
KEY_TEXT_COPY,
|
||||
KEY_TEXT_CUT,
|
||||
KEY_TEXT_PASTE,
|
||||
KEY_TEXT_ENTER,
|
||||
KEY_TEXT_CTRL,
|
||||
KEY_TEXT_SELECTALL,
|
||||
KEY_TEXT_ESCAPE,
|
||||
KEY_ZOOM_SHIFT,
|
||||
KEY_ZOOM_CTRL,
|
||||
KEY_EQUAL_DIMENSIONS,
|
||||
KEY_COUNT
|
||||
};
|
||||
|
||||
typedef unsigned KeyCode;
|
||||
|
||||
void stop_key_input();
|
||||
void set_pen_button_up(const SDL_PenButtonEvent& e);
|
||||
void set_pen_button_down(const SDL_PenButtonEvent& e);
|
||||
uint32_t make_generic_key_mod(SDL_Keymod m);
|
||||
|
||||
void set_key_up(const SDL_KeyboardEvent& e, KeyCode kCode);
|
||||
void set_key_down(const SDL_KeyboardEvent& e, KeyCode kCode);
|
||||
void backend_key_up_update(const SDL_KeyboardEvent& e);
|
||||
void backend_key_down_update(const SDL_KeyboardEvent& e);
|
||||
|
||||
bool get_clipboard_paste_happened();
|
||||
std::string get_clipboard_str();
|
||||
void set_clipboard_str(std::string_view s);
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
bool clipboardPasteEventHappened = false;
|
||||
std::string clipboardPasteEventData;
|
||||
#endif
|
||||
|
||||
std::string key_assignment_to_str(const Vector2ui32& k);
|
||||
Vector2ui32 key_assignment_from_str(const std::string& s);
|
||||
|
||||
const KeyData& key(KeyCode kCode);
|
||||
|
||||
std::unordered_map<Vector2ui32, KeyCode> keyAssignments;
|
||||
std::unordered_map<Vector2ui32, KeyCode> defaultKeyAssignments;
|
||||
std::array<KeyData, KEY_COUNT> keys;
|
||||
};
|
||||
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM(InputManager::KeyCodeEnum, {
|
||||
{InputManager::KEY_CAMERA_ROTATE_CLOCKWISE, "Rotate Canvas CW"},
|
||||
{InputManager::KEY_CAMERA_ROTATE_COUNTERCLOCKWISE, "Rotate Canvas CCW"},
|
||||
{InputManager::KEY_DRAW_DELETE, "Delete Selection"},
|
||||
{InputManager::KEY_DRAW_UNSELECT, "Unselect Elements"},
|
||||
{InputManager::KEY_UNDO, "Undo"},
|
||||
{InputManager::KEY_REDO, "Redo"},
|
||||
{InputManager::KEY_NOGUI, "Toggle GUI"},
|
||||
{InputManager::KEY_FULLSCREEN, "Toggle Fullscreen"},
|
||||
{InputManager::KEY_SAVE, "Save"},
|
||||
{InputManager::KEY_SAVE_AS, "Save As"},
|
||||
{InputManager::KEY_COPY, "Copy"},
|
||||
{InputManager::KEY_CUT, "Cut"},
|
||||
{InputManager::KEY_PASTE, "Paste"},
|
||||
{InputManager::KEY_DRAW_TOOL_BRUSH, "Brush Tool"},
|
||||
{InputManager::KEY_DRAW_TOOL_ERASER, "Eraser Tool"},
|
||||
{InputManager::KEY_DRAW_TOOL_TEXTBOX, "Textbox Tool"},
|
||||
{InputManager::KEY_DRAW_TOOL_RECTANGLE, "Rectangle Tool"},
|
||||
{InputManager::KEY_DRAW_TOOL_ELLIPSE, "Ellipse Tool"},
|
||||
{InputManager::KEY_DRAW_TOOL_RECTSELECT, "Rect Select Tool"},
|
||||
{InputManager::KEY_DRAW_TOOL_INKDROPPER, "Color Select Tool"},
|
||||
{InputManager::KEY_DRAW_TOOL_EDIT, "Edit Tool"},
|
||||
{InputManager::KEY_DRAW_TOOL_SCREENSHOT, "Screenshot Tool"},
|
||||
{InputManager::KEY_SHOW_METRICS, "Show Metrics"},
|
||||
{InputManager::KEY_OPEN_CHAT, "Open Chat"},
|
||||
{InputManager::KEY_SHOW_PLAYER_LIST, "Show Player List"}
|
||||
})
|
||||
277
src/MainProgram.cpp
Executable file
277
src/MainProgram.cpp
Executable file
|
|
@ -0,0 +1,277 @@
|
|||
#include "MainProgram.hpp"
|
||||
#include "cereal/archives/portable_binary.hpp"
|
||||
#include "cereal/details/helpers.hpp"
|
||||
#include <chrono>
|
||||
#include <include/core/SkAlphaType.h>
|
||||
#include <include/core/SkColor.h>
|
||||
#include <include/core/SkPaint.h>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <include/core/SkFont.h>
|
||||
#include <include/core/SkData.h>
|
||||
#include <include/core/SkSurfaceProps.h>
|
||||
#include "InputManager.hpp"
|
||||
#include "Server/CommandList.hpp"
|
||||
#include <cereal/types/vector.hpp>
|
||||
#include <cereal/types/unordered_map.hpp>
|
||||
#include <Eigen/Core>
|
||||
#include <fstream>
|
||||
#include <cereal/archives/json.hpp>
|
||||
#include <Helpers/Serializers.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <include/core/SkSurface.h>
|
||||
#ifdef USE_SKIA_BACKEND_GRAPHITE
|
||||
#include <include/gpu/graphite/Surface.h>
|
||||
#elif USE_SKIA_BACKEND_GANESH
|
||||
#include <include/gpu/ganesh/GrDirectContext.h>
|
||||
#include <include/gpu/ganesh/SkSurfaceGanesh.h>
|
||||
#endif
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
|
||||
#include <Helpers/Logger.hpp>
|
||||
#include <Helpers/Networking/NetLibrary.hpp>
|
||||
|
||||
MainProgram::MainProgram():
|
||||
toolbar(*this),
|
||||
grid(*this)
|
||||
{
|
||||
displayName = Random::get().alphanumeric_str(10);
|
||||
|
||||
Logger::get().add_log("WORLDFATAL", [&](const std::string& text) {
|
||||
*logFile << "[WORLDFATAL] " << text << std::endl;
|
||||
std::cout << "[WORLDFATAL] " << text << std::endl;
|
||||
logMessages.emplace_front(Toolbar::LogMessage{text, Toolbar::LogMessage::COLOR_ERROR});
|
||||
if(logMessages.size() == LOG_SIZE)
|
||||
logMessages.pop_back();
|
||||
});
|
||||
|
||||
Logger::get().add_log("USERINFO", [&](const std::string& text) {
|
||||
*logFile << "[USERINFO] " << text << std::endl;
|
||||
std::cout << "[USERINFO] " << text << std::endl;
|
||||
logMessages.emplace_front(Toolbar::LogMessage{text, Toolbar::LogMessage::COLOR_NORMAL});
|
||||
if(logMessages.size() == LOG_SIZE)
|
||||
logMessages.pop_back();
|
||||
});
|
||||
|
||||
Logger::get().add_log("CHAT", [&](const std::string& text) {
|
||||
*logFile << "[CHAT] " << text << std::endl;
|
||||
std::cout << "[CHAT] " << text << std::endl;
|
||||
});
|
||||
}
|
||||
|
||||
void MainProgram::init_draw_program_cache() {
|
||||
if(window.size.x() < 0 || window.size.y() < 0)
|
||||
return;
|
||||
SkImageInfo imgInfo = SkImageInfo::MakeN32Premul(window.size.x(), window.size.y());
|
||||
#ifdef USE_SKIA_BACKEND_GRAPHITE
|
||||
drawProgCache.surface = SkSurfaces::RenderTarget(window.recorder(), imgInfo, skgpu::Mipmapped::kNo, window.defaultMSAASurfaceProps);
|
||||
#elif USE_SKIA_BACKEND_GANESH
|
||||
drawProgCache.surface = SkSurfaces::RenderTarget(window.ctx.get(), skgpu::Budgeted::kNo, imgInfo, window.defaultMSAASampleCount, &window.defaultMSAASurfaceProps);
|
||||
#endif
|
||||
if(!drawProgCache.surface)
|
||||
throw std::runtime_error("[MainProgram::init_draw_program_cache] Could not make surface");
|
||||
drawProgCache.canvas = drawProgCache.surface->getCanvas();
|
||||
|
||||
if(!drawProgCache.canvas)
|
||||
throw std::runtime_error("[MainProgram::init_draw_program_cache] Could not create canvas");
|
||||
|
||||
drawProgCache.canvas->clear(SkColor4f{0, 0, 0, 0});
|
||||
|
||||
drawProgCache.refresh = true;
|
||||
}
|
||||
|
||||
|
||||
void MainProgram::update() {
|
||||
while(std::chrono::duration_cast<std::chrono::duration<float>>(std::chrono::steady_clock::now() - lastFrameTime).count() < 1.0 / fpsLimit);
|
||||
lastFrameTime = std::chrono::steady_clock::now();
|
||||
|
||||
std::erase_if(input.droppedItems, [&](auto& droppedItem) {
|
||||
if(droppedItem.isFile && std::filesystem::is_regular_file(droppedItem.droppedData)) {
|
||||
std::filesystem::path droppedFilePath(droppedItem.droppedData);
|
||||
if(droppedFilePath.has_extension() && droppedFilePath.extension().string() == std::string("." + World::FILE_EXTENSION)) {
|
||||
new_tab({
|
||||
.conType = World::CONNECTIONTYPE_LOCAL,
|
||||
.fileSource = droppedFilePath.string()
|
||||
}, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if(setTabToClose) {
|
||||
worlds.erase(worlds.begin() + setTabToClose.value());
|
||||
setTabToClose.reset();
|
||||
}
|
||||
|
||||
if(worldIndex >= worlds.size())
|
||||
worldIndex = worlds.size() - 1;
|
||||
if(worlds.size() == 0)
|
||||
new_tab({
|
||||
.conType = World::CONNECTIONTYPE_LOCAL
|
||||
}, true);
|
||||
|
||||
std::shared_ptr<World> oldWorld = world;
|
||||
world = worlds[worldIndex];
|
||||
if(oldWorld != world)
|
||||
drawProgCache.refresh = true;
|
||||
|
||||
deltaTime.update_time_since();
|
||||
deltaTime.update_time_point();
|
||||
|
||||
toolbar.update();
|
||||
|
||||
if(input.key(InputManager::KEY_NOGUI).pressed)
|
||||
drawGui = !drawGui;
|
||||
|
||||
if(input.key(InputManager::KEY_FULLSCREEN).pressed)
|
||||
input.toggleFullscreen = true;
|
||||
|
||||
for(auto& w : worlds) {
|
||||
if(w == world)
|
||||
w->focus_update();
|
||||
else
|
||||
w->unfocus_update();
|
||||
}
|
||||
|
||||
NetLibrary::update();
|
||||
|
||||
if(tabSetToOpen) {
|
||||
tabSetToOpen = false;
|
||||
new_tab_open();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void MainProgram::new_tab(const World::OpenWorldInfo& tabInfo, bool createSameThread) {
|
||||
newTabToOpenInfo = tabInfo;
|
||||
if(createSameThread)
|
||||
new_tab_open();
|
||||
else
|
||||
tabSetToOpen = true;
|
||||
}
|
||||
|
||||
void MainProgram::new_tab_open() {
|
||||
std::unique_ptr<World> newWorld;
|
||||
try {
|
||||
newWorld = std::make_unique<World>(*this, newTabToOpenInfo);
|
||||
}
|
||||
catch(const std::runtime_error& e) {
|
||||
Logger::get().log("WORLDFATAL", "Failed to open canvas: " + std::string(newTabToOpenInfo.fileSource) + " with error: " + e.what());
|
||||
return;
|
||||
}
|
||||
worlds.emplace_back(std::move(newWorld));
|
||||
worldIndex = worlds.size() - 1;
|
||||
}
|
||||
|
||||
void MainProgram::set_tab_to_close(size_t tabToClose) {
|
||||
setTabToClose = tabToClose;
|
||||
}
|
||||
|
||||
void MainProgram::save_config() {
|
||||
std::ofstream f(configPath / "config.json");
|
||||
if(f.is_open()) {
|
||||
using json = nlohmann::json;
|
||||
json j;
|
||||
|
||||
j["settings"] = toolbar.get_config_json();
|
||||
j["window"]["pos"] = window.writtenPos;
|
||||
j["window"]["size"] = window.writtenSize;
|
||||
j["window"]["maximized"] = window.maximized;
|
||||
j["window"]["fullscreen"] = window.fullscreen;
|
||||
j["fileselectorpath"] = toolbar.file_selector_path();
|
||||
|
||||
f << std::setw(4) << j;
|
||||
f.close();
|
||||
}
|
||||
toolbar.save_palettes();
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
EM_ASM(
|
||||
FS.syncfs(false, (err) => {
|
||||
if(err)
|
||||
console.log("Error syncing IDBFS: ", err);
|
||||
else
|
||||
console.log("Synced to IDBFS");
|
||||
});
|
||||
);
|
||||
#endif
|
||||
}
|
||||
|
||||
void MainProgram::load_config() {
|
||||
std::ifstream f(configPath / "config.json");
|
||||
toolbar.file_selector_path() = homePath;
|
||||
if(f.is_open()) {
|
||||
using json = nlohmann::json;
|
||||
try {
|
||||
json j;
|
||||
f >> j;
|
||||
try {
|
||||
toolbar.set_config_json(j["settings"]);
|
||||
}
|
||||
catch(...) {}
|
||||
#ifndef __EMSCRIPTEN__
|
||||
try {
|
||||
j.at("window").at("size").get_to(window.size);
|
||||
window.writtenSize = window.size;
|
||||
j.at("window").at("pos").get_to(window.pos);
|
||||
window.writtenPos = window.pos;
|
||||
j.at("window").at("maximized").get_to(window.maximized);
|
||||
j.at("window").at("fullscreen").get_to(window.fullscreen);
|
||||
}
|
||||
catch(...) {}
|
||||
try {
|
||||
j.at("fileselectorpath").get_to(toolbar.file_selector_path());
|
||||
}
|
||||
catch(...) {
|
||||
toolbar.file_selector_path() = homePath;
|
||||
}
|
||||
#endif
|
||||
f.close();
|
||||
} catch(...) {}
|
||||
}
|
||||
#ifdef __EMSCRIPTEN__
|
||||
else
|
||||
toolbar.viewWebVersionWelcome = true;
|
||||
#endif
|
||||
toolbar.load_palettes();
|
||||
toolbar.load_theme();
|
||||
toolbar.load_licenses();
|
||||
}
|
||||
|
||||
void MainProgram::draw(SkCanvas* canvas) {
|
||||
canvas->clear(transparentBackground ? SkColor4f{0.0f, 0.0f, 0.0f, 0.0f} : canvasTheme.backColor);
|
||||
grid.draw(canvas, world->drawData);
|
||||
world->draw(canvas);
|
||||
toolbar.draw(canvas);
|
||||
}
|
||||
|
||||
bool MainProgram::network_being_used() {
|
||||
for(auto& w : worlds) {
|
||||
if(w->network_being_used())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MainProgram::net_server_hosted() {
|
||||
for(auto& w : worlds) {
|
||||
if(w->conType == World::CONNECTIONTYPE_SERVER)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MainProgram::early_destroy() {
|
||||
for(auto& w : worlds)
|
||||
w->early_destroy();
|
||||
}
|
||||
|
||||
MainProgram::~MainProgram() {
|
||||
NetLibrary::destroy();
|
||||
}
|
||||
131
src/MainProgram.hpp
Normal file
131
src/MainProgram.hpp
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
#pragma once
|
||||
#include "DrawData.hpp"
|
||||
#include "GridManager.hpp"
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include "InputManager.hpp"
|
||||
#include "FontData.hpp"
|
||||
#include "TimePoint.hpp"
|
||||
#include "SharedTypes.hpp"
|
||||
#include <Eigen/Dense>
|
||||
#include <include/core/SkSurfaceProps.h>
|
||||
#include "Toolbar.hpp"
|
||||
#include "World.hpp"
|
||||
|
||||
#ifdef USE_SKIA_BACKEND_GRAPHITE
|
||||
#include "include/gpu/graphite/Recorder.h"
|
||||
#elif USE_SKIA_BACKEND_GANESH
|
||||
#include "include/gpu/ganesh/GrDirectContext.h"
|
||||
#endif
|
||||
|
||||
using namespace Eigen;
|
||||
|
||||
class MainProgram {
|
||||
public:
|
||||
static constexpr size_t LOG_SIZE = 30;
|
||||
|
||||
InputManager input;
|
||||
|
||||
std::chrono::steady_clock::time_point lastFrameTime = std::chrono::steady_clock::now();
|
||||
float fpsLimit = 10000.0f;
|
||||
|
||||
Vector3f userColor;
|
||||
|
||||
struct DrawingProgramImageCache {
|
||||
bool refresh = true;
|
||||
sk_sp<SkSurface> surface;
|
||||
std::shared_ptr<DrawComponent> firstCompUpdate = nullptr;
|
||||
SkCanvas* canvas;
|
||||
} drawProgCache;
|
||||
|
||||
struct Window {
|
||||
Vector2i size = {-1, -1};
|
||||
Vector2i pos = {-1, -1};
|
||||
Vector2i writtenSize = {-1, -1};
|
||||
Vector2i writtenPos = {-1, -1};
|
||||
bool maximized = false;
|
||||
bool fullscreen = false;
|
||||
SkColorType defaultColorType;
|
||||
SkAlphaType defaultAlphaType;
|
||||
|
||||
float scale = 1.0f;
|
||||
|
||||
int defaultMSAASampleCount = 0;
|
||||
SkSurfaceProps defaultMSAASurfaceProps;
|
||||
|
||||
#ifdef USE_SKIA_BACKEND_GRAPHITE
|
||||
std::function<skgpu::graphite::Recorder*()> recorder;
|
||||
#elif USE_SKIA_BACKEND_GANESH
|
||||
sk_sp<GrDirectContext> ctx;
|
||||
#endif
|
||||
sk_sp<SkSurface> surface;
|
||||
|
||||
SDL_Window* sdlWindow;
|
||||
} window;
|
||||
|
||||
struct Clipboard {
|
||||
std::vector<std::shared_ptr<DrawComponent>> components;
|
||||
std::unordered_map<ServerClientID, ResourceData> resources;
|
||||
WorldVec pos;
|
||||
} clipboard;
|
||||
|
||||
FontData fonts;
|
||||
TimePoint deltaTime;
|
||||
Toolbar toolbar;
|
||||
GridManager grid;
|
||||
std::shared_ptr<World> world;
|
||||
|
||||
std::ofstream* logFile;
|
||||
std::deque<Toolbar::LogMessage> logMessages;
|
||||
|
||||
bool disableIntelWorkaround = false;
|
||||
|
||||
std::string newAddr;
|
||||
|
||||
MainProgram();
|
||||
void update();
|
||||
void draw(SkCanvas* canvas);
|
||||
|
||||
void init_draw_program_cache();
|
||||
|
||||
bool setToQuit = false;
|
||||
|
||||
void early_destroy();
|
||||
|
||||
struct CanvasTheme {
|
||||
SkColor4f backColor = {0.12f, 0.12f, 0.12f, 1.0f};
|
||||
SkColor4f toolFrontColor = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||
} canvasTheme;
|
||||
|
||||
void new_tab(const World::OpenWorldInfo& tabInfo, bool createSameThread = false);
|
||||
void set_tab_to_close(size_t tabToClose);
|
||||
bool network_being_used();
|
||||
bool net_server_hosted();
|
||||
|
||||
void save_config();
|
||||
void load_config();
|
||||
|
||||
std::filesystem::path homePath;
|
||||
std::filesystem::path configPath;
|
||||
|
||||
bool drawGui = true;
|
||||
bool takingScreenshot = false;
|
||||
bool transparentBackground = false;
|
||||
|
||||
size_t worldIndex = 0;
|
||||
std::vector<std::shared_ptr<World>> worlds;
|
||||
|
||||
std::string displayName;
|
||||
|
||||
~MainProgram();
|
||||
private:
|
||||
void new_tab_open();
|
||||
std::atomic<bool> tabSetToOpen = false;
|
||||
World::OpenWorldInfo newTabToOpenInfo;
|
||||
|
||||
std::optional<size_t> setTabToClose;
|
||||
|
||||
std::string gen_random_display_name();
|
||||
|
||||
void gui();
|
||||
void draw_grid(SkCanvas* canvas);
|
||||
};
|
||||
65
src/ResourceDisplay/FileResourceDisplay.cpp
Normal file
65
src/ResourceDisplay/FileResourceDisplay.cpp
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#include "FileResourceDisplay.hpp"
|
||||
#include "../FontData.hpp"
|
||||
#include "../GUIStuff/Elements/Element.hpp"
|
||||
#include "../DrawData.hpp"
|
||||
#include "../Toolbar.hpp"
|
||||
#include "../MainProgram.hpp"
|
||||
#include <include/core/SkStream.h>
|
||||
#include <include/core/SkPaint.h>
|
||||
#include <include/core/SkFontMetrics.h>
|
||||
|
||||
std::string FILE_ICON_PATH = "data/icons/file.svg";
|
||||
|
||||
bool FileResourceDisplay::update_draw() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void FileResourceDisplay::update(World& w) {
|
||||
}
|
||||
|
||||
bool FileResourceDisplay::load(const std::string& fileName, const std::string& fileData) {
|
||||
this->fileName = fileName;
|
||||
return true;
|
||||
}
|
||||
|
||||
void FileResourceDisplay::draw(SkCanvas* canvas, const DrawData& drawData, const SkRect& imRect) {
|
||||
std::shared_ptr<GUIStuff::UpdateInputData>& io = drawData.main->toolbar.io;
|
||||
auto findSVGData = io->svgData.find(FILE_ICON_PATH);
|
||||
sk_sp<SkSVGDOM> svgDom;
|
||||
if(findSVGData == io->svgData.end()) {
|
||||
auto stream = SkStream::MakeFromFile(FILE_ICON_PATH.c_str());
|
||||
if(!stream)
|
||||
throw std::runtime_error("[FileResourceDisplay::draw] Could not open file " + FILE_ICON_PATH);
|
||||
svgDom = SkSVGDOM::Builder().make(*stream);
|
||||
if(!svgDom)
|
||||
throw std::runtime_error("[SVGIcon::update] Could not parse SVG " + FILE_ICON_PATH);
|
||||
}
|
||||
else
|
||||
svgDom = findSVGData->second;
|
||||
|
||||
SkPaint blendingPaint;
|
||||
blendingPaint.setColorFilter(SkColorFilters::Blend(drawData.main->canvasTheme.toolFrontColor, SkColorSpace::MakeSRGB(), SkBlendMode::kSrcIn));
|
||||
canvas->saveLayer(imRect, &blendingPaint);
|
||||
canvas->translate(imRect.x(), imRect.y());
|
||||
canvas->scale(imRect.width() / svgDom->containerSize().width(), imRect.height() / svgDom->containerSize().height());
|
||||
svgDom->render(canvas);
|
||||
canvas->restore();
|
||||
|
||||
SkFont f = io->get_font(40.0f);
|
||||
SkFontMetrics metrics;
|
||||
f.getMetrics(&metrics);
|
||||
|
||||
float nextText = f.measureText(fileName.c_str(), fileName.length(), SkTextEncoding::kUTF8, nullptr);
|
||||
Vector2f bounds{nextText, - metrics.fAscent + metrics.fDescent};
|
||||
|
||||
SkPaint fontPaint(drawData.main->canvasTheme.toolFrontColor);
|
||||
canvas->drawSimpleText(fileName.c_str(), fileName.length(), SkTextEncoding::kUTF8, imRect.x() + imRect.width() * 0.5f - bounds.x() * 0.5f, imRect.y() + imRect.height() + bounds.y(), f, fontPaint);
|
||||
}
|
||||
|
||||
Vector2f FileResourceDisplay::get_dimensions() const {
|
||||
return {100.0f, 100.0f};
|
||||
}
|
||||
|
||||
float FileResourceDisplay::get_dimension_scale() const {
|
||||
return 0.1f;
|
||||
}
|
||||
15
src/ResourceDisplay/FileResourceDisplay.hpp
Normal file
15
src/ResourceDisplay/FileResourceDisplay.hpp
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#pragma once
|
||||
#include "ResourceDisplay.hpp"
|
||||
#include <modules/svg/include/SkSVGDOM.h>
|
||||
|
||||
class FileResourceDisplay : public ResourceDisplay {
|
||||
public:
|
||||
virtual void update(World& w) override;
|
||||
virtual bool load(const std::string& fileName, const std::string& fileData) override;
|
||||
virtual bool update_draw() const override;
|
||||
virtual void draw(SkCanvas* canvas, const DrawData& drawData, const SkRect& imRect) override;
|
||||
virtual Vector2f get_dimensions() const override;
|
||||
virtual float get_dimension_scale() const override;
|
||||
private:
|
||||
std::string fileName;
|
||||
};
|
||||
82
src/ResourceDisplay/ImageResourceDisplay.cpp
Normal file
82
src/ResourceDisplay/ImageResourceDisplay.cpp
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#include "ImageResourceDisplay.hpp"
|
||||
#include <include/codec/SkPngDecoder.h>
|
||||
#include <include/codec/SkJpegDecoder.h>
|
||||
#include <include/codec/SkBmpDecoder.h>
|
||||
#include <include/codec/SkIcoDecoder.h>
|
||||
#include <include/codec/SkGifDecoder.h>
|
||||
#include "../MainProgram.hpp"
|
||||
#include <Helpers/Logger.hpp>
|
||||
|
||||
std::array<const SkCodecs::Decoder, 5> decoders = {
|
||||
SkPngDecoder::Decoder(),
|
||||
SkJpegDecoder::Decoder(),
|
||||
SkBmpDecoder::Decoder(),
|
||||
SkIcoDecoder::Decoder(),
|
||||
SkGifDecoder::Decoder()
|
||||
};
|
||||
|
||||
bool ImageResourceDisplay::update_draw() const {
|
||||
return mustUpdateDraw;
|
||||
}
|
||||
|
||||
void ImageResourceDisplay::update(World& w) {
|
||||
if(frames.size() <= 1)
|
||||
return;
|
||||
currentTime += w.main.deltaTime * 1000.0;
|
||||
mustUpdateDraw = false;
|
||||
for(;;) {
|
||||
if(currentTime >= frames[frameIndex].duration) {
|
||||
currentTime -= frames[frameIndex].duration;
|
||||
frameIndex++;
|
||||
if(frameIndex >= frames.size())
|
||||
frameIndex = 0;
|
||||
mustUpdateDraw = true;
|
||||
}
|
||||
else
|
||||
return;
|
||||
if(frames[frameIndex].duration <= 0.0f)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool ImageResourceDisplay::load(const std::string& fileName, const std::string& fileData) {
|
||||
sk_sp<SkData> newData = SkData::MakeWithoutCopy(fileData.c_str(), fileData.size());
|
||||
std::unique_ptr<SkCodec> codec = SkCodec::MakeFromData(newData, decoders);
|
||||
if(codec) {
|
||||
auto origin = codec->getOrigin();
|
||||
bool rotate = (origin == kLeftTop_SkEncodedOrigin || origin == kRightTop_SkEncodedOrigin || origin == kRightBottom_SkEncodedOrigin || origin == kLeftBottom_SkEncodedOrigin);
|
||||
int frameCount = codec->getFrameCount();
|
||||
SkImageInfo imageInfo = codec->getInfo();
|
||||
if(rotate) {
|
||||
Logger::get().log("INFO", "Image loaded is rotated, so must rotate dimensions");
|
||||
imageInfo = imageInfo.makeDimensions(SkISize(imageInfo.height(), imageInfo.width()));
|
||||
}
|
||||
for(int i = 0; i < frameCount; i++) {
|
||||
SkCodec::Options imageOptions;
|
||||
imageOptions.fFrameIndex = i;
|
||||
SkCodec::FrameInfo frameInfo;
|
||||
float frameTime = -1.0f;
|
||||
if(codec->getFrameInfo(i, &frameInfo))
|
||||
frameTime = frameInfo.fDuration;
|
||||
auto decodeResult = codec->getImage(imageInfo, &imageOptions);
|
||||
if(std::get<1>(decodeResult) == SkCodec::Result::kSuccess)
|
||||
frames.emplace_back(std::get<0>(decodeResult)->withDefaultMipmaps(), frameTime);
|
||||
else
|
||||
throw std::runtime_error("Could not decode image, got error code " + std::to_string(std::get<1>(decodeResult)) + ". Image has dimensions " + std::to_string(imageInfo.width()) + " " + std::to_string(imageInfo.height()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ImageResourceDisplay::draw(SkCanvas* canvas, const DrawData& drawData, const SkRect& imRect) {
|
||||
canvas->drawImageRect(frames[frameIndex].data, imRect, {SkFilterMode::kLinear, SkMipmapMode::kLinear});
|
||||
}
|
||||
|
||||
Vector2f ImageResourceDisplay::get_dimensions() const {
|
||||
return {frames.front().data->width(), frames.front().data->height()};
|
||||
}
|
||||
|
||||
float ImageResourceDisplay::get_dimension_scale() const {
|
||||
return 0.3f;
|
||||
}
|
||||
23
src/ResourceDisplay/ImageResourceDisplay.hpp
Normal file
23
src/ResourceDisplay/ImageResourceDisplay.hpp
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
#include "ResourceDisplay.hpp"
|
||||
#include <include/core/SkImage.h>
|
||||
#include <vector>
|
||||
|
||||
class ImageResourceDisplay : public ResourceDisplay {
|
||||
public:
|
||||
virtual void update(World& w) override;
|
||||
virtual bool load(const std::string& fileName, const std::string& fileData) override;
|
||||
virtual bool update_draw() const override;
|
||||
virtual void draw(SkCanvas* canvas, const DrawData& drawData, const SkRect& imRect) override;
|
||||
virtual Vector2f get_dimensions() const override;
|
||||
virtual float get_dimension_scale() const override;
|
||||
private:
|
||||
struct FrameData {
|
||||
sk_sp<SkImage> data;
|
||||
float duration;
|
||||
};
|
||||
std::vector<FrameData> frames;
|
||||
float currentTime = 0.0f;
|
||||
size_t frameIndex = 0;
|
||||
bool mustUpdateDraw = false;
|
||||
};
|
||||
3
src/ResourceDisplay/ResourceDisplay.cpp
Normal file
3
src/ResourceDisplay/ResourceDisplay.cpp
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#include "ResourceDisplay.hpp"
|
||||
|
||||
ResourceDisplay::~ResourceDisplay() {}
|
||||
20
src/ResourceDisplay/ResourceDisplay.hpp
Normal file
20
src/ResourceDisplay/ResourceDisplay.hpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include <include/core/SkRect.h>
|
||||
#include <Eigen/Dense>
|
||||
|
||||
using namespace Eigen;
|
||||
|
||||
struct DrawData;
|
||||
class World;
|
||||
|
||||
class ResourceDisplay {
|
||||
public:
|
||||
virtual void update(World& w) = 0;
|
||||
virtual bool load(const std::string& fileName, const std::string& fileData) = 0;
|
||||
virtual bool update_draw() const = 0;
|
||||
virtual void draw(SkCanvas* canvas, const DrawData& drawData, const SkRect& imRect) = 0;
|
||||
virtual Vector2f get_dimensions() const = 0;
|
||||
virtual float get_dimension_scale() const = 0;
|
||||
virtual ~ResourceDisplay();
|
||||
};
|
||||
132
src/ResourceManager.cpp
Normal file
132
src/ResourceManager.cpp
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
#include "ResourceManager.hpp"
|
||||
#include "Helpers/Networking/NetLibrary.hpp"
|
||||
#include <include/core/SkImage.h>
|
||||
#include "Server/CommandList.hpp"
|
||||
#include "ResourceDisplay/ImageResourceDisplay.hpp"
|
||||
#include "ResourceDisplay/FileResourceDisplay.hpp"
|
||||
#include <cereal/types/string.hpp>
|
||||
#include <Helpers/Logger.hpp>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
#include "World.hpp"
|
||||
#include "ResourceDisplay/FileResourceDisplay.hpp"
|
||||
|
||||
ResourceManager::ResourceManager(World& initWorld):
|
||||
world(initWorld)
|
||||
{}
|
||||
|
||||
void ResourceManager::init_client_callbacks() {
|
||||
world.con.client_add_recv_callback(CLIENT_NEW_RESOURCE, [&](cereal::PortableBinaryInputArchive& message) {
|
||||
ServerClientID resourceID;
|
||||
message(resourceID);
|
||||
message(resources[resourceID]);
|
||||
std::cout << "Received new resource: " << resourceID.first << " " << resourceID.second << " " << resources[resourceID].data->size() << std::endl;
|
||||
});
|
||||
}
|
||||
|
||||
void ResourceManager::update() {
|
||||
for(auto& [k, v] : displays)
|
||||
v->update(world);
|
||||
}
|
||||
|
||||
ServerClientID ResourceManager::add_resource_file(std::string_view fileName) {
|
||||
// https://nullptr.org/cpp-read-file-into-string/
|
||||
std::ifstream file(fileName.data(), std::ios::in | std::ios::binary | std::ios::ate);
|
||||
if(!file.is_open()) {
|
||||
Logger::get().log("INFO", "[ResourceManager::add_resource_file] Could not open file " + std::string(fileName));
|
||||
return {0, 0};
|
||||
}
|
||||
|
||||
size_t resourcesize;
|
||||
|
||||
auto tellgResult = file.tellg();
|
||||
|
||||
if(tellgResult == -1) {
|
||||
Logger::get().log("INFO", "[ResourceManager::add_resource_file] tellg failed for file " + std::string(fileName));
|
||||
return {0, 0};
|
||||
}
|
||||
|
||||
resourcesize = static_cast<size_t>(tellgResult);
|
||||
|
||||
file.seekg(0, std::ios_base::beg);
|
||||
|
||||
ResourceData resource;
|
||||
resource.data = std::make_shared<std::string>();
|
||||
resource.name = std::filesystem::path(fileName).filename().string();
|
||||
std::string& toPlaceIn = *resource.data;
|
||||
|
||||
toPlaceIn.resize(resourcesize);
|
||||
|
||||
file.read(toPlaceIn.data(), resourcesize);
|
||||
|
||||
file.close();
|
||||
|
||||
Logger::get().log("INFO", "[ResourceManager::add_resource_file] Successfully read file " + std::string(fileName));
|
||||
|
||||
return add_resource(resource);
|
||||
}
|
||||
|
||||
ServerClientID ResourceManager::add_resource(const ResourceData& resource) {
|
||||
auto it = std::find_if(resources.begin(), resources.end(), [&](const auto& p) {
|
||||
return p.second.data == resource.data || (*p.second.data) == (*resource.data);
|
||||
});
|
||||
if(it != resources.end()) {
|
||||
Logger::get().log("INFO", "[ResourceManager::add_resource] File " + std::string(resource.name) + " is a duplicate");
|
||||
return it->first;
|
||||
}
|
||||
ServerClientID newID = world.get_new_id();
|
||||
resources[newID] = resource;
|
||||
world.con.client_send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_RESOURCE_INIT);
|
||||
world.con.client_send_items_to_server(RESOURCE_COMMAND_CHANNEL, SERVER_RESOURCE_DATA, resource);
|
||||
return newID;
|
||||
}
|
||||
|
||||
void ResourceManager::copy_resource_set_to_map(const std::unordered_set<ServerClientID>& resourceSet, std::unordered_map<ServerClientID, ResourceData>& resourceMap) const {
|
||||
resourceMap.clear();
|
||||
for(auto& [k, v] : resources) {
|
||||
if(resourceSet.contains(k))
|
||||
resourceMap[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
ClientPortionID ResourceManager::get_max_id(ServerPortionID serverID) const {
|
||||
ClientPortionID maxClientID = 0;
|
||||
for(auto& p : resources) {
|
||||
if(p.first.first == serverID)
|
||||
maxClientID = std::max(maxClientID, p.first.second);
|
||||
}
|
||||
return maxClientID;
|
||||
}
|
||||
|
||||
std::optional<ResourceData> ResourceManager::get_resource(const ServerClientID& id) const {
|
||||
auto it = resources.find(id);
|
||||
if(it == resources.end())
|
||||
return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::shared_ptr<ResourceDisplay> ResourceManager::get_display_data(const ServerClientID& fileID) {
|
||||
auto loadedDisplayIt = displays.find(fileID);
|
||||
if(loadedDisplayIt != displays.end())
|
||||
return loadedDisplayIt->second;
|
||||
|
||||
auto resourceIt = resources.find(fileID);
|
||||
if(resourceIt == resources.end())
|
||||
return nullptr;
|
||||
|
||||
auto imgResource(std::make_shared<ImageResourceDisplay>());
|
||||
if(imgResource->load(resourceIt->second.name, *resourceIt->second.data)) {
|
||||
displays[fileID] = imgResource;
|
||||
return imgResource;
|
||||
}
|
||||
|
||||
auto fileResource(std::make_shared<FileResourceDisplay>());
|
||||
fileResource->load(resourceIt->second.name, *resourceIt->second.data);
|
||||
displays[fileID] = fileResource;
|
||||
return fileResource;
|
||||
}
|
||||
|
||||
const std::unordered_map<ServerClientID, ResourceData>& ResourceManager::resource_list() {
|
||||
return resources;
|
||||
}
|
||||
54
src/ResourceManager.hpp
Normal file
54
src/ResourceManager.hpp
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#pragma once
|
||||
#include "SharedTypes.hpp"
|
||||
#include <include/codec/SkCodec.h>
|
||||
#include <include/core/SkImage.h>
|
||||
#include "ResourceDisplay/ResourceDisplay.hpp"
|
||||
#include <cereal/types/utility.hpp>
|
||||
#include <cereal/types/string.hpp>
|
||||
#include <cereal/types/unordered_map.hpp>
|
||||
#include <queue>
|
||||
|
||||
class World;
|
||||
|
||||
struct ResourceData {
|
||||
std::string name;
|
||||
std::shared_ptr<std::string> data;
|
||||
template <typename Archive> void save(Archive& a) const {
|
||||
a(name, *data);
|
||||
}
|
||||
template <typename Archive> void load(Archive& a) {
|
||||
data = std::make_shared<std::string>();
|
||||
a(name, *data);
|
||||
}
|
||||
};
|
||||
|
||||
class ResourceManager {
|
||||
public:
|
||||
ResourceManager(World& initWorld);
|
||||
template <typename Archive> void serialize(Archive& a) {
|
||||
a(resources);
|
||||
}
|
||||
template <typename Archive> void save_strip_unused_resources(Archive& a, const std::unordered_set<ServerClientID>& usedResources) const {
|
||||
std::unordered_map<ServerClientID, ResourceData> strippedResources = resources;
|
||||
std::erase_if(strippedResources, [&](const auto& p) {
|
||||
return !usedResources.contains(p.first);
|
||||
});
|
||||
a(strippedResources);
|
||||
}
|
||||
void update();
|
||||
void copy_resource_set_to_map(const std::unordered_set<ServerClientID>& resourceSet, std::unordered_map<ServerClientID, ResourceData>& resourceMap) const;
|
||||
void init_client_callbacks();
|
||||
ServerClientID add_resource_file(std::string_view fileName);
|
||||
ServerClientID add_resource(const ResourceData& resource);
|
||||
std::shared_ptr<ResourceDisplay> get_display_data(const ServerClientID& fileID);
|
||||
std::optional<ResourceData> get_resource(const ServerClientID& id) const;
|
||||
ClientPortionID get_max_id(ServerPortionID serverID) const;
|
||||
const std::unordered_map<ServerClientID, ResourceData>& resource_list();
|
||||
private:
|
||||
std::unordered_map<ServerClientID, ResourceData> resources;
|
||||
std::unordered_map<ServerClientID, std::shared_ptr<ResourceDisplay>> displays;
|
||||
|
||||
static std::array<const SkCodecs::Decoder, 5> decoders;
|
||||
World& world;
|
||||
};
|
||||
|
||||
36
src/Server/CMakeLists.txt
Normal file
36
src/Server/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
cmake_minimum_required(VERSION 3.27.0)
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
project(canvasprogramserver)
|
||||
|
||||
set(sources "servermain.cpp"
|
||||
"MainServer.cpp"
|
||||
"ClientData.cpp"
|
||||
"../../include/Helpers/Networking/NetServer.cpp"
|
||||
"../../include/Helpers/Networking/NetClient.cpp"
|
||||
"../../include/Helpers/Networking/ByteStream.cpp"
|
||||
"../../include/Helpers/Random.cpp"
|
||||
"../DrawComponents/DrawBrushStroke.cpp"
|
||||
"../DrawComponents/DrawComponent.cpp"
|
||||
"../DrawComponents/DrawEllipse.cpp"
|
||||
"../DrawComponents/DrawRectangle.cpp"
|
||||
"../DrawComponents/DrawTextBox.cpp"
|
||||
"../DrawComponents/DrawImage.cpp"
|
||||
)
|
||||
|
||||
|
||||
find_package(GameNetworkingSockets)
|
||||
add_executable(canvasprogramserver ${sources})
|
||||
target_compile_definitions(canvasprogramserver PRIVATE IS_SERVER)
|
||||
|
||||
if(WIN32)
|
||||
set_property(TARGET canvasprogramserver PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
target_include_directories(canvasprogramserver PRIVATE "../../include" "../..")
|
||||
target_link_libraries(canvasprogramserver GameNetworkingSockets::shared)
|
||||
else()
|
||||
target_include_directories(canvasprogramserver PRIVATE "../../include" "../.." "../../../GameNetworkingSockets/include")
|
||||
target_compile_options(canvasprogramserver PRIVATE -O3)
|
||||
target_link_options(canvasprogramserver PRIVATE -O3)
|
||||
target_link_libraries(canvasprogramserver GameNetworkingSockets::shared)
|
||||
endif()
|
||||
|
||||
install(TARGETS canvasprogramserver DESTINATION bin)
|
||||
9
src/Server/ClientData.cpp
Normal file
9
src/Server/ClientData.cpp
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#include "ClientData.hpp"
|
||||
#include "MainServer.hpp"
|
||||
#include "CommandList.hpp"
|
||||
|
||||
|
||||
ServerClientID ClientData::get_next_id() {
|
||||
id.second++;
|
||||
return id;
|
||||
}
|
||||
30
src/Server/ClientData.hpp
Normal file
30
src/Server/ClientData.hpp
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#pragma once
|
||||
#include "../SharedTypes.hpp"
|
||||
#include <Helpers/Serializers.hpp>
|
||||
#include "../CoordSpaceHelper.hpp"
|
||||
#include "../ResourceManager.hpp"
|
||||
#include <Helpers/Networking/NetServer.hpp>
|
||||
#include <chrono>
|
||||
|
||||
class MainServer;
|
||||
|
||||
class ClientData {
|
||||
public:
|
||||
CoordSpaceHelper camCoords;
|
||||
Vector2f windowSize;
|
||||
Vector2f cursorPos;
|
||||
|
||||
Vector3f cursorColor;
|
||||
std::string displayName;
|
||||
ServerClientID id = {0, 0};
|
||||
|
||||
std::queue<ResourceData> pendingResourceData;
|
||||
std::queue<ServerClientID> pendingResourceID;
|
||||
|
||||
template <typename Archive> void serialize(Archive& a) {
|
||||
a(camCoords, windowSize, cursorPos, cursorColor, displayName);
|
||||
}
|
||||
|
||||
ServerClientID get_next_id();
|
||||
};
|
||||
|
||||
39
src/Server/CommandList.hpp
Normal file
39
src/Server/CommandList.hpp
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Helpers/Networking/NetLibrary.hpp>
|
||||
|
||||
enum ServerCommands : MessageCommandType {
|
||||
SERVER_FRAGMENT = 0,
|
||||
SERVER_KEEP_ALIVE,
|
||||
SERVER_INITIAL_DATA,
|
||||
SERVER_MOVE_MOUSE,
|
||||
SERVER_PLACE_COMPONENT,
|
||||
SERVER_TRANSFORM_COMPONENT,
|
||||
SERVER_ERASE_COMPONENT,
|
||||
SERVER_UPDATE_COMPONENT,
|
||||
SERVER_RESOURCE_INIT,
|
||||
SERVER_RESOURCE_DATA,
|
||||
SERVER_CHAT_MESSAGE,
|
||||
SERVER_NEW_BOOKMARK,
|
||||
SERVER_REMOVE_BOOKMARK
|
||||
};
|
||||
|
||||
enum ClientCommands : MessageCommandType {
|
||||
CLIENT_FRAGMENT = 0,
|
||||
CLIENT_KEEP_ALIVE,
|
||||
CLIENT_INITIAL_DATA,
|
||||
CLIENT_USER_CONNECT,
|
||||
CLIENT_USER_DISCONNECT,
|
||||
CLIENT_PLACE_COMPONENT,
|
||||
CLIENT_TRANSFORM_COMPONENT,
|
||||
CLIENT_ERASE_COMPONENT,
|
||||
CLIENT_UPDATE_COMPONENT,
|
||||
CLIENT_MOVE_MOUSE,
|
||||
CLIENT_NEW_RESOURCE,
|
||||
CLIENT_CHAT_MESSAGE,
|
||||
CLIENT_NEW_BOOKMARK,
|
||||
CLIENT_REMOVE_BOOKMARK
|
||||
};
|
||||
|
||||
#define SAVEFILE_HEADER_LEN 12
|
||||
#define SAVEFILE_HEADER "INFPNT000001"
|
||||
257
src/Server/MainServer.cpp
Normal file
257
src/Server/MainServer.cpp
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
#include "MainServer.hpp"
|
||||
#include "CommandList.hpp"
|
||||
#include "Helpers/HsvRgb.hpp"
|
||||
#include "Helpers/Networking/NetLibrary.hpp"
|
||||
#include "ServerData.hpp"
|
||||
#include "../DrawComponents/DrawBrushStroke.hpp"
|
||||
#include "../DrawComponents/DrawRectangle.hpp"
|
||||
#include "../DrawComponents/DrawEllipse.hpp"
|
||||
#include "../SharedTypes.hpp"
|
||||
#include "../CoordSpaceHelper.hpp"
|
||||
#include <cereal/archives/portable_binary.hpp>
|
||||
#include <cereal/types/vector.hpp>
|
||||
#include <cereal/types/unordered_map.hpp>
|
||||
#include <cereal/types/utility.hpp>
|
||||
#include <cereal/types/string.hpp>
|
||||
#include <Helpers/Serializers.hpp>
|
||||
#include <Helpers/Random.hpp>
|
||||
#include <chrono>
|
||||
#include <limits>
|
||||
#include <fstream>
|
||||
#include "../DrawComponents/DrawComponent.hpp"
|
||||
#include "../World.hpp"
|
||||
|
||||
WorldVec cursorPos;
|
||||
|
||||
void ServerData::save(cereal::PortableBinaryOutputArchive& a) const {
|
||||
a((uint64_t)components.size());
|
||||
for(uint64_t i = 0; i < components.size(); i++)
|
||||
a(components[i].second->get_type(), components[i].first, components[i].second->coords, *(components[i].second));
|
||||
a(bookmarks);
|
||||
}
|
||||
|
||||
void ServerData::load(cereal::PortableBinaryInputArchive& a) {
|
||||
uint64_t compCount;
|
||||
a(compCount);
|
||||
for(uint64_t i = 0; i < compCount; i++) {
|
||||
DrawComponentType t;
|
||||
ServerClientID id;
|
||||
a(t, id);
|
||||
auto newComp = DrawComponent::allocate_comp_type(t);
|
||||
a(newComp->coords, *newComp);
|
||||
components.emplace_back(id, newComp);
|
||||
}
|
||||
a(bookmarks);
|
||||
}
|
||||
|
||||
ClientPortionID ServerData::get_max_id(ServerPortionID serverID) const {
|
||||
ClientPortionID maxClientID = 0;
|
||||
for(auto& p : resources) {
|
||||
if(p.first.first == serverID)
|
||||
maxClientID = std::max(maxClientID, p.first.second);
|
||||
}
|
||||
for(auto& p : components) {
|
||||
if(p.first.first == serverID)
|
||||
maxClientID = std::max(maxClientID, p.first.second);
|
||||
}
|
||||
return maxClientID;
|
||||
}
|
||||
|
||||
MainServer::MainServer(World& initWorld, const std::string& serverLocalID):
|
||||
world(initWorld)
|
||||
{
|
||||
for(auto& c : world.drawProg.components.client_list())
|
||||
data.components.emplace_back(c->id, c->obj->copy());
|
||||
data.bookmarks = world.bMan.bookmark_list();
|
||||
data.resources = world.rMan.resource_list();
|
||||
|
||||
lastKeepAliveSent = std::chrono::steady_clock::now();
|
||||
|
||||
netServer = std::make_shared<NetServer>(serverLocalID);
|
||||
NetLibrary::register_server(netServer);
|
||||
|
||||
netServer->add_recv_callback(SERVER_INITIAL_DATA, [&](NetServer::ClientData& client, cereal::PortableBinaryInputArchive& message) {
|
||||
bool isDirectConnect;
|
||||
ClientData newClient;
|
||||
Vector3f hsv;
|
||||
hsv[0] = Random::get().real_range(0.0f, 360.0f);
|
||||
hsv[1] = Random::get().real_range(0.3f, 0.7f);
|
||||
hsv[2] = 1.0;
|
||||
newClient.cursorColor = hsv_to_rgb<Vector3f>(hsv);
|
||||
message(newClient.displayName, isDirectConnect);
|
||||
ensure_display_name_unique(newClient.displayName);
|
||||
|
||||
newClient.id.first = Random::get().int_range<ServerPortionID>(1, std::numeric_limits<ServerPortionID>::max());
|
||||
client.customID = newClient.id.first;
|
||||
newClient.id.second = data.get_max_id(newClient.id.first);
|
||||
netServer->send_items_to_all_clients_except(client, RELIABLE_COMMAND_CHANNEL, CLIENT_USER_CONNECT, newClient.id.first, newClient.displayName, newClient.cursorColor);
|
||||
if(isDirectConnect)
|
||||
netServer->send_items_to_client(client, RELIABLE_COMMAND_CHANNEL, CLIENT_INITIAL_DATA, isDirectConnect, newClient.id.first, newClient.cursorColor);
|
||||
else {
|
||||
netServer->send_items_to_client(client, RELIABLE_COMMAND_CHANNEL, CLIENT_INITIAL_DATA, isDirectConnect, newClient.id.first, newClient.cursorColor, newClient.displayName, clients, data);
|
||||
for(auto& [id, rData] : data.resources)
|
||||
netServer->send_items_to_client(client, RESOURCE_COMMAND_CHANNEL, CLIENT_NEW_RESOURCE, id, rData);
|
||||
}
|
||||
clients.emplace(newClient.id.first, newClient);
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_MOVE_MOUSE, [&](NetServer::ClientData& client, cereal::PortableBinaryInputArchive& message) {
|
||||
auto& c = clients[client.customID];
|
||||
message(c.camCoords, c.windowSize, c.cursorPos);
|
||||
netServer->send_items_to_all_clients_except(client, UNRELIABLE_COMMAND_CHANNEL, CLIENT_MOVE_MOUSE, c.id.first, c.camCoords, c.windowSize, c.cursorPos);
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_PLACE_COMPONENT, [&](NetServer::ClientData& client, cereal::PortableBinaryInputArchive& message) {
|
||||
uint64_t placement;
|
||||
DrawComponentType type;
|
||||
message(placement, type);
|
||||
auto newComp = DrawComponent::allocate_comp_type(type);
|
||||
message(newComp->coords, *newComp);
|
||||
ServerClientID id = clients[client.customID].get_next_id();
|
||||
placement = std::min<uint64_t>(placement, data.components.size());
|
||||
data.components.insert(data.components.begin() + placement, {id, newComp});
|
||||
newComp->server_send_place(*this, id, placement);
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_ERASE_COMPONENT, [&](NetServer::ClientData& client, cereal::PortableBinaryInputArchive& message) {
|
||||
ServerClientID compToRemove;
|
||||
message(compToRemove);
|
||||
std::erase_if(data.components, [&](auto& c){ return c.first == compToRemove; });
|
||||
DrawComponent::server_send_erase(*this, compToRemove);
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_TRANSFORM_COMPONENT, [&](NetServer::ClientData& client, cereal::PortableBinaryInputArchive& message) {
|
||||
bool isTemp;
|
||||
ServerClientID idToTransform;
|
||||
message(isTemp, idToTransform);
|
||||
auto a = std::find_if(data.components.begin(), data.components.end(), [&](auto& c) {
|
||||
return (c.first == idToTransform);
|
||||
});
|
||||
if(a != data.components.end()) {
|
||||
std::shared_ptr<DrawComponent>& comp = (*a).second;
|
||||
message(comp->coords);
|
||||
if(isTemp)
|
||||
comp->server_send_transform_temp(*this, idToTransform);
|
||||
else
|
||||
comp->server_send_transform_final(*this, idToTransform);
|
||||
}
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_UPDATE_COMPONENT, [&](NetServer::ClientData& client, cereal::PortableBinaryInputArchive& message) {
|
||||
bool isTemp;
|
||||
ServerClientID idToTransform;
|
||||
message(isTemp, idToTransform);
|
||||
auto a = std::find_if(data.components.begin(), data.components.end(), [&](auto& c) {
|
||||
return (c.first == idToTransform);
|
||||
});
|
||||
if(a != data.components.end()) {
|
||||
std::shared_ptr<DrawComponent>& comp = (*a).second;
|
||||
message(*comp);
|
||||
if(isTemp)
|
||||
comp->server_send_update_temp(*this, idToTransform);
|
||||
else
|
||||
comp->server_send_update_final(*this, idToTransform);
|
||||
}
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_RESOURCE_INIT, [&](NetServer::ClientData& client, cereal::PortableBinaryInputArchive& message) {
|
||||
auto& c = clients[client.customID];
|
||||
ServerClientID newID = c.get_next_id();
|
||||
if(c.pendingResourceData.empty())
|
||||
c.pendingResourceID.emplace(newID);
|
||||
else {
|
||||
netServer->send_items_to_all_clients_except(client, RESOURCE_COMMAND_CHANNEL, CLIENT_NEW_RESOURCE, newID, c.pendingResourceData.front());
|
||||
data.resources.emplace(newID, c.pendingResourceData.front());
|
||||
c.pendingResourceData.pop();
|
||||
}
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_RESOURCE_DATA, [&](NetServer::ClientData& client, cereal::PortableBinaryInputArchive& message) {
|
||||
ResourceData resourceData;
|
||||
message(resourceData);
|
||||
auto& c = clients[client.customID];
|
||||
if(c.pendingResourceID.empty()) {
|
||||
c.pendingResourceData.emplace();
|
||||
c.pendingResourceData.back() = std::move(resourceData);
|
||||
}
|
||||
else {
|
||||
netServer->send_items_to_all_clients_except(client, RESOURCE_COMMAND_CHANNEL, CLIENT_NEW_RESOURCE, c.pendingResourceID.front(), resourceData);
|
||||
data.resources.emplace(c.pendingResourceID.front(), resourceData);
|
||||
c.pendingResourceID.pop();
|
||||
}
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_CHAT_MESSAGE, [&](NetServer::ClientData& client, cereal::PortableBinaryInputArchive& message) {
|
||||
std::string chatMessage;
|
||||
Vector4f c{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
message(chatMessage);
|
||||
netServer->send_items_to_all_clients_except(client, RELIABLE_COMMAND_CHANNEL, CLIENT_CHAT_MESSAGE, chatMessage, c);
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_NEW_BOOKMARK, [&](NetServer::ClientData& client, cereal::PortableBinaryInputArchive& message) {
|
||||
std::string name;
|
||||
message(name);
|
||||
Bookmark& b = data.bookmarks[name];
|
||||
message(b);
|
||||
netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_NEW_BOOKMARK, name, b);
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_REMOVE_BOOKMARK, [&](NetServer::ClientData& client, cereal::PortableBinaryInputArchive& message) {
|
||||
std::string name;
|
||||
message(name);
|
||||
data.bookmarks.erase(name);
|
||||
netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_REMOVE_BOOKMARK, name);
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_KEEP_ALIVE, [&](NetServer::ClientData& client, cereal::PortableBinaryInputArchive& message) {
|
||||
});
|
||||
netServer->add_disconnect_callback([&](NetServer::ClientData& client) {
|
||||
auto it = clients.find(client.customID);
|
||||
if(it != clients.end()) {
|
||||
netServer->send_items_to_all_clients_except(client, RELIABLE_COMMAND_CHANNEL, CLIENT_USER_DISCONNECT, client.customID);
|
||||
clients.erase(it);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MainServer::ensure_display_name_unique(std::string& displayName) {
|
||||
for(;;) {
|
||||
bool isUnique = true;
|
||||
for(auto& [id, client] : clients) {
|
||||
if(client.displayName == displayName) {
|
||||
size_t leftParenthesisIndex = displayName.find_last_of('(');
|
||||
size_t rightParenthesisIndex = displayName.find_last_of(')');
|
||||
bool failToIncrement = true;
|
||||
if(leftParenthesisIndex != std::string::npos && rightParenthesisIndex != std::string::npos && leftParenthesisIndex < rightParenthesisIndex) {
|
||||
std::string numStr = displayName.substr(leftParenthesisIndex + 1, rightParenthesisIndex - leftParenthesisIndex - 1);
|
||||
bool isAllDigits = true;
|
||||
for(char c : numStr) {
|
||||
if(!isdigit(c)) {
|
||||
isAllDigits = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(isAllDigits) {
|
||||
try {
|
||||
int s = std::stoi(numStr);
|
||||
s++;
|
||||
displayName = displayName.substr(0, leftParenthesisIndex);
|
||||
displayName += "(" + std::to_string(s) + ")";
|
||||
failToIncrement = false;
|
||||
}
|
||||
catch(...) {}
|
||||
}
|
||||
}
|
||||
if(failToIncrement)
|
||||
displayName += " (2)";
|
||||
isUnique = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(isUnique)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MainServer::update() {
|
||||
netServer->update();
|
||||
for(auto& c : data.components)
|
||||
c.second->server_update(*this, c.first);
|
||||
if(std::chrono::steady_clock::now() - lastKeepAliveSent > std::chrono::seconds(2)) {
|
||||
netServer->send_items_to_all_clients(UNRELIABLE_COMMAND_CHANNEL, CLIENT_KEEP_ALIVE);
|
||||
lastKeepAliveSent = std::chrono::steady_clock::now();
|
||||
}
|
||||
}
|
||||
|
||||
MainServer::~MainServer() {
|
||||
//netServer->close_server();
|
||||
}
|
||||
20
src/Server/MainServer.hpp
Normal file
20
src/Server/MainServer.hpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
#include <Helpers/Networking/NetServer.hpp>
|
||||
#include <chrono>
|
||||
#include "ClientData.hpp"
|
||||
#include "Helpers/Networking/NetLibrary.hpp"
|
||||
#include "ServerData.hpp"
|
||||
|
||||
class MainServer {
|
||||
public:
|
||||
MainServer(World& initWorld, const std::string& serverLocalID);
|
||||
void update();
|
||||
~MainServer();
|
||||
std::shared_ptr<NetServer> netServer;
|
||||
std::unordered_map<ServerPortionID, ClientData> clients;
|
||||
private:
|
||||
World& world;
|
||||
void ensure_display_name_unique(std::string& displayName);
|
||||
ServerData data;
|
||||
std::chrono::steady_clock::time_point lastKeepAliveSent;
|
||||
};
|
||||
18
src/Server/ServerData.hpp
Normal file
18
src/Server/ServerData.hpp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#pragma once
|
||||
#include "cereal/archives/portable_binary.hpp"
|
||||
#include "../SharedTypes.hpp"
|
||||
#include "../BookmarkManager.hpp"
|
||||
#include "../ResourceManager.hpp"
|
||||
|
||||
class DrawComponent;
|
||||
|
||||
class ServerData {
|
||||
public:
|
||||
std::vector<std::pair<ServerClientID, std::shared_ptr<DrawComponent>>> components;
|
||||
std::unordered_map<ServerClientID, ResourceData> resources;
|
||||
std::unordered_map<std::string, Bookmark> bookmarks;
|
||||
void save(cereal::PortableBinaryOutputArchive& a) const;
|
||||
void write_to_file(cereal::PortableBinaryOutputArchive& a) const;
|
||||
void load(cereal::PortableBinaryInputArchive& a);
|
||||
ClientPortionID get_max_id(ServerPortionID serverID) const;
|
||||
};
|
||||
55
src/Server/servermain.cpp
Normal file
55
src/Server/servermain.cpp
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#include "MainServer.hpp"
|
||||
#include "steam/steamnetworkingsockets.h"
|
||||
#include "steam/steamnetworkingtypes.h"
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
std::mutex inputMutex;
|
||||
std::queue<std::string> inputQueue;
|
||||
|
||||
std::unique_ptr<std::thread> inputThread;
|
||||
|
||||
void user_input_init() {
|
||||
inputThread = std::make_unique<std::thread>([&]() {
|
||||
for(;;) {
|
||||
std::string newMessage;
|
||||
std::cin >> newMessage;
|
||||
inputMutex.lock();
|
||||
inputQueue.emplace(newMessage);
|
||||
inputMutex.unlock();
|
||||
if(newMessage == "stop")
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
SteamDatagramErrMsg errMsg;
|
||||
if(!GameNetworkingSockets_Init(nullptr, errMsg))
|
||||
std::cout << "[GameNetworkingSockets_Init] Failed to initialize network sockets " << errMsg << std::endl;
|
||||
|
||||
MainServer server("");
|
||||
user_input_init();
|
||||
for(;;) {
|
||||
server.update();
|
||||
inputMutex.lock();
|
||||
bool isQuitting = false;
|
||||
while(!inputQueue.empty()) {
|
||||
std::string newMessage = inputQueue.front();
|
||||
inputQueue.pop();
|
||||
if(newMessage == "stop") {
|
||||
isQuitting = true;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
std::cout << "Unknown command: " << newMessage << std::endl;
|
||||
}
|
||||
}
|
||||
inputMutex.unlock();
|
||||
if(isQuitting)
|
||||
break;
|
||||
}
|
||||
inputThread->join();
|
||||
return 0;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue