// Fry3DX — Frying 3D Extreme (the app: its state and what it does, declared; defined in app*.cpp). A 3D modeller that works the way you'd expect: pick a tool from the // toolbox, click and drag. The view in the middle is drawn by the engine's renderer: plain // and clear while you model (Solid), or properly lit (Rendered). The panels are Dear ImGui. // // fry3dx [file.fry] #pragma once #include "bvh.h" #include "commands.h" #include "icons.h" #include "mesh.h" #include "scene.h" #include "script.h" #include "sculpt.h" #include "shapes.h" #include "import.h" #include "text.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace fry; namespace stdfs = std::filesystem; namespace fryapp { inline double now() { return double(SDL_GetTicks()) / 1000.0; } // (seconds; works before the UI exists) constexpr float kUnits = 39.37f; // the engine's units (inches) in a metre: the view renders in them constexpr float kPi = 3.14159265358979f; enum class Mode { Object, Edit }; enum class Tool { Select, Move, Rotate, Scale, Draw, Extrude, Inset, Bevel, LoopCut, Knife, Hole }; enum class Tab { Model, Paint, Layout, Render, Print }; enum class LayTool { Select, Move, Rotate, Scale }; enum class PrintTool { None, Measure, LayFlat }; enum class PaintTool { Brush, Eraser, Fill, Picker, Picture }; enum class Elem { Vertex, Edge, Face }; enum class View { Perspective, Front, Back, Left, Right, Top, Bottom }; enum class Orient { Local, World, Normal }; // which way the gizmo's arrows point enum class Pivot { Middle, Origin, Opposite, Bottom }; // what stays put when turning / sizing enum class Shading { Solid, Rendered, Wire }; enum class SelShape { Box, Brush, Lasso }; struct ToolInfo { Tool tool; Icon icon; const char* name; const char* key; const char* hint; bool objectMode, editMode; }; const ToolInfo kTools[] = { {Tool::Select, Icon::Select, "Select", "Q", "Click to select; drag round things. Objects: Shift adds, Ctrl takes away. Shape: Ctrl adds/takes one, Shift+click a whole loop, Shift+Ctrl+click an edge ring.", true, true}, {Tool::Move, Icon::Move, "Move", "W", "Drag an arrow to move along it, the square to move freely. Hold Ctrl to snap.", true, true}, {Tool::Rotate, Icon::Rotate, "Rotate", "E", "Drag a ring to turn about it; the outer ring turns it as you see it. Ctrl: 15 degree steps.", true, true}, {Tool::Scale, Icon::Scale, "Scale", "R", "Drag a side's handle to stretch it (the other side stays, or see Tool options); hold Shift to stretch both sides from the middle. Middle: all ways.", true, true}, {Tool::Draw, Icon::Draw, "Draw shape", "D", "Click points round a shape (on the ground, or on any face of an object); click the first point (or press Enter) to finish. Esc cancels.", true, false}, {Tool::Extrude, Icon::Extrude, "Extrude", "X", "Select faces, then drag the arrow to pull them out (along the way they face).", false, true}, {Tool::Inset, Icon::Inset, "Inset", "I", "Select faces, then drag the handle to make a smaller face inside each.", false, true}, {Tool::Bevel, Icon::Bevel, "Round edges", "B", "Select edges (or faces), then drag the round handle to round them off.", false, true}, {Tool::LoopCut, Icon::LoopCut, "Loop cut", "C", "Point at an edge: the cut shows across. Click to cut there.", false, true}, {Tool::Knife, Icon::Knife, "Knife", "K", "Click points along where to cut, or drag a straight line. Enter or double-click cuts. Cutting right through: Tool options.", false, true}, {Tool::Hole, Icon::Hole, "Hole", "O", "Press on a face and drag out the size: round, rectangle, triangle or hexagon (nuts); all the way through, a pocket, or only its outline (a new face).", true, true}, }; struct PaintToolInfo { PaintTool tool; Icon icon; const char* name; const char* key; const char* hint; }; struct LayToolInfo { LayTool tool; Icon icon; const char* name; const char* key; const char* hint; }; const LayToolInfo kLayTools[] = { {LayTool::Select, Icon::Select, "Select", "Q", "Click frames (or corners) to pick them, Shift adds; drag round them to pick many. Clicking the model picks its frame too."}, {LayTool::Move, Icon::Move, "Move", "W", "Drag frames (or corners) about the picture."}, {LayTool::Rotate, Icon::Rotate, "Turn", "E", "Drag round to turn what's picked (Ctrl: 15 degree steps)."}, {LayTool::Scale, Icon::Scale, "Size", "R", "Drag out or in to make what's picked bigger or smaller (Shift: only across or only up)."}, }; // Printers' build plates (mm, across x deep x high) and what's printed with. struct PrinterInfo { const char* name; float x, y, z; }; const PrinterInfo kPrinters[] = { {"Prusa MK4 / MK4S", 250, 210, 220}, {"Prusa Core One", 250, 220, 270}, {"Prusa MINI+", 180, 180, 180}, {"Bambu Lab X1 / P1 / P2", 256, 256, 256}, {"Bambu Lab A1", 256, 256, 256}, {"Bambu Lab A1 mini", 180, 180, 180}, {"Creality Ender 3 (V2, V3)", 220, 220, 250}, {"Creality K1", 220, 220, 250}, {"Elegoo Neptune 4", 225, 225, 265}, {"Anycubic Kobra 3", 250, 250, 260}, {"Your own size", 0, 0, 0}, }; struct MaterialInfo { const char* name; float density; // g/cm3 }; // What the Script panel starts with. inline const char* kFirstScript = R"(-- A script builds or changes the scene. F5 or Ctrl+Enter runs it; -- Ctrl+Z takes back everything it did. The ? button lists what there is. local legs = 6 for i = 1, legs do local a = math.rad(i * 360 / legs) local leg = fry.add("cylinder", {radius = 0.03, height = 0.7, color = "#c47a3c"}) leg.position = {math.cos(a) * 0.45, math.sin(a) * 0.45, 0} end local top = fry.add("cylinder", {radius = 0.6, height = 0.05, sides = 48, color = "#d9a066"}) top.position = {0, 0, 0.7} fry.frame() )"; const MaterialInfo kMaterials[] = {{"PLA", 1.24f}, {"PETG", 1.27f}, {"ABS", 1.04f}, {"ASA", 1.07f}, {"TPU", 1.21f}, {"Nylon", 1.14f}, {"Resin", 1.12f}}; struct SculptToolInfo { SculptBrush brush; Icon icon; const char* name; const char* key; const char* hint; }; const SculptToolInfo kSculptTools[] = { {SculptBrush::Draw, Icon::Sculpt, "Raise", "1", "Builds the surface up where you brush. Ctrl digs in instead."}, {SculptBrush::Inflate, Icon::Inflate, "Puff out", "2", "Swells it out all round, like blowing it up. Ctrl shrinks it."}, {SculptBrush::Smooth, Icon::Smooth, "Smooth", "3", "Evens out bumps. Hold Shift with any brush to smooth."}, {SculptBrush::Flatten, Icon::Flatten, "Flatten", "4", "Presses it flat, like a palette knife."}, {SculptBrush::Pinch, Icon::Pinch, "Pinch", "5", "Pulls it in towards the middle of the brush: sharp creases and ridges. Ctrl pushes apart."}, {SculptBrush::Grab, Icon::Grab, "Grab", "6", "Take hold of a patch and drag it where it should be."}, }; const PaintToolInfo kPaintTools[] = { {PaintTool::Brush, Icon::PaintBrush, "Brush", "B", "Paint on the model or on the flat picture. Alt+click picks a colour; [ and ] change the size."}, {PaintTool::Eraser, Icon::Eraser, "Eraser", "E", "Paints its plain colour back."}, {PaintTool::Fill, Icon::Fill, "Fill", "G", "Fills the patch of about the same colour where you click."}, {PaintTool::Picker, Icon::Picker, "Pick colour", "I", "Click to take a colour from the model or the picture."}, {PaintTool::Picture, Icon::Picture, "Place a picture", "Ctrl+V", "Paste a picture (copied in any app) or add one from a file, then drag its corners to fit. Enter places it."}, }; inline glm::vec3 forwardOf(float yaw, float pitch) { const float y = glm::radians(yaw), p = glm::radians(pitch); return {std::cos(p) * std::cos(y), std::cos(p) * std::sin(y), -std::sin(p)}; } // The engine's view-projection (render/renderer.cpp), in metres. inline glm::mat4 viewProjection(const glm::vec3& eye, float yaw, float pitch, float fov, float aspect, glm::vec2 shift, glm::vec2 nearFar) { const glm::vec3 f = forwardOf(yaw, pitch); glm::mat4 view = glm::lookAt(eye * kUnits, eye * kUnits + f, glm::vec3(0, 0, 1)); glm::mat4 proj = glm::perspective(glm::radians(fov), aspect, nearFar.x, nearFar.y); proj[1][1] *= -1.0f; proj[2][0] -= shift.x, proj[2][1] -= shift.y; return proj * view * glm::scale(glm::mat4(1.0f), glm::vec3(kUnits)); } struct Ray { glm::vec3 origin, dir; }; // Möller–Trumbore; t along the ray, or nothing. inline std::optional hitPlane(const Ray& r, const glm::vec3& point, const glm::vec3& normal) { const float d = glm::dot(r.dir, normal); if (std::abs(d) < 1e-8f) return std::nullopt; const float t = glm::dot(point - r.origin, normal) / d; return t > 0 ? std::optional(r.origin + r.dir * t) : std::nullopt; } // Where along the line (from `p`, along unit `axis`) the ray passes closest. inline float closestOnAxis(const Ray& r, const glm::vec3& p, const glm::vec3& axis) { const glm::vec3 w = p - r.origin; const float b = glm::dot(axis, r.dir), d = glm::dot(axis, w), e = glm::dot(r.dir, w); const float den = 1.0f - b * b; return den < 1e-6f ? 0.0f : (b * e - d) / den; } inline float distToSegment(ImVec2 p, ImVec2 a, ImVec2 b) { const float vx = b.x - a.x, vy = b.y - a.y, len = vx * vx + vy * vy; const float t = len > 0 ? std::clamp(((p.x - a.x) * vx + (p.y - a.y) * vy) / len, 0.0f, 1.0f) : 0.0f; const float dx = a.x + vx * t - p.x, dy = a.y + vy * t - p.y; return std::sqrt(dx * dx + dy * dy); } inline std::string lower(std::string s) { std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return char(std::tolower(c)); }); return s; } inline bool insidePolygon(ImVec2 p, const std::vector& poly) { bool in = false; for (size_t i = 0, j = poly.size() - 1; i < poly.size(); j = i++) if ((poly[i].y > p.y) != (poly[j].y > p.y) && p.x < (poly[j].x - poly[i].x) * (p.y - poly[i].y) / (poly[j].y - poly[i].y) + poly[i].x) in = !in; return in; } inline float snapTo(float v, float step) { return step > 0 ? std::round(v / step) * step : v; } using Axes = std::array; inline glm::vec3 inFrame(const Axes& a, const glm::vec3& p) { return {glm::dot(p, a[0]), glm::dot(p, a[1]), glm::dot(p, a[2])}; } inline glm::vec3 fromFrame(const Axes& a, const glm::vec3& c) { return a[0] * c.x + a[1] * c.y + a[2] * c.z; } const ImU32 kAxisCol[3] = {IM_COL32(235, 70, 70, 255), IM_COL32(110, 210, 80, 255), IM_COL32(70, 130, 245, 255)}; const ImU32 kHot = IM_COL32(255, 225, 90, 255); constexpr ImU32 kAccent = IM_COL32(255, 120, 40, 255); // The UI's colours: Niveus's roles, in the appearance that's showing (keel/ui). inline ImVec4 roleColor(keel::ui::Role r) { return ImGui::ColorConvertU32ToFloat4(keel::ui::color(r)); } inline ImVec4 accentText() { return roleColor(keel::ui::Role::AccentText); } // The one button that does it, filled with the accent (until popAccent). inline void pushAccent() { using keel::ui::Role; ImGui::PushStyleColor(ImGuiCol_Button, roleColor(Role::Accent)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, roleColor(Role::AccentHover)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, roleColor(Role::AccentActive)); ImGui::PushStyleColor(ImGuiCol_Text, roleColor(Role::OnAccent)); } inline void popAccent() { ImGui::PopStyleColor(4); } // An object as it'll be printed: where it stands, with its finish, checked (kept until it changes). struct PrintInfo { uint64_t key = ~0ull; Mesh world; PrintCheck check; glm::vec3 lo{0.0f}, hi{0.0f}; keel::ModelId overhang{}; bool hasOverhang = false; // Walls too thin to print: how thick at each face (worked out again only when its shape or // size changes: moving or turning it doesn't), how many under the least, the thinnest. uint64_t thickKey = ~0ull; std::vector thick; int thinFaces = 0; float thinnest = 1e30f; keel::ModelId thin{}; // The slice preview's layer: where it was cut, and for which shape (kept beside what it's of). mutable uint64_t sliceKey = ~0ull; mutable float sliceZ = -1; mutable std::vector> slice; }; extern keel::Cvar fry_printer; extern keel::Cvar fry_bed_x; extern keel::Cvar fry_bed_y; extern keel::Cvar fry_bed_z; extern keel::Cvar fry_touch; extern keel::Cvar fry_uiscale; extern keel::Cvar fry_uisize; extern keel::Cvar fry_theme; extern keel::Cvar fry_movespeed; extern keel::Cvar fry_camera; extern keel::Cvar fry_pluginsoff; extern keel::Cvar fry_autosave; class App : public keel::Game {public: explicit App(std::string path) : startPath_(std::move(path)) {} const char* name() const override { return "Fry3DX"; } // (its settings folder, and bin/Fry3DX/) const char* title() const override { return "Fry3DX"; } bool wantsCursor() const override { return true; } bool dockingUi() const override { return true; } // "Spiral_stairs" is "Spiral stairs" (typed in the console). static bool sameCommand(const std::string& name, std::string typed); void init() override; // Going into the background: the work is saved now (a phone may close the app while it's // away, to free memory), and a phone's next start carries on with it, view and all. std::string resumeFile() const { return keel::prefPath() + "resume.txt"; } void onBackground() override; void resumeWork(); void frame(double) override { autosave(); } bool allowQuit() override; void shutdown() override { if (autosaveJob_.valid()) autosaveJob_.wait(); } void onEscape() override; void render(keel::Renderer& r) override; private: // ── The document ── Scene scene_; std::string startPath_, path_; bool dirty_ = false; // An undo step. Its objects without their shapes (points, faces, painting's layout): those are // kept apart and shared with the steps before and after where they're the same, so a step // that moved one thing doesn't copy every shape in the scene again. struct Shape { std::shared_ptr mesh; std::shared_ptr>> uvs; }; struct Snapshot { Scene scene; std::map shapes; std::set selected; uint32_t active; Mode mode; std::set verts, faces; std::set> edges; std::string label; // (what was done after it: the message said then) double at = 0; }; std::vector undo_, redo_; std::map lastShapes_; // (each object's shape as last kept: the next step shares it if it's the same) Snapshot snapshot(std::string label, double at); std::vector redoKept_; // (what redo had before the last undo point: back if that step is dropped) Mode mode_ = Mode::Object; Tool tool_ = Tool::Move; Elem elem_ = Elem::Face; std::set selected_; uint32_t active_ = 0; // the object last picked (edited in shape editing) uint32_t activeRef_ = 0; // a reference picture picked in the Layers panel std::set selVerts_, selFaces_; std::set> selEdges_; // ── Tabs: modelling, painting pictures onto it, making a picture of it ── Tab tab_ = Tab::Model; // ── The view ── keel::Renderer* r_ = nullptr; glm::vec2 shift_{0.0f}; // (the view's middle on the screen: it sits between the panels) View view_ = View::Perspective; glm::vec3 target_{0, 0, 0.4f}; float yaw_ = -135.0f, pitch_ = 25.0f, dist_ = 4.0f, fov_ = 50.0f; glm::vec2 nearFar_{2.0f, 16384.0f}; std::map> radii_; // (each object's mesh: how far it reaches from its place) ImVec2 viewMin_{0, 0}, viewMax_{1, 1}; glm::mat4 vp_{1.0f}; glm::vec3 eye_{0.0f}; bool orbiting_ = false, panning_ = false, looking_ = false; // (looking: right drag turns the camera where it stands) float nearest_ = 1.0f; // (how far the nearest thing is from the eye, m: worked out with the view) bool xray_ = false, showGrid_ = true, renderGrid_ = false; std::string appliedLook_; keel::ModelId ground_{}; bool haveGround_ = false; Shading shading_ = Shading::Solid; // ── Tools ── bool snap_ = false; int snapKind_ = 0; // (moving: 0 steps, 1 points, 2 edges' middles, 3 faces' middles) float snapMove_ = 0.05f, snapAngle_ = 15.0f, snapScale_ = 0.1f; float drawThickness_ = 0.05f, extrudeAmount_ = 0.1f, insetAmount_ = 0.2f, mergeDistance_ = 0.001f; float bevelWidth_ = 0.05f; int bevelSteps_ = 3, cuts_ = 1; Orient orientObject_ = Orient::Local, orientEdit_ = Orient::Normal; Pivot rotatePivot_ = Pivot::Middle, scalePivot_ = Pivot::Opposite; SelShape selShape_ = SelShape::Box; float brushRadius_ = 30.0f; std::vector drawPts_; // Drawing on an object: the plane of the face the first point went on, and what's made with it // (0 a new object standing on it, 1 added to it, 2 cut into it). glm::vec3 drawOnP_{0.0f}, drawOnN_{0.0f, 0.0f, 1.0f}; uint32_t drawOnto_ = 0; int drawOnMode_ = 0; struct Drag { bool active = false; int handle = -1; // 0-2 an axis, 3 free / even / about the view, 4-6 an axis's other end Tool tool = Tool::Select; Axes axes{}; glm::vec3 pivot{0.0f}, anchor{0.0f}, handlePoint{0.0f}; ImVec2 startMouse{0, 0}; float startT = 0.0f, startDist = 0.0f, startMid = 0.0f; // (the handle's way from what stays put; from the middle) glm::vec3 startHit{0.0f}; Scene before; std::set> bevelEdges; std::vector> soft; // points near the selection that follow it, and how much std::string typed; // (a number typed while dragging: exactly that much) } drag_; // Soft selection (shape editing): points within `softReach_` of the selection follow it, less // the further they are (Blender's proportional editing, Maya's soft select). bool softSel_ = false; float softReach_ = 0.3f; // Push out / pull in (Alt+S): the points and the shape as it was when it started. bool showPush_ = false; float pushBy_ = 0.0f; std::set pushPts_; Mesh pushFrom_; uint32_t pushObject_ = 0; bool boxing_ = false, painting_ = false, lassoing_ = false, tapDraw_ = false; ImVec2 boxStart_{0, 0}; std::vector lasso_; std::pair cutHover_{-1, -1}; // The knife: points clicked on the screen (or a straight drag). std::vector knifePts_; bool knifeDown_ = false; ImVec2 knifePress_{0, 0}; bool knifeThrough_ = false, knifeFill_ = true; int knifeAfter_ = 1; // cut right through, then: 0 just the lines, 1 pieces of the one object, 2 objects of their own, 3 the smaller ones gone // Holes: pressed on a face, dragged out to size. // (sizes in metres where it stands: across = a round's width, a rectangle's width, a triangle's // side, a hexagon's flats; tall: a rectangle's height) float holeSize_ = 0.2f, holeTall_ = 0.1f, holeDepth_ = 0.1f, holeTurn_ = 0.0f, holeRoom_ = 0.0003f; int holeShape_ = 0, holeSides_ = 32; // a HoleShape int holeMode_ = 0; // 0 all the way through, 1 a pocket, 2 the outline only (a new face) struct HoleDrag { bool active = false; uint32_t object = 0; int face = -1; glm::vec3 centre{0.0f}; // (the object's own space) float radius = 0.0f; ImVec2 press{0, 0}; } hole_; // ── Painting ── PaintTool paintTool_ = PaintTool::Brush; glm::vec4 paintColor_{0.85f, 0.25f, 0.1f, 1.0f}; // a: how strong float brushPx_ = 16.0f, hardness_ = 0.5f, fillTolerance_ = 0.15f; int pictureSize_ = 1024; bool showUvLines_ = true; struct PaintStroke { bool active = false, in2D = false, haveLast = false, waiting = false; // waiting: pressed off the object, starts when the brush gets to it uint32_t object = 0; std::shared_ptr image; // (the object's picture: a copy made for this stroke) Stroke stroke; glm::vec3 last{0.0f}; glm::vec2 last2D{0.0f}; } paint_; // Sculpting (the Paint tab's Shape side): brushes that push the surface about like clay. bool sculpting_ = false; SculptBrush sculptBrush_ = SculptBrush::Draw; float sculptStrength_ = 0.5f, sculptPx_ = 60.0f; // (its own brush size: sculpting wants a bigger one than painting) bool paintMirror_ = false, stencilOn_ = false, drawingStencil_ = false; std::vector stencil_; // (on the screen) bool sculptMirror_ = false; // the same on the other side (left and right: the object's Y) struct SculptStroke { bool active = false, haveLast = false, waiting = false; uint32_t object = 0; std::unique_ptr mesh; glm::vec3 last{0.0f}; // Grab: where it was taken hold of (the world), and what (the object's own space). glm::vec3 start{0.0f}; std::vector> held[2]; std::vector from[2]; } sculpt_; // A picture layer unlocked to be moved: which, and what's being dragged (a corner 0-3, 4 the // whole of it, 5 the turning handle) from where. struct LayerEdit { uint32_t object = 0; int index = -1, drag = -1; bool fresh = false; // (just pasted: Esc takes it away again) glm::vec2 grab{0.0f}, offset{0.0f}; std::array startCorners{}; PictureLayer start; } ledit_; bool pasteOnSurface_ = true; uint32_t renamingFolder_ = 0; // Touch (set when it starts: fry_touch). bool touchMode_ = false, portrait_ = false, showToolbox_ = false, panelsOpen_ = false, viewsOpen_ = false, shiftLatch_ = false, ctrlLatch_ = false, touchOrbit_ = false; struct Gesture { bool two = false, longDone = false, rightUp = false; ImVec2 mid{}, downPos{}; float spread = 1; double downAt = -1; } gest_; std::string captureUri_; // (a picture of the view going into a picked file) struct CopiesSetting { int count = 3, axis = 2; bool round = false, joined = false, path = false, follow = true; // (path: along the last picked object) glm::vec3 step{0.3f, 0.0f, 0.0f}, centre{0.0f}; float angle = 360.0f; } copies_; bool showCopies_ = false, showThicken_ = false, thicknessOut_ = false; float thickness_ = 0.002f; // The Layout tab: what's picked (frames, or corners as face and corner) and what's dragged. struct LayoutState { uint32_t object = 0; bool corners = false; std::set frames; std::set> points; int drag = 0; // 1 moving / turning / sizing, 2 a box glm::vec2 grab{0.0f}, centre{0.0f}; ImVec2 boxFrom{}; std::vector> start; std::shared_ptr startPicture; double remapAt = 0; } lay_; LayTool layTool_ = LayTool::Move; bool layKeepPaint_ = true, layChecker_ = false; // The Print tab. PrintTool printTool_ = PrintTool::None; std::vector measure_; bool measureSnap_ = true, showOverhangs_ = true, sizing_ = false, showThin_ = true, showLayer_ = false; int layer_ = 1; float layerHeight_ = 0.0002f; // (m) float minWall_ = 0.0008f; // (m: the thinnest wall that prints well, twice a 0.4 mm nozzle) float overhangDeg_ = 45.0f, infill_ = 15.0f; int material_ = 0; std::map printInfos_; keel::ModelId bed_{}; glm::vec3 bedFor_{0.0f}; // Closing, New or Open with changes not saved: asked first (askSave_), then done; afterSave_: // what to do once the Save it chose has finished (0 nothing, 1 quit, 2 new, 3 open). int askSave_ = 0, afterSave_ = 0; // (what to do then: 1 quit, 2 new, 3 open..., 4 open dropped_) std::string dropped_; bool dialogCancelled_ = false; // Autosave: a copy packed small in the background every fry_autosave minutes while there are // changes, kept until saved or closed properly; offered back after a crash. std::string autosavePath_; uint64_t edits_ = 0, autosavedEdits_ = 0; double lastAutosave_ = 0; std::future autosaveJob_; std::vector recover_; bool showRecover_ = false; std::map> owner_; // which layer shows at each pixel struct Uploaded { uint64_t stamp = 0; double at = 0; bool dirty = false; }; std::map uploaded_; std::map paintTex_; float picZoom_ = 0.0f; // the Picture panel: screen pixels a picture pixel, and where its corner is ImVec2 picPan_{0, 0}; uint32_t picFor_ = 0; // ── Saving a picture of the view (cropped to it, without the gizmos) ── int captureFrames_ = 0; std::string capturePath_; // ── GPU side ── std::map> models_; // object id → its model, the state built std::map versions_; uint64_t versionClock_ = 1; struct Shown { uint64_t version = 0; // (the shape key it was made for) bool mirrorOnly = false; Mesh mesh; std::shared_ptr tree; // (made with it, when it was made on another thread) std::future>> coming; // (a big one being made on another thread) uint64_t comingVersion = 0; bool comingMirror = false; }; std::map shown_; struct Picture { bool ok = false; float aspect = 1.0f; }; std::map pictures_; keel::TextureId logo_{}; bool logoTried_ = false, haveLogo_ = false; // ── UI ── int appliedTheme_ = -1; // The Script panel: the script being written, where it's from, what it printed. bool showScript_ = false, showScriptHelp_ = false; size_t script_ = 0; // the file's script shown in the Script panel std::string scriptPath_; // where it was last saved or opened from (Save as starts there) // Plug-ins (Script > Plug-ins): each file, whether it's on, and how loading it went. struct PluginEntry { std::string path, file, error; bool bundled = false, on = true; std::unique_ptr p; }; std::vector plugins_; bool showPlugins_ = false; std::pair pluginTool_; // (the plug-in tool that's on: its file and name) std::string pluginFilter_; // ── Commands: found by typing, on keys of your own, round the mouse ── std::vector commands_; fry::CommandSearch search_; std::vector recentCommands_; std::map myKeys_; // (a command's id: the key you gave it) int pie_ = 0; // 1: the tools round the mouse (Space held), 2: the views (Shift+Space) ImVec2 pieAt_{0, 0}; bool pieStays_ = false; // (opened from the menu or console: until a click) std::string settingKey_; bool showHistory_ = false; // ── Workspaces: the panels as they were, kept under a name ── bool showWorkspaces_ = false, layoutAgain_ = false; std::string pendingLayout_; char workspaceName_[64] = "My workspace"; // (the command waiting for its new key, in Keys and mouse) // A plug-in's command waiting for its settings (the form), and a material being named. struct PendingCommand { size_t plugin = 0, command = 0; std::vector values; }; std::optional pendingCommand_; bool nameMaterial_ = false; std::string materialName_; bool materialKeepColor_ = false; std::vector> scriptLog_; // (line, an error) bool scriptLogToBottom_ = false; bool styled_ = false, layoutDone_ = false, showHelp_ = false, showAbout_ = false; std::string iniPath_, message_; double messageAt_ = -100.0; char renameBuf_[128] = ""; uint32_t renaming_ = 0; std::mutex dialogMutex_; std::string dialogResult_; int dialogKind_ = 0; // 1 open, 2 save, 3 export, 4 reference picture, 5 picture of the view, 6 a picture to paint on, 7 save the painted picture int pendingKind_ = 0, dialogFilter_ = -1; // ───────────────────────────── document ───────────────────────────── Object* find(uint32_t id); Reference* findRef(uint32_t id); Object* editing() { return mode_ == Mode::Edit ? find(active_) : nullptr; } void touched(uint32_t id) { versions_[id] = ++versionClock_, dirty_ = true, ++edits_; } // What decides the shape an object shows, as a number: its points and faces, its finishing // touches, creases, cuts, shading and painting's layout (not where it is). Worked out again // only when it's been touched: moving a smoothed object doesn't smooth it again each frame. struct ShapeKey { uint64_t version = 0, key = 0; }; std::map shapeKeys_; uint64_t shapeKey(const Object& o); void touchAll() { for (const Object& o : scene_.objects) versions_[o.id] = ++versionClock_; } // Its shape has been changed by hand: the library settings no longer apply. void handMade(Object& o) { o.recipe.clear(), o.settings.clear(); } void say(std::string text); void pushUndo(); // The undo point just made, dropped (what it was for didn't happen): redo as it was before. void dropUndo(); // Back to the undo point just made, and it dropped. void takeBack(); void restore(Snapshot& s); void undo(); void redo(); void newScene(); Object& addObject(const std::string& base, Mesh mesh); std::string uniqueName(const std::string& base); // A shape from the library, standing on the ground under the middle of the view. void addShape(const std::string& id, bool undoable = true); void deleteSelected(); void duplicateSelected(); // The selected objects become one (the last picked keeps its place, look and name). void joinSelected(); // The selected faces become an object of their own. void separateSelected(); // ───────────────────────────── files ───────────────────────────── // ── Files a phone's file picker gives (content://...): read and written through SDL ── static bool isUri(const std::string& p) { return p.starts_with("content://"); } // A file's name as shown (a picked file's: from the end of its address, decoded). static std::string fileName(const std::string& p); // A picked file copied to a file of our own to read ("" if it couldn't be). static std::string fromUri(const std::string& uri, const std::string& name); // A file of ours written into the picked one. static bool toUri(const std::string& local, const std::string& uri); static std::string tempFile(const std::string& name); void openFile(const std::string& picked); bool saveFile(std::string path); static std::string sizeText(uintmax_t n) { return n < 10240 ? std::format("{} bytes", n) : n < 10 << 20 ? std::format("{} KB", n >> 10) : std::format("{:.1f} MB", double(n) / 1048576.0); } static std::vector pngBytes(const Image& im); void exportFile(std::string path, int filter = -1, const std::string& asExt = ""); // ── Closing with changes, autosave ── // New, Open, Quit: asks about saving first if there are changes. void whenSaved(int action); void doAfterAsking(int action); // A file dropped on the window (or opened with Fry3DX from the desktop): a .fry opens (after // asking about changes), a script goes in the Script panel, a picture goes on as a reference // (or, in the Paint tab, as a sticker on the model). void onFileDropped(const std::string& path) override; void saveQuestion(); std::string autosaveDir() const { return keel::prefPath() + "autosave/"; } // Every fry_autosave minutes (when there are changes and nothing's being dragged): a copy // packed as small as it goes, in the background. void autosave(bool force = false); // Its autosave isn't needed (saved, or closed on purpose). void dropAutosave(); // Autosaves left behind (it didn't close properly): offered back. Only the newest 15 are // kept; older ones are deleted. void findAutosaves(); void recoverWindow(); // A picture of the 3D view only (no panels, gizmos or outlines): the screen is saved a few // frames on, with those hidden, then cut down to the view. void savePicture(std::string path); bool capturing() const { return captureFrames_ > 0; } void captureStep(); static bool loadImageFile(const std::string& path, Image& out); static bool writePng(const std::string& path, const Image& im) { return stbi_write_png(path.c_str(), im.w, im.h, 4, im.px.data(), im.w * 4) != 0; } void addPicture(const std::string& path); // SDL's own file dialogs (the desktop's): the answer comes back later, maybe on another thread. void dialog(int kind); void takeDialogResult(); // ───────────────────────────── modes and tools ───────────────────────────── void setTool(Tool t); void enterEdit(); void leaveEdit(); void clearElements() { selVerts_.clear(), selFaces_.clear(), selEdges_.clear(); } void selectAllElements(); void selectAll(); void selectNone(); // The points the selection covers, whichever kind is picked. std::set selectedPoints(); // Selects (in the current kind) what these points cover. void selectPoints(const std::set& pts); void growSelection() { if (Object* o = editing()) selectPoints(growVerts(o->mesh, selectedPoints())); } void shrinkSelection(); // Like the picked: faces that face the same way, edges as long, points with as many edges. void selectSimilar(); // The picked edges' whole loops (or the rings across them). void loopsOfSelected(bool ring); void selectLinked() { if (Object* o = editing()) selectPoints(linkedVerts(o->mesh, selectedPoints())); } void invertSelection(); // The edges a rounding would work on: those selected, or round the selected faces, or between // selected points. std::set> edgesToRound(); void deleteElements(); // After a change that renumbers things: nothing selected, the shape is hand-made now. void changedShape(Object& o, bool keepSelection = false); void extrudeNow(float amount); void insetNow(float amount); void bevelNow(); void subdivideNow(bool smooth); void cutAt(Object& o, std::pair edge); void makeFaceNow(); void mergeNow(); void smoothPointsNow(); void startPush(); void pushWindow(); void bridgeNow(); void flipNow(); // Rotation and scale baked into the points (so the object's turn is none and its size is 1). void applyTransform(Object& o); void applyFinish(Object& o); void originToMiddle(Object& o); void originToBottom(Object& o); void mirror(Object& o, int axis); // ───────────────────────────── tabs ───────────────────────────── void setTab(Tab t); Shading shading() const { return tab_ == Tab::Render ? Shading::Rendered : shading_; } // ───────────────────────────── the knife and holes ───────────────────────────── // Snapped to steps of 45 degrees from the last point while Ctrl is held. ImVec2 knifeEnd(const ImGuiIO& io) const; static Mesh subMesh(const Mesh& m, const std::set& faces); // A new object like `from` (its look, place and finish), with another shape. uint32_t objectLike(uint32_t from, Mesh mesh, const std::string& name); void knifeApply(uint32_t id, std::vector pts); static float objectScale(const Object& o) { return std::max(std::cbrt(std::abs(glm::determinant(glm::mat3(objectMatrix(o))))), 1e-6f); } // How wide (across) a shape is when it reaches `reach` from its middle (dragging it out). float holeAcrossFor(float reach) const { return HoleShape(holeShape_) == HoleShape::Triangle || HoleShape(holeShape_) == HoleShape::Hexagon ? reach * std::sqrt(3.0f) : reach * 2.0f; } // The outline, `across` wide in the object's own size (a rectangle keeps its proportions). std::vector holeShapeOutline(const Object& o, int face, const glm::vec3& centre, float across) const; void applyHole(uint32_t id, int face, const glm::vec3& centre, float across); // The hole tool's outline under the mouse (or being dragged out), in the world. std::vector holePreview(const ImGuiIO& io); // Where the mouse is on a face's plane (the object's own space). std::optional onFacePlane(const Object& o, int face, ImVec2 mouse); // ───────────────────────────── painting ───────────────────────────── // The object ready to be painted: its rounding and smoothing made part of it, its surface laid // out on a picture (made the plain colour of it at first). Object* readyToPaint(uint32_t id); // The object's picture, copied so this change can be undone; the copy is now the object's. Image& changePicture(Object& o); void beginStroke(Object& o, bool in2D); void endStroke(); // Where on its picture a ray hits an object (0..1), or nothing. std::optional uvUnder(const Object& o, const Ray& ray); // One dab of the brush on the model: on every face near it, into the picture where that face // lies (so it carries on across the seams between pieces of the layout). // One dab of paint at a point of the object (the world), on the faces towards `eye` (the // mirrored dab: towards the eye mirrored). The stencil: only where the surface shows inside // it on the screen (the mirrored dab: where its mirror image does, `back` taking it there). void dab3D(Object& o, const glm::vec3& at, float radius, const glm::vec3* eyeAt = nullptr, const glm::mat4* back = nullptr); // The brush at a point of the view: starts a stroke on what's there, or carries one on. void paintAt(ImVec2 mouse, bool start); void pickColor(const Object& o, glm::vec2 uv); void fillAt(Object& o, glm::vec2 uv); // ── Sculpting ── // The object ready to sculpt: its finishing touches made part of it, and made finer (the same // shape) if it has too few points to push about. Object* readyToSculpt(uint32_t id); void endSculpt(); // The brush at a point of the view: starts a stroke on what's there, or carries one on. void sculptAt(ImVec2 mouse, bool start, bool ctrl, bool shift); void sculptViewport(const ImGuiIO& io); // ── Picture layers ── PictureLayer* editLayer(); // A picture put on the selected object as a new layer, unlocked to be moved: a sticker on the // surface in the middle of the view, or pinned onto the flat picture. void startFloating(Image img); void floatFromFile(const std::string& path); void pasteFromClipboard(); // Enter: it stays where it is, a layer (click it in the Picture panel to move it again). void lockLayer(); void unlockLayer(uint32_t object, int index); // Esc: a picture just pasted goes again; one unlocked is locked again. void cancelFloating(); void deleteLayer(Object& o, int i); // The layer painted into the painting for good (it can't be moved after). void mergeLayer(Object& o, int i); // The handles of a sticker being moved, in the world: its corners (top-left, top-right, // bottom-right, bottom-left), its turning handle, its middle — on its face's plane. bool stickerHandles(const Object& o, const PictureLayer& l, std::array& out); int stickerHandle(const Object& o, const PictureLayer& l, ImVec2 mouse); // Moving a sticker on the model: it slides over the surface (onto other faces too), its // corners size it, its handle turns it. void stickerMouse(const ImGuiIO& io); // A cheap fingerprint of what the object's picture shows (its painting, its layers, its shape). uint64_t pictureStamp(const Object& o); // The object's picture on the graphics card (its painting with its layers on top): sent again // when it changes — while painting or dragging, at most 20 times a second. void uploadPicture(const Object& o, bool now_ = false); // ───────────────────────────── the Layout tab ───────────────────────────── // The frames an object's faces are laid out in on its picture, moved, turned and sized by // hand (whole frames, or their corners); what's painted goes with them unless you say not. // What's picked, as (face, corner) pairs of its layout. std::vector> layoutPicked(const Object& o, const std::vector>& islands) const; // A corner and every other on the same point of the model at the same place on the picture. std::set> layoutCorner(const Object& o, int f, int c) const; void layoutSelectAll(); // The picked corners moved by `to` (picture pixels), painting carried along if wanted. void layoutChange(Object& o, const std::vector>& from, const std::shared_ptr& picture, const std::vector>& picked, const std::function& to, bool final); // Buttons' changes (turn, flip, straighten): about the middle of what's picked. void layoutApply(const std::function& to); void layoutStraighten(); // Laid out again by itself, what's painted carried over onto the new layout. void layoutAgain(Object& o); template void layoutCanvas(Object& o, ImDrawList* dl, F toScreen, glm::vec2 mouse, bool hovered, int W, int H) { const ImGuiIO& io = ImGui::GetIO(); if (lay_.object != o.id) lay_ = {}, lay_.object = o.id; const glm::vec2 size{float(W), float(H)}; const auto islands = uvIslands(o.mesh, o.uvs); std::vector islandOf(o.mesh.faces.size(), -1); for (size_t i = 0; i < islands.size(); ++i) for (int f : islands[i]) islandOf[size_t(f)] = int(i); auto screen = [&](int f, int c) { return toScreen(o.uvs[size_t(f)][size_t(c)] * size); }; auto faceUnder = [&]() { for (size_t f = 0; f < o.mesh.faces.size(); ++f) { std::vector poly; for (size_t c = 0; c < o.uvs[f].size(); ++c) poly.push_back(screen(int(f), int(c))); if (insidePolygon(io.MousePos, poly)) return int(f); } return -1; }; auto cornerUnder = [&]() { std::pair best{-1, -1}; float d = 9.0f; for (size_t f = 0; f < o.uvs.size(); ++f) for (size_t c = 0; c < o.uvs[f].size(); ++c) { const ImVec2 q = screen(int(f), int(c)); if (const float e = std::hypot(q.x - io.MousePos.x, q.y - io.MousePos.y); e < d) d = e, best = {int(f), int(c)}; } return best; }; const auto picked = layoutPicked(o, islands); std::set pickedFaces; for (auto [f, c] : picked) pickedFaces.insert(f); // Pressing: pick (what's under it, or a box), then move / turn / size what's picked. if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !lay_.drag) { bool hit = false, wasPicked = false; if (lay_.corners) { const auto [f, c] = cornerUnder(); if (f >= 0) { const auto same = layoutCorner(o, f, c); hit = true, wasPicked = lay_.points.count({f, c}) > 0; if (io.KeyShift && wasPicked) for (auto p : same) lay_.points.erase(p); else { if (!io.KeyShift && !wasPicked) lay_.points.clear(); lay_.points.insert(same.begin(), same.end()); } } } else if (const int f = faceUnder(); f >= 0) { const int i = islandOf[size_t(f)]; hit = true, wasPicked = lay_.frames.count(i) > 0; if (io.KeyShift && wasPicked) lay_.frames.erase(i); else { if (!io.KeyShift && !wasPicked) lay_.frames.clear(); lay_.frames.insert(i); } } const bool anything = !lay_.frames.empty() || !lay_.points.empty(); const bool transform = layTool_ != LayTool::Select && anything && !(io.KeyShift && hit) && (hit || layTool_ != LayTool::Move); if (transform) { pushUndo(); lay_.drag = 1, lay_.grab = mouse, lay_.start = o.uvs, lay_.startPicture = o.picture; glm::vec2 lo(1e30f), hi(-1e30f); for (auto [f, c] : layoutPicked(o, islands)) lo = glm::min(lo, o.uvs[size_t(f)][size_t(c)] * size), hi = glm::max(hi, o.uvs[size_t(f)][size_t(c)] * size); lay_.centre = (lo + hi) * 0.5f; } else if (!hit) { if (!io.KeyShift) lay_.frames.clear(), lay_.points.clear(); lay_.drag = 2, lay_.boxFrom = io.MousePos; } } if (lay_.drag == 1) { const glm::vec2 g = lay_.grab - lay_.centre, m = mouse - lay_.centre; std::function to; if (layTool_ == LayTool::Move || layTool_ == LayTool::Select) { const glm::vec2 d = mouse - lay_.grab; to = [d](glm::vec2 p) { return p + d; }; message_ = std::format("Moved {:.0f}, {:.0f} px", d.x, d.y); } else if (layTool_ == LayTool::Rotate) { float a = std::atan2(m.y, m.x) - std::atan2(g.y, g.x); if (io.KeyCtrl) a = glm::radians(snapTo(glm::degrees(a), 15.0f)); const glm::vec2 c = lay_.centre; to = [a, c](glm::vec2 p) { return c + glm::vec2(std::cos(a) * (p - c).x - std::sin(a) * (p - c).y, std::sin(a) * (p - c).x + std::cos(a) * (p - c).y); }; message_ = std::format("Turned {:.0f} degrees", glm::degrees(a)); } else { glm::vec2 k(glm::length(m) / std::max(glm::length(g), 1.0f)); if (io.KeyShift) // (only across or only up: whichever the drag goes along more) k = std::abs(g.x) > std::abs(g.y) ? glm::vec2(m.x / (std::abs(g.x) > 1 ? g.x : 1.0f), 1.0f) : glm::vec2(1.0f, m.y / (std::abs(g.y) > 1 ? g.y : 1.0f)); const glm::vec2 c = lay_.centre; to = [k, c](glm::vec2 p) { return c + (p - c) * k; }; message_ = std::format("Size {:.0f}% x {:.0f}%", k.x * 100, k.y * 100); } messageAt_ = now(); const bool done = !ImGui::IsMouseDown(ImGuiMouseButton_Left); layoutChange(o, lay_.start, lay_.startPicture, layoutPicked(o, islands), to, done); if (done) lay_.drag = 0, lay_.startPicture.reset(); } // Drawing: every face's outline, what's picked filled; corners as dots. const ImU32 fill = (kAccent & 0x00FFFFFF) | 0x38000000; for (size_t f = 0; f < o.mesh.faces.size(); ++f) { std::vector pts; for (size_t c = 0; c < o.uvs[f].size(); ++c) pts.push_back(screen(int(f), int(c))); const bool on = pickedFaces.count(int(f)) && !lay_.corners; if (on) for (const auto& t : triangulate(o.mesh, int(f))) { ImVec2 q[3]; for (int k = 0; k < 3; ++k) q[k] = pts[size_t(std::find(o.mesh.faces[f].begin(), o.mesh.faces[f].end(), t[size_t(k)]) - o.mesh.faces[f].begin())]; dl->AddTriangleFilled(q[0], q[1], q[2], fill); } dl->AddPolyline(pts.data(), int(pts.size()), on ? kAccent : IM_COL32(255, 255, 255, 150), ImDrawFlags_Closed, on ? 2.0f : 1.2f); } if (lay_.corners) { const auto hot = cornerUnder(); for (size_t f = 0; f < o.uvs.size(); ++f) for (size_t c = 0; c < o.uvs[f].size(); ++c) { const ImVec2 q = screen(int(f), int(c)); const bool on = lay_.points.count({int(f), int(c)}) > 0; dl->AddCircleFilled(q, on ? 4.5f : 3.0f, on ? kAccent : hot == std::pair{int(f), int(c)} ? kHot : IM_COL32(255, 255, 255, 200)); } } else if (hovered && !lay_.drag) if (const int f = faceUnder(); f >= 0 && !lay_.frames.count(islandOf[size_t(f)])) for (int g : islands[size_t(islandOf[size_t(f)])]) { std::vector pts; for (size_t c = 0; c < o.uvs[size_t(g)].size(); ++c) pts.push_back(screen(g, int(c))); dl->AddPolyline(pts.data(), int(pts.size()), kHot, ImDrawFlags_Closed, 1.5f); } if (lay_.drag == 1 && layTool_ != LayTool::Move && layTool_ != LayTool::Select) { // (the middle it turns or sizes about) const ImVec2 c = toScreen(lay_.centre); dl->AddCircle(c, 6, kAccent, 16, 2.0f); dl->AddLine(c, io.MousePos, (kAccent & 0x00FFFFFF) | 0x90000000, 1.0f); } if (lay_.drag == 2) { // a box to pick with const ImVec2 a = lay_.boxFrom, b = io.MousePos; dl->AddRectFilled(ImVec2(std::min(a.x, b.x), std::min(a.y, b.y)), ImVec2(std::max(a.x, b.x), std::max(a.y, b.y)), IM_COL32(255, 120, 40, 30)); dl->AddRect(ImVec2(std::min(a.x, b.x), std::min(a.y, b.y)), ImVec2(std::max(a.x, b.x), std::max(a.y, b.y)), kAccent); if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) { auto in = [&](ImVec2 q) { return q.x >= std::min(a.x, b.x) && q.x <= std::max(a.x, b.x) && q.y >= std::min(a.y, b.y) && q.y <= std::max(a.y, b.y); }; for (size_t f = 0; f < o.uvs.size(); ++f) for (size_t c = 0; c < o.uvs[f].size(); ++c) if (in(screen(int(f), int(c)))) { if (lay_.corners) lay_.points.insert({int(f), int(c)}); else lay_.frames.insert(islandOf[f]); } lay_.drag = 0; } } } // In the 3D view: clicking the model picks the frame its face is in (Shift: add). void layoutViewport(const ImGuiIO& io); void layoutOverlay(ImDrawList* dl); void layoutToolbox(float w); void layoutOptions(float w); void layoutProperties(float w); // ───────────────────────────── the Print tab ───────────────────────────── glm::vec3 bedSize() const; const PrintInfo& printInfo(const Object& o); std::vector printTargets(); void drawPrintBed(keel::Renderer& r, const keel::ModelLighting& light); void putOnBed(); void middleOfBed(); // Everything picked made `mm` big along `axis`, keeping its shape, standing where it stood. void printResize(int axis, float mm); // Everything picked made `k` times as big, standing where it stood. void printScale(float k); // The scaler: by a share, to fit the plate, or from the wrong units (a model made in inches, // centimetres or metres comes in 25.4, 10 or 1000 times off). float scalePercent_ = 100.0f; void printScaler(float w); void printSizeWindow(); void printToolbox(float w); void printOptions(float w); void printViewport(const ImGuiIO& io); // Where the mouse is on the model (a corner if near one), or on the bed. std::optional measurePoint(ImVec2 mouse); // The slice preview: where the printer goes round at the chosen layer, over the model. void layerOverlay(ImDrawList* dl); void printOverlay(ImDrawList* dl, const ImGuiIO& io); void printProperties(float w); // ───────────────────────────── layers: folders and going to things ───────────────────────────── Folder* findFolder(uint32_t id); bool inFolder(uint32_t folder, uint32_t within); std::set objectsIn(uint32_t folder); // The camera brought round to these (as framing them, but keeping the way it looks). void goTo(const std::set& ids); // The picked objects into a new folder (Ctrl+G): a group, picked and moved together. void groupSelected(); // Ungrouped: what's inside goes up a level, the folder goes. void ungroup(uint32_t id); void deleteFolder(uint32_t id); // A folder's eye or lock: all in it the same. void showFolder(Folder& f, bool on); void lockFolder(Folder& f, bool on); void moveToFolder(const std::set& ids, uint32_t folder); // Only these shown (again: everything shown). void showOnly(const std::set& ids); void folderMenu(uint32_t into, const std::set& ids, int depth = 0); // What a right-click on an object (or the picked ones) offers. void objectMenu(Object& o); // A row's show / hide or lock button: the picture of how it is now, dim when it's the usual way. bool rowToggle(const char* id, Icon icon, bool usual, const char* tip); // The ⌖ button at the end of a row: brings the camera to it. bool goButton(const char* tip); // Rows for what's in a folder (0: the top): its folders, then its objects. void layerRows(uint32_t parent, int depth); // ───────────────────────────── combining, copies, thickness ───────────────────────────── // The last picked object with the others added, taken out of it, or only where they overlap // kept; the others go. void combineSelected(Combine op); void makeCopies(); void copiesWindow(); void thickenActive(); // A model made elsewhere: each of its objects, standing on the ground in the middle of the view. void importFile(const std::string& file); // ── Spin: an outline turned round an axis (a vase, a wheel, a ring), or a screw ── bool showSpin_ = false, spinWorld_ = false, spinKeep_ = false; int spinAxis_ = 2, spinSteps_ = 48; float spinDegrees_ = 360.0f, spinRise_ = 0.0f; void spinWindow(); void spinSelected(); // ── Text as a shape ── bool showText_ = false; char textBuf_[512] = "Fry3DX"; int textFont_ = 2, textAlign_ = 0; float textHeight_ = 0.05f, textDepth_ = 0.01f, textSpacing_ = 0.0f; bool textStanding_ = false; std::string textFontFile_; // (a font of your own, picked from a file) static constexpr const char* kFonts[] = {"Instrument Sans", "Instrument Sans (semi-bold)", "Instrument Sans (bold)", "JetBrains Mono"}; static constexpr const char* kFontFiles[] = {"fonts/InstrumentSans-Regular.ttf", "fonts/InstrumentSans-SemiBold.ttf", "fonts/InstrumentSans-Bold.ttf", "fonts/JetBrainsMono-Regular.ttf"}; void textWindow(); void addText(); void thickenWindow(); void cleanFlatFaces(); // ───────────────────────────── touch: phones and tablets ───────────────────────────── // A phone: everything drawn bigger, the side panels only when asked for, a bar of big buttons // at the bottom (undo, Shift and Ctrl that stay down, editing the shape...), and fingers: // one drags the view round (or a tool's arrows), two move it and pinch to zoom, holding one // still is a right-click. bool touch() const { return touchMode_; } // How much bigger than its pixels the UI is drawn. float touchScale() const; void touchFrame(ImGuiIO& io); // The bar along the bottom of the view: big buttons, in rows if the view's narrow. void touchBar(); // ───────────────────────────── the camera ───────────────────────────── glm::vec3 viewDir() const { return forwardOf(yaw_, pitch_); } float viewHeight() const { return 2.0f * dist_ * std::tan(glm::radians(fov_) * 0.5f); } // metres across the view's height at the target void setView(View v); void leaveFlatView(); void frameBox(glm::vec3 lo, glm::vec3 hi); void frameAll(); void frameSelected(); bool mouseInView(const ImGuiIO& io) const; void orbitBy(ImVec2 d); void panBy(ImVec2 d); void updateCamera(ImGuiIO& io); // Where the eye is and how the view maps to the screen, from the camera as it is now: again // just before drawing, so the model and the lines over it agree (a touch can turn it between). void settleView(); // How near and far the view draws (units), going by how far away it looks: close up to a // 5 mm part, or out to see a whole town, the same share of the depth's precision. glm::vec2 nearFar(); // World point → screen (false behind the eye). // The window and its view are up (commands given at the start run before the first frame). bool viewReady() const { return ImGui::GetCurrentContext() && viewMax_.x - viewMin_.x > 1 && viewMax_.y - viewMin_.y > 1; } bool project(const glm::vec3& p, ImVec2& out) const; Ray mouseRay(ImVec2 m) const; // How many metres a pixel is, at a point. float pixelSize(const glm::vec3& at) const; // ───────────────────────────── what's shown ───────────────────────────── // The mesh as seen: with its rounding, smoothing and mirror — but while its shape is being // edited, only the mirror (the rest would hide the points you're moving). // Editable cuts show (but not while its own shape's being edited: the points you move). // A big one (smoothed into many faces, cut) is made on another thread: what was shown stays // till it's ready, so the window doesn't stop. const Mesh& shownMesh(const Object& o); // The key of the mesh actually shown (the one before, while a new one's being made). uint64_t meshKey(const Object& o, const Mesh& mesh); // Each object something's cut out of, and what its cutters are (their shapes, where they are): // a change, and it's made again (the caches follow its version). std::map cutKeys_; std::map followAt_; // (where each cut object and cutter was last frame) void followCuts(); bool isCutter(const Object& o) const { return fry::cutTarget(scene_, o) != nullptr; } // Editable cut: the last picked has the others cut out of it, and they stay objects to move. void cutEditable(); // The cut made for good: one cutter (or all of what's cut out of it) into the shape, gone. void applyCuts(uint32_t target, uint32_t only = 0); // ───────────────────────────── picking ───────────────────────────── struct Hit { uint32_t object = 0; int face = -1; float t = 1e30f; }; // base: against its own mesh (face numbers of that), not the rounded or smoothed one shown. Hit pickObject(const Ray& ray, uint32_t only = 0, bool base = false); // Each object's triangles in the world, in a tree of boxes: made again only when it changes // (its shape, where it is), not each time something's picked. struct Tree { // (in the object's own space: moving it doesn't make it again) uint64_t version = 0; size_t faces = 0; std::shared_ptr tree; }; std::map, Tree> trees_; // (an object's own mesh, or the one shown) const fry::TriangleTree& treeOf(const Object& o, const Mesh& mesh); // Can the eye see this point of the object (nothing of it in the way)? bool visible(const Object& o, const glm::vec3& world); // Whether the eye can see an object's point (nothing of it in between): each tried when it's // first asked about, kept till the object or the view changes. (Only what's drawn is asked: // the outline's points, not every point of a big mesh, as the view turns.) struct Visibility { uint64_t version = 0; glm::vec3 eye{1e30f}; glm::mat4 vp{0.0f}, matrix{0.0f}; glm::vec3 eyeIn{0.0f}; // (the eye in the object's own space) std::vector point; // 0 not tried yet, 1 seen, 2 hidden }; std::map, Visibility> visibility_; // (an object's own mesh, or the one shown) bool seen(const Object& o, const Mesh& mesh, int i); // Each edge of a mesh and the (first two) faces it's on, with the faces' normals: worked out // again only when the mesh changes. struct Adjacency { uint64_t version = 0; size_t verts = 0, faces = 0; std::vector> edges; // a, b, a face, the other (or -1) std::vector plane; // each face's normal, and its distance along it }; std::map, Adjacency> adjacency_; const Adjacency& adjacencyOf(const Object& o, const Mesh& mesh); bool pointSeen(const Object& o, int v) { return xray_ || seen(o, o.mesh, v); } // The edge of the edited object nearest the mouse on screen (that can be seen), or (-1, -1). std::pair nearestEdge(Object& o, ImVec2 mouse, float within); void clickSelect(const ImGuiIO& io); // A whole loop: of edges (points), or a ring of faces, through the edge under the mouse. void loopSelect(const ImGuiIO& io, bool ring = false); // Selects what's on screen inside a shape (a box, a lasso, the brush). void regionSelect(const std::function& inside, bool add, bool remove, bool clearFirst); // ───────────────────────────── the gizmo ───────────────────────────── bool hasSelection(); // Every point of the selection, in the world. std::vector selectionPoints(); glm::vec3 pivotOfSelection(); // The selection's box, in a gizmo's axes. std::pair selectionBox(const Axes& a); glm::vec3 selectionMiddle(const Axes& a); Orient& orient() { return mode_ == Mode::Edit ? orientEdit_ : orientObject_; } // The gizmo's axes: the object's own (turning with it), the world's, or (shape editing) the // way the selected faces face — so "up" on a side face is out of that face. Axes gizmoAxes(); // The extrude / inset handle: the selected faces' middle, their normal. bool faceHandle(glm::vec3& at, glm::vec3& normal); // The round-edges handle: off to the right of the selection's middle. bool bevelHandle(ImVec2& centre, ImVec2& handle); // The scale box's handles: 0-2 the far side along each axis, 4-6 the near side, 3 the middle. // (Pushed out a little where the box is flat, so they don't sit on top of each other.) glm::vec3 scaleHandle(const Axes& a, glm::vec3 lo, glm::vec3 hi, int h); // Which handle is under the mouse (-1 none). int hoverHandle(ImVec2 mouse); glm::vec3 originPoint(); glm::vec3 rotatePivot(const Axes& a) { return rotatePivot_ == Pivot::Origin ? originPoint() : selectionMiddle(a); } void startDrag(const ImGuiIO& io, int handle); // The points within reach of the selection (in the world: the object's size counts), each with // how much it follows: a grid of the selected points, so a big mesh stays quick. void findSoft(); void cancelDrag(); void updateDrag(const ImGuiIO& io); // Mirror editing: each point that moved, its twin across the shape's own X (the point that was // where it is, mirrored) moved the mirrored way. Points on the middle stay on it. void mirrorEdits(Object& o, const Mesh& before); // What moving snaps to near the mouse (points, edges' or faces' middles), in the world: not // what's being moved. Marked with a ring. std::optional snapTarget(ImVec2 mouse, float within = 22.0f); // The number typed while dragging, if any (just "-" or "." is none yet). std::optional typed() const; void dragNow(const ImGuiIO& io); // ───────────────────────────── drawing a shape ───────────────────────────── // The plane shapes are drawn on: the one the view looks straight at, or the ground. void workPlane(glm::vec3& point, glm::vec3& normal) const; // The plane drawn on: the face under the first point if it was on an object, else the view's. void drawPlane(glm::vec3& p, glm::vec3& n); std::optional drawPoint(const ImGuiIO& io); std::string sketchTyped_; // (drawing a shape: the next line's length, typed) // Drawing a shape: numbers typed go to the next line's length, Enter puts the point there. bool sketchKeys(const ImGuiIO& io); void finishShape(); // ───────────────────────────── the viewport's mouse ───────────────────────────── void viewport(ImGuiIO& io); void addDrawPoint(const ImGuiIO& io); // Painting: on the model under the mouse. void paintViewport(ImGuiIO& io); // The brush: whatever passes under it is selected (or, with Ctrl, let go). void paint(const ImGuiIO& io); // ───────────────────────────── keys ───────────────────────────── void shortcuts(ImGuiIO& io); // ───────────────────────────── 3D ───────────────────────────── #if defined(__ANDROID__) static constexpr int kRayLight = 0; // (light bounced by ray tracing: not on phones) #else static constexpr int kRayLight = 1; #endif // Solid: a plain grey room and soft studio light, no shadows or effects — for seeing shapes. // Rendered: sky, sun, shadows and effects. void applyShading(); // Painted, and shown as painted (with rounding or smoothing added since, it isn't). bool painted(const Object& o) const { return o.picture && o.uvsFit() && !o.hasFinish() && !cutKeys_.count(o.id); } keel::ModelId modelOf(const Object& o); const Picture& picture(const Reference& ref); void loadLogo(); void logoImage(float size); // A picture of ours in the UI, read the way ImGui's colours are (not turned linear: too dark). void uiImage(ImDrawList* dl, keel::TextureId t, ImVec2 a, ImVec2 b); float gridStep() const { return std::pow(10.0f, std::floor(std::log10(std::max(viewHeight() / 4.0f, 1e-4f)))); } void draw3D(keel::Renderer& r); // (the Print tab's: the printer's plate and overhangs) // On top of the scene: outlines and wires, points, the gizmo, the selection shapes. // An empty scene: how to start, in the middle of the view (gone once there's anything). void emptyHints(ImDrawList* dl); void overlays(); void gizmo(ImDrawList* dl, const ImGuiIO& io); // ───────────────────────────── panels ───────────────────────────── void setupUi(); // The least a panel can be and still read right (its words, its rows), by the font's size. static ImVec2 panelMin(std::string_view name); // Docked panels kept at their least size: where a split leaves one side smaller, the split // moves (when the other side has room to give). ImGui's own splitters know only one least // size for everything. Gives what the node needs. ImVec2 keepMinSizes(ImGuiDockNode* n); void defaultLayout(ImGuiID dock); // ───────────────────────────── commands ───────────────────────────── // Everything that can be done, by name: for the command search, keys of your own, the pies. void buildCommands(); void openSearch(); void runCommand(const std::string& id); void commandItem(const char* id); void commandSearch(); // The pies: the tools (Space), or the views (Shift+Space), round the mouse while it's held. void pieFrame(ImGuiIO& io); // Keys of your own: kept in keys.txt (a line each: the command's id, a tab, the key). void loadMyKeys(); void saveMyKeys(); void menus(); void menuBar(); void addMenuItems(); // How the last item's key looks: filled when it's the one in use, sunk while held. static keel::ui::KeyState keyState(bool on); // A square button with a picture on it (the accent when it's the one in use). bool iconButton(const char* id, Icon icon, bool on, const char* tip, float size = 38.0f); // A wide one, with words beside the picture. bool wideButton(const char* id, Icon icon, const char* label, bool on, float width, const char* tip = nullptr, float h = 30.0f); // Buttons in rows, as many as fit. template void grid(int count, float size, F each) { const float gap = 5.0f; const int perRow = std::max(1, int((ImGui::GetContentRegionAvail().x + gap) / (size + gap))); for (int i = 0; i < count; ++i) { if (i % perRow) ImGui::SameLine(0, gap); each(i); } } void toolbox(); // The plug-in tool that's on: which plug-in and which of its tools (none: -1). std::pair pluginToolAt() const; bool pluginToolFound() const { return pluginToolAt().first >= 0; } // A plug-in tool's click: the spot on what's under the mouse, or on the ground. void clickPluginTool(ImVec2 mouse); void paintToolbox(float w); void sculptOptions(float w); void paintOptions(float w); void softChoice(float w); void toolOptions(); void layersPanel(); // A value that's undoable: the undo point is taken when you start dragging or typing. template bool undoable(F widget) { const bool changed = widget(); if (ImGui::IsItemActivated()) pushUndo(); return changed; } void paintProperties(float w); void renderProperties(float w); // The Paint tab's picture: the selected object's, flat, to paint on and place pictures on. void picturePanel(); void propertiesPanel(); // A material to start from: sets colour, metal and rough in one go (each a little ball of it). // Yours (saved here, or from plug-ins and scripts) first; "Save its look as a material" adds one. void materialPicker(Object& o); void editProperties(Object& o, float w); void statusBar(); // The view's corner: which way you're looking, buttons to look straight at a side, and // handles to drag to orbit, pan and zoom (for a touchpad or a pen). void viewButtons(); // ───────────────────────────── scripts ───────────────────────────── void scriptLine(std::string text, bool error = false); fry::SceneScript* currentScript() { return script_ < scene_.scripts.size() ? &scene_.scripts[script_] : nullptr; } // "little_town.lua" -> "little town" (a script's name in the list). static std::string scriptName(const std::string& path); // Adds a script to the file's list and shows it (the same one again: just shows it). void addScript(std::string name, std::string code); // Runs the Script panel's script: one undo step; if it stops with an error, everything it did // is taken back. void runScriptNow(); // After a script or plug-in command: kept as one undo step, or all taken back on an error. void finishRun(const fry::ScriptResult& r, const std::string& name, bool frame); void openScript(const std::string& path, const std::string& shownName); void saveScript(std::string file); // The scripts that come with it (content/Fry3DX/scripts), as menu items that add one. void exampleScripts(); void exampleItems(bool selectable); // ── fry_bench: frames timed while the camera turns or a setting's dragged (finding what's slow) ── struct Bench { std::string what; int left = 0; std::vector ms; } bench_; std::vector> holding_; // (fry_hold: keys held down for that many more frames) float aoReach() const; void lookBy(ImVec2 d); void refocus(); float nearestThing(); float viewScale() const { return std::max(std::min(dist_, nearest_), 0.001f); } // (how big the view is where it matters: what's ahead, or nearer) void benchStep(); void benchEnd(std::chrono::steady_clock::time_point began); // ── Own materials (materials.txt in the settings folder) ── static std::string ownMaterialsPath() { return keel::prefPath() + "materials.txt"; } void loadOwnMaterials(); void saveOwnMaterials(); // ── Plug-ins: content/Fry3DX/plugins (they come with it) and the settings folder's plugins ── static std::string pluginsFolder() { return keel::prefPath() + "plugins/"; } std::set pluginsOff() const; void setPluginOn(const std::string& file, bool on); void loadPlugins(); void copyPlugin(const PluginEntry& e); void openPluginsFolder(); bool pluginUsable(size_t i) const { return i < plugins_.size() && plugins_[i].p && plugins_[i].error.empty(); } void pluginMenuItems(const char* menu); void pluginMenuItem(size_t i, size_t c); void runPluginCommand(size_t i, size_t c, const std::vector& values); // A plug-in command's settings, asked for before it runs. void commandForm(); void pluginsWindow(); static int growString(ImGuiInputTextCallbackData* d); // A one-line field for a std::string (it grows as it's typed into). static bool inputString(const char* label, std::string& text, const char* hint = nullptr) { return ImGui::InputTextWithHint(label, hint ? hint : "", text.data(), text.capacity() + 1, ImGuiInputTextFlags_CallbackResize, growString, &text); } void scriptPanel(); void helpWindow(); // Keys of your own: any command on any key (they come before the usual ones). void myKeysTable(); // ── (being made) ── bool showPrintSize_ = false, liveMirror_ = false, keepProportions_ = false; // Each picked object (or all, in the Print tab with none picked) made ready to print. void repairSelected(); // Crease (Shift+E): the picked edges (or those between picked points / round picked faces) // kept sharp when the shape's smoothed; again: not. void creaseSelected(); // Slide (Alt+G): the picked points glide along their edges, each along the edge that goes // most the way the mouse goes, never past its other end. Click: done. Esc / right click: as it was. struct Slide { bool active = false; uint32_t object = 0; Mesh from; std::vector pts; std::vector> rails; // (each point's neighbours that aren't picked: where it can go) ImVec2 start{0, 0}; } slide_; void startSlide(); void cancelSlide(); void slideFrame(const ImGuiIO& io); // Looking straight at a reference picture's plane (the front picture from the front or the back...). bool lookingAt(Axis a) const; std::string workspaceDir() const { return keel::prefPath() + "workspaces/"; } void resetLayout(); void workspacesWindow(); static std::string fileSafe(std::string n); // The steps that can be undone, oldest first: a click goes back (or on) to just after it. void historyWindow(); void aboutWindow(); }; } // namespace fryapp