Smoother: dragging smoothing, rounding or the shading angle in Properties no longer stalls (the angle only re-lights it; rounded and smoothed shapes made on another thread, the newest wanted next, nothing waited on); the picking tree in the object's own space (moving it doesn't make it again; made with the shape on that thread); the grid drawn in pieces, each a pixel wide where it is and fading where its lines crowd together (no crawling towards the horizon), a hair towards the eye so it doesn't fight the ground in Rendered; the near plane further out (less flicker far off); fry_bench to time frames; app.h one declaration a line

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-21 09:10:13 +02:00
commit 9a7ed67d80
3 changed files with 332 additions and 116 deletions

View file

@ -258,6 +258,10 @@ auto App::init() -> void {
if (a.size() > 4) l->width = std::stof(a[3]), l->height = std::stof(a[4]);
settleSticker(o->mesh, *l);
}, "Fry3DX: fry_sticker x y [w h]: move the sticker being moved to that point of the view");
static keel::Cmd bench("fry_bench", [](const keel::Args& a) {
if (a.size() < 3) return self->say("fry_bench orbit|smooth|angle|round frames");
self->bench_ = {a[1], std::max(1, std::stoi(a[2])), {}};
}, "Fry3DX: fry_bench orbit|smooth|angle|round frames: time that many frames while the camera turns or the picked object's setting is dragged");
static keel::Cmd capture("fry_capture", [](const keel::Args& a) { if (a.size() > 1) self->savePicture(a[1]); }, "Fry3DX: save a picture of the view");
static keel::Cmd layout("fry_layout", [](const keel::Args& a) {
Object* o = self->find(self->active_);
@ -454,7 +458,33 @@ auto App::onEscape() -> void {
else selected_.clear();
}
// (fry_bench) Each frame: the change being timed.
auto App::benchStep() -> void {
if (bench_.left <= 0) return;
const int i = int(bench_.ms.size());
Object* o = find(active_);
if (bench_.what == "orbit") yaw_ += 2.0f;
else if (o && bench_.what == "smooth") o->smoothLevels = 1 + i / 4 % 3, o->smooth = true, touched(o->id);
else if (o && bench_.what == "angle") o->smooth = true, o->smoothAngle = 20.0f + float(i % 40), touched(o->id);
else if (o && bench_.what == "round") o->round = 0.01f + 0.001f * float(i % 30), touched(o->id);
}
auto App::benchEnd(std::chrono::steady_clock::time_point began) -> void {
if (bench_.left <= 0) return;
bench_.ms.push_back(std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - began).count());
if (--bench_.left) return;
double sum = 0, most = 0;
for (double m : bench_.ms) sum += m, most = std::max(most, m);
say(std::format("fry_bench {}: {} frames, {:.2f} ms each, the slowest {:.2f} ms", bench_.what, bench_.ms.size(), sum / double(bench_.ms.size()), most));
}
auto App::render(keel::Renderer& r) -> void {
struct Timed { // (for fry_bench: the whole frame, however it ends)
App* app;
std::chrono::steady_clock::time_point began = std::chrono::steady_clock::now();
~Timed() { app->benchEnd(began); }
} timed{this};
benchStep();
r_ = &r;
captureStep();
setupUi();
@ -565,7 +595,8 @@ auto App::shapeKey(const Object& o) -> uint64_t {
const uint64_t cut = cutKeys_.count(o.id) ? cutKeys_[o.id] : 0;
const void* pic = o.picture.get();
mix(&cut, 8), mix(&pic, sizeof pic);
mix(&o.mirror, 4), mix(&o.round, 4), mix(&o.roundAngle, 4), mix(&o.roundSteps, 4), mix(&o.smoothLevels, 4), mix(&o.smoothKeepSharp, 1), mix(&o.smooth, 1), mix(&o.smoothAngle, 4);
mix(&o.mirror, 4), mix(&o.round, 4), mix(&o.roundAngle, 4), mix(&o.roundSteps, 4), mix(&o.smoothLevels, 4), mix(&o.smoothKeepSharp, 1);
// (not smooth shading and its angle: they change only how it's lit, see modelOf)
k = {v, h ? h : 1};
return k.key;
}

330
src/app.h
View file

@ -35,6 +35,7 @@
#include <stb_image_write.h>
#include <algorithm>
#include <bit>
#include <cmath>
#include <cstring>
#include <filesystem>
@ -68,7 +69,8 @@ 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 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 };
@ -284,16 +286,21 @@ class App : public keel::Game {public:
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;
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();
std::string resumeFile() const { return keel::prefPath() + "resume.txt"; }
void onBackground() override;
void resumeWork();
void frame(double) override { autosave(); } bool allowQuit() override;
void frame(double) override { autosave(); }
bool allowQuit() override;
void shutdown() override {
if (autosaveJob_.valid()) autosaveJob_.wait();
} void onEscape() override;
}
void onEscape() override;
void render(keel::Renderer& r) override;
@ -329,7 +336,8 @@ private:
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 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_;
@ -411,7 +419,8 @@ private:
// 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)
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;
@ -545,7 +554,8 @@ private:
uint64_t version = 0; // (the shape key it was made for)
bool mirrorOnly = false;
Mesh mesh;
std::future<Mesh> coming; // (a big one being made on another thread)
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;
};
@ -579,7 +589,8 @@ private:
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)
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_;
@ -587,7 +598,8 @@ private:
// ── 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)
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;
@ -611,7 +623,8 @@ private:
// ───────────────────────────── document ─────────────────────────────
Object* find(uint32_t id); Reference* findRef(uint32_t id);
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
@ -620,20 +633,27 @@ private:
struct ShapeKey {
uint64_t version = 0, key = 0;
};
std::map<uint32_t, ShapeKey> shapeKeys_; uint64_t shapeKey(const Object& o);
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 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 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);
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);
@ -657,21 +677,27 @@ private:
// 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);
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);
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 = "");
}
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);
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();
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.
@ -680,44 +706,72 @@ private:
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();
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);
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();
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();
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();
}
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();
}
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();
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);
void applyTransform(Object& o);
void applyFinish(Object& o);
void originToMiddle(Object& o);
void originToBottom(Object& o);
void mirror(Object& o, int axis);
// ───────────────────────────── tabs ─────────────────────────────
@ -728,16 +782,19 @@ private:
// ───────────────────────────── 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);
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);
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);
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).
@ -749,7 +806,9 @@ private:
// 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();
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
@ -759,31 +818,40 @@ private:
// 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);
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();
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);
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();
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);
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);
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);
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);
@ -800,12 +868,14 @@ private:
// 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();
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();
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);
@ -954,36 +1024,58 @@ private:
}
}
// 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);
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();
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);
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);
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);
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);
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);
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);
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.
@ -997,14 +1089,19 @@ private:
// 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();
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();
float spinDegrees_ = 360.0f, spinRise_ = 0.0f;
void spinWindow();
void spinSelected();
// ── Text as a shape ──
bool showText_ = false;
@ -1014,9 +1111,12 @@ private:
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();
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();
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
@ -1026,7 +1126,8 @@ private:
bool touch() const { return touchMode_; }
// How much bigger than its pixels the UI is drawn.
float touchScale() const; void touchFrame(ImGuiIO& io);
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();
@ -1034,11 +1135,16 @@ private:
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();
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 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
@ -1050,7 +1156,9 @@ private:
// 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;
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;
@ -1086,11 +1194,10 @@ private:
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 {
struct Tree { // (in the object's own space: moving it doesn't make it again)
uint64_t version = 0;
glm::mat4 matrix{0.0f};
size_t faces = 0;
fry::TriangleTree tree;
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);
@ -1102,7 +1209,8 @@ private:
struct Visibility {
uint64_t version = 0;
glm::vec3 eye{1e30f};
glm::mat4 vp{0.0f};
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)
@ -1113,9 +1221,11 @@ private:
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::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);
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).
@ -1133,9 +1243,11 @@ private:
bool hasSelection();
// Every point of the selection, in the world.
std::vector<glm::vec3> selectionPoints(); glm::vec3 pivotOfSelection();
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);
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.
@ -1157,7 +1269,8 @@ private:
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 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
@ -1167,21 +1280,25 @@ private:
// 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);
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);
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();
bool sketchKeys(const ImGuiIO& io);
void finishShape();
// ───────────────────────────── the viewport's mouse ─────────────────────────────
void viewport(ImGuiIO& io); void addDrawPoint(const ImGuiIO& io);
void viewport(ImGuiIO& io);
void addDrawPoint(const ImGuiIO& io);
// Painting: on the model under the mouse.
void paintViewport(ImGuiIO& io);
@ -1209,7 +1326,8 @@ private:
const Picture& picture(const Reference& ref);
void loadLogo(); void logoImage(float size);
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);
@ -1220,7 +1338,8 @@ private:
// (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 emptyHints(ImDrawList* dl);
void overlays();
void gizmo(ImDrawList* dl, const ImGuiIO& io);
@ -1240,13 +1359,18 @@ private:
// ───────────────────────────── 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();
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 loadMyKeys();
void saveMyKeys();
void menus(); void menuBar();
void menus();
void menuBar();
void addMenuItems();
@ -1280,7 +1404,8 @@ private:
void paintOptions(float w);
void softChoice(float w); void toolOptions();
void softChoice(float w);
void toolOptions();
void layersPanel();
@ -1325,22 +1450,47 @@ private:
// 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);
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);
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_;
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();
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);
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);
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 scriptPanel();
void helpWindow();
@ -1363,11 +1513,17 @@ private:
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);
} 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);
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();

View file

@ -226,7 +226,9 @@ auto App::nearFar() -> glm::vec2 {
}
reach = std::max(reach, glm::length(o.position) + radius * std::max({std::abs(o.scale.x), std::abs(o.scale.y), std::abs(o.scale.z)}));
}
const float nearM = std::clamp(dist_ * 0.002f, 1e-5f, 50.0f);
// (the near plane as far out as it can be: the depth's precision is spread between them, so
// the nearer it is, the more surfaces far off flicker through each other)
const float nearM = std::clamp(dist_ * 0.005f, 1e-5f, 50.0f);
const float farM = std::max({dist_ * 60.0f, (glm::length(eye_) + reach) * 2.0f, 100.0f});
return glm::vec2(nearM, farM) * kUnits;
}
@ -268,19 +270,21 @@ auto App::shownMesh(const Object& o) -> const Mesh& {
Shown& s = shown_[o.id];
const uint64_t v = shapeKey(o);
if (s.coming.valid() && s.coming.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
Mesh got = s.coming.get();
if (s.comingVersion == v && s.comingMirror == editingThis) s.mesh = std::move(got), s.version = v, s.mirrorOnly = editingThis;
auto [got, tree] = s.coming.get();
if (s.comingVersion == v && s.comingMirror == editingThis) s.mesh = std::move(got), s.tree = std::move(tree), s.version = v, s.mirrorOnly = editingThis;
}
if (s.version == v && v && s.mirrorOnly == editingThis) return s.mesh;
// (what it'll come to: its faces four times over for each level of smoothing)
const size_t faces = o.mesh.faces.size() * (size_t(1) << (2 * std::clamp(editingThis ? 0 : o.smoothLevels, 0, 6))) * (o.mirror ? 2 : 1);
const bool big = faces > 40000 || (cut && o.mesh.faces.size() > 20000);
// (what it'll come to: its faces four times over for each level of smoothing, more for each
// step of rounding; past a few thousand it's made on another thread, so dragging a setting
// in Properties doesn't hold up every frame)
const size_t rounded = !editingThis && o.round > 0.0f ? size_t(std::max(o.roundSteps, 1)) + 1 : 1;
const size_t faces = o.mesh.faces.size() * rounded * (size_t(1) << (2 * std::clamp(editingThis ? 0 : o.smoothLevels, 0, 6))) * (o.mirror ? 2 : 1);
const bool big = faces > (editingThis ? 40000u : 6000u) || (cut && o.mesh.faces.size() > 2000); // (the mirror while editing: with each drag, so only when huge)
if (!big || s.mesh.verts.empty()) { // (small: now; the first time: now too, there's nothing to show yet)
s.mesh = cut ? fry::cutMesh(scene_, o) : finishedMesh(o, editingThis), s.version = v, s.mirrorOnly = editingThis;
s.mesh = cut ? fry::cutMesh(scene_, o) : finishedMesh(o, editingThis), s.tree.reset(), s.version = v, s.mirrorOnly = editingThis;
return s.mesh;
}
if (!s.coming.valid() || s.comingVersion != v || s.comingMirror != editingThis) {
if (s.coming.valid()) s.coming.wait(); // (one at a time for each object: the old one's thrown away)
if (!s.coming.valid()) { // (one at a time for each object: while one's being made, the newest wanted waits for it to land)
Scene part; // (just it and what cuts it: the thread has its own copies)
part.objects.push_back(o);
if (cut)
@ -288,7 +292,9 @@ auto App::shownMesh(const Object& o) -> const Mesh& {
if (c.cuts == o.id && c.id != o.id) part.objects.push_back(c);
s.comingVersion = v, s.comingMirror = editingThis;
s.coming = std::async(std::launch::async, [part = std::move(part), cut, editingThis] {
return cut ? fry::cutMesh(part, part.objects[0]) : finishedMesh(part.objects[0], editingThis);
Mesh m = cut ? fry::cutMesh(part, part.objects[0]) : finishedMesh(part.objects[0], editingThis);
auto tree = std::make_shared<const fry::TriangleTree>(m, glm::mat4(1.0f)); // (its picking tree too, while it's here)
return std::pair{std::move(m), std::move(tree)};
});
message_ = "Working it out...", messageAt_ = now();
}
@ -388,19 +394,24 @@ auto App::pickObject(const Ray& ray, uint32_t only, bool base) -> Hit {
for (const Object& o : scene_.objects) {
if (!o.visible || (only && o.id != only)) continue;
const Mesh& mesh = base || (mode_ == Mode::Edit && o.id == active_) ? o.mesh : shownMesh(o);
if (auto h = treeOf(o, mesh).nearest(ray.origin, ray.dir, best.t)) best = {o.id, h->face, h->t};
// (the ray into the object's own space, where its tree is: t is the same there)
const glm::mat4 m = objectMatrix(o);
if (std::abs(glm::determinant(m)) < 1e-12f) continue; // (squashed flat to nothing)
const glm::mat4 inv = glm::inverse(m);
const glm::vec3 from(inv * glm::vec4(ray.origin, 1.0f)), dir(inv * glm::vec4(ray.dir, 0.0f));
if (auto h = treeOf(o, mesh).nearest(from, dir, best.t)) best = {o.id, h->face, h->t};
}
return best;
}
// (an object's own mesh, or the one shown)
auto App::treeOf(const Object& o, const Mesh& mesh) -> const fry::TriangleTree& {
if (auto s = shown_.find(o.id); s != shown_.end() && &s->second.mesh == &mesh && s->second.tree) return *s->second.tree; // (made with it)
Tree& t = trees_[{o.id, &mesh != &o.mesh}];
const glm::mat4 m = objectMatrix(o);
const uint64_t version = meshKey(o, mesh);
if (t.version != version || !version || t.matrix != m || t.faces != mesh.faces.size() || t.tree.points().size() != mesh.verts.size())
t.version = version, t.matrix = m, t.faces = mesh.faces.size(), t.tree = fry::TriangleTree(mesh, m);
return t.tree;
if (!t.tree || t.version != version || !version || t.faces != mesh.faces.size() || t.tree->points().size() != mesh.verts.size())
t.version = version, t.faces = mesh.faces.size(), t.tree = std::make_shared<const fry::TriangleTree>(mesh, glm::mat4(1.0f));
return *t.tree;
}
// Can the eye see this point of the object (nothing of it in the way)?
@ -416,17 +427,20 @@ auto App::visible(const Object& o, const glm::vec3& world) -> bool {
auto App::seen(const Object& o, const Mesh& mesh, int i) -> bool {
Visibility& v = visibility_[{o.id, &mesh != &o.mesh}];
const uint64_t version = versions_[o.id];
if (v.version != version || glm::distance(v.eye, eye_) >= 1e-6f || v.vp != vp_ || v.point.size() != mesh.verts.size())
v.version = version, v.eye = eye_, v.vp = vp_, v.point.assign(mesh.verts.size(), 0);
const glm::mat4 m = objectMatrix(o);
if (v.version != version || glm::distance(v.eye, eye_) >= 1e-6f || v.vp != vp_ || v.matrix != m || v.point.size() != mesh.verts.size()) {
v.version = version, v.eye = eye_, v.vp = vp_, v.matrix = m, v.point.assign(mesh.verts.size(), 0);
v.eyeIn = std::abs(glm::determinant(m)) < 1e-12f ? eye_ : glm::vec3(glm::inverse(m) * glm::vec4(eye_, 1.0f));
}
char& got = v.point[size_t(i)];
if (got) return got == 1;
const fry::TriangleTree& tree = treeOf(o, mesh);
const glm::vec3 w = tree.points()[size_t(i)];
const glm::vec3 p = tree.points()[size_t(i)], w(m * glm::vec4(p, 1.0f));
const glm::vec4 c = vp_ * glm::vec4(w, 1.0f);
const glm::vec3 d = w - eye_;
const float len = glm::length(d);
const float len = glm::length(w - eye_);
if (c.w < 1e-5f || std::abs(c.x) > c.w || std::abs(c.y) > c.w || len < 1e-6f) return (got = 1); // (off the screen: not drawn anyway)
got = tree.blocked(eye_, d / len, len - std::max(len * 0.002f, 0.0005f), i) ? 2 : 1;
// (from the eye to the point in the object's own space: 1 is the point, a hair short of it in the world)
got = tree.blocked(v.eyeIn, p - v.eyeIn, 1.0f - std::max(0.002f, 0.0005f / len), i) ? 2 : 1;
return got == 1;
}
@ -1114,7 +1128,8 @@ auto App::applyShading() -> void {
auto App::modelOf(const Object& o) -> keel::ModelId {
const bool editingThis = mode_ == Mode::Edit && o.id == active_;
const Mesh& mesh = shownMesh(o);
const uint64_t want = (meshKey(o, mesh) * 2 + editingThis) * 2 + painted(o);
uint64_t want = (meshKey(o, mesh) * 2 + editingThis) * 2 + painted(o);
want = (want ^ uint64_t(o.smooth) ^ uint64_t(std::bit_cast<uint32_t>(o.smoothAngle)) << 1) * 1099511628211ull; // (how it's lit: smooth or not, and the angle)
auto it = models_.find(o.id);
if (it != models_.end() && it->second.second == want) return it->second.first;
keel::KmdlModel km;
@ -1290,13 +1305,27 @@ auto App::draw3D(keel::Renderer& r) -> void {
glm::vec4 col = glm::vec4(glm::vec3(shade), (major ? (rendered ? 0.35f : 0.13f) : (rendered ? 0.16f : 0.06f)) * fade);
if (axis) col = dir ? (n.x > 0.5f ? glm::vec4(0.3f, 0.75f, 0.25f, 0.9f) : glm::vec4(0.9f, 0.25f, 0.25f, 0.9f))
: (n.z > 0.5f ? glm::vec4(0.3f, 0.75f, 0.25f, 0.9f) : glm::vec4(0.25f, 0.45f, 0.95f, 0.9f));
const float w = pixelSize(target_) * (major || axis ? 1.6f : 1.0f) * kUnits;
const float from = mid - count * s, to = mid + count * s;
const glm::vec3 a = across * at + along * from, b = across * at + along * to;
if (level == 0 || std::abs(at - (dir ? cv : cu)) > inner * 1.0001f) r.drawStreak(a * kUnits, b * kUnits, w, col, keel::kWhiteTexture);
else { // (a far line through the fine ring, which has it already: its two ends)
r.drawStreak(a * kUnits, (across * at + along * (mid - inner)) * kUnits, w, col, keel::kWhiteTexture);
r.drawStreak((across * at + along * (mid + inner)) * kUnits, b * kUnits, w, col, keel::kWhiteTexture);
// (a far line through the fine ring, which has it already: only its two ends)
const bool split = level == 1 && std::abs(at - (dir ? cv : cu)) <= inner * 1.0001f;
const int pieces = level ? 24 : 8;
for (int k = 0; k < pieces; ++k) {
const float t0 = from + (to - from) * float(k) / float(pieces), t1 = from + (to - from) * float(k + 1) / float(pieces);
if (split && t1 > mid - inner && t0 < mid + inner) continue;
const glm::vec3 a = across * at + along * t0, b = across * at + along * t1, c = (a + b) * 0.5f;
// Each piece about a pixel wide where it is, and fading as the lines get
// too close together on the screen to tell apart (far off, or seen edge
// on): no crawling, flickering band towards the horizon.
const glm::vec3 sight = glm::normalize(c - eye_);
const float px = pixelSize(c), seen = glm::length(across - sight * glm::dot(across, sight));
const float apart = s * (major ? 10.0f : 1.0f) * seen / std::max(px, 1e-9f); // (pixels to the next line like it: a major line's are ten apart)
glm::vec4 pc = col;
pc.a *= axis ? std::max(glm::smoothstep(1.5f, 6.0f, apart), 0.35f) : glm::smoothstep(3.0f, 12.0f, apart);
if (pc.a < 0.004f) continue;
// (each end a hair towards the eye along its own line of sight: the same
// place on the screen, but in front of the ground it lies on, so they don't fight)
auto lift = [&](const glm::vec3& q) { return q + (eye_ - q) * 0.002f; };
r.drawStreak(lift(a) * kUnits, lift(b) * kUnits, px * (major || axis ? 1.6f : 1.0f) * kUnits, pc, keel::kWhiteTexture);
}
}
}