mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
1543 lines
77 KiB
C++
1543 lines
77 KiB
C++
// 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 <SDL3/SDL.h>
|
||
#include <core/cvar.h>
|
||
#include <core/sysinfo.h>
|
||
#include <core/log.h>
|
||
#include <engine.h>
|
||
#include <fs/vfs.h>
|
||
#include <render/renderer.h>
|
||
#include <render/screenshot.h>
|
||
#include <ui/ui.h>
|
||
|
||
#include <glm/gtc/matrix_transform.hpp>
|
||
#include <glm/gtc/type_ptr.hpp>
|
||
#include <glm/gtx/matrix_decompose.hpp>
|
||
#include <glm/gtx/quaternion.hpp>
|
||
#include <imgui.h>
|
||
#include <imgui_internal.h>
|
||
#include <stb_image.h>
|
||
#include <stb_image_write.h>
|
||
|
||
#include <algorithm>
|
||
#include <bit>
|
||
#include <cmath>
|
||
#include <cstring>
|
||
#include <filesystem>
|
||
#include <format>
|
||
#include <chrono>
|
||
#include <ctime>
|
||
#include <fstream>
|
||
#include <functional>
|
||
#include <future>
|
||
#include <map>
|
||
#include <mutex>
|
||
#include <memory>
|
||
#include <optional>
|
||
#include <span>
|
||
|
||
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<glm::vec3> 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<glm::vec3>(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<ImVec2>& 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<glm::vec3, 3>;
|
||
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<float> 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<std::pair<glm::vec3, glm::vec3>> 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<const Mesh> mesh;
|
||
std::shared_ptr<const std::vector<std::vector<glm::vec2>>> uvs;
|
||
};
|
||
struct Snapshot {
|
||
Scene scene;
|
||
std::map<uint32_t, Shape> shapes;
|
||
std::set<uint32_t> selected;
|
||
uint32_t active;
|
||
Mode mode;
|
||
std::set<int> verts, faces;
|
||
std::set<std::pair<int, int>> edges;
|
||
std::string label; // (what was done after it: the message said then)
|
||
double at = 0;
|
||
};
|
||
std::vector<Snapshot> undo_, redo_;
|
||
std::map<uint32_t, Shape> 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<Snapshot> 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<uint32_t> 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<int> selVerts_, selFaces_;
|
||
std::set<std::pair<int, int>> 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<uint32_t, std::pair<uint64_t, float>> 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<glm::vec3> 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<std::pair<int, int>> bevelEdges;
|
||
std::vector<std::pair<int, float>> 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<int> pushPts_;
|
||
Mesh pushFrom_;
|
||
uint32_t pushObject_ = 0;
|
||
bool boxing_ = false, painting_ = false, lassoing_ = false, tapDraw_ = false;
|
||
ImVec2 boxStart_{0, 0};
|
||
std::vector<ImVec2> lasso_;
|
||
std::pair<int, int> cutHover_{-1, -1};
|
||
// The knife: points clicked on the screen (or a straight drag).
|
||
std::vector<ImVec2> 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> 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<ImVec2> 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<SculptMesh> 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<std::pair<int, float>> held[2];
|
||
std::vector<glm::vec3> 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<glm::vec2, 4> 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<int> frames;
|
||
std::set<std::pair<int, int>> points;
|
||
int drag = 0; // 1 moving / turning / sizing, 2 a box
|
||
glm::vec2 grab{0.0f}, centre{0.0f};
|
||
ImVec2 boxFrom{};
|
||
std::vector<std::vector<glm::vec2>> start;
|
||
std::shared_ptr<const Image> startPicture;
|
||
double remapAt = 0;
|
||
} lay_;
|
||
LayTool layTool_ = LayTool::Move;
|
||
bool layKeepPaint_ = true, layChecker_ = false;
|
||
// The Print tab.
|
||
PrintTool printTool_ = PrintTool::None;
|
||
std::vector<glm::vec3> 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<uint32_t, PrintInfo> 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<bool> autosaveJob_;
|
||
std::vector<std::string> recover_;
|
||
bool showRecover_ = false;
|
||
std::map<uint32_t, std::vector<uint8_t>> owner_; // which layer shows at each pixel
|
||
struct Uploaded {
|
||
uint64_t stamp = 0;
|
||
double at = 0;
|
||
bool dirty = false;
|
||
};
|
||
std::map<uint32_t, Uploaded> uploaded_;
|
||
std::map<uint32_t, keel::TextureId> 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<uint32_t, std::pair<keel::ModelId, uint64_t>> models_; // object id → its model, the state built
|
||
std::map<uint32_t, uint64_t> 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<const fry::TriangleTree> tree; // (made with it, when it was made on another thread)
|
||
std::future<std::pair<Mesh, std::shared_ptr<const fry::TriangleTree>>> coming; // (a big one being made on another thread)
|
||
uint64_t comingVersion = 0;
|
||
bool comingMirror = false;
|
||
};
|
||
std::map<uint32_t, Shown> shown_;
|
||
struct Picture {
|
||
bool ok = false;
|
||
float aspect = 1.0f;
|
||
};
|
||
std::map<std::string, Picture> 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<fry::Plugin> p;
|
||
};
|
||
std::vector<PluginEntry> plugins_;
|
||
bool showPlugins_ = false;
|
||
std::pair<std::string, std::string> 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<fry::Command> commands_;
|
||
fry::CommandSearch search_;
|
||
std::vector<std::string> recentCommands_;
|
||
std::map<std::string, fry::Chord> 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<double> values;
|
||
};
|
||
std::optional<PendingCommand> pendingCommand_;
|
||
bool nameMaterial_ = false;
|
||
std::string materialName_;
|
||
bool materialKeepColor_ = false;
|
||
std::vector<std::pair<std::string, bool>> 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<uint32_t, ShapeKey> 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<uint8_t> 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<int> selectedPoints();
|
||
// Selects (in the current kind) what these points cover.
|
||
void selectPoints(const std::set<int>& 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<std::pair<int, int>> 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<int, int> 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<int>& 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<ImVec2> 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<glm::vec3> 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<glm::vec3> holePreview(const ImGuiIO& io);
|
||
// Where the mouse is on a face's plane (the object's own space).
|
||
std::optional<glm::vec3> 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<glm::vec2> 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<glm::vec3, 6>& 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<std::pair<int, int>> layoutPicked(const Object& o, const std::vector<std::vector<int>>& islands) const;
|
||
// A corner and every other on the same point of the model at the same place on the picture.
|
||
std::set<std::pair<int, int>> 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<std::vector<glm::vec2>>& from, const std::shared_ptr<const Image>& picture,
|
||
const std::vector<std::pair<int, int>>& picked, const std::function<glm::vec2(glm::vec2)>& to, bool final);
|
||
// Buttons' changes (turn, flip, straighten): about the middle of what's picked.
|
||
void layoutApply(const std::function<glm::vec2(glm::vec2, glm::vec2)>& to);
|
||
void layoutStraighten();
|
||
// Laid out again by itself, what's painted carried over onto the new layout.
|
||
void layoutAgain(Object& o);
|
||
|
||
template <class F>
|
||
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<int> 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<ImVec2> 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<int, int> 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<int> 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<glm::vec2(glm::vec2)> 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<ImVec2> 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<ImVec2> 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<Object*> 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<glm::vec3> 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<uint32_t> objectsIn(uint32_t folder);
|
||
// The camera brought round to these (as framing them, but keeping the way it looks).
|
||
void goTo(const std::set<uint32_t>& 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<uint32_t>& ids, uint32_t folder);
|
||
// Only these shown (again: everything shown).
|
||
void showOnly(const std::set<uint32_t>& ids);
|
||
void folderMenu(uint32_t into, const std::set<uint32_t>& 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<uint32_t, uint64_t> cutKeys_;
|
||
std::map<uint32_t, glm::mat4> 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<const fry::TriangleTree> tree;
|
||
};
|
||
std::map<std::pair<uint32_t, bool>, 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<char> point; // 0 not tried yet, 1 seen, 2 hidden
|
||
};
|
||
std::map<std::pair<uint32_t, bool>, 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<std::array<int, 4>> edges; // a, b, a face, the other (or -1)
|
||
std::vector<glm::vec4> plane;
|
||
// each face's normal, and its distance along it
|
||
};
|
||
std::map<std::pair<uint32_t, bool>, 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<int, int> 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<bool(ImVec2)>& inside, bool add, bool remove, bool clearFirst);
|
||
|
||
// ───────────────────────────── the gizmo ─────────────────────────────
|
||
|
||
bool hasSelection();
|
||
// Every point of the selection, in the world.
|
||
std::vector<glm::vec3> selectionPoints();
|
||
glm::vec3 pivotOfSelection();
|
||
// The selection's box, in a gizmo's axes.
|
||
std::pair<glm::vec3, glm::vec3> 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<glm::vec3> snapTarget(ImVec2 mouse, float within = 22.0f);
|
||
// The number typed while dragging, if any (just "-" or "." is none yet).
|
||
std::optional<float> 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<glm::vec3> 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 <class F>
|
||
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<int, int> 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 <class F>
|
||
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<double> ms;
|
||
} bench_;
|
||
std::vector<std::pair<ImGuiKey, int>> 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<std::string> 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<double>& 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<int> pts;
|
||
std::vector<std::vector<int>> 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
|