mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 12:34:27 +00:00
3D text (fonts' overlapping strokes joined, holes by winding), import STL / OBJ / 3MF / glTF (own DEFLATE for 3MF), sturdier face triangles; tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ecd7c2cfda
commit
c2bbafd40c
9 changed files with 889 additions and 7 deletions
|
|
@ -1,8 +1,8 @@
|
|||
# Fry3DX: the app (app.cpp) on the engine, and its modelling core (mesh, scene) as a library the
|
||||
# tests use too.
|
||||
add_library(fry_core STATIC mesh.cpp bvh.cpp tools.cpp cut.cpp uv.cpp image.cpp sticker.cpp shapes.cpp scene.cpp package.cpp export.cpp print.cpp boolean.cpp modify.cpp sculpt.cpp)
|
||||
add_library(fry_core STATIC mesh.cpp bvh.cpp text.cpp import.cpp tools.cpp cut.cpp uv.cpp image.cpp sticker.cpp shapes.cpp scene.cpp package.cpp export.cpp print.cpp boolean.cpp modify.cpp sculpt.cpp)
|
||||
target_include_directories(fry_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(fry_core PUBLIC glm::glm libzstd_static)
|
||||
target_link_libraries(fry_core PUBLIC glm::glm libzstd_static PRIVATE stb cgltf)
|
||||
target_compile_definitions(fry_core PUBLIC GLM_ENABLE_EXPERIMENTAL)
|
||||
# Scripts (Lua) that build and change the scene: also without a window, for the tests.
|
||||
add_library(fry_script STATIC script.cpp)
|
||||
|
|
|
|||
125
src/app.cpp
125
src/app.cpp
|
|
@ -11,6 +11,8 @@
|
|||
#include "script.h"
|
||||
#include "sculpt.h"
|
||||
#include "shapes.h"
|
||||
#include "import.h"
|
||||
#include "text.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#if defined(__ANDROID__)
|
||||
|
|
@ -595,6 +597,10 @@ public:
|
|||
self->pie_ = a.size() > 1 && a[1] == "views" ? 2 : 1, self->pieStays_ = true;
|
||||
self->pieAt_ = ImVec2((self->viewMin_.x + self->viewMax_.x) / 2, (self->viewMin_.y + self->viewMax_.y) / 2);
|
||||
}, "Fry3DX: show a pie menu (tools | views)");
|
||||
static keel::Cmd import("fry_import", [](const keel::Args& a) { if (a.size() > 1) self->importFile(a[1]); }, "Fry3DX: import a model (STL, OBJ, 3MF, glTF)");
|
||||
static keel::Cmd text("fry_text", [](const keel::Args& a) {
|
||||
if (a.size() > 1) std::snprintf(self->textBuf_, sizeof self->textBuf_, "%s", a[1].c_str()), self->addText();
|
||||
}, "Fry3DX: add text as a shape");
|
||||
static keel::Cmd about("fry_about", [](const keel::Args&) { self->showAbout_ = true; }, "Fry3DX: show the About window");
|
||||
static keel::Cmd plugins("fry_plugins", [](const keel::Args& a) {
|
||||
if (a.size() > 1 && a[1] == "reload") self->loadPlugins();
|
||||
|
|
@ -722,6 +728,7 @@ public:
|
|||
commandForm();
|
||||
copiesWindow();
|
||||
thickenWindow();
|
||||
textWindow();
|
||||
pushWindow();
|
||||
saveQuestion();
|
||||
recoverWindow();
|
||||
|
|
@ -1546,6 +1553,14 @@ private:
|
|||
if (kind == 10) SDL_ShowOpenFileDialog(done, this, w, lua, 1, at, false);
|
||||
else SDL_ShowSaveFileDialog(done, this, w, lua, 1, scriptPath_.empty() || isUri(scriptPath_) ? at : scriptPath_.c_str()); // (at: the last folder)
|
||||
}
|
||||
else if (kind == 12) {
|
||||
static const SDL_DialogFileFilter in[] = {{"3D models (STL, OBJ, 3MF, glTF)", "stl;obj;3mf;glb;gltf"}};
|
||||
SDL_ShowOpenFileDialog(done, this, w, in, 1, at, true);
|
||||
}
|
||||
else if (kind == 13) {
|
||||
static const SDL_DialogFileFilter fonts[] = {{"Fonts", "ttf;otf"}};
|
||||
SDL_ShowOpenFileDialog(done, this, w, fonts, 1, at, false);
|
||||
}
|
||||
else SDL_ShowOpenFileDialog(done, this, w, pics, 1, at, false);
|
||||
}
|
||||
void takeDialogResult() {
|
||||
|
|
@ -1567,6 +1582,11 @@ private:
|
|||
return;
|
||||
}
|
||||
if (isUri(file) && (kind == 8 || kind == 9)) return exportFile(file, -1, kind == 8 ? ".3mf" : ".stl");
|
||||
if (kind == 12) return importFile(isUri(file) ? fromUri(file, "imported-" + fileName(file)) : file);
|
||||
if (kind == 13) {
|
||||
textFontFile_ = isUri(file) ? fromUri(file, "font-" + fileName(file)) : file;
|
||||
return;
|
||||
}
|
||||
if (kind == 10) return openScript(isUri(file) ? fromUri(file, "opened.lua") : file, fileName(file));
|
||||
if (kind == 11) return saveScript(file);
|
||||
if (kind == 1) openFile(file);
|
||||
|
|
@ -3692,6 +3712,104 @@ private:
|
|||
changedShape(*o);
|
||||
say(std::format("{} made {:.1f} mm thick{}", o->name, thickness_ * 1000, checkForPrint(o->mesh).closed() ? " (closed all round)" : ""));
|
||||
}
|
||||
// A model made elsewhere: each of its objects, standing on the ground in the middle of the view.
|
||||
void importFile(const std::string& file) {
|
||||
std::string err;
|
||||
std::vector<fry::ImportedMesh> got = fry::importModel(file, &err);
|
||||
if (got.empty()) return say(err.empty() ? "Nothing in " + fileName(file) : err);
|
||||
pushUndo();
|
||||
if (mode_ == Mode::Edit) leaveEdit();
|
||||
glm::vec3 lo(1e30f), hi(-1e30f);
|
||||
for (const auto& g : got) lo = glm::min(lo, boundsMin(g.mesh)), hi = glm::max(hi, boundsMax(g.mesh));
|
||||
const glm::vec3 shift(-(lo.x + hi.x) / 2, -(lo.y + hi.y) / 2, -lo.z), at(std::round(target_.x * 10) / 10, std::round(target_.y * 10) / 10, 0.0f);
|
||||
selected_.clear();
|
||||
size_t faces = 0;
|
||||
for (auto& g : got) {
|
||||
// (each object's own middle as its place, so it turns about itself)
|
||||
const glm::vec3 lo1 = boundsMin(g.mesh) + shift, hi1 = boundsMax(g.mesh) + shift, mid((lo1.x + hi1.x) / 2, (lo1.y + hi1.y) / 2, lo1.z);
|
||||
transform(g.mesh, glm::translate(glm::mat4(1.0f), shift - mid));
|
||||
faces += g.mesh.faces.size();
|
||||
Object& o = addObject(g.name.empty() ? "Imported" : g.name, std::move(g.mesh));
|
||||
o.position = at + mid;
|
||||
selected_.insert(o.id), active_ = o.id;
|
||||
}
|
||||
const glm::vec3 size = (hi - lo) * 1000.0f;
|
||||
say(std::format("{}: {} object{}, {} faces, {:.1f} x {:.1f} x {:.1f} mm", fileName(file), got.size(), got.size() == 1 ? "" : "s", faces, size.x, size.y, size.z));
|
||||
if (glm::length(hi - lo) > 0) frameSelected();
|
||||
}
|
||||
|
||||
// ── Text as a shape ──
|
||||
bool showText_ = false;
|
||||
char textBuf_[512] = "Fry3DX";
|
||||
int textFont_ = 2, textAlign_ = 0;
|
||||
float textHeight_ = 0.05f, textDepth_ = 0.01f, textSpacing_ = 0.0f;
|
||||
bool textStanding_ = false;
|
||||
std::string textFontFile_; // (a font of your own, picked from a file)
|
||||
static constexpr const char* kFonts[] = {"Instrument Sans", "Instrument Sans (semi-bold)", "Instrument Sans (bold)", "JetBrains Mono"};
|
||||
static constexpr const char* kFontFiles[] = {"fonts/InstrumentSans-Regular.ttf", "fonts/InstrumentSans-SemiBold.ttf", "fonts/InstrumentSans-Bold.ttf", "fonts/JetBrainsMono-Regular.ttf"};
|
||||
void textWindow() {
|
||||
if (!showText_) return;
|
||||
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
if (!ImGui::Begin("Add text", &showText_, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking)) return ImGui::End();
|
||||
const float w = 300;
|
||||
ImGui::InputTextMultiline("##text", textBuf_, sizeof textBuf_, ImVec2(w, ImGui::GetTextLineHeight() * 3.6f));
|
||||
ImGui::SetNextItemWidth(w);
|
||||
const std::string fontName = textFontFile_.empty() ? kFonts[textFont_] : stdfs::path(textFontFile_).filename().string();
|
||||
if (ImGui::BeginCombo("##font", fontName.c_str())) {
|
||||
for (int i = 0; i < int(std::size(kFonts)); ++i)
|
||||
if (ImGui::Selectable(kFonts[i], textFontFile_.empty() && textFont_ == i)) textFont_ = i, textFontFile_.clear();
|
||||
if (ImGui::Selectable("A font file of your own (.ttf, .otf)...")) dialog(13);
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
float mm[3] = {textHeight_ * 1000, textDepth_ * 1000, textSpacing_ * 100};
|
||||
ImGui::SetNextItemWidth(w);
|
||||
if (ImGui::DragFloat("##h", &mm[0], 0.2f, 0.5f, 1e5f, "letters %.1f mm tall")) textHeight_ = std::max(mm[0], 0.1f) / 1000;
|
||||
ImGui::SetNextItemWidth(w);
|
||||
if (ImGui::DragFloat("##d", &mm[1], 0.1f, 0.1f, 1e5f, "%.1f mm thick")) textDepth_ = std::max(mm[1], 0.05f) / 1000;
|
||||
ImGui::SetNextItemWidth(w);
|
||||
if (ImGui::DragFloat("##s", &mm[2], 0.2f, -30, 100, "letters %+.0f%% further apart")) textSpacing_ = mm[2] / 100;
|
||||
ImGui::SetNextItemWidth(w);
|
||||
ImGui::Combo("##al", &textAlign_, "Lines to the left\0Lines in the middle\0Lines to the right\0");
|
||||
ImGui::Checkbox("Standing up (a sign), not lying (a label to print)", &textStanding_);
|
||||
pushAccent();
|
||||
if (ImGui::Button("Add it", ImVec2(w, 0))) addText();
|
||||
popAccent();
|
||||
ImGui::End();
|
||||
}
|
||||
void addText() {
|
||||
std::vector<uint8_t> font;
|
||||
if (!textFontFile_.empty()) {
|
||||
std::ifstream in(textFontFile_, std::ios::binary);
|
||||
font.assign(std::istreambuf_iterator<char>(in), {});
|
||||
} else if (auto f = keel::fs::read(kFontFiles[textFont_]))
|
||||
font = std::move(*f);
|
||||
fry::TextStyle st;
|
||||
st.height = textHeight_, st.depth = textDepth_, st.spacing = textSpacing_, st.align = textAlign_;
|
||||
std::string err;
|
||||
Mesh m = fry::makeText(font, textBuf_, st, &err);
|
||||
if (m.faces.empty()) return say(err.empty() ? "No text to make" : err);
|
||||
// (the letters' middle on the ground at the origin; standing up: facing the front)
|
||||
const glm::vec3 lo = boundsMin(m), hi = boundsMax(m);
|
||||
transform(m, glm::translate(glm::mat4(1.0f), glm::vec3(-(lo.x + hi.x) / 2, -(lo.y + hi.y) / 2, 0.0f)));
|
||||
if (textStanding_) transform(m, glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, (hi.y - lo.y) / 2)) * glm::rotate(glm::mat4(1.0f), kPi / 2, glm::vec3(1, 0, 0)));
|
||||
pushUndo();
|
||||
if (mode_ == Mode::Edit) leaveEdit();
|
||||
std::string name = textBuf_;
|
||||
if (const size_t nl = name.find('\n'); nl != std::string::npos) name.resize(nl);
|
||||
if (name.size() > 24) name = name.substr(0, 24) + "...";
|
||||
Object& o = addObject("Text: " + name, std::move(m));
|
||||
o.position = glm::vec3(std::round(target_.x * 10) / 10, std::round(target_.y * 10) / 10, 0.0f);
|
||||
// (reading left to right as the view looks: turned to the nearest quarter)
|
||||
const glm::vec3 f = viewDir();
|
||||
if (std::abs(f.x) + std::abs(f.y) > 1e-4f) {
|
||||
const float turn = std::round(std::atan2(-f.x, f.y) / (kPi / 2)) * (kPi / 2);
|
||||
o.rotation = glm::angleAxis(turn, glm::vec3(0, 0, 1));
|
||||
}
|
||||
selected_ = {o.id}, active_ = o.id;
|
||||
showText_ = false;
|
||||
say(std::format("Text added, {:.0f} mm tall letters, {} faces", textHeight_ * 1000, o.mesh.faces.size()));
|
||||
}
|
||||
|
||||
void thickenWindow() {
|
||||
if (!showThicken_) return;
|
||||
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
|
|
@ -4904,7 +5022,7 @@ private:
|
|||
if (key(ImGuiKey_Z)) shift ? redo() : undo();
|
||||
if (key(ImGuiKey_Y)) redo();
|
||||
if (key(ImGuiKey_S)) shift || path_.empty() ? dialog(2) : void(saveFile(path_));
|
||||
if (key(ImGuiKey_O)) whenSaved(3);
|
||||
if (key(ImGuiKey_O)) shift ? dialog(12) : whenSaved(3);
|
||||
if (key(ImGuiKey_N)) whenSaved(2);
|
||||
if (key(ImGuiKey_E)) dialog(3);
|
||||
if (key(ImGuiKey_Q)) whenSaved(1);
|
||||
|
|
@ -5561,7 +5679,7 @@ private:
|
|||
auto model = [this] { return tab_ == Tab::Model; };
|
||||
add("file.new", "New", "File", "Ctrl+N", [this] { whenSaved(2); }, {}, "empty start");
|
||||
add("file.open", "Open...", "File", "Ctrl+O", [this] { whenSaved(3); }, {}, "load fry");
|
||||
add("file.import", "Import a model (STL, OBJ, 3MF)...", "File", "Ctrl+Shift+O", [this] { dialog(12); }, {}, "load mesh stl obj 3mf");
|
||||
add("file.import", "Import a model (STL, OBJ, 3MF, glTF)...", "File", "Ctrl+Shift+O", [this] { dialog(12); }, {}, "load mesh stl obj 3mf");
|
||||
add("file.save", "Save", "File", "Ctrl+S", [this] { path_.empty() ? dialog(2) : void(saveFile(path_)); });
|
||||
add("file.saveas", "Save as...", "File", "Ctrl+Shift+S", [this] { dialog(2); });
|
||||
add("file.export", "Export (GLB, glTF, OBJ, STL, PLY, 3MF, FBX)...", "File", "Ctrl+E", [this] { dialog(3); }, {}, "stl obj gltf glb ply fbx 3mf print");
|
||||
|
|
@ -5728,6 +5846,7 @@ private:
|
|||
if (ImGui::MenuItem("Save", "Ctrl+S")) path_.empty() ? dialog(2) : void(saveFile(path_));
|
||||
if (ImGui::MenuItem("Save as...", "Ctrl+Shift+S")) dialog(2);
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Import a model (STL, OBJ, 3MF, glTF)...", "Ctrl+Shift+O")) dialog(12);
|
||||
if (ImGui::MenuItem("Export (GLB, glTF, OBJ, STL, PLY)...", "Ctrl+E")) dialog(3);
|
||||
if (ImGui::BeginMenu("Autosave")) {
|
||||
static const int every[] = {1, 2, 5, 10, 0};
|
||||
|
|
@ -7522,7 +7641,7 @@ private:
|
|||
}
|
||||
|
||||
// ── (being made) ──
|
||||
bool showText_ = false, showSpin_ = false, showPrintSize_ = false, liveMirror_ = false;
|
||||
bool showSpin_ = false, showPrintSize_ = false, liveMirror_ = false;
|
||||
void repairSelected() {}
|
||||
void startSlide() {}
|
||||
|
||||
|
|
|
|||
454
src/import.cpp
Normal file
454
src/import.cpp
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
#include "import.h"
|
||||
|
||||
#define CGLTF_IMPLEMENTATION
|
||||
#include <cgltf.h>
|
||||
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
namespace fry {
|
||||
|
||||
// ───────────────────────────── DEFLATE (as zlib's puff: small, not fast) ─────────────────────────────
|
||||
|
||||
namespace {
|
||||
|
||||
struct Bits {
|
||||
const uint8_t* in;
|
||||
size_t size, at = 0;
|
||||
uint32_t buf = 0;
|
||||
int count = 0;
|
||||
bool bad = false;
|
||||
int need(int n) {
|
||||
while (count < n) {
|
||||
if (at >= size) return bad = true, 0;
|
||||
buf |= uint32_t(in[at++]) << count, count += 8;
|
||||
}
|
||||
const int v = int(buf & ((1u << n) - 1));
|
||||
buf >>= n, count -= n;
|
||||
return v;
|
||||
}
|
||||
};
|
||||
|
||||
struct Huffman {
|
||||
short count[16] = {}, symbol[288] = {};
|
||||
// From each symbol's code length; false if they don't make a code.
|
||||
bool make(const short* lengths, int n) {
|
||||
std::fill(std::begin(count), std::end(count), 0);
|
||||
for (int s = 0; s < n; ++s) ++count[lengths[s]];
|
||||
if (count[0] == n) return true;
|
||||
int left = 1;
|
||||
for (int len = 1; len < 16; ++len)
|
||||
if ((left = left * 2 - count[len]) < 0) return false;
|
||||
short offs[16] = {};
|
||||
for (int len = 1; len < 15; ++len) offs[len + 1] = short(offs[len] + count[len]);
|
||||
for (int s = 0; s < n; ++s)
|
||||
if (lengths[s]) symbol[offs[lengths[s]]++] = short(s);
|
||||
return true;
|
||||
}
|
||||
int decode(Bits& b) const {
|
||||
int code = 0, first = 0, index = 0;
|
||||
for (int len = 1; len < 16; ++len) {
|
||||
code |= b.need(1);
|
||||
if (b.bad) return -1;
|
||||
const int c = count[len];
|
||||
if (code - c < first) return symbol[index + (code - first)];
|
||||
index += c, first += c, first <<= 1, code <<= 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
const short kLenBase[] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258};
|
||||
const short kLenExtra[] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
|
||||
const short kDistBase[] = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577};
|
||||
const short kDistExtra[] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
|
||||
|
||||
bool codes(Bits& b, std::vector<uint8_t>& out, const Huffman& lit, const Huffman& dist) {
|
||||
for (;;) {
|
||||
int s = lit.decode(b);
|
||||
if (s < 0) return false;
|
||||
if (s < 256) out.push_back(uint8_t(s));
|
||||
else if (s == 256) return true;
|
||||
else {
|
||||
s -= 257;
|
||||
if (s >= 29) return false;
|
||||
const int len = kLenBase[s] + b.need(kLenExtra[s]);
|
||||
const int d = dist.decode(b);
|
||||
if (d < 0 || d >= 30) return false;
|
||||
const size_t back = size_t(kDistBase[d] + b.need(kDistExtra[d]));
|
||||
if (b.bad || back > out.size()) return false;
|
||||
for (int i = 0; i < len; ++i) out.push_back(out[out.size() - back]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<uint8_t> inflate(const uint8_t* data, size_t size, size_t expect) {
|
||||
Bits b{data, size};
|
||||
std::vector<uint8_t> out;
|
||||
out.reserve(expect);
|
||||
for (bool last = false; !last;) {
|
||||
last = b.need(1);
|
||||
const int type = b.need(2);
|
||||
if (b.bad) return {};
|
||||
if (type == 0) { // stored
|
||||
b.buf = 0, b.count = 0;
|
||||
if (b.at + 4 > size) return {};
|
||||
const size_t len = size_t(data[b.at] | data[b.at + 1] << 8);
|
||||
b.at += 4;
|
||||
if (b.at + len > size) return {};
|
||||
out.insert(out.end(), data + b.at, data + b.at + len), b.at += len;
|
||||
} else if (type == 1) { // the fixed codes
|
||||
static Huffman lit, dist;
|
||||
static bool made = false;
|
||||
if (!made) {
|
||||
short l[288];
|
||||
for (int i = 0; i < 288; ++i) l[i] = short(i < 144 ? 8 : i < 256 ? 9 : i < 280 ? 7 : 8);
|
||||
lit.make(l, 288);
|
||||
for (int i = 0; i < 30; ++i) l[i] = 5;
|
||||
dist.make(l, 30), made = true;
|
||||
}
|
||||
if (!codes(b, out, lit, dist)) return {};
|
||||
} else if (type == 2) { // codes of its own
|
||||
const int nlen = b.need(5) + 257, ndist = b.need(5) + 1, ncode = b.need(4) + 4;
|
||||
static const short order[19] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15};
|
||||
short lengths[320] = {};
|
||||
for (int i = 0; i < ncode; ++i) lengths[order[i]] = short(b.need(3));
|
||||
Huffman lc;
|
||||
if (b.bad || !lc.make(lengths, 19)) return {};
|
||||
for (int i = 0; i < nlen + ndist;) {
|
||||
int s = lc.decode(b);
|
||||
if (s < 0) return {};
|
||||
if (s < 16) lengths[i++] = short(s);
|
||||
else {
|
||||
short len = 0;
|
||||
int rep;
|
||||
if (s == 16) {
|
||||
if (!i) return {};
|
||||
len = lengths[i - 1], rep = 3 + b.need(2);
|
||||
} else rep = s == 17 ? 3 + b.need(3) : 11 + b.need(7);
|
||||
if (i + rep > nlen + ndist) return {};
|
||||
while (rep--) lengths[i++] = len;
|
||||
}
|
||||
}
|
||||
Huffman lit, dist;
|
||||
if (!lit.make(lengths, nlen) || !dist.make(lengths + nlen, ndist) || !codes(b, out, lit, dist)) return {};
|
||||
} else
|
||||
return {};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ───────────────────────────── the formats ─────────────────────────────
|
||||
|
||||
namespace {
|
||||
|
||||
std::string lower(std::string s) {
|
||||
for (char& c : s) c = char(std::tolower(static_cast<unsigned char>(c)));
|
||||
return s;
|
||||
}
|
||||
float toFloat(std::string_view s) { return std::strtof(std::string(s).c_str(), nullptr); }
|
||||
|
||||
void tidy(Mesh& m) {
|
||||
mergeByDistance(m, 1e-7f);
|
||||
removeUnused(m);
|
||||
std::erase_if(m.faces, [](const std::vector<int>& f) { return f.size() < 3 || std::set<int>(f.begin(), f.end()).size() < 3; });
|
||||
}
|
||||
|
||||
std::vector<ImportedMesh> readStl(const std::vector<uint8_t>& d, const std::string& name, std::string* error) {
|
||||
Mesh m;
|
||||
const bool ascii = d.size() >= 5 && std::memcmp(d.data(), "solid", 5) == 0 && std::string_view(reinterpret_cast<const char*>(d.data()), std::min<size_t>(d.size(), 600)).find("facet") != std::string::npos;
|
||||
if (!ascii) {
|
||||
if (d.size() < 84) return *error = "That STL is too short", std::vector<ImportedMesh>{};
|
||||
uint32_t n;
|
||||
std::memcpy(&n, d.data() + 80, 4);
|
||||
if (d.size() < 84 + size_t(n) * 50) return *error = "That STL is cut short", std::vector<ImportedMesh>{};
|
||||
for (uint32_t i = 0; i < n; ++i) {
|
||||
const uint8_t* t = d.data() + 84 + size_t(i) * 50 + 12;
|
||||
std::vector<int> f;
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
float v[3];
|
||||
std::memcpy(v, t + k * 12, 12);
|
||||
f.push_back(int(m.verts.size())), m.verts.push_back(glm::vec3(v[0], v[1], v[2]) * 0.001f);
|
||||
}
|
||||
m.faces.push_back(f);
|
||||
}
|
||||
} else {
|
||||
std::istringstream in(std::string(d.begin(), d.end()));
|
||||
std::string word;
|
||||
std::vector<int> f;
|
||||
while (in >> word) {
|
||||
if (word == "vertex") {
|
||||
glm::vec3 v;
|
||||
in >> v.x >> v.y >> v.z;
|
||||
f.push_back(int(m.verts.size())), m.verts.push_back(v * 0.001f);
|
||||
} else if (word == "endfacet") {
|
||||
if (f.size() >= 3) m.faces.push_back(f);
|
||||
f.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
tidy(m);
|
||||
return {{name, std::move(m)}};
|
||||
}
|
||||
|
||||
std::vector<ImportedMesh> readObj(const std::vector<uint8_t>& d, const std::string& name) {
|
||||
std::vector<glm::vec3> all;
|
||||
std::vector<ImportedMesh> out{{name, {}}};
|
||||
std::map<int, int> local; // (the file's point number: the one in this object's mesh)
|
||||
std::istringstream in(std::string(d.begin(), d.end()));
|
||||
for (std::string line; std::getline(in, line);) {
|
||||
if (line.size() < 2) continue;
|
||||
std::istringstream l(line);
|
||||
std::string what;
|
||||
l >> what;
|
||||
if (what == "v") {
|
||||
glm::vec3 v;
|
||||
l >> v.x >> v.y >> v.z;
|
||||
all.push_back(glm::vec3(v.x, -v.z, v.y)); // (OBJ's up is Y; here it's Z)
|
||||
} else if ((what == "o" || what == "g") && !out.back().mesh.faces.empty()) {
|
||||
std::string n;
|
||||
std::getline(l >> std::ws, n);
|
||||
out.push_back({n.empty() ? name : n, {}}), local.clear();
|
||||
} else if (what == "o" || what == "g") {
|
||||
std::string n;
|
||||
std::getline(l >> std::ws, n);
|
||||
if (!n.empty()) out.back().name = n;
|
||||
} else if (what == "f") {
|
||||
std::vector<int> f;
|
||||
for (std::string c; l >> c;) {
|
||||
int i = std::atoi(c.c_str());
|
||||
i = i < 0 ? int(all.size()) + i : i - 1;
|
||||
if (i < 0 || i >= int(all.size())) continue;
|
||||
auto [it, fresh] = local.try_emplace(i, int(out.back().mesh.verts.size()));
|
||||
if (fresh) out.back().mesh.verts.push_back(all[size_t(i)]);
|
||||
f.push_back(it->second);
|
||||
}
|
||||
if (f.size() >= 3) out.back().mesh.faces.push_back(f);
|
||||
}
|
||||
}
|
||||
std::erase_if(out, [](const ImportedMesh& m) { return m.mesh.faces.empty(); });
|
||||
// (no units in an OBJ: one this big is millimetres, as most made for printing are)
|
||||
float big = 0;
|
||||
for (const ImportedMesh& m : out)
|
||||
if (!m.mesh.verts.empty()) big = std::max(big, glm::length(boundsMax(m.mesh) - boundsMin(m.mesh)));
|
||||
for (ImportedMesh& m : out) {
|
||||
if (big > 20) transform(m.mesh, glm::scale(glm::mat4(1.0f), glm::vec3(0.001f)));
|
||||
tidy(m.mesh);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// A zip's file (stored or deflated), or empty.
|
||||
std::vector<uint8_t> zipFile(const std::vector<uint8_t>& z, const std::function<bool(const std::string&)>& wanted, std::string* found = nullptr) {
|
||||
auto u16 = [&](size_t at) { return at + 2 <= z.size() ? uint32_t(z[at] | z[at + 1] << 8) : 0u; };
|
||||
auto u32 = [&](size_t at) { return at + 4 <= z.size() ? u16(at) | u16(at + 2) << 16 : 0u; };
|
||||
// The central directory, from its end record (sizes are right there even when the local headers leave them out).
|
||||
size_t end = z.size() >= 22 ? z.size() - 22 : 0;
|
||||
while (end > 0 && u32(end) != 0x06054b50) --end;
|
||||
if (u32(end) != 0x06054b50) return {};
|
||||
size_t at = u32(end + 16);
|
||||
for (uint32_t n = u16(end + 10); n-- && u32(at) == 0x02014b50;) {
|
||||
const uint32_t method = u16(at + 10), packed = u32(at + 20), size = u32(at + 24), nameLen = u16(at + 28), extra = u16(at + 30), comment = u16(at + 32), local = u32(at + 42);
|
||||
const std::string name(z.begin() + long(at + 46), z.begin() + long(std::min(z.size(), at + 46 + nameLen)));
|
||||
at += 46 + nameLen + extra + comment;
|
||||
if (!wanted(name)) continue;
|
||||
const size_t data = local + 30 + u16(local + 26) + u16(local + 28);
|
||||
if (data + packed > z.size()) return {};
|
||||
if (found) *found = name;
|
||||
if (method == 0) return std::vector<uint8_t>(z.begin() + long(data), z.begin() + long(data + packed));
|
||||
if (method == 8) return inflate(z.data() + data, packed, size);
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// The value of attribute `a` in a tag (as text).
|
||||
std::string_view attr(std::string_view tag, std::string_view a) {
|
||||
for (size_t at = 0; (at = tag.find(a, at)) != std::string_view::npos; at += a.size()) {
|
||||
if (at > 0 && !std::isspace(static_cast<unsigned char>(tag[at - 1]))) continue;
|
||||
size_t q = at + a.size();
|
||||
while (q < tag.size() && std::isspace(static_cast<unsigned char>(tag[q]))) ++q;
|
||||
if (q >= tag.size() || tag[q] != '=') continue;
|
||||
++q;
|
||||
while (q < tag.size() && std::isspace(static_cast<unsigned char>(tag[q]))) ++q;
|
||||
if (q >= tag.size()) break;
|
||||
const char quote = tag[q];
|
||||
const size_t close = tag.find(quote, q + 1);
|
||||
if (close == std::string_view::npos) break;
|
||||
return tag.substr(q + 1, close - q - 1);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
// XML's & < > " ' and &#...; made the characters again.
|
||||
std::string unescape(std::string_view s) {
|
||||
std::string out;
|
||||
for (size_t i = 0; i < s.size(); ++i) {
|
||||
const size_t semi = s[i] == '&' ? s.find(';', i) : std::string_view::npos;
|
||||
if (semi == std::string_view::npos || semi - i > 10) {
|
||||
out += s[i];
|
||||
continue;
|
||||
}
|
||||
const std::string_view e = s.substr(i + 1, semi - i - 1);
|
||||
uint32_t c = e == "amp" ? '&' : e == "lt" ? '<' : e == "gt" ? '>' : e == "quot" ? '"' : e == "apos" ? '\'' : 0;
|
||||
if (!c && e.starts_with("#x")) c = uint32_t(std::strtoul(std::string(e.substr(2)).c_str(), nullptr, 16));
|
||||
else if (!c && e.starts_with("#")) c = uint32_t(std::strtoul(std::string(e.substr(1)).c_str(), nullptr, 10));
|
||||
if (!c) {
|
||||
out += s[i];
|
||||
continue;
|
||||
}
|
||||
if (c < 0x80) out += char(c);
|
||||
else if (c < 0x800) out += char(0xC0 | c >> 6), out += char(0x80 | (c & 0x3F));
|
||||
else if (c < 0x10000) out += char(0xE0 | c >> 12), out += char(0x80 | (c >> 6 & 0x3F)), out += char(0x80 | (c & 0x3F));
|
||||
else out += char(0xF0 | c >> 18), out += char(0x80 | (c >> 12 & 0x3F)), out += char(0x80 | (c >> 6 & 0x3F)), out += char(0x80 | (c & 0x3F));
|
||||
i = semi;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
glm::mat4 transform3mf(std::string_view t) {
|
||||
if (t.empty()) return glm::mat4(1.0f);
|
||||
float v[12] = {1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0};
|
||||
std::istringstream in{std::string(t)};
|
||||
for (float& f : v) in >> f;
|
||||
glm::mat4 m(1.0f);
|
||||
for (int r = 0; r < 4; ++r)
|
||||
for (int c = 0; c < 3; ++c) m[r][c] = v[r * 3 + c];
|
||||
return m;
|
||||
}
|
||||
|
||||
std::vector<ImportedMesh> read3mf(const std::vector<uint8_t>& d, const std::string& name, std::string* error) {
|
||||
const std::vector<uint8_t> xml = zipFile(d, [](const std::string& n) { return lower(n).ends_with(".model"); });
|
||||
if (xml.empty()) return *error = "No model in that 3MF (or it's packed a way that can't be read)", std::vector<ImportedMesh>{};
|
||||
const std::string_view s(reinterpret_cast<const char*>(xml.data()), xml.size());
|
||||
float unit = 0.001f;
|
||||
if (const size_t m = s.find("<model"); m != std::string_view::npos) {
|
||||
const std::string u(attr(s.substr(m, s.find('>', m) - m), "unit"));
|
||||
unit = u == "micron" ? 1e-6f : u == "centimeter" ? 0.01f : u == "meter" ? 1.0f : u == "inch" ? 0.0254f : u == "foot" ? 0.3048f : 0.001f;
|
||||
}
|
||||
struct Obj {
|
||||
std::string name;
|
||||
Mesh mesh;
|
||||
std::vector<std::pair<int, glm::mat4>> parts;
|
||||
};
|
||||
std::map<int, Obj> objs;
|
||||
std::vector<std::pair<int, glm::mat4>> items;
|
||||
Obj* cur = nullptr;
|
||||
for (size_t at = 0; (at = s.find('<', at)) != std::string_view::npos;) {
|
||||
const size_t close = s.find('>', at);
|
||||
if (close == std::string_view::npos) break;
|
||||
const std::string_view tag = s.substr(at + 1, close - at - 1);
|
||||
at = close + 1;
|
||||
auto is = [&](std::string_view t) { return tag.starts_with(t) && tag.size() > t.size() && (std::isspace(static_cast<unsigned char>(tag[t.size()])) || tag[t.size()] == '/'); };
|
||||
if (is("object")) {
|
||||
cur = &objs[std::atoi(std::string(attr(tag, "id")).c_str())];
|
||||
cur->name = unescape(attr(tag, "name"));
|
||||
} else if (is("vertex") && cur)
|
||||
cur->mesh.verts.push_back(glm::vec3(toFloat(attr(tag, "x")), toFloat(attr(tag, "y")), toFloat(attr(tag, "z"))));
|
||||
else if (is("triangle") && cur)
|
||||
cur->mesh.faces.push_back({std::atoi(std::string(attr(tag, "v1")).c_str()), std::atoi(std::string(attr(tag, "v2")).c_str()), std::atoi(std::string(attr(tag, "v3")).c_str())});
|
||||
else if (is("component") && cur)
|
||||
cur->parts.push_back({std::atoi(std::string(attr(tag, "objectid")).c_str()), transform3mf(attr(tag, "transform"))});
|
||||
else if (is("item"))
|
||||
items.push_back({std::atoi(std::string(attr(tag, "objectid")).c_str()), transform3mf(attr(tag, "transform"))});
|
||||
}
|
||||
std::function<void(int, const glm::mat4&, Mesh&, int)> place = [&](int id, const glm::mat4& m, Mesh& into, int depth) {
|
||||
auto it = objs.find(id);
|
||||
if (it == objs.end() || depth > 8) return;
|
||||
for (const auto& f : it->second.mesh.faces)
|
||||
for (int v : f)
|
||||
if (v < 0 || v >= int(it->second.mesh.verts.size())) return;
|
||||
append(into, it->second.mesh, m);
|
||||
for (const auto& [part, pm] : it->second.parts) place(part, m * pm, into, depth + 1);
|
||||
};
|
||||
std::vector<ImportedMesh> out;
|
||||
if (items.empty())
|
||||
for (const auto& [id, o] : objs) items.push_back({id, glm::mat4(1.0f)});
|
||||
for (const auto& [id, m] : items) {
|
||||
ImportedMesh im{objs.count(id) && !objs[id].name.empty() ? objs[id].name : name, {}};
|
||||
place(id, glm::scale(glm::mat4(1.0f), glm::vec3(unit)) * m, im.mesh, 0);
|
||||
tidy(im.mesh);
|
||||
if (!im.mesh.faces.empty()) out.push_back(std::move(im));
|
||||
}
|
||||
if (out.empty()) *error = "No shapes in that 3MF";
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<ImportedMesh> readGltf(const std::string& path, const std::string& name, std::string* error) {
|
||||
cgltf_options opt{};
|
||||
cgltf_data* data = nullptr;
|
||||
if (cgltf_parse_file(&opt, path.c_str(), &data) != cgltf_result_success || cgltf_load_buffers(&opt, data, path.c_str()) != cgltf_result_success) {
|
||||
if (data) cgltf_free(data);
|
||||
*error = "That glTF can't be read";
|
||||
return {};
|
||||
}
|
||||
std::vector<ImportedMesh> out;
|
||||
for (size_t n = 0; n < data->nodes_count; ++n) {
|
||||
const cgltf_node& node = data->nodes[n];
|
||||
if (!node.mesh) continue;
|
||||
float w[16];
|
||||
cgltf_node_transform_world(&node, w);
|
||||
const glm::mat4 m = glm::mat4(1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1) * glm::make_mat4(w); // (glTF's up is Y; here it's Z)
|
||||
ImportedMesh im{node.name ? node.name : node.mesh->name ? node.mesh->name : name, {}};
|
||||
for (size_t p = 0; p < node.mesh->primitives_count; ++p) {
|
||||
const cgltf_primitive& prim = node.mesh->primitives[p];
|
||||
if (prim.type != cgltf_primitive_type_triangles) continue;
|
||||
const cgltf_accessor* pos = nullptr;
|
||||
for (size_t a = 0; a < prim.attributes_count; ++a)
|
||||
if (prim.attributes[a].type == cgltf_attribute_type_position) pos = prim.attributes[a].data;
|
||||
if (!pos) continue;
|
||||
const int base = int(im.mesh.verts.size());
|
||||
for (size_t i = 0; i < pos->count; ++i) {
|
||||
float v[3] = {};
|
||||
cgltf_accessor_read_float(pos, i, v, 3);
|
||||
im.mesh.verts.push_back(glm::vec3(m * glm::vec4(v[0], v[1], v[2], 1)));
|
||||
}
|
||||
const size_t count = prim.indices ? prim.indices->count : pos->count;
|
||||
for (size_t i = 0; i + 2 < count; i += 3) {
|
||||
std::vector<int> f(3);
|
||||
for (int k = 0; k < 3; ++k) f[size_t(k)] = base + int(prim.indices ? cgltf_accessor_read_index(prim.indices, i + size_t(k)) : i + size_t(k));
|
||||
im.mesh.faces.push_back(f);
|
||||
}
|
||||
}
|
||||
tidy(im.mesh);
|
||||
if (!im.mesh.faces.empty()) out.push_back(std::move(im));
|
||||
}
|
||||
cgltf_free(data);
|
||||
if (out.empty()) *error = "No shapes in that glTF";
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<std::string>& importFormats() {
|
||||
static const std::vector<std::string> f = {".stl", ".obj", ".3mf", ".glb", ".gltf"};
|
||||
return f;
|
||||
}
|
||||
|
||||
std::vector<ImportedMesh> importModel(const std::string& path, std::string* error) {
|
||||
std::string dummy;
|
||||
if (!error) error = &dummy;
|
||||
const std::filesystem::path p(path);
|
||||
const std::string ext = lower(p.extension().string()), name = p.stem().string();
|
||||
if (ext == ".glb" || ext == ".gltf") return readGltf(path, name, error);
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
if (!in) return *error = "Couldn't read " + p.filename().string(), std::vector<ImportedMesh>{};
|
||||
const std::vector<uint8_t> d{std::istreambuf_iterator<char>(in), {}};
|
||||
if (ext == ".stl") return readStl(d, name, error);
|
||||
if (ext == ".obj") return readObj(d, name);
|
||||
if (ext == ".3mf") return read3mf(d, name, error);
|
||||
*error = "Fry3DX can't read " + ext + " files (STL, OBJ, 3MF, glTF and GLB it can)";
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
24
src/import.h
Normal file
24
src/import.h
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
// Models made elsewhere, read in: STL, OBJ, 3MF, glTF / GLB. Each object in the file comes back
|
||||
// as its own mesh (in metres; points that share a place, one).
|
||||
#include "mesh.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fry {
|
||||
|
||||
struct ImportedMesh {
|
||||
std::string name;
|
||||
Mesh mesh;
|
||||
};
|
||||
|
||||
// The file's objects, or empty with `error` set. STL and 3MF are in millimetres: made metres.
|
||||
std::vector<ImportedMesh> importModel(const std::string& path, std::string* error = nullptr);
|
||||
// The file types it reads, for the file picker (".stl", ...).
|
||||
const std::vector<std::string>& importFormats();
|
||||
|
||||
// A raw DEFLATE stream unpacked (what a zip, so a 3MF, holds); empty if it's broken.
|
||||
std::vector<uint8_t> inflate(const uint8_t* data, size_t size, size_t expect = 0);
|
||||
|
||||
} // namespace fry
|
||||
17
src/mesh.cpp
17
src/mesh.cpp
|
|
@ -182,6 +182,10 @@ std::vector<std::array<int, 3>> triangulate(const Mesh& m, int face) {
|
|||
const glm::vec3 v = glm::cross(n, u);
|
||||
std::vector<glm::vec2> p;
|
||||
for (int i : f) p.emplace_back(glm::dot(m.verts[size_t(i)], u), glm::dot(m.verts[size_t(i)], v));
|
||||
// (the tests go by the face's own size: a letter 5 mm tall the same as a wall 5 m wide)
|
||||
glm::vec2 lo(1e30f), hi(-1e30f);
|
||||
for (const glm::vec2& q : p) lo = glm::min(lo, q), hi = glm::max(hi, q);
|
||||
const float span = std::max(hi.x - lo.x, hi.y - lo.y), eps = std::max(span * span * 1e-9f, 1e-30f);
|
||||
auto cross2 = [](glm::vec2 a, glm::vec2 b, glm::vec2 c) { return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); };
|
||||
std::vector<int> left(f.size());
|
||||
std::iota(left.begin(), left.end(), 0);
|
||||
|
|
@ -189,12 +193,16 @@ std::vector<std::array<int, 3>> triangulate(const Mesh& m, int face) {
|
|||
bool cut = false;
|
||||
for (size_t i = 0; i < left.size() && !cut; ++i) {
|
||||
const int a = left[(i + left.size() - 1) % left.size()], b = left[i], c = left[(i + 1) % left.size()];
|
||||
if (cross2(p[size_t(a)], p[size_t(b)], p[size_t(c)]) <= 1e-12f) continue; // dents in, or flat
|
||||
const glm::vec2 pa = p[size_t(a)], pb = p[size_t(b)], pc = p[size_t(c)];
|
||||
if (cross2(pa, pb, pc) <= eps) continue; // dents in, or flat
|
||||
bool inside = false;
|
||||
for (int o : left) {
|
||||
if (o == a || o == b || o == c) continue;
|
||||
const glm::vec2 q = p[size_t(o)];
|
||||
if (cross2(p[size_t(a)], p[size_t(b)], q) >= 0 && cross2(p[size_t(b)], p[size_t(c)], q) >= 0 && cross2(p[size_t(c)], p[size_t(a)], q) >= 0) {
|
||||
if (q == pa || q == pb || q == pc) continue; // (the same place: where a hole's cut joins)
|
||||
// (strictly inside, or on the new edge a-c: a point on the outline's own edges a-b, b-c is no harm)
|
||||
const float ab = cross2(pa, pb, q), bc = cross2(pb, pc, q), ca = cross2(pc, pa, q);
|
||||
if (ab > eps && bc > eps && ca >= -eps) {
|
||||
inside = true;
|
||||
break;
|
||||
}
|
||||
|
|
@ -204,6 +212,11 @@ std::vector<std::array<int, 3>> triangulate(const Mesh& m, int face) {
|
|||
left.erase(left.begin() + std::ptrdiff_t(i));
|
||||
cut = true;
|
||||
}
|
||||
if (!cut) // (no ear: a point in a straight line between its neighbours goes, it covers nothing)
|
||||
for (size_t i = 0; i < left.size() && !cut; ++i) {
|
||||
const int a = left[(i + left.size() - 1) % left.size()], b = left[i], c = left[(i + 1) % left.size()];
|
||||
if (std::abs(cross2(p[size_t(a)], p[size_t(b)], p[size_t(c)])) <= eps) left.erase(left.begin() + std::ptrdiff_t(i)), cut = true;
|
||||
}
|
||||
if (!cut) { // (a tangled face: a fan of what's left)
|
||||
for (size_t i = 1; i + 1 < left.size(); ++i) out.push_back({f[size_t(left[0])], f[size_t(left[i])], f[size_t(left[i + 1])]});
|
||||
return out;
|
||||
|
|
|
|||
202
src/text.cpp
Normal file
202
src/text.cpp
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
#include "text.h"
|
||||
|
||||
#define STBTT_STATIC
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <stb_truetype.h>
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace fry {
|
||||
namespace {
|
||||
|
||||
// The next character of UTF-8 text (a bad byte: itself).
|
||||
uint32_t nextChar(const std::string& s, size_t& i) {
|
||||
const auto b = [&](size_t k) { return k < s.size() ? uint32_t(uint8_t(s[k])) : 0u; };
|
||||
const uint32_t c = b(i);
|
||||
if (c < 0x80) return ++i, c;
|
||||
if ((c & 0xE0) == 0xC0) return i += 2, ((c & 0x1F) << 6) | (b(i - 1) & 0x3F);
|
||||
if ((c & 0xF0) == 0xE0) return i += 3, ((c & 0x0F) << 12) | ((b(i - 2) & 0x3F) << 6) | (b(i - 1) & 0x3F);
|
||||
if ((c & 0xF8) == 0xF0) return i += 4, ((c & 0x07) << 18) | ((b(i - 3) & 0x3F) << 12) | ((b(i - 2) & 0x3F) << 6) | (b(i - 1) & 0x3F);
|
||||
return ++i, c;
|
||||
}
|
||||
|
||||
using Contour = std::vector<glm::vec2>;
|
||||
|
||||
// A glyph's outlines (font units), its curves cut into `detail` straight pieces each.
|
||||
std::vector<Contour> glyphContours(const stbtt_fontinfo& f, int glyph, int detail) {
|
||||
stbtt_vertex* v = nullptr;
|
||||
const int n = stbtt_GetGlyphShape(&f, glyph, &v);
|
||||
std::vector<Contour> out;
|
||||
glm::vec2 at(0.0f);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const glm::vec2 to(v[i].x, v[i].y);
|
||||
switch (v[i].type) {
|
||||
case STBTT_vmove:
|
||||
out.emplace_back();
|
||||
out.back().push_back(to);
|
||||
break;
|
||||
case STBTT_vline:
|
||||
if (!out.empty()) out.back().push_back(to);
|
||||
break;
|
||||
case STBTT_vcurve: {
|
||||
const glm::vec2 c(v[i].cx, v[i].cy);
|
||||
for (int k = 1; k <= detail && !out.empty(); ++k) {
|
||||
const float t = float(k) / float(detail);
|
||||
out.back().push_back((1 - t) * (1 - t) * at + 2 * (1 - t) * t * c + t * t * to);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case STBTT_vcubic: {
|
||||
const glm::vec2 c1(v[i].cx, v[i].cy), c2(v[i].cx1, v[i].cy1);
|
||||
for (int k = 1; k <= detail + 2 && !out.empty(); ++k) {
|
||||
const float t = float(k) / float(detail + 2), u = 1 - t;
|
||||
out.back().push_back(u * u * u * at + 3 * u * u * t * c1 + 3 * u * t * t * c2 + t * t * t * to);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
at = to;
|
||||
}
|
||||
stbtt_FreeShape(&f, v);
|
||||
for (Contour& c : out) {
|
||||
while (c.size() > 1 && glm::distance(c.front(), c.back()) < 1e-3f) c.pop_back();
|
||||
Contour clean; // (no point twice in a row, none in a straight line between its neighbours)
|
||||
for (const glm::vec2& p : c)
|
||||
if (clean.empty() || glm::distance(clean.back(), p) > 1e-3f) clean.push_back(p);
|
||||
c = clean;
|
||||
}
|
||||
std::erase_if(out, [](const Contour& c) { return c.size() < 3; });
|
||||
return out;
|
||||
}
|
||||
|
||||
// Its area: positive when it turns counter-clockwise.
|
||||
float area(const Contour& c) {
|
||||
float a = 0;
|
||||
for (size_t i = 0; i < c.size(); ++i) {
|
||||
const glm::vec2 p = c[i], q = c[(i + 1) % c.size()];
|
||||
a += p.x * q.y - q.x * p.y;
|
||||
}
|
||||
return a * 0.5f;
|
||||
}
|
||||
|
||||
// A glyph made solid. Fonts tell holes by the way an outline turns: the other way from the
|
||||
// letter's body. Pieces that turn the body's way are joined (a letter can be drawn as overlapping
|
||||
// strokes), then the holes cut out of what they're in.
|
||||
Mesh solidGlyph(const std::vector<Contour>& cs, float scale, glm::vec2 at, float depth) {
|
||||
if (cs.empty()) return {};
|
||||
size_t biggest = 0;
|
||||
for (size_t i = 1; i < cs.size(); ++i)
|
||||
if (std::abs(area(cs[i])) > std::abs(area(cs[biggest]))) biggest = i;
|
||||
const bool bodyTurn = area(cs[biggest]) > 0;
|
||||
auto prism = [&](const Contour& c, float thick) {
|
||||
std::vector<glm::vec3> pts;
|
||||
for (const glm::vec2& p : c) pts.push_back(glm::vec3(at + p * scale, 0.0f));
|
||||
Mesh m = extrudeOutline(pts, glm::vec3(0, 0, 1), thick);
|
||||
transform(m, glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, depth * 0.5f)));
|
||||
return m;
|
||||
};
|
||||
struct Box {
|
||||
glm::vec2 lo{1e30f}, hi{-1e30f};
|
||||
bool meets(const Box& o) const { return lo.x <= o.hi.x && o.lo.x <= hi.x && lo.y <= o.hi.y && o.lo.y <= hi.y; }
|
||||
};
|
||||
std::vector<Box> box(cs.size());
|
||||
for (size_t i = 0; i < cs.size(); ++i)
|
||||
for (const glm::vec2& p : cs[i]) box[i].lo = glm::min(box[i].lo, p), box[i].hi = glm::max(box[i].hi, p);
|
||||
// The body's pieces in groups that touch (each group one solid).
|
||||
std::vector<size_t> body, holes;
|
||||
for (size_t i = 0; i < cs.size(); ++i) (area(cs[i]) > 0) == bodyTurn ? body.push_back(i) : holes.push_back(i);
|
||||
std::vector<int> group(cs.size(), -1);
|
||||
int groups = 0;
|
||||
for (size_t i : body) {
|
||||
if (group[i] >= 0) continue;
|
||||
std::vector<size_t> todo{i};
|
||||
group[i] = groups;
|
||||
while (!todo.empty()) {
|
||||
const size_t k = todo.back();
|
||||
todo.pop_back();
|
||||
for (size_t j : body)
|
||||
if (group[j] < 0 && box[k].meets(box[j])) group[j] = groups, todo.push_back(j);
|
||||
}
|
||||
++groups;
|
||||
}
|
||||
Mesh out;
|
||||
for (int g = 0; g < groups; ++g) {
|
||||
Mesh solid;
|
||||
Box gb;
|
||||
for (size_t i : body) {
|
||||
if (group[i] != g) continue;
|
||||
gb.lo = glm::min(gb.lo, box[i].lo), gb.hi = glm::max(gb.hi, box[i].hi);
|
||||
if (solid.faces.empty()) solid = prism(cs[i], depth);
|
||||
else if (Mesh joined = solid; combine(joined, prism(cs[i], depth), Combine::Add)) solid = std::move(joined);
|
||||
else append(solid, prism(cs[i], depth)); // (couldn't join: overlapping, but there)
|
||||
}
|
||||
for (size_t h : holes)
|
||||
if (gb.meets(box[h]))
|
||||
if (Mesh cut = solid; combine(cut, prism(cs[h], depth * 1.5f), Combine::Subtract)) solid = std::move(cut);
|
||||
append(out, solid);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Mesh makeText(const std::vector<uint8_t>& font, const std::string& text, const TextStyle& style, std::string* error) {
|
||||
stbtt_fontinfo f;
|
||||
if (font.empty() || !stbtt_InitFont(&f, font.data(), stbtt_GetFontOffsetForIndex(font.data(), 0))) {
|
||||
if (error) *error = "That isn't a font that can be read (TrueType or OpenType)";
|
||||
return {};
|
||||
}
|
||||
// The size: a capital H as tall as asked.
|
||||
int x0, y0, x1, y1;
|
||||
float capital = 0;
|
||||
if (stbtt_GetCodepointBox(&f, 'H', &x0, &y0, &x1, &y1)) capital = float(y1);
|
||||
if (capital <= 0) {
|
||||
int ascent, descent, gap;
|
||||
stbtt_GetFontVMetrics(&f, &ascent, &descent, &gap);
|
||||
capital = float(ascent) * 0.7f;
|
||||
}
|
||||
const float scale = style.height / std::max(capital, 1.0f);
|
||||
// The lines, each as wide as its letters.
|
||||
std::vector<std::string> lines(1);
|
||||
for (char c : text) c == '\n' ? (void)lines.emplace_back() : lines.back().push_back(c);
|
||||
auto width = [&](const std::string& line) {
|
||||
float x = 0;
|
||||
int prev = 0;
|
||||
for (size_t i = 0; i < line.size();) {
|
||||
const int g = stbtt_FindGlyphIndex(&f, int(nextChar(line, i)));
|
||||
int adv, lsb;
|
||||
stbtt_GetGlyphHMetrics(&f, g, &adv, &lsb);
|
||||
if (prev) x += float(stbtt_GetGlyphKernAdvance(&f, prev, g)) * scale;
|
||||
x += float(adv) * scale + style.spacing * style.height;
|
||||
prev = g;
|
||||
}
|
||||
return x;
|
||||
};
|
||||
float widest = 0;
|
||||
for (const std::string& l : lines) widest = std::max(widest, width(l));
|
||||
Mesh out;
|
||||
float y = 0;
|
||||
for (const std::string& line : lines) {
|
||||
const float w = width(line);
|
||||
float x = style.align == 1 ? (widest - w) * 0.5f : style.align == 2 ? widest - w : 0.0f;
|
||||
int prev = 0;
|
||||
for (size_t i = 0; i < line.size();) {
|
||||
const int g = stbtt_FindGlyphIndex(&f, int(nextChar(line, i)));
|
||||
if (prev) x += float(stbtt_GetGlyphKernAdvance(&f, prev, g)) * scale;
|
||||
append(out, solidGlyph(glyphContours(f, g, std::max(1, style.detail)), scale, glm::vec2(x, y), style.depth));
|
||||
int adv, lsb;
|
||||
stbtt_GetGlyphHMetrics(&f, g, &adv, &lsb);
|
||||
x += float(adv) * scale + style.spacing * style.height;
|
||||
prev = g;
|
||||
}
|
||||
y -= style.height * (1.0f + std::max(0.0f, style.lineGap) + 0.4f); // (a capital, what hangs below, and the gap)
|
||||
}
|
||||
if (out.faces.empty() && error) *error = "No letters to make (only spaces?)";
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
24
src/text.h
Normal file
24
src/text.h
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
// Text as a 3D shape: the letters' outlines from a font (TrueType / OpenType), made solid.
|
||||
#include "mesh.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fry {
|
||||
|
||||
struct TextStyle {
|
||||
float height = 0.1f; // a capital letter's height (m)
|
||||
float depth = 0.02f; // how thick the letters are (m)
|
||||
float spacing = 0.0f; // more (or less) room between letters, times the height
|
||||
float lineGap = 0.25f; // room between lines, times the height
|
||||
int align = 0; // 0 left, 1 middle, 2 right
|
||||
int detail = 6; // points along each curve of a letter
|
||||
};
|
||||
|
||||
// The letters lying on the ground (XY), the first line's base along X from 0, going up out of the
|
||||
// ground to `depth`. Lines split at '\n'. Empty, with `error` set, if the font can't be read.
|
||||
Mesh makeText(const std::vector<uint8_t>& font, const std::string& text, const TextStyle& style, std::string* error = nullptr);
|
||||
|
||||
} // namespace fry
|
||||
Loading…
Reference in a new issue