From f8c38a8a378e10b75d386b6471fd2828bcdc0d66 Mon Sep 17 00:00:00 2001 From: Ilusha Date: Sun, 17 Mar 2024 10:04:52 +0300 Subject: [PATCH] Sketch ini --- CMakeLists.txt | 1 + Sketch3D/CMakeLists.txt | 22 + Sketch3D/applications/Entry.cpp | 34 + Sketch3D/private/StrokesWidget.cpp | 260 +++++++ Sketch3D/private/strokes.cpp | 501 +++++++++++++ Sketch3D/private/strokesobject.cpp | 42 ++ Sketch3D/private/temp.cpp | 1049 ++++++++++++++++++++++++++++ Sketch3D/public/StrokesWidget.h | 29 + Sketch3D/public/strokes.h | 153 ++++ Sketch3D/public/strokes_versions.h | 178 +++++ Sketch3D/rsc/Font.ttf | Bin 0 -> 42624 bytes Sketch3D/rsc/shaders/Texture.frag | 15 + Sketch3D/rsc/shaders/Texture.vert | 12 + Sketch3D/rsc/shaders/stroke.frag | 28 + Sketch3D/rsc/shaders/stroke.geom | 170 +++++ Sketch3D/rsc/shaders/stroke.vert | 16 + 16 files changed, 2510 insertions(+) create mode 100644 Sketch3D/CMakeLists.txt create mode 100644 Sketch3D/applications/Entry.cpp create mode 100644 Sketch3D/private/StrokesWidget.cpp create mode 100644 Sketch3D/private/strokes.cpp create mode 100644 Sketch3D/private/strokesobject.cpp create mode 100644 Sketch3D/private/temp.cpp create mode 100644 Sketch3D/public/StrokesWidget.h create mode 100644 Sketch3D/public/strokes.h create mode 100644 Sketch3D/public/strokes_versions.h create mode 100644 Sketch3D/rsc/Font.ttf create mode 100644 Sketch3D/rsc/shaders/Texture.frag create mode 100644 Sketch3D/rsc/shaders/Texture.vert create mode 100644 Sketch3D/rsc/shaders/stroke.frag create mode 100644 Sketch3D/rsc/shaders/stroke.geom create mode 100644 Sketch3D/rsc/shaders/stroke.vert diff --git a/CMakeLists.txt b/CMakeLists.txt index 989e7e6..5f9f17f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,3 +26,4 @@ add_subdirectory(DataAnalysis) add_subdirectory(Objects) add_subdirectory(Widgets) add_subdirectory(LibraryViewer) +add_subdirectory(Sketch3D) \ No newline at end of file diff --git a/Sketch3D/CMakeLists.txt b/Sketch3D/CMakeLists.txt new file mode 100644 index 0000000..567767b --- /dev/null +++ b/Sketch3D/CMakeLists.txt @@ -0,0 +1,22 @@ +project(Sketch3Dlib) + +### ---------------------- Externals --------------------- ### +set(BINDINGS_INCLUDE ../Externals/glfw/include ${GLEW_INCLUDE_DIR}) +set(BINDINGS_LIBS glfw Imgui ${GLEW_LIB}) + +### ---------------------- Static Library --------------------- ### +file(GLOB SOURCES "./private/*.cpp" "./private/*/*.cpp") +file(GLOB HEADERS "./public/*.hpp" "./public/*/*.hpp") + +add_library(${PROJECT_NAME} STATIC ${SOURCES} ${HEADERS}) + +target_include_directories(${PROJECT_NAME} PUBLIC ./public/ ${BINDINGS_INCLUDE} ./ext/) +target_link_libraries(${PROJECT_NAME} PUBLIC Graphics Connection Widgets Math Strings) +target_link_libraries(${PROJECT_NAME} PUBLIC ${BINDINGS_LIBS}) + +file(COPY "rsc" DESTINATION "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/") + +### -------------------------- Applications -------------------------- ### +add_executable(Sketch3D ./applications/Entry.cpp) +target_link_libraries(Sketch3D ${PROJECT_NAME}) + diff --git a/Sketch3D/applications/Entry.cpp b/Sketch3D/applications/Entry.cpp new file mode 100644 index 0000000..33b0d6c --- /dev/null +++ b/Sketch3D/applications/Entry.cpp @@ -0,0 +1,34 @@ + +#include "nodes.h" +#include "StrokesWidget.h" + +int main(int argc, char* argv[]) { + + auto nodes = (argc == 2) ? nd::NodesCore::createFast(argv[1]) : nd::NodesCore::createFast(); + + tp::init_utils(); + + obj::NDO->define(&obj::StrokesObject::TypeData); + obj::NDO->define(&nd::StrokesWidget::TypeData); + obj::NDO->type_groups.addType(&obj::StrokesObject::TypeData, { "Strokes", "StrokesProject" }); + obj::NDO->type_groups.addType(&nd::StrokesWidget::TypeData, { "Strokes", "StrokesWidget" }); + + nd::StrokesWidget::OpsInit(); + nd::StrokesWidget::addShortcuts(nodes); + + auto root_widget = NDO_CAST(nd::Widget, obj::NDO->create("RootWidget")); + + auto childs = root_widget->getMember("childs"); + + childs->pushBack(obj::NDO->create("StrokesWidget")); + + nodes->bindRootWindow(root_widget); + + nodes->run(); + + nd::StrokesWidget::OpsUnInit(); + tp::finalize_utils(); + + nodes->destroyFast(); + tp::terminate(); +} \ No newline at end of file diff --git a/Sketch3D/private/StrokesWidget.cpp b/Sketch3D/private/StrokesWidget.cpp new file mode 100644 index 0000000..3e72dd8 --- /dev/null +++ b/Sketch3D/private/StrokesWidget.cpp @@ -0,0 +1,260 @@ + +#include "StrokesWidget.h" +#include "operator.h" + +using namespace nd; + +void StrokesWidget::constructor(StrokesWidget* self) { + new (&self->mRenderer) strokes::Renderer({ 1980, 1020 }); + self->createMember("link", "TargetProject"); + self->createMember("StrokesProject", "default"); + self->createMember("rgba", "draw_color"); + self->addMember(obj::ColorObject::create({ 0.13f, 0.13f, 0.13f, 1.f }), "bg_color"); +} + +void StrokesWidget::destructor(StrokesWidget* self) { + self->mRenderer.~Renderer(); + self->mImage.free(self->mDrawer); + self->mImage.~ImageHandle(); +} + +void StrokesWidget::copy(Widget* self, const Widget* in) {} + +strokes::Project* StrokesWidget::getTargetProject() { + auto link = getMember("TargetProject"); + if (!link) { + return NULL; + } + auto obj = link->getLink(); + if (!obj) { + link->setLink(getMember("default")); + return getTargetProject(); + } + auto strokes = NDO_CAST(obj::StrokesObject, obj); + if (!strokes) { + return NULL; + } + + return &strokes->mProject; +} + +void StrokesWidget::procInputs(StrokesWidget* self, nd::GUIInputs* inputs) { + auto proj = self->getTargetProject(); + if (!proj) { + return; + } + + auto rec = self->getRect(); + proj->mCamera.set_ratio(rec.w / rec.z); + + auto col_obj_bg = self->getMember("bg_color"); + if (col_obj_bg) { + proj->mBackgroundColor = col_obj_bg->mCol; + } + + if (!rec.inside(inputs->mCrsPrev)) { + return; + } + + auto pressure = inputs->mPressure; + if (!inputs->Anticipating()) { + pressure = 0.f; + } + + auto idx = proj->mBrushes.presents(proj->mActiveBrush); + if (idx) { + auto brush = proj->mBrushes.getSlotVal(idx); + auto crs = (inputs->mCrs - rec.pos); + crs.x /= rec.z; + crs.y /= rec.w; + crs = (crs - 0.5) * 2; + + if (brush->mType == "pencil") { + auto col_obj = self->getMember("draw_color"); + if (col_obj) { + ((strokes::PencilBrush*)brush)->mCol = col_obj->mCol; + } + } + + brush->sample(proj, crs, pressure); + } +} + +void StrokesWidget::presentOutput(StrokesWidget* self, nd::GUIdrawer* drawer) { + auto proj = self->getTargetProject(); + if (!proj) { + return; + } + + if (!self->mImage.mId) { + self->mDrawer = drawer; + self->mImage.createFromBuff(self->mRenderer.getBuff(), drawer); + } + + auto rec = self->getRect(); + self->mRenderer.setViewport({ 0, 0, rec.z, rec.w }); + self->mRenderer.setClearCol(proj->mBackgroundColor); + self->mRenderer.renderBegin(); + + for (auto lay : proj->mLayers) { + if (lay.data()->enabled) { + for (auto str : lay.data()->strokes) { + self->mRenderer.drawStroke(str.data(), &proj->mCamera); + } + } + } + + auto idx = proj->mBrushes.presents(proj->mActiveBrush); + if (idx) { + auto brush = proj->mBrushes.getSlotVal(idx); + brush->draw(&self->mRenderer, &proj->mCamera); + } + + self->mRenderer.renderEnd(); + + drawer->drawImage(rec, &self->mImage, 0, 1, 12); +} + +struct obj::ObjectType StrokesWidget::TypeData = { + .base = &Widget::TypeData, + .constructor = (obj::object_constructor)StrokesWidget::constructor, + .destructor = (obj::object_destructor)StrokesWidget::destructor, + .copy = (obj::object_copy)StrokesWidget::copy, + .size = sizeof(StrokesWidget), + .name = "StrokesWidget", + .vtable = &StrokesWidget::vtable +}; + +struct StrokesWidget::Vtable StrokesWidget::vtable = { + (void (*)(Widget*, nd::GUIInputs*)) & StrokesWidget::procInputs, + (void (*)(Widget*, nd::GUIdrawer*)) & StrokesWidget::presentOutput, +}; + + +class StrokesWidgetOps : public nd::TypeOperators { + + struct CameraOrbit : public nd::Operator { + tp::vec2f prev_crs; + struct Invoke : public nd::OpCallBack { + Invoke() { + auto transf = obj::EnumObject::create({ "move", "rotate" , "scale" }); + arg_interface.put("Gui", obj::NDO->create("link")); + arg_interface.put("transf", transf); + + func = exec; + description = "invoke"; + } + static void exec(Operator* op, obj::DictObject* args) { + auto active = OpCallBack::getActiveWidget(args); + if (!active) { + return; + } + + auto proj = active->getTargetProject(); + if (!proj) { + return; + } + + auto rec = active->getRect(); + + auto gui = OpCallBack::getGUI(args); + auto prev_crs = ((CameraOrbit*)op)->prev_crs; + prev_crs = tp::vec2f(prev_crs.x / rec.size.x, prev_crs.y / rec.size.y); + prev_crs = (prev_crs - 0.5f) * 2.f; + auto crs = tp::vec2f(gui->mInputs.mCrs.x / rec.size.x, gui->mInputs.mCrs.y / rec.size.y); + crs = (crs - 0.5f) * 2.f; + auto delta = (crs - prev_crs); + + if (tp::abs(delta.x) < 0.1 && tp::abs(delta.y) < 0.1) { + + auto type = OpCallBack::getArg(args, "transf"); + switch (type->active) { + case 0: { + proj->mCamera.move(crs, prev_crs); + break; + } + case 1: { + proj->mCamera.rotate(-delta.x * 5, -delta.y * 5); + break; + } + case 2: { + proj->mCamera.zoom((crs.y + 1.f) / (prev_crs.y + 1.f)); + break; + } + } + + } + ((CameraOrbit*)op)->prev_crs = gui->mInputs.mCrs; + } + } invoke_callback; + + CameraOrbit() { + description = "Orbits camera"; + callbacks.put("invoke", &invoke_callback); + } + } camera_orbit_op; + +public: + StrokesWidgetOps() { + operators.put("camera_orbit", &camera_orbit_op); + } +}; + +void StrokesWidget::OpsInit() { + assert(!StrokesWidget::TypeData.nodes_custom_data); + StrokesWidget::TypeData.nodes_custom_data = new StrokesWidgetOps(); +} + +void StrokesWidget::OpsUnInit() { + assert(StrokesWidget::TypeData.nodes_custom_data); + auto del = (StrokesWidgetOps*)StrokesWidget::TypeData.nodes_custom_data; + StrokesWidget::TypeData.nodes_custom_data = NULL; + delete del; +} + +void StrokesWidget::addShortcuts(nd::NodesCore* nodes) { + auto uis_list = nodes->getMemberAssert("uis"); + NDO_CASTV(nd::GUI, uis_list->getItems()[0], gui); + NDO_CASTV(nd::TUI, uis_list->getItems()[1], tui); + + auto shortcuts = tui->getMemberAssert("shortcuts"); + auto inputs = tui->getMemberAssert("inputs"); + auto input_states = tui->getMemberAssert("input_states"); + auto input_state_hold = input_states->get("hold"); + + { // orbit + auto shortcut = nd::Shortcut::createShortcut("StrokesWidget", "camera_orbit"); + auto args = NDO_CAST(obj::DictObject, shortcut->callbacks_arguments->get("invoke")); + NDO_CAST(obj::LinkObject, args->get("Gui"))->setLink(gui); + NDO_CAST(obj::EnumObject, args->get("transf"))->active = 1; + + shortcuts->pushBack(shortcut); + + auto trigger = shortcut->addTrigger("invoke"); + auto back = tui->getInput(tp::Keycode::LEFT_ALT)->getMember("state"); + trigger->addComparator("comp", back, input_state_hold); + } + + { // zoom + auto shortcut = nd::Shortcut::createShortcut("StrokesWidget", "camera_orbit"); + auto args = NDO_CAST(obj::DictObject, shortcut->callbacks_arguments->get("invoke")); + NDO_CAST(obj::LinkObject, args->get("Gui"))->setLink(gui); + NDO_CAST(obj::EnumObject, args->get("transf"))->active = 2; + shortcuts->pushBack(shortcut); + + auto trigger = shortcut->addTrigger("invoke"); + auto back = tui->getInput(tp::Keycode::LEFT_CONTROL)->getMember("state"); + trigger->addComparator("comp", back, input_state_hold); + } + + { // pan + auto shortcut = nd::Shortcut::createShortcut("StrokesWidget", "camera_orbit"); + auto args = NDO_CAST(obj::DictObject, shortcut->callbacks_arguments->get("invoke")); + NDO_CAST(obj::LinkObject, args->get("Gui"))->setLink(gui); + shortcuts->pushBack(shortcut); + + auto trigger = shortcut->addTrigger("invoke"); + auto back = tui->getInput(tp::Keycode::SPACE)->getMember("state"); + trigger->addComparator("comp", back, input_state_hold); + } +} \ No newline at end of file diff --git a/Sketch3D/private/strokes.cpp b/Sketch3D/private/strokes.cpp new file mode 100644 index 0000000..b38ae87 --- /dev/null +++ b/Sketch3D/private/strokes.cpp @@ -0,0 +1,501 @@ + +#include "Strokes.h" + +using namespace strokes; + +Stroke::GLHandles::GLHandles() { + glGenVertexArrays(1, &VertexArrayID); + glBindVertexArray(VertexArrayID); + + glGenBuffers(1, &vertexbuffer); +} + +void Stroke::GLHandles::sendDataToGPU(tp::Array* mPoints) { + glBindVertexArray(VertexArrayID); + vbo_len = mPoints->length(); + glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(mPoints[0]) * vbo_len, mPoints->buff(), GL_STATIC_DRAW); +} + +Stroke::GLHandles::~GLHandles() { + glDeleteBuffers(1, &vertexbuffer); + glDeleteVertexArrays(1, &VertexArrayID); +} + +Stroke::Stroke() {} + +tp::Array& Stroke::buff() { + return mPoints; +} + +void Stroke::updateGpuBuffers() { + mGl.sendDataToGPU(&mPoints); +} + +void Stroke::denoisePos(tp::halni passes) { + for (auto pass : tp::Range(passes)) { + for (auto pi : tp::Range(mPoints.length() - 2)) { + mPoints[pi + 1].pos = (mPoints[pi + 1].pos + mPoints[pi].pos + mPoints[pi + 2].pos) / 3.f; + } + } +} + +void Stroke::denoiseThickness(tp::halni passes) { + for (auto pass : tp::Range(passes)) { + for (auto pi : tp::Range(mPoints.length() - 2)) { + mPoints[pi + 1].thickness = (mPoints[pi].thickness + mPoints[pi + 2].thickness) / 2.f; + } + } +} + +void Stroke::compress(tp::halnf factor) { + if (mPoints.length() < 3) { + return; + } + + tp::List passed_poits; + + for (auto idx : tp::Range(mPoints.length())) { + passed_poits.pushBack(mPoints[idx]); + } + + tp::ListNode* min_node = NULL; + do { + min_node = NULL; + tp::halnf min_factor = factor; + + tp::ListNode* iter = passed_poits.first()->next; + for (; iter->next; iter = iter->next) { + tp::vec3f dir1 = (iter->data.pos - iter->prev->data.pos).normalize(); + tp::vec3f dir2 = (iter->next->data.pos - iter->data.pos).normalize(); + tp::halnf factor = 1 - dir1.dot(dir2); + + if (factor < min_factor) { + min_node = iter; + min_factor = factor; + } + } + + if (min_node) { + passed_poits.delNode(min_node); + } + } while (min_node); + + + mPoints.reserve(passed_poits.length()); + for (auto point : passed_poits) { + mPoints[point.idx()] = point.data(); + } +} + +void Stroke::subdiv(tp::halnf precision, tp::Camera* cam, tp::halni passes) { + // TODO + + if (mPoints.length() < 4) { + return; + } + + tp::List new_points; + for (auto idx : tp::Range(mPoints.length())) { + new_points.pushBack(mPoints[idx]); + } + + auto viewmat = cam->viewmat(); + auto projmat = cam->projmat(); + + for (auto i : tp::Range(passes)) { + + auto n_points = new_points.length(); + + auto p0 = new_points.first(); + auto p1 = p0->next; + auto p2 = p1->next; + auto p3 = p2->next; + + while (p3) { + + auto p1_2d = cam->project(p1->data.pos, viewmat, projmat); + auto p2_2d = cam->project(p2->data.pos, viewmat, projmat); + + auto len = (p1_2d - p2_2d).length(); + + if (len > precision * 1.5f) { + auto const a = (p1->data.pos - p0->data.pos).unitv(); + auto const b = (p2->data.pos - p3->data.pos).unitv(); + auto const l = p1->data.pos - p2->data.pos; + auto const l_len = l.length(); + auto const ab = a.dot(b); + auto const la = l.dot(a); + auto const lb = l.dot(b); + tp::vec3f mid; + + if (1 - tp::abs(ab) < 0.001f) { + goto SKIP; + } + + auto k = (lb - ab * la) / (1 - ab * ab); + auto t = ab * k - la; + + if (k < 0 || t < 0) { + goto SKIP; + } + + if (k > l_len) { + k = l_len; + } + + if (t > l_len) { + t = l_len; + } + + auto const p1_mid = p1->data.pos + (a * t); + auto const p2_mid = p2->data.pos + (b * k); + + mid = (p1_mid + p2_mid) / 2.f; + { + auto const p0 = ((p2->data.pos - p1->data.pos) / 2.f) + p1->data.pos; + mid = ((mid - p0) / 3.f) + p0; + } + + auto node = new_points.newNode(); + node->data.pos = mid; + node->data.thickness = (p1->data.thickness + p2->data.thickness) / 2.f; + + auto insert_after = p1; + + p0 = p1; + p1 = p2; + p2 = p3; + p3 = p3->next; + + new_points.attach(node, insert_after); + p0 = p0->next; + continue; + } + + SKIP: + p0 = p1; + p1 = p2; + p2 = p3; + p3 = p3->next; + } + + if (n_points == new_points.length()) { + break; + } + } + + if (new_points.length() != mPoints.length()) { + mPoints.reserve(new_points.length()); + for (auto point : new_points) { + mPoints[point.idx()] = point.data(); + } + } +} + +tp::halni Stroke::saveSize() { + tp::halni out = 0; + out += sizeof(tp::rgba); + out += sizeof(tp::alni); + out += mPoints.length() * sizeof(Point); + return out; +} + +void Stroke::save(tp::File& file) { + file.write(&mCol); + + tp::alni length = mPoints.length(); + file.write(&length); + for (auto piter : mPoints) { + file.write(&piter.data()); + } +} + +void Stroke::load(tp::File& file) { + tp::rgba color; + file.read(&color); + + tp::alni p_len; + file.read(&p_len); + + mPoints.reserve(p_len); + for (auto piter : mPoints) { + file.read(&piter.data()); + } + + updateGpuBuffers(); +} + +PencilBrush::PencilBrush() { + mType = "pencil"; +} + +void PencilBrush::unsureReady(Stroke* stroke, tp::Camera* cam, bool debug) { + if (stroke->buff().length() == 1) { + auto new_point = stroke->buff()[0]; + new_point.pos += 0.00001f; + stroke->buff().pushBack(new_point); + } + + if (mEnableCompression && !debug) { + stroke->subdiv(mPrecision, cam, mSubdivPasses); + stroke->denoisePos(mDenoisePassesPos); + stroke->compress(mCompressionFactor); + } + + stroke->updateGpuBuffers(); +} + +void PencilBrush::sample(Project* proj, tp::vec2f crs, tp::halnf pressure) { + if (proj->mActiveLayer == -1) { + return; + } + + bool max_level = mStroke && mStroke->mPoints.length() > mMaxPoints; + if (!pressure || max_level) { + if (mStroke) { + unsureReady(mStroke, &proj->mCamera); + proj->mLayers[proj->mActiveLayer]->strokes.pushBack(mStroke); + mStroke = NULL; + } + + if (max_level) { + mStroke = new Stroke(); + mStroke->mCol = mCol; + + mStroke->mPoints.pushBack(proj->mLayers[proj->mActiveLayer]->strokes.last()->data->mPoints.last()); + } + + if (!pressure) { + return; + } + } + + auto thickness = pressure * (proj->mCamera.project({ 0.f, 0.f }) - proj->mCamera.project({ mSize, 0.f })).length(); + bool point_passed = true; + + if (mStroke && mStroke->buff().length()) { + auto last_point_2d = proj->mCamera.project(mStroke->buff().last().pos); + point_passed = (crs - last_point_2d).length() > mPrecision; + } + + if (!point_passed) { + return; + } + + if (!mStroke) { + mStroke = new Stroke(); + mStroke->mCol = mCol; + } + + auto point_coords = proj->mCamera.project(crs); + mStroke->buff().pushBack({ point_coords, (tp::halnf) thickness }); + mStroke->updateGpuBuffers(); +} + +void PencilBrush::draw(Renderer* render, tp::Camera* camera) { + if (mStroke) { + if (mEnableCompression) { + mShowStroke.buff() = mStroke->buff(); + mShowStroke.mCol = mStroke->mCol; + unsureReady(&mShowStroke, camera, 0); + render->drawStroke(&mShowStroke, camera); + } else { + render->drawStroke(mStroke, camera); + } + } +} + +PencilBrush::~PencilBrush() { + if (mStroke) delete mStroke; +} + +Project::Layer::~Layer() { + for (auto str : strokes) { + delete str.data(); + } +} + +Project::Project() { + mCamera.lookat({ 0.0f, 0.0f, 0.0f }, { 2.0f, 0.0f, 0.f }, { 0, 0, 1 }); + + auto lay = new Layer(); + lay->enabled = true; + mLayers.pushBack(lay); + mActiveLayer = 0; + + mBrushes.put("eraser", new EraserBrush()); + mBrushes.put("pencil", new PencilBrush()); + + mActiveBrush = "pencil"; + + + auto vec = mCamera.project({0, 0, 0}); + vec = mCamera.project({-1, 0, 0}); + vec = mCamera.project({1, 0, 0}); +} + +Project::~Project() { + for (auto brush : mBrushes) { + delete brush->val; + } +} + +tp::alni Project::saveSize() { + tp::alni out = 0; + out += sizeof(tp::Camera); + out += sizeof(tp::rgba); + out += sizeof(tp::halni); + + out += sizeof(tp::alni); + for (auto layer : mLayers) { + out += layer.data()->name.save_size(); + out += sizeof(bool); + out += sizeof(tp::alni); + for (auto stiter : layer.data()->strokes) { + out += stiter->saveSize(); + } + } + return out; +} + +void Project::save(tp::File& file) { + file.write(&mCamera); + file.write(&mBackgroundColor); + file.write(&mActiveLayer); + + tp::alni lay_len = mLayers.length(); + file.write(&lay_len); + for (auto layer : mLayers) { + layer.data()->name.save(&file); + + file.write(&layer.data()->enabled); + + tp::alni len = layer.data()->strokes.length(); + file.write(&len); + for (auto stiter : layer.data()->strokes) { + stiter->save(file); + } + } +} + +void Project::load(tp::File& file) { + file.read(&mCamera); + file.read(&mBackgroundColor); + file.read(&mActiveLayer); + + tp::alni layers_len; + file.read(&layers_len); + mLayers.reserve(layers_len); + + for (tp::alni idx = 0; idx < layers_len; idx++) { + + tp::string key; key.load(&file); + auto layer = new Layer(); + layer->name = key; + mLayers[idx] = layer; + + file.read(&layer->enabled); + + tp::alni len; + file.read(&len); + + for (tp::alni str_idx = 0; str_idx < len; str_idx++) { + auto str = new Stroke(); + layer->strokes.pushBack(str); + layer->strokes.last()->data->load(file); + } + } +} + +Renderer::Renderer(tp::vec2f size) : + mBuffer(size, { 0.f, 0.f, 1.f, 1.f }, 4), + mBufferDowncast(size, { 0.f, 0.f, 1.f, 1.f }), + mShader(".\\rsc\\shaders\\stroke", ".\\rsc\\shaders\\stroke", ".\\rsc\\shaders\\stroke") +{ + mMatrixUniform = mShader.getu("MVP"); + mColorUniform = mShader.getu("Color"); + mRatioUniform = mShader.getu("Ratio"); + mTargetUniform = mShader.getu("Target"); + mBGColUniform = mShader.getu("BGCol"); +} + +void Renderer::renderBegin() { + mBuffer.begin_draw(); + mBuffer.clear(); +} + +void Renderer::setViewport(tp::rectf viewport) { + mBuffer.setViewport(viewport); +} + +void Renderer::drawStroke(Stroke* str, tp::Camera* camera) { + //return; + + glEnable(GL_DEPTH_TEST); + glEnable(GL_MULTISAMPLE); + + GLint val; + glGetIntegerv(GL_SAMPLES, &val); + + glBindVertexArray(str->mGl.VertexArrayID); + mShader.bind(); + + auto cam_mat = camera->transform_mat().transpose(); + glUniformMatrix4fv(mMatrixUniform, 1, GL_FALSE, &cam_mat[0][0]); + + glUniform4fv(mColorUniform, 1, &str->mCol.r); + + glUniform4fv(mBGColUniform, 1, &mBuffer.col_clear.r); + + auto ratio = camera->get_ratio(); + glUniform1fv(mRatioUniform, 1, &ratio); + + auto target = tp::halnf(((camera->get_target() - camera->get_pos()).length() - camera->get_near()) + / (camera->get_far() - camera->get_near())); + + glUniform1fv(mTargetUniform, 1, &target); + + // 1st attribute buffer : vertices + glEnableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, str->mGl.vertexbuffer); + glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, (void*)0); + + glDrawArrays(GL_LINE_STRIP, 0, str->mGl.vbo_len); + + glDisableVertexAttribArray(0); + + mShader.unbind(); + +} + +void Renderer::renderEnd() { + mBuffer.end_draw(); + + mBufferDowncast.begin_draw(); + + auto size = mBufferDowncast.getSize(); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, mBuffer.buffId()); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mBufferDowncast.buffId()); + mBufferDowncast.clear(); + glBlitFramebuffer(0, 0, size.x, size.y, 0, 0, size.x, size.y, GL_COLOR_BUFFER_BIT, GL_NEAREST); + + mBufferDowncast.end_draw(); +} + +tp::uhalni Renderer::getTextudeId() { + return mBufferDowncast.texId(); +} + +tp::ogl::fbuffer* Renderer::getBuff() { + return &mBufferDowncast; +} + +void Renderer::setClearCol(tp::rgba col) { + mBuffer.col_clear = col; + mBufferDowncast.col_clear = col; +} + +Renderer::~Renderer() { +} diff --git a/Sketch3D/private/strokesobject.cpp b/Sketch3D/private/strokesobject.cpp new file mode 100644 index 0000000..49ade1c --- /dev/null +++ b/Sketch3D/private/strokesobject.cpp @@ -0,0 +1,42 @@ + +#include "strokesobject.h" + +using namespace tp; +using namespace obj; + +void StrokesObject::constructor(StrokesObject* self) { + new (&self->mProject) strokes::Project(); +} + +void StrokesObject::copy(StrokesObject* self, const StrokesObject* in) { + self->mProject = in->mProject; +} + +void StrokesObject::destructor(StrokesObject* self) { + self->mProject.~Project(); +} + +static alni save_size(StrokesObject* self) { + return self->mProject.saveSize(); +} + +static void save(StrokesObject* self, File& file_self) { + self->mProject.save(file_self); +} + +static void load(File& file_self, StrokesObject* self) { + new (&self->mProject) strokes::Project(); + self->mProject.load(file_self); +} + +struct ObjectType obj::StrokesObject::TypeData = { + .base = NULL, + .constructor = (object_constructor) StrokesObject::constructor, + .destructor = (object_destructor) StrokesObject::destructor, + .copy = (object_copy) StrokesObject::copy, + .size = sizeof(StrokesObject), + .name = "StrokesProject", + .save_size = (object_save_size) save_size, + .save = (object_save) save, + .load = (object_load) load, +}; diff --git a/Sketch3D/private/temp.cpp b/Sketch3D/private/temp.cpp new file mode 100644 index 0000000..0a0e4d2 --- /dev/null +++ b/Sketch3D/private/temp.cpp @@ -0,0 +1,1049 @@ + /* + +#include "app.h" + +#include "glutils.h" + +#include "shader.h" +#include "strings.h" + +#include "imgui_internal.h" + +#include "ImGuiUtils.h" + +using namespace tp; + +void StrokeApp::draw_explorer() { + using namespace ImGui; + //objects_gui.oexplorer(200); + //if (SubMenuBegin("Type Info", 1)) { + //objects_gui.oproperties(objects_gui.active->type); + //SubMenuEnd(1); + //} +} + +void StrokeApp::draw_brush_properties(rectf rect) { + using namespace ImGui; + if (ImGui::Button(project->sampler.eraser ? "Eraser" : "Pencil")) { + project->sampler.eraser = !project->sampler.eraser; + } ImGui::SameLine(); + + SetNextItemWidth(rect.z / 2.f); + //ImGui::SetNextItemWidth(rect.w); + if (project->sampler.eraser) { + ImGui::SliderFloat(" ", &project->sampler.eraser_size, 0.001f, 0.2f); + } else { + ImGui::SliderFloat(" ", &project->sampler.screen_thikness, 0.001f, 0.2f); + + SetNextItemWidth(rect.z / 1.5f); + int flags = ImGuiColorEditFlags_NoLabel | + ImGuiColorEditFlags_NoSmallPreview | + ImGuiColorEditFlags_NoSidePreview | + ImGuiColorEditFlags_NoInputs; + ImGui::ColorPicker4("Color", &project->sampler.stroke_col.r, flags); + + PushItemWidth(rect.z / 4.f); + if (SubMenuBegin("Sampler Settings", 1)) { + + if (SubMenuBegin("Denoising Passes", 2)) { + SliderInt("Position", &project->denoise_passes, 0, 10); + SliderInt("Thickness", &project->denoise_passes_thikness, 0, 10); + SubMenuEnd(2); + } + + if (SubMenuBegin("Size Reduction", 2)) { + SliderFloat("Input Precision", &project->sampler.screen_precision, 0, 0.005); + Checkbox("Auto Apply", &project->auto_reduction); + SubMenuEnd(2); + } + + if (SubMenuBegin("Pen Pressure", 2)) { + //ImGui::Bezier("easeOutSine", tablet_input_formater); + SubMenuEnd(2); + } + + SubMenuEnd(1); + } + PopItemWidth(); + } + + +} + +void StrokeApp::draw_toolbar(rectf rec) { + const halnf nbuttons = project ? 6 : 1; + halnf buttsize = clamp(rec.z / nbuttons, 5.f, 50.f) + 30; + vec2f buttrec = {buttsize - 20, buttsize - 50}; + + vec2f midpoint = (rec.pos + rec.size_vecw()) / 2.f; + rec.size = {buttsize * nbuttons + 20, buttsize - 30}; + rec.pos = midpoint - (rec.size / 2.f); + + //ImGui::SetNextWindowPos(ImVec2(rec.x, window.size.y - rec.y)); + ImGui::SetNextWindowSize(ImVec2(rec.z, rec.w)); + ImGui::Begin("ToolBar", 0, ImGui::frame_window); + + vec2f popup_size = vec2f(200, 300); + + auto popup = ImGui::ButtonHoverPopupBegin("Explore", buttrec, popup_size); + if (popup) { draw_explorer(); } + ImGui::HoverPopupEnd(popup); + + if (!project) { + ImGui::End(); + return; + } + + ImGui::SameLine(); + popup = ImGui::ButtonHoverPopupBegin("Layers", buttrec, popup_size); + if (popup) { + if (ImGui::Button("Add New", ImVec2(150, 30))) { + project->layers.pushBack(new drawlayer()); + } + + ImGui::Separator(); + + for (auto lay : project->layers) { + ImGui::PushID((int) lay.idx()); + + bool higlight = lay.data() == project->active_layer; + if (higlight) { + ImGui::PushStyleColor(ImGuiCol_Button, GImGui->Style.Colors[ImGuiCol_ButtonActive]); + } + + if (ImGui::Button(lay->name.cstr(), ImVec2(100, 30))) { + project->active_layer = lay.data(); + } + + if (higlight) { + ImGui::PopStyleColor(); + } + + + if (ImGui::BeginPopupContextItem(lay->name.cstr(), ImGuiPopupFlags_MouseButtonRight)) { + static char name[150] = {"asdas"}; + if (ImGui::InputTextEx(" ", "new name", name, 150, {150 , 30}, ImGuiInputTextFlags_EnterReturnsTrue)) { + lay->name = name; + lay->name.capture(); + } + if (ImGui::Button("Delete", ImVec2(150, 30))) { + project->active_layer = lay.node()->prev ? lay.node()->prev->data : NULL; + delete lay.node()->data; + project->layers.delNode(lay.node()); + ImGui::EndPopup(); + ImGui::PopID(); + break; + } + if (lay.node()->prev && ImGui::Button("Move Up", ImVec2(150, 30))) { + swap(lay.node()->prev->data, lay.data()); + } + if (lay.node()->next && ImGui::Button("Move Down", ImVec2(150, 30))) { + swap(lay.node()->next->data, lay.data()); + } + ImGui::EndPopup(); + } + + ImGui::SameLine(); + bool visable = !lay->hiden; + ImGui::Checkbox("", &visable); + lay->hiden = !visable; + + ImGui::PopID(); + } + } + HoverPopupEnd(popup); + + if (!project->active_layer) { + ImGui::End(); + return; + } + + ImGui::SameLine(); + popup = ImGui::ButtonHoverPopupBegin("Hist", buttrec, vec2f(200, 300)); + if (popup) { + if (ImGui::Button("Undo", ImVec2(100, 30))) { + project->active_layer->undo(); + } + if (ImGui::Button("Redo", ImVec2(100, 30))) { + project->active_layer->redo(); + } + if (ImGui::Button("Clear History", ImVec2(100, 30))) { + project->active_layer->clear_history(); + } + } + HoverPopupEnd(popup); + + ImGui::SameLine(); + popup = ImGui::ButtonHoverPopupBegin("Tool", buttrec, popup_size); + if (popup) { draw_brush_properties(rectf(0, 0, 200, 600)); } + ImGui::HoverPopupEnd(popup); + + + ImGui::SameLine(); + popup = ImGui::ButtonHoverPopupBegin("Proj", buttrec, popup_size); + if (popup) { + + ImGui::SetNextItemWidth(popup_size.x / 2.f); + int flags = ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_NoSidePreview | ImGuiColorEditFlags_NoInputs; + ImGui::ColorPicker4("Background Color", &project->canvas_color.r, flags); + + halni width = ImGui::GetWindowContentRegionWidth(); + if (ImGui::Button("Clear All Strokes", ImVec2(width, 30))) { + project->active_layer->clear_canvas(); + } + + if (ImGui::Button("Reduce Size", ImVec2(width, 30))) { + alni length = project->save_size() / (1024); + project->active_layer->reduce_size(project->pass_factor); + alni length_after = project->save_size() / (1024); + ImGui::Notify(sfmt("Size Redused from %i to %i kb", length, length_after).cstr()); + } + + ImGui::SetNextItemWidth(width / 2); + ImGui::SliderFloat("Pass Facor", &project->pass_factor, 0, 0.01); + } + HoverPopupEnd(popup); + + ImGui::SameLine(); + popup = ImGui::ButtonHoverPopupBegin("Cam", buttrec, vec2f(200, 300)); + if (popup) { + if (ImGui::Button("Reset", ImVec2(100, 30))) { + project->cam.lookat({0, 0, 0}, {100, 0, 0}, {0, 0, 1}); + } + halnf fov = project->cam.get_fov(); + ImGui::Text("Field Of View"); + ImGui::SetNextItemWidth(110); + ImGui::SliderFloat(" ", &fov, 0.1, PI - 0.1); + project->cam.set_fov(fov); + } + HoverPopupEnd(popup); + + ImGui::End(); +} + +void StrokeApp::gui_draw() { + /* + halnf toolbar_size = 290; + draw_toolbar(rectf(window.size.x / 2.f - toolbar_size / 2.f, window.size.y, toolbar_size, 60)); + + if (!project) { + DrawTextR(rectf(window.size / 2.2, 0), "Select Strokes Object", rgba(0.75, 0.75, 0.75, 1)); + + } else if (!gui_is_active || 1) { + + if (project->sampler.eraser) { + halnf size = project->sampler.eraser_size * window.size.x / 2.f; + DrawCircle(window.cursor(), size, rgba(0.9, 0.9, 0.9, 0.7), 2.f); + } else { + halnf size = project->sampler.screen_thikness / 2.f * window.size.x / 2.f; + DrawCircle(window.cursor(), size, rgba(0, 0, 0, 0.7), 4.f); + DrawCircle(window.cursor(), size, project->sampler.stroke_col, 2.f); + } + } +} +*/ + +/* + +#pragma once + +#include "gl.h" +#include "glutils.h" +#include "window.h" +#include "fbuffer.h" + +#include "filesystem.h" + +#include "strokes.h" +#include "ImGuiObjectEditor.h" + +#include "primitives/primitives.h" +#include "strokesobject.h" + +class StrokeApp { + + strokes_project* project = NULL; + + tp::halnf tablet_input_formater[5] = {0.5f, 0.f, 1.f, .5f}; + + public: + + StrokeApp(tp::vec2f size = tp::vec2f(1400, 800)); + ~StrokeApp(); + + private: + + void MainProcTick(); + void MainDrawTick(); + + void camera_controller(); + + private: + + void gui_draw(); + void draw_explorer(); + void draw_toolbar(tp::rectf rect); + void draw_brush_properties(tp::rectf rect); +}; + +using namespace tp; +using namespace obj; + +StrokeApp::StrokeApp(vec2f size) { + //window.col_clear = rgba(0.2, 0.2, 0.23, 1); + //main_window = false; + //window.minsize.assign(400, 400); + + //Object* new_scratch = NULL; + //alni dict_idx = objects_gui.root->items.presents("Scratch"); + //if (dict_idx == -1) { + //new_scratch = NDO->create("strokes"); + //objects_gui.root->items.put("Scratch", new_scratch); + //} else { + //new_scratch = objects_gui.root->items[dict_idx]; + //} + //objects_gui.cd(new_scratch, "Scratch"); +} + +void StrokeApp::MainProcTick() { + + if (!objects_gui.active || objects_gui.active->type->name != "strokes") { + project = NULL; + window.col_clear = rgba(0.22, 0.22, 0.25, 1); + return; + } + + project = &NDO_CAST(StrokesObject, objects_gui.active)->project; + + if (gui_is_active) { + return; + } + + if (!project->active_layer) { + return; + } + + camera_controller(); + + halnf pen_pressure = ImGui::BezierValue(window.pen_pressure(), tablet_input_formater); + project->sampler.sample(&project->active_layer->strokes, &project->active_layer->strokes_undo, window.cursor(1), pen_pressure, &project->cam); + + if (!project->sampler.active_state() && project->sampler.has_input()) { + stroke& str = project->sampler.get_stroke(); + + str.denoise_positions(project->denoise_passes); + str.denoise_thickness(project->denoise_passes_thikness); + + if (project->auto_reduction) { + str.reduce_nof_points(project->pass_factor); + } + + project->active_layer->add_stroke(project->sampler.get_stroke()); + project->sampler.clear(); + } + + whait_for_event = !project->sampler.active_state(); +} + +void StrokeApp::MainDrawTick() { + gui_draw(); + + if (project) { + mat4f cammat = (project->cam.projmat() * project->cam.viewmat()).transpose(); + project->sampler.draw(cammat); + + for (alni idx = project->layers.length() - 1; idx > -1; idx--) { + if (!project->layers[idx]->hiden) { + project->layers[idx]->draw(cammat); + } + } + + window.col_clear = project->canvas_color; + } +} + + +void StrokeApp::camera_controller() { + + vec2f prevcur = window.prevcursor(1); + vec2f cur = window.cursor(1); + + vec2f delta = prevcur - cur; + vec3f target = project->cam.get_target(); + + if (glfwGetKey(window.geth(), GLFW_KEY_LEFT_ALT) == GLFW_PRESS) { + project->cam.rotate(delta.x * 5, delta.y * 5); + } + + if (glfwGetKey(window.geth(), GLFW_KEY_Z) == GLFW_PRESS) { + project->cam.zoom((prevcur.y + 1.f) / (cur.y + 1.f)); + } + + if (glfwGetKey(window.geth(), GLFW_KEY_SPACE) == GLFW_PRESS) { + project->cam.move(cur, prevcur); + } + + if (glfwGetKey(window.geth(), GLFW_KEY_X) == GLFW_PRESS) { + project->pickTargetLength(cur); + } + + if (glfwGetKey(window.geth(), GLFW_KEY_B) == GLFW_PRESS) { + project->sampler.eraser = false; + } + + if (glfwGetKey(window.geth(), GLFW_KEY_E) == GLFW_PRESS) { + project->sampler.eraser = true; + } + + project->cam.set_ratio(window.aspect_ratio()); +} + +StrokeApp::~StrokeApp() {} +*/ + + +/* + +struct StrokesRootWidget : nd::Widget { + + static obj::ObjectType TypeData; + static struct Vtable : Widget::Vtable {} vtable; + + static void constructor(StrokesRootWidget* self) { + } + + static void destructor(StrokesRootWidget* self) {} + static void copy(StrokesRootWidget* self, const StrokesRootWidget* in) {} + + static void procInputs(StrokesRootWidget* self, nd::GUI* gui) { + + auto project = &self->strokes->project; + auto rec = self->getRect(); + + //camera_controller(); + auto cursor = gui->gui_window.mNativeWindow.mEvents.mCursor; + + cursor.y = rec.size.y - cursor.y; + cursor -= tp::vec2f(rec.size) / 2.f; + cursor /= tp::vec2f(rec.size) / 2.f; + + //bool = gui->gui_window.mNativeWindow.mEvents.mLMouseDown; + tp::alnf pressure = gui->gui_window.mNativeWindow.mEvents.mLMouseDown; + //tp::halnf pen_pressure = ImGui::BezierValue(window.pen_pressure(), tablet_input_formater); + project->sampler.sample(&project->active_layer->strokes, &project->active_layer->strokes_undo, cursor, pressure, &project->cam); + + if (!project->sampler.active_state() && project->sampler.has_input()) { + stroke& str = project->sampler.get_stroke(); + + str.denoise_positions(project->denoise_passes); + str.denoise_thickness(project->denoise_passes_thikness); + + if (project->auto_reduction) { + str.reduce_nof_points(project->pass_factor); + } + + project->active_layer->add_stroke(project->sampler.get_stroke()); + project->sampler.clear(); + } + } + + static void presentOutput(StrokesRootWidget* self, nd::GUI* gui) { + if (!self->retrieveProject()) { + return; + } + + tp::rect rec = self->getRect(); + rec.pos = 0; + + glViewport(0, 0, rec.z, rec.w); + glClearColor(0, 0, 0, 0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + auto project = &self->strokes->project; + + tp::mat4f cammat = (project->cam.projmat() * project->cam.viewmat()).transpose(); + project->sampler.draw(cammat); + + for (auto lay : project->layers) { + if (!lay->hiden) { + lay->draw(cammat); + } + } + + //window.col_clear = project->canvas_color; + } + +}; + +*/ + + +/* + +// ----------------------------- strokes ------------------------ // + + +#pragma once + +#include "glcommon.h" +#include "shader.h" + +#include "array.h" +#include "list.h" +#include "map.h" +#include "topology.h" +#include "strings.h" + +struct StrokeMesh { + tp::Array vbo; + + tp::ogl::shader* shader; + tp::rgba color = tp::rgba(1); + + GLuint VertexArrayID; + GLuint vertexbuffer; + GLuint MatrixID; + GLuint ColorID; + + void init(); + + StrokeMesh(); + void operator=(const StrokeMesh& in); + + void bind_buffers(); + void draw_mesh(const tp::mat4f& cammat); + ~StrokeMesh(); +}; + +struct stroke_point { + tp::vec3f pos; + tp::vec3f normal; + tp::halnf thikness; + stroke_point(); +}; + +struct stroke { + + tp::Array points; + + StrokeMesh mesh; + + void gen_quad(tp::alni pidx, stroke_point* p1, stroke_point* p2, tp::vec3f dir1, tp::vec3f dir2); + tp::vec3f split_dir(tp::vec3f v1, tp::vec3f v2, const tp::vec3f& norm); + + void gen_mesh(); + + void drawcall(const tp::mat4f& cammat); + void add_point(const stroke_point& p); + + +}; + +class drawlayer { + public: + tp::string name = "new layer"; + tp::List strokes; + tp::List strokes_undo; + bool hiden = false; + + void undo(); + void redo(); + + void add_stroke(const stroke& str); + void draw(const tp::mat4f& cammat); + + void clear_history() { + strokes_undo.free(); + } + + void clear_canvas() { + tp::alni len = strokes.length(); + for (tp::alni idx = 0; idx < len; idx++) { + undo(); + } + } + + void reduce_size(tp::halnf pass_factor) { + for (auto str : strokes) { + str.data().reduce_nof_points(pass_factor); + str.data().gen_mesh(); + } + } + + void clear() { + strokes.free(); + strokes_undo.free(); + } +}; + +class inputsmpler { + public: + + bool is_active = false; + + stroke input; + tp::halnf pressure; + + tp::rgba stroke_col = tp::rgba(0.77, 0.77, 0.77, 1); + + tp::halnf screen_precision = 0.000f; + tp::halnf precision = 0.002; + tp::halnf screen_thikness = 0.01f; + tp::halnf thickness = 0.04; + + bool eraser = false; + tp::halnf eraser_size = 0.1f; + + void add_point(const tp::vec3f& pos, const tp::vec3f& norm, float thickness); + bool passed(const tp::vec3f& point); + void start(const tp::vec2f& cpos, tp::Camera* cam); + void sample_util(const tp::vec2f& cpos, tp::Camera* cam); + void erase_util(tp::List* pull, tp::List* undo, const tp::vec2f& cpos, tp::Camera* cam); + void finish(const tp::vec2f& cpos, tp::Camera* cam); + + void sample(tp::List* pool, tp::List* undo, tp::vec2f curs, float pressure, tp::Camera* cam); + void draw(const tp::mat4f& cammat); + + bool active_state(); + bool has_input(); + void clear(); + stroke& get_stroke(); +}; + +struct strokes_project { + + tp::Camera cam; + drawlayer* active_layer = NULL; + tp::List layers; + inputsmpler sampler; + tp::rgba canvas_color = tp::rgba(0.22f, 0.22f, 0.25f, 1.f); + + tp::halni denoise_passes = 1; + tp::halni denoise_passes_thikness = 3; + bool auto_reduction = true; + tp::halnf pass_factor = tp::halnf(0.001); + + strokes_project() { + cam.lookat({0, 0, 0}, {100, 0, 0}, {0, 0, 1}); + } + + drawlayer* get_base_layer() { + if (!layers.length()) { + layers.pushBack(new drawlayer()); + } + return layers[0]; + } + + void append_layers(drawlayer* base) { + for (auto lay : layers) { + base->strokes += lay->strokes; + } + } + + void pickTargetLength(const tp::vec2f& cur) { + using namespace tp; + stroke_point* target_point = NULL; + alnf min_length = FLT_MAX; + alnf min_tollerance = 0.05f; + + for (auto lay : layers) { + for (auto stroke : lay->strokes) { + for (auto pnt : stroke.data().points) { + + alnf tollerance = (cam.project(pnt.data().pos) - cur).length(); + alnf len = (pnt->pos - cam.get_pos()).dot(cam.get_fw()); + + alnf toll2 = abs(min_tollerance - tollerance); + bool selected = (toll2 < 0.01) ? (min_length > len) : (min_tollerance > tollerance); + + if (selected) { + target_point = &pnt.data(); + min_length = len; + min_tollerance = tollerance; + } + } + } + } + + if (target_point) { + cam.lookat(cam.get_pos() + (cam.get_fw() * min_length), cam.get_pos(), cam.get_up()); + } + } + + tp::alni save_size(); + void save(tp::File& file); + void load(tp::File& file); + + ~strokes_project() { + for (auto lay : layers) { + delete lay.data(); + } + } +}; + + + +#include "strokes.h" + +#include "glutils.h" + +#include "StrokesVersions.h" + +using namespace tp; + +void StrokeMesh::init() { + static ogl::shader shader("rsc/shaders/stroke", NULL, "rsc/shaders/stroke"); + this->shader = &shader; + + MatrixID = shader.getu("MVP"); + ColorID = shader.getu("StrokeColor"); + + glGenVertexArrays(1, &VertexArrayID); + glBindVertexArray(VertexArrayID); + + glGenBuffers(1, &vertexbuffer); +} + +StrokeMesh::StrokeMesh() { + init(); +} + +void StrokeMesh::operator=(const StrokeMesh& in) { + + glDeleteBuffers(1, &vertexbuffer); + glDeleteVertexArrays(1, &VertexArrayID); + + init(); + + vbo = in.vbo; + color = in.color; + + bind_buffers(); +} + +void StrokeMesh::bind_buffers() { + + glBindVertexArray(VertexArrayID); + + glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(vbo[0]) * vbo.length(), vbo.buff(), GL_STATIC_DRAW); +} + + +void StrokeMesh::draw_mesh(const mat4f& cammat) { + + glBindVertexArray(VertexArrayID); + + shader->bind(); + glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &cammat[0][0]); + glUniform4fv(ColorID, 1, &color.rgbs.r); + + // 1st attribute buffer : vertices + glEnableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*) 0); + + // Draw the triangles ! // mode. count. type. element. array buffer offset + glDrawArrays(GL_TRIANGLES, 0, vbo.length()); + + glDisableVertexAttribArray(0); + + shader->unbind(); +} + +StrokeMesh::~StrokeMesh() { + glDeleteBuffers(1, &vertexbuffer); + glDeleteVertexArrays(1, &VertexArrayID); +} + + +stroke_point::stroke_point() { + pos = vec3f(0, 0, 0); + normal = vec3f(0, 1, 0); + thikness = 0.06; +} + +void stroke::gen_quad(alni pidx, stroke_point* p1, stroke_point* p2, vec3f dir1, vec3f dir2) { + //vec3 perp = normalize(cross(p1->normal, p2->pos - p1->pos)) * p1->thikness; + //float cosa1 = dot(perp, dir1) / (dir1.length() * perp.length()); + //float cosa2 = dot(perp, dir2) / (dir2.length() * perp.length()); + //float thickness1 = -p1->thikness / cosa1; + //float thickness2 = -p2->thikness / cosa2; + +vec3f v1 = p1->pos + dir1 * p1->thikness; +vec3f v2 = p2->pos + dir2 * p2->thikness; +vec3f v3 = p2->pos - dir2 * p2->thikness; +vec3f v4 = p1->pos - dir1 * p1->thikness; + +alni vidx = pidx * 6; + +mesh.vbo[vidx] = v1; +mesh.vbo[vidx + 1] = v2; +mesh.vbo[vidx + 2] = v3; +mesh.vbo[vidx + 3] = v3; +mesh.vbo[vidx + 4] = v4; +mesh.vbo[vidx + 5] = v1; +} + +vec3f stroke::split_dir(vec3f v1, vec3f v2, const vec3f& norm) { + + v1.normalize(); + v2.normalize(); + + vec3f plane_normal; + + halnf val = v2.dot(v1); + + if (val > -0.999999) { + vec3f crossp = v1.cross(v2); + mat3f rot_matrix = mat3f::rotmat(crossp, acos(v1.dot(v2)) / 2.f); + vec3f middle = rot_matrix * v1; + plane_normal = middle.cross(crossp); + } + else { + plane_normal = v2; + } + + return plane_normal.cross(norm); +} + +void stroke::gen_mesh() { + + alni nvert = (points.length() - 1) * 6; + + mesh.vbo.reserve(nvert); + + for (alni pidx = 0; pidx < points.length() - 1; pidx++) { + + stroke_point pt0; + stroke_point pt1 = points[pidx]; + stroke_point pt2 = points[pidx + 1]; + stroke_point pt3; + + vec3f dir1; + vec3f dir2; + + if (pidx > 0) { + pt0 = points[pidx - 1]; + } + else { + pt0.pos = pt1.pos + (pt1.pos - pt2.pos); + pt0.thikness = 0.001; + } + + if (pidx < points.length() - 2) { + pt3 = points[pidx + 2]; + } + else { + pt3.pos = pt2.pos + (pt2.pos - pt1.pos); + pt3.thikness = 0.001; + } + + dir1 = split_dir(pt0.pos - pt1.pos, pt2.pos - pt1.pos, pt1.normal); + dir2 = split_dir(pt1.pos - pt2.pos, pt3.pos - pt2.pos, pt2.normal); + + if (dir1.dot(dir2) < 0) { + dir1 *= -1; + } + + gen_quad(pidx, &pt1, &pt2, dir1.unitv(), dir2.unitv()); + } + + mesh.bind_buffers(); +} + +void stroke::drawcall(const mat4f& cammat) { + mesh.draw_mesh(cammat); +} + +void stroke::add_point(const stroke_point& p) { + points.pushBack(p); +} + + +void drawlayer::undo() { + if (strokes.last()) { + strokes_undo.pushBack(strokes.last()->data); + strokes.delNode(strokes.last()); + } +} + +void drawlayer::redo() { + if (strokes_undo.last()) { + strokes.pushBack(strokes_undo.last()->data); + strokes_undo.delNode(strokes_undo.last()); + } +} + +void drawlayer::add_stroke(const stroke& str) { + strokes.pushBack(str); + strokes.last()->data.gen_mesh(); +} + +void drawlayer::draw(const mat4f& cammat) { + for (ListNode* str = strokes.last(); str; str = str->prev) { + str->data.drawcall(cammat); + } +} + +void inputsmpler::add_point(const vec3f& pos, const vec3f& norm, float thickness) { + stroke_point p; + p.pos = pos; + p.normal = norm; + p.thikness = thickness * pressure; + input.add_point(p); +} + +bool inputsmpler::passed(const vec3f& point) { + if (input.points.length()) { + return (point - input.points[input.points.length() - 1].pos).length() > precision; + } + return true; +} + +void inputsmpler::start(const vec2f& cpos, Camera* cam) { + input.points.free(); + vec3f point = cam->project(cpos); + add_point(point, cam->get_fw(), 0.001f); +} + +void inputsmpler::sample_util(const vec2f& cpos, Camera* cam) { + vec3f point = cam->project(cpos); + if (passed(point)) { + add_point(point, cam->get_fw(), thickness); + } +} + +void inputsmpler::erase_util(List* pull, List* undo, const vec2f& cpos, Camera* cam) { + + ListNode* str = pull->first(); + while (str) { + bool remove = false; + + for (auto pnt : str->data.points) { + if ((cam->project(pnt.data().pos) - cpos).length() < eraser_size) { + remove = true; + break; + } + } + + if (remove) { + ListNode* tmp = str->next; + undo->pushBack(str->data); + pull->delNode(str); + str = tmp; + } + else { + str = str->next; + } + } +} + +void inputsmpler::finish(const vec2f& cpos, Camera* cam) { + if (input.points.length() <= 1) { + input.points.free(); + } + else { + input.points[input.points.length() - 1].thikness = 0.001f; + cam->offset_target(0.0001); + } +} + +void inputsmpler::sample(List* pull, List* undo, vec2f curs, float pressure, Camera* cam) { + + this->input.mesh.color = stroke_col; + this->pressure = pressure; + + if (eraser) { + if (pressure > 0) { + input.points.free(); + erase_util(pull, undo, curs, cam); + } + return; + } + else { + vec3f zero_projection = cam->project(vec2f(0.f)); + thickness = (cam->project(vec2f(0.f, screen_thikness)) - zero_projection).length(); + precision = (cam->project(vec2f(0.f, screen_precision)) - zero_projection).length(); + } + + switch (is_active) { + case false: + { + if (pressure) { + start(curs, cam); + is_active = true; + } + return; + } + case true: + { + sample_util(curs, cam); + if (!pressure) { + finish(curs, cam); + is_active = false; + return; + } + return; + } + } +} + +void inputsmpler::draw(const mat4f& cammat) { + + static int prev_len = input.points.length(); + if (input.points.length() > 1) { + if (prev_len < input.points.length()) { + input.gen_mesh(); + } + input.drawcall(cammat); + } + prev_len = input.points.length(); +} + +bool inputsmpler::active_state() { + return is_active; +} +bool inputsmpler::has_input() { + return input.points.length() > 1; +} +void inputsmpler::clear() { + input.points.free(); + is_active = false; +} +stroke& inputsmpler::get_stroke() { + return input; +} + + +alni strokes_project::save_size() { + alni version_size = StrokesVersion1::save_size(this); + return version_size + sizeof(ProjectInfo); +} + +void strokes_project::save(File& file) { + ProjectInfo head; + head.init(1); + file.write(&head); + StrokesVersion1::save(file, this); +} + +void strokes_project::load(File& file) { + ProjectInfo head; + file.read(&head); + + if (string(head.name) != "strokes") { + return; + } + + alni version = alni(string(head.version)); + if (version > 1 || version < 0) { + return; + } + + version == 1 ? StrokesVersion1::load(file, this) : StrokesVersion0::load(file, this); + + active_layer = get_base_layer(); +} + +*/ \ No newline at end of file diff --git a/Sketch3D/public/StrokesWidget.h b/Sketch3D/public/StrokesWidget.h new file mode 100644 index 0000000..729c8f1 --- /dev/null +++ b/Sketch3D/public/StrokesWidget.h @@ -0,0 +1,29 @@ + +#include "gui/Widgets.h" + +#include "nodes.h" +#include "StrokesObject.h" + +namespace nd { + struct StrokesWidget : Widget { + static obj::ObjectType TypeData; + static struct Vtable : Widget::Vtable {} vtable; + + strokes::Renderer mRenderer; + nd::GUIdrawer::ImageHandle mImage; + nd::GUIdrawer* mDrawer = NULL; + + static void constructor(StrokesWidget* self); + static void destructor(StrokesWidget* self); + static void copy(Widget* self, const Widget* in); + static void procInputs(StrokesWidget* self, nd::GUIInputs* inputs); + static void presentOutput(StrokesWidget* self, nd::GUIdrawer* drawer); + + strokes::Project* getTargetProject(); + + static void OpsInit(); + static void OpsUnInit(); + + static void addShortcuts(nd::NodesCore* core); + }; +}; \ No newline at end of file diff --git a/Sketch3D/public/strokes.h b/Sketch3D/public/strokes.h new file mode 100644 index 0000000..f8bf250 --- /dev/null +++ b/Sketch3D/public/strokes.h @@ -0,0 +1,153 @@ + +#include "Buffer.hpp" +#include "List.hpp" +#include "Map.hpp" +#include "Topology.hpp" +#include "Color.hpp" +#include "Strings.hpp" + +namespace strokes { + + class Renderer; + class Project; + + class Stroke { + + friend Renderer; + + public: + struct Point { + tp::vec3f pos; + tp::halnf thickness = NULL; + }; + + tp::Array mPoints; + tp::rgba mCol; + + private: + struct GLHandles { + GLuint VertexArrayID = 0; + GLuint vertexbuffer = 0; + GLuint vbo_len = 0; + GLHandles(); + void sendDataToGPU(tp::Array* mPoints); + ~GLHandles(); + } mGl; + + public: + + Stroke(); + + void denoisePos(tp::halni passes); + void denoiseThickness(tp::halni passes); + void compress(tp::halnf factor); + void subdiv(tp::halnf precition, tp::Camera* cam, tp::halni passes = 1); + + void updateGpuBuffers(); + + tp::Array& buff(); + + tp::halni saveSize(); + void save(tp::File& file); + void load(tp::File& file); + }; + + class Brush { + public: + tp::string mType = "equal"; + Brush() {} + virtual void sample(Project* proj, tp::vec2f crs, tp::halnf pressure) {} + virtual void draw(Renderer* render, tp::Camera* camera) {} + virtual ~Brush() {} + }; + + class PencilBrush : public Brush { + + tp::halnf mPrecision = 0.001f; + + tp::halni mDenoisePassesPos = 1; + tp::halni mDenoisePassesThick = 3; + tp::halnf mCompressionFactor = 0.0001f; + tp::halni mSubdivPasses = 3; + bool mEnableCompression = true; + + tp::halni mMaxPoints = 100; + + Stroke* mStroke = NULL; + Stroke mShowStroke; + + void unsureReady(Stroke* stroke, tp::Camera* cam, bool debug = false); + + public: + + tp::rgba mCol = tp::rgba(1.0f); + tp::halnf mSize = 0.01f; + + PencilBrush(); + virtual void sample(Project* proj, tp::vec2f crs, tp::halnf pressure) override; + virtual void draw(Renderer* render, tp::Camera* camera) override; + virtual ~PencilBrush(); + }; + + struct EraserBrush : public Brush { + EraserBrush() { mType = "eraser"; } + virtual void sample(Project* proj, tp::vec2f crs, tp::halnf pressure) override {} + virtual void draw(Renderer* render, tp::Camera* camera) override {} + virtual ~EraserBrush() {} + }; + + class Project { + + public: + + struct Layer { + tp::string name = "new layer"; + tp::List strokes; + bool enabled = true; + ~Layer(); + }; + + tp::Array mLayers; + tp::halni mActiveLayer = -1; + + tp::Camera mCamera; + tp::rgba mBackgroundColor = { 0.22f, 0.22f, 0.25f, 1.f }; + + tp::HashMap mBrushes; + tp::string mActiveBrush; + + Project(); + ~Project(); + + tp::alni saveSize(); + void save(tp::File& file); + void load(tp::File& file); + }; + + class Renderer { + tp::ogl::fbuffer mBufferDowncast; + tp::ogl::fbuffer mBuffer; + tp::ogl::shader mShader; + + // shader uniforms + GLuint mMatrixUniform = 0; + GLuint mColorUniform = 0; + GLuint mRatioUniform = 0; + GLuint mTargetUniform = 0; + GLuint mBGColUniform = 0; + + public: + Renderer(tp::vec2f size); + + void renderBegin(); + void setViewport(tp::rectf viewport); + void drawStroke(Stroke* str, tp::Camera* camera); + void renderEnd(); + + void setClearCol(tp::rgba col); + tp::uhalni getTextudeId(); + tp::ogl::fbuffer* getBuff(); + + ~Renderer(); + }; +}; \ No newline at end of file diff --git a/Sketch3D/public/strokes_versions.h b/Sketch3D/public/strokes_versions.h new file mode 100644 index 0000000..869221e --- /dev/null +++ b/Sketch3D/public/strokes_versions.h @@ -0,0 +1,178 @@ +#pragma once + +#include "strokes.h" + +struct ProjectInfo { + char name[10] = {0}; + char version[10] = {0}; + + void init(tp::alni v) { + tp::string version_s(v); + tp::memcp(name, "strokes", tp::slen("strokes")); + tp::memcp(version, version_s.cstr(), version_s.size()); + } +}; + +namespace StrokesVersion0 { + + tp::alni save_size(strokes_project* proj) { + tp::alni out = 0; + out += sizeof(tp::Camera); + out += sizeof(tp::rgba); + + out += sizeof(tp::alni); + for (auto layer : proj->layers) { + for (auto stiter : layer->strokes) { + stroke* str = &stiter.data(); + + out += sizeof(tp::rgba); + + out += sizeof(tp::alni); + out += str->points.length() * sizeof(stroke_point); + } + } + return out; + } + + void save(tp::File& file, strokes_project* proj) { + + file.write(&proj->cam); + file.write(&proj->canvas_color); + + drawlayer base_layer; + proj->append_layers(&base_layer); + + tp::alni len = base_layer.strokes.length(); + file.write(&len); + for (auto stiter : base_layer.strokes) { + stroke* str = &stiter.data(); + + tp::alni length = str->points.length(); + file.write(&length); + file.write(&str->mesh.color); + + for (auto piter : str->points) { + file.write(&piter.data()); + } + } + } + + void load(tp::File& file, strokes_project* proj) { + file.read(&proj->cam); + + file.read(&proj->canvas_color); + + drawlayer* base = proj->get_base_layer(); + + tp::alni len; + file.read(&len); + + for (tp::alni str_idx = 0; str_idx < len; str_idx++) { + stroke str = stroke(); + + tp::alni p_len; file.read(&p_len); + tp::rgba color; file.read(&color); + + str.points.reserve(p_len); + for (auto piter : str.points) { + file.read(&piter.data()); + } + + str.mesh.color = color; + base->add_stroke(str); + } + } +}; + +namespace StrokesVersion1 { + + tp::alni save_size(strokes_project* proj) { + tp::alni out = 0; + out += sizeof(tp::Camera); + out += sizeof(tp::halnf); + out += sizeof(tp::halnf); + out += sizeof(tp::rgba); + + out += sizeof(tp::alni); + for (auto layer : proj->layers) { + out += layer->name.save_size(); + out += sizeof(bool); + out += sizeof(tp::alni); + for (auto stiter : layer->strokes) { + stroke* str = &stiter.data(); + out += sizeof(tp::rgba); + out += sizeof(tp::alni); + out += str->points.length() * sizeof(stroke_point); + } + } + return out; + } + + void save(tp::File& file, strokes_project* proj) { + + file.write(&proj->cam); + file.write(&proj->sampler.eraser_size); + file.write(&proj->sampler.screen_thikness); + file.write(&proj->canvas_color); + + tp::alni lay_len = proj->layers.length(); + file.write(&lay_len); + for (auto layer : proj->layers) { + layer->name.save(&file); + + file.write(&layer->hiden); + + tp::alni len = layer->strokes.length(); + file.write(&len); + for (auto stiter : layer->strokes) { + stroke* str = &stiter.data(); + + file.write(&str->mesh.color); + + tp::alni length = str->points.length(); + file.write(&length); + for (auto piter : str->points) { + file.write(&piter.data()); + } + } + } + } + + void load(tp::File& file, strokes_project* proj) { + file.read(&proj->cam); + + file.read(&proj->sampler.eraser_size); + file.read(&proj->sampler.screen_thikness); + file.read(&proj->canvas_color); + + tp::alni layers_len; + file.read(&layers_len); + for (tp::alni str_idx = 0; str_idx < layers_len; str_idx++) { + + tp::string key; key.load(&file); + drawlayer* layer = new drawlayer(); + layer->name = key; + proj->layers.pushBack(layer); + + file.read(&layer->hiden); + + tp::alni len; + file.read(&len); + + for (tp::alni str_idx = 0; str_idx < len; str_idx++) { + stroke str = stroke(); + + tp::rgba color; file.read(&color); + + tp::alni p_len; file.read(&p_len); + str.points.reserve(p_len); + for (auto piter : str.points) { + file.read(&piter.data()); + } + + str.mesh.color = color; + layer->add_stroke(str); + } + } + } +}; diff --git a/Sketch3D/rsc/Font.ttf b/Sketch3D/rsc/Font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8a630544c99ec9956153ebd9509abeac421c6d75 GIT binary patch literal 42624 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