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

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