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:
Nexoniarz 2026-10-19 21:18:35 +02:00
commit f3e8eca87b
135 changed files with 23423 additions and 0 deletions

13
src/CMakeLists.txt Normal file
View 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

File diff suppressed because it is too large Load diff

207
src/boolean.cpp Normal file
View 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
View 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
View 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 == '&' ? "&amp;" : ch == '<' ? "&lt;" : ch == '>' ? "&gt;" : ch == '"' ? "&quot;" : 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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