diff --git a/src/app.cpp b/src/app.cpp index 6ac1b9f..f9d1757 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -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(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; } diff --git a/src/app.h b/src/app.h index 8983237..f101d67 100644 --- a/src/app.h +++ b/src/app.h @@ -35,6 +35,7 @@ #include #include +#include #include #include #include @@ -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 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 selVerts_, selFaces_; std::set> 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 coming; // (a big one being made on another thread) + std::shared_ptr tree; // (made with it, when it was made on another thread) + std::future>> coming; // (a big one being made on another thread) uint64_t comingVersion = 0; bool comingMirror = false; }; @@ -579,7 +589,8 @@ private: fry::CommandSearch search_; std::vector recentCommands_; std::map myKeys_; // (a command's id: the key you gave it) - int pie_ = 0; // 1: the tools round the mouse (Space held), 2: the views (Shift+Space) + 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 shapeKeys_; uint64_t shapeKey(const Object& o); + std::map shapeKeys_; + uint64_t shapeKey(const Object& o); void touchAll() { for (const Object& o : scene_.objects) versions_[o.id] = ++versionClock_; } // Its shape has been changed by hand: the library settings no longer apply. - void handMade(Object& o) { o.recipe.clear(), o.settings.clear(); } void say(std::string text); + void 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 pngBytes(const Image& im); void exportFile(std::string path, int filter = -1, const std::string& asExt = ""); + } + static std::vector pngBytes(const Image& im); + void exportFile(std::string path, int filter = -1, const std::string& asExt = ""); // ── Closing with changes, autosave ── // New, Open, Quit: asks about saving first if there are changes. - void whenSaved(int action); void doAfterAsking(int action); + 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 selectedPoints(); // Selects (in the current kind) what these points cover. void selectPoints(const std::set& pts); void growSelection() { if (Object* o = editing()) selectPoints(growVerts(o->mesh, selectedPoints())); - } void shrinkSelection(); + } + 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> edgesToRound(); void deleteElements(); // After a change that renumbers things: nothing selected, the shape is hand-made now. - void changedShape(Object& o, bool keepSelection = false); void extrudeNow(float amount); void insetNow(float amount); void bevelNow(); void subdivideNow(bool smooth); void cutAt(Object& o, std::pair edge); void makeFaceNow(); void mergeNow(); void smoothPointsNow(); void startPush(); void pushWindow(); void bridgeNow(); void flipNow(); + void changedShape(Object& o, bool keepSelection = false); + void extrudeNow(float amount); + void insetNow(float amount); + void bevelNow(); + void subdivideNow(bool smooth); + void cutAt(Object& o, std::pair edge); + void makeFaceNow(); + void mergeNow(); + void smoothPointsNow(); + void startPush(); + void pushWindow(); + void bridgeNow(); + void flipNow(); // Rotation and scale baked into the points (so the object's turn is none and its size is 1). - void applyTransform(Object& o); void applyFinish(Object& o); void originToMiddle(Object& o); void originToBottom(Object& o); void mirror(Object& o, int axis); + 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& faces); + ImVec2 knifeEnd(const ImGuiIO& io) const; + static Mesh subMesh(const Mesh& m, const std::set& faces); // A new object like `from` (its look, place and finish), with another shape. - uint32_t objectLike(uint32_t from, Mesh mesh, const std::string& name); void knifeApply(uint32_t id, std::vector pts); + uint32_t objectLike(uint32_t from, Mesh mesh, const std::string& name); + void knifeApply(uint32_t id, std::vector pts); static float objectScale(const Object& o) { return std::max(std::cbrt(std::abs(glm::determinant(glm::mat3(objectMatrix(o))))), 1e-6f); } // How wide (across) a shape is when it reaches `reach` from its middle (dragging it out). float holeAcrossFor(float reach) const { return HoleShape(holeShape_) == HoleShape::Triangle || HoleShape(holeShape_) == HoleShape::Hexagon ? reach * std::sqrt(3.0f) : reach * 2.0f; } // The outline, `across` wide in the object's own size (a rectangle keeps its proportions). - std::vector holeShapeOutline(const Object& o, int face, const glm::vec3& centre, float across) const; void applyHole(uint32_t id, int face, const glm::vec3& centre, float across); + std::vector holeShapeOutline(const Object& o, int face, const glm::vec3& centre, float across) const; + void applyHole(uint32_t id, int face, const glm::vec3& centre, float across); // The hole tool's outline under the mouse (or being dragged out), in the world. std::vector holePreview(const ImGuiIO& io); // Where the mouse is on a face's plane (the object's own space). @@ -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 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& out); int stickerHandle(const Object& o, const PictureLayer& l, ImVec2 mouse); + bool stickerHandles(const Object& o, const PictureLayer& l, std::array& out); + int stickerHandle(const Object& o, const PictureLayer& l, ImVec2 mouse); // Moving a sticker on the model: it slides over the surface (onto other faces too), its // corners size it, its handle turns it. void stickerMouse(const ImGuiIO& io); @@ -800,12 +868,14 @@ private: // What's picked, as (face, corner) pairs of its layout. std::vector> layoutPicked(const Object& o, const std::vector>& islands) const; // A corner and every other on the same point of the model at the same place on the picture. - std::set> layoutCorner(const Object& o, int f, int c) const; void layoutSelectAll(); + std::set> layoutCorner(const Object& o, int f, int c) const; + void layoutSelectAll(); // The picked corners moved by `to` (picture pixels), painting carried along if wanted. void layoutChange(Object& o, const std::vector>& from, const std::shared_ptr& picture, const std::vector>& picked, const std::function& to, bool final); // Buttons' changes (turn, flip, straighten): about the middle of what's picked. - void layoutApply(const std::function& to); void layoutStraighten(); + void layoutApply(const std::function& to); + void layoutStraighten(); // Laid out again by itself, what's painted carried over onto the new layout. void layoutAgain(Object& o); @@ -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 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 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 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 objectsIn(uint32_t folder); + Folder* findFolder(uint32_t id); + bool inFolder(uint32_t folder, uint32_t within); + std::set objectsIn(uint32_t folder); // The camera brought round to these (as framing them, but keeping the way it looks). void goTo(const std::set& ids); // The picked objects into a new folder (Ctrl+G): a group, picked and moved together. void groupSelected(); // Ungrouped: what's inside goes up a level, the folder goes. - void ungroup(uint32_t id); void deleteFolder(uint32_t id); + void ungroup(uint32_t id); + void deleteFolder(uint32_t id); // A folder's eye or lock: all in it the same. - void showFolder(Folder& f, bool on); void lockFolder(Folder& f, bool on); void moveToFolder(const std::set& ids, uint32_t folder); + void showFolder(Folder& f, bool on); + void lockFolder(Folder& f, bool on); + void moveToFolder(const std::set& ids, uint32_t folder); // Only these shown (again: everything shown). - void showOnly(const std::set& ids); void folderMenu(uint32_t into, const std::set& ids, int depth = 0); + void showOnly(const std::set& ids); + void folderMenu(uint32_t into, const std::set& ids, int depth = 0); // What a right-click on an object (or the picked ones) offers. void objectMenu(Object& o); // A row's show / hide or lock button: the picture of how it is now, dim when it's the usual way. @@ -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 tree; }; std::map, 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 point; // 0 not tried yet, 1 seen, 2 hidden }; std::map, 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> edges; // a, b, a face, the other (or -1) - std::vector plane; // each face's normal, and its distance along it + std::vector plane; + // each face's normal, and its distance along it }; - std::map, Adjacency> adjacency_; const Adjacency& adjacencyOf(const Object& o, const Mesh& mesh); + std::map, Adjacency> adjacency_; + const Adjacency& adjacencyOf(const Object& o, const Mesh& mesh); bool pointSeen(const Object& o, int v) { return xray_ || seen(o, o.mesh, v); } // The edge of the edited object nearest the mouse on screen (that can be seen), or (-1, -1). @@ -1133,9 +1243,11 @@ private: bool hasSelection(); // Every point of the selection, in the world. - std::vector selectionPoints(); glm::vec3 pivotOfSelection(); + std::vector selectionPoints(); + glm::vec3 pivotOfSelection(); // The selection's box, in a gizmo's axes. - std::pair selectionBox(const Axes& a); glm::vec3 selectionMiddle(const Axes& a); + std::pair selectionBox(const Axes& a); + glm::vec3 selectionMiddle(const Axes& a); Orient& orient() { return mode_ == Mode::Edit ? orientEdit_ : orientObject_; } // The gizmo's axes: the object's own (turning with it), the world's, or (shape editing) the // way the selected faces face — so "up" on a side face is out of that face. @@ -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 snapTarget(ImVec2 mouse, float within = 22.0f); // The number typed while dragging, if any (just "-" or "." is none yet). - std::optional typed() const; void dragNow(const ImGuiIO& io); + std::optional typed() const; + void dragNow(const ImGuiIO& io); // ───────────────────────────── drawing a shape ───────────────────────────── // The plane shapes are drawn on: the one the view looks straight at, or the ground. void workPlane(glm::vec3& point, glm::vec3& normal) const; // The plane drawn on: the face under the first point if it was on an object, else the view's. - void drawPlane(glm::vec3& p, glm::vec3& n); std::optional drawPoint(const ImGuiIO& io); + void drawPlane(glm::vec3& p, glm::vec3& n); + std::optional drawPoint(const ImGuiIO& io); std::string sketchTyped_; // (drawing a shape: the next line's length, typed) // Drawing a shape: numbers typed go to the next line's length, Enter puts the point there. - bool sketchKeys(const ImGuiIO& io); void finishShape(); + 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 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 pluginsOff() const; void setPluginOn(const std::string& file, bool on); void loadPlugins(); void copyPlugin(const PluginEntry& e); void openPluginsFolder(); - bool pluginUsable(size_t i) const { return i < plugins_.size() && plugins_[i].p && plugins_[i].error.empty(); } void pluginMenuItems(const char* menu); void pluginMenuItem(size_t i, size_t c); void runPluginCommand(size_t i, size_t c, const std::vector& values); + static std::string pluginsFolder() { return keel::prefPath() + "plugins/"; } + std::set pluginsOff() const; + void setPluginOn(const std::string& file, bool on); + void loadPlugins(); + void copyPlugin(const PluginEntry& e); + void openPluginsFolder(); + bool pluginUsable(size_t i) const { return i < plugins_.size() && plugins_[i].p && plugins_[i].error.empty(); } + void pluginMenuItems(const char* menu); + void pluginMenuItem(size_t i, size_t c); + void runPluginCommand(size_t i, size_t c, const std::vector& values); // A plug-in command's settings, asked for before it runs. - void commandForm(); void pluginsWindow(); static int growString(ImGuiInputTextCallbackData* d); + 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 pts; std::vector> rails; // (each point's neighbours that aren't picked: where it can go) ImVec2 start{0, 0}; - } slide_; void startSlide(); void cancelSlide(); void slideFrame(const ImGuiIO& io); + } 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(); diff --git a/src/app_view.cpp b/src/app_view.cpp index cafecbd..b5f2aa3 100644 --- a/src/app_view.cpp +++ b/src/app_view.cpp @@ -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(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(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(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); } } }