mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
Command search (F3 / Ctrl+Space), pie menus (Space: tools, Shift+Space: views), keys of your own, undo history, workspaces, select similar / loops / rings; (being made: the rest)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a79a6c9020
commit
48a9dbe918
4 changed files with 629 additions and 8 deletions
390
src/app.cpp
390
src/app.cpp
|
|
@ -3,6 +3,7 @@
|
|||
// and clear while you model (Solid), or properly lit (Rendered). The panels are Dear ImGui.
|
||||
//
|
||||
// fry3dx [file.fry]
|
||||
#include "commands.h"
|
||||
#include "icons.h"
|
||||
#include "mesh.h"
|
||||
#include "scene.h"
|
||||
|
|
@ -584,6 +585,15 @@ public:
|
|||
self->changedShape(*o, true);
|
||||
} else if (a[1] == "soft") self->softSel_ = a.size() < 3 || a[2] != "0", self->softReach_ = a.size() > 3 ? std::stof(a[3]) : self->softReach_;
|
||||
}, "Fry3DX: shape editing: fry_shapeop smooth | bridge | push metres | soft 1 [reach] | move x y z");
|
||||
static keel::Cmd search("fry_search", [](const keel::Args& a) { // fry_search [words]: the command search, typed in
|
||||
self->openSearch();
|
||||
if (a.size() > 1) std::snprintf(self->search_.text, sizeof self->search_.text, "%s", a[1].c_str());
|
||||
}, "Fry3DX: open the command search (F3), with words typed in");
|
||||
static keel::Cmd runCmd("fry_command", [](const keel::Args& a) { if (a.size() > 1) self->buildCommands(), self->runCommand(a[1]); }, "Fry3DX: run a command by its id (file.save, view.top...)");
|
||||
static keel::Cmd pie("fry_pie", [](const keel::Args& a) {
|
||||
self->pie_ = a.size() > 1 && a[1] == "views" ? 2 : 1, self->pieStays_ = true;
|
||||
self->pieAt_ = ImVec2((self->viewMin_.x + self->viewMax_.x) / 2, (self->viewMin_.y + self->viewMax_.y) / 2);
|
||||
}, "Fry3DX: show a pie menu (tools | views)");
|
||||
static keel::Cmd about("fry_about", [](const keel::Args&) { self->showAbout_ = true; }, "Fry3DX: show the About window");
|
||||
static keel::Cmd plugins("fry_plugins", [](const keel::Args& a) {
|
||||
if (a.size() > 1 && a[1] == "reload") self->loadPlugins();
|
||||
|
|
@ -608,6 +618,8 @@ public:
|
|||
newScene();
|
||||
loadOwnMaterials();
|
||||
loadPlugins();
|
||||
loadMyKeys();
|
||||
buildCommands();
|
||||
if (!startPath_.empty()) openFile(startPath_);
|
||||
findAutosaves();
|
||||
if (startPath_.empty()) resumeWork();
|
||||
|
|
@ -682,6 +694,7 @@ public:
|
|||
takeDialogResult();
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
if (touch()) touchFrame(io);
|
||||
if (!pendingLayout_.empty()) ImGui::LoadIniSettingsFromMemory(pendingLayout_.data(), pendingLayout_.size()), pendingLayout_.clear(); // (a workspace: before any panel)
|
||||
const ImGuiID dock = ImGui::DockSpaceOverViewport(0, nullptr, ImGuiDockNodeFlags_PassthruCentralNode);
|
||||
if (touch() && (io.DisplaySize.y > io.DisplaySize.x) != portrait_) portrait_ = !portrait_, layoutDone_ = false; // (the phone turned: its layout)
|
||||
if (!layoutDone_) defaultLayout(dock);
|
||||
|
|
@ -702,6 +715,8 @@ public:
|
|||
if (showScript_) scriptPanel();
|
||||
if (showHelp_) helpWindow();
|
||||
if (showAbout_) aboutWindow();
|
||||
if (showHistory_) historyWindow();
|
||||
if (showWorkspaces_) workspacesWindow();
|
||||
if (showPlugins_) pluginsWindow();
|
||||
commandForm();
|
||||
copiesWindow();
|
||||
|
|
@ -709,7 +724,9 @@ public:
|
|||
pushWindow();
|
||||
saveQuestion();
|
||||
recoverWindow();
|
||||
if (!askSave_ && !showRecover_) shortcuts(io);
|
||||
commandSearch();
|
||||
if (!askSave_ && !showRecover_ && !search_.open) shortcuts(io);
|
||||
if (pie_) return draw3D(r), overlays(), pieFrame(io);
|
||||
viewport(io);
|
||||
draw3D(r);
|
||||
overlays();
|
||||
|
|
@ -727,6 +744,8 @@ private:
|
|||
Mode mode;
|
||||
std::set<int> verts, faces;
|
||||
std::set<std::pair<int, int>> edges;
|
||||
std::string label; // (what was done after it: the message said then)
|
||||
double at = 0;
|
||||
};
|
||||
std::vector<Snapshot> undo_, redo_;
|
||||
std::vector<Snapshot> redoKept_; // (what redo had before the last undo point: back if that step is dropped)
|
||||
|
|
@ -967,6 +986,20 @@ private:
|
|||
};
|
||||
std::vector<PluginEntry> plugins_;
|
||||
bool showPlugins_ = false;
|
||||
// ── Commands: found by typing, on keys of your own, round the mouse ──
|
||||
std::vector<fry::Command> commands_;
|
||||
fry::CommandSearch search_;
|
||||
std::vector<std::string> recentCommands_;
|
||||
std::map<std::string, fry::Chord> myKeys_; // (a command's id: the key you gave it)
|
||||
int pie_ = 0; // 1: the tools round the mouse (Space held), 2: the views (Shift+Space)
|
||||
ImVec2 pieAt_{0, 0};
|
||||
bool pieStays_ = false; // (opened from the menu or console: until a click)
|
||||
std::string settingKey_;
|
||||
bool showHistory_ = false;
|
||||
// ── Workspaces: the panels as they were, kept under a name ──
|
||||
bool showWorkspaces_ = false, layoutAgain_ = false;
|
||||
std::string pendingLayout_;
|
||||
char workspaceName_[64] = "My workspace"; // (the command waiting for its new key, in Keys and mouse)
|
||||
// A plug-in's command waiting for its settings (the form), and a material being named.
|
||||
struct PendingCommand {
|
||||
size_t plugin = 0, command = 0;
|
||||
|
|
@ -1009,11 +1042,12 @@ private:
|
|||
void handMade(Object& o) { o.recipe.clear(), o.settings.clear(); }
|
||||
void say(std::string text) {
|
||||
message_ = std::move(text), messageAt_ = now();
|
||||
if (!undo_.empty() && undo_.back().label.empty() && now() - undo_.back().at < 1.0) undo_.back().label = message_; // (names the step just done)
|
||||
keel::log::info("{}", message_);
|
||||
}
|
||||
|
||||
void pushUndo() {
|
||||
undo_.push_back({scene_, selected_, active_, mode_, selVerts_, selFaces_, selEdges_});
|
||||
undo_.push_back({scene_, selected_, active_, mode_, selVerts_, selFaces_, selEdges_, "", now()});
|
||||
if (undo_.size() > 200) undo_.erase(undo_.begin());
|
||||
redoKept_ = std::move(redo_), redo_.clear();
|
||||
dirty_ = true, ++edits_;
|
||||
|
|
@ -1041,14 +1075,14 @@ private:
|
|||
}
|
||||
void undo() {
|
||||
if (undo_.empty()) return say("Nothing to undo");
|
||||
redo_.push_back({scene_, selected_, active_, mode_, selVerts_, selFaces_, selEdges_});
|
||||
redo_.push_back({scene_, selected_, active_, mode_, selVerts_, selFaces_, selEdges_, undo_.back().label, now()}); // (its name: the step it brings back)
|
||||
restore(undo_.back());
|
||||
undo_.pop_back();
|
||||
redoKept_.clear();
|
||||
}
|
||||
void redo() {
|
||||
if (redo_.empty()) return say("Nothing to redo");
|
||||
undo_.push_back({scene_, selected_, active_, mode_, selVerts_, selFaces_, selEdges_});
|
||||
undo_.push_back({scene_, selected_, active_, mode_, selVerts_, selFaces_, selEdges_, redo_.back().label, now() - 10});
|
||||
restore(redo_.back());
|
||||
redo_.pop_back();
|
||||
redoKept_.clear();
|
||||
|
|
@ -1651,6 +1685,48 @@ private:
|
|||
if (!border.count(v)) keep.insert(v);
|
||||
selectPoints(keep);
|
||||
}
|
||||
// Like the picked: faces that face the same way, edges as long, points with as many edges.
|
||||
void selectSimilar() {
|
||||
Object* o = editing();
|
||||
if (!o) return;
|
||||
const Mesh& m = o->mesh;
|
||||
if (elem_ == Elem::Face) {
|
||||
std::vector<glm::vec3> ns;
|
||||
for (int f : selFaces_) ns.push_back(faceNormal(m, f));
|
||||
if (ns.empty()) return say("Pick a face first");
|
||||
for (size_t f = 0; f < m.faces.size(); ++f)
|
||||
for (const glm::vec3& n : ns)
|
||||
if (glm::dot(faceNormal(m, int(f)), n) > 0.996f) selFaces_.insert(int(f));
|
||||
} else if (elem_ == Elem::Edge) {
|
||||
std::vector<float> lens;
|
||||
for (auto [a, b] : selEdges_) lens.push_back(glm::distance(m.verts[size_t(a)], m.verts[size_t(b)]));
|
||||
if (lens.empty()) return say("Pick an edge first");
|
||||
for (auto [a, b] : edges(m)) {
|
||||
const float l = glm::distance(m.verts[size_t(a)], m.verts[size_t(b)]);
|
||||
for (float k : lens)
|
||||
if (std::abs(l - k) <= k * 0.02f + 1e-6f) selEdges_.insert({a, b});
|
||||
}
|
||||
} else {
|
||||
std::vector<int> count(m.verts.size(), 0);
|
||||
for (auto [a, b] : edges(m)) ++count[size_t(a)], ++count[size_t(b)];
|
||||
std::set<int> want;
|
||||
for (int v : selVerts_) want.insert(count[size_t(v)]);
|
||||
if (want.empty()) return say("Pick a point first");
|
||||
for (size_t v = 0; v < m.verts.size(); ++v)
|
||||
if (want.count(count[size_t(v)])) selVerts_.insert(int(v));
|
||||
}
|
||||
say("Picked the similar ones");
|
||||
}
|
||||
// The picked edges' whole loops (or the rings across them).
|
||||
void loopsOfSelected(bool ring) {
|
||||
Object* o = editing();
|
||||
if (!o) return;
|
||||
if (elem_ != Elem::Edge || selEdges_.empty()) return say("Pick an edge or two first (Edges, 2)");
|
||||
std::set<std::pair<int, int>> add;
|
||||
for (auto e : selEdges_)
|
||||
for (auto f : ring ? edgeRing(o->mesh, e) : edgeLoop(o->mesh, e)) add.insert(f);
|
||||
selEdges_.insert(add.begin(), add.end());
|
||||
}
|
||||
void selectLinked() {
|
||||
if (Object* o = editing()) selectPoints(linkedVerts(o->mesh, selectedPoints()));
|
||||
}
|
||||
|
|
@ -4776,6 +4852,13 @@ private:
|
|||
if (io.WantTextInput) return;
|
||||
const bool ctrl = io.KeyCtrl, shift = io.KeyShift, alt = io.KeyAlt;
|
||||
auto key = [](ImGuiKey k) { return ImGui::IsKeyPressed(k, false); };
|
||||
if (key(ImGuiKey_F3) || (ctrl && key(ImGuiKey_Space))) return openSearch();
|
||||
if (!settingKey_.empty()) return; // (Keys and mouse is waiting for a key)
|
||||
if (const auto chord = fry::pressedChord())
|
||||
for (const auto& [id, c] : myKeys_)
|
||||
if (c == *chord) return void(runCommand(id));
|
||||
if (key(ImGuiKey_Space) && !ctrl && !alt && mouseInView(io) && (tab_ == Tab::Model || tab_ == Tab::Render || tab_ == Tab::Print))
|
||||
return void((pie_ = shift ? 2 : 1, pieAt_ = io.MousePos));
|
||||
if (key(ImGuiKey_F5)) return runScriptNow();
|
||||
if (ctrl && key(ImGuiKey_1)) setTab(Tab::Model);
|
||||
if (ctrl && key(ImGuiKey_2)) setTab(Tab::Paint);
|
||||
|
|
@ -5439,7 +5522,7 @@ private:
|
|||
void defaultLayout(ImGuiID dock) {
|
||||
layoutDone_ = true;
|
||||
ImGuiDockNode* node = ImGui::DockBuilderGetNode(dock);
|
||||
if (!touch() && node && (node->IsSplitNode() || node->Windows.Size)) return; // (the user's own, from the .ini)
|
||||
if (!touch() && !std::exchange(layoutAgain_, false) && node && (node->IsSplitNode() || node->Windows.Size)) return; // (the user's own, from the .ini)
|
||||
ImGui::DockBuilderRemoveNodeChildNodes(dock);
|
||||
ImGui::DockBuilderSetNodeSize(dock, ImGui::GetMainViewport()->WorkSize); // (splits are shares of this)
|
||||
ImGuiID center = dock;
|
||||
|
|
@ -5483,6 +5566,180 @@ private:
|
|||
ImGui::DockBuilderFinish(dock);
|
||||
}
|
||||
|
||||
// ───────────────────────────── commands ─────────────────────────────
|
||||
|
||||
// Everything that can be done, by name: for the command search, keys of your own, the pies.
|
||||
void buildCommands() {
|
||||
commands_.clear();
|
||||
auto add = [&](std::string id, std::string name, std::string group, std::string keys, std::function<void()> run, std::function<bool()> ok = {}, std::string words = {}) {
|
||||
commands_.push_back({std::move(id), std::move(name), std::move(group), std::move(keys), std::move(run), std::move(ok), std::move(words)});
|
||||
};
|
||||
auto editing = [this] { return mode_ == Mode::Edit; };
|
||||
auto picked = [this] { return !selected_.empty(); };
|
||||
auto many = [this] { return selected_.size() >= 2 && mode_ == Mode::Object; };
|
||||
auto model = [this] { return tab_ == Tab::Model; };
|
||||
add("file.new", "New", "File", "Ctrl+N", [this] { whenSaved(2); }, {}, "empty start");
|
||||
add("file.open", "Open...", "File", "Ctrl+O", [this] { whenSaved(3); }, {}, "load fry");
|
||||
add("file.import", "Import a model (STL, OBJ, 3MF)...", "File", "Ctrl+Shift+O", [this] { dialog(12); }, {}, "load mesh stl obj 3mf");
|
||||
add("file.save", "Save", "File", "Ctrl+S", [this] { path_.empty() ? dialog(2) : void(saveFile(path_)); });
|
||||
add("file.saveas", "Save as...", "File", "Ctrl+Shift+S", [this] { dialog(2); });
|
||||
add("file.export", "Export (GLB, glTF, OBJ, STL, PLY, 3MF, FBX)...", "File", "Ctrl+E", [this] { dialog(3); }, {}, "stl obj gltf glb ply fbx 3mf print");
|
||||
add("file.picture", "Save a picture of the view...", "File", "F12", [this] { dialog(5); }, {}, "screenshot render png");
|
||||
add("file.recover", "Recover autosaved work...", "File", "", [this] { findAutosaves(), showRecover_ = !recover_.empty(); });
|
||||
add("file.quit", "Quit", "File", "Ctrl+Q", [this] { whenSaved(1); }, {}, "exit close");
|
||||
add("edit.undo", "Undo", "Edit", "Ctrl+Z", [this] { undo(); }, [this] { return !undo_.empty(); });
|
||||
add("edit.redo", "Redo", "Edit", "Ctrl+Y", [this] { redo(); }, [this] { return !redo_.empty(); });
|
||||
add("edit.history", "Undo history", "Edit", "", [this] { showHistory_ = true; }, {}, "steps back");
|
||||
add("edit.duplicate", "Duplicate", "Edit", "Ctrl+D", [this] { duplicateSelected(); }, [=, this] { return picked() && mode_ == Mode::Object; }, "copy clone");
|
||||
add("edit.join", "Join into one", "Edit", "Ctrl+J", [this] { joinSelected(); }, many, "merge objects");
|
||||
add("edit.delete", "Delete", "Edit", "Del", [this] { deleteSelected(); }, {}, "remove erase");
|
||||
add("edit.shape", "Edit the shape / stop", "Edit", "Tab", [this] { mode_ == Mode::Edit ? leaveEdit() : enterEdit(); }, [=, this] { return editing() || picked(); }, "edit mode points faces");
|
||||
add("select.all", "Select all", "Select", "Ctrl+A", [this] { selectAll(); });
|
||||
add("select.none", "Select none", "Select", "Alt+A", [this] { selectNone(); }, {}, "deselect");
|
||||
add("select.invert", "Invert the selection", "Select", "Ctrl+I", [this] { invertSelection(); });
|
||||
add("select.grow", "Select more (grow)", "Select", "Ctrl +", [this] { growSelection(); }, editing);
|
||||
add("select.shrink", "Select less (shrink)", "Select", "Ctrl -", [this] { shrinkSelection(); }, editing);
|
||||
add("select.linked", "Select everything joined to it", "Select", "L", [this] { selectLinked(); }, editing, "linked connected");
|
||||
add("select.similar", "Select similar faces (facing the same way)", "Select", "Shift+G", [this] { selectSimilar(); }, editing, "same normal size");
|
||||
add("select.loop", "Select whole loops (of the picked edges)", "Select", "", [this] { loopsOfSelected(false); }, editing, "edge loop");
|
||||
add("select.ring", "Select rings (across the picked edges)", "Select", "", [this] { loopsOfSelected(true); }, editing, "edge ring");
|
||||
for (const auto& [t, n] : std::initializer_list<std::pair<SelShape, const char*>>{{SelShape::Box, "box"}, {SelShape::Brush, "brush"}, {SelShape::Lasso, "lasso"}})
|
||||
add(std::string("select.with.") + n, std::string("Select with a ") + n, "Select", "", [this, t] { selShape_ = t; });
|
||||
for (const ShapeRecipe& r : shapeRecipes())
|
||||
add(std::string("add.") + r.id, std::string("Add ") + (r.starter ? "a starter " : "a ") + r.name, "Add", "", [this, id = std::string(r.id)] { setTab(Tab::Model), addShape(id); }, {},
|
||||
r.starter ? "starter model" : "shape primitive");
|
||||
add("add.text", "Add text (3D letters)...", "Add", "", [this] { setTab(Tab::Model), showText_ = true; }, {}, "font letters words label name");
|
||||
add("add.draw", "Draw a shape", "Add", "D", [this] { if (mode_ == Mode::Edit) leaveEdit(); setTool(Tool::Draw); }, {}, "sketch outline polygon");
|
||||
add("add.picture", "Add a reference picture...", "Add", "", [this] { dialog(4); }, {}, "image background blueprint");
|
||||
add("object.goto", "Go to it (the camera)", "Object", "F", [this] { goTo(selected_); }, picked, "frame focus");
|
||||
add("object.copies", "Make copies (in a row, round, along a path)...", "Object", "", [this] { showCopies_ = true; }, picked, "array repeat pattern circular curve");
|
||||
add("object.group", "Group into a folder", "Object", "Ctrl+G", [this] { groupSelected(); }, picked);
|
||||
add("object.add", "Add them together", "Object", "Ctrl+Shift++", [this] { combineSelected(Combine::Add); }, many, "boolean union");
|
||||
add("object.subtract", "Take the others out of it", "Object", "Ctrl+Shift+-", [this] { combineSelected(Combine::Subtract); }, many, "boolean difference cut");
|
||||
add("object.overlap", "Keep only where they overlap", "Object", "", [this] { combineSelected(Combine::Overlap); }, many, "boolean intersect");
|
||||
add("object.thicken", "Make it thick (or hollow)...", "Object", "", [this] { showThicken_ = true; }, picked, "shell solidify wall");
|
||||
add("object.cleanflat", "Join flat faces side by side", "Object", "", [this] { cleanFlatFaces(); }, picked, "dissolve tidy");
|
||||
add("object.spin", "Spin (turn its outline round an axis)...", "Object", "", [this] { showSpin_ = true; }, [this] { return !selected_.empty(); }, "lathe screw revolve");
|
||||
add("object.repair", "Repair for printing (close holes, fix faces)", "Print", "", [this] { repairSelected(); }, picked, "fix manifold watertight holes");
|
||||
add("object.scale", "Scale to a size (mm)...", "Print", "", [this] { setTab(Tab::Print), showPrintSize_ = true; }, picked, "resize fit dimensions percent scaler");
|
||||
for (const ToolInfo& t : kTools)
|
||||
add(std::string("tool.") + t.name, std::string("Tool: ") + t.name, "Tools", t.key, [this, tool = t.tool, obj = t.objectMode] {
|
||||
if (tab_ != Tab::Model) setTab(Tab::Model);
|
||||
if (!obj && mode_ != Mode::Edit) enterEdit();
|
||||
setTool(tool);
|
||||
});
|
||||
add("shape.subdivide", "Cut every face in four", "Shape", "", [this] { subdivideNow(false); }, editing, "subdivide");
|
||||
add("shape.smoothall", "Smooth it all (cut and round)", "Shape", "", [this] { subdivideNow(true); }, editing, "subdivision surface");
|
||||
add("shape.merge", "Merge points", "Shape", "M", [this] { mergeNow(); }, editing, "weld collapse");
|
||||
add("shape.face", "Make a face", "Shape", "", [this] { makeFaceNow(); }, editing, "fill");
|
||||
add("shape.flip", "Flip faces", "Shape", "", [this] { flipNow(); }, editing, "normals reverse");
|
||||
add("shape.smooth", "Smooth points", "Shape", "", [this] { smoothPointsNow(); }, editing, "relax");
|
||||
add("shape.push", "Push out / pull in...", "Shape", "Alt+S", [this] { startPush(); }, editing, "shrink fatten inflate");
|
||||
add("shape.bridge", "Bridge two faces", "Shape", "", [this] { bridgeNow(); }, editing, "tunnel connect");
|
||||
add("shape.soft", "Soft selection on / off", "Shape", "Shift+O", [this] { softSel_ = !softSel_; }, {}, "proportional falloff");
|
||||
add("shape.separate", "Make into its own object", "Shape", "P", [this] { separateSelected(); }, editing, "separate split");
|
||||
add("shape.slide", "Slide the points along their edges...", "Shape", "", [this] { startSlide(); }, editing, "edge slide vertex slide");
|
||||
add("shape.mirror", "Mirror editing (both sides change together)", "Shape", "", [this] { liveMirror_ = !liveMirror_; }, {}, "symmetry symmetric x");
|
||||
add("view.solid", "Show: solid", "View", "Z", [this] { shading_ = Shading::Solid; });
|
||||
add("view.rendered", "Show: rendered (sky, sun, shadows)", "View", "Z", [this] { shading_ = Shading::Rendered; });
|
||||
add("view.wire", "Show: wireframe", "View", "Shift+Z", [this] { shading_ = Shading::Wire; });
|
||||
add("view.xray", "X-ray on / off", "View", "Alt+Z", [this] { xray_ = !xray_; }, {}, "see through");
|
||||
add("view.grid", "Grid on / off", "View", "", [this] { showGrid_ = !showGrid_; });
|
||||
add("view.persp", "View: perspective", "View", "Numpad 5", [this] { leaveFlatView(); });
|
||||
for (const auto& [v, n, k] : std::initializer_list<std::tuple<View, const char*, const char*>>{
|
||||
{View::Front, "front", "Numpad 1"}, {View::Back, "back", "Shift+Numpad 1"}, {View::Left, "left side", "Numpad 3"}, {View::Right, "right side", "Shift+Numpad 3"},
|
||||
{View::Top, "top", "Numpad 7"}, {View::Bottom, "bottom", "Shift+Numpad 7"}})
|
||||
add(std::string("view.") + n, std::string("View: ") + n, "View", k, [this, v] { setView(v); }, {}, "orthographic camera");
|
||||
add("view.frame", "Frame the selected", "View", "F", [this] { frameSelected(); }, {}, "zoom focus");
|
||||
add("view.all", "Frame everything", "View", "Home", [this] { frameAll(); }, {}, "zoom extents");
|
||||
add("view.only", "Show only the selected (again: all)", "View", "/", [this] { showOnly(selected_); }, picked, "isolate local view hide");
|
||||
add("view.unhide", "Show everything", "View", "Alt+H", [this] { for (Object& o : scene_.objects) o.visible = true; }, {}, "unhide");
|
||||
add("view.layout", "Put the panels back as they were", "View", "", [this] { resetLayout(); }, {}, "reset layout workspace default panels");
|
||||
add("view.workspace", "Workspaces (save the panels as they are)...", "View", "", [this] { showWorkspaces_ = true; }, {}, "layout save panels");
|
||||
for (int i = 0; i < 3; ++i)
|
||||
add(std::string("view.look") + char('0' + i), std::string("Appearance: ") + (i == 0 ? "like the desktop" : i == 1 ? "dark" : "light"), "View", "",
|
||||
[i] { keel::execute(std::format("fry_theme {}", i)); }, {}, "theme colours");
|
||||
const char* tabs[] = {"Model", "Paint", "Layout", "Render", "Print"};
|
||||
for (int i = 0; i < 5; ++i)
|
||||
add(std::string("tab.") + tabs[i], std::string("Go to ") + tabs[i], "Tabs", std::format("Ctrl+{}", i + 1), [this, i] { setTab(Tab(i)); }, {}, i == 1 ? "sculpt texture" : "");
|
||||
add("script.panel", "The Script panel", "Script", "", [this] { showScript_ = !showScript_; }, {}, "lua code");
|
||||
add("script.run", "Run the script", "Script", "F5", [this] { runScriptNow(); }, [this] { return currentScript() != nullptr; });
|
||||
add("script.new", "New script", "Script", "", [this] { addScript("Script", kFirstScript); });
|
||||
add("script.plugins", "Plug-ins...", "Script", "", [this] { showPlugins_ = true; }, {}, "addons extensions");
|
||||
add("help.keys", "Keys and mouse (and your own keys)", "Help", "F1", [this] { showHelp_ = true; }, {}, "shortcuts hotkeys keymap bind");
|
||||
add("help.about", "About Fry3DX", "Help", "", [this] { showAbout_ = true; }, {}, "version");
|
||||
add("help.search", "Search for a command", "Help", "F3", [this] { openSearch(); }, {}, "find palette");
|
||||
for (size_t i = 0; i < plugins_.size(); ++i)
|
||||
if (pluginUsable(i))
|
||||
for (size_t c = 0; c < plugins_[i].p->commands.size(); ++c) {
|
||||
const fry::PluginCommand& cmd = plugins_[i].p->commands[c];
|
||||
add("plugin." + plugins_[i].file + "." + cmd.name, cmd.name + (cmd.settings.empty() ? "" : "..."), "Plug-in: " + plugins_[i].p->name, "", [this, i, c] {
|
||||
if (i >= plugins_.size() || !pluginUsable(i) || c >= plugins_[i].p->commands.size()) return;
|
||||
const fry::PluginCommand& cm = plugins_[i].p->commands[c];
|
||||
if (cm.settings.empty()) return runPluginCommand(i, c, {});
|
||||
pendingCommand_ = PendingCommand{i, c, {}};
|
||||
for (const auto& [n, v] : cm.settings) pendingCommand_->values.push_back(v);
|
||||
}, {}, cmd.help);
|
||||
}
|
||||
(void)model;
|
||||
}
|
||||
void openSearch() {
|
||||
buildCommands();
|
||||
search_.show();
|
||||
}
|
||||
void runCommand(const std::string& id) {
|
||||
for (const fry::Command& c : commands_)
|
||||
if (c.id == id) {
|
||||
if (!c.usable()) return say(c.name + ": not now (" + (c.group == "Shape" ? "edit a shape first, Tab" : "pick something first") + ")");
|
||||
std::erase(recentCommands_, id);
|
||||
recentCommands_.insert(recentCommands_.begin(), id);
|
||||
if (recentCommands_.size() > 8) recentCommands_.pop_back();
|
||||
return c.run();
|
||||
}
|
||||
}
|
||||
void commandSearch() {
|
||||
if (!search_.open) return;
|
||||
if (const int i = search_.draw(commands_, recentCommands_); i >= 0) runCommand(commands_[size_t(i)].id);
|
||||
}
|
||||
// The pies: the tools (Space), or the views (Shift+Space), round the mouse while it's held.
|
||||
void pieFrame(ImGuiIO& io) {
|
||||
const bool released = !pieStays_ && !ImGui::IsKeyDown(ImGuiKey_Space);
|
||||
std::vector<fry::PieItem> items;
|
||||
std::vector<std::function<void()>> runs;
|
||||
if (pie_ == 1) {
|
||||
for (const ToolInfo& t : kTools)
|
||||
if (mode_ == Mode::Edit ? t.editMode : t.objectMode)
|
||||
items.push_back({t.name, t.icon, true, tool_ == t.tool}), runs.push_back([this, tool = t.tool] { setTool(tool); });
|
||||
items.push_back({mode_ == Mode::Edit ? "Done (objects)" : "Edit the shape", mode_ == Mode::Edit ? Icon::Objects : Icon::Shape, true, false});
|
||||
runs.push_back([this] { mode_ == Mode::Edit ? leaveEdit() : enterEdit(); });
|
||||
} else {
|
||||
const std::pair<View, const char*> views[] = {{View::Top, "Top"}, {View::Right, "Right"}, {View::Front, "Front"}, {View::Perspective, "3D"},
|
||||
{View::Bottom, "Bottom"}, {View::Back, "Back"}, {View::Left, "Left"}};
|
||||
for (const auto& [v, n] : views) {
|
||||
items.push_back({n, Icon::Select, false, view_ == v});
|
||||
runs.push_back([this, v] { v == View::Perspective ? leaveFlatView() : setView(v); });
|
||||
}
|
||||
items.push_back({"Frame", Icon::Select, false, false}), runs.push_back([this] { frameSelected(); });
|
||||
}
|
||||
const int got = fry::pieMenu(pie_ == 1 ? "Tools" : "Views", pieAt_, items, released);
|
||||
if (got == -1) return;
|
||||
if (got >= 0) runs[size_t(got)]();
|
||||
pie_ = 0, pieStays_ = false;
|
||||
(void)io;
|
||||
}
|
||||
// Keys of your own: kept in keys.txt (a line each: the command's id, a tab, the key).
|
||||
void loadMyKeys() {
|
||||
myKeys_.clear();
|
||||
std::ifstream in(keel::prefPath() + "keys.txt");
|
||||
for (std::string line; std::getline(in, line);)
|
||||
if (const size_t tab = line.find('\t'); tab != std::string::npos)
|
||||
if (const auto c = fry::parseChord(line.substr(tab + 1))) myKeys_[line.substr(0, tab)] = *c;
|
||||
}
|
||||
void saveMyKeys() {
|
||||
std::ofstream out(keel::prefPath() + "keys.txt");
|
||||
for (const auto& [id, c] : myKeys_) out << id << '\t' << fry::chordName(c) << '\n';
|
||||
}
|
||||
|
||||
void menus() {
|
||||
if (ImGui::BeginMenu("File")) {
|
||||
if (ImGui::MenuItem("New", "Ctrl+N")) whenSaved(2);
|
||||
|
|
@ -7209,6 +7466,9 @@ private:
|
|||
}
|
||||
ImGui::EndTable();
|
||||
};
|
||||
rows("Finding things", {{"F3 or Ctrl+Space", "Search for any command by name and run it"}, {"Space (held)", "The tools round the mouse: point at one, let go"},
|
||||
{"Shift+Space (held)", "The views round the mouse"}});
|
||||
myKeysTable();
|
||||
rows("Looking around", {{"Right drag", "Orbit round what you look at"}, {"Middle drag / Shift+Right drag", "Pan"}, {"Wheel", "Zoom (towards the mouse)"},
|
||||
{"Orbit / Pan / Zoom buttons", "Drag them (touchpads)"}, {"F", "Frame the selection"}, {"Home", "Frame everything"},
|
||||
{"Numpad 1 / 3 / 7", "Front / side / top (Shift: the opposite)"}, {"Numpad 5", "Flat view / 3D"},
|
||||
|
|
@ -7237,6 +7497,126 @@ private:
|
|||
ImGui::End();
|
||||
}
|
||||
|
||||
// Keys of your own: any command on any key (they come before the usual ones).
|
||||
void myKeysTable() {
|
||||
static char filter[64] = {};
|
||||
if (!ImGui::CollapsingHeader("Your own keys")) return;
|
||||
if (commands_.empty()) buildCommands();
|
||||
ImGui::TextWrapped("Give any command a key: click Set, then press the key (with Ctrl, Shift or Alt if you like). Your keys come before the usual ones.");
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
ImGui::InputTextWithHint("##kf", "Find a command", filter, sizeof filter);
|
||||
if (!settingKey_.empty()) {
|
||||
ImGui::TextColored(accentText(), "Press the key for it now (Esc: never mind)");
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) settingKey_.clear();
|
||||
else if (const auto c = fry::pressedChord()) {
|
||||
std::erase_if(myKeys_, [&](const auto& kv) { return kv.second == *c; }); // (a key does one thing)
|
||||
myKeys_[settingKey_] = *c;
|
||||
saveMyKeys();
|
||||
settingKey_.clear();
|
||||
}
|
||||
}
|
||||
if (!ImGui::BeginTable("mykeys", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY, ImVec2(0, 260))) return;
|
||||
ImGui::TableSetupColumn("what");
|
||||
ImGui::TableSetupColumn("key", ImGuiTableColumnFlags_WidthFixed, 130);
|
||||
ImGui::TableSetupColumn("do", ImGuiTableColumnFlags_WidthFixed, 96);
|
||||
for (const fry::Command& c : commands_) {
|
||||
if (filter[0] && !fry::commandScore(c, filter)) continue;
|
||||
ImGui::PushID(c.id.c_str());
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(c.name.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
const auto mine = myKeys_.find(c.id);
|
||||
if (mine != myKeys_.end()) ImGui::TextColored(accentText(), "%s", fry::chordName(mine->second).c_str());
|
||||
else ImGui::TextDisabled("%s", c.keys.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::SmallButton(settingKey_ == c.id ? "..." : "Set")) settingKey_ = c.id;
|
||||
if (mine != myKeys_.end()) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Clear")) myKeys_.erase(c.id), saveMyKeys();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
// ── (being made) ──
|
||||
bool showText_ = false, showSpin_ = false, showPrintSize_ = false, liveMirror_ = false;
|
||||
void repairSelected() {}
|
||||
void startSlide() {}
|
||||
|
||||
std::string workspaceDir() const { return keel::prefPath() + "workspaces/"; }
|
||||
void resetLayout() {
|
||||
layoutAgain_ = true, layoutDone_ = false;
|
||||
say("The panels are back where they started");
|
||||
}
|
||||
void workspacesWindow() {
|
||||
ImGui::SetNextWindowSize(ImVec2(360, 0), ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Workspaces", &showWorkspaces_, ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_AlwaysAutoResize)) return ImGui::End();
|
||||
ImGui::TextWrapped("The panels as they are now, kept under a name: one for modelling, one for painting, one for a small screen...");
|
||||
ImGui::SetNextItemWidth(220);
|
||||
ImGui::InputText("##wname", workspaceName_, sizeof workspaceName_);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Keep") && workspaceName_[0]) {
|
||||
std::error_code ec;
|
||||
stdfs::create_directories(workspaceDir(), ec);
|
||||
size_t size = 0;
|
||||
const char* ini = ImGui::SaveIniSettingsToMemory(&size);
|
||||
std::ofstream(workspaceDir() + fileSafe(workspaceName_) + ".ini", std::ios::binary).write(ini, std::streamsize(size));
|
||||
say(std::format("Workspace \"{}\" kept", workspaceName_));
|
||||
}
|
||||
ImGui::SeparatorText("Kept");
|
||||
if (ImGui::Selectable("As it started (the usual)")) resetLayout();
|
||||
std::error_code ec;
|
||||
std::vector<stdfs::path> kept;
|
||||
for (const auto& e : stdfs::directory_iterator(workspaceDir(), ec))
|
||||
if (e.path().extension() == ".ini") kept.push_back(e.path());
|
||||
std::sort(kept.begin(), kept.end());
|
||||
for (const stdfs::path& f : kept) {
|
||||
ImGui::PushID(f.string().c_str());
|
||||
if (ImGui::Selectable(f.stem().string().c_str(), false, ImGuiSelectableFlags_AllowOverlap, ImVec2(250, 0))) {
|
||||
std::ifstream in(f, std::ios::binary);
|
||||
pendingLayout_.assign(std::istreambuf_iterator<char>(in), {});
|
||||
say("Workspace: " + f.stem().string());
|
||||
}
|
||||
ImGui::SameLine(270);
|
||||
if (ImGui::SmallButton("Delete")) stdfs::remove(f, ec);
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
static std::string fileSafe(std::string n) {
|
||||
for (char& c : n)
|
||||
if (std::strchr("\\/:*?\"<>|", c)) c = '_';
|
||||
return n;
|
||||
}
|
||||
|
||||
// The steps that can be undone, oldest first: a click goes back (or on) to just after it.
|
||||
void historyWindow() {
|
||||
ImGui::SetNextWindowSize(ImVec2(340, 420), ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Undo history", &showHistory_)) return ImGui::End();
|
||||
int back = 0, on = 0; // (how many undos / redos the click asks for)
|
||||
if (ImGui::Selectable("The start", undo_.empty())) back = int(undo_.size());
|
||||
for (size_t i = 0; i < undo_.size(); ++i) {
|
||||
const std::string& l = undo_[i].label;
|
||||
ImGui::PushID(int(i));
|
||||
if (ImGui::Selectable(l.empty() ? "A change" : l.c_str(), i + 1 == undo_.size())) back = int(undo_.size() - 1 - i);
|
||||
ImGui::PopID();
|
||||
}
|
||||
for (size_t i = redo_.size(); i-- > 0;) { // (what redo would bring back, greyed)
|
||||
ImGui::PushID(int(10000 + i));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
|
||||
const std::string& l = redo_[i].label;
|
||||
if (ImGui::Selectable(l.empty() ? "A change (undone)" : (l + " (undone)").c_str())) on = int(redo_.size() - i);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (undo_.size() >= 200) ImGui::TextDisabled("(the 200 latest steps are kept)");
|
||||
ImGui::End();
|
||||
for (int i = 0; i < back; ++i) undo();
|
||||
for (int i = 0; i < on; ++i) redo();
|
||||
}
|
||||
|
||||
void aboutWindow() {
|
||||
using keel::ui::Role;
|
||||
const float w = 460.0f;
|
||||
|
|
|
|||
Loading…
Reference in a new issue