mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
Fry3DX 0.4: an easy 3D modeller
Shapes with live settings, shape editing, knife and hole tools, drawing shapes onto other objects, combining objects (add / subtract / overlap), copies, thickness, painting with stickers and layers, a Layout tab, a Print tab with checks, packed .fry files with autosave, export to GLB, glTF, FBX, OBJ, 3MF, STL and PLY, folders in Layers, its own crash screen, and a website for GitHub Pages in docs/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NMSrd5oe1GpUD7E6d825KP
This commit is contained in:
commit
f3e8eca87b
135 changed files with 23423 additions and 0 deletions
13
src/CMakeLists.txt
Normal file
13
src/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# 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 tools.cpp cut.cpp uv.cpp image.cpp sticker.cpp shapes.cpp scene.cpp package.cpp export.cpp print.cpp boolean.cpp modify.cpp)
|
||||
target_include_directories(fry_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(fry_core PUBLIC glm::glm libzstd_static)
|
||||
target_compile_definitions(fry_core PUBLIC GLM_ENABLE_EXPERIMENTAL)
|
||||
|
||||
add_executable(fry3dx app.cpp icons.cpp)
|
||||
target_link_libraries(fry3dx PRIVATE fry_core keel stb)
|
||||
add_dependencies(fry3dx content)
|
||||
foreach(t fry_core fry3dx)
|
||||
target_compile_options(${t} PRIVATE -Wall -Wextra -Wno-missing-field-initializers)
|
||||
endforeach()
|
||||
5864
src/app.cpp
Normal file
5864
src/app.cpp
Normal file
File diff suppressed because it is too large
Load diff
207
src/boolean.cpp
Normal file
207
src/boolean.cpp
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
// Combining shapes: one added to, taken from, or kept where it overlaps another. A BSP tree of
|
||||
// each's faces cuts the other's (as csg.js does), then the pieces are joined up again: points
|
||||
// shared, points that land on another piece's edge set into it (so it stays closed all round).
|
||||
#include "mesh.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
namespace fry {
|
||||
namespace {
|
||||
|
||||
constexpr float kEps = 1e-6f;
|
||||
|
||||
struct Poly {
|
||||
std::vector<glm::vec3> v;
|
||||
glm::vec3 n{0.0f};
|
||||
float w = 0.0f;
|
||||
void plane() {
|
||||
n = glm::normalize(glm::cross(v[1] - v[0], v[2] - v[0]));
|
||||
w = glm::dot(n, v[0]);
|
||||
}
|
||||
void flip() {
|
||||
std::reverse(v.begin(), v.end());
|
||||
n = -n, w = -w;
|
||||
}
|
||||
};
|
||||
|
||||
// Splits `p` by the plane (n, w) into the four lists, as csg.js.
|
||||
void split(const glm::vec3& n, float w, const Poly& p, std::vector<Poly>& coFront, std::vector<Poly>& coBack, std::vector<Poly>& front, std::vector<Poly>& back) {
|
||||
enum { Co = 0, Front = 1, Back = 2, Spanning = 3 };
|
||||
int kind = 0;
|
||||
std::vector<int> types(p.v.size());
|
||||
for (size_t i = 0; i < p.v.size(); ++i) {
|
||||
const float t = glm::dot(n, p.v[i]) - w;
|
||||
types[i] = t < -kEps ? Back : t > kEps ? Front : Co;
|
||||
kind |= types[i];
|
||||
}
|
||||
switch (kind) {
|
||||
case Co: (glm::dot(n, p.n) > 0 ? coFront : coBack).push_back(p); break;
|
||||
case Front: front.push_back(p); break;
|
||||
case Back: back.push_back(p); break;
|
||||
default: {
|
||||
Poly f, b;
|
||||
for (size_t i = 0; i < p.v.size(); ++i) {
|
||||
const size_t j = (i + 1) % p.v.size();
|
||||
const int ti = types[i], tj = types[j];
|
||||
const glm::vec3 vi = p.v[i], vj = p.v[j];
|
||||
if (ti != Back) f.v.push_back(vi);
|
||||
if (ti != Front) b.v.push_back(vi);
|
||||
if ((ti | tj) == Spanning) {
|
||||
const float t = (w - glm::dot(n, vi)) / glm::dot(n, vj - vi);
|
||||
const glm::vec3 x = vi + (vj - vi) * t;
|
||||
f.v.push_back(x), b.v.push_back(x);
|
||||
}
|
||||
}
|
||||
f.n = b.n = p.n, f.w = b.w = p.w;
|
||||
if (f.v.size() >= 3) front.push_back(f);
|
||||
if (b.v.size() >= 3) back.push_back(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Node {
|
||||
bool hasPlane = false;
|
||||
glm::vec3 n{0.0f};
|
||||
float w = 0.0f;
|
||||
std::unique_ptr<Node> front, back;
|
||||
std::vector<Poly> polys;
|
||||
|
||||
void invert() {
|
||||
for (Poly& p : polys) p.flip();
|
||||
n = -n, w = -w;
|
||||
if (front) front->invert();
|
||||
if (back) back->invert();
|
||||
std::swap(front, back);
|
||||
}
|
||||
std::vector<Poly> clip(const std::vector<Poly>& in) const {
|
||||
if (!hasPlane) return in;
|
||||
std::vector<Poly> f, b;
|
||||
for (const Poly& p : in) split(n, w, p, f, b, f, b);
|
||||
if (front) f = front->clip(f);
|
||||
if (back) b = back->clip(b);
|
||||
else b.clear();
|
||||
f.insert(f.end(), b.begin(), b.end());
|
||||
return f;
|
||||
}
|
||||
void clipTo(const Node& other) {
|
||||
polys = other.clip(polys);
|
||||
if (front) front->clipTo(other);
|
||||
if (back) back->clipTo(other);
|
||||
}
|
||||
void all(std::vector<Poly>& out) const {
|
||||
out.insert(out.end(), polys.begin(), polys.end());
|
||||
if (front) front->all(out);
|
||||
if (back) back->all(out);
|
||||
}
|
||||
void build(const std::vector<Poly>& in) {
|
||||
if (in.empty()) return;
|
||||
if (!hasPlane) hasPlane = true, n = in[0].n, w = in[0].w;
|
||||
std::vector<Poly> f, b;
|
||||
for (const Poly& p : in) split(n, w, p, polys, polys, f, b);
|
||||
if (!f.empty()) {
|
||||
if (!front) front = std::make_unique<Node>();
|
||||
front->build(f);
|
||||
}
|
||||
if (!b.empty()) {
|
||||
if (!back) back = std::make_unique<Node>();
|
||||
back->build(b);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<Poly> polysOf(const Mesh& m) {
|
||||
std::vector<Poly> out;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f)
|
||||
for (const auto& t : triangulate(m, int(f))) {
|
||||
Poly p;
|
||||
p.v = {m.verts[size_t(t[0])], m.verts[size_t(t[1])], m.verts[size_t(t[2])]};
|
||||
if (glm::length(glm::cross(p.v[1] - p.v[0], p.v[2] - p.v[0])) < 1e-12f) continue;
|
||||
p.plane();
|
||||
out.push_back(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// The pieces as a mesh: points shared, and points that lie along another piece's edge set into it.
|
||||
Mesh meshOf(const std::vector<Poly>& polys) {
|
||||
Mesh m;
|
||||
std::map<std::array<int64_t, 3>, int> at;
|
||||
const float q = 1e5f; // (0.01 mm)
|
||||
auto point = [&](const glm::vec3& p) {
|
||||
const std::array<int64_t, 3> k{std::llround(p.x * q), std::llround(p.y * q), std::llround(p.z * q)};
|
||||
auto [it, fresh] = at.try_emplace(k, int(m.verts.size()));
|
||||
if (fresh) m.verts.push_back(p);
|
||||
return it->second;
|
||||
};
|
||||
for (const Poly& p : polys) {
|
||||
std::vector<int> f;
|
||||
for (const glm::vec3& v : p.v) {
|
||||
const int i = point(v);
|
||||
if (f.empty() || f.back() != i) f.push_back(i);
|
||||
}
|
||||
while (f.size() > 1 && f.front() == f.back()) f.pop_back();
|
||||
if (f.size() >= 3) m.faces.push_back(f);
|
||||
}
|
||||
// T-junctions: a point in the middle of an edge it isn't a corner of.
|
||||
std::vector<int> order(m.verts.size());
|
||||
for (size_t i = 0; i < order.size(); ++i) order[i] = int(i);
|
||||
std::sort(order.begin(), order.end(), [&](int a, int b) { return m.verts[size_t(a)].x < m.verts[size_t(b)].x; });
|
||||
for (auto& f : m.faces) {
|
||||
std::vector<int> out;
|
||||
for (size_t i = 0; i < f.size(); ++i) {
|
||||
const int a = f[i], b = f[(i + 1) % f.size()];
|
||||
out.push_back(a);
|
||||
const glm::vec3 pa = m.verts[size_t(a)], pb = m.verts[size_t(b)], d = pb - pa;
|
||||
const float len2 = glm::dot(d, d);
|
||||
if (len2 < 1e-14f) continue;
|
||||
std::vector<std::pair<float, int>> on;
|
||||
const float lo = std::min(pa.x, pb.x) - 2e-5f, hi = std::max(pa.x, pb.x) + 2e-5f;
|
||||
auto it = std::lower_bound(order.begin(), order.end(), lo, [&](int v, float x) { return m.verts[size_t(v)].x < x; });
|
||||
for (; it != order.end() && m.verts[size_t(*it)].x <= hi; ++it) {
|
||||
const int v = *it;
|
||||
if (v == a || v == b) continue;
|
||||
const float t = glm::dot(m.verts[size_t(v)] - pa, d) / len2;
|
||||
if (t <= 1e-4f || t >= 1 - 1e-4f) continue;
|
||||
if (const glm::vec3 off = pa + d * t - m.verts[size_t(v)]; glm::dot(off, off) < 1e-10f) on.push_back({t, v});
|
||||
}
|
||||
std::sort(on.begin(), on.end());
|
||||
for (auto [t, v] : on) out.push_back(v);
|
||||
}
|
||||
f = out;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool combine(Mesh& a, const Mesh& b, Combine op) {
|
||||
Node A, B;
|
||||
A.build(polysOf(a));
|
||||
B.build(polysOf(b));
|
||||
if (!A.hasPlane || !B.hasPlane) return false;
|
||||
std::vector<Poly> bp;
|
||||
switch (op) {
|
||||
case Combine::Add:
|
||||
A.clipTo(B), B.clipTo(A), B.invert(), B.clipTo(A), B.invert();
|
||||
B.all(bp), A.build(bp);
|
||||
break;
|
||||
case Combine::Subtract:
|
||||
A.invert(), A.clipTo(B), B.clipTo(A), B.invert(), B.clipTo(A), B.invert();
|
||||
B.all(bp), A.build(bp), A.invert();
|
||||
break;
|
||||
case Combine::Overlap:
|
||||
A.invert(), B.clipTo(A), B.invert(), A.clipTo(B), B.clipTo(A);
|
||||
B.all(bp), A.build(bp), A.invert();
|
||||
break;
|
||||
}
|
||||
std::vector<Poly> out;
|
||||
A.all(out);
|
||||
a = meshOf(out);
|
||||
mergeFlatFaces(a);
|
||||
return !a.faces.empty();
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
409
src/cut.cpp
Normal file
409
src/cut.cpp
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
// The knife, pulling pieces apart, and holes.
|
||||
#include "mesh.h"
|
||||
|
||||
#include <glm/gtc/constants.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
namespace fry {
|
||||
namespace {
|
||||
|
||||
using Edge = std::pair<int, int>;
|
||||
Edge key(int a, int b) { return {std::min(a, b), std::max(a, b)}; }
|
||||
|
||||
std::optional<float> rayTriangle(const glm::vec3& o, const glm::vec3& d, const glm::vec3& a, const glm::vec3& b, const glm::vec3& c) {
|
||||
const glm::vec3 e1 = b - a, e2 = c - a, p = glm::cross(d, e2);
|
||||
const float det = glm::dot(e1, p);
|
||||
if (std::abs(det) < 1e-14f) return std::nullopt;
|
||||
const glm::vec3 s = o - a;
|
||||
const float u = glm::dot(s, p) / det;
|
||||
if (u < 0 || u > 1) return std::nullopt;
|
||||
const glm::vec3 q = glm::cross(s, e1);
|
||||
const float v = glm::dot(d, q) / det;
|
||||
if (v < 0 || u + v > 1) return std::nullopt;
|
||||
const float t = glm::dot(e2, q) / det;
|
||||
return t > 0 ? std::optional<float>(t) : std::nullopt;
|
||||
}
|
||||
|
||||
float sizeOf(const Mesh& m) {
|
||||
const glm::vec3 d = boundsMax(m) - boundsMin(m);
|
||||
return std::max({d.x, d.y, d.z, 1e-6f});
|
||||
}
|
||||
|
||||
// Puts point `p` on every face's edge (a, b), between them.
|
||||
void putOnEdge(Mesh& m, int a, int b, int p) {
|
||||
for (auto& f : m.faces)
|
||||
for (size_t i = 0; i < f.size(); ++i) {
|
||||
const int u = f[i], v = f[(i + 1) % f.size()];
|
||||
if ((u == a && v == b) || (u == b && v == a)) {
|
||||
f.insert(f.begin() + long(i) + 1, p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Two faces from one, cut between two of its corners (false if they're next to each other).
|
||||
bool splitFace(Mesh& m, int face, int p, int q, int* other = nullptr) {
|
||||
std::vector<int> f = m.faces[size_t(face)];
|
||||
auto i = std::find(f.begin(), f.end(), p) - f.begin(), j = std::find(f.begin(), f.end(), q) - f.begin();
|
||||
const long n = long(f.size());
|
||||
if (i == n || j == n || i == j) return false;
|
||||
if (i > j) std::swap(i, j);
|
||||
if (j - i < 2 || (i == 0 && j == n - 1)) return false;
|
||||
std::vector<int> a(f.begin() + i, f.begin() + j + 1), b(f.begin() + j, f.end());
|
||||
b.insert(b.end(), f.begin(), f.begin() + i + 1);
|
||||
m.faces[size_t(face)] = a;
|
||||
m.faces.push_back(b);
|
||||
if (other) *other = int(m.faces.size()) - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::set<Edge> knifeCut(Mesh& m, const glm::vec3& eye, const std::vector<glm::vec3>& path, bool through) {
|
||||
std::set<Edge> made;
|
||||
const float eps = sizeOf(m) * 1e-5f;
|
||||
for (size_t s = 0; s + 1 < path.size(); ++s) {
|
||||
const glm::vec3 da = path[s] - eye, db = path[s + 1] - eye;
|
||||
glm::vec3 n = glm::cross(da, db);
|
||||
if (glm::length(n) < 1e-12f) continue;
|
||||
n = glm::normalize(n);
|
||||
// Between the step's two rays: both of these >= 0 (linear along a line, so easy to clip).
|
||||
auto side1 = [&](const glm::vec3& x) { return glm::dot(glm::cross(da, x - eye), n); };
|
||||
auto side2 = [&](const glm::vec3& x) { return glm::dot(glm::cross(x - eye, db), n); };
|
||||
struct Tri {
|
||||
glm::vec3 a, b, c;
|
||||
int face;
|
||||
};
|
||||
std::vector<Tri> tris;
|
||||
if (!through)
|
||||
for (size_t f = 0; f < m.faces.size(); ++f)
|
||||
for (const auto& t : triangulate(m, int(f))) tris.push_back({m.verts[size_t(t[0])], m.verts[size_t(t[1])], m.verts[size_t(t[2])], int(f)});
|
||||
struct Crossing {
|
||||
int vert = -1, a = -1, b = -1;
|
||||
glm::vec3 p{0.0f};
|
||||
};
|
||||
struct Cut {
|
||||
int face;
|
||||
Crossing c[2];
|
||||
};
|
||||
std::vector<Cut> cuts;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
const auto& fv = m.faces[f];
|
||||
std::vector<float> d(fv.size());
|
||||
for (size_t k = 0; k < fv.size(); ++k) d[k] = glm::dot(n, m.verts[size_t(fv[k])] - eye);
|
||||
std::vector<Crossing> xs;
|
||||
for (size_t k = 0; k < fv.size(); ++k) {
|
||||
const size_t k1 = (k + 1) % fv.size();
|
||||
if (std::abs(d[k]) <= eps) xs.push_back({fv[k], -1, -1, m.verts[size_t(fv[k])]});
|
||||
else if (std::abs(d[k1]) > eps && (d[k] > 0) != (d[k1] > 0)) {
|
||||
const float t = d[k] / (d[k] - d[k1]);
|
||||
xs.push_back({-1, fv[k], fv[k1], glm::mix(m.verts[size_t(fv[k])], m.verts[size_t(fv[k1])], t)});
|
||||
}
|
||||
}
|
||||
if (xs.size() != 2) continue; // (misses it, touches a corner, lies in it, or a bent face)
|
||||
// The part of the chord inside the step's wedge.
|
||||
float t0 = 0, t1 = 1;
|
||||
for (auto sideFn : {0, 1}) {
|
||||
const float g0 = sideFn ? side2(xs[0].p) : side1(xs[0].p), g1 = sideFn ? side2(xs[1].p) : side1(xs[1].p);
|
||||
if (std::abs(g1 - g0) < 1e-12f) {
|
||||
if (g0 < 0) t0 = 1, t1 = 0;
|
||||
continue;
|
||||
}
|
||||
const float at = -g0 / (g1 - g0);
|
||||
if (g1 > g0) t0 = std::max(t0, at);
|
||||
else t1 = std::min(t1, at);
|
||||
}
|
||||
if (t1 - t0 < 1e-5f) continue;
|
||||
if (!through) {
|
||||
const glm::vec3 c = faceCenter(m, int(f));
|
||||
if (glm::dot(faceNormal(m, int(f)), eye - c) <= 0) continue;
|
||||
const glm::vec3 mid = glm::mix(xs[0].p, xs[1].p, (t0 + t1) * 0.5f), dir = mid - eye;
|
||||
const float len = glm::length(dir);
|
||||
bool hidden = false;
|
||||
for (const Tri& t : tris)
|
||||
if (t.face != int(f))
|
||||
if (auto h = rayTriangle(eye, dir / len, t.a, t.b, t.c); h && *h < len - eps * 10) {
|
||||
hidden = true;
|
||||
break;
|
||||
}
|
||||
if (hidden) continue;
|
||||
}
|
||||
cuts.push_back({int(f), {xs[0], xs[1]}});
|
||||
}
|
||||
// New points on the edges crossed (shared with the faces either side), then the splits.
|
||||
std::map<Edge, int> onEdge;
|
||||
for (Cut& c : cuts)
|
||||
for (Crossing& x : c.c)
|
||||
if (x.vert < 0) {
|
||||
auto [it, fresh] = onEdge.try_emplace(key(x.a, x.b), int(m.verts.size()));
|
||||
if (fresh) {
|
||||
m.verts.push_back(x.p);
|
||||
putOnEdge(m, x.a, x.b, it->second);
|
||||
}
|
||||
x.vert = it->second;
|
||||
}
|
||||
for (const Cut& c : cuts)
|
||||
if (splitFace(m, c.face, c.c[0].vert, c.c[1].vert)) made.insert(key(c.c[0].vert, c.c[1].vert));
|
||||
else if (std::abs(c.c[0].vert - c.c[1].vert) > 0) {
|
||||
// Already an edge of the face (it ran along one): still part of the cut.
|
||||
const auto& f = m.faces[size_t(c.face)];
|
||||
for (size_t i = 0; i < f.size(); ++i)
|
||||
if (key(f[i], f[(i + 1) % f.size()]) == key(c.c[0].vert, c.c[1].vert)) made.insert(key(c.c[0].vert, c.c[1].vert));
|
||||
}
|
||||
}
|
||||
return made;
|
||||
}
|
||||
|
||||
std::vector<std::set<int>> splitAlong(Mesh& m, const std::set<Edge>& cut, bool fill) {
|
||||
std::map<Edge, std::vector<int>> byEdge;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
const auto& fv = m.faces[f];
|
||||
for (size_t i = 0; i < fv.size(); ++i) byEdge[key(fv[i], fv[(i + 1) % fv.size()])].push_back(int(f));
|
||||
}
|
||||
// The pieces: faces reached from each other without crossing the cut.
|
||||
std::vector<int> piece(m.faces.size(), -1);
|
||||
int pieces = 0;
|
||||
for (size_t start = 0; start < m.faces.size(); ++start) {
|
||||
if (piece[start] >= 0) continue;
|
||||
std::vector<int> todo{int(start)};
|
||||
piece[start] = pieces;
|
||||
while (!todo.empty()) {
|
||||
const int f = todo.back();
|
||||
todo.pop_back();
|
||||
const auto& fv = m.faces[size_t(f)];
|
||||
for (size_t i = 0; i < fv.size(); ++i) {
|
||||
const Edge e = key(fv[i], fv[(i + 1) % fv.size()]);
|
||||
if (cut.count(e)) continue;
|
||||
for (int g : byEdge[e])
|
||||
if (piece[size_t(g)] < 0) piece[size_t(g)] = pieces, todo.push_back(g);
|
||||
}
|
||||
}
|
||||
++pieces;
|
||||
}
|
||||
std::vector<std::set<int>> out(static_cast<size_t>(pieces));
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) out[size_t(piece[f])].insert(int(f));
|
||||
if (pieces < 2) return out;
|
||||
// Points shared between pieces: one copy each.
|
||||
std::vector<int> owner(m.verts.size(), -1), original;
|
||||
for (size_t v = 0; v < m.verts.size(); ++v) original.push_back(int(v));
|
||||
std::map<std::pair<int, int>, int> copyOf; // (point, piece) → its copy
|
||||
for (size_t f = 0; f < m.faces.size(); ++f)
|
||||
for (int& v : m.faces[f]) {
|
||||
const int p = piece[f];
|
||||
if (owner[size_t(v)] < 0) owner[size_t(v)] = p;
|
||||
if (owner[size_t(v)] == p) continue;
|
||||
auto [it, fresh] = copyOf.try_emplace({v, p}, int(m.verts.size()));
|
||||
if (fresh) m.verts.push_back(m.verts[size_t(v)]), original.push_back(v);
|
||||
v = it->second;
|
||||
}
|
||||
if (!fill) return out;
|
||||
// Close each piece's openings along the cut.
|
||||
for (int p = 0; p < pieces; ++p) {
|
||||
std::map<Edge, int> uses;
|
||||
for (int f : out[size_t(p)]) {
|
||||
const auto& fv = m.faces[size_t(f)];
|
||||
for (size_t i = 0; i < fv.size(); ++i) ++uses[key(fv[i], fv[(i + 1) % fv.size()])];
|
||||
}
|
||||
std::multimap<int, int> next; // the cap's edges: each open edge the other way round
|
||||
for (int f : out[size_t(p)]) {
|
||||
const auto& fv = m.faces[size_t(f)];
|
||||
for (size_t i = 0; i < fv.size(); ++i) {
|
||||
const int a = fv[i], b = fv[(i + 1) % fv.size()];
|
||||
if (uses[key(a, b)] == 1 && cut.count(key(original[size_t(a)], original[size_t(b)]))) next.insert({b, a});
|
||||
}
|
||||
}
|
||||
while (!next.empty()) {
|
||||
std::vector<int> loop{next.begin()->first};
|
||||
int at = next.begin()->second;
|
||||
next.erase(next.begin());
|
||||
while (at != loop.front()) {
|
||||
loop.push_back(at);
|
||||
auto it = next.find(at);
|
||||
if (it == next.end()) break;
|
||||
at = it->second;
|
||||
next.erase(it);
|
||||
}
|
||||
if (at == loop.front() && loop.size() >= 3) {
|
||||
m.faces.push_back(loop);
|
||||
out[size_t(p)].insert(int(m.faces.size()) - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
float volumeOf(const Mesh& m, const std::set<int>& faces) {
|
||||
float v = 0;
|
||||
for (int f : faces)
|
||||
for (const auto& t : triangulate(m, f)) v += glm::dot(m.verts[size_t(t[0])], glm::cross(m.verts[size_t(t[1])], m.verts[size_t(t[2])])) / 6.0f;
|
||||
return std::abs(v);
|
||||
}
|
||||
|
||||
std::vector<glm::vec3> holeOutline(const Mesh& m, int face, const glm::vec3& centre, HoleShape shape, float across, float tall, int sides, float turn) {
|
||||
const auto& fv = m.faces[size_t(face)];
|
||||
const glm::vec3 n = faceNormal(m, face);
|
||||
glm::vec3 u(0.0f);
|
||||
for (size_t i = 0; i < fv.size(); ++i) {
|
||||
const glm::vec3 d = m.verts[size_t(fv[(i + 1) % fv.size()])] - m.verts[size_t(fv[i])];
|
||||
if (glm::length(d) > glm::length(u)) u = d;
|
||||
}
|
||||
u -= n * glm::dot(u, n);
|
||||
if (glm::length(u) < 1e-9f) u = glm::cross(n, std::abs(n.z) < 0.9f ? glm::vec3(0, 0, 1) : glm::vec3(1, 0, 0));
|
||||
u = glm::normalize(u);
|
||||
const float a0 = glm::radians(turn);
|
||||
const glm::vec3 v0 = glm::cross(n, u), ut = u * std::cos(a0) + v0 * std::sin(a0), vt = glm::cross(n, ut);
|
||||
const glm::vec3 c = centre - n * glm::dot(centre - faceCenter(m, face), n);
|
||||
std::vector<glm::vec3> out;
|
||||
auto ring = [&](int count, float r, float start) { // (counter-clockwise about the normal)
|
||||
for (int k = 0; k < count; ++k) {
|
||||
const float a = start + glm::two_pi<float>() * float(k) / float(count);
|
||||
out.push_back(c + (ut * std::cos(a) + vt * std::sin(a)) * r);
|
||||
}
|
||||
};
|
||||
switch (shape) {
|
||||
case HoleShape::Round: ring(std::max(sides, 3), across * 0.5f, 0.0f); break;
|
||||
case HoleShape::Rectangle: {
|
||||
const float hw = across * 0.5f, hh = (tall > 0 ? tall : across) * 0.5f;
|
||||
for (glm::vec2 k : {glm::vec2(-1, -1), glm::vec2(1, -1), glm::vec2(1, 1), glm::vec2(-1, 1)}) out.push_back(c + ut * (hw * k.x) + vt * (hh * k.y));
|
||||
break;
|
||||
}
|
||||
case HoleShape::Triangle: ring(3, across / std::sqrt(3.0f), glm::half_pi<float>()); break;
|
||||
case HoleShape::Hexagon: ring(6, across / std::sqrt(3.0f), 0.0f); break; // (corners left and right: flats top and bottom)
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<glm::vec3> holeOutline(const Mesh& m, int face, const glm::vec3& centre, float radius, int sides) {
|
||||
if (sides == 4) return holeOutline(m, face, centre, HoleShape::Rectangle, radius * 2.0f, radius * 2.0f);
|
||||
return holeOutline(m, face, centre, HoleShape::Round, radius * 2.0f, 0.0f, sides);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Sets a loop of new points (counter-clockwise about the face's normal, inside it) into a face:
|
||||
// the face becomes a ring of faces round the loop. Returns the loop's points, or nothing if it
|
||||
// doesn't fit (nothing changed).
|
||||
std::optional<std::vector<int>> setIntoFace(Mesh& m, int face, const std::vector<glm::vec3>& loop) {
|
||||
const std::vector<int> fv = m.faces[size_t(face)];
|
||||
const glm::vec3 n = faceNormal(m, face);
|
||||
const glm::vec3 u = glm::normalize(std::abs(n.z) < 0.9f ? glm::cross(glm::vec3(0, 0, 1), n) : glm::cross(glm::vec3(1, 0, 0), n)), v = glm::cross(n, u);
|
||||
glm::vec3 c(0.0f);
|
||||
for (const glm::vec3& p : loop) c += p;
|
||||
c /= float(loop.size());
|
||||
auto flat = [&](const glm::vec3& p) { return glm::vec2(glm::dot(p - c, u), glm::dot(p - c, v)); };
|
||||
std::vector<glm::vec2> outer;
|
||||
for (int i : fv) outer.push_back(flat(m.verts[size_t(i)]));
|
||||
auto inside = [&](glm::vec2 p) {
|
||||
bool in = false;
|
||||
for (size_t i = 0, j = outer.size() - 1; i < outer.size(); j = i++)
|
||||
if ((outer[i].y > p.y) != (outer[j].y > p.y) && p.x < (outer[j].x - outer[i].x) * (p.y - outer[i].y) / (outer[j].y - outer[i].y) + outer[i].x) in = !in;
|
||||
return in;
|
||||
};
|
||||
for (const glm::vec3& p : loop)
|
||||
if (!inside(flat(p))) return std::nullopt;
|
||||
// The face's corners must go round the middle in order (so each can be joined to the loop).
|
||||
auto angle = [](glm::vec2 p) { return std::atan2(p.y, p.x); };
|
||||
// Angles round the middle, each further on than the last (a face that doubles back goes
|
||||
// more than once round, and doesn't fit).
|
||||
auto unwrap = [&](const std::vector<glm::vec2>& pts, float near) {
|
||||
std::vector<float> a;
|
||||
for (const glm::vec2& p : pts) {
|
||||
float x = angle(p);
|
||||
if (a.empty()) {
|
||||
while (x < near - glm::pi<float>()) x += glm::two_pi<float>();
|
||||
while (x >= near + glm::pi<float>()) x -= glm::two_pi<float>();
|
||||
} else {
|
||||
while (x <= a.back()) x += glm::two_pi<float>();
|
||||
while (x > a.back() + glm::two_pi<float>()) x -= glm::two_pi<float>();
|
||||
}
|
||||
a.push_back(x);
|
||||
}
|
||||
return a;
|
||||
};
|
||||
const std::vector<float> ao = unwrap(outer, angle(outer[0]));
|
||||
if (ao.back() - ao.front() >= glm::two_pi<float>()) return std::nullopt;
|
||||
std::vector<glm::vec2> inner;
|
||||
for (const glm::vec3& p : loop) inner.push_back(flat(p));
|
||||
const std::vector<float> ai = unwrap(inner, ao[0]);
|
||||
if (ai.back() - ai.front() >= glm::two_pi<float>()) return std::nullopt;
|
||||
// Each corner of the face joins the loop point nearest its way round.
|
||||
const int N = int(loop.size()), M = int(fv.size());
|
||||
auto loopAngle = [&](int k) { // (k may go past either end: whole turns)
|
||||
const int q = ((k % N) + N) % N, turns = (k - q) / N;
|
||||
return ai[size_t(q)] + glm::two_pi<float>() * float(turns);
|
||||
};
|
||||
std::vector<int> at(size_t(M) + 1);
|
||||
int k = -N;
|
||||
for (int i = 0; i < M; ++i) {
|
||||
while (std::abs(loopAngle(k + 1) - ao[size_t(i)]) <= std::abs(loopAngle(k) - ao[size_t(i)])) ++k;
|
||||
at[size_t(i)] = k;
|
||||
}
|
||||
at[size_t(M)] = at[0] + N;
|
||||
const int base = int(m.verts.size());
|
||||
for (const glm::vec3& p : loop) m.verts.push_back(p);
|
||||
auto L = [&](int k) { return base + ((k % N) + N) % N; };
|
||||
bool first = true;
|
||||
for (int i = 0; i < M; ++i) {
|
||||
std::vector<int> f{fv[size_t(i)], fv[size_t((i + 1) % M)]};
|
||||
for (int q = at[size_t(i) + 1]; q >= at[size_t(i)]; --q) f.push_back(L(q));
|
||||
if (first) m.faces[size_t(face)] = f, first = false;
|
||||
else m.faces.push_back(f);
|
||||
}
|
||||
std::vector<int> out;
|
||||
for (int q = 0; q < N; ++q) out.push_back(base + q);
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int imprintOutline(Mesh& m, int face, const std::vector<glm::vec3>& outline) {
|
||||
if (face < 0 || face >= int(m.faces.size()) || outline.size() < 3) return -1;
|
||||
auto inner = setIntoFace(m, face, outline);
|
||||
if (!inner) return -1;
|
||||
m.faces.push_back(*inner);
|
||||
return int(m.faces.size()) - 1;
|
||||
}
|
||||
|
||||
bool cutHole(Mesh& m, int face, const std::vector<glm::vec3>& outline, float depth) {
|
||||
if (face < 0 || face >= int(m.faces.size()) || outline.size() < 3) return false;
|
||||
const Mesh before = m;
|
||||
const glm::vec3 n = faceNormal(m, face);
|
||||
glm::vec3 c(0.0f);
|
||||
for (const glm::vec3& p : outline) c += p;
|
||||
c /= float(outline.size());
|
||||
// The face behind, found before anything changes.
|
||||
int behind = -1;
|
||||
float tBehind = 1e30f;
|
||||
if (depth <= 0)
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
if (int(f) == face) continue;
|
||||
for (const auto& t : triangulate(m, int(f)))
|
||||
if (auto h = rayTriangle(c, -n, m.verts[size_t(t[0])], m.verts[size_t(t[1])], m.verts[size_t(t[2])]); h && *h > 1e-6f && *h < tBehind) tBehind = *h, behind = int(f);
|
||||
}
|
||||
if (depth <= 0 && behind < 0) return false;
|
||||
auto front = setIntoFace(m, face, outline);
|
||||
if (!front) return false;
|
||||
if (depth > 0) {
|
||||
m.faces.push_back(*front);
|
||||
extrudeFaces(m, {int(m.faces.size()) - 1}, -depth);
|
||||
return true;
|
||||
}
|
||||
// Through: the same outline, carried straight back onto the face behind.
|
||||
const glm::vec3 nb = faceNormal(m, behind), pb = faceCenter(m, behind);
|
||||
if (std::abs(glm::dot(nb, n)) < 1e-4f) return m = before, false;
|
||||
std::vector<glm::vec3> back;
|
||||
for (auto it = outline.rbegin(); it != outline.rend(); ++it) back.push_back(*it - n * (glm::dot(nb, pb - *it) / glm::dot(nb, -n)));
|
||||
auto rear = setIntoFace(m, behind, back);
|
||||
if (!rear) return m = before, false;
|
||||
const int N = int(outline.size());
|
||||
for (int k = 0; k < N; ++k) {
|
||||
const int k1 = (k + 1) % N;
|
||||
const int bk = (*rear)[size_t(N - 1 - k)], bk1 = (*rear)[size_t(N - 1 - k1)];
|
||||
m.faces.push_back({(*front)[size_t(k)], (*front)[size_t(k1)], bk1, bk});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
495
src/export.cpp
Normal file
495
src/export.cpp
Normal file
|
|
@ -0,0 +1,495 @@
|
|||
// Exporting for other programs: glTF / GLB (scene.cpp), OBJ, STL and PLY here.
|
||||
#include "scene.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
|
||||
namespace fry {
|
||||
|
||||
bool exportGltf(const std::string& path, const Scene& scene, const std::vector<std::vector<uint8_t>>& pngs, bool binary, std::string* error);
|
||||
|
||||
namespace {
|
||||
|
||||
// An object's triangles where it stands (Fry3DX's axes, metres), shared corners where they agree.
|
||||
struct Tris {
|
||||
std::vector<glm::vec3> pos, nrm;
|
||||
std::vector<glm::vec2> uv;
|
||||
std::vector<uint32_t> idx;
|
||||
};
|
||||
Tris trianglesOf(const Object& shown, bool painted) {
|
||||
Object o = shown;
|
||||
if (o.hasFinish()) o.mesh = finishedMesh(shown);
|
||||
const glm::mat4 m = objectMatrix(o);
|
||||
const glm::mat3 nm = glm::transpose(glm::inverse(glm::mat3(m)));
|
||||
const bool mirrored = glm::determinant(glm::mat3(m)) < 0.0f;
|
||||
const auto normals = cornerNormals(o.mesh, o.smooth, o.smoothAngle);
|
||||
Tris t;
|
||||
std::map<std::array<float, 8>, uint32_t> seen;
|
||||
for (size_t f = 0; f < o.mesh.faces.size(); ++f) {
|
||||
const auto& face = o.mesh.faces[f];
|
||||
for (const auto& tri : triangulate(o.mesh, int(f))) {
|
||||
std::array<uint32_t, 3> c{};
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const size_t corner = size_t(std::find(face.begin(), face.end(), tri[size_t(k)]) - face.begin());
|
||||
const glm::vec3 p(m * glm::vec4(o.mesh.verts[size_t(tri[size_t(k)])], 1.0f)), n = glm::normalize(nm * normals[f][corner]);
|
||||
const glm::vec2 tc = painted ? o.uvs[f][corner] : glm::vec2(0.0f);
|
||||
auto [it, fresh] = seen.try_emplace(std::array<float, 8>{p.x, p.y, p.z, n.x, n.y, n.z, tc.x, tc.y}, uint32_t(t.pos.size()));
|
||||
if (fresh) t.pos.push_back(p), t.nrm.push_back(n), t.uv.push_back(tc);
|
||||
c[size_t(k)] = it->second;
|
||||
}
|
||||
if (mirrored) std::swap(c[1], c[2]);
|
||||
t.idx.insert(t.idx.end(), c.begin(), c.end());
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
bool painted(const Scene& s, const std::vector<std::vector<uint8_t>>& pngs, size_t i) {
|
||||
return i < pngs.size() && !pngs[i].empty() && s.objects[i].uvsFit() && !s.objects[i].hasFinish();
|
||||
}
|
||||
|
||||
bool writeBytes(const std::filesystem::path& p, const void* data, size_t n, std::string* error) {
|
||||
std::ofstream f(p, std::ios::binary);
|
||||
if (f.write(static_cast<const char*>(data), std::streamsize(n))) return true;
|
||||
if (error) *error = "can't write " + p.filename().string();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string safeName(std::string s) {
|
||||
for (char& ch : s)
|
||||
if (ch == ' ' || uint8_t(ch) < 0x20) ch = '_';
|
||||
return s;
|
||||
}
|
||||
|
||||
bool exportObj(const std::string& path, const Scene& scene, const std::vector<std::vector<uint8_t>>& pngs, std::string* error) {
|
||||
const std::filesystem::path p(path);
|
||||
const std::string mtlName = p.stem().string() + ".mtl";
|
||||
std::ostringstream obj, mtl;
|
||||
obj.precision(7), mtl.precision(5);
|
||||
obj << "# Fry3DX\nmtllib " << mtlName << "\n";
|
||||
mtl << "# Fry3DX\n";
|
||||
auto yUp = [](glm::vec3 v) { return glm::vec3(v.y, v.z, v.x); }; // (as glTF's)
|
||||
size_t base = 1, any = 0;
|
||||
for (size_t i = 0; i < scene.objects.size(); ++i) {
|
||||
const Object& o = scene.objects[i];
|
||||
if (!o.visible || o.mesh.faces.empty()) continue;
|
||||
const bool pic = painted(scene, pngs, i);
|
||||
const Tris t = trianglesOf(o, pic);
|
||||
const std::string name = safeName(o.name), mat = std::format("{}_{}", name, i + 1);
|
||||
obj << "o " << name << "\nusemtl " << mat << "\n";
|
||||
for (const glm::vec3& v : t.pos) obj << "v " << yUp(v).x << " " << yUp(v).y << " " << yUp(v).z << "\n";
|
||||
for (const glm::vec2& v : t.uv) obj << "vt " << v.x << " " << 1.0f - v.y << "\n";
|
||||
for (const glm::vec3& v : t.nrm) obj << "vn " << yUp(v).x << " " << yUp(v).y << " " << yUp(v).z << "\n";
|
||||
for (size_t k = 0; k < t.idx.size(); k += 3) {
|
||||
obj << "f";
|
||||
for (int j = 0; j < 3; ++j) obj << " " << base + t.idx[k + size_t(j)] << "/" << base + t.idx[k + size_t(j)] << "/" << base + t.idx[k + size_t(j)];
|
||||
obj << "\n";
|
||||
}
|
||||
base += t.pos.size(), ++any;
|
||||
const glm::vec3 c = pic ? glm::vec3(1.0f) : glm::pow(o.color, glm::vec3(2.2f));
|
||||
mtl << "newmtl " << mat << "\nKd " << c.r << " " << c.g << " " << c.b << "\nKs 0.04 0.04 0.04\nNs " << (1.0f - o.rough) * 200.0f << "\nPr " << o.rough
|
||||
<< "\nPm " << o.metal << "\n";
|
||||
if (pic) {
|
||||
const std::string png = std::format("{}_{}.png", p.stem().string(), i + 1);
|
||||
if (!writeBytes(p.parent_path() / png, pngs[i].data(), pngs[i].size(), error)) return false;
|
||||
mtl << "map_Kd " << png << "\n";
|
||||
}
|
||||
}
|
||||
if (!any) {
|
||||
if (error) *error = "nothing to export (no visible objects)";
|
||||
return false;
|
||||
}
|
||||
const std::string a = obj.str(), b = mtl.str();
|
||||
return writeFileSafely(path, a.data(), a.size(), error) && writeFileSafely((p.parent_path() / mtlName).string(), b.data(), b.size(), error);
|
||||
}
|
||||
|
||||
bool exportStl(const std::string& path, const Scene& scene, std::string* error) {
|
||||
std::vector<uint8_t> out(80, 0);
|
||||
const char head[] = "Fry3DX, millimetres, Z up";
|
||||
std::memcpy(out.data(), head, sizeof head);
|
||||
out.resize(84);
|
||||
uint32_t count = 0;
|
||||
auto put = [&](glm::vec3 v) { out.insert(out.end(), reinterpret_cast<uint8_t*>(&v), reinterpret_cast<uint8_t*>(&v) + 12); };
|
||||
for (const Object& o : scene.objects) {
|
||||
if (!o.visible) continue;
|
||||
const Tris t = trianglesOf(o, false);
|
||||
for (size_t k = 0; k < t.idx.size(); k += 3) {
|
||||
const glm::vec3 a = t.pos[t.idx[k]] * 1000.0f, b = t.pos[t.idx[k + 1]] * 1000.0f, c = t.pos[t.idx[k + 2]] * 1000.0f;
|
||||
const glm::vec3 n = glm::cross(b - a, c - a);
|
||||
put(glm::length(n) > 0 ? glm::normalize(n) : glm::vec3(0.0f)), put(a), put(b), put(c);
|
||||
out.push_back(0), out.push_back(0);
|
||||
++count;
|
||||
}
|
||||
}
|
||||
if (!count) {
|
||||
if (error) *error = "nothing to export (no visible objects)";
|
||||
return false;
|
||||
}
|
||||
std::memcpy(out.data() + 80, &count, 4);
|
||||
return writeFileSafely(path, out.data(), out.size(), error);
|
||||
}
|
||||
|
||||
bool exportPly(const std::string& path, const Scene& scene, const std::vector<std::vector<uint8_t>>& pngs, std::string* error) {
|
||||
std::vector<uint8_t> verts, faces;
|
||||
size_t nv = 0, nf = 0;
|
||||
auto put = [](std::vector<uint8_t>& to, const void* p, size_t n) { to.insert(to.end(), static_cast<const uint8_t*>(p), static_cast<const uint8_t*>(p) + n); };
|
||||
for (size_t i = 0; i < scene.objects.size(); ++i) {
|
||||
const Object& o = scene.objects[i];
|
||||
if (!o.visible) continue;
|
||||
const bool pic = painted(scene, pngs, i);
|
||||
const Tris t = trianglesOf(o, pic);
|
||||
const uint8_t rgb[3] = {uint8_t(std::clamp(o.color.r, 0.0f, 1.0f) * 255.0f + 0.5f), uint8_t(std::clamp(o.color.g, 0.0f, 1.0f) * 255.0f + 0.5f),
|
||||
uint8_t(std::clamp(o.color.b, 0.0f, 1.0f) * 255.0f + 0.5f)};
|
||||
for (size_t v = 0; v < t.pos.size(); ++v) {
|
||||
const glm::vec2 st(t.uv[v].x, 1.0f - t.uv[v].y);
|
||||
put(verts, &t.pos[v], 12), put(verts, &t.nrm[v], 12), put(verts, &st, 8), put(verts, rgb, 3);
|
||||
}
|
||||
for (size_t k = 0; k < t.idx.size(); k += 3) {
|
||||
const uint8_t three = 3;
|
||||
const uint32_t f[3] = {uint32_t(nv + t.idx[k]), uint32_t(nv + t.idx[k + 1]), uint32_t(nv + t.idx[k + 2])};
|
||||
put(faces, &three, 1), put(faces, f, 12);
|
||||
}
|
||||
nv += t.pos.size(), nf += t.idx.size() / 3;
|
||||
}
|
||||
if (!nf) {
|
||||
if (error) *error = "nothing to export (no visible objects)";
|
||||
return false;
|
||||
}
|
||||
std::string head = std::format("ply\nformat binary_little_endian 1.0\ncomment Fry3DX, metres, Z up\nelement vertex {}\n"
|
||||
"property float x\nproperty float y\nproperty float z\nproperty float nx\nproperty float ny\nproperty float nz\n"
|
||||
"property float s\nproperty float t\nproperty uchar red\nproperty uchar green\nproperty uchar blue\n"
|
||||
"element face {}\nproperty list uchar uint vertex_indices\nend_header\n", nv, nf);
|
||||
std::vector<uint8_t> out(head.begin(), head.end());
|
||||
out.insert(out.end(), verts.begin(), verts.end()), out.insert(out.end(), faces.begin(), faces.end());
|
||||
return writeFileSafely(path, out.data(), out.size(), error);
|
||||
}
|
||||
|
||||
// A zip with its files stored as they are (3MF is a zip).
|
||||
std::vector<uint8_t> storedZip(const std::vector<std::pair<std::string, std::string>>& files) {
|
||||
static uint32_t table[256];
|
||||
if (!table[1])
|
||||
for (uint32_t i = 0; i < 256; ++i) {
|
||||
uint32_t c = i;
|
||||
for (int k = 0; k < 8; ++k) c = c & 1 ? 0xEDB88320u ^ (c >> 1) : c >> 1;
|
||||
table[i] = c;
|
||||
}
|
||||
auto crc = [&](const std::string& d) {
|
||||
uint32_t c = 0xFFFFFFFFu;
|
||||
for (unsigned char ch : d) c = table[(c ^ ch) & 0xFF] ^ (c >> 8);
|
||||
return c ^ 0xFFFFFFFFu;
|
||||
};
|
||||
std::vector<uint8_t> out, dir;
|
||||
auto u16 = [](std::vector<uint8_t>& v, uint16_t x) { v.push_back(uint8_t(x)), v.push_back(uint8_t(x >> 8)); };
|
||||
auto u32 = [&](std::vector<uint8_t>& v, uint32_t x) { u16(v, uint16_t(x)), u16(v, uint16_t(x >> 16)); };
|
||||
for (const auto& [name, data] : files) {
|
||||
const uint32_t at = uint32_t(out.size()), c = crc(data), n = uint32_t(data.size());
|
||||
u32(out, 0x04034b50), u16(out, 20), u16(out, 0), u16(out, 0), u16(out, 0), u16(out, 0x21), u32(out, c), u32(out, n), u32(out, n);
|
||||
u16(out, uint16_t(name.size())), u16(out, 0), out.insert(out.end(), name.begin(), name.end()), out.insert(out.end(), data.begin(), data.end());
|
||||
u32(dir, 0x02014b50), u16(dir, 20), u16(dir, 20), u16(dir, 0), u16(dir, 0), u16(dir, 0), u16(dir, 0x21), u32(dir, c), u32(dir, n), u32(dir, n);
|
||||
u16(dir, uint16_t(name.size())), u16(dir, 0), u16(dir, 0), u16(dir, 0), u16(dir, 0), u32(dir, 0), u32(dir, at);
|
||||
dir.insert(dir.end(), name.begin(), name.end());
|
||||
}
|
||||
const uint32_t dirAt = uint32_t(out.size()), dirSize = uint32_t(dir.size());
|
||||
out.insert(out.end(), dir.begin(), dir.end());
|
||||
u32(out, 0x06054b50), u16(out, 0), u16(out, 0), u16(out, uint16_t(files.size())), u16(out, uint16_t(files.size())), u32(out, dirSize), u32(out, dirAt), u16(out, 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool export3mf(const std::string& path, const Scene& scene, std::string* error) {
|
||||
std::ostringstream model, items;
|
||||
model.precision(7);
|
||||
model << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<model unit=\"millimeter\" xml:lang=\"en-US\" "
|
||||
"xmlns=\"http://schemas.microsoft.com/3dmanufacturing/core/2015/02\">\n<metadata name=\"Application\">Fry3DX</metadata>\n<resources>\n";
|
||||
auto xml = [](const std::string& s) {
|
||||
std::string o;
|
||||
for (char ch : s) o += ch == '&' ? "&" : ch == '<' ? "<" : ch == '>' ? ">" : ch == '"' ? """ : std::string(1, ch);
|
||||
return o;
|
||||
};
|
||||
int id = 0;
|
||||
for (const Object& o : scene.objects) {
|
||||
if (!o.visible || o.mesh.faces.empty()) continue;
|
||||
const Tris t = trianglesOf(o, false);
|
||||
// (corners joined again: slicers want each point once, or they see the model as not closed)
|
||||
std::map<std::array<float, 3>, uint32_t> at;
|
||||
std::vector<uint32_t> remap(t.pos.size());
|
||||
std::ostringstream verts;
|
||||
verts.precision(7);
|
||||
for (size_t v = 0; v < t.pos.size(); ++v) {
|
||||
const glm::vec3 p = t.pos[v] * 1000.0f;
|
||||
auto [it, fresh] = at.try_emplace({p.x, p.y, p.z}, uint32_t(at.size()));
|
||||
if (fresh) verts << "<vertex x=\"" << p.x << "\" y=\"" << p.y << "\" z=\"" << p.z << "\"/>\n";
|
||||
remap[v] = it->second;
|
||||
}
|
||||
++id;
|
||||
model << "<object id=\"" << id << "\" name=\"" << xml(o.name) << "\" type=\"model\">\n<mesh>\n<vertices>\n" << verts.str() << "</vertices>\n<triangles>\n";
|
||||
for (size_t k = 0; k < t.idx.size(); k += 3) {
|
||||
const uint32_t a = remap[t.idx[k]], b = remap[t.idx[k + 1]], c = remap[t.idx[k + 2]];
|
||||
if (a != b && b != c && a != c) model << "<triangle v1=\"" << a << "\" v2=\"" << b << "\" v3=\"" << c << "\"/>\n";
|
||||
}
|
||||
model << "</triangles>\n</mesh>\n</object>\n";
|
||||
items << "<item objectid=\"" << id << "\"/>\n";
|
||||
}
|
||||
if (!id) {
|
||||
if (error) *error = "nothing to export (no visible objects)";
|
||||
return false;
|
||||
}
|
||||
model << "</resources>\n<build>\n" << items.str() << "</build>\n</model>\n";
|
||||
const auto zip = storedZip({
|
||||
{"[Content_Types].xml", "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">"
|
||||
"<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>"
|
||||
"<Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/></Types>\n"},
|
||||
{"_rels/.rels", "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">"
|
||||
"<Relationship Target=\"/3D/3dmodel.model\" Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/></Relationships>\n"},
|
||||
{"3D/3dmodel.model", model.str()},
|
||||
});
|
||||
return writeFileSafely(path, zip.data(), zip.size(), error);
|
||||
}
|
||||
|
||||
// ── FBX (binary, 7.4: what Blender, Unity, Unreal and Maya read) ──
|
||||
// A tree of nodes, each a name, a list of typed values, and nodes inside it.
|
||||
struct FbxNode {
|
||||
std::string name;
|
||||
std::vector<uint8_t> props;
|
||||
uint32_t count = 0;
|
||||
std::vector<FbxNode> kids;
|
||||
|
||||
template <class T>
|
||||
void raw(const T& v) { props.insert(props.end(), reinterpret_cast<const uint8_t*>(&v), reinterpret_cast<const uint8_t*>(&v) + sizeof v); }
|
||||
FbxNode& I(int32_t v) { return props.push_back('I'), raw(v), ++count, *this; }
|
||||
FbxNode& L(int64_t v) { return props.push_back('L'), raw(v), ++count, *this; }
|
||||
FbxNode& D(double v) { return props.push_back('D'), raw(v), ++count, *this; }
|
||||
FbxNode& C(bool v) { return props.push_back('C'), props.push_back(v ? 1 : 0), ++count, *this; }
|
||||
FbxNode& S(const std::string& v, char type = 'S') {
|
||||
props.push_back(uint8_t(type)), raw(uint32_t(v.size())), props.insert(props.end(), v.begin(), v.end());
|
||||
return ++count, *this;
|
||||
}
|
||||
FbxNode& R(const std::vector<uint8_t>& v) {
|
||||
props.push_back('R'), raw(uint32_t(v.size())), props.insert(props.end(), v.begin(), v.end());
|
||||
return ++count, *this;
|
||||
}
|
||||
template <class T>
|
||||
FbxNode& array(char type, const std::vector<T>& v) {
|
||||
props.push_back(uint8_t(type)), raw(uint32_t(v.size())), raw(uint32_t(0)), raw(uint32_t(v.size() * sizeof(T)));
|
||||
props.insert(props.end(), reinterpret_cast<const uint8_t*>(v.data()), reinterpret_cast<const uint8_t*>(v.data()) + v.size() * sizeof(T));
|
||||
return ++count, *this;
|
||||
}
|
||||
FbxNode& add(const std::string& n) { return kids.push_back({n, {}, 0, {}}), kids.back(); }
|
||||
// Properties70's P: name, type, label, flags, then its values.
|
||||
FbxNode& P(const std::string& n, const std::string& type, const std::string& label, const std::string& flags = "") {
|
||||
return add("P").S(n).S(type).S(label).S(flags);
|
||||
}
|
||||
|
||||
void write(std::vector<uint8_t>& out) const {
|
||||
const size_t at = out.size();
|
||||
out.resize(at + 12); // (where it ends, how many values, their size: filled in below)
|
||||
out.push_back(uint8_t(name.size())), out.insert(out.end(), name.begin(), name.end());
|
||||
out.insert(out.end(), props.begin(), props.end());
|
||||
if (!kids.empty() || props.empty()) {
|
||||
for (const FbxNode& k : kids) k.write(out);
|
||||
out.resize(out.size() + 13); // (the end of the list inside it)
|
||||
}
|
||||
const uint32_t head[3] = {uint32_t(out.size()), count, uint32_t(props.size())};
|
||||
std::memcpy(out.data() + at, head, 12);
|
||||
}
|
||||
};
|
||||
|
||||
bool exportFbx(const std::string& path, const Scene& scene, const std::vector<std::vector<uint8_t>>& pngs, std::string* error) {
|
||||
const std::filesystem::path file(path);
|
||||
auto yUp = [](glm::vec3 v) { return glm::dvec3(v.y, v.z, v.x) * 100.0; }; // (glTF's axes; FBX counts in centimetres)
|
||||
std::vector<FbxNode> top;
|
||||
{
|
||||
FbxNode& h = top.emplace_back(FbxNode{"FBXHeaderExtension"});
|
||||
h.add("FBXHeaderVersion").I(1003), h.add("FBXVersion").I(7400);
|
||||
FbxNode& t = h.add("CreationTimeStamp");
|
||||
t.add("Version").I(1000);
|
||||
for (const char* k : {"Year", "Month", "Day", "Hour", "Minute", "Second", "Millisecond"}) t.add(k).I(std::string(k) == "Year" ? 1970 : 1);
|
||||
h.add("Creator").S("Fry3DX");
|
||||
}
|
||||
// (the same file id and time Blender writes, which go with the footer below)
|
||||
top.push_back(FbxNode{"FileId"}), top.back().R({0x28, 0xb3, 0x2a, 0xeb, 0xb6, 0x24, 0xcc, 0xc2, 0xbf, 0xc8, 0xb0, 0x2a, 0xa9, 0x2b, 0xfc, 0xf1});
|
||||
top.push_back(FbxNode{"CreationTime"}), top.back().S("1970-01-01 10:00:00:000");
|
||||
top.push_back(FbxNode{"Creator"}), top.back().S("Fry3DX");
|
||||
{
|
||||
FbxNode& g = top.emplace_back(FbxNode{"GlobalSettings"});
|
||||
g.add("Version").I(1000);
|
||||
FbxNode& p = g.add("Properties70");
|
||||
const std::pair<const char*, int> axes[] = {{"UpAxis", 1}, {"UpAxisSign", 1}, {"FrontAxis", 2}, {"FrontAxisSign", 1}, {"CoordAxis", 0}, {"CoordAxisSign", 1},
|
||||
{"OriginalUpAxis", 1}, {"OriginalUpAxisSign", 1}};
|
||||
for (auto [k, v] : axes) p.P(k, "int", "Integer").I(v);
|
||||
p.P("UnitScaleFactor", "double", "Number").D(1.0), p.P("OriginalUnitScaleFactor", "double", "Number").D(1.0);
|
||||
}
|
||||
{
|
||||
FbxNode& d = top.emplace_back(FbxNode{"Documents"});
|
||||
d.add("Count").I(1);
|
||||
FbxNode& doc = d.add("Document").L(1000000).S("").S("Scene");
|
||||
doc.add("Properties70");
|
||||
doc.add("RootNode").L(0);
|
||||
}
|
||||
top.push_back(FbxNode{"References"});
|
||||
FbxNode objects{"Objects"}, links{"Connections"};
|
||||
int64_t next = 2000000;
|
||||
int models = 0, textures = 0;
|
||||
for (size_t i = 0; i < scene.objects.size(); ++i) {
|
||||
const Object& shown = scene.objects[i];
|
||||
if (!shown.visible || shown.mesh.faces.empty()) continue;
|
||||
const bool pic = painted(scene, pngs, i);
|
||||
Object o = shown;
|
||||
if (o.hasFinish()) o.mesh = finishedMesh(shown);
|
||||
const glm::mat4 rs = glm::mat4_cast(o.rotation) * glm::scale(glm::mat4(1.0f), o.scale);
|
||||
const glm::mat3 nm = glm::transpose(glm::inverse(glm::mat3(rs)));
|
||||
const bool mirrored = glm::determinant(glm::mat3(rs)) < 0.0f;
|
||||
const auto normals = cornerNormals(o.mesh, o.smooth, o.smoothAngle);
|
||||
std::vector<double> verts, nrm, uv;
|
||||
std::vector<int32_t> index, uvIndex;
|
||||
for (const glm::vec3& v : o.mesh.verts) {
|
||||
const glm::dvec3 p = yUp(glm::vec3(rs * glm::vec4(v, 1.0f)));
|
||||
verts.insert(verts.end(), {p.x, p.y, p.z});
|
||||
}
|
||||
for (size_t f = 0; f < o.mesh.faces.size(); ++f) {
|
||||
const auto& fv = o.mesh.faces[f];
|
||||
for (size_t k = 0; k < fv.size(); ++k) {
|
||||
const size_t c = mirrored ? fv.size() - 1 - k : k;
|
||||
index.push_back(k + 1 == fv.size() ? -fv[c] - 1 : fv[c]); // (the last of a face: negative, less one)
|
||||
const glm::dvec3 n = yUp(glm::normalize(nm * normals[f][c])) / 100.0;
|
||||
nrm.insert(nrm.end(), {n.x, n.y, n.z});
|
||||
if (pic) {
|
||||
uvIndex.push_back(int32_t(uv.size() / 2));
|
||||
uv.insert(uv.end(), {double(o.uvs[f][c].x), 1.0 - double(o.uvs[f][c].y)});
|
||||
}
|
||||
}
|
||||
}
|
||||
const std::string name = o.name.empty() ? std::format("Object {}", i + 1) : o.name;
|
||||
const int64_t geo = next++, model = next++, mat = next++;
|
||||
FbxNode& g = objects.add("Geometry").L(geo).S(name + std::string("\x00\x01", 2) + "Geometry", 'S').S("Mesh");
|
||||
g.add("Properties70");
|
||||
g.add("GeometryVersion").I(124);
|
||||
g.add("Vertices").array('d', verts);
|
||||
g.add("PolygonVertexIndex").array('i', index);
|
||||
FbxNode& ln = g.add("LayerElementNormal").I(0);
|
||||
ln.add("Version").I(101), ln.add("Name").S(""), ln.add("MappingInformationType").S("ByPolygonVertex"), ln.add("ReferenceInformationType").S("Direct");
|
||||
ln.add("Normals").array('d', nrm);
|
||||
if (pic) {
|
||||
FbxNode& lu = g.add("LayerElementUV").I(0);
|
||||
lu.add("Version").I(101), lu.add("Name").S("UVMap"), lu.add("MappingInformationType").S("ByPolygonVertex"), lu.add("ReferenceInformationType").S("IndexToDirect");
|
||||
lu.add("UV").array('d', uv), lu.add("UVIndex").array('i', uvIndex);
|
||||
}
|
||||
FbxNode& lm = g.add("LayerElementMaterial").I(0);
|
||||
lm.add("Version").I(101), lm.add("Name").S(""), lm.add("MappingInformationType").S("AllSame"), lm.add("ReferenceInformationType").S("IndexToDirect");
|
||||
lm.add("Materials").array('i', std::vector<int32_t>{0});
|
||||
FbxNode& layer = g.add("Layer").I(0);
|
||||
layer.add("Version").I(100);
|
||||
for (const char* t : {"LayerElementNormal", "LayerElementMaterial", "LayerElementUV"}) {
|
||||
if (!pic && std::string(t) == "LayerElementUV") continue;
|
||||
FbxNode& e = layer.add("LayerElement");
|
||||
e.add("Type").S(t), e.add("TypedIndex").I(0);
|
||||
}
|
||||
|
||||
FbxNode& m = objects.add("Model").L(model).S(name + std::string("\x00\x01", 2) + "Model").S("Mesh");
|
||||
m.add("Version").I(232);
|
||||
FbxNode& mp = m.add("Properties70");
|
||||
const glm::dvec3 t = yUp(o.position);
|
||||
mp.P("Lcl Translation", "Lcl Translation", "", "A").D(t.x).D(t.y).D(t.z);
|
||||
m.add("Shading").C(true), m.add("Culling").S("CullingOff");
|
||||
|
||||
const glm::vec3 col = pic ? glm::vec3(1.0f) : glm::pow(o.color, glm::vec3(2.2f));
|
||||
FbxNode& ma = objects.add("Material").L(mat).S(name + std::string("\x00\x01", 2) + "Material").S("");
|
||||
ma.add("Version").I(102), ma.add("ShadingModel").S("phong"), ma.add("MultiLayer").I(0);
|
||||
FbxNode& pp = ma.add("Properties70");
|
||||
pp.P("DiffuseColor", "Color", "", "A").D(col.r).D(col.g).D(col.b);
|
||||
pp.P("DiffuseFactor", "Number", "", "A").D(1.0);
|
||||
pp.P("SpecularFactor", "Number", "", "A").D(0.5 * (1.0 - o.rough));
|
||||
pp.P("Shininess", "Number", "", "A").D(2.0 + (1.0 - o.rough) * 98.0);
|
||||
pp.P("ReflectionFactor", "Number", "", "A").D(o.metal);
|
||||
links.add("C").S("OO").L(model).L(0);
|
||||
links.add("C").S("OO").L(geo).L(model);
|
||||
links.add("C").S("OO").L(mat).L(model);
|
||||
++models;
|
||||
if (pic) { // its picture: inside the file, and beside it as a PNG
|
||||
const std::string png = std::format("{}_{}.png", file.stem().string(), i + 1);
|
||||
if (!writeBytes(file.parent_path() / png, pngs[i].data(), pngs[i].size(), error)) return false;
|
||||
const int64_t tex = next++, video = next++;
|
||||
FbxNode& v = objects.add("Video").L(video).S(png + std::string("\x00\x01", 2) + "Video").S("Clip");
|
||||
v.add("Type").S("Clip");
|
||||
v.add("Properties70").P("Path", "KString", "XRefUrl", "").S(png);
|
||||
v.add("UseMipMap").I(0), v.add("Filename").S(png), v.add("RelativeFilename").S(png), v.add("Content").R(pngs[i]);
|
||||
FbxNode& tx = objects.add("Texture").L(tex).S(png + std::string("\x00\x01", 2) + "Texture").S("");
|
||||
tx.add("Type").S("TextureVideoClip"), tx.add("Version").I(202), tx.add("TextureName").S(png + std::string("\x00\x01", 2) + "Texture");
|
||||
tx.add("Properties70").P("UseMaterial", "bool", "", "").I(1);
|
||||
tx.add("Media").S(png + std::string("\x00\x01", 2) + "Video"), tx.add("FileName").S(png), tx.add("RelativeFilename").S(png);
|
||||
tx.add("ModelUVTranslation").D(0).D(0), tx.add("ModelUVScaling").D(1).D(1), tx.add("Texture_Alpha_Source").S("None");
|
||||
tx.add("Cropping").I(0).I(0).I(0).I(0);
|
||||
links.add("C").S("OP").L(tex).L(mat).S("DiffuseColor");
|
||||
links.add("C").S("OO").L(video).L(tex);
|
||||
++textures;
|
||||
}
|
||||
}
|
||||
if (!models) {
|
||||
if (error) *error = "nothing to export (no visible objects)";
|
||||
return false;
|
||||
}
|
||||
{
|
||||
FbxNode& d = top.emplace_back(FbxNode{"Definitions"});
|
||||
d.add("Version").I(100);
|
||||
const std::pair<const char*, int> kinds[] = {{"GlobalSettings", 1}, {"Model", models}, {"Geometry", models}, {"Material", models}, {"Texture", textures}, {"Video", textures}};
|
||||
int all = 0;
|
||||
for (auto [k, n] : kinds) all += n;
|
||||
d.add("Count").I(all);
|
||||
for (auto [k, n] : kinds)
|
||||
if (n) d.add("ObjectType").S(k).add("Count").I(n);
|
||||
}
|
||||
top.push_back(std::move(objects));
|
||||
top.push_back(std::move(links));
|
||||
|
||||
std::vector<uint8_t> out;
|
||||
const char head[] = "Kaydara FBX Binary ";
|
||||
out.insert(out.end(), head, head + sizeof head); // (with its 0)
|
||||
out.push_back(0x1a), out.push_back(0);
|
||||
const uint32_t version = 7400;
|
||||
out.insert(out.end(), reinterpret_cast<const uint8_t*>(&version), reinterpret_cast<const uint8_t*>(&version) + 4);
|
||||
for (const FbxNode& n : top) n.write(out);
|
||||
out.resize(out.size() + 13);
|
||||
const uint8_t foot[] = {0xfa, 0xbc, 0xab, 0x09, 0xd0, 0xc8, 0xd4, 0x66, 0xb1, 0x76, 0xfb, 0x83, 0x1c, 0xf7, 0x26, 0x7e};
|
||||
out.insert(out.end(), foot, foot + 16), out.resize(out.size() + 4);
|
||||
size_t pad = ((out.size() + 15) & ~size_t(15)) - out.size();
|
||||
out.resize(out.size() + (pad ? pad : 16));
|
||||
out.insert(out.end(), reinterpret_cast<const uint8_t*>(&version), reinterpret_cast<const uint8_t*>(&version) + 4);
|
||||
out.resize(out.size() + 120);
|
||||
const uint8_t end[] = {0xf8, 0x5a, 0x8c, 0x6a, 0xde, 0xf5, 0xd9, 0x7e, 0xec, 0xe9, 0x0c, 0xe3, 0x75, 0x8f, 0x29, 0x0b};
|
||||
out.insert(out.end(), end, end + 16);
|
||||
return writeFileSafely(path, out.data(), out.size(), error);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<std::pair<std::string, std::string>>& exportFormats() {
|
||||
static const std::vector<std::pair<std::string, std::string>> formats = {
|
||||
{".glb", "glTF binary, all in one file (games: Godot, Unity, Unreal; Blender)"},
|
||||
{".gltf", "glTF with its .bin and pictures beside it"},
|
||||
{".fbx", "FBX (Blender, Unity, Unreal, Maya, 3ds Max)"},
|
||||
{".obj", "Wavefront OBJ with an .mtl and pictures (nearly every 3D program)"},
|
||||
{".3mf", "3MF for 3D printing slicers (PrusaSlicer, Bambu Studio, Cura, Orca)"},
|
||||
{".stl", "STL for 3D printing (millimetres, shape only)"},
|
||||
{".ply", "PLY (shape, colours; scanners, MeshLab)"},
|
||||
};
|
||||
return formats;
|
||||
}
|
||||
|
||||
bool exportModel(const std::string& path, const Scene& scene, const std::vector<std::vector<uint8_t>>& pngs, std::string* error) {
|
||||
std::string ext = std::filesystem::path(path).extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return char(std::tolower(c)); });
|
||||
if (ext == ".gltf" || ext == ".glb") return exportGltf(path, scene, pngs, ext == ".glb", error);
|
||||
if (ext == ".fbx") return exportFbx(path, scene, pngs, error);
|
||||
if (ext == ".obj") return exportObj(path, scene, pngs, error);
|
||||
if (ext == ".3mf") return export3mf(path, scene, error);
|
||||
if (ext == ".stl") return exportStl(path, scene, error);
|
||||
if (ext == ".ply") return exportPly(path, scene, pngs, error);
|
||||
if (error) *error = "can't export to " + ext + " files";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
185
src/icons.cpp
Normal file
185
src/icons.cpp
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#include "icons.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <initializer_list>
|
||||
#include <utility>
|
||||
|
||||
namespace fry {
|
||||
|
||||
void drawIcon(ImDrawList* dl, Icon icon, ImVec2 o, float s, ImU32 c) {
|
||||
auto P = [&](float x, float y) { return ImVec2(o.x + x * s, o.y + y * s); };
|
||||
const float t = std::max(1.5f, s * 0.07f);
|
||||
const ImU32 dim = (c & 0x00FFFFFF) | 0x60000000;
|
||||
auto line = [&](float x0, float y0, float x1, float y1, float w = 0) { dl->AddLine(P(x0, y0), P(x1, y1), c, w > 0 ? w : t); };
|
||||
auto tri = [&](float x0, float y0, float x1, float y1, float x2, float y2) { dl->AddTriangleFilled(P(x0, y0), P(x1, y1), P(x2, y2), c); };
|
||||
auto quad = [&](float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3, bool fill, ImU32 col) {
|
||||
if (fill) dl->AddQuadFilled(P(x0, y0), P(x1, y1), P(x2, y2), P(x3, y3), col);
|
||||
else dl->AddQuad(P(x0, y0), P(x1, y1), P(x2, y2), P(x3, y3), col, t);
|
||||
};
|
||||
auto dot = [&](float x, float y, float r) { dl->AddCircleFilled(P(x, y), r * s, c); };
|
||||
switch (icon) {
|
||||
case Icon::Select: {
|
||||
const ImVec2 pts[] = {P(0.28f, 0.14f), P(0.28f, 0.80f), P(0.44f, 0.65f), P(0.56f, 0.88f), P(0.66f, 0.83f), P(0.55f, 0.60f), P(0.76f, 0.60f)};
|
||||
dl->AddConcavePolyFilled(pts, 7, c);
|
||||
break;
|
||||
}
|
||||
case Icon::Move:
|
||||
line(0.5f, 0.2f, 0.5f, 0.8f), line(0.2f, 0.5f, 0.8f, 0.5f);
|
||||
tri(0.5f, 0.08f, 0.38f, 0.24f, 0.62f, 0.24f), tri(0.5f, 0.92f, 0.38f, 0.76f, 0.62f, 0.76f);
|
||||
tri(0.08f, 0.5f, 0.24f, 0.38f, 0.24f, 0.62f), tri(0.92f, 0.5f, 0.76f, 0.38f, 0.76f, 0.62f);
|
||||
break;
|
||||
case Icon::Rotate:
|
||||
dl->PathArcTo(P(0.5f, 0.52f), 0.30f * s, 3.14159f * 0.15f, 3.14159f * 1.75f, 24);
|
||||
dl->PathStroke(c, 0, t * 1.2f);
|
||||
tri(0.80f, 0.18f, 0.86f, 0.46f, 0.60f, 0.36f);
|
||||
break;
|
||||
case Icon::Scale:
|
||||
dl->AddRect(P(0.16f, 0.16f), P(0.84f, 0.84f), dim, 0, 0, t);
|
||||
dl->AddRectFilled(P(0.16f, 0.56f), P(0.44f, 0.84f), c);
|
||||
line(0.44f, 0.56f, 0.76f, 0.24f);
|
||||
tri(0.84f, 0.16f, 0.62f, 0.22f, 0.78f, 0.38f);
|
||||
break;
|
||||
case Icon::Draw: {
|
||||
const ImVec2 pts[] = {P(0.18f, 0.78f), P(0.30f, 0.26f), P(0.62f, 0.18f), P(0.82f, 0.50f), P(0.60f, 0.82f)};
|
||||
dl->AddPolyline(pts, 5, c, ImDrawFlags_Closed, t);
|
||||
for (const ImVec2& p : pts) dl->AddCircleFilled(p, 0.07f * s, c);
|
||||
break;
|
||||
}
|
||||
case Icon::Extrude:
|
||||
quad(0.14f, 0.70f, 0.56f, 0.70f, 0.86f, 0.52f, 0.44f, 0.52f, true, dim);
|
||||
quad(0.14f, 0.42f, 0.56f, 0.42f, 0.86f, 0.24f, 0.44f, 0.24f, false, c);
|
||||
line(0.50f, 0.62f, 0.50f, 0.36f), tri(0.50f, 0.10f, 0.40f, 0.30f, 0.60f, 0.30f);
|
||||
break;
|
||||
case Icon::Inset:
|
||||
dl->AddRect(P(0.14f, 0.14f), P(0.86f, 0.86f), c, 0, 0, t);
|
||||
dl->AddRectFilled(P(0.34f, 0.34f), P(0.66f, 0.66f), c);
|
||||
line(0.14f, 0.14f, 0.34f, 0.34f, t * 0.6f), line(0.86f, 0.14f, 0.66f, 0.34f, t * 0.6f);
|
||||
line(0.14f, 0.86f, 0.34f, 0.66f, t * 0.6f), line(0.86f, 0.86f, 0.66f, 0.66f, t * 0.6f);
|
||||
break;
|
||||
case Icon::Bevel:
|
||||
dl->PathLineTo(P(0.16f, 0.86f));
|
||||
dl->PathLineTo(P(0.16f, 0.16f));
|
||||
dl->PathLineTo(P(0.40f, 0.16f));
|
||||
dl->PathArcTo(P(0.40f, 0.60f), 0.44f * s, -3.14159f * 0.5f, 0.0f, 16);
|
||||
dl->PathLineTo(P(0.84f, 0.86f));
|
||||
dl->PathFillConvex(dim);
|
||||
dl->PathLineTo(P(0.40f, 0.16f));
|
||||
dl->PathArcTo(P(0.40f, 0.60f), 0.44f * s, -3.14159f * 0.5f, 0.0f, 16);
|
||||
dl->PathStroke(c, 0, t * 1.4f);
|
||||
break;
|
||||
case Icon::LoopCut:
|
||||
dl->AddRect(P(0.12f, 0.22f), P(0.88f, 0.78f), dim, 0, 0, t);
|
||||
line(0.12f, 0.50f, 0.88f, 0.50f, t * 0.6f);
|
||||
line(0.50f, 0.10f, 0.50f, 0.90f, t * 1.4f);
|
||||
break;
|
||||
case Icon::Objects:
|
||||
case Icon::Shape:
|
||||
quad(0.5f, 0.12f, 0.86f, 0.30f, 0.5f, 0.48f, 0.14f, 0.30f, icon == Icon::Objects, icon == Icon::Objects ? c : dim);
|
||||
quad(0.14f, 0.30f, 0.5f, 0.48f, 0.5f, 0.88f, 0.14f, 0.70f, icon == Icon::Objects, icon == Icon::Objects ? dim : dim);
|
||||
quad(0.86f, 0.30f, 0.5f, 0.48f, 0.5f, 0.88f, 0.86f, 0.70f, false, c);
|
||||
if (icon == Icon::Shape)
|
||||
for (auto [x, y] : {std::pair{0.5f, 0.12f}, {0.86f, 0.30f}, {0.5f, 0.48f}, {0.14f, 0.30f}, {0.14f, 0.70f}, {0.5f, 0.88f}, {0.86f, 0.70f}}) dot(x, y, 0.07f);
|
||||
break;
|
||||
case Icon::Points:
|
||||
case Icon::Edges:
|
||||
case Icon::Faces:
|
||||
if (icon == Icon::Faces) dl->AddRectFilled(P(0.2f, 0.2f), P(0.8f, 0.8f), c);
|
||||
dl->AddRect(P(0.2f, 0.2f), P(0.8f, 0.8f), icon == Icon::Faces ? c : dim, 0, 0, t);
|
||||
if (icon == Icon::Edges) line(0.2f, 0.2f, 0.8f, 0.2f, t * 2.2f), line(0.8f, 0.2f, 0.8f, 0.8f, t * 2.2f);
|
||||
if (icon == Icon::Points)
|
||||
for (auto [x, y] : {std::pair{0.2f, 0.2f}, {0.8f, 0.2f}, {0.8f, 0.8f}, {0.2f, 0.8f}}) dot(x, y, 0.11f);
|
||||
break;
|
||||
case Icon::Box:
|
||||
for (int i = 0; i < 4; ++i) { // dashed
|
||||
const float a = 0.16f + float(i) * 0.18f, b = a + 0.1f;
|
||||
line(a, 0.18f, b, 0.18f), line(a, 0.82f, b, 0.82f), line(0.16f, a + 0.02f, 0.16f, b + 0.02f), line(0.84f, a + 0.02f, 0.84f, b + 0.02f);
|
||||
}
|
||||
break;
|
||||
case Icon::Brush:
|
||||
dl->AddCircle(P(0.5f, 0.5f), 0.32f * s, c, 24, t);
|
||||
dot(0.5f, 0.5f, 0.07f);
|
||||
break;
|
||||
case Icon::Lasso: {
|
||||
dl->PathArcTo(P(0.52f, 0.40f), 0.30f * s, 0.0f, 6.2832f, 24);
|
||||
dl->PathStroke(c, ImDrawFlags_Closed, t);
|
||||
line(0.32f, 0.62f, 0.26f, 0.88f);
|
||||
break;
|
||||
}
|
||||
case Icon::Add:
|
||||
line(0.5f, 0.18f, 0.5f, 0.82f, t * 1.6f), line(0.18f, 0.5f, 0.82f, 0.5f, t * 1.6f);
|
||||
break;
|
||||
case Icon::Knife: {
|
||||
const ImVec2 blade[] = {P(0.10f, 0.90f), P(0.56f, 0.30f), P(0.70f, 0.44f)};
|
||||
dl->AddTriangleFilled(blade[0], blade[1], blade[2], c);
|
||||
line(0.66f, 0.38f, 0.88f, 0.14f, t * 2.4f);
|
||||
line(0.14f, 0.62f, 0.34f, 0.62f, t * 0.6f), line(0.06f, 0.74f, 0.22f, 0.74f, t * 0.6f); // (the cut)
|
||||
break;
|
||||
}
|
||||
case Icon::Hole:
|
||||
dl->AddRect(P(0.12f, 0.12f), P(0.88f, 0.88f), dim, 0, 0, t);
|
||||
dl->AddCircleFilled(P(0.5f, 0.5f), 0.26f * s, (c & 0x00FFFFFF) | 0x40000000, 24);
|
||||
dl->AddCircle(P(0.5f, 0.5f), 0.26f * s, c, 24, t * 1.2f);
|
||||
dl->PathArcTo(P(0.5f, 0.5f), 0.18f * s, 3.14159f * 0.15f, 3.14159f * 0.85f, 12);
|
||||
dl->PathStroke(c, 0, t * 0.7f);
|
||||
break;
|
||||
case Icon::PaintBrush:
|
||||
line(0.84f, 0.14f, 0.46f, 0.52f, t * 1.6f);
|
||||
dl->AddQuadFilled(P(0.40f, 0.48f), P(0.52f, 0.60f), P(0.36f, 0.80f), P(0.22f, 0.66f), c);
|
||||
dl->AddTriangleFilled(P(0.22f, 0.66f), P(0.36f, 0.80f), P(0.10f, 0.90f), c);
|
||||
break;
|
||||
case Icon::Eraser:
|
||||
quad(0.16f, 0.62f, 0.50f, 0.28f, 0.82f, 0.60f, 0.48f, 0.94f, false, c);
|
||||
quad(0.16f, 0.62f, 0.33f, 0.45f, 0.65f, 0.77f, 0.48f, 0.94f, true, c);
|
||||
break;
|
||||
case Icon::Fill:
|
||||
quad(0.16f, 0.46f, 0.46f, 0.16f, 0.76f, 0.46f, 0.46f, 0.76f, false, c);
|
||||
quad(0.16f, 0.46f, 0.76f, 0.46f, 0.61f, 0.61f, 0.31f, 0.61f, true, dim);
|
||||
dot(0.84f, 0.74f, 0.08f), tri(0.84f, 0.52f, 0.77f, 0.72f, 0.91f, 0.72f);
|
||||
break;
|
||||
case Icon::Picker:
|
||||
line(0.18f, 0.82f, 0.60f, 0.40f, t * 1.6f);
|
||||
line(0.50f, 0.36f, 0.64f, 0.50f, t * 1.6f);
|
||||
dot(0.72f, 0.28f, 0.13f);
|
||||
dot(0.16f, 0.84f, 0.05f);
|
||||
break;
|
||||
case Icon::Picture:
|
||||
dl->AddRect(P(0.10f, 0.18f), P(0.90f, 0.82f), c, 0, 0, t);
|
||||
tri(0.16f, 0.76f, 0.40f, 0.40f, 0.62f, 0.76f), tri(0.46f, 0.76f, 0.66f, 0.52f, 0.84f, 0.76f);
|
||||
dot(0.70f, 0.34f, 0.07f);
|
||||
break;
|
||||
case Icon::Render:
|
||||
dl->AddRect(P(0.10f, 0.32f), P(0.90f, 0.82f), c, 0.08f * s, 0, t);
|
||||
dl->AddRectFilled(P(0.30f, 0.20f), P(0.54f, 0.33f), c);
|
||||
dl->AddCircle(P(0.5f, 0.57f), 0.16f * s, c, 20, t);
|
||||
dot(0.78f, 0.42f, 0.04f);
|
||||
break;
|
||||
case Icon::Layout: // (a frame unfolded: a cross of squares)
|
||||
dl->AddRect(P(0.38f, 0.10f), P(0.62f, 0.34f), c, 0, 0, t);
|
||||
dl->AddRect(P(0.14f, 0.34f), P(0.86f, 0.58f), c, 0, 0, t);
|
||||
dl->AddRect(P(0.38f, 0.58f), P(0.62f, 0.90f), c, 0, 0, t);
|
||||
line(0.38f, 0.34f, 0.38f, 0.58f), line(0.62f, 0.34f, 0.62f, 0.58f);
|
||||
break;
|
||||
case Icon::Print: // (a nozzle over what it's printed)
|
||||
quad(0.34f, 0.10f, 0.66f, 0.10f, 0.66f, 0.30f, 0.34f, 0.30f, true, c);
|
||||
tri(0.40f, 0.30f, 0.60f, 0.30f, 0.50f, 0.44f);
|
||||
for (int i = 0; i < 3; ++i) dl->AddRectFilled(P(0.22f + 0.04f * float(i), 0.56f + 0.11f * float(i)), P(0.78f - 0.04f * float(i), 0.64f + 0.11f * float(i)), i ? dim : c);
|
||||
line(0.10f, 0.90f, 0.90f, 0.90f);
|
||||
break;
|
||||
case Icon::Ruler: {
|
||||
quad(0.08f, 0.66f, 0.66f, 0.08f, 0.92f, 0.34f, 0.34f, 0.92f, false, c);
|
||||
for (int i = 1; i < 6; ++i) {
|
||||
const float k = float(i) / 6.0f, x = 0.08f + 0.58f * k, y = 0.66f - 0.58f * k, l = i % 2 ? 0.09f : 0.15f;
|
||||
line(x, y, x + l, y + l, t * 0.8f);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Icon::LayFlat:
|
||||
quad(0.22f, 0.40f, 0.62f, 0.22f, 0.86f, 0.50f, 0.46f, 0.68f, false, c);
|
||||
line(0.10f, 0.88f, 0.90f, 0.88f);
|
||||
line(0.54f, 0.06f, 0.54f, 0.30f), tri(0.54f, 0.36f, 0.46f, 0.24f, 0.62f, 0.24f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
11
src/icons.h
Normal file
11
src/icons.h
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
// The toolbox's pictures, drawn as lines and shapes (sharp at any size, no files).
|
||||
#include <imgui.h>
|
||||
|
||||
namespace fry {
|
||||
|
||||
enum class Icon { Select, Move, Rotate, Scale, Draw, Extrude, Inset, Bevel, LoopCut, Objects, Shape, Points, Edges, Faces, Box, Brush, Lasso, Add, Knife, Hole, PaintBrush, Eraser, Fill, Picker, Picture, Render, Layout, Print, Ruler, LayFlat };
|
||||
|
||||
void drawIcon(ImDrawList* dl, Icon icon, ImVec2 topLeft, float size, ImU32 color);
|
||||
|
||||
} // namespace fry
|
||||
119
src/image.cpp
Normal file
119
src/image.cpp
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
#include "image.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace fry {
|
||||
|
||||
Image makeImage(int w, int h, const glm::vec4& c) {
|
||||
Image im;
|
||||
im.w = w, im.h = h;
|
||||
im.px.resize(size_t(w) * size_t(h) * 4);
|
||||
const uint8_t b[4] = {uint8_t(std::lround(std::clamp(c.r, 0.0f, 1.0f) * 255)), uint8_t(std::lround(std::clamp(c.g, 0.0f, 1.0f) * 255)),
|
||||
uint8_t(std::lround(std::clamp(c.b, 0.0f, 1.0f) * 255)), uint8_t(std::lround(std::clamp(c.a, 0.0f, 1.0f) * 255))};
|
||||
for (size_t i = 0; i < im.px.size(); i += 4) std::copy_n(b, 4, &im.px[i]);
|
||||
return im;
|
||||
}
|
||||
|
||||
glm::vec4 pixel(const Image& im, int x, int y) {
|
||||
const uint8_t* p = im.at(std::clamp(x, 0, im.w - 1), std::clamp(y, 0, im.h - 1));
|
||||
return glm::vec4(p[0], p[1], p[2], p[3]) / 255.0f;
|
||||
}
|
||||
|
||||
glm::vec4 sample(const Image& im, glm::vec2 at) {
|
||||
const glm::vec2 p = at - 0.5f;
|
||||
const int x = int(std::floor(p.x)), y = int(std::floor(p.y));
|
||||
const float fx = p.x - float(x), fy = p.y - float(y);
|
||||
return glm::mix(glm::mix(pixel(im, x, y), pixel(im, x + 1, y), fx), glm::mix(pixel(im, x, y + 1), pixel(im, x + 1, y + 1), fx), fy);
|
||||
}
|
||||
|
||||
void Stroke::begin(const Image& im, const glm::vec4& c) {
|
||||
base = im, color = c;
|
||||
cover.assign(size_t(im.w) * size_t(im.h), 0.0f);
|
||||
}
|
||||
|
||||
void Stroke::put(Image& im, int x, int y, float amount) {
|
||||
if (x < 0 || y < 0 || x >= im.w || y >= im.h) return;
|
||||
float& c = cover[size_t(y) * size_t(im.w) + size_t(x)];
|
||||
if (amount <= c) return;
|
||||
c = amount;
|
||||
const float a = std::clamp(c * color.a, 0.0f, 1.0f);
|
||||
const uint8_t* b = base.at(x, y);
|
||||
uint8_t* p = im.at(x, y);
|
||||
for (int k = 0; k < 3; ++k) p[k] = uint8_t(std::lround(float(b[k]) + (color[k] * 255.0f - float(b[k])) * a));
|
||||
p[3] = uint8_t(std::lround(float(b[3]) + (255.0f - float(b[3])) * a));
|
||||
}
|
||||
|
||||
void Stroke::dab(Image& im, glm::vec2 c, float r, float hardness) {
|
||||
r = std::max(r, 0.5f);
|
||||
const int x0 = int(std::floor(c.x - r)), x1 = int(std::ceil(c.x + r)), y0 = int(std::floor(c.y - r)), y1 = int(std::ceil(c.y + r));
|
||||
const float inner = std::clamp(hardness, 0.0f, 0.999f) * r;
|
||||
for (int y = std::max(y0, 0); y <= std::min(y1, im.h - 1); ++y)
|
||||
for (int x = std::max(x0, 0); x <= std::min(x1, im.w - 1); ++x) {
|
||||
const float d = std::hypot(float(x) + 0.5f - c.x, float(y) + 0.5f - c.y);
|
||||
if (d >= r) continue;
|
||||
const float t = d <= inner ? 1.0f : 1.0f - (d - inner) / (r - inner);
|
||||
put(im, x, y, t * t * (3 - 2 * t));
|
||||
}
|
||||
}
|
||||
|
||||
void floodFill(Image& im, glm::ivec2 at, const glm::vec4& color, float tolerance) {
|
||||
if (at.x < 0 || at.y < 0 || at.x >= im.w || at.y >= im.h) return;
|
||||
const glm::vec4 from = pixel(im, at.x, at.y);
|
||||
const Image before = im;
|
||||
std::vector<char> done(size_t(im.w) * size_t(im.h), 0);
|
||||
std::vector<glm::ivec2> todo{at};
|
||||
const uint8_t c[4] = {uint8_t(std::lround(color.r * 255)), uint8_t(std::lround(color.g * 255)), uint8_t(std::lround(color.b * 255)), 255};
|
||||
while (!todo.empty()) {
|
||||
const glm::ivec2 p = todo.back();
|
||||
todo.pop_back();
|
||||
if (p.x < 0 || p.y < 0 || p.x >= im.w || p.y >= im.h) continue;
|
||||
char& d = done[size_t(p.y) * size_t(im.w) + size_t(p.x)];
|
||||
if (d) continue;
|
||||
d = 1;
|
||||
const glm::vec4 q = pixel(before, p.x, p.y);
|
||||
if (std::max({std::abs(q.r - from.r), std::abs(q.g - from.g), std::abs(q.b - from.b), std::abs(q.a - from.a)}) > tolerance) continue;
|
||||
std::copy_n(c, 4, im.at(p.x, p.y));
|
||||
todo.insert(todo.end(), {{p.x + 1, p.y}, {p.x - 1, p.y}, {p.x, p.y + 1}, {p.x, p.y - 1}});
|
||||
}
|
||||
}
|
||||
|
||||
glm::mat3 squareToQuad(const std::array<glm::vec2, 4>& q) {
|
||||
// Heckbert's: (0,0)→q0, (1,0)→q1, (1,1)→q2, (0,1)→q3.
|
||||
const glm::vec2 d1 = q[1] - q[2], d2 = q[3] - q[2], d3 = q[0] - q[1] + q[2] - q[3];
|
||||
float g = 0, h = 0;
|
||||
if (std::abs(d3.x) > 1e-9f || std::abs(d3.y) > 1e-9f) {
|
||||
const float den = d1.x * d2.y - d2.x * d1.y;
|
||||
if (std::abs(den) > 1e-12f) g = (d3.x * d2.y - d2.x * d3.y) / den, h = (d1.x * d3.y - d3.x * d1.y) / den;
|
||||
}
|
||||
const float a = q[1].x - q[0].x + g * q[1].x, b = q[3].x - q[0].x + h * q[3].x, c = q[0].x;
|
||||
const float d = q[1].y - q[0].y + g * q[1].y, e = q[3].y - q[0].y + h * q[3].y, f = q[0].y;
|
||||
return glm::mat3(glm::vec3(a, d, g), glm::vec3(b, e, h), glm::vec3(c, f, 1.0f)); // (columns)
|
||||
}
|
||||
|
||||
glm::vec2 mapSquare(const glm::mat3& m, glm::vec2 uv) {
|
||||
const glm::vec3 p = m * glm::vec3(uv, 1.0f);
|
||||
return glm::vec2(p) / p.z;
|
||||
}
|
||||
|
||||
void stampQuad(Image& dst, const Image& src, const std::array<glm::vec2, 4>& q, float opacity, std::vector<uint8_t>* owner, uint8_t id) {
|
||||
if (src.empty()) return;
|
||||
const glm::mat3 inv = glm::inverse(squareToQuad(q));
|
||||
glm::vec2 lo(1e30f), hi(-1e30f);
|
||||
for (const glm::vec2& p : q) lo = glm::min(lo, p), hi = glm::max(hi, p);
|
||||
for (int y = std::max(int(lo.y), 0); y <= std::min(int(hi.y) + 1, dst.h - 1); ++y)
|
||||
for (int x = std::max(int(lo.x), 0); x <= std::min(int(hi.x) + 1, dst.w - 1); ++x) {
|
||||
const glm::vec3 h = inv * glm::vec3(float(x) + 0.5f, float(y) + 0.5f, 1.0f);
|
||||
if (std::abs(h.z) < 1e-12f) continue;
|
||||
const glm::vec2 uv = glm::vec2(h) / h.z;
|
||||
if (uv.x < 0 || uv.y < 0 || uv.x > 1 || uv.y > 1) continue;
|
||||
const glm::vec4 s = sample(src, uv * glm::vec2(src.w, src.h));
|
||||
const float a = s.a * opacity;
|
||||
if (owner && a > 0.05f) (*owner)[size_t(y) * size_t(dst.w) + size_t(x)] = id;
|
||||
uint8_t* p = dst.at(x, y);
|
||||
for (int k = 0; k < 3; ++k) p[k] = uint8_t(std::lround(float(p[k]) + (s[k] * 255.0f - float(p[k])) * a));
|
||||
p[3] = uint8_t(std::lround(float(p[3]) + (255.0f - float(p[3])) * a));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
49
src/image.h
Normal file
49
src/image.h
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#pragma once
|
||||
// Pictures to paint: RGBA, 8 bits a channel, as seen (sRGB), rows from the top.
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace fry {
|
||||
|
||||
struct Image {
|
||||
int w = 0, h = 0;
|
||||
std::vector<uint8_t> px;
|
||||
bool empty() const { return w <= 0 || h <= 0; }
|
||||
uint8_t* at(int x, int y) { return &px[(size_t(y) * size_t(w) + size_t(x)) * 4]; }
|
||||
const uint8_t* at(int x, int y) const { return &px[(size_t(y) * size_t(w) + size_t(x)) * 4]; }
|
||||
};
|
||||
|
||||
Image makeImage(int w, int h, const glm::vec4& color); // colour 0..1
|
||||
glm::vec4 pixel(const Image& im, int x, int y); // 0..1
|
||||
// Smoothly between pixels (pixel centres at .5), edges held.
|
||||
glm::vec4 sample(const Image& im, glm::vec2 at);
|
||||
|
||||
// A brush stroke: paints over the picture as it was when the stroke began, so going over the same
|
||||
// place again in one stroke doesn't build up (as in GIMP).
|
||||
struct Stroke {
|
||||
Image base;
|
||||
std::vector<float> cover; // how much of the colour each pixel has had, this stroke
|
||||
glm::vec4 color{1.0f}; // a: how strong the stroke is
|
||||
void begin(const Image& im, const glm::vec4& c);
|
||||
// Paints `amount` (0..1) of the colour at a pixel.
|
||||
void put(Image& im, int x, int y, float amount);
|
||||
// A round dab, `radius` pixels, soft edge (hardness 1: none).
|
||||
void dab(Image& im, glm::vec2 centre, float radius, float hardness);
|
||||
};
|
||||
|
||||
// Fills the patch round a pixel whose colour is within `tolerance` (0..1) of it.
|
||||
void floodFill(Image& im, glm::ivec2 at, const glm::vec4& color, float tolerance);
|
||||
|
||||
// The perspective map from the unit square (0,0 top-left .. 1,1) to a four-cornered shape
|
||||
// (corners top-left, top-right, bottom-right, bottom-left): (u, v, 1) → (x·w, y·w, w).
|
||||
glm::mat3 squareToQuad(const std::array<glm::vec2, 4>& q);
|
||||
glm::vec2 mapSquare(const glm::mat3& m, glm::vec2 uv);
|
||||
// `src` painted into `dst` with its corners at these four pixel points: moved, stretched, turned
|
||||
// or bent in perspective, as the corners say.
|
||||
// owner: marks the pixels it shows on with `id` (which picture is on top where).
|
||||
void stampQuad(Image& dst, const Image& src, const std::array<glm::vec2, 4>& corners, float opacity = 1.0f, std::vector<uint8_t>* owner = nullptr, uint8_t id = 0);
|
||||
|
||||
} // namespace fry
|
||||
431
src/mesh.cpp
Normal file
431
src/mesh.cpp
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
#include "mesh.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
|
||||
namespace fry {
|
||||
|
||||
namespace {
|
||||
constexpr float kPi = 3.14159265358979f;
|
||||
|
||||
// Removes repeated neighbours from a face (and the last if it's the first again).
|
||||
void tidyFace(std::vector<int>& f) {
|
||||
f.erase(std::unique(f.begin(), f.end()), f.end());
|
||||
while (f.size() > 1 && f.front() == f.back()) f.pop_back();
|
||||
}
|
||||
|
||||
// Renumbers points through `to` (old → new, -1: gone) and tidies the faces, dropping any left
|
||||
// with fewer than three corners.
|
||||
void remapFaces(Mesh& m, const std::vector<int>& to) {
|
||||
std::vector<std::vector<int>> faces;
|
||||
for (auto f : m.faces) {
|
||||
bool gone = false;
|
||||
for (int& v : f) gone |= (v = to[size_t(v)]) < 0;
|
||||
tidyFace(f);
|
||||
if (!gone && f.size() >= 3) faces.push_back(std::move(f));
|
||||
}
|
||||
m.faces = std::move(faces);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ── Shapes ──
|
||||
|
||||
Mesh makeCube(float size) {
|
||||
const float h = size * 0.5f;
|
||||
Mesh m;
|
||||
for (int i = 0; i < 8; ++i) m.verts.emplace_back(i & 1 ? h : -h, i & 2 ? h : -h, i & 4 ? h : -h);
|
||||
m.faces = {{4, 5, 7, 6}, {0, 2, 3, 1}, {1, 3, 7, 5}, {0, 4, 6, 2}, {2, 6, 7, 3}, {0, 1, 5, 4}};
|
||||
return m;
|
||||
}
|
||||
|
||||
Mesh makePlane(float size) {
|
||||
const float h = size * 0.5f;
|
||||
return {{{-h, -h, 0}, {h, -h, 0}, {h, h, 0}, {-h, h, 0}}, {{0, 1, 2, 3}}};
|
||||
}
|
||||
|
||||
Mesh makeCylinder(int sides, float radius, float height) {
|
||||
sides = std::max(sides, 3);
|
||||
Mesh m;
|
||||
for (int z = 0; z < 2; ++z)
|
||||
for (int i = 0; i < sides; ++i) {
|
||||
const float a = 2 * kPi * float(i) / float(sides);
|
||||
m.verts.emplace_back(radius * std::cos(a), radius * std::sin(a), (z ? 0.5f : -0.5f) * height);
|
||||
}
|
||||
std::vector<int> top, bottom;
|
||||
for (int i = 0; i < sides; ++i) {
|
||||
const int j = (i + 1) % sides;
|
||||
m.faces.push_back({i, j, sides + j, sides + i});
|
||||
top.push_back(sides + i), bottom.push_back(sides - 1 - i);
|
||||
}
|
||||
m.faces.push_back(top), m.faces.push_back(bottom);
|
||||
return m;
|
||||
}
|
||||
|
||||
Mesh makeCone(int sides, float radius, float height) {
|
||||
sides = std::max(sides, 3);
|
||||
Mesh m;
|
||||
for (int i = 0; i < sides; ++i) {
|
||||
const float a = 2 * kPi * float(i) / float(sides);
|
||||
m.verts.emplace_back(radius * std::cos(a), radius * std::sin(a), -0.5f * height);
|
||||
}
|
||||
m.verts.emplace_back(0, 0, 0.5f * height);
|
||||
std::vector<int> bottom;
|
||||
for (int i = 0; i < sides; ++i) m.faces.push_back({i, (i + 1) % sides, sides}), bottom.push_back(sides - 1 - i);
|
||||
m.faces.push_back(bottom);
|
||||
return m;
|
||||
}
|
||||
|
||||
Mesh makeSphere(int segments, int rings, float radius) {
|
||||
segments = std::max(segments, 3), rings = std::max(rings, 2);
|
||||
Mesh m;
|
||||
m.verts.emplace_back(0, 0, radius); // the top
|
||||
for (int k = 1; k < rings; ++k) {
|
||||
const float t = kPi * float(k) / float(rings);
|
||||
for (int i = 0; i < segments; ++i) {
|
||||
const float a = 2 * kPi * float(i) / float(segments);
|
||||
m.verts.emplace_back(radius * std::sin(t) * std::cos(a), radius * std::sin(t) * std::sin(a), radius * std::cos(t));
|
||||
}
|
||||
}
|
||||
const int bottom = int(m.verts.size());
|
||||
m.verts.emplace_back(0, 0, -radius);
|
||||
auto at = [&](int k, int i) { return 1 + (k - 1) * segments + (i % segments); };
|
||||
for (int i = 0; i < segments; ++i) {
|
||||
m.faces.push_back({0, at(1, i), at(1, i + 1)});
|
||||
for (int k = 1; k + 1 < rings; ++k) m.faces.push_back({at(k, i), at(k + 1, i), at(k + 1, i + 1), at(k, i + 1)});
|
||||
m.faces.push_back({at(rings - 1, i), bottom, at(rings - 1, i + 1)});
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
Mesh makeTorus(int segments, int sides, float radius, float tube) {
|
||||
segments = std::max(segments, 3), sides = std::max(sides, 3);
|
||||
Mesh m;
|
||||
for (int u = 0; u < segments; ++u)
|
||||
for (int v = 0; v < sides; ++v) {
|
||||
const float a = 2 * kPi * float(u) / float(segments), b = 2 * kPi * float(v) / float(sides);
|
||||
const float r = radius + tube * std::cos(b);
|
||||
m.verts.emplace_back(r * std::cos(a), r * std::sin(a), tube * std::sin(b));
|
||||
}
|
||||
auto at = [&](int u, int v) { return (u % segments) * sides + (v % sides); };
|
||||
for (int u = 0; u < segments; ++u)
|
||||
for (int v = 0; v < sides; ++v) m.faces.push_back({at(u, v), at(u + 1, v), at(u + 1, v + 1), at(u, v + 1)});
|
||||
return m;
|
||||
}
|
||||
|
||||
Mesh extrudeOutline(const std::vector<glm::vec3>& outline, const glm::vec3& planeNormal, float thickness) {
|
||||
std::vector<glm::vec3> pts;
|
||||
for (const glm::vec3& p : outline)
|
||||
if (pts.empty() || glm::distance(pts.back(), p) > 1e-5f) pts.push_back(p);
|
||||
while (pts.size() > 1 && glm::distance(pts.front(), pts.back()) < 1e-5f) pts.pop_back();
|
||||
Mesh m;
|
||||
if (pts.size() < 3) return m;
|
||||
const glm::vec3 n = glm::normalize(planeNormal);
|
||||
Mesh flat{pts, {std::vector<int>(pts.size())}};
|
||||
std::iota(flat.faces[0].begin(), flat.faces[0].end(), 0);
|
||||
if (glm::dot(faceNormal(flat, 0), n) < 0.0f) std::reverse(pts.begin(), pts.end()); // counter-clockwise round n
|
||||
const int c = int(pts.size());
|
||||
const glm::vec3 half = n * (thickness * 0.5f);
|
||||
for (const glm::vec3& p : pts) m.verts.push_back(p + half);
|
||||
for (const glm::vec3& p : pts) m.verts.push_back(p - half);
|
||||
std::vector<int> front(static_cast<size_t>(c)), back(static_cast<size_t>(c));
|
||||
for (int i = 0; i < c; ++i) front[size_t(i)] = i, back[size_t(i)] = c + (c - 1 - i);
|
||||
m.faces.push_back(front);
|
||||
if (thickness > 0.0f) {
|
||||
m.faces.push_back(back);
|
||||
for (int i = 0; i < c; ++i) {
|
||||
const int j = (i + 1) % c;
|
||||
m.faces.push_back({c + i, c + j, j, i});
|
||||
}
|
||||
} else {
|
||||
m.verts.resize(size_t(c)); // (no thickness: just the flat shape)
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
// ── Looking at it ──
|
||||
|
||||
glm::vec3 faceNormal(const Mesh& m, int face) {
|
||||
const auto& f = m.faces[size_t(face)];
|
||||
glm::vec3 n(0.0f);
|
||||
for (size_t i = 0; i < f.size(); ++i) {
|
||||
const glm::vec3 &a = m.verts[size_t(f[i])], &b = m.verts[size_t(f[(i + 1) % f.size()])];
|
||||
n += glm::vec3((a.y - b.y) * (a.z + b.z), (a.z - b.z) * (a.x + b.x), (a.x - b.x) * (a.y + b.y));
|
||||
}
|
||||
const float l = glm::length(n);
|
||||
return l > 1e-12f ? n / l : glm::vec3(0, 0, 1);
|
||||
}
|
||||
|
||||
glm::vec3 faceCenter(const Mesh& m, int face) {
|
||||
glm::vec3 c(0.0f);
|
||||
for (int v : m.faces[size_t(face)]) c += m.verts[size_t(v)];
|
||||
return c / float(std::max<size_t>(m.faces[size_t(face)].size(), 1));
|
||||
}
|
||||
|
||||
float faceArea(const Mesh& m, int face) {
|
||||
const auto& f = m.faces[size_t(face)];
|
||||
glm::vec3 n(0.0f);
|
||||
for (size_t i = 0; i < f.size(); ++i) n += glm::cross(m.verts[size_t(f[i])], m.verts[size_t(f[(i + 1) % f.size()])]);
|
||||
return glm::length(n) * 0.5f;
|
||||
}
|
||||
|
||||
std::vector<std::array<int, 3>> triangulate(const Mesh& m, int face) {
|
||||
const auto& f = m.faces[size_t(face)];
|
||||
std::vector<std::array<int, 3>> out;
|
||||
if (f.size() < 3) return out;
|
||||
if (f.size() == 3) return {{f[0], f[1], f[2]}};
|
||||
// Onto the face's plane (u × v = its normal, so counter-clockwise stays so), then cut off
|
||||
// ears: corners that bulge out with nothing of the face inside them.
|
||||
const glm::vec3 n = faceNormal(m, face);
|
||||
const glm::vec3 u = glm::normalize(std::abs(n.z) < 0.9f ? glm::cross(glm::vec3(0, 0, 1), n) : glm::cross(glm::vec3(1, 0, 0), n));
|
||||
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));
|
||||
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);
|
||||
while (left.size() > 3) {
|
||||
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
|
||||
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) {
|
||||
inside = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (inside) continue;
|
||||
out.push_back({f[size_t(a)], f[size_t(b)], f[size_t(c)]});
|
||||
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;
|
||||
}
|
||||
}
|
||||
out.push_back({f[size_t(left[0])], f[size_t(left[1])], f[size_t(left[2])]});
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::pair<int, int>> edges(const Mesh& m) {
|
||||
std::set<std::pair<int, int>> s;
|
||||
for (const auto& f : m.faces)
|
||||
for (size_t i = 0; i < f.size(); ++i) {
|
||||
const int a = f[i], b = f[(i + 1) % f.size()];
|
||||
s.insert({std::min(a, b), std::max(a, b)});
|
||||
}
|
||||
return {s.begin(), s.end()};
|
||||
}
|
||||
|
||||
std::vector<std::vector<glm::vec3>> cornerNormals(const Mesh& m, bool smooth, float angle) {
|
||||
std::vector<glm::vec3> fn(m.faces.size());
|
||||
std::vector<float> area(m.faces.size());
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) fn[f] = faceNormal(m, int(f)), area[f] = faceArea(m, int(f));
|
||||
std::vector<std::vector<glm::vec3>> out(m.faces.size());
|
||||
std::vector<std::vector<int>> round(m.verts.size());
|
||||
if (smooth)
|
||||
for (size_t f = 0; f < m.faces.size(); ++f)
|
||||
for (int v : m.faces[f]) round[size_t(v)].push_back(int(f));
|
||||
const float minDot = std::cos(glm::radians(angle));
|
||||
for (size_t f = 0; f < m.faces.size(); ++f)
|
||||
for (int v : m.faces[f]) {
|
||||
glm::vec3 n = fn[f];
|
||||
if (smooth) {
|
||||
n = glm::vec3(0.0f);
|
||||
for (int g : round[size_t(v)])
|
||||
if (glm::dot(fn[f], fn[size_t(g)]) >= minDot - 1e-4f) n += fn[size_t(g)] * std::max(area[size_t(g)], 1e-8f);
|
||||
n = glm::length(n) > 1e-12f ? glm::normalize(n) : fn[f];
|
||||
}
|
||||
out[f].push_back(n);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
glm::vec3 boundsMin(const Mesh& m) {
|
||||
glm::vec3 lo(1e30f);
|
||||
for (const glm::vec3& v : m.verts) lo = glm::min(lo, v);
|
||||
return m.verts.empty() ? glm::vec3(0.0f) : lo;
|
||||
}
|
||||
|
||||
glm::vec3 boundsMax(const Mesh& m) {
|
||||
glm::vec3 hi(-1e30f);
|
||||
for (const glm::vec3& v : m.verts) hi = glm::max(hi, v);
|
||||
return m.verts.empty() ? glm::vec3(0.0f) : hi;
|
||||
}
|
||||
|
||||
// ── Changing it ──
|
||||
|
||||
std::vector<int> extrudeFaces(Mesh& m, const std::set<int>& faces, float distance) {
|
||||
std::set<std::pair<int, int>> inSel;
|
||||
std::map<int, glm::vec3> along; // each point of the piece: which way it moves
|
||||
glm::vec3 region(0.0f);
|
||||
for (int f : faces) {
|
||||
const auto& fv = m.faces[size_t(f)];
|
||||
const glm::vec3 n = faceNormal(m, f);
|
||||
region += n * std::max(faceArea(m, f), 1e-8f);
|
||||
for (size_t i = 0; i < fv.size(); ++i) {
|
||||
inSel.insert({fv[i], fv[(i + 1) % fv.size()]});
|
||||
along[fv[i]] += n;
|
||||
}
|
||||
}
|
||||
// One flat piece moves as one, along its normal; a bent one, each point along its own.
|
||||
region = glm::length(region) > 1e-12f ? glm::normalize(region) : glm::vec3(0, 0, 1);
|
||||
bool flat = true;
|
||||
for (int f : faces) flat &= glm::dot(faceNormal(m, f), region) > 0.99f;
|
||||
std::map<int, int> moved;
|
||||
std::vector<int> made;
|
||||
for (auto& [v, n] : along) {
|
||||
const glm::vec3 dir = flat ? region : glm::normalize(n);
|
||||
moved[v] = int(m.verts.size());
|
||||
made.push_back(moved[v]);
|
||||
m.verts.push_back(m.verts[size_t(v)] + dir * distance);
|
||||
}
|
||||
for (int f : faces)
|
||||
for (int& v : m.faces[size_t(f)]) v = moved[v];
|
||||
for (auto [a, b] : inSel) // the piece's outline: a wall under each of its edges
|
||||
if (!inSel.count({b, a})) m.faces.push_back({a, b, moved[b], moved[a]});
|
||||
return made;
|
||||
}
|
||||
|
||||
void insetFaces(Mesh& m, const std::set<int>& faces, float amount) {
|
||||
for (int f : faces) {
|
||||
std::vector<int> outer = m.faces[size_t(f)];
|
||||
const glm::vec3 c = faceCenter(m, f);
|
||||
std::vector<int> inner;
|
||||
for (int v : outer) {
|
||||
inner.push_back(int(m.verts.size()));
|
||||
m.verts.push_back(glm::mix(m.verts[size_t(v)], c, amount));
|
||||
}
|
||||
m.faces[size_t(f)] = inner;
|
||||
for (size_t i = 0; i < outer.size(); ++i) {
|
||||
const size_t j = (i + 1) % outer.size();
|
||||
m.faces.push_back({outer[i], outer[j], inner[j], inner[i]});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void flipFaces(Mesh& m, const std::set<int>& faces) {
|
||||
for (int f : faces) std::reverse(m.faces[size_t(f)].begin(), m.faces[size_t(f)].end());
|
||||
}
|
||||
|
||||
void deleteFaces(Mesh& m, const std::set<int>& faces) {
|
||||
std::vector<std::vector<int>> keep;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f)
|
||||
if (!faces.count(int(f))) keep.push_back(m.faces[f]);
|
||||
m.faces = std::move(keep);
|
||||
removeUnused(m);
|
||||
}
|
||||
|
||||
void deleteVerts(Mesh& m, const std::set<int>& verts) {
|
||||
std::vector<int> to(m.verts.size());
|
||||
std::vector<glm::vec3> kept;
|
||||
for (size_t v = 0; v < m.verts.size(); ++v) {
|
||||
to[v] = verts.count(int(v)) ? -1 : int(kept.size());
|
||||
if (to[v] >= 0) kept.push_back(m.verts[v]);
|
||||
}
|
||||
remapFaces(m, to);
|
||||
m.verts = std::move(kept);
|
||||
}
|
||||
|
||||
bool makeFace(Mesh& m, const std::vector<int>& verts, const glm::vec3& towards) {
|
||||
if (verts.size() < 3) return false;
|
||||
// Round their middle, in order, on the plane they lie on.
|
||||
glm::vec3 c(0.0f);
|
||||
for (int v : verts) c += m.verts[size_t(v)];
|
||||
c /= float(verts.size());
|
||||
glm::vec3 n(0.0f);
|
||||
for (size_t i = 0; i < verts.size(); ++i)
|
||||
for (size_t j = i + 1; j < verts.size(); ++j) {
|
||||
glm::vec3 k = glm::cross(m.verts[size_t(verts[i])] - c, m.verts[size_t(verts[j])] - c);
|
||||
if (glm::dot(k, n) < 0.0f) k = -k;
|
||||
n += k;
|
||||
}
|
||||
if (glm::length(n) < 1e-12f) return false; // (all in a line)
|
||||
n = glm::normalize(n);
|
||||
if (glm::dot(n, towards) < 0.0f) n = -n;
|
||||
const glm::vec3 u = glm::normalize(std::abs(n.z) < 0.9f ? glm::cross(glm::vec3(0, 0, 1), n) : glm::cross(glm::vec3(1, 0, 0), n));
|
||||
const glm::vec3 w = glm::cross(n, u);
|
||||
std::vector<int> f = verts;
|
||||
std::sort(f.begin(), f.end(), [&](int a, int b) {
|
||||
const glm::vec3 pa = m.verts[size_t(a)] - c, pb = m.verts[size_t(b)] - c;
|
||||
return std::atan2(glm::dot(pa, w), glm::dot(pa, u)) < std::atan2(glm::dot(pb, w), glm::dot(pb, u));
|
||||
});
|
||||
m.faces.push_back(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
int mergeByDistance(Mesh& m, float distance) {
|
||||
std::vector<int> to(m.verts.size(), -1);
|
||||
std::vector<glm::vec3> kept;
|
||||
for (size_t v = 0; v < m.verts.size(); ++v) {
|
||||
for (size_t k = 0; k < kept.size(); ++k)
|
||||
if (glm::distance(kept[k], m.verts[v]) <= distance) {
|
||||
to[v] = int(k);
|
||||
break;
|
||||
}
|
||||
if (to[v] < 0) to[v] = int(kept.size()), kept.push_back(m.verts[v]);
|
||||
}
|
||||
const int gone = int(m.verts.size() - kept.size());
|
||||
remapFaces(m, to);
|
||||
m.verts = std::move(kept);
|
||||
return gone;
|
||||
}
|
||||
|
||||
void mergeToCenter(Mesh& m, const std::set<int>& verts) {
|
||||
if (verts.size() < 2) return;
|
||||
glm::vec3 c(0.0f);
|
||||
for (int v : verts) c += m.verts[size_t(v)];
|
||||
c /= float(verts.size());
|
||||
std::vector<int> to(m.verts.size());
|
||||
std::iota(to.begin(), to.end(), 0);
|
||||
const int first = *verts.begin();
|
||||
m.verts[size_t(first)] = c;
|
||||
for (int v : verts) to[size_t(v)] = first;
|
||||
remapFaces(m, to);
|
||||
removeUnused(m);
|
||||
}
|
||||
|
||||
void removeUnused(Mesh& m) {
|
||||
std::vector<bool> used(m.verts.size());
|
||||
for (const auto& f : m.faces)
|
||||
for (int v : f) used[size_t(v)] = true;
|
||||
std::vector<int> to(m.verts.size(), -1);
|
||||
std::vector<glm::vec3> kept;
|
||||
for (size_t v = 0; v < m.verts.size(); ++v)
|
||||
if (used[v]) to[v] = int(kept.size()), kept.push_back(m.verts[v]);
|
||||
remapFaces(m, to);
|
||||
m.verts = std::move(kept);
|
||||
}
|
||||
|
||||
void transform(Mesh& m, const glm::mat4& matrix) {
|
||||
for (glm::vec3& v : m.verts) v = glm::vec3(matrix * glm::vec4(v, 1.0f));
|
||||
if (glm::determinant(glm::mat3(matrix)) < 0.0f) // (mirrored: keep the faces facing out)
|
||||
for (auto& f : m.faces) std::reverse(f.begin(), f.end());
|
||||
}
|
||||
|
||||
void append(Mesh& m, const Mesh& other, const glm::mat4& matrix) {
|
||||
const int base = int(m.verts.size());
|
||||
for (const glm::vec3& v : other.verts) m.verts.push_back(glm::vec3(matrix * glm::vec4(v, 1.0f)));
|
||||
const bool flip = glm::determinant(glm::mat3(matrix)) < 0.0f;
|
||||
for (auto f : other.faces) {
|
||||
for (int& v : f) v += base;
|
||||
if (flip) std::reverse(f.begin(), f.end());
|
||||
m.faces.push_back(std::move(f));
|
||||
}
|
||||
}
|
||||
|
||||
Mesh makeFrustum(int sides, float bottom, float top, float height) {
|
||||
if (top <= 1e-6f) return makeCone(sides, bottom, height);
|
||||
Mesh m = makeCylinder(sides, bottom, height);
|
||||
for (glm::vec3& v : m.verts)
|
||||
if (v.z > 0) v.x *= top / bottom, v.y *= top / bottom;
|
||||
return m;
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
171
src/mesh.h
Normal file
171
src/mesh.h
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
#pragma once
|
||||
// Meshes as people think of them: points, and faces round them (any number of corners, flat or
|
||||
// nearly). Fry3DX's space: metres, Z up, X forward (the model's front looks along +X), Y left —
|
||||
// the same as glTF's once turned, so what you make faces the same way everywhere.
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fry {
|
||||
|
||||
struct Mesh {
|
||||
std::vector<glm::vec3> verts;
|
||||
std::vector<std::vector<int>> faces; // corners counter-clockwise, seen from outside
|
||||
};
|
||||
|
||||
// ── Shapes to start from (centred on the origin) ──
|
||||
Mesh makeCube(float size = 1.0f);
|
||||
Mesh makePlane(float size = 1.0f);
|
||||
Mesh makeCylinder(int sides = 24, float radius = 0.5f, float height = 1.0f);
|
||||
Mesh makeCone(int sides = 24, float radius = 0.5f, float height = 1.0f);
|
||||
// A cylinder narrower at the top (a cone if `top` is 0).
|
||||
Mesh makeFrustum(int sides, float bottom, float top, float height);
|
||||
Mesh makeSphere(int segments = 24, int rings = 12, float radius = 0.5f);
|
||||
Mesh makeTorus(int segments = 32, int sides = 12, float radius = 0.5f, float tube = 0.15f);
|
||||
// An outline (points round it, on one plane, either way round) made solid: a front and a back
|
||||
// face `thickness` apart, centred on the outline's plane, and the sides between them.
|
||||
Mesh extrudeOutline(const std::vector<glm::vec3>& outline, const glm::vec3& planeNormal, float thickness);
|
||||
|
||||
// ── Looking at it ──
|
||||
glm::vec3 faceNormal(const Mesh& m, int face); // unit (Newell's: fine for faces a little bent)
|
||||
glm::vec3 faceCenter(const Mesh& m, int face);
|
||||
float faceArea(const Mesh& m, int face);
|
||||
// Triangles covering a face (ear clipping: works for L shapes and other dents), as corners of
|
||||
// the mesh, counter-clockwise like the face.
|
||||
std::vector<std::array<int, 3>> triangulate(const Mesh& m, int face);
|
||||
// Each edge once, lower index first.
|
||||
std::vector<std::pair<int, int>> edges(const Mesh& m);
|
||||
// Every face corner's normal: flat (its face's), or smooth — averaged with the faces round the
|
||||
// point that meet it at less than `angle` degrees (sharp creases stay sharp).
|
||||
std::vector<std::vector<glm::vec3>> cornerNormals(const Mesh& m, bool smooth, float angle);
|
||||
glm::vec3 boundsMin(const Mesh& m);
|
||||
glm::vec3 boundsMax(const Mesh& m);
|
||||
|
||||
// ── Changing it ──
|
||||
// Pulls the faces out `distance` along their normals, as one piece, with walls round the edge
|
||||
// of the piece (the faces keep their numbers; returns the new points).
|
||||
std::vector<int> extrudeFaces(Mesh& m, const std::set<int>& faces, float distance);
|
||||
// Shrinks each face towards its middle by `amount` (0..1 of the way), leaving a ring of faces.
|
||||
void insetFaces(Mesh& m, const std::set<int>& faces, float amount);
|
||||
void flipFaces(Mesh& m, const std::set<int>& faces);
|
||||
void deleteFaces(Mesh& m, const std::set<int>& faces);
|
||||
// Deletes points and every face that uses one.
|
||||
void deleteVerts(Mesh& m, const std::set<int>& verts);
|
||||
// A new face through the points, in the order given (made to face the camera's way if `towards`
|
||||
// isn't zero). False if fewer than 3.
|
||||
bool makeFace(Mesh& m, const std::vector<int>& verts, const glm::vec3& towards = glm::vec3(0.0f));
|
||||
// Points closer than `distance` become one (the first). Returns how many went.
|
||||
int mergeByDistance(Mesh& m, float distance);
|
||||
// Every point to their middle (merging them).
|
||||
void mergeToCenter(Mesh& m, const std::set<int>& verts);
|
||||
// Drops points no face uses, renumbering the rest.
|
||||
void removeUnused(Mesh& m);
|
||||
void transform(Mesh& m, const glm::mat4& matrix);
|
||||
// Adds another mesh's points and faces (moved by `matrix`).
|
||||
void append(Mesh& m, const Mesh& other, const glm::mat4& matrix = glm::mat4(1.0f));
|
||||
|
||||
// ── More tools (tools.cpp) ──
|
||||
// Rounds (or with 1 segment, cuts off) edges: each becomes a strip `width` wide across, curved
|
||||
// in `segments` steps, with rounded corners where several meet. Edges lower index first; edges
|
||||
// on a hole's rim are left alone.
|
||||
void bevelEdges(Mesh& m, const std::set<std::pair<int, int>>& edges, float width, int segments);
|
||||
// Edges where the faces meet sharper than `angle` degrees.
|
||||
std::set<std::pair<int, int>> sharpEdges(const Mesh& m, float angle);
|
||||
// Every face cut into four-sided pieces; `smooth` rounds it (Catmull–Clark) — edges sharper
|
||||
// than `keepAngle` degrees stay sharp (180: none do).
|
||||
void subdivide(Mesh& m, bool smooth, float keepAngle = 180.0f);
|
||||
// The ring of four-sided faces across an edge, cut round the middle `cuts` times. Returns the new
|
||||
// points (empty if the edge has no four-sided face).
|
||||
std::vector<int> loopCut(Mesh& m, std::pair<int, int> edge, int cuts = 1);
|
||||
// The ring of faces a loop cut through `edge` would cut (for showing it before you click).
|
||||
std::vector<int> faceRing(const Mesh& m, std::pair<int, int> edge);
|
||||
// The same faces' corners, each starting with the edge the ring comes in through: the cut runs
|
||||
// from corners 0–1 across to corners 3–2.
|
||||
std::vector<std::array<int, 4>> ringQuads(const Mesh& m, std::pair<int, int> edge);
|
||||
// The edges across that ring (each quad's way in and way out, `edge` too): a ring of edges.
|
||||
std::set<std::pair<int, int>> edgeRing(const Mesh& m, std::pair<int, int> edge);
|
||||
// The line of edges an edge continues into, through points with four edges (edges lower first).
|
||||
std::set<std::pair<int, int>> edgeLoop(const Mesh& m, std::pair<int, int> edge);
|
||||
// ── Cutting (cut.cpp) ──
|
||||
// The knife: a path drawn on the screen, seen from `eye` (all in the mesh's own space; only the
|
||||
// way from the eye to each point matters). Each step of the path sweeps a wedge out from the
|
||||
// eye, and every face it passes over is split along it, edge to edge. `through`: every face in
|
||||
// its way, front and back (a cut all the way through); else only faces the eye can see.
|
||||
// Returns the new edges.
|
||||
std::set<std::pair<int, int>> knifeCut(Mesh& m, const glm::vec3& eye, const std::vector<glm::vec3>& path, bool through);
|
||||
// Pulls a mesh apart along edges that go all the way round it: each piece gets its own points
|
||||
// there, and with `fill` a face closing each opening. Returns each piece's faces (one piece if
|
||||
// the edges don't part it).
|
||||
std::vector<std::set<int>> splitAlong(Mesh& m, const std::set<std::pair<int, int>>& edges, bool fill);
|
||||
// How much a closed piece holds (cubic metres).
|
||||
float volumeOf(const Mesh& m, const std::set<int>& faces);
|
||||
// A hole's outline on a face: `sides` points round `centre` (on the face's plane), `radius` out;
|
||||
// 4 sides make a square lined up with the face's longest edge. Counter-clockwise, seen from outside.
|
||||
std::vector<glm::vec3> holeOutline(const Mesh& m, int face, const glm::vec3& centre, float radius, int sides);
|
||||
// The hole tool's shapes, lined up with the face's longest edge, turned `turn` degrees more:
|
||||
// Round: `across` wide (`sides` points), Rectangle: `across` x `tall`, Triangle: sides `across`
|
||||
// long (a point up), Hexagon: `across` its flats (a nut's spanner size; flats top and bottom).
|
||||
enum class HoleShape { Round, Rectangle, Triangle, Hexagon };
|
||||
std::vector<glm::vec3> holeOutline(const Mesh& m, int face, const glm::vec3& centre, HoleShape shape, float across, float tall = 0.0f, int sides = 32,
|
||||
float turn = 0.0f);
|
||||
// Only the outline cut into the face (nothing taken away): the face becomes a ring round a new
|
||||
// face inside the outline, to extrude, bevel or paint on its own. The new face, or -1 if the
|
||||
// outline doesn't fit inside the face (nothing changed).
|
||||
int imprintOutline(Mesh& m, int face, const std::vector<glm::vec3>& outline);
|
||||
// Cuts a hole in a face along `outline` (inside the face). depth > 0: a pocket that deep with a
|
||||
// floor; depth <= 0: all the way through to the face behind (the outline must fit in that face
|
||||
// too). False if it doesn't fit (nothing changed).
|
||||
bool cutHole(Mesh& m, int face, const std::vector<glm::vec3>& outline, float depth);
|
||||
|
||||
// ── Pictures on it (uv.cpp) ──
|
||||
// Where each face corner sits on a picture (0..1 across and down): the shape is cut into
|
||||
// pieces that face about the same way, each laid flat, all packed in with `margin` round them.
|
||||
std::vector<std::vector<glm::vec2>> autoUnwrap(const Mesh& m, float margin = 0.015f);
|
||||
|
||||
// Every face joined to these by edges.
|
||||
std::set<int> linkedFaces(const Mesh& m, const std::set<int>& faces);
|
||||
std::set<int> linkedVerts(const Mesh& m, const std::set<int>& verts);
|
||||
// The points next to these (by an edge), and these.
|
||||
std::set<int> growVerts(const Mesh& m, const std::set<int>& verts);
|
||||
|
||||
// ── Whole shapes ──
|
||||
|
||||
// Faces next to each other lying flat together (normals within `angle` degrees) joined into
|
||||
// one where they make a single outline; how many faces went.
|
||||
int mergeFlatFaces(Mesh& m, float angle = 0.5f);
|
||||
// Made `thickness` thick: a surface gets a back and walls round its open edges; a closed shape
|
||||
// gets an inside (hollow, walls that thick). Negative: the new side outwards instead.
|
||||
void thicken(Mesh& m, float thickness);
|
||||
// Two shapes (in the same space) made one: `b` added to `a`, taken out of it, or only where they
|
||||
// overlap kept. The result goes in `a`; false if nothing's left.
|
||||
enum class Combine { Add, Subtract, Overlap };
|
||||
bool combine(Mesh& a, const Mesh& b, Combine op);
|
||||
|
||||
// ── For 3D printing ──
|
||||
|
||||
// What a slicer would mind: edges with a face on one side only (holes: not closed), edges with
|
||||
// more than two faces, neighbours facing opposite ways (one turned inside out), faces with no
|
||||
// area; and how much it holds, its surface, and how much of it hangs over the air (faces facing
|
||||
// down more steeply than `overhang` degrees from upright, not lying on the bed at bedZ).
|
||||
struct PrintCheck {
|
||||
int openEdges = 0, crowdedEdges = 0, flippedEdges = 0, tinyFaces = 0, pieces = 0;
|
||||
bool insideOut = false;
|
||||
float volume = 0, area = 0, overhangArea = 0; // (signed volume: < 0 when inside out)
|
||||
bool closed() const { return openEdges == 0 && crowdedEdges == 0 && flippedEdges == 0; }
|
||||
};
|
||||
PrintCheck checkForPrint(const Mesh& m, float overhang = 45.0f, float bedZ = 0.0f);
|
||||
bool overhangs(const Mesh& m, int face, float overhang, float bedZ);
|
||||
// Closes each hole with a face across it; how many.
|
||||
int closeHoles(Mesh& m);
|
||||
// Turns faces so each piece's all face the same way, outwards; how many were turned.
|
||||
int faceOutward(Mesh& m);
|
||||
|
||||
// ── Its picture's layout ──
|
||||
|
||||
// The frames of the layout: faces joined where they share an edge laid out the same on both.
|
||||
std::vector<std::vector<int>> uvIslands(const Mesh& m, const std::vector<std::vector<glm::vec2>>& uvs);
|
||||
|
||||
} // namespace fry
|
||||
107
src/modify.cpp
Normal file
107
src/modify.cpp
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// Whole-shape changes: flat faces joined up, and a surface made thick.
|
||||
#include "mesh.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
#include <set>
|
||||
|
||||
namespace fry {
|
||||
|
||||
int mergeFlatFaces(Mesh& m, float angle) {
|
||||
const float same = std::cos(glm::radians(angle));
|
||||
std::map<std::pair<int, int>, std::vector<int>> byEdge;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
const auto& fv = m.faces[f];
|
||||
for (size_t i = 0; i < fv.size(); ++i) byEdge[{std::min(fv[i], fv[(i + 1) % fv.size()]), std::max(fv[i], fv[(i + 1) % fv.size()])}].push_back(int(f));
|
||||
}
|
||||
std::vector<glm::vec3> normal(m.faces.size());
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) normal[f] = faceNormal(m, int(f));
|
||||
std::vector<int> root(m.faces.size());
|
||||
std::iota(root.begin(), root.end(), 0);
|
||||
auto find = [&](int f) {
|
||||
while (root[size_t(f)] != f) f = root[size_t(f)] = root[size_t(root[size_t(f)])];
|
||||
return f;
|
||||
};
|
||||
for (const auto& [e, fs] : byEdge)
|
||||
if (fs.size() == 2 && glm::dot(normal[size_t(fs[0])], normal[size_t(fs[1])]) > same &&
|
||||
std::abs(glm::dot(normal[size_t(fs[0])], faceCenter(m, fs[1]) - faceCenter(m, fs[0]))) < 1e-5f)
|
||||
root[size_t(find(fs[0]))] = find(fs[1]);
|
||||
std::map<int, std::vector<int>> groups;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) groups[find(int(f))].push_back(int(f));
|
||||
std::vector<std::vector<int>> out;
|
||||
int merged = 0;
|
||||
for (const auto& [r, fs] : groups) {
|
||||
if (fs.size() == 1) {
|
||||
out.push_back(m.faces[size_t(fs[0])]);
|
||||
continue;
|
||||
}
|
||||
// Its outline: the edges no other face of it has the other way round. One loop that
|
||||
// doesn't touch itself makes one face; otherwise the faces stay as they were.
|
||||
std::set<std::pair<int, int>> inside;
|
||||
for (int f : fs)
|
||||
for (size_t i = 0; i < m.faces[size_t(f)].size(); ++i) inside.insert({m.faces[size_t(f)][i], m.faces[size_t(f)][(i + 1) % m.faces[size_t(f)].size()]});
|
||||
std::map<int, int> next;
|
||||
bool ok = true;
|
||||
for (auto [a, b] : inside)
|
||||
if (!inside.count({b, a})) ok &= next.emplace(a, b).second;
|
||||
std::vector<int> loop;
|
||||
if (ok && !next.empty()) {
|
||||
int v = next.begin()->first;
|
||||
do {
|
||||
loop.push_back(v);
|
||||
v = next.count(v) ? next[v] : -1;
|
||||
} while (v >= 0 && v != loop.front() && loop.size() <= next.size());
|
||||
ok = v == loop.front() && loop.size() == next.size();
|
||||
}
|
||||
if (ok && loop.size() >= 3) out.push_back(loop), merged += int(fs.size()) - 1;
|
||||
else
|
||||
for (int f : fs) out.push_back(m.faces[size_t(f)]);
|
||||
}
|
||||
m.faces = out;
|
||||
return merged;
|
||||
}
|
||||
|
||||
void thicken(Mesh& m, float thickness) {
|
||||
const size_t n = m.verts.size();
|
||||
// Each point's way out: its faces' normals, weighted by their corners' angles; pushed out
|
||||
// further where faces meet at an angle, so the walls come out evenly thick.
|
||||
std::vector<glm::vec3> out(n, glm::vec3(0.0f));
|
||||
std::vector<std::vector<glm::vec3>> around(n);
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
const auto& fv = m.faces[f];
|
||||
const glm::vec3 fn = faceNormal(m, int(f));
|
||||
for (size_t i = 0; i < fv.size(); ++i) {
|
||||
const glm::vec3 p = m.verts[size_t(fv[i])], a = m.verts[size_t(fv[(i + fv.size() - 1) % fv.size()])], b = m.verts[size_t(fv[(i + 1) % fv.size()])];
|
||||
const float ang = std::acos(std::clamp(glm::dot(glm::normalize(a - p), glm::normalize(b - p)), -1.0f, 1.0f));
|
||||
out[size_t(fv[i])] += fn * ang;
|
||||
around[size_t(fv[i])].push_back(fn);
|
||||
}
|
||||
}
|
||||
for (size_t v = 0; v < n; ++v) {
|
||||
if (glm::length(out[v]) < 1e-12f) continue;
|
||||
out[v] = glm::normalize(out[v]);
|
||||
float c = 1.0f;
|
||||
for (const glm::vec3& fn : around[v]) c = std::min(c, glm::dot(fn, out[v]));
|
||||
out[v] /= std::max(c, 0.35f);
|
||||
}
|
||||
// The other side: the points moved in (thickness > 0: in, against the faces' way), its faces
|
||||
// turned round; then walls along the open edges.
|
||||
for (size_t v = 0; v < n; ++v) m.verts.push_back(m.verts[v] - out[v] * thickness);
|
||||
std::set<std::pair<int, int>> directed;
|
||||
const size_t faces = m.faces.size();
|
||||
for (size_t f = 0; f < faces; ++f)
|
||||
for (size_t i = 0; i < m.faces[f].size(); ++i) directed.insert({m.faces[f][i], m.faces[f][(i + 1) % m.faces[f].size()]});
|
||||
for (size_t f = 0; f < faces; ++f) {
|
||||
std::vector<int> back;
|
||||
for (auto it = m.faces[f].rbegin(); it != m.faces[f].rend(); ++it) back.push_back(*it + int(n));
|
||||
m.faces.push_back(back);
|
||||
}
|
||||
for (auto [a, b] : directed)
|
||||
if (!directed.count({b, a})) m.faces.push_back({b, a, a + int(n), b + int(n)});
|
||||
if (thickness < 0) // (made outwards: the new side is the outside)
|
||||
for (auto& f : m.faces) std::reverse(f.begin(), f.end());
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
169
src/package.cpp
Normal file
169
src/package.cpp
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
// Fry3DX's own file: the scene's text and its pictures, packed into one small file.
|
||||
// "FRY3DX\x1a\n", u32 version, u64 size unpacked, then one zstd frame of chunks:
|
||||
// u32 name length, name, u64 size, bytes. "scene": the scene as text. A picture: u32 w, u32 h,
|
||||
// then its R, G, B, A planes, each pixel as the difference from what its neighbours guess
|
||||
// (left, above, above-left: JPEG-LS's guess), which zstd packs far smaller than raw or PNG.
|
||||
#include "scene.h"
|
||||
|
||||
#include <zstd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
|
||||
namespace fry {
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[8] = {'F', 'R', 'Y', '3', 'D', 'X', '\x1a', '\n'};
|
||||
constexpr uint32_t kVersion = 1;
|
||||
|
||||
uint8_t guess(int a, int b, int c) { // a left, b above, c above-left
|
||||
if (c >= std::max(a, b)) return uint8_t(std::min(a, b));
|
||||
if (c <= std::min(a, b)) return uint8_t(std::max(a, b));
|
||||
return uint8_t(a + b - c);
|
||||
}
|
||||
|
||||
void packImage(std::string& out, const Image& im) {
|
||||
const uint32_t wh[2] = {uint32_t(im.w), uint32_t(im.h)};
|
||||
out.append(reinterpret_cast<const char*>(wh), 8);
|
||||
const size_t at = out.size(), n = size_t(im.w) * size_t(im.h);
|
||||
out.resize(at + n * 4);
|
||||
for (int ch = 0; ch < 4; ++ch) {
|
||||
char* plane = out.data() + at + n * size_t(ch);
|
||||
auto px = [&](int x, int y) { return x < 0 || y < 0 ? 0 : int(im.px[(size_t(y) * size_t(im.w) + size_t(x)) * 4 + size_t(ch)]); };
|
||||
for (int y = 0; y < im.h; ++y)
|
||||
for (int x = 0; x < im.w; ++x)
|
||||
plane[size_t(y) * size_t(im.w) + size_t(x)] = char(uint8_t(px(x, y) - guess(px(x - 1, y), px(x, y - 1), px(x - 1, y - 1))));
|
||||
}
|
||||
}
|
||||
|
||||
bool unpackImage(const std::string& in, Image& im) {
|
||||
if (in.size() < 8) return false;
|
||||
uint32_t wh[2];
|
||||
std::memcpy(wh, in.data(), 8);
|
||||
const size_t n = size_t(wh[0]) * size_t(wh[1]);
|
||||
if (!wh[0] || !wh[1] || wh[0] > 32768 || wh[1] > 32768 || in.size() != 8 + n * 4) return false;
|
||||
im.w = int(wh[0]), im.h = int(wh[1]), im.px.assign(n * 4, 0);
|
||||
for (int ch = 0; ch < 4; ++ch) {
|
||||
const char* plane = in.data() + 8 + n * size_t(ch);
|
||||
auto px = [&](int x, int y) { return x < 0 || y < 0 ? 0 : int(im.px[(size_t(y) * size_t(im.w) + size_t(x)) * 4 + size_t(ch)]); };
|
||||
for (int y = 0; y < im.h; ++y)
|
||||
for (int x = 0; x < im.w; ++x)
|
||||
im.px[(size_t(y) * size_t(im.w) + size_t(x)) * 4 + size_t(ch)] =
|
||||
uint8_t(uint8_t(plane[size_t(y) * size_t(im.w) + size_t(x)]) + guess(px(x - 1, y), px(x, y - 1), px(x - 1, y - 1)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void chunk(std::string& out, const std::string& name, const std::string& data) {
|
||||
const uint32_t n = uint32_t(name.size());
|
||||
const uint64_t size = data.size();
|
||||
out.append(reinterpret_cast<const char*>(&n), 4), out += name;
|
||||
out.append(reinterpret_cast<const char*>(&size), 8), out += data;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool isPackage(const std::string& path) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
char head[8] = {};
|
||||
return f.read(head, 8) && std::memcmp(head, kMagic, 8) == 0;
|
||||
}
|
||||
|
||||
bool savePackage(const std::string& path, const Scene& scene, int level, std::string* error) {
|
||||
Scene s = scene;
|
||||
std::string payload;
|
||||
for (size_t i = 0; i < s.objects.size(); ++i) {
|
||||
Object& o = s.objects[i];
|
||||
o.pictureFile.clear();
|
||||
if (o.picture) {
|
||||
o.pictureFile = std::format("picture {}", i);
|
||||
std::string px;
|
||||
packImage(px, *o.picture);
|
||||
chunk(payload, o.pictureFile, px);
|
||||
}
|
||||
for (size_t j = 0; j < o.layers.size(); ++j) {
|
||||
PictureLayer& l = o.layers[j];
|
||||
l.file.clear();
|
||||
if (!l.image) continue;
|
||||
l.file = std::format("layer {} {}", i, j);
|
||||
std::string px;
|
||||
packImage(px, *l.image);
|
||||
chunk(payload, l.file, px);
|
||||
}
|
||||
}
|
||||
chunk(payload, "scene", sceneText(s));
|
||||
|
||||
ZSTD_CCtx* cx = ZSTD_createCCtx();
|
||||
ZSTD_CCtx_setParameter(cx, ZSTD_c_compressionLevel, level);
|
||||
ZSTD_CCtx_setParameter(cx, ZSTD_c_checksumFlag, 1);
|
||||
if (level >= 15) ZSTD_CCtx_setParameter(cx, ZSTD_c_enableLongDistanceMatching, 1);
|
||||
std::string out(20 + ZSTD_compressBound(payload.size()), '\0');
|
||||
const uint32_t lo = uint32_t(payload.size()), hi = uint32_t(uint64_t(payload.size()) >> 32); // (the u64 as two halves)
|
||||
std::memcpy(out.data(), kMagic, 8), std::memcpy(out.data() + 8, &kVersion, 4), std::memcpy(out.data() + 12, &lo, 4), std::memcpy(out.data() + 16, &hi, 4);
|
||||
const size_t packed = ZSTD_compress2(cx, out.data() + 20, out.size() - 20, payload.data(), payload.size());
|
||||
ZSTD_freeCCtx(cx);
|
||||
if (ZSTD_isError(packed)) {
|
||||
if (error) *error = ZSTD_getErrorName(packed);
|
||||
return false;
|
||||
}
|
||||
return writeFileSafely(path, out.data(), 20 + packed, error);
|
||||
}
|
||||
|
||||
std::optional<Scene> loadPackage(const std::string& path, std::string* error) {
|
||||
auto fail = [&](const std::string& why) {
|
||||
if (error) *error = why;
|
||||
return std::nullopt;
|
||||
};
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f) return fail("can't open " + path);
|
||||
std::string file((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
if (file.size() < 20 || std::memcmp(file.data(), kMagic, 8) != 0) {
|
||||
std::istringstream text(file); // (the old plain-text .fry)
|
||||
return parseScene(text, path, error);
|
||||
}
|
||||
uint32_t version = 0, lo = 0, hi = 0;
|
||||
std::memcpy(&version, file.data() + 8, 4), std::memcpy(&lo, file.data() + 12, 4), std::memcpy(&hi, file.data() + 16, 4);
|
||||
if (version > kVersion) return fail("it was saved by a newer Fry3DX");
|
||||
const uint64_t size = uint64_t(lo) | uint64_t(hi) << 32;
|
||||
if (size > (uint64_t(4) << 30)) return fail("it's damaged");
|
||||
std::string payload(size, '\0');
|
||||
ZSTD_DCtx* dx = ZSTD_createDCtx();
|
||||
ZSTD_DCtx_setParameter(dx, ZSTD_d_windowLogMax, 31);
|
||||
const size_t got = ZSTD_decompressDCtx(dx, payload.data(), payload.size(), file.data() + 20, file.size() - 20);
|
||||
ZSTD_freeDCtx(dx);
|
||||
if (ZSTD_isError(got) || got != size) return fail("it's damaged (" + std::string(ZSTD_isError(got) ? ZSTD_getErrorName(got) : "too short") + ")");
|
||||
std::map<std::string, std::string> chunks;
|
||||
for (size_t at = 0; at + 4 <= payload.size();) {
|
||||
uint32_t n = 0;
|
||||
uint64_t len = 0;
|
||||
std::memcpy(&n, payload.data() + at, 4);
|
||||
if (at + 4 + n + 8 > payload.size()) return fail("it's damaged");
|
||||
std::string name = payload.substr(at + 4, n);
|
||||
std::memcpy(&len, payload.data() + at + 4 + n, 8);
|
||||
at += 4 + n + 8;
|
||||
if (at + len > payload.size()) return fail("it's damaged");
|
||||
chunks[name] = payload.substr(at, len);
|
||||
at += len;
|
||||
}
|
||||
std::istringstream text(chunks["scene"]);
|
||||
auto s = parseScene(text, path, error);
|
||||
if (!s) return s;
|
||||
for (Object& o : s->objects) {
|
||||
Image im;
|
||||
if (!o.pictureFile.empty() && unpackImage(chunks[o.pictureFile], im)) o.picture = std::make_shared<Image>(std::move(im));
|
||||
o.pictureFile.clear();
|
||||
for (PictureLayer& l : o.layers) {
|
||||
Image li;
|
||||
if (!l.file.empty() && unpackImage(chunks[l.file], li)) l.image = std::make_shared<Image>(std::move(li));
|
||||
l.file.clear();
|
||||
}
|
||||
std::erase_if(o.layers, [](const PictureLayer& l) { return !l.image; });
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
122
src/print.cpp
Normal file
122
src/print.cpp
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
// Checking and fixing models for 3D printing.
|
||||
#include "mesh.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
namespace fry {
|
||||
|
||||
bool overhangs(const Mesh& m, int face, float overhang, float bedZ) {
|
||||
if (faceNormal(m, face).z > -std::sin(glm::radians(90.0f - overhang))) return false;
|
||||
for (int v : m.faces[size_t(face)])
|
||||
if (m.verts[size_t(v)].z > bedZ + 1e-4f) return true;
|
||||
return false; // (lying on the bed)
|
||||
}
|
||||
|
||||
PrintCheck checkForPrint(const Mesh& m, float overhang, float bedZ) {
|
||||
PrintCheck c;
|
||||
std::map<std::pair<int, int>, int> directed; // (how many faces go a -> b)
|
||||
for (const auto& f : m.faces)
|
||||
for (size_t i = 0; i < f.size(); ++i) ++directed[{f[i], f[(i + 1) % f.size()]}];
|
||||
std::set<std::pair<int, int>> seen;
|
||||
for (const auto& [e, n] : directed) {
|
||||
const std::pair<int, int> key{std::min(e.first, e.second), std::max(e.first, e.second)};
|
||||
if (!seen.insert(key).second) continue;
|
||||
const int ab = n, ba = directed.count({e.second, e.first}) ? directed.at({e.second, e.first}) : 0;
|
||||
if (ab + ba == 1) ++c.openEdges;
|
||||
else if (ab + ba > 2) ++c.crowdedEdges;
|
||||
else if (ab == 2 || ba == 2) ++c.flippedEdges;
|
||||
}
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
const float a = faceArea(m, int(f));
|
||||
c.area += a;
|
||||
if (a < 1e-10f) ++c.tinyFaces;
|
||||
else if (overhangs(m, int(f), overhang, bedZ)) c.overhangArea += a;
|
||||
for (const auto& t : triangulate(m, int(f))) c.volume += glm::dot(m.verts[size_t(t[0])], glm::cross(m.verts[size_t(t[1])], m.verts[size_t(t[2])])) / 6.0f;
|
||||
}
|
||||
std::set<int> left;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) left.insert(int(f));
|
||||
while (!left.empty()) {
|
||||
const std::set<int> piece = linkedFaces(m, {*left.begin()});
|
||||
for (int f : piece) left.erase(f);
|
||||
++c.pieces;
|
||||
}
|
||||
c.insideOut = c.volume < 0;
|
||||
return c;
|
||||
}
|
||||
|
||||
int closeHoles(Mesh& m) {
|
||||
std::set<std::pair<int, int>> directed;
|
||||
for (const auto& f : m.faces)
|
||||
for (size_t i = 0; i < f.size(); ++i) directed.insert({f[i], f[(i + 1) % f.size()]});
|
||||
std::map<int, int> next; // (round each hole: a boundary edge a -> b, then on from b)
|
||||
for (const auto& [a, b] : directed)
|
||||
if (!directed.count({b, a})) next[a] = b;
|
||||
int holes = 0;
|
||||
while (!next.empty()) {
|
||||
std::vector<int> loop;
|
||||
int v = next.begin()->first;
|
||||
while (next.count(v)) {
|
||||
loop.push_back(v);
|
||||
const int n = next[v];
|
||||
next.erase(v);
|
||||
v = n;
|
||||
}
|
||||
if (loop.size() < 3 || v != loop.front()) continue; // (not a clean loop)
|
||||
std::reverse(loop.begin(), loop.end()); // (the cap goes round the other way to the faces beside it)
|
||||
m.faces.push_back(loop);
|
||||
++holes;
|
||||
}
|
||||
return holes;
|
||||
}
|
||||
|
||||
int faceOutward(Mesh& m) {
|
||||
std::map<std::pair<int, int>, std::vector<int>> byEdge;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
const auto& fv = m.faces[f];
|
||||
for (size_t i = 0; i < fv.size(); ++i) byEdge[{std::min(fv[i], fv[(i + 1) % fv.size()]), std::max(fv[i], fv[(i + 1) % fv.size()])}].push_back(int(f));
|
||||
}
|
||||
auto goes = [&](int f, int a, int b) { // (face f has the edge a -> b that way round)
|
||||
const auto& fv = m.faces[size_t(f)];
|
||||
for (size_t i = 0; i < fv.size(); ++i)
|
||||
if (fv[i] == a && fv[(i + 1) % fv.size()] == b) return true;
|
||||
return false;
|
||||
};
|
||||
int turned = 0;
|
||||
std::vector<char> done(m.faces.size(), 0);
|
||||
for (size_t start = 0; start < m.faces.size(); ++start) {
|
||||
if (done[start]) continue;
|
||||
std::vector<int> piece{int(start)}, todo{int(start)};
|
||||
int flips = 0;
|
||||
done[start] = 1;
|
||||
while (!todo.empty()) {
|
||||
const int f = todo.back();
|
||||
todo.pop_back();
|
||||
const auto fv = m.faces[size_t(f)];
|
||||
for (size_t i = 0; i < fv.size(); ++i) {
|
||||
const int a = fv[i], b = fv[(i + 1) % fv.size()];
|
||||
const auto& around = byEdge[{std::min(a, b), std::max(a, b)}];
|
||||
if (around.size() != 2) continue; // (only across clean edges)
|
||||
for (int g : around) {
|
||||
if (done[size_t(g)]) continue;
|
||||
done[size_t(g)] = 1;
|
||||
if (goes(g, a, b)) std::reverse(m.faces[size_t(g)].begin(), m.faces[size_t(g)].end()), ++flips;
|
||||
piece.push_back(g), todo.push_back(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
float v = 0;
|
||||
for (int f : piece)
|
||||
for (const auto& t : triangulate(m, f)) v += glm::dot(m.verts[size_t(t[0])], glm::cross(m.verts[size_t(t[1])], m.verts[size_t(t[2])]));
|
||||
if (v < 0) { // (all of it inside out: turned round, those just turned are back as they were)
|
||||
for (int f : piece) std::reverse(m.faces[size_t(f)].begin(), m.faces[size_t(f)].end());
|
||||
flips = int(piece.size()) - flips;
|
||||
}
|
||||
turned += flips;
|
||||
}
|
||||
return turned;
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
454
src/scene.cpp
Normal file
454
src/scene.cpp
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
#include "scene.h"
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
namespace fry {
|
||||
|
||||
glm::quat fromEuler(const glm::vec3& degrees) {
|
||||
const glm::vec3 r = glm::radians(degrees);
|
||||
return glm::angleAxis(r.z, glm::vec3(0, 0, 1)) * glm::angleAxis(r.y, glm::vec3(0, 1, 0)) * glm::angleAxis(r.x, glm::vec3(1, 0, 0));
|
||||
}
|
||||
|
||||
glm::vec3 toEuler(const glm::quat& q) {
|
||||
const glm::mat3 m = glm::mat3_cast(glm::normalize(q)); // = Rz Ry Rx; m[column][row]
|
||||
const float y = std::asin(std::clamp(-m[0][2], -1.0f, 1.0f));
|
||||
float x, z;
|
||||
if (std::abs(m[0][2]) < 0.9999f) x = std::atan2(m[1][2], m[2][2]), z = std::atan2(m[0][1], m[0][0]);
|
||||
else x = std::atan2(-m[2][1], m[1][1]), z = 0.0f; // (straight up or down: X and Z turn the same way)
|
||||
return glm::degrees(glm::vec3(x, y, z));
|
||||
}
|
||||
|
||||
glm::mat4 objectMatrix(const Object& o) {
|
||||
return glm::translate(glm::mat4(1.0f), o.position) * glm::mat4_cast(o.rotation) * glm::scale(glm::mat4(1.0f), o.scale);
|
||||
}
|
||||
|
||||
Mesh finishedMesh(const Object& o, bool mirrorOnly) {
|
||||
Mesh m = o.mesh;
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
if (!(o.mirror & (1 << axis))) continue;
|
||||
glm::vec3 s(1.0f);
|
||||
s[axis] = -1.0f;
|
||||
const size_t before = m.verts.size();
|
||||
append(m, Mesh(m), glm::scale(glm::mat4(1.0f), s));
|
||||
// Points on the mirror line join their copies (so the halves are one piece there).
|
||||
const float eps = 1e-4f;
|
||||
std::vector<int> to(m.verts.size());
|
||||
for (size_t i = 0; i < to.size(); ++i) to[i] = int(i);
|
||||
for (size_t i = before; i < m.verts.size(); ++i)
|
||||
if (std::abs(m.verts[i][axis]) < eps) to[i] = int(i - before);
|
||||
for (auto& f : m.faces)
|
||||
for (int& v : f) v = to[size_t(v)];
|
||||
std::erase_if(m.faces, [](const std::vector<int>& f) { return std::set<int>(f.begin(), f.end()).size() < 3; });
|
||||
// (a face lying on the mirror line would now be there twice, back to back: both go)
|
||||
std::map<std::vector<int>, int> seen;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
std::vector<int> k = m.faces[f];
|
||||
std::sort(k.begin(), k.end());
|
||||
seen[k]++;
|
||||
}
|
||||
std::erase_if(m.faces, [&](const std::vector<int>& f) {
|
||||
std::vector<int> k = f;
|
||||
std::sort(k.begin(), k.end());
|
||||
return seen[k] > 1;
|
||||
});
|
||||
removeUnused(m);
|
||||
}
|
||||
if (mirrorOnly) return m;
|
||||
if (o.round > 0.0f) bevelEdges(m, sharpEdges(m, o.roundAngle), o.round, o.roundSteps);
|
||||
for (int i = 0; i < std::min(o.smoothLevels, 4); ++i) subdivide(m, true, o.smoothKeepSharp ? 30.0f : 180.0f);
|
||||
return m;
|
||||
}
|
||||
|
||||
std::array<glm::vec3, 4> referenceCorners(const Reference& r, float aspect) {
|
||||
// Each view's right, up and towards-you directions (see the cameras in app.cpp).
|
||||
glm::vec3 right, up;
|
||||
switch (r.axis) {
|
||||
case Axis::Front: right = {0, 1, 0}, up = {0, 0, 1}; break;
|
||||
case Axis::Side: right = {-1, 0, 0}, up = {0, 0, 1}; break;
|
||||
default: right = {0, -1, 0}, up = {1, 0, 0}; break;
|
||||
}
|
||||
const glm::vec3 c = right * r.offset.x + up * r.offset.y;
|
||||
const glm::vec3 x = right * (r.width * 0.5f), y = up * (std::abs(r.width) * aspect * 0.5f);
|
||||
return {c - x - y, c + x - y, c + x + y, c - x + y};
|
||||
}
|
||||
|
||||
// ── .fry ──
|
||||
|
||||
namespace {
|
||||
std::string quoted(const std::string& s) {
|
||||
std::string out = "\"";
|
||||
for (char ch : s) {
|
||||
if (ch == '"' || ch == '\\') out += '\\';
|
||||
out += ch;
|
||||
}
|
||||
return out + "\"";
|
||||
}
|
||||
|
||||
std::string readQuoted(std::istream& in) {
|
||||
std::string out;
|
||||
char ch;
|
||||
in >> std::ws;
|
||||
if (in.peek() != '"') {
|
||||
in >> out;
|
||||
return out;
|
||||
}
|
||||
in.get();
|
||||
while (in.get(ch) && ch != '"') {
|
||||
if (ch == '\\') in.get(ch);
|
||||
out += ch;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const char* axisName(Axis a) { return a == Axis::Front ? "front" : a == Axis::Side ? "side" : "top"; }
|
||||
} // namespace
|
||||
|
||||
std::string sceneText(const Scene& scene) {
|
||||
std::ostringstream out;
|
||||
out.precision(9); // (floats come back exactly)
|
||||
out << "fry3dx 1\n";
|
||||
const Look& k = scene.look;
|
||||
out << "look " << k.sunTurn << " " << k.sunHeight << " " << k.sunStrength << " " << k.sunColor.r << " " << k.sunColor.g << " " << k.sunColor.b << " " << k.sky
|
||||
<< " " << k.ground << " " << k.background.r << " " << k.background.g << " " << k.background.b << " " << k.groundColor.r << " " << k.groundColor.g << " "
|
||||
<< k.groundColor.b << " " << k.exposure << " " << k.ao << " " << k.bloom << " " << k.saturation << " " << k.vignette << " " << k.sharpen << " "
|
||||
<< k.shadowSoft << "\n";
|
||||
for (const Reference& r : scene.references)
|
||||
out << "reference " << quoted(r.path) << " " << axisName(r.axis) << " " << r.offset.x << " " << r.offset.y << " " << r.width << " " << r.opacity
|
||||
<< " " << r.visible << "\n";
|
||||
// Folders by their place in the file (1 up; 0 the top), as ids change on loading.
|
||||
std::map<uint32_t, size_t> folderAt;
|
||||
for (size_t i = 0; i < scene.folders.size(); ++i) folderAt[scene.folders[i].id] = i + 1;
|
||||
for (const Folder& f : scene.folders)
|
||||
out << "folder " << quoted(f.name) << " " << (folderAt.count(f.parent) ? folderAt[f.parent] : 0) << " " << f.open << " " << f.visible << " " << f.locked << "\n";
|
||||
for (const Object& o : scene.objects) {
|
||||
out << "object " << quoted(o.name) << "\n";
|
||||
if (folderAt.count(o.folder)) out << " folder " << folderAt[o.folder] << "\n";
|
||||
out << " position " << o.position.x << " " << o.position.y << " " << o.position.z << "\n";
|
||||
out << " rotation " << o.rotation.w << " " << o.rotation.x << " " << o.rotation.y << " " << o.rotation.z << "\n"; // (a quaternion: w x y z)
|
||||
out << " scale " << o.scale.x << " " << o.scale.y << " " << o.scale.z << "\n";
|
||||
out << " color " << o.color.r << " " << o.color.g << " " << o.color.b << "\n";
|
||||
out << " metal " << o.metal << "\n rough " << o.rough << "\n";
|
||||
out << " smooth " << o.smooth << " " << o.smoothAngle << "\n";
|
||||
out << " visible " << o.visible << "\n locked " << o.locked << "\n";
|
||||
if (!o.recipe.empty()) {
|
||||
out << " recipe " << o.recipe;
|
||||
for (float v : o.settings) out << " " << v;
|
||||
out << "\n";
|
||||
}
|
||||
if (o.mirror) out << " mirror " << o.mirror << "\n";
|
||||
if (o.round > 0.0f) out << " round " << o.round << " " << o.roundSteps << " " << o.roundAngle << "\n";
|
||||
if (o.smoothLevels) out << " subdivide " << o.smoothLevels << " " << o.smoothKeepSharp << "\n";
|
||||
if (!o.pictureFile.empty()) out << " picture " << quoted(o.pictureFile) << "\n";
|
||||
for (const PictureLayer& l : o.layers) {
|
||||
if (l.file.empty()) continue;
|
||||
out << " layer " << quoted(l.name) << " " << quoted(l.file) << " " << l.sticker << " " << l.visible << " " << l.opacity;
|
||||
for (const glm::vec2& c : l.corners) out << " " << c.x << " " << c.y;
|
||||
out << " " << l.face << " " << l.centre.x << " " << l.centre.y << " " << l.centre.z << " " << l.up.x << " " << l.up.y << " " << l.up.z << " " << l.width << " "
|
||||
<< l.height << "\n";
|
||||
}
|
||||
for (const glm::vec3& v : o.mesh.verts) out << " v " << v.x << " " << v.y << " " << v.z << "\n";
|
||||
for (const auto& f : o.mesh.faces) {
|
||||
out << " f";
|
||||
for (int i : f) out << " " << i;
|
||||
out << "\n";
|
||||
}
|
||||
if (o.picture && o.uvsFit())
|
||||
for (const auto& t : o.uvs) {
|
||||
out << " t";
|
||||
for (const glm::vec2& q : t) out << " " << q.x << " " << q.y;
|
||||
out << "\n";
|
||||
}
|
||||
out << "end\n";
|
||||
}
|
||||
return out.str();
|
||||
}
|
||||
|
||||
bool writeFileSafely(const std::string& path, const void* data, size_t size, std::string* error) {
|
||||
// Written beside it, then moved over it: a crash mid-save never leaves half a file.
|
||||
const std::string tmp = path + ".tmp";
|
||||
{
|
||||
std::ofstream f(tmp, std::ios::binary);
|
||||
if (!f.write(static_cast<const char*>(data), std::streamsize(size)) || !(f.flush())) {
|
||||
if (error) *error = "can't write " + tmp;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
std::error_code ec;
|
||||
std::filesystem::rename(tmp, path, ec);
|
||||
if (ec && error) *error = ec.message();
|
||||
return !ec;
|
||||
}
|
||||
|
||||
bool saveScene(const std::string& path, const Scene& scene, std::string* error) {
|
||||
const std::string text = sceneText(scene);
|
||||
return writeFileSafely(path, text.data(), text.size(), error);
|
||||
}
|
||||
|
||||
std::optional<Scene> loadScene(const std::string& path, std::string* error) {
|
||||
std::ifstream in(path);
|
||||
if (!in) {
|
||||
if (error) *error = "can't open " + path;
|
||||
return std::nullopt;
|
||||
}
|
||||
return parseScene(in, path, error);
|
||||
}
|
||||
|
||||
std::optional<Scene> parseScene(std::istream& in, const std::string& path, std::string* error) {
|
||||
std::string word;
|
||||
int version = 0;
|
||||
if (!(in >> word >> version) || word != "fry3dx") {
|
||||
if (error) *error = path + " isn't a Fry3DX file";
|
||||
return std::nullopt;
|
||||
}
|
||||
Scene s;
|
||||
Object* o = nullptr;
|
||||
for (std::string line; std::getline(in, line);) {
|
||||
std::istringstream l(line);
|
||||
if (!(l >> word)) continue;
|
||||
if (word == "reference") {
|
||||
Reference r;
|
||||
r.id = s.nextId++;
|
||||
r.path = readQuoted(l);
|
||||
std::string axis;
|
||||
l >> axis >> r.offset.x >> r.offset.y >> r.width >> r.opacity >> r.visible;
|
||||
r.axis = axis == "front" ? Axis::Front : axis == "top" ? Axis::Top : Axis::Side;
|
||||
s.references.push_back(r);
|
||||
} else if (word == "look") {
|
||||
Look& k = s.look;
|
||||
l >> k.sunTurn >> k.sunHeight >> k.sunStrength >> k.sunColor.r >> k.sunColor.g >> k.sunColor.b >> k.sky >> k.ground >> k.background.r >> k.background.g >>
|
||||
k.background.b >> k.groundColor.r >> k.groundColor.g >> k.groundColor.b >> k.exposure >> k.ao >> k.bloom >> k.saturation >> k.vignette >> k.sharpen >>
|
||||
k.shadowSoft;
|
||||
} else if (word == "folder" && !o) {
|
||||
Folder f;
|
||||
f.id = s.nextId++;
|
||||
f.name = readQuoted(l);
|
||||
size_t parent = 0;
|
||||
l >> parent >> f.open >> f.visible >> f.locked;
|
||||
f.parent = uint32_t(parent); // (a place in the file for now)
|
||||
s.folders.push_back(f);
|
||||
} else if (word == "object") {
|
||||
s.objects.emplace_back();
|
||||
o = &s.objects.back();
|
||||
o->id = s.nextId++;
|
||||
o->name = readQuoted(l);
|
||||
} else if (!o) {
|
||||
continue;
|
||||
} else if (word == "position") l >> o->position.x >> o->position.y >> o->position.z;
|
||||
else if (word == "rotation") l >> o->rotation.w >> o->rotation.x >> o->rotation.y >> o->rotation.z;
|
||||
else if (word == "scale") l >> o->scale.x >> o->scale.y >> o->scale.z;
|
||||
else if (word == "color") l >> o->color.r >> o->color.g >> o->color.b;
|
||||
else if (word == "metal") l >> o->metal;
|
||||
else if (word == "rough") l >> o->rough;
|
||||
else if (word == "smooth") l >> o->smooth >> o->smoothAngle;
|
||||
else if (word == "folder") {
|
||||
size_t at = 0;
|
||||
l >> at;
|
||||
o->folder = uint32_t(at); // (a place in the file for now)
|
||||
} else if (word == "visible") l >> o->visible;
|
||||
else if (word == "locked") l >> o->locked;
|
||||
else if (word == "recipe") {
|
||||
l >> o->recipe;
|
||||
for (float v; l >> v;) o->settings.push_back(v);
|
||||
} else if (word == "mirror") l >> o->mirror;
|
||||
else if (word == "round") l >> o->round >> o->roundSteps >> o->roundAngle;
|
||||
else if (word == "subdivide") l >> o->smoothLevels >> o->smoothKeepSharp;
|
||||
else if (word == "picture") o->pictureFile = readQuoted(l);
|
||||
else if (word == "layer") {
|
||||
PictureLayer p;
|
||||
p.name = readQuoted(l);
|
||||
l >> std::ws;
|
||||
p.file = readQuoted(l);
|
||||
l >> p.sticker >> p.visible >> p.opacity;
|
||||
for (glm::vec2& c : p.corners) l >> c.x >> c.y;
|
||||
l >> p.face >> p.centre.x >> p.centre.y >> p.centre.z >> p.up.x >> p.up.y >> p.up.z >> p.width >> p.height;
|
||||
o->layers.push_back(p);
|
||||
}
|
||||
else if (word == "t") {
|
||||
std::vector<glm::vec2> t;
|
||||
for (glm::vec2 q; l >> q.x >> q.y;) t.push_back(q);
|
||||
o->uvs.push_back(t);
|
||||
}
|
||||
else if (word == "v") {
|
||||
glm::vec3 v;
|
||||
l >> v.x >> v.y >> v.z;
|
||||
o->mesh.verts.push_back(v);
|
||||
} else if (word == "f") {
|
||||
std::vector<int> f;
|
||||
for (int i; l >> i;)
|
||||
if (i >= 0 && i < int(o->mesh.verts.size())) f.push_back(i);
|
||||
if (f.size() >= 3) o->mesh.faces.push_back(f);
|
||||
} else if (word == "end") o = nullptr;
|
||||
}
|
||||
// Places in the file to ids (a parent or folder that isn't there: the top).
|
||||
auto idAt = [&](uint32_t at) { return at >= 1 && at <= s.folders.size() ? s.folders[at - 1].id : 0u; };
|
||||
for (Folder& f : s.folders) f.parent = idAt(f.parent);
|
||||
for (Folder& f : s.folders) // (a folder inside itself, round a loop: put at the top)
|
||||
for (uint32_t p = f.parent, n = 0; p; ++n) {
|
||||
if (p == f.id || n > s.folders.size()) {
|
||||
f.parent = 0;
|
||||
break;
|
||||
}
|
||||
p = std::find_if(s.folders.begin(), s.folders.end(), [&](const Folder& g) { return g.id == p; })->parent;
|
||||
}
|
||||
for (Object& o : s.objects) o.folder = idAt(o.folder);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ── glTF ──
|
||||
|
||||
bool exportGltf(const std::string& path, const Scene& scene, const std::vector<std::vector<uint8_t>>& pngs, bool binary, std::string* error) {
|
||||
// Fry3DX: X forward, Y left, Z up. glTF: Z forward, X left, Y up.
|
||||
auto toGltf = [](glm::vec3 v) { return glm::vec3(v.y, v.z, v.x); };
|
||||
std::vector<uint8_t> bin;
|
||||
auto put = [&](const void* p, size_t n) {
|
||||
while (bin.size() % 4) bin.push_back(0);
|
||||
const size_t at = bin.size();
|
||||
bin.insert(bin.end(), (const uint8_t*)p, (const uint8_t*)p + n);
|
||||
return at;
|
||||
};
|
||||
std::ostringstream views, accessors, meshes, nodes, materials, images;
|
||||
int nImage = 0;
|
||||
int nView = 0, nAcc = 0, nMesh = 0;
|
||||
std::vector<int> roots;
|
||||
auto view = [&](size_t offset, size_t length, int target) {
|
||||
views << (nView ? "," : "") << "{\"buffer\":0,\"byteOffset\":" << offset << ",\"byteLength\":" << length;
|
||||
if (target) views << ",\"target\":" << target;
|
||||
views << "}";
|
||||
return nView++;
|
||||
};
|
||||
auto escape = [](const std::string& s) {
|
||||
std::string out;
|
||||
for (char ch : s) {
|
||||
if (ch == '"' || ch == '\\') out += '\\';
|
||||
if (uint8_t(ch) >= 0x20) out += ch;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const std::filesystem::path p(path);
|
||||
for (size_t oi = 0; oi < scene.objects.size(); ++oi) {
|
||||
const Object& shown = scene.objects[oi];
|
||||
if (!shown.visible || shown.mesh.faces.empty()) continue;
|
||||
Object o = shown;
|
||||
if (o.hasFinish()) o.mesh = finishedMesh(shown);
|
||||
// Painted: its picture, laid on as painted (not with rounding or smoothing added since).
|
||||
const bool painted = oi < pngs.size() && !pngs[oi].empty() && shown.uvsFit() && !shown.hasFinish();
|
||||
// Its turn and size go into its points; its place stays the node's (its origin: a hinge).
|
||||
const glm::mat4 rs = glm::mat4_cast(o.rotation) * glm::scale(glm::mat4(1.0f), o.scale);
|
||||
const glm::mat3 nm = glm::transpose(glm::inverse(glm::mat3(rs)));
|
||||
const bool mirrored = glm::determinant(glm::mat3(rs)) < 0.0f;
|
||||
const auto normals = cornerNormals(o.mesh, o.smooth, o.smoothAngle);
|
||||
std::vector<glm::vec3> pos, nrm;
|
||||
std::vector<glm::vec2> uv;
|
||||
std::vector<uint32_t> idx;
|
||||
std::map<std::array<float, 8>, uint32_t> seen; // (shared corners where the normal and picture agree)
|
||||
for (size_t f = 0; f < o.mesh.faces.size(); ++f) {
|
||||
const auto& face = o.mesh.faces[f];
|
||||
for (const auto& tri : triangulate(o.mesh, int(f))) {
|
||||
std::array<uint32_t, 3> t{};
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const size_t corner = size_t(std::find(face.begin(), face.end(), tri[size_t(k)]) - face.begin());
|
||||
const glm::vec3 p = toGltf(glm::vec3(rs * glm::vec4(o.mesh.verts[size_t(tri[size_t(k)])], 1.0f)));
|
||||
const glm::vec3 n = toGltf(glm::normalize(nm * normals[f][corner]));
|
||||
// Pictures wrap round it from the side it faces most (1 m = once), or as painted.
|
||||
const glm::vec3 a = glm::abs(n);
|
||||
const glm::vec2 tc = painted ? o.uvs[f][corner]
|
||||
: a.x >= a.y && a.x >= a.z ? glm::vec2(p.z, -p.y) : a.y >= a.z ? glm::vec2(p.x, -p.z) : glm::vec2(p.x, -p.y);
|
||||
const std::array<float, 8> key{p.x, p.y, p.z, n.x, n.y, n.z, tc.x, tc.y};
|
||||
auto [it, fresh] = seen.try_emplace(key, uint32_t(pos.size()));
|
||||
if (fresh) {
|
||||
uv.push_back(tc);
|
||||
pos.push_back(p), nrm.push_back(n);
|
||||
}
|
||||
t[size_t(k)] = it->second;
|
||||
}
|
||||
if (mirrored) std::swap(t[1], t[2]);
|
||||
idx.insert(idx.end(), t.begin(), t.end());
|
||||
}
|
||||
}
|
||||
glm::vec3 lo(1e30f), hi(-1e30f);
|
||||
for (const glm::vec3& p : pos) lo = glm::min(lo, p), hi = glm::max(hi, p);
|
||||
const int vp = view(put(pos.data(), pos.size() * 12), pos.size() * 12, 34962);
|
||||
const int vn = view(put(nrm.data(), nrm.size() * 12), nrm.size() * 12, 34962);
|
||||
const int vu = view(put(uv.data(), uv.size() * 8), uv.size() * 8, 34962);
|
||||
const int vi = view(put(idx.data(), idx.size() * 4), idx.size() * 4, 34963);
|
||||
accessors << (nAcc ? "," : "") << "{\"bufferView\":" << vp << ",\"componentType\":5126,\"count\":" << pos.size() << ",\"type\":\"VEC3\",\"min\":["
|
||||
<< lo.x << "," << lo.y << "," << lo.z << "],\"max\":[" << hi.x << "," << hi.y << "," << hi.z << "]}"
|
||||
<< ",{\"bufferView\":" << vn << ",\"componentType\":5126,\"count\":" << nrm.size() << ",\"type\":\"VEC3\"}"
|
||||
<< ",{\"bufferView\":" << vu << ",\"componentType\":5126,\"count\":" << uv.size() << ",\"type\":\"VEC2\"}"
|
||||
<< ",{\"bufferView\":" << vi << ",\"componentType\":5125,\"count\":" << idx.size() << ",\"type\":\"SCALAR\"}";
|
||||
meshes << (nMesh ? "," : "") << "{\"name\":\"" << escape(o.name) << "\",\"primitives\":[{\"attributes\":{\"POSITION\":" << nAcc
|
||||
<< ",\"NORMAL\":" << nAcc + 1 << ",\"TEXCOORD_0\":" << nAcc + 2 << "},\"indices\":" << nAcc + 3 << ",\"material\":" << nMesh << "}]}";
|
||||
nAcc += 4;
|
||||
const glm::vec3 lin = painted ? glm::vec3(1.0f) : glm::pow(o.color, glm::vec3(2.2f)); // (picked as seen, sRGB; glTF's factor is linear)
|
||||
materials << (nMesh ? "," : "") << "{\"name\":\"" << escape(o.name) << "\",\"pbrMetallicRoughness\":{\"baseColorFactor\":[" << lin.r << ","
|
||||
<< lin.g << "," << lin.b << ",1],";
|
||||
if (painted) {
|
||||
materials << "\"baseColorTexture\":{\"index\":" << nImage << "},";
|
||||
if (binary) images << (nImage ? "," : "") << "{\"mimeType\":\"image/png\",\"bufferView\":" << view(put(pngs[oi].data(), pngs[oi].size()), pngs[oi].size(), 0) << "}";
|
||||
else {
|
||||
const std::string name = std::format("{}_{}.png", p.stem().string(), oi + 1);
|
||||
std::ofstream f(p.parent_path() / name, std::ios::binary);
|
||||
if (!f.write(reinterpret_cast<const char*>(pngs[oi].data()), std::streamsize(pngs[oi].size()))) {
|
||||
if (error) *error = "can't write " + name;
|
||||
return false;
|
||||
}
|
||||
images << (nImage ? "," : "") << "{\"uri\":\"" << escape(name) << "\"}";
|
||||
}
|
||||
++nImage;
|
||||
}
|
||||
materials << "\"metallicFactor\":" << o.metal << ",\"roughnessFactor\":" << o.rough << "}}";
|
||||
const glm::vec3 t = toGltf(o.position);
|
||||
nodes << (nMesh ? "," : "") << "{\"name\":\"" << escape(o.name) << "\",\"mesh\":" << nMesh << ",\"translation\":[" << t.x << "," << t.y << ","
|
||||
<< t.z << "]}";
|
||||
roots.push_back(nMesh);
|
||||
++nMesh;
|
||||
}
|
||||
if (!nMesh) {
|
||||
if (error) *error = "nothing to export (no visible objects)";
|
||||
return false;
|
||||
}
|
||||
const std::string binName = p.stem().string() + ".bin";
|
||||
std::ostringstream j;
|
||||
j << "{\"asset\":{\"version\":\"2.0\",\"generator\":\"Fry3DX\"},\"scene\":0,\"scenes\":[{\"nodes\":[";
|
||||
for (size_t i = 0; i < roots.size(); ++i) j << (i ? "," : "") << roots[i];
|
||||
j << "]}],\"nodes\":[" << nodes.str() << "],\"meshes\":[" << meshes.str() << "],\"materials\":[" << materials.str() << "],\"accessors\":["
|
||||
<< accessors.str() << "]";
|
||||
if (nImage) {
|
||||
j << ",\"samplers\":[{}],\"images\":[" << images.str() << "],\"textures\":[";
|
||||
for (int i = 0; i < nImage; ++i) j << (i ? "," : "") << "{\"sampler\":0,\"source\":" << i << "}";
|
||||
j << "]";
|
||||
}
|
||||
j << ",\"bufferViews\":[" << views.str() << "],\"buffers\":[{";
|
||||
if (!binary) j << "\"uri\":\"" << escape(binName) << "\",";
|
||||
j << "\"byteLength\":" << bin.size() << "}]}";
|
||||
if (binary) { // .glb: a header, the JSON, then the binary, each padded to 4 bytes
|
||||
std::string json = j.str();
|
||||
while (json.size() % 4) json += ' ';
|
||||
while (bin.size() % 4) bin.push_back(0);
|
||||
std::vector<uint8_t> out;
|
||||
auto u32 = [&](uint32_t v) { out.insert(out.end(), reinterpret_cast<uint8_t*>(&v), reinterpret_cast<uint8_t*>(&v) + 4); };
|
||||
u32(0x46546C67), u32(2), u32(uint32_t(12 + 8 + json.size() + 8 + bin.size()));
|
||||
u32(uint32_t(json.size())), u32(0x4E4F534A), out.insert(out.end(), json.begin(), json.end());
|
||||
u32(uint32_t(bin.size())), u32(0x004E4942), out.insert(out.end(), bin.begin(), bin.end());
|
||||
return writeFileSafely(path, out.data(), out.size(), error);
|
||||
}
|
||||
j << "\n";
|
||||
std::ofstream jf(path, std::ios::binary), bf((p.parent_path() / binName).string(), std::ios::binary);
|
||||
if (!(jf << j.str()) || !bf.write((const char*)bin.data(), std::streamsize(bin.size()))) {
|
||||
if (error) *error = "can't write " + path;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
166
src/scene.h
Normal file
166
src/scene.h
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
#pragma once
|
||||
// What you're making: objects (a mesh each, placed, coloured) and reference pictures to trace.
|
||||
// Saved as .fry (plain text), exported as glTF for games (Blender, Godot, Unity).
|
||||
#include "image.h"
|
||||
#include "mesh.h"
|
||||
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fry {
|
||||
|
||||
// A picture put onto an object's painting, kept apart from it (a layer) so it can be moved again.
|
||||
// A sticker sits on the surface: `centre` on face `face` (the object's own space), `up` its top's
|
||||
// way, `width` x `height` across, and wraps over edges like a real one. Otherwise it's pinned onto
|
||||
// the flat picture by its four corners (pixels: top-left, top-right, bottom-right, bottom-left).
|
||||
struct PictureLayer {
|
||||
std::string name;
|
||||
std::shared_ptr<const Image> image;
|
||||
bool sticker = true, visible = true;
|
||||
float opacity = 1.0f;
|
||||
std::array<glm::vec2, 4> corners{};
|
||||
int face = 0;
|
||||
glm::vec3 centre{0.0f}, up{0.0f, 0.0f, 1.0f};
|
||||
float width = 0.5f, height = 0.5f;
|
||||
std::string file; // (its PNG's name, set when saving)
|
||||
};
|
||||
|
||||
// A folder in the Layers panel: objects (and folders) kept together, and picked together (a group).
|
||||
struct Folder {
|
||||
uint32_t id = 0;
|
||||
std::string name;
|
||||
uint32_t parent = 0; // (0: at the top)
|
||||
bool open = true, visible = true, locked = false;
|
||||
};
|
||||
|
||||
struct Object {
|
||||
uint32_t id = 0;
|
||||
std::string name;
|
||||
uint32_t folder = 0; // (0: at the top)
|
||||
Mesh mesh; // around its origin (the point it turns and scales about)
|
||||
glm::vec3 position{0.0f}; // metres
|
||||
glm::quat rotation{1.0f, 0.0f, 0.0f, 0.0f};
|
||||
glm::vec3 scale{1.0f};
|
||||
glm::vec3 color{0.62f, 0.62f, 0.64f}; // as picked (sRGB)
|
||||
float metal = 0.0f, rough = 0.5f;
|
||||
bool smooth = false; // smooth shading, creases sharper than smoothAngle kept
|
||||
float smoothAngle = 35.0f;
|
||||
bool visible = true, locked = false;
|
||||
// A shape from the library keeps its recipe and settings (changeable in Properties) until
|
||||
// its shape is edited by hand.
|
||||
std::string recipe;
|
||||
std::vector<float> settings;
|
||||
// Finishing touches, not baked into the mesh until applied: mirrored copies across its own
|
||||
// X / Y / Z (bits 1, 2, 4), rounded edges (those sharper than roundAngle), smoothing.
|
||||
int mirror = 0;
|
||||
float round = 0.0f, roundAngle = 30.0f;
|
||||
int roundSteps = 3;
|
||||
int smoothLevels = 0;
|
||||
bool smoothKeepSharp = false;
|
||||
bool hasFinish() const { return mirror || round > 0.0f || smoothLevels > 0; }
|
||||
// A painted picture on it: where each face corner sits on it (0..1), and the picture
|
||||
// (shared with the undo steps until it's painted on again). Saved beside the .fry as a PNG.
|
||||
std::vector<std::vector<glm::vec2>> uvs;
|
||||
std::shared_ptr<const Image> picture;
|
||||
std::string pictureFile; // (the PNG's name, set when saving or exporting)
|
||||
std::vector<PictureLayer> layers; // on top of the painting, bottom first
|
||||
bool uvsFit() const {
|
||||
if (uvs.size() != mesh.faces.size()) return false;
|
||||
for (size_t f = 0; f < uvs.size(); ++f)
|
||||
if (uvs[f].size() != mesh.faces[f].size()) return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// The picture as it shows: the painting with its layers on top. owner (if given): for each pixel,
|
||||
// which is on top there (0 the painting, i + 1 layer i).
|
||||
Image composePicture(const Object& o, std::vector<uint8_t>* owner = nullptr);
|
||||
// A sticker's face (the one it was put on, or the nearest if the shape's changed), and its axes
|
||||
// there: right, up (along the face), and the face's normal.
|
||||
int stickerFace(const Mesh& m, const PictureLayer& l);
|
||||
bool stickerFrame(const Mesh& m, const PictureLayer& l, glm::vec3& right, glm::vec3& up, glm::vec3& normal);
|
||||
// A sticker whose middle has gone past its face's edge moves onto the face its middle is on now
|
||||
// (bent over the same way): it lies on the face with most of it and bends from there.
|
||||
void settleSticker(const Mesh& m, PictureLayer& l);
|
||||
// A sticker painted onto a picture (uvs: the object's), unfolded across edges from its face.
|
||||
void stampSticker(Image& dst, const Mesh& m, const std::vector<std::vector<glm::vec2>>& uvs, const PictureLayer& l, std::vector<uint8_t>* owner = nullptr, uint8_t id = 0);
|
||||
|
||||
// After frames of the layout moved (`before` to the object's uvs now): what was painted on those
|
||||
// faces moved with them, so the model looks the same.
|
||||
void remapPicture(Image& img, const Mesh& m, const std::vector<std::vector<glm::vec2>>& before, const std::vector<std::vector<glm::vec2>>& after,
|
||||
const std::vector<int>& faces);
|
||||
|
||||
// The mesh as shown and exported: with its mirror, rounding and smoothing.
|
||||
Mesh finishedMesh(const Object& o, bool mirrorOnly = false);
|
||||
|
||||
// A picture to trace over, standing on the plane through the origin that a view looks at.
|
||||
enum class Axis { Front, Side, Top }; // the view it faces: from +X, from +Y (the left), from above
|
||||
struct Reference {
|
||||
uint32_t id = 0;
|
||||
std::string path; // the picture file (PNG, JPG...)
|
||||
Axis axis = Axis::Side;
|
||||
glm::vec2 offset{0.0f}; // its middle on that plane, metres
|
||||
float width = 1.0f; // metres across
|
||||
float opacity = 0.6f;
|
||||
bool visible = true;
|
||||
};
|
||||
|
||||
// The Render tab's light and look.
|
||||
struct Look {
|
||||
float sunTurn = 35.0f, sunHeight = 50.0f, sunStrength = 0.6f; // degrees round (from +X), degrees up
|
||||
glm::vec3 sunColor{1.0f, 0.96f, 0.9f};
|
||||
bool sky = true, ground = true;
|
||||
glm::vec3 background{0.2f, 0.2f, 0.215f}, groundColor{0.33f, 0.33f, 0.35f}; // (sRGB)
|
||||
float exposure = 1.0f, ao = 1.0f, bloom = 0.035f, saturation = 1.05f, vignette = 0.25f, sharpen = 0.3f, shadowSoft = 1.0f;
|
||||
};
|
||||
|
||||
struct Scene {
|
||||
Look look;
|
||||
std::vector<Object> objects;
|
||||
std::vector<Folder> folders;
|
||||
std::vector<Reference> references;
|
||||
uint32_t nextId = 1;
|
||||
};
|
||||
|
||||
glm::mat4 objectMatrix(const Object& o);
|
||||
// A turn as degrees about X, then Y, then Z (what the Properties panel shows), and back.
|
||||
glm::vec3 toEuler(const glm::quat& q);
|
||||
glm::quat fromEuler(const glm::vec3& degrees);
|
||||
// Points of a reference's picture: its four corners (counter-clockwise, as seen from its view),
|
||||
// given its aspect (height / width).
|
||||
std::array<glm::vec3, 4> referenceCorners(const Reference& r, float aspect);
|
||||
|
||||
// The scene as plain text (the old .fry, and what's inside a new one), pictures by file name.
|
||||
std::string sceneText(const Scene& scene);
|
||||
std::optional<Scene> parseScene(std::istream& in, const std::string& path, std::string* error = nullptr);
|
||||
bool saveScene(const std::string& path, const Scene& scene, std::string* error = nullptr);
|
||||
std::optional<Scene> loadScene(const std::string& path, std::string* error = nullptr);
|
||||
// Written beside it then moved over it: a crash mid-save never leaves half a file.
|
||||
bool writeFileSafely(const std::string& path, const void* data, size_t size, std::string* error = nullptr);
|
||||
|
||||
// Fry3DX's own file (.fry): one file, the scene and every picture in it, packed small (pictures
|
||||
// as colour planes of differences, then all of it zstd'd). Opens the old text .fry files too.
|
||||
// level: zstd's, 1 (quick) to 19 (smallest).
|
||||
bool savePackage(const std::string& path, const Scene& scene, int level = 12, std::string* error = nullptr);
|
||||
std::optional<Scene> loadPackage(const std::string& path, std::string* error = nullptr);
|
||||
bool isPackage(const std::string& path);
|
||||
|
||||
// Exported for other programs, by the file's ending:
|
||||
// .gltf (and a .bin and PNGs beside it), .glb (all in one): glTF 2.0, its own axes, metres; each
|
||||
// visible object a node at its origin, turned and scaled into its points, one material each.
|
||||
// .fbx (binary 7.4; pictures inside it and beside it): Y up, centimetres.
|
||||
// .obj (and an .mtl and PNGs): Y up, metres. .ply: Z up, metres, with colours.
|
||||
// .stl (binary): Z up, millimetres (for 3D printing), all objects as one.
|
||||
// .3mf: for slicers (PrusaSlicer, Bambu Studio, Cura, OrcaSlicer): millimetres, each object apart.
|
||||
// pngs: each object's picture as a PNG file's bytes (or empty: just its colour).
|
||||
bool exportModel(const std::string& path, const Scene& scene, const std::vector<std::vector<uint8_t>>& pngs, std::string* error = nullptr);
|
||||
// What exportModel can write: (ending, name) pairs.
|
||||
const std::vector<std::pair<std::string, std::string>>& exportFormats();
|
||||
|
||||
} // namespace fry
|
||||
359
src/shapes.cpp
Normal file
359
src/shapes.cpp
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
#include "shapes.h"
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
|
||||
namespace fry {
|
||||
|
||||
namespace {
|
||||
constexpr float kPi = 3.14159265358979f;
|
||||
|
||||
glm::mat4 at(glm::vec3 p, glm::vec3 size = glm::vec3(1.0f)) { return glm::scale(glm::translate(glm::mat4(1.0f), p), size); }
|
||||
|
||||
// A box from (corner) lo to hi, as its own six faces.
|
||||
void addBox(Mesh& m, glm::vec3 lo, glm::vec3 hi) { append(m, makeCube(1.0f), at((lo + hi) * 0.5f, hi - lo)); }
|
||||
|
||||
// An outline drawn on the X–Z plane (x forward, z up), made `depth` thick across Y.
|
||||
Mesh sideProfile(const std::vector<glm::vec2>& xz, float depth) {
|
||||
std::vector<glm::vec3> pts;
|
||||
for (glm::vec2 p : xz) pts.emplace_back(p.x, 0.0f, p.y);
|
||||
return extrudeOutline(pts, glm::vec3(0, 1, 0), depth);
|
||||
}
|
||||
|
||||
// Grid box: a box cut along the given coordinates on each axis (points shared between sides).
|
||||
Mesh gridBox(const std::vector<float> (&c)[3]) {
|
||||
Mesh m;
|
||||
std::map<std::array<int, 3>, int> index;
|
||||
auto point = [&](int ix, int iy, int iz) {
|
||||
const std::array<int, 3> k{ix, iy, iz};
|
||||
auto [it, fresh] = index.try_emplace(k, int(m.verts.size()));
|
||||
if (fresh) m.verts.emplace_back(c[0][size_t(ix)], c[1][size_t(iy)], c[2][size_t(iz)]);
|
||||
return it->second;
|
||||
};
|
||||
// Each side: its axis, which end, and the two axes across it (so their cross is outwards).
|
||||
const int sides[6][4] = {{0, 1, 1, 2}, {0, 0, 2, 1}, {1, 1, 2, 0}, {1, 0, 0, 2}, {2, 1, 0, 1}, {2, 0, 1, 0}};
|
||||
for (const auto& s : sides) {
|
||||
const int axis = s[0], u = s[2], v = s[3];
|
||||
const int fixed = s[1] ? int(c[axis].size()) - 1 : 0;
|
||||
for (size_t i = 0; i + 1 < c[u].size(); ++i)
|
||||
for (size_t j = 0; j + 1 < c[v].size(); ++j) {
|
||||
auto P = [&](size_t a, size_t b) {
|
||||
int idx[3];
|
||||
idx[axis] = fixed, idx[u] = int(a), idx[v] = int(b);
|
||||
return point(idx[0], idx[1], idx[2]);
|
||||
};
|
||||
m.faces.push_back({P(i, j), P(i + 1, j), P(i + 1, j + 1), P(i, j + 1)});
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Mesh makeRoundedBox(glm::vec3 size, float radius, int segments) {
|
||||
const glm::vec3 h = size * 0.5f;
|
||||
radius = std::clamp(radius, 0.0f, std::min({h.x, h.y, h.z}));
|
||||
segments = std::clamp(segments, 1, 16);
|
||||
std::vector<float> c[3];
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
// Across the rounded strip at each end: steps at even angles (the 45° line always there,
|
||||
// where it turns from one side onto the next).
|
||||
std::vector<float> angles;
|
||||
for (int k = 0; k <= segments; ++k)
|
||||
if (float(k) * 90.0f / float(segments) < 45.0f - 1e-3f) angles.push_back(float(k) * 90.0f / float(segments));
|
||||
angles.push_back(45.0f);
|
||||
std::vector<float> lo;
|
||||
for (auto it = angles.rbegin(); it != angles.rend(); ++it) lo.push_back(-h[a] + radius - radius * std::tan(glm::radians(*it)));
|
||||
std::vector<float>& v = c[a];
|
||||
for (float x : lo) v.push_back(x);
|
||||
for (auto it = lo.rbegin(); it != lo.rend(); ++it) v.push_back(-*it);
|
||||
std::sort(v.begin(), v.end());
|
||||
v.erase(std::unique(v.begin(), v.end(), [](float p, float q) { return std::abs(p - q) < 1e-6f; }), v.end());
|
||||
}
|
||||
Mesh m = gridBox(c);
|
||||
if (radius > 1e-6f) {
|
||||
const glm::vec3 inner = h - glm::vec3(radius);
|
||||
for (glm::vec3& p : m.verts) {
|
||||
const glm::vec3 core = glm::clamp(p, -inner, inner);
|
||||
const glm::vec3 d = p - core;
|
||||
if (glm::length(d) > 1e-9f) p = core + glm::normalize(d) * radius;
|
||||
}
|
||||
}
|
||||
mergeByDistance(m, 1e-6f);
|
||||
return m;
|
||||
}
|
||||
|
||||
Mesh lathe(const std::vector<glm::vec2>& profile, int sides, bool closed) {
|
||||
sides = std::max(sides, 3);
|
||||
Mesh m;
|
||||
std::vector<std::vector<int>> rings;
|
||||
for (glm::vec2 p : profile) {
|
||||
std::vector<int> ring;
|
||||
if (p.x < 1e-6f) {
|
||||
ring.assign(size_t(sides), int(m.verts.size()));
|
||||
m.verts.emplace_back(0.0f, 0.0f, p.y);
|
||||
} else
|
||||
for (int i = 0; i < sides; ++i) {
|
||||
const float a = 2 * kPi * float(i) / float(sides);
|
||||
ring.push_back(int(m.verts.size()));
|
||||
m.verts.emplace_back(p.x * std::cos(a), p.x * std::sin(a), p.y);
|
||||
}
|
||||
rings.push_back(ring);
|
||||
}
|
||||
const size_t n = rings.size();
|
||||
for (size_t k = 0; k + (closed ? 0 : 1) < n; ++k) {
|
||||
const auto &lo = rings[k], &hi = rings[(k + 1) % n];
|
||||
for (int i = 0; i < sides; ++i) {
|
||||
const size_t a = size_t(i), b = size_t((i + 1) % sides);
|
||||
std::vector<int> f{lo[a], lo[b], hi[b], hi[a]};
|
||||
f.erase(std::unique(f.begin(), f.end()), f.end());
|
||||
while (f.size() > 1 && f.front() == f.back()) f.pop_back();
|
||||
if (f.size() >= 3) m.faces.push_back(f);
|
||||
}
|
||||
}
|
||||
if (!closed) {
|
||||
if (profile.front().x >= 1e-6f) m.faces.emplace_back(rings.front().rbegin(), rings.front().rend());
|
||||
if (profile.back().x >= 1e-6f) m.faces.push_back(rings.back());
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
namespace {
|
||||
float S(const std::vector<float>& s, size_t i) { return s[i]; }
|
||||
int N(const std::vector<float>& s, size_t i) { return int(std::lround(s[i])); }
|
||||
|
||||
std::vector<glm::vec2> arc(glm::vec2 centre, float r, float from, float to, int steps) {
|
||||
std::vector<glm::vec2> p;
|
||||
for (int k = 0; k <= steps; ++k) {
|
||||
const float a = glm::radians(from + (to - from) * float(k) / float(steps));
|
||||
p.push_back(centre + glm::vec2(std::cos(a), std::sin(a)) * r);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
const std::vector<ShapeRecipe> kRecipes = {
|
||||
// ── Basic ──
|
||||
{"cube", "Cube", false, false, {{"Width", 1, 0.001f, 100}, {"Depth", 1, 0.001f, 100}, {"Height", 1, 0.001f, 100}},
|
||||
[](const std::vector<float>& s) {
|
||||
Mesh m = makeCube(1.0f);
|
||||
transform(m, at({0, 0, 0}, {S(s, 1), S(s, 0), S(s, 2)}));
|
||||
return m;
|
||||
}},
|
||||
{"rounded_cube", "Rounded cube", false, true,
|
||||
{{"Width", 1, 0.001f, 100}, {"Depth", 1, 0.001f, 100}, {"Height", 1, 0.001f, 100}, {"Roundness", 0.1f, 0, 50}, {"Steps", 4, 1, 12, true}},
|
||||
[](const std::vector<float>& s) { return makeRoundedBox({S(s, 1), S(s, 0), S(s, 2)}, S(s, 3), N(s, 4)); }},
|
||||
{"plane", "Plane", false, false, {{"Width", 2, 0.001f, 1000}, {"Depth", 2, 0.001f, 1000}, {"Cuts", 0, 0, 64, true}},
|
||||
[](const std::vector<float>& s) {
|
||||
Mesh m;
|
||||
const int n = N(s, 2) + 1;
|
||||
for (int j = 0; j <= n; ++j)
|
||||
for (int i = 0; i <= n; ++i) m.verts.emplace_back((float(j) / float(n) - 0.5f) * S(s, 1), (float(i) / float(n) - 0.5f) * S(s, 0), 0.0f);
|
||||
for (int j = 0; j < n; ++j)
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const int a = j * (n + 1) + i;
|
||||
m.faces.push_back({a, a + n + 1, a + n + 2, a + 1});
|
||||
}
|
||||
return m;
|
||||
}},
|
||||
{"circle", "Circle", false, false, {{"Sides", 32, 3, 256, true}, {"Radius", 0.5f, 0.001f, 100}},
|
||||
[](const std::vector<float>& s) {
|
||||
Mesh m;
|
||||
std::vector<int> f;
|
||||
for (int i = 0; i < N(s, 0); ++i) {
|
||||
const float a = 2 * kPi * float(i) / float(N(s, 0));
|
||||
f.push_back(int(m.verts.size()));
|
||||
m.verts.emplace_back(std::cos(a) * S(s, 1), std::sin(a) * S(s, 1), 0.0f);
|
||||
}
|
||||
m.faces.push_back(f);
|
||||
return m;
|
||||
}},
|
||||
{"cylinder", "Cylinder", false, true, {{"Sides", 24, 3, 256, true}, {"Radius", 0.5f, 0.001f, 100}, {"Height", 1, 0.001f, 100}},
|
||||
[](const std::vector<float>& s) { return makeCylinder(N(s, 0), S(s, 1), S(s, 2)); }},
|
||||
{"tube", "Tube (pipe)", false, true,
|
||||
{{"Sides", 24, 3, 256, true}, {"Outside radius", 0.5f, 0.001f, 100}, {"Wall", 0.08f, 0.001f, 100}, {"Height", 1, 0.001f, 100}},
|
||||
[](const std::vector<float>& s) {
|
||||
const float r = S(s, 1), in = std::max(r - S(s, 2), 0.001f), h = S(s, 3);
|
||||
return lathe({{r, -h / 2}, {r, h / 2}, {in, h / 2}, {in, -h / 2}}, N(s, 0), true);
|
||||
}},
|
||||
{"cone", "Cone", false, true,
|
||||
{{"Sides", 24, 3, 256, true}, {"Bottom radius", 0.5f, 0, 100}, {"Top radius", 0, 0, 100}, {"Height", 1, 0.001f, 100}},
|
||||
[](const std::vector<float>& s) { return makeFrustum(N(s, 0), std::max(S(s, 1), 0.001f), S(s, 2), S(s, 3)); }},
|
||||
{"pyramid", "Pyramid", false, false, {{"Width", 1, 0.001f, 100}, {"Height", 1, 0.001f, 100}},
|
||||
[](const std::vector<float>& s) {
|
||||
Mesh m = makeCone(4, S(s, 0) * 0.70710678f, S(s, 1));
|
||||
transform(m, glm::rotate(glm::mat4(1.0f), kPi / 4, glm::vec3(0, 0, 1)));
|
||||
return m;
|
||||
}},
|
||||
{"sphere", "Sphere", false, true, {{"Segments", 24, 3, 256, true}, {"Rings", 12, 2, 128, true}, {"Radius", 0.5f, 0.001f, 100}},
|
||||
[](const std::vector<float>& s) { return makeSphere(N(s, 0), N(s, 1), S(s, 2)); }},
|
||||
{"capsule", "Capsule", false, true, {{"Sides", 24, 3, 256, true}, {"Radius", 0.25f, 0.001f, 100}, {"Height", 1, 0.002f, 100}},
|
||||
[](const std::vector<float>& s) {
|
||||
const float r = std::min(S(s, 1), S(s, 2) / 2), h = S(s, 2);
|
||||
std::vector<glm::vec2> p;
|
||||
for (auto q : arc({0, -h / 2 + r}, r, -90, 0, 6)) p.push_back(q);
|
||||
for (auto q : arc({0, h / 2 - r}, r, 0, 90, 6)) p.push_back(q);
|
||||
for (auto& q : p) q.x = std::max(q.x, 0.0f);
|
||||
return lathe(p, N(s, 0));
|
||||
}},
|
||||
{"torus", "Torus (ring)", false, true,
|
||||
{{"Segments", 32, 3, 256, true}, {"Sides", 12, 3, 128, true}, {"Radius", 0.5f, 0.001f, 100}, {"Thickness", 0.15f, 0.001f, 100}},
|
||||
[](const std::vector<float>& s) { return makeTorus(N(s, 0), N(s, 1), S(s, 2), S(s, 3)); }},
|
||||
{"wedge", "Wedge (ramp)", false, false, {{"Width", 1, 0.001f, 100}, {"Length", 1, 0.001f, 100}, {"Height", 0.5f, 0.001f, 100}},
|
||||
[](const std::vector<float>& s) {
|
||||
const float l = S(s, 1), h = S(s, 2);
|
||||
return sideProfile({{-l / 2, 0}, {l / 2, 0}, {-l / 2, h}}, S(s, 0));
|
||||
}},
|
||||
{"stairs", "Stairs", false, false,
|
||||
{{"Steps", 6, 1, 100, true}, {"Width", 1, 0.001f, 100}, {"Length", 1.6f, 0.001f, 100}, {"Height", 1.0f, 0.001f, 100}},
|
||||
[](const std::vector<float>& s) {
|
||||
const int n = N(s, 0);
|
||||
const float L = S(s, 2), H = S(s, 3), d = L / float(n), h = H / float(n);
|
||||
std::vector<glm::vec2> p{{0, 0}, {L, 0}, {L, H}};
|
||||
for (int k = n - 1; k >= 1; --k) p.push_back({float(k) * d, float(k + 1) * h}), p.push_back({float(k) * d, float(k) * h});
|
||||
p.push_back({0, h});
|
||||
for (auto& q : p) q.x -= L / 2;
|
||||
return sideProfile(p, S(s, 1));
|
||||
}},
|
||||
{"arch", "Arch", false, false,
|
||||
{{"Width", 1.2f, 0.01f, 100}, {"Height", 2.2f, 0.01f, 100}, {"Thickness", 0.15f, 0.001f, 50}, {"Depth", 0.3f, 0.001f, 100}, {"Steps", 12, 2, 128, true}},
|
||||
[](const std::vector<float>& s) {
|
||||
const float W = S(s, 0) / 2, H = S(s, 1), t = std::min(S(s, 2), W * 0.9f), r = W - t, spring = std::max(H - t - r, 0.0f);
|
||||
std::vector<glm::vec2> p{{-W, 0}, {-W, H}, {W, H}, {W, 0}, {W - t, 0}};
|
||||
for (auto q : arc({0, spring}, r, 0, 180, N(s, 4))) p.push_back(q);
|
||||
p.push_back({-W + t, 0});
|
||||
// (drawn across X; turned so the opening faces forward)
|
||||
Mesh m = sideProfile(p, S(s, 3));
|
||||
transform(m, glm::rotate(glm::mat4(1.0f), kPi / 2, glm::vec3(0, 0, 1)));
|
||||
return m;
|
||||
}},
|
||||
// ── Starter models ──
|
||||
{"table", "Table", true, false,
|
||||
{{"Width", 1.2f, 0.05f, 20}, {"Depth", 0.7f, 0.05f, 20}, {"Height", 0.75f, 0.05f, 5}, {"Top thickness", 0.04f, 0.005f, 1}, {"Legs", 0.05f, 0.005f, 1}},
|
||||
[](const std::vector<float>& s) {
|
||||
const float w = S(s, 0) / 2, d = S(s, 1) / 2, h = S(s, 2), t = S(s, 3), l = S(s, 4), in = l;
|
||||
Mesh m;
|
||||
addBox(m, {-d, -w, h - t}, {d, w, h});
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const float x = (i & 1 ? 1 : -1) * (d - in - l / 2), y = (i & 2 ? 1 : -1) * (w - in - l / 2);
|
||||
addBox(m, {x - l / 2, y - l / 2, 0}, {x + l / 2, y + l / 2, h - t});
|
||||
}
|
||||
return m;
|
||||
}},
|
||||
{"chair", "Chair", true, false,
|
||||
{{"Width", 0.45f, 0.05f, 5}, {"Depth", 0.45f, 0.05f, 5}, {"Seat height", 0.46f, 0.05f, 3}, {"Back height", 0.45f, 0, 3}, {"Legs", 0.04f, 0.005f, 1}},
|
||||
[](const std::vector<float>& s) {
|
||||
const float w = S(s, 0) / 2, d = S(s, 1) / 2, h = S(s, 2), b = S(s, 3), l = S(s, 4), t = 0.04f;
|
||||
Mesh m;
|
||||
addBox(m, {-d, -w, h - t}, {d, w, h});
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const float x = (i & 1 ? 1 : -1) * (d - l / 2), y = (i & 2 ? 1 : -1) * (w - l / 2);
|
||||
addBox(m, {x - l / 2, y - l / 2, 0}, {x + l / 2, y + l / 2, h - t});
|
||||
}
|
||||
if (b > 0) { // the back, at the chair's back (−X): two posts and a board
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const float y = (i ? 1 : -1) * (w - l / 2);
|
||||
addBox(m, {-d, y - l / 2, h}, {-d + l, y + l / 2, h + b});
|
||||
}
|
||||
addBox(m, {-d, -w, h + b * 0.55f}, {-d + l * 0.8f, w, h + b});
|
||||
}
|
||||
return m;
|
||||
}},
|
||||
{"shelf", "Shelf", true, false,
|
||||
{{"Width", 0.8f, 0.05f, 20}, {"Depth", 0.3f, 0.05f, 5}, {"Height", 1.8f, 0.05f, 10}, {"Shelves", 5, 2, 30, true}, {"Board", 0.02f, 0.003f, 0.5f}},
|
||||
[](const std::vector<float>& s) {
|
||||
const float w = S(s, 0) / 2, d = S(s, 1) / 2, h = S(s, 2), t = S(s, 4);
|
||||
const int n = N(s, 3);
|
||||
Mesh m;
|
||||
addBox(m, {-d, -w, 0}, {d, -w + t, h});
|
||||
addBox(m, {-d, w - t, 0}, {d, w, h});
|
||||
addBox(m, {-d, -w + t, 0}, {-d + t, w - t, h}); // the back
|
||||
for (int k = 0; k < n; ++k) {
|
||||
const float z = (h - t) * float(k) / float(n - 1);
|
||||
addBox(m, {-d + t, -w + t, z}, {d, w - t, z + t});
|
||||
}
|
||||
return m;
|
||||
}},
|
||||
{"barrel", "Barrel", true, true, {{"Sides", 24, 6, 128, true}, {"Radius", 0.3f, 0.01f, 10}, {"Height", 0.9f, 0.01f, 10}, {"Bulge", 0.05f, 0, 1}},
|
||||
[](const std::vector<float>& s) {
|
||||
std::vector<glm::vec2> p;
|
||||
const float r = S(s, 1), h = S(s, 2);
|
||||
p.push_back({0, 0});
|
||||
for (int k = 0; k <= 8; ++k) {
|
||||
const float t = float(k) / 8.0f;
|
||||
p.push_back({r + S(s, 3) * std::sin(t * kPi), t * h});
|
||||
}
|
||||
p.push_back({0, h});
|
||||
Mesh m = lathe(p, N(s, 0));
|
||||
transform(m, at({0, 0, -h / 2}));
|
||||
return m;
|
||||
}},
|
||||
{"bottle", "Bottle", true, true, {{"Sides", 24, 6, 128, true}, {"Radius", 0.04f, 0.005f, 2}, {"Height", 0.3f, 0.02f, 5}, {"Neck", 0.013f, 0.002f, 1}},
|
||||
[](const std::vector<float>& s) {
|
||||
const float r = S(s, 1), h = S(s, 2), n = std::min(S(s, 3), r);
|
||||
std::vector<glm::vec2> p{{0, 0}, {r * 0.92f, 0}, {r, h * 0.03f}, {r, h * 0.6f}, {r * 0.75f, h * 0.7f}, {n, h * 0.8f}, {n, h * 0.97f},
|
||||
{n * 1.2f, h * 0.975f}, {n * 1.2f, h}, {0, h}};
|
||||
Mesh m = lathe(p, N(s, 0));
|
||||
transform(m, at({0, 0, -h / 2}));
|
||||
return m;
|
||||
}},
|
||||
{"pillar", "Pillar", true, true, {{"Sides", 16, 4, 128, true}, {"Radius", 0.2f, 0.01f, 10}, {"Height", 3, 0.1f, 50}},
|
||||
[](const std::vector<float>& s) {
|
||||
const float r = S(s, 1), h = S(s, 2), slab = r * 0.5f;
|
||||
Mesh m;
|
||||
addBox(m, {-r * 1.4f, -r * 1.4f, 0}, {r * 1.4f, r * 1.4f, slab});
|
||||
addBox(m, {-r * 1.4f, -r * 1.4f, h - slab}, {r * 1.4f, r * 1.4f, h});
|
||||
append(m, makeCylinder(N(s, 0), r, h - 2 * slab), at({0, 0, h / 2}));
|
||||
return m;
|
||||
}},
|
||||
{"door_frame", "Door frame", true, false,
|
||||
{{"Width", 0.9f, 0.1f, 10}, {"Height", 2.1f, 0.1f, 10}, {"Frame", 0.08f, 0.005f, 1}, {"Depth", 0.15f, 0.005f, 2}},
|
||||
[](const std::vector<float>& s) {
|
||||
const float W = S(s, 0) / 2 + S(s, 2), H = S(s, 1) + S(s, 2), t = S(s, 2);
|
||||
Mesh m = sideProfile({{-W, 0}, {-W, H}, {W, H}, {W, 0}, {W - t, 0}, {W - t, H - t}, {-W + t, H - t}, {-W + t, 0}}, S(s, 3));
|
||||
transform(m, glm::rotate(glm::mat4(1.0f), kPi / 2, glm::vec3(0, 0, 1)));
|
||||
return m;
|
||||
}},
|
||||
{"crate", "Crate", true, false, {{"Size", 0.6f, 0.05f, 10}, {"Frame", 0.06f, 0.005f, 1}},
|
||||
[](const std::vector<float>& s) {
|
||||
const float h = S(s, 0) / 2, f = std::min(S(s, 1), h * 0.45f);
|
||||
Mesh m = makeCube(S(s, 0));
|
||||
std::set<int> all;
|
||||
for (int i = 0; i < 6; ++i) all.insert(i);
|
||||
insetFaces(m, all, 1.0f - (h - f) / h); // a frame round each side, the panels set in
|
||||
extrudeFaces(m, {0}, -f * 0.3f);
|
||||
for (int i = 1; i < 6; ++i) extrudeFaces(m, {i}, -f * 0.3f);
|
||||
return m;
|
||||
}},
|
||||
};
|
||||
} // namespace
|
||||
|
||||
const std::vector<ShapeRecipe>& shapeRecipes() { return kRecipes; }
|
||||
|
||||
const ShapeRecipe* findRecipe(std::string_view id) {
|
||||
for (const ShapeRecipe& r : kRecipes)
|
||||
if (id == r.id) return &r;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<float> defaultSettings(const ShapeRecipe& r) {
|
||||
std::vector<float> s;
|
||||
for (const ShapeSetting& p : r.settings) s.push_back(p.value);
|
||||
return s;
|
||||
}
|
||||
|
||||
Mesh makeFromRecipe(const ShapeRecipe& r, std::vector<float> s) {
|
||||
for (size_t i = 0; i < r.settings.size(); ++i) {
|
||||
if (i >= s.size()) s.push_back(r.settings[i].value);
|
||||
s[i] = std::clamp(s[i], r.settings[i].min, r.settings[i].max);
|
||||
if (r.settings[i].whole) s[i] = std::round(s[i]);
|
||||
}
|
||||
Mesh m = r.make(s);
|
||||
// Standing on the ground at the origin.
|
||||
const glm::vec3 lo = boundsMin(m), hi = boundsMax(m);
|
||||
transform(m, at({-(lo.x + hi.x) / 2, -(lo.y + hi.y) / 2, -lo.z}));
|
||||
return m;
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
41
src/shapes.h
Normal file
41
src/shapes.h
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
// Shapes to start from, with settings you can change after adding one (until you edit its
|
||||
// shape by hand): the basic ones, and starter models (a table, a chair...). All stand on the
|
||||
// ground at their origin (z = 0 is their bottom), centred on it the other ways.
|
||||
#include "mesh.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace fry {
|
||||
|
||||
struct ShapeSetting {
|
||||
const char* name;
|
||||
float value, min, max;
|
||||
bool whole = false; // a count (sides, steps...)
|
||||
};
|
||||
|
||||
struct ShapeRecipe {
|
||||
const char* id; // as saved
|
||||
const char* name; // as shown
|
||||
bool starter; // a starter model (else a basic shape)
|
||||
bool smooth; // looks best smooth-shaded
|
||||
std::vector<ShapeSetting> settings;
|
||||
Mesh (*make)(const std::vector<float>& s);
|
||||
};
|
||||
|
||||
const std::vector<ShapeRecipe>& shapeRecipes();
|
||||
const ShapeRecipe* findRecipe(std::string_view id);
|
||||
// The recipe's mesh for these settings (missing ones take their defaults).
|
||||
Mesh makeFromRecipe(const ShapeRecipe& r, std::vector<float> settings);
|
||||
std::vector<float> defaultSettings(const ShapeRecipe& r);
|
||||
|
||||
// A box with rounded edges and corners (`radius`, in `segments` steps round each).
|
||||
Mesh makeRoundedBox(glm::vec3 size, float radius, int segments);
|
||||
// A shape turned round the Z axis: `profile` is (distance out, height) from the bottom up.
|
||||
// Points at distance 0 become a single point; open ends get a lid; `closed` joins the last
|
||||
// profile point back to the first (a ring, like a tube's cross-section).
|
||||
Mesh lathe(const std::vector<glm::vec2>& profile, int sides, bool closed = false);
|
||||
|
||||
} // namespace fry
|
||||
202
src/sticker.cpp
Normal file
202
src/sticker.cpp
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
// Pictures on top of the painting: stickers that wrap over edges, and pictures pinned by corners.
|
||||
#include "scene.h"
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <queue>
|
||||
|
||||
namespace fry {
|
||||
|
||||
int stickerFace(const Mesh& m, const PictureLayer& l) {
|
||||
if (l.face >= 0 && l.face < int(m.faces.size())) return l.face;
|
||||
int best = -1;
|
||||
float d = 1e30f;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f)
|
||||
if (const float e = glm::distance(faceCenter(m, int(f)), l.centre); e < d) d = e, best = int(f);
|
||||
return best;
|
||||
}
|
||||
|
||||
bool stickerFrame(const Mesh& m, const PictureLayer& l, glm::vec3& right, glm::vec3& up, glm::vec3& normal) {
|
||||
const int f = stickerFace(m, l);
|
||||
if (f < 0) return false;
|
||||
normal = faceNormal(m, f);
|
||||
up = l.up - normal * glm::dot(l.up, normal);
|
||||
if (glm::length(up) < 1e-6f) up = glm::cross(normal, std::abs(normal.x) < 0.9f ? glm::vec3(1, 0, 0) : glm::vec3(0, 1, 0));
|
||||
up = glm::normalize(up);
|
||||
right = glm::cross(up, normal);
|
||||
return true;
|
||||
}
|
||||
|
||||
void stampSticker(Image& dst, const Mesh& m, const std::vector<std::vector<glm::vec2>>& uvs, const PictureLayer& l, std::vector<uint8_t>* owner, uint8_t id) {
|
||||
if (!l.image || l.image->empty() || uvs.size() != m.faces.size() || l.width <= 0 || l.height <= 0) return;
|
||||
glm::vec3 right, up, n;
|
||||
if (!stickerFrame(m, l, right, up, n)) return;
|
||||
const int root = stickerFace(m, l);
|
||||
const glm::vec3 c = l.centre - n * glm::dot(l.centre - faceCenter(m, root), n); // (on its face's plane)
|
||||
const float hw = l.width * 0.5f, hh = l.height * 0.5f;
|
||||
const Image& src = *l.image;
|
||||
const glm::vec2 size(float(dst.w), float(dst.h));
|
||||
std::map<std::pair<int, int>, std::vector<int>> byEdge;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
const auto& fv = m.faces[f];
|
||||
for (size_t i = 0; i < fv.size(); ++i) byEdge[{std::min(fv[i], fv[(i + 1) % fv.size()]), std::max(fv[i], fv[(i + 1) % fv.size()])}].push_back(int(f));
|
||||
}
|
||||
std::vector<char> seen(m.faces.size(), 0), done(size_t(dst.w) * size_t(dst.h), 0);
|
||||
// Each face reached: how to fold it flat into the sticker's plane (turned about the edges
|
||||
// between it and the sticker's face, as a sticker bends over them). Nearest the sticker's
|
||||
// middle first, so a face round a corner gets the part of it that reaches it straight, not
|
||||
// what's left after going the long way round over the faces beside it.
|
||||
struct Todo {
|
||||
float near;
|
||||
int face;
|
||||
glm::mat4 fold;
|
||||
bool operator<(const Todo& o) const { return near > o.near; }
|
||||
};
|
||||
std::priority_queue<Todo> todo;
|
||||
todo.push({0.0f, root, glm::mat4(1.0f)});
|
||||
while (!todo.empty()) {
|
||||
const Todo next = todo.top();
|
||||
todo.pop();
|
||||
const int f = next.face;
|
||||
const glm::mat4 fold = next.fold;
|
||||
if (seen[size_t(f)]) continue;
|
||||
seen[size_t(f)] = 1;
|
||||
const auto& fv = m.faces[size_t(f)];
|
||||
auto flat = [&](const glm::vec3& p) {
|
||||
const glm::vec3 q = glm::vec3(fold * glm::vec4(p, 1.0f)) - c;
|
||||
return glm::vec2(glm::dot(q, right), glm::dot(q, up));
|
||||
};
|
||||
glm::vec2 lo(1e30f), hi(-1e30f);
|
||||
for (int v : fv) {
|
||||
const glm::vec2 q = flat(m.verts[size_t(v)]);
|
||||
lo = glm::min(lo, q), hi = glm::max(hi, q);
|
||||
}
|
||||
if (hi.x < -hw || lo.x > hw || hi.y < -hh || lo.y > hh) continue; // (the sticker doesn't reach it, nor past it)
|
||||
for (const auto& t : triangulate(m, f)) {
|
||||
glm::vec3 w[3];
|
||||
glm::vec2 px[3];
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
w[k] = m.verts[size_t(t[size_t(k)])];
|
||||
px[k] = uvs[size_t(f)][size_t(std::find(fv.begin(), fv.end(), t[size_t(k)]) - fv.begin())] * size;
|
||||
}
|
||||
const float twice = (px[1].x - px[0].x) * (px[2].y - px[0].y) - (px[2].x - px[0].x) * (px[1].y - px[0].y);
|
||||
if (std::abs(twice) < 1e-9f) continue;
|
||||
const glm::vec2 plo = glm::max(glm::min(px[0], glm::min(px[1], px[2])) - 2.0f, glm::vec2(0.0f));
|
||||
const glm::vec2 phi = glm::min(glm::max(px[0], glm::max(px[1], px[2])) + 2.0f, size - 1.0f);
|
||||
for (int y = int(plo.y); y <= int(phi.y); ++y)
|
||||
for (int x = int(plo.x); x <= int(phi.x); ++x) {
|
||||
char& d = done[size_t(y) * size_t(dst.w) + size_t(x)];
|
||||
if (d) continue;
|
||||
const glm::vec2 q(float(x) + 0.5f, float(y) + 0.5f);
|
||||
float b[3];
|
||||
bool in = true;
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const glm::vec2 a = px[(k + 1) % 3], e = px[(k + 2) % 3];
|
||||
b[k] = ((e.x - a.x) * (q.y - a.y) - (q.x - a.x) * (e.y - a.y)) / twice;
|
||||
in &= b[k] * std::abs(twice) / std::max(glm::length(e - a), 1e-6f) >= -1.5f; // (and a little past: no seams)
|
||||
}
|
||||
if (!in) continue;
|
||||
const glm::vec2 s = flat(w[0] * b[0] + w[1] * b[1] + w[2] * b[2]);
|
||||
const glm::vec2 uv(s.x / l.width + 0.5f, 0.5f - s.y / l.height);
|
||||
if (uv.x < 0 || uv.y < 0 || uv.x > 1 || uv.y > 1) continue;
|
||||
d = 1;
|
||||
const glm::vec4 col = sample(src, uv * glm::vec2(src.w, src.h));
|
||||
const float a = col.a * l.opacity;
|
||||
if (owner && a > 0.05f) (*owner)[size_t(y) * size_t(dst.w) + size_t(x)] = id;
|
||||
uint8_t* p = dst.at(x, y);
|
||||
for (int k = 0; k < 3; ++k) p[k] = uint8_t(std::lround(float(p[k]) + (col[k] * 255.0f - float(p[k])) * a));
|
||||
}
|
||||
}
|
||||
// On to the faces next to it, folded about the edge between.
|
||||
const glm::vec3 nf = faceNormal(m, f);
|
||||
for (size_t i = 0; i < fv.size(); ++i) {
|
||||
const int a = fv[i], b = fv[(i + 1) % fv.size()];
|
||||
// (how near the sticker's middle the edge comes, flat)
|
||||
const glm::vec2 ea = flat(m.verts[size_t(a)]), eb = flat(m.verts[size_t(b)]);
|
||||
const float along = glm::clamp(glm::dot(-ea, eb - ea) / std::max(glm::dot(eb - ea, eb - ea), 1e-12f), 0.0f, 1.0f);
|
||||
const float near = std::max(next.near, glm::length(ea + (eb - ea) * along));
|
||||
for (int g : byEdge[{std::min(a, b), std::max(a, b)}]) {
|
||||
if (seen[size_t(g)]) continue;
|
||||
const glm::vec3 ng = faceNormal(m, g), pa = m.verts[size_t(a)], e = glm::normalize(m.verts[size_t(b)] - pa);
|
||||
const float angle = std::atan2(glm::dot(glm::cross(ng, nf), e), glm::dot(ng, nf));
|
||||
todo.push({near, g, fold * glm::translate(glm::mat4(1.0f), pa) * glm::rotate(glm::mat4(1.0f), angle, e) * glm::translate(glm::mat4(1.0f), -pa)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void settleSticker(const Mesh& m, PictureLayer& l) {
|
||||
glm::vec3 right, up, n;
|
||||
if (!stickerFrame(m, l, right, up, n)) return;
|
||||
const int root = stickerFace(m, l);
|
||||
const glm::vec3 c = l.centre - n * glm::dot(l.centre - faceCenter(m, root), n);
|
||||
std::map<std::pair<int, int>, std::vector<int>> byEdge;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
const auto& fv = m.faces[f];
|
||||
for (size_t i = 0; i < fv.size(); ++i) byEdge[{std::min(fv[i], fv[(i + 1) % fv.size()]), std::max(fv[i], fv[(i + 1) % fv.size()])}].push_back(int(f));
|
||||
}
|
||||
// Unfolded nearest-first (as stampSticker does) until the face its middle lies on.
|
||||
struct Todo {
|
||||
float near;
|
||||
int face;
|
||||
glm::mat4 fold;
|
||||
bool operator<(const Todo& o) const { return near > o.near; }
|
||||
};
|
||||
std::priority_queue<Todo> todo;
|
||||
todo.push({0.0f, root, glm::mat4(1.0f)});
|
||||
std::vector<char> seen(m.faces.size(), 0);
|
||||
const float reach = std::max(l.width, l.height);
|
||||
while (!todo.empty()) {
|
||||
const Todo next = todo.top();
|
||||
todo.pop();
|
||||
if (seen[size_t(next.face)] || next.near > reach) continue;
|
||||
seen[size_t(next.face)] = 1;
|
||||
const auto& fv = m.faces[size_t(next.face)];
|
||||
std::vector<glm::vec2> poly;
|
||||
for (int v : fv) {
|
||||
const glm::vec3 q = glm::vec3(next.fold * glm::vec4(m.verts[size_t(v)], 1.0f)) - c;
|
||||
poly.push_back({glm::dot(q, right), glm::dot(q, up)});
|
||||
}
|
||||
bool inside = false; // (the middle, 0 0, in the face as unfolded)
|
||||
for (size_t i = 0, j = poly.size() - 1; i < poly.size(); j = i++)
|
||||
if ((poly[i].y > 0) != (poly[j].y > 0) && 0 < (poly[j].x - poly[i].x) * (0 - poly[i].y) / (poly[j].y - poly[i].y) + poly[i].x) inside = !inside;
|
||||
if (inside) {
|
||||
if (next.face == root) return;
|
||||
const glm::mat4 back = glm::inverse(next.fold); // (the sticker's plane folded onto that face)
|
||||
l.face = next.face;
|
||||
l.centre = glm::vec3(back * glm::vec4(c, 1.0f));
|
||||
l.up = glm::normalize(glm::vec3(back * glm::vec4(up, 0.0f)));
|
||||
return;
|
||||
}
|
||||
const glm::vec3 nf = faceNormal(m, next.face);
|
||||
for (size_t i = 0; i < fv.size(); ++i) {
|
||||
const int a = fv[i], b = fv[(i + 1) % fv.size()];
|
||||
const glm::vec2 ea = poly[i], eb = poly[(i + 1) % fv.size()];
|
||||
const float along = glm::clamp(glm::dot(-ea, eb - ea) / std::max(glm::dot(eb - ea, eb - ea), 1e-12f), 0.0f, 1.0f);
|
||||
const float near = std::max(next.near, glm::length(ea + (eb - ea) * along));
|
||||
for (int g : byEdge[{std::min(a, b), std::max(a, b)}]) {
|
||||
if (seen[size_t(g)]) continue;
|
||||
const glm::vec3 ng = faceNormal(m, g), pa = m.verts[size_t(a)], e = glm::normalize(m.verts[size_t(b)] - pa);
|
||||
const float angle = std::atan2(glm::dot(glm::cross(ng, nf), e), glm::dot(ng, nf));
|
||||
todo.push({near, g, next.fold * glm::translate(glm::mat4(1.0f), pa) * glm::rotate(glm::mat4(1.0f), angle, e) * glm::translate(glm::mat4(1.0f), -pa)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image composePicture(const Object& o, std::vector<uint8_t>* owner) {
|
||||
Image img = *o.picture;
|
||||
if (owner) owner->assign(size_t(img.w) * size_t(img.h), 0);
|
||||
for (size_t i = 0; i < o.layers.size() && i < 255; ++i) {
|
||||
const PictureLayer& l = o.layers[i];
|
||||
if (!l.visible || !l.image) continue;
|
||||
if (l.sticker) stampSticker(img, o.mesh, o.uvs, l, owner, uint8_t(i + 1));
|
||||
else stampQuad(img, *l.image, l.corners, l.opacity, owner, uint8_t(i + 1));
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
491
src/tools.cpp
Normal file
491
src/tools.cpp
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
// The bigger mesh tools: rounding edges, smoothing, loop cuts, and finding loops and rings.
|
||||
#include "mesh.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
|
||||
namespace fry {
|
||||
|
||||
namespace {
|
||||
using Edge = std::pair<int, int>;
|
||||
Edge key(int a, int b) { return a < b ? Edge{a, b} : Edge{b, a}; }
|
||||
|
||||
void tidy(std::vector<int>& f) {
|
||||
f.erase(std::unique(f.begin(), f.end()), f.end());
|
||||
while (f.size() > 1 && f.front() == f.back()) f.pop_back();
|
||||
}
|
||||
|
||||
// Each edge as its faces run along it: (from, to) → (face, the corner it starts at).
|
||||
std::map<Edge, std::pair<int, int>> directed(const Mesh& m) {
|
||||
std::map<Edge, std::pair<int, int>> d;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
const auto& fv = m.faces[f];
|
||||
for (size_t i = 0; i < fv.size(); ++i) d[{fv[i], fv[(i + 1) % fv.size()]}] = {int(f), int(i)};
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
glm::vec3 normalOf(const std::vector<glm::vec3>& v, const std::vector<int>& f) { // (Newell's)
|
||||
glm::vec3 n(0.0f);
|
||||
for (size_t i = 0; i < f.size(); ++i) {
|
||||
const glm::vec3 a = v[size_t(f[i])], b = v[size_t(f[(i + 1) % f.size()])];
|
||||
n += glm::vec3((a.y - b.y) * (a.z + b.z), (a.z - b.z) * (a.x + b.x), (a.x - b.x) * (a.y + b.y));
|
||||
}
|
||||
return glm::length(n) > 1e-20f ? glm::normalize(n) : n;
|
||||
}
|
||||
|
||||
std::map<int, std::set<int>> neighbours(const Mesh& m) {
|
||||
std::map<int, std::set<int>> n;
|
||||
for (const auto& f : m.faces)
|
||||
for (size_t i = 0; i < f.size(); ++i) {
|
||||
const int a = f[i], b = f[(i + 1) % f.size()];
|
||||
n[a].insert(b), n[b].insert(a);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::set<std::pair<int, int>> sharpEdges(const Mesh& m, float angle) {
|
||||
const auto d = directed(m);
|
||||
const float c = std::cos(glm::radians(angle));
|
||||
std::set<Edge> out;
|
||||
for (const auto& [e, fc] : d) {
|
||||
if (e.first > e.second) continue;
|
||||
auto twin = d.find({e.second, e.first});
|
||||
if (twin != d.end() && glm::dot(faceNormal(m, fc.first), faceNormal(m, twin->second.first)) < c) out.insert(e);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── Bevel ──
|
||||
//
|
||||
// Every face corner at a point of a rounded edge is replaced: where both its edges are rounded,
|
||||
// by a point inside the face `width` from both; where one is, by a point slid along the other
|
||||
// (shared with the face across it); where neither, by the point itself plus any slid points on
|
||||
// its edges. Each rounded edge then gets a strip between its two faces' new edges, curved along a
|
||||
// circle; whatever holes are left round a point (the corners where several strips meet) are
|
||||
// filled, as part of a sphere when they're rounded corners, flat otherwise.
|
||||
void bevelEdges(Mesh& m, const std::set<std::pair<int, int>>& want, float width, int segments) {
|
||||
if (width <= 0.0f) return;
|
||||
segments = std::clamp(segments, 1, 16);
|
||||
const auto d = directed(m);
|
||||
std::set<Edge> bev;
|
||||
for (auto [a, b] : want)
|
||||
if (d.count({a, b}) && d.count({b, a})) bev.insert(key(a, b));
|
||||
if (bev.empty()) return;
|
||||
auto isBev = [&](int a, int b) { return bev.count(key(a, b)) > 0; };
|
||||
std::set<int> at;
|
||||
for (auto [a, b] : bev) at.insert(a), at.insert(b);
|
||||
const int nOrig = int(m.verts.size());
|
||||
const std::vector<glm::vec3> orig = m.verts;
|
||||
std::map<int, int> owner; // new point → the point it came from
|
||||
auto add = [&](glm::vec3 p, int from) {
|
||||
owner[int(m.verts.size())] = from;
|
||||
m.verts.push_back(p);
|
||||
return int(m.verts.size()) - 1;
|
||||
};
|
||||
auto dirTo = [&](int v, int w) {
|
||||
const glm::vec3 x = orig[size_t(w)] - orig[size_t(v)];
|
||||
const float l = glm::length(x);
|
||||
return l > 1e-12f ? x / l : glm::vec3(0.0f);
|
||||
};
|
||||
auto sinOf = [](glm::vec3 a, glm::vec3 b) { return std::max(glm::length(glm::cross(a, b)), 0.05f); };
|
||||
|
||||
// Points slid along edges, shared by the faces either side.
|
||||
std::map<Edge, std::pair<glm::vec3, int>> slideSum; // (point, towards) → sum, count
|
||||
for (const auto& fv : m.faces)
|
||||
for (size_t i = 0; i < fv.size(); ++i) {
|
||||
const int v = fv[i], prev = fv[(i + fv.size() - 1) % fv.size()], next = fv[(i + 1) % fv.size()];
|
||||
if (!at.count(v)) continue;
|
||||
const bool in = isBev(prev, v), out = isBev(v, next);
|
||||
if (in == out) continue;
|
||||
const glm::vec3 a = dirTo(v, prev), b = dirTo(v, next);
|
||||
const int along = in ? next : prev;
|
||||
const float t = std::min(width / sinOf(a, b), glm::distance(orig[size_t(v)], orig[size_t(along)]) * 0.49f);
|
||||
auto& s = slideSum[{v, along}];
|
||||
s.first += orig[size_t(v)] + (in ? b : a) * t, ++s.second;
|
||||
}
|
||||
std::map<Edge, int> slide;
|
||||
for (const auto& [k, s] : slideSum) slide[k] = add(s.first / float(s.second), k.first);
|
||||
|
||||
// What each face corner becomes.
|
||||
std::vector<std::vector<std::vector<int>>> rep(m.faces.size());
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
const auto& fv = m.faces[f];
|
||||
rep[f].resize(fv.size());
|
||||
for (size_t i = 0; i < fv.size(); ++i) {
|
||||
const int v = fv[i], prev = fv[(i + fv.size() - 1) % fv.size()], next = fv[(i + 1) % fv.size()];
|
||||
auto& r = rep[f][i];
|
||||
if (!at.count(v)) {
|
||||
r = {v};
|
||||
continue;
|
||||
}
|
||||
const bool in = isBev(prev, v), out = isBev(v, next);
|
||||
if (in && out) {
|
||||
const glm::vec3 a = dirTo(v, prev), b = dirTo(v, next);
|
||||
const float most = std::min(glm::distance(orig[size_t(v)], orig[size_t(prev)]), glm::distance(orig[size_t(v)], orig[size_t(next)])) * 0.49f;
|
||||
glm::vec3 off = (a + b) * (width / sinOf(a, b));
|
||||
if (glm::length(off) > most * 1.42f) off *= most * 1.42f / glm::length(off);
|
||||
r = {add(orig[size_t(v)] + off, v)};
|
||||
} else if (in) r = {slide[{v, next}]};
|
||||
else if (out) r = {slide[{v, prev}]};
|
||||
else {
|
||||
const auto sp = slide.find({v, prev}), sn = slide.find({v, next});
|
||||
if (sp != slide.end()) r.push_back(sp->second);
|
||||
if (sp == slide.end() || sn == slide.end()) r.push_back(v);
|
||||
if (sn != slide.end()) r.push_back(sn->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<std::vector<int>> faces;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
std::vector<int> nf;
|
||||
for (const auto& r : rep[f]) nf.insert(nf.end(), r.begin(), r.end());
|
||||
faces.push_back(std::move(nf));
|
||||
}
|
||||
|
||||
// The strips, curved: a conic from one side to the other, its middle towards where the old
|
||||
// edge was (a true circle when both sides are the same distance from it).
|
||||
struct Round {
|
||||
glm::vec3 centre;
|
||||
float radius;
|
||||
};
|
||||
std::map<int, std::vector<Round>> rounds; // by original point
|
||||
std::map<Edge, std::vector<int>> arcs;
|
||||
auto arc = [&](int p, int q, int v, glm::vec3 dir) {
|
||||
if (p == q) return std::vector<int>(size_t(segments + 1), p);
|
||||
if (auto it = arcs.find({p, q}); it != arcs.end()) return it->second;
|
||||
if (auto it = arcs.find({q, p}); it != arcs.end()) return std::vector<int>(it->second.rbegin(), it->second.rend());
|
||||
const glm::vec3 A = m.verts[size_t(p)], B = m.verts[size_t(q)], V = orig[size_t(v)];
|
||||
const glm::vec3 C = V + dir * std::max(glm::dot(A - V, dir), glm::dot(B - V, dir));
|
||||
const glm::vec3 la = A - C, lb = B - C;
|
||||
const float lenA = glm::length(la), lenB = glm::length(lb);
|
||||
const float cosPhi = lenA > 1e-9f && lenB > 1e-9f ? std::clamp(glm::dot(la, lb) / (lenA * lenB), -1.0f, 1.0f) : -1.0f;
|
||||
const float phi = std::acos(cosPhi), w = std::sin(phi * 0.5f);
|
||||
std::vector<int> pts{p};
|
||||
for (int k = 1; k < segments; ++k) {
|
||||
const float t = float(k) / float(segments), b0 = (1 - t) * (1 - t), b1 = 2 * t * (1 - t) * w, b2 = t * t;
|
||||
pts.push_back(add((A * b0 + C * b1 + B * b2) / (b0 + b1 + b2), v));
|
||||
}
|
||||
pts.push_back(q);
|
||||
if (segments > 1 && 1.0f + cosPhi > 1e-3f) rounds[v].push_back({C + (la + lb) / (1.0f + cosPhi), (lenA + lenB) * 0.5f * std::tan(phi * 0.5f)});
|
||||
arcs[{p, q}] = pts;
|
||||
return pts;
|
||||
};
|
||||
for (auto [a, b] : bev) {
|
||||
const auto [f, ia] = d.at({a, b});
|
||||
const auto [g, ib] = d.at({b, a});
|
||||
const size_t nf = rep[size_t(f)].size(), ng = rep[size_t(g)].size();
|
||||
const int Aa = rep[size_t(f)][size_t(ia)].back(), Ab = rep[size_t(f)][(size_t(ia) + 1) % nf].front();
|
||||
const int Bb = rep[size_t(g)][size_t(ib)].back(), Ba = rep[size_t(g)][(size_t(ib) + 1) % ng].front();
|
||||
const glm::vec3 dir = dirTo(a, b);
|
||||
const auto atA = arc(Aa, Ba, a, dir), atB = arc(Ab, Bb, b, -dir);
|
||||
for (int k = 0; k < segments; ++k) faces.push_back({atB[size_t(k)], atA[size_t(k)], atA[size_t(k) + 1], atB[size_t(k) + 1]});
|
||||
}
|
||||
for (auto& f : faces) tidy(f);
|
||||
std::erase_if(faces, [](const std::vector<int>& f) { return f.size() < 3; });
|
||||
|
||||
// Fill the holes round the points.
|
||||
auto relevant = [&](int x) { return x >= nOrig || at.count(x); };
|
||||
std::set<Edge> has;
|
||||
for (const auto& f : faces)
|
||||
for (size_t i = 0; i < f.size(); ++i) has.insert({f[i], f[(i + 1) % f.size()]});
|
||||
std::map<int, int> nextOf;
|
||||
std::set<int> unclear;
|
||||
for (auto [x, y] : has)
|
||||
if (!has.count({y, x}) && relevant(x) && relevant(y)) {
|
||||
if (nextOf.count(y)) unclear.insert(y);
|
||||
nextOf[y] = x;
|
||||
}
|
||||
std::set<int> used;
|
||||
std::map<Edge, int> faceOfEdge;
|
||||
for (size_t f = 0; f < faces.size(); ++f)
|
||||
for (size_t i = 0; i < faces[f].size(); ++i) faceOfEdge[{faces[f][i], faces[f][(i + 1) % faces[f].size()]}] = int(f);
|
||||
std::set<int> mergedInto;
|
||||
for (const auto& [start, unused] : nextOf) {
|
||||
if (used.count(start) || unclear.count(start)) continue;
|
||||
std::vector<int> loop{start};
|
||||
bool ok = false;
|
||||
for (int x = nextOf[start], n = 0; n < 4096; ++n) {
|
||||
if (x == start) {
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
if (!nextOf.count(x) || unclear.count(x) || used.count(x)) break;
|
||||
loop.push_back(x);
|
||||
x = nextOf[x];
|
||||
}
|
||||
if (!ok || loop.size() < 3) continue;
|
||||
used.insert(loop.begin(), loop.end());
|
||||
const bool hasOrig = std::any_of(loop.begin(), loop.end(), [&](int x) { return x < nOrig; });
|
||||
const int v = hasOrig ? -1 : owner[loop[0]];
|
||||
if (!hasOrig && segments > 1 && rounds.count(v)) { // a rounded corner: part of a sphere
|
||||
glm::vec3 O(0.0f), c(0.0f);
|
||||
float r = 0.0f;
|
||||
for (const Round& rd : rounds[v]) O += rd.centre, r += rd.radius;
|
||||
O /= float(rounds[v].size()), r /= float(rounds[v].size());
|
||||
for (int x : loop) c += m.verts[size_t(x)];
|
||||
c /= float(loop.size());
|
||||
auto onBall = [&](glm::vec3 p) { return glm::length(p - O) > 1e-9f ? O + glm::normalize(p - O) * r : p; };
|
||||
const int rings = std::max(1, segments / 2);
|
||||
std::vector<int> prev = loop;
|
||||
for (int j = 1; j < rings; ++j) {
|
||||
std::vector<int> ring;
|
||||
for (int x : loop) ring.push_back(add(onBall(glm::mix(m.verts[size_t(x)], c, float(j) / float(rings))), v));
|
||||
for (size_t i = 0; i < loop.size(); ++i) faces.push_back({prev[i], prev[(i + 1) % loop.size()], ring[(i + 1) % loop.size()], ring[i]});
|
||||
prev = ring;
|
||||
}
|
||||
const int mid = add(onBall(c), v);
|
||||
for (size_t i = 0; i < loop.size(); ++i) faces.push_back({prev[i], prev[(i + 1) % loop.size()], mid});
|
||||
continue;
|
||||
}
|
||||
// Flat: joined into the face beside it when that's flat with it (one face, not two).
|
||||
const glm::vec3 n = normalOf(m.verts, loop);
|
||||
bool merged = false;
|
||||
for (size_t i = 0; i < loop.size() && !merged; ++i) {
|
||||
const int x = loop[i], y = loop[(i + 1) % loop.size()];
|
||||
auto it = faceOfEdge.find({y, x});
|
||||
if (it == faceOfEdge.end() || mergedInto.count(it->second)) continue;
|
||||
auto& g = faces[size_t(it->second)];
|
||||
int shared = 0;
|
||||
for (size_t k = 0; k < loop.size(); ++k) shared += faceOfEdge.count({loop[(k + 1) % loop.size()], loop[k]}) && faceOfEdge[{loop[(k + 1) % loop.size()], loop[k]}] == it->second;
|
||||
if (shared != 1 || glm::dot(normalOf(m.verts, g), n) < 0.999f) continue;
|
||||
// g runs y → x; put the loop's way from y round to x between them.
|
||||
std::vector<int> path;
|
||||
for (size_t k = 1; k + 1 < loop.size(); ++k) path.push_back(loop[(i + 1 + k) % loop.size()]);
|
||||
const auto pos = size_t(std::find(g.begin(), g.end(), y) - g.begin());
|
||||
g.insert(g.begin() + std::ptrdiff_t(pos + 1), path.begin(), path.end());
|
||||
mergedInto.insert(it->second);
|
||||
merged = true;
|
||||
}
|
||||
if (!merged) faces.push_back(loop);
|
||||
}
|
||||
m.faces = std::move(faces);
|
||||
removeUnused(m);
|
||||
}
|
||||
|
||||
// ── Subdivide (Catmull–Clark) ──
|
||||
|
||||
void subdivide(Mesh& m, bool smooth, float keepAngle) {
|
||||
if (m.faces.empty()) return;
|
||||
const auto d = directed(m);
|
||||
const std::set<Edge> sharp = keepAngle < 179.9f ? sharpEdges(m, keepAngle) : std::set<Edge>{};
|
||||
auto crease = [&](int a, int b) { return !d.count({a, b}) || !d.count({b, a}) || sharp.count(key(a, b)); };
|
||||
Mesh out;
|
||||
out.verts = m.verts;
|
||||
std::vector<int> facePt(m.faces.size());
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) facePt[f] = int(out.verts.size()), out.verts.push_back(faceCenter(m, int(f)));
|
||||
std::map<Edge, int> edgePt;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
const auto& fv = m.faces[f];
|
||||
for (size_t i = 0; i < fv.size(); ++i) {
|
||||
const int a = fv[i], b = fv[(i + 1) % fv.size()];
|
||||
if (edgePt.count(key(a, b))) continue;
|
||||
glm::vec3 p = (m.verts[size_t(a)] + m.verts[size_t(b)]) * 0.5f;
|
||||
if (smooth && !crease(a, b))
|
||||
p = (m.verts[size_t(a)] + m.verts[size_t(b)] + out.verts[size_t(facePt[size_t(d.at({a, b}).first)])] +
|
||||
out.verts[size_t(facePt[size_t(d.at({b, a}).first)])]) *
|
||||
0.25f;
|
||||
edgePt[key(a, b)] = int(out.verts.size());
|
||||
out.verts.push_back(p);
|
||||
}
|
||||
}
|
||||
if (smooth) {
|
||||
std::vector<glm::vec3> faceSum(m.verts.size(), glm::vec3(0.0f));
|
||||
std::vector<int> faceCount(m.verts.size(), 0);
|
||||
for (size_t f = 0; f < m.faces.size(); ++f)
|
||||
for (int v : m.faces[f]) faceSum[size_t(v)] += out.verts[size_t(facePt[f])], ++faceCount[size_t(v)];
|
||||
for (const auto& [v, nb] : neighbours(m)) {
|
||||
const glm::vec3 P = m.verts[size_t(v)];
|
||||
std::vector<int> creases;
|
||||
glm::vec3 mid(0.0f);
|
||||
for (int w : nb) {
|
||||
mid += (P + m.verts[size_t(w)]) * 0.5f;
|
||||
if (crease(v, w)) creases.push_back(w);
|
||||
}
|
||||
const float n = float(nb.size());
|
||||
if (creases.size() >= 3) continue; // a corner: stays
|
||||
if (creases.size() == 2) out.verts[size_t(v)] = P * 0.75f + (m.verts[size_t(creases[0])] + m.verts[size_t(creases[1])]) * 0.125f;
|
||||
else if (faceCount[size_t(v)] > 0 && n >= 3)
|
||||
out.verts[size_t(v)] = (faceSum[size_t(v)] / float(faceCount[size_t(v)]) + 2.0f * mid / n + (n - 3.0f) * P) / n;
|
||||
}
|
||||
}
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
const auto& fv = m.faces[f];
|
||||
const size_t n = fv.size();
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
out.faces.push_back({fv[i], edgePt[key(fv[i], fv[(i + 1) % n])], facePt[f], edgePt[key(fv[(i + n - 1) % n], fv[i])]});
|
||||
}
|
||||
m = std::move(out);
|
||||
removeUnused(m);
|
||||
}
|
||||
|
||||
// ── Rings and loops ──
|
||||
|
||||
namespace {
|
||||
// The four-sided faces across an edge, each with the corner its way in starts at: the way in is
|
||||
// corners (i, i+1), the way out (i+3, i+2), so each "first" point is on the same side.
|
||||
std::vector<std::pair<int, int>> ringSteps(const Mesh& m, Edge e) {
|
||||
const auto d = directed(m);
|
||||
std::vector<std::pair<int, int>> steps;
|
||||
std::set<int> seen;
|
||||
auto walk = [&](int a, int b, bool front) {
|
||||
std::vector<std::pair<int, int>> got;
|
||||
for (;;) {
|
||||
auto it = d.find({a, b});
|
||||
if (it == d.end()) break;
|
||||
const auto [f, i] = it->second;
|
||||
const auto& fv = m.faces[size_t(f)];
|
||||
if (fv.size() != 4 || seen.count(f)) break;
|
||||
seen.insert(f);
|
||||
got.push_back({f, i});
|
||||
a = fv[(size_t(i) + 3) % 4], b = fv[(size_t(i) + 2) % 4];
|
||||
}
|
||||
if (front) steps = got;
|
||||
else steps.insert(steps.begin(), got.rbegin(), got.rend());
|
||||
};
|
||||
walk(e.first, e.second, true);
|
||||
walk(e.second, e.first, false);
|
||||
return steps;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::vector<int> faceRing(const Mesh& m, std::pair<int, int> edge) {
|
||||
std::vector<int> out;
|
||||
for (auto [f, i] : ringSteps(m, edge)) out.push_back(f);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::array<int, 4>> ringQuads(const Mesh& m, std::pair<int, int> edge) {
|
||||
std::vector<std::array<int, 4>> out;
|
||||
for (auto [f, i] : ringSteps(m, edge)) {
|
||||
const auto& fv = m.faces[size_t(f)];
|
||||
out.push_back({fv[size_t(i)], fv[(size_t(i) + 1) % 4], fv[(size_t(i) + 2) % 4], fv[(size_t(i) + 3) % 4]});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<int> loopCut(Mesh& m, std::pair<int, int> edge, int cuts) {
|
||||
cuts = std::clamp(cuts, 1, 64);
|
||||
const auto steps = ringSteps(m, edge);
|
||||
if (steps.empty()) return {};
|
||||
std::map<Edge, std::vector<int>> pts; // cut edge (lower first) → its new points, lower end first
|
||||
std::vector<int> made;
|
||||
auto along = [&](int p, int q) {
|
||||
const Edge k = key(p, q);
|
||||
auto it = pts.find(k);
|
||||
if (it == pts.end()) {
|
||||
std::vector<int> v;
|
||||
for (int c = 1; c <= cuts; ++c) {
|
||||
v.push_back(int(m.verts.size()));
|
||||
made.push_back(v.back());
|
||||
m.verts.push_back(glm::mix(m.verts[size_t(k.first)], m.verts[size_t(k.second)], float(c) / float(cuts + 1)));
|
||||
}
|
||||
it = pts.emplace(k, v).first;
|
||||
}
|
||||
std::vector<int> v = it->second;
|
||||
if (p > q) std::reverse(v.begin(), v.end());
|
||||
return v;
|
||||
};
|
||||
std::set<int> ring;
|
||||
std::vector<std::vector<int>> added;
|
||||
for (auto [f, i] : steps) {
|
||||
ring.insert(f);
|
||||
const auto fv = m.faces[size_t(f)];
|
||||
const int x0 = fv[size_t(i)], x1 = fv[(size_t(i) + 1) % 4], x2 = fv[(size_t(i) + 2) % 4], x3 = fv[(size_t(i) + 3) % 4];
|
||||
std::vector<int> l0{x0}, l1{x3};
|
||||
for (int p : along(x0, x1)) l0.push_back(p);
|
||||
for (int p : along(x3, x2)) l1.push_back(p);
|
||||
l0.push_back(x1), l1.push_back(x2);
|
||||
for (size_t k = 0; k + 1 < l0.size(); ++k) added.push_back({l0[k], l0[k + 1], l1[k + 1], l1[k]});
|
||||
}
|
||||
std::vector<std::vector<int>> faces;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
if (ring.count(int(f))) continue;
|
||||
const auto& fv = m.faces[f];
|
||||
std::vector<int> nf;
|
||||
for (size_t i = 0; i < fv.size(); ++i) {
|
||||
const int p = fv[i], q = fv[(i + 1) % fv.size()];
|
||||
nf.push_back(p);
|
||||
if (pts.count(key(p, q))) // (a face at the ring's end: the cut points go into its edge)
|
||||
for (int x : along(p, q)) nf.push_back(x);
|
||||
}
|
||||
faces.push_back(std::move(nf));
|
||||
}
|
||||
faces.insert(faces.end(), added.begin(), added.end());
|
||||
m.faces = std::move(faces);
|
||||
return made;
|
||||
}
|
||||
|
||||
std::set<std::pair<int, int>> edgeLoop(const Mesh& m, std::pair<int, int> edge) {
|
||||
const auto nb = neighbours(m);
|
||||
std::map<Edge, std::set<int>> facesOf;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f)
|
||||
for (size_t i = 0; i < m.faces[f].size(); ++i) facesOf[key(m.faces[f][i], m.faces[f][(i + 1) % m.faces[f].size()])].insert(int(f));
|
||||
std::set<Edge> out{key(edge.first, edge.second)};
|
||||
for (int dir = 0; dir < 2; ++dir) {
|
||||
int prev = dir ? edge.second : edge.first, cur = dir ? edge.first : edge.second;
|
||||
for (int n = 0; n < 100000; ++n) {
|
||||
auto it = nb.find(cur);
|
||||
if (it == nb.end() || it->second.size() != 4) break;
|
||||
const auto& besideFaces = facesOf[key(prev, cur)];
|
||||
int next = -1;
|
||||
for (int w : it->second) {
|
||||
if (w == prev) continue;
|
||||
bool touches = false;
|
||||
for (int f : facesOf[key(cur, w)]) touches |= besideFaces.count(f) > 0;
|
||||
if (!touches) next = w;
|
||||
}
|
||||
if (next < 0 || !out.insert(key(cur, next)).second) break;
|
||||
prev = cur, cur = next;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::set<int> linkedVerts(const Mesh& m, const std::set<int>& verts) {
|
||||
const auto nb = neighbours(m);
|
||||
std::set<int> out = verts;
|
||||
std::vector<int> todo(verts.begin(), verts.end());
|
||||
while (!todo.empty()) {
|
||||
const int v = todo.back();
|
||||
todo.pop_back();
|
||||
if (auto it = nb.find(v); it != nb.end())
|
||||
for (int w : it->second)
|
||||
if (out.insert(w).second) todo.push_back(w);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::set<int> linkedFaces(const Mesh& m, const std::set<int>& faces) {
|
||||
std::set<int> verts;
|
||||
for (int f : faces)
|
||||
if (f < int(m.faces.size())) verts.insert(m.faces[size_t(f)].begin(), m.faces[size_t(f)].end());
|
||||
verts = linkedVerts(m, verts);
|
||||
std::set<int> out;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f)
|
||||
if (verts.count(m.faces[f][0])) out.insert(int(f));
|
||||
return out;
|
||||
}
|
||||
|
||||
std::set<int> growVerts(const Mesh& m, const std::set<int>& verts) {
|
||||
const auto nb = neighbours(m);
|
||||
std::set<int> out = verts;
|
||||
for (int v : verts)
|
||||
if (auto it = nb.find(v); it != nb.end()) out.insert(it->second.begin(), it->second.end());
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
|
||||
namespace fry {
|
||||
|
||||
std::set<std::pair<int, int>> edgeRing(const Mesh& m, std::pair<int, int> edge) {
|
||||
auto key = [](int a, int b) { return std::pair{std::min(a, b), std::max(a, b)}; };
|
||||
std::set<std::pair<int, int>> out{key(edge.first, edge.second)};
|
||||
for (const auto& q : ringQuads(m, edge)) out.insert(key(q[0], q[1])), out.insert(key(q[3], q[2]));
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
175
src/uv.cpp
Normal file
175
src/uv.cpp
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
// Laying a shape out flat for a picture: pieces facing about the same way, packed in rows.
|
||||
#include "mesh.h"
|
||||
#include "scene.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
|
||||
namespace fry {
|
||||
|
||||
std::vector<std::vector<glm::vec2>> autoUnwrap(const Mesh& m, float margin) {
|
||||
const size_t F = m.faces.size();
|
||||
std::vector<std::vector<glm::vec2>> uv(F);
|
||||
if (!F) return uv;
|
||||
std::vector<glm::vec3> normal(F);
|
||||
std::vector<float> area(F);
|
||||
for (size_t f = 0; f < F; ++f) normal[f] = faceNormal(m, int(f)), area[f] = faceArea(m, int(f));
|
||||
std::map<std::pair<int, int>, std::vector<int>> byEdge;
|
||||
for (size_t f = 0; f < F; ++f) {
|
||||
const auto& fv = m.faces[f];
|
||||
for (size_t i = 0; i < fv.size(); ++i) byEdge[{std::min(fv[i], fv[(i + 1) % fv.size()]), std::max(fv[i], fv[(i + 1) % fv.size()])}].push_back(int(f));
|
||||
}
|
||||
// Pieces: from the biggest faces out, taking in neighbours that face within 60 degrees.
|
||||
std::vector<int> order(F), chart(F, -1);
|
||||
for (size_t f = 0; f < F; ++f) order[f] = int(f);
|
||||
std::sort(order.begin(), order.end(), [&](int a, int b) { return area[size_t(a)] > area[size_t(b)]; });
|
||||
struct Chart {
|
||||
std::vector<int> faces;
|
||||
glm::vec2 lo{1e30f}, hi{-1e30f}, at{0.0f};
|
||||
};
|
||||
std::vector<Chart> charts;
|
||||
const float limit = std::cos(glm::radians(60.0f));
|
||||
for (int seed : order) {
|
||||
if (chart[size_t(seed)] >= 0) continue;
|
||||
const int id = int(charts.size());
|
||||
charts.push_back({});
|
||||
glm::vec3 sum = normal[size_t(seed)] * area[size_t(seed)];
|
||||
std::vector<int> todo{seed};
|
||||
chart[size_t(seed)] = id;
|
||||
while (!todo.empty()) {
|
||||
const int f = todo.back();
|
||||
todo.pop_back();
|
||||
charts.back().faces.push_back(f);
|
||||
const auto& fv = m.faces[size_t(f)];
|
||||
for (size_t i = 0; i < fv.size(); ++i)
|
||||
for (int g : byEdge[{std::min(fv[i], fv[(i + 1) % fv.size()]), std::max(fv[i], fv[(i + 1) % fv.size()])}]) {
|
||||
const glm::vec3 avg = glm::length(sum) > 0 ? glm::normalize(sum) : normal[size_t(seed)];
|
||||
if (chart[size_t(g)] < 0 && glm::dot(normal[size_t(g)], normal[size_t(seed)]) > limit && glm::dot(normal[size_t(g)], avg) > limit) {
|
||||
chart[size_t(g)] = id, sum += normal[size_t(g)] * area[size_t(g)];
|
||||
todo.push_back(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Flat onto the piece's average facing, lined up with its first face's longest edge.
|
||||
const glm::vec3 n = glm::normalize(glm::length(sum) > 1e-12f ? sum : glm::vec3(0, 0, 1));
|
||||
const auto& sf = m.faces[size_t(seed)];
|
||||
glm::vec3 u(0.0f);
|
||||
for (size_t i = 0; i < sf.size(); ++i) {
|
||||
const glm::vec3 d = m.verts[size_t(sf[(i + 1) % sf.size()])] - m.verts[size_t(sf[i])];
|
||||
if (glm::length(d) > glm::length(u)) u = d;
|
||||
}
|
||||
u -= n * glm::dot(u, n);
|
||||
if (glm::length(u) < 1e-9f) u = glm::cross(n, std::abs(n.z) < 0.9f ? glm::vec3(0, 0, 1) : glm::vec3(1, 0, 0));
|
||||
u = glm::normalize(u);
|
||||
const glm::vec3 v = glm::cross(n, u);
|
||||
Chart& c = charts.back();
|
||||
for (int f : c.faces)
|
||||
for (int p : m.faces[size_t(f)]) {
|
||||
const glm::vec2 q(glm::dot(m.verts[size_t(p)], u), -glm::dot(m.verts[size_t(p)], v)); // (down the picture: v flipped, so it isn't mirrored)
|
||||
uv[size_t(f)].push_back(q);
|
||||
c.lo = glm::min(c.lo, q), c.hi = glm::max(c.hi, q);
|
||||
}
|
||||
}
|
||||
// Packed in rows, tallest first, into a square.
|
||||
float total = 0;
|
||||
for (const Chart& c : charts) total += (c.hi.x - c.lo.x) * (c.hi.y - c.lo.y);
|
||||
const float side = std::sqrt(std::max(total, 1e-12f)), pad = margin * side * 1.2f;
|
||||
float rowWidth = side * 1.2f;
|
||||
for (const Chart& c : charts) rowWidth = std::max(rowWidth, c.hi.x - c.lo.x + 2 * pad);
|
||||
std::vector<int> byHeight(charts.size());
|
||||
for (size_t i = 0; i < charts.size(); ++i) byHeight[i] = int(i);
|
||||
std::sort(byHeight.begin(), byHeight.end(), [&](int a, int b) { return charts[size_t(a)].hi.y - charts[size_t(a)].lo.y > charts[size_t(b)].hi.y - charts[size_t(b)].lo.y; });
|
||||
float x = 0, y = 0, rowH = 0, usedW = 0;
|
||||
for (int i : byHeight) {
|
||||
Chart& c = charts[size_t(i)];
|
||||
const float w = c.hi.x - c.lo.x + 2 * pad, h = c.hi.y - c.lo.y + 2 * pad;
|
||||
if (x > 0 && x + w > rowWidth) y += rowH, x = 0, rowH = 0;
|
||||
c.at = glm::vec2(x + pad, y + pad) - c.lo;
|
||||
x += w, rowH = std::max(rowH, h), usedW = std::max(usedW, x);
|
||||
}
|
||||
const float scale = 1.0f / std::max(usedW, y + rowH);
|
||||
for (const Chart& c : charts)
|
||||
for (int f : c.faces)
|
||||
for (glm::vec2& q : uv[size_t(f)]) q = (q + c.at) * scale;
|
||||
return uv;
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
|
||||
namespace fry {
|
||||
|
||||
std::vector<std::vector<int>> uvIslands(const Mesh& m, const std::vector<std::vector<glm::vec2>>& uvs) {
|
||||
std::vector<std::vector<int>> out;
|
||||
if (uvs.size() != m.faces.size()) return out;
|
||||
// (an edge's two ends where they sit on the picture, for each face that has it)
|
||||
std::map<std::pair<int, int>, std::vector<std::pair<int, std::pair<glm::vec2, glm::vec2>>>> byEdge;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
const auto& fv = m.faces[f];
|
||||
if (uvs[f].size() != fv.size()) return {};
|
||||
for (size_t i = 0; i < fv.size(); ++i) {
|
||||
const size_t j = (i + 1) % fv.size();
|
||||
const bool up = fv[i] < fv[j];
|
||||
byEdge[{std::min(fv[i], fv[j]), std::max(fv[i], fv[j])}].push_back({int(f), up ? std::pair{uvs[f][i], uvs[f][j]} : std::pair{uvs[f][j], uvs[f][i]}});
|
||||
}
|
||||
}
|
||||
std::vector<int> island(m.faces.size(), -1);
|
||||
for (size_t start = 0; start < m.faces.size(); ++start) {
|
||||
if (island[start] >= 0) continue;
|
||||
out.push_back({});
|
||||
std::vector<int> todo{int(start)};
|
||||
island[start] = int(out.size()) - 1;
|
||||
while (!todo.empty()) {
|
||||
const int f = todo.back();
|
||||
todo.pop_back();
|
||||
out.back().push_back(f);
|
||||
const auto& fv = m.faces[size_t(f)];
|
||||
for (size_t i = 0; i < fv.size(); ++i) {
|
||||
const auto& around = byEdge[{std::min(fv[i], fv[(i + 1) % fv.size()]), std::max(fv[i], fv[(i + 1) % fv.size()])}];
|
||||
const auto mine = std::find_if(around.begin(), around.end(), [&](const auto& e) { return e.first == f; })->second;
|
||||
for (const auto& [g, ends] : around)
|
||||
if (island[size_t(g)] < 0 && glm::distance(ends.first, mine.first) < 1e-5f && glm::distance(ends.second, mine.second) < 1e-5f)
|
||||
island[size_t(g)] = island[start], todo.push_back(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void remapPicture(Image& img, const Mesh& m, const std::vector<std::vector<glm::vec2>>& before, const std::vector<std::vector<glm::vec2>>& after,
|
||||
const std::vector<int>& faces) {
|
||||
const Image src = img;
|
||||
const glm::vec2 size(float(img.w), float(img.h));
|
||||
for (int f : faces) {
|
||||
if (f < 0 || f >= int(m.faces.size()) || before[size_t(f)].size() != m.faces[size_t(f)].size() || after[size_t(f)].size() != m.faces[size_t(f)].size()) continue;
|
||||
const auto& fv = m.faces[size_t(f)];
|
||||
for (const auto& t : triangulate(m, f)) {
|
||||
glm::vec2 a[3], b[3];
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const size_t c = size_t(std::find(fv.begin(), fv.end(), t[size_t(k)]) - fv.begin());
|
||||
a[k] = after[size_t(f)][c] * size, b[k] = before[size_t(f)][c] * size;
|
||||
}
|
||||
const float twice = (a[1].x - a[0].x) * (a[2].y - a[0].y) - (a[2].x - a[0].x) * (a[1].y - a[0].y);
|
||||
if (std::abs(twice) < 1e-9f) continue;
|
||||
const glm::vec2 lo = glm::max(glm::min(a[0], glm::min(a[1], a[2])) - 2.0f, glm::vec2(0.0f));
|
||||
const glm::vec2 hi = glm::min(glm::max(a[0], glm::max(a[1], a[2])) + 2.0f, size - 1.0f);
|
||||
for (int y = int(lo.y); y <= int(hi.y); ++y)
|
||||
for (int x = int(lo.x); x <= int(hi.x); ++x) {
|
||||
const glm::vec2 q(float(x) + 0.5f, float(y) + 0.5f);
|
||||
float w[3];
|
||||
bool in = true;
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const glm::vec2 p = a[(k + 1) % 3], e = a[(k + 2) % 3];
|
||||
w[k] = ((e.x - p.x) * (q.y - p.y) - (q.x - p.x) * (e.y - p.y)) / twice;
|
||||
in &= w[k] * std::abs(twice) / std::max(glm::length(e - p), 1e-6f) >= -1.5f; // (and a little past: no seams)
|
||||
}
|
||||
if (!in) continue;
|
||||
const glm::vec4 c = sample(src, b[0] * w[0] + b[1] * w[1] + b[2] * w[2]);
|
||||
uint8_t* p = img.at(x, y);
|
||||
for (int k = 0; k < 4; ++k) p[k] = uint8_t(std::lround(std::clamp(c[k], 0.0f, 1.0f) * 255.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
Loading…
Reference in a new issue