From 015b0d7384f3a786748fbd0b38e64ac4cc2a293f Mon Sep 17 00:00:00 2001 From: valmme Date: Tue, 19 May 2026 17:22:35 +0400 Subject: [PATCH 1/5] 3d text (beta) --- CMakeLists.txt | 28 +- quark-libs/freetype | 1 + src/editor/editor_components_ui.cpp | 82 ++++++ src/editor/editor_components_ui.h | 1 + src/editor/editor_entity.cpp | 6 + src/headers/component.h | 31 +++ src/headers/models.h | 2 +- src/headers/text_mesh.h | 10 + src/main.cpp | 5 + src/models.cpp | 22 +- src/text_mesh.cpp | 415 ++++++++++++++++++++++++++++ 11 files changed, 599 insertions(+), 4 deletions(-) create mode 160000 quark-libs/freetype create mode 100644 src/headers/text_mesh.h create mode 100644 src/text_mesh.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c78d45d..480fb11 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,7 @@ set(BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE) set(GLFW_BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE) add_subdirectory(quark-libs/raylib) +add_subdirectory(quark-libs/freetype) include_directories( ${CMAKE_SOURCE_DIR}/quark-libs/imgui/include @@ -34,6 +35,7 @@ target_include_directories(imgui PUBLIC ${CMAKE_SOURCE_DIR}/quark-libs/imgui/include ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/src/headers + ${CMAKE_SOURCE_DIR}/quark-libs/freetype/include ) if(WIN32) @@ -48,7 +50,7 @@ else() ) endif() -target_link_libraries(imgui PRIVATE raylib) +target_link_libraries(imgui PRIVATE raylib freetype) set_target_properties(imgui PROPERTIES PREFIX "" @@ -87,10 +89,15 @@ if(BUILD_ENGINE) src/rlights.cpp src/scene.cpp src/tex.cpp + src/text_mesh.cpp ) add_executable(${PROJECT_NAME} ${SOURCE_FILES} ${EDITOR_FILES}) + target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_SOURCE_DIR}/quark-libs/freetype/include + ) + if(WIN32) target_compile_definitions(${PROJECT_NAME} PRIVATE USE_LIBTYPE_SHARED @@ -103,7 +110,7 @@ if(BUILD_ENGINE) ) endif() - target_link_libraries(${PROJECT_NAME} PRIVATE raylib imgui) + target_link_libraries(${PROJECT_NAME} PRIVATE raylib imgui freetype) if(WIN32) target_link_libraries(${PROJECT_NAME} PRIVATE winmm gdi32 opengl32) @@ -126,6 +133,10 @@ if(BUILD_ENGINE) ) if(WIN32) + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} + ) + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different $ @@ -137,6 +148,19 @@ if(BUILD_ENGINE) ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ) endif() + + if(WIN32) + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} + ) + + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + ) + endif() endif() if(BUILD_PLUGIN) diff --git a/quark-libs/freetype b/quark-libs/freetype new file mode 160000 index 0000000..7e0e56f --- /dev/null +++ b/quark-libs/freetype @@ -0,0 +1 @@ +Subproject commit 7e0e56f84fd53cf38378d33c8fc8f92d12ab9ac6 diff --git a/src/editor/editor_components_ui.cpp b/src/editor/editor_components_ui.cpp index 95911b1..3de409d 100644 --- a/src/editor/editor_components_ui.cpp +++ b/src/editor/editor_components_ui.cpp @@ -3,6 +3,7 @@ #include "editor_ui.h" #include "editor_viewers.h" #include "imgui.h" +#include "headers/text_mesh.h" #include "headers/models.h" #include "headers/entity.h" #include "editor/editor_ui.h" @@ -47,6 +48,7 @@ void ComponentUIHelper::draw_entity_inspector(Editor& editor, Entity& entity, Sh else if (auto light = std::dynamic_pointer_cast(comp)) draw_light_component(editor, entity, light.get(), shader); else if (auto material = std::dynamic_pointer_cast(comp)) draw_material_component(editor, entity, material.get()); else if (auto collision = std::dynamic_pointer_cast(comp)) draw_collision_component(editor, entity, collision.get()); + else if (auto text = std::dynamic_pointer_cast(comp)) draw_3d_text_component(editor, entity, text.get()); ImGui::Spacing(); @@ -136,6 +138,25 @@ void ComponentUIHelper::draw_entity_inspector(Editor& editor, Entity& entity, Sh components_manager->add_component(light); } else { editor.undo(); } } + + if (ImGui::MenuItem(lang.word("text"))) { + editor.save_state(); + bool already_exists = false; + + for (size_t j = 0; j < components_manager->get_component_count(); ++j) { + auto existing = components_manager->get_component(j); + if (existing && existing->get_type() == COMPONENT_CUSTOM) { + already_exists = true; + break; + } + + } + + if (!already_exists) { + auto text = std::make_shared(); + components_manager->add_component(text); + } else { editor.undo(); } + } ImGui::EndPopup(); } @@ -634,3 +655,64 @@ void ComponentUIHelper::draw_collision_component(Editor& editor, Entity& entity, mark_entity_bounds_dirty(&entity); } } + +void ComponentUIHelper::draw_3d_text_component(Editor& editor, Entity& entity, Text3DComponent* text) { + if (!text) return; + + char buf[256] = {}; + strncpy_s(buf, sizeof(buf), text->text.c_str(), _TRUNCATE); + + if (ImGui::InputText("Text", buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue)) { + editor.save_state(); + text->text = buf; + update_model(&entity); + mark_entity_bounds_dirty(&entity); + } + + auto drag = [&](const char* label, float& val, float spd, float mn, float mx) { + if (ImGui::DragFloat(label, &val, spd, mn, mx)) { + editor.save_state(); + update_model(&entity); + mark_entity_bounds_dirty(&entity); + } + }; + + drag("Size", text->size, 0.01f, 0.05f, 10.f); + drag("Thickness", text->thickness, 0.01f, 0.01f, 5.f); + drag("Letter Spacing", text->letter_spacing, 0.005f, 0.f, 100); + + ImGui::Separator(); + + static std::vector> s_fonts; + static std::vector s_font_names; + static int s_selected = -1; + static bool s_scanned = false; + + if (!s_scanned) { + s_fonts = get_system_fonts(); + s_font_names.clear(); + for (auto& f : s_fonts) s_font_names.push_back(f.first.c_str()); + + for (int i = 0; i < (int)s_fonts.size(); i++) { + if (s_fonts[i].second == text->font_path) { s_selected = i; break; } + } + s_scanned = true; + } + + ImGui::Text("Font"); + if (!s_font_names.empty()) { + if (ImGui::Combo("##font", &s_selected, s_font_names.data(), (int)s_font_names.size())) { + editor.save_state(); + text->font_path = s_fonts[s_selected].second; + update_model(&entity); + mark_entity_bounds_dirty(&entity); + } + } + + else { + ImGui::TextDisabled("No fonts found"); + } + + if (!text->font_path.empty() && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", text->font_path.c_str()); +} \ No newline at end of file diff --git a/src/editor/editor_components_ui.h b/src/editor/editor_components_ui.h index b055500..c08efe9 100644 --- a/src/editor/editor_components_ui.h +++ b/src/editor/editor_components_ui.h @@ -14,6 +14,7 @@ class ComponentUIHelper { static void draw_light_component(Editor& editor, Entity& entity, LightComponent* light, Shader shader); static void draw_material_component(Editor& editor, Entity& entity, MaterialComponent* material); static void draw_collision_component(Editor& editor, Entity& entity, CollisionComponent* collision); + static void draw_3d_text_component(Editor& editor, Entity& entity, Text3DComponent* text); private: static bool should_show_component_menu; diff --git a/src/editor/editor_entity.cpp b/src/editor/editor_entity.cpp index 7708076..e9cbb4a 100644 --- a/src/editor/editor_entity.cpp +++ b/src/editor/editor_entity.cpp @@ -37,7 +37,13 @@ Entity make_entity_from_asset(Scene& scene, ModelAsset& asset) { store_material_textures(&entity); mat->texture_source = TEXTURE_NONE; mat->texture_name.clear(); + + if (asset.name == "Text") { + auto text_comp = std::make_shared(); + entity.get_components()->add_component(text_comp); + } } + else { if (!load_model_instance(asset, mesh->model)) { mesh->asset = nullptr; diff --git a/src/headers/component.h b/src/headers/component.h index 005aa68..80d3424 100644 --- a/src/headers/component.h +++ b/src/headers/component.h @@ -317,3 +317,34 @@ class ComponentManager { void deserialize(const nlohmann::json& json); }; + +class Text3DComponent : public Component { +public: + std::string text = "3D Text"; + float size = 1.0f; + float thickness = 0.2f; + float letter_spacing = 0.1f; + std::string font_path; + + + Text3DComponent() { name = "3D Text"; type = COMPONENT_CUSTOM; } + + std::string get_type_name() const override { return "3D Text"; } + ComponentType get_type() const override { return COMPONENT_CUSTOM; } + + void serialize(nlohmann::json& j) const override { + j["text"] = text; + j["size"] = size; + j["thickness"] = thickness; + j["letter_spacing"] = letter_spacing; + j["font_path"] = font_path; + } + + void deserialize(const nlohmann::json& j) override { + if (j.contains("text")) text = j["text"]; + if (j.contains("size")) size = j["size"]; + if (j.contains("thickness")) thickness = j["thickness"]; + if (j.contains("letter_spacing")) letter_spacing = j["letter_spacing"]; + if (j.contains("font_path")) font_path = j["font_path"]; + } +}; \ No newline at end of file diff --git a/src/headers/models.h b/src/headers/models.h index 21a2f22..f6f8738 100644 --- a/src/headers/models.h +++ b/src/headers/models.h @@ -23,4 +23,4 @@ bool entity_has_mesh_overrides(const Entity& entity); void capture_mesh_overrides_from_model(Entity& entity); bool apply_mesh_overrides(Entity& entity); bool get_mesh_triangle_vertex_indices(const Mesh& mesh, int triangle_index, int out_indices[3]); -bool detach_mesh_triangles(Entity& entity); +bool detach_mesh_triangles(Entity& entity); \ No newline at end of file diff --git a/src/headers/text_mesh.h b/src/headers/text_mesh.h new file mode 100644 index 0000000..4be7384 --- /dev/null +++ b/src/headers/text_mesh.h @@ -0,0 +1,10 @@ +#include +#include +#include + +void init_freetype(); +void shutdown_freetype(); + +std::vector> get_system_fonts(); +std::string get_default_font_path(); +Model generate_text_mesh(const std::string& text, float size, float thickness, float letter_spacing, const std::string& font_path); \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 4e74903..dd7c66c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include "plugins/plugin_manager.h" #include "headers/lighting.h" #include "headers/language_manager.h" +#include "headers/text_mesh.h" #include "editor/editor.h" #include "editor/editor_entity.h" #include "headers/camera.h" @@ -409,6 +410,7 @@ int main(int argc, char* argv[]) { InitWindow(1280, 720, "Quark Engine"); SetTargetFPS(GetMonitorRefreshRate(GetCurrentMonitor())); + init_freetype(); rlImGuiSetup(true); reload_editor_fonts(LanguageManager::get().current); @@ -519,6 +521,8 @@ int main(int argc, char* argv[]) { bool has_shadow_light = false; int active_shadow_type = LIGHT_DIRECTIONAL; + + for (auto& e : editor.scene.entities) { LightComponent* light = e.get_light_component(); TransformComponent* transform = e.get_transform_component(); @@ -640,6 +644,7 @@ int main(int argc, char* argv[]) { unload_shadowmap_render_texture(shadow_map); g_plugin_manager->unload_all(); rlImGuiShutdown(); + shutdown_freetype(); CloseWindow(); return 0; } diff --git a/src/models.cpp b/src/models.cpp index a29f23a..6fccd13 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -1,5 +1,6 @@ #include "headers/models.h" #include "headers/scene.h" +#include "headers/text_mesh.h" #include "editor/editor_utils.h" #include #include @@ -805,6 +806,17 @@ void load_models() { torus_asset.is_procedural = true; torus_asset.generator = [](int seg) { return LoadModelFromMesh(GenMeshTorus(1.0f, 1.0f, seg, seg)); }; assets.push_back(torus_asset); + + ModelAsset text_asset; + text_asset.name = "3D Text"; + text_asset.type = CUBE; + text_asset.is_procedural = true; + text_asset.generator = [](int) { + std::string fp = get_default_font_path(); + return generate_text_mesh("Text", 0.3f, 0.15f, 0.2f, fp); + }; + + assets.push_back(text_asset); } void update_model(Entity* e) @@ -816,6 +828,14 @@ void update_model(Entity* e) if (!mesh) return; if (mesh->model.meshCount > 0 && entity_owns_model(*e)) UnloadModel(mesh->model); + + if (auto* tc = e->get_components()->get_component_of_type().get()) { + std::string fp = tc->font_path.empty() ? get_default_font_path() : tc->font_path; + mesh->model = generate_text_mesh(tc->text, tc->size, tc->thickness, tc->letter_spacing, fp); + mesh->owns_model_instance = true; + return; + } + if (mesh->is_editable_mesh) { mesh->model = {}; @@ -959,4 +979,4 @@ void refresh_models(std::string project_path, Scene& scene) { } } } -} +} \ No newline at end of file diff --git a/src/text_mesh.cpp b/src/text_mesh.cpp new file mode 100644 index 0000000..5918974 --- /dev/null +++ b/src/text_mesh.cpp @@ -0,0 +1,415 @@ +#include "ft2build.h" +#include FT_FREETYPE_H +#include FT_OUTLINE_H +#include +#include +#include +#include +#include +#include + +#if _WIN32 + #include + +#elif __APPLE__ + #include + #include + +#endif + +#include "headers/text_mesh.h" + + +namespace fs = std::filesystem; + +FT_Library g_ft = nullptr; + +struct FTContour { std::vector pts; }; +struct FTOutlineCtx { + std::vector contours; + Vector2 current = {0, 0}; + float scale = 1.0f; +}; + +static std::string lowercase_copy(const std::string& str) { + std::string result = str; + + std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { + return std::tolower(c); + }); + + return result; +} + +void init_freetype() { + if (FT_Init_FreeType(&g_ft)) { + TraceLog(LOG_ERROR, "FreeType: failed to init"); + } +} + +void shutdown_freetype() { + if (g_ft) { + FT_Done_FreeType(g_ft); + g_ft = nullptr; + } +} + +std::vector> get_system_fonts() { + std::vector> result; + + auto scan_dir = [&](const fs::path& dir) { + if (!fs::exists(dir)) return; + std::error_code ec; + + for (auto& entry : fs::recursive_directory_iterator(dir, fs::directory_options::skip_permission_denied, ec)) { + if (!entry.is_regular_file(ec)) continue; + + auto ext = lowercase_copy(entry.path().extension().string()); + if (ext != ".ttf" && ext != ".otf") continue; + + std::string name = entry.path().stem().string(); + result.push_back({name, entry.path().string()}); + } + }; + +#if _WIN32 + const char* win = getenv("WINDIR"); + + if (win) { + scan_dir(std::string(win) + "\\Fonts"); + } + +#elif __APPLE__ + scan_dir("/System/Library/Fonts"); + scan_dir("/Library/Fonts"); + + const char* home = getenv("HOME"); + if (home) scan_dir(std::string(home) + "/Library/Fonts"); + +#else + scan_dir("/usr/share/fonts"); + scan_dir("/usr/local/share/fonts"); + + const char* home = getenv("HOME"); + if (home) scan_dir(std::string(home) + "/.fonts"); + if (home) scan_dir(std::string(home) + "/.local/share/fonts"); + + #endif + + std::sort(result.begin(), result.end(), [](auto& a, auto& b) { return a.first < b.first; }); + return result; +} + +static float cross2(Vector2 o, Vector2 a, Vector2 b) { + return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x); +} + +static void push_quad_bezier(std::vector& out, Vector2 p0, Vector2 p1, Vector2 p2, int steps = 8) { + for (int i = 1; i <= steps; i++) { + float t = (float)i / steps; + float it = 1.f - t; + + out.push_back({ + it*it*p0.x + 2*it*t*p1.x + t*t*p2.x, + it*it*p0.y + 2*it*t*p1.y + t*t*p2.y + }); + } +} + +static void push_cubic_bezier(std::vector& out, Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, int steps = 8) { + for (int i = 1; i <= steps; i++) { + float t = (float)i/steps, it = 1.f-t; + out.push_back({ + it*it*it*p0.x + 3*it*it*t*p1.x + 3*it*t*t*p2.x + t*t*t*p3.x, + it*it*it*p0.y + 3*it*it*t*p1.y + 3*it*t*t*p2.y + t*t*t*p3.y + }); + } +} + +static float polygon_signed_area(const std::vector& p) { + float a = 0; + int n = (int)p.size(); + + for (int i = 0; i < n; i++) { + int j = (i + 1) % n; + a += p[i].x * p[j].y - p[j].x * p[i].y; + } + + return a * .5f; +} + +static std::vector ear_clip(const std::vector& pts) { + std::vector result; + int n = (int)pts.size(); + if (n < 3) return result; + + std::vector idx(n); + std::iota(idx.begin(), idx.end(), 0); + + float a = 0; + for (int i = 0; i < n; i++) { + int j = (i+1)%n; + a += pts[i].x*pts[j].y - pts[j].x*pts[i].y; + } + + if (a < 0) std::reverse(idx.begin(), idx.end()); + + auto point_in_tri = [&](Vector2 p, Vector2 a, Vector2 b, Vector2 c) { + return cross2(a,b,p) >= 0 && cross2(b,c,p) >= 0 && cross2(c,a,p) >= 0; + }; + + int safety = n * n + 10; + int i = 0; + + while ((int)idx.size() > 3 && safety-- > 0) { + int sz = (int)idx.size(); + int prev = (i - 1 + sz) % sz; + int next = (i + 1) % sz; + + Vector2 a = pts[idx[prev]], b = pts[idx[i]], c = pts[idx[next]]; + bool ear = cross2(a, b, c) > 0; + if (ear) { + for (int k = 0; k < sz && ear; k++) { + if (k == prev || k == i || k == next) continue; + if (point_in_tri(pts[idx[k]], a, b, c)) ear = false; + } + } + + if (ear) { + result.push_back(idx[prev]); + result.push_back(idx[i]); + result.push_back(idx[next]); + idx.erase(idx.begin() + i); + sz--; + if (i >= sz) i = 0; + } + + else { + i = (i + 1) % sz; + } + } + + if ((int)idx.size() == 3) { + result.push_back(idx[0]); + result.push_back(idx[1]); + result.push_back(idx[2]); + } + + return result; +} + +static int ft_move_to(const FT_Vector* to, void* user) { + FTOutlineCtx* ctx = (FTOutlineCtx*)user; + + ctx->contours.push_back({}); + ctx->current = { (float)to->x * ctx->scale, (float)to->y * ctx->scale }; + ctx->contours.back().pts.push_back(ctx->current); + + return 0; +} + +static int ft_line_to(const FT_Vector* to, void* user) { + FTOutlineCtx* ctx = (FTOutlineCtx*)user; + + ctx->current = { (float)to->x * ctx->scale, (float)to->y * ctx->scale }; + + if (!ctx->contours.empty()) { + ctx->contours.back().pts.push_back(ctx->current); + } + + return 0; +} + +static int ft_conic_to(const FT_Vector* ctrl, const FT_Vector* to, void* user) { + FTOutlineCtx* ctx = (FTOutlineCtx*)user; + if (ctx->contours.empty()) return 0; + + Vector2 p1 = { (float)ctrl->x * ctx->scale, (float)ctrl->y * ctx->scale }; + Vector2 p2 = { (float)to->x * ctx->scale, (float)to->y * ctx->scale }; + + push_quad_bezier(ctx->contours.back().pts, ctx->current, p1, p2); + ctx->current = p2; + + return 0; +} + +static int ft_cubic_to(const FT_Vector* c1, const FT_Vector* c2, const FT_Vector* to, void* user) { + auto* ctx = (FTOutlineCtx*)user; + if (ctx->contours.empty()) return 0; + + Vector2 p1 = { (float)c1->x * ctx->scale, (float)c1->y * ctx->scale }; + Vector2 p2 = { (float)c2->x * ctx->scale, (float)c2->y * ctx->scale }; + Vector2 p3 = { (float)to->x * ctx->scale, (float)to->y * ctx->scale }; + + push_cubic_bezier(ctx->contours.back().pts, ctx->current, p1, p2, p3); + ctx->current = p3; + + return 0; +} + +static const FT_Outline_Funcs g_ft_outline_funcs = { + ft_move_to, ft_line_to, ft_conic_to, ft_cubic_to, 0, 0 +}; + +struct MeshBuilder { + std::vector verts; + std::vector norms; + std::vector uvs; + std::vector indices; + int base = 0; + + void add_vertex(float x, float y, float z, float nx, float ny, float nz, float u, float v) { + verts.insert(verts.end(), { x, y, z }); + norms.insert(norms.end(), { nx, ny, nz }); + uvs.insert(uvs.end(), { u, v }); + } + + void add_face(const std::vector& contour, float z, float normal_z, bool flip_winding) { + auto tris = ear_clip(contour); + int n = (int)contour.size(); + + for (int i = 0; i < n; i++) { + add_vertex(contour[i].x, contour[i].y, z, 0, 0, normal_z, contour[i].x, contour[i].y); + } + + for (int k = 0; k + 2 < (int)tris.size(); k += 3) { + int a = base + tris[k]; + int b = base + tris[k+1]; + int c = base + tris[k+2]; + + if (flip_winding) std::swap(b, c); + + indices.push_back((unsigned short)a); + indices.push_back((unsigned short)b); + indices.push_back((unsigned short)c); + } + + base += n; + } + + void add_wall(const std::vector& contour, float depth) { + int n = (int)contour.size(); + + for (int i = 0; i < n; i++) { + int j = (i + 1) % n; + Vector2 a = contour[i], b = contour[j]; + + float ex = b.y - a.y, ey = -(b.x - a.x); + float len = sqrtf(ex*ex + ey*ey); + + if (len > 0.00001f) { ex /= len; ey /= len; } + + int v0 = base; + add_vertex(a.x, a.y, 0, ex, ey, 0, 0, 0); + add_vertex(b.x, b.y, 0, ex, ey, 0, 1, 0); + add_vertex(b.x, b.y, depth, ex, ey, 0, 1, 1); + add_vertex(a.x, a.y, depth, ex, ey, 0, 0, 1); + base += 4; + + indices.push_back(v0); indices.push_back(v0+1); + indices.push_back(v0+2); indices.push_back(v0); + indices.push_back(v0+2); indices.push_back(v0+3); + } + } + + Mesh build() { + Mesh m = {0}; + if (verts.empty()) return m; + + m.vertexCount = (int)(verts.size() / 3); + m.triangleCount = (int)(indices.size() / 3); + + m.vertices = (float*)MemAlloc((unsigned int)verts.size() * sizeof(float)); + m.normals = (float*)MemAlloc((unsigned int)norms.size() * sizeof(float)); + m.texcoords = (float*)MemAlloc((unsigned int)uvs.size() * sizeof(float)); + m.indices = (unsigned short*)MemAlloc((unsigned int)indices.size() * sizeof(unsigned short)); + + memcpy(m.vertices, verts.data(), verts.size() * sizeof(float)); + memcpy(m.normals, norms.data(), norms.size() * sizeof(float)); + memcpy(m.texcoords, uvs.data(), uvs.size() * sizeof(float)); + memcpy(m.indices, indices.data(), indices.size() * sizeof(unsigned short)); + + UploadMesh(&m, false); + return m; + } +}; + +Model generate_text_mesh(const std::string& text, float size, float thickness, float letter_spacing, const std::string& font_path) +{ + auto make_fallback = []() { + return LoadModelFromMesh(GenMeshCube(0.001f, 0.001f, 0.001f)); + }; + + if (!g_ft) { TraceLog(LOG_WARNING, "FreeType not initialised"); return make_fallback(); } + if (text.empty() || font_path.empty()) return make_fallback(); + + FT_Face face; + if (FT_New_Face(g_ft, font_path.c_str(), 0, &face)) { + TraceLog(LOG_WARNING, "FreeType: cannot load font %s", font_path.c_str()); + return make_fallback(); + } + + const int FT_RES = 128; + FT_Set_Pixel_Sizes(face, 0, FT_RES); + float scale = size / (float)FT_RES; + + MeshBuilder builder; + float cursor_x = 0.f; + + for (unsigned char ch : text) { + if (FT_Load_Char(face, ch, FT_LOAD_NO_BITMAP | FT_LOAD_NO_HINTING)) continue; + + FT_GlyphSlot slot = face->glyph; + if (slot->format != FT_GLYPH_FORMAT_OUTLINE) { + cursor_x += (slot->advance.x >> 6) * scale + letter_spacing; + continue; + } + + FTOutlineCtx ctx; + ctx.scale = scale; + FT_Outline_Decompose(&slot->outline, &g_ft_outline_funcs, &ctx); + + for (auto& c : ctx.contours) { + for (auto& p : c.pts) + p.x += cursor_x; + } + + for (auto& c : ctx.contours) { + if (c.pts.size() < 3) continue; + float area = polygon_signed_area(c.pts); + bool is_hole = area < 0; + + builder.add_face(c.pts, thickness, 1.f, is_hole); + builder.add_face(c.pts, 0.f, -1.f, !is_hole); + builder.add_wall(c.pts, thickness); + } + + cursor_x += (slot->advance.x >> 6) * scale + letter_spacing; + } + + FT_Done_Face(face); + + Mesh mesh = builder.build(); + if (mesh.vertexCount == 0) return make_fallback(); + + float half_w = cursor_x * 0.5f; + for (int i = 0; i < mesh.vertexCount; i++) + mesh.vertices[i * 3] -= half_w; + + UpdateMeshBuffer(mesh, 0, mesh.vertices, mesh.vertexCount * 3 * sizeof(float), 0); + + Model model = LoadModelFromMesh(mesh); + + if (model.materialCount == 0) { + model.materials = (Material*)MemAlloc(sizeof(Material)); + model.materials[0] = LoadMaterialDefault(); + model.materialCount = 1; + } + return model; +} + +std::string get_default_font_path() { + auto fonts = get_system_fonts(); + if (!fonts.empty()) return fonts[0].second; + return ""; +} \ No newline at end of file From a396d5b39397e284d16b431fc64ff9a0a39da902 Mon Sep 17 00:00:00 2001 From: valmme Date: Thu, 21 May 2026 16:56:33 +0400 Subject: [PATCH 2/5] no cmakelists.txt in freetype fix --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87ed1dc..e19cfa0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + submodules: recursive - name: Install Linux dependencies if: runner.os == 'Linux' From bdcbaff8ac4c8288b46c22c86ab791a44b0c5e7c Mon Sep 17 00:00:00 2001 From: valmme Date: Thu, 21 May 2026 17:00:06 +0400 Subject: [PATCH 3/5] Remove broken freetype submodule --- quark-libs/freetype | 1 - 1 file changed, 1 deletion(-) delete mode 160000 quark-libs/freetype diff --git a/quark-libs/freetype b/quark-libs/freetype deleted file mode 160000 index 7e0e56f..0000000 --- a/quark-libs/freetype +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7e0e56f84fd53cf38378d33c8fc8f92d12ab9ac6 From af2af1305ee33a46d09c0e8dde11f5b034632bd7 Mon Sep 17 00:00:00 2001 From: valmme Date: Thu, 21 May 2026 17:02:21 +0400 Subject: [PATCH 4/5] add freetype submodule --- .gitmodules | 3 +++ quark-libs/freetype | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 quark-libs/freetype diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0b93a41 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "quark-libs/freetype"] + path = quark-libs/freetype + url = https://github.com/freetype/freetype.git diff --git a/quark-libs/freetype b/quark-libs/freetype new file mode 160000 index 0000000..7e0e56f --- /dev/null +++ b/quark-libs/freetype @@ -0,0 +1 @@ +Subproject commit 7e0e56f84fd53cf38378d33c8fc8f92d12ab9ac6 From fca65b71f470ba62f932b929ecf80bc43a06da61 Mon Sep 17 00:00:00 2001 From: valmme Date: Thu, 21 May 2026 17:06:10 +0400 Subject: [PATCH 5/5] linux & macos fix --- src/editor/editor_components_ui.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/editor/editor_components_ui.cpp b/src/editor/editor_components_ui.cpp index 3de409d..c9d21bb 100644 --- a/src/editor/editor_components_ui.cpp +++ b/src/editor/editor_components_ui.cpp @@ -10,6 +10,7 @@ #include "editor/editor.h" #include #include +#include #define lang LanguageManager::get() @@ -660,7 +661,8 @@ void ComponentUIHelper::draw_3d_text_component(Editor& editor, Entity& entity, T if (!text) return; char buf[256] = {}; - strncpy_s(buf, sizeof(buf), text->text.c_str(), _TRUNCATE); + std::strncpy(buf, text->text.c_str(), sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; if (ImGui::InputText("Text", buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue)) { editor.save_state();