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
|
|
@ -9,11 +9,11 @@ add_library(fry_script STATIC script.cpp)
|
|||
target_link_libraries(fry_script PUBLIC fry_core PRIVATE lua)
|
||||
|
||||
if(ANDROID) # SDL's Java side loads the app as libmain.so and calls its (SDL_)main
|
||||
add_library(fry3dx SHARED app.cpp icons.cpp)
|
||||
add_library(fry3dx SHARED app.cpp commands.cpp icons.cpp)
|
||||
set_target_properties(fry3dx PROPERTIES OUTPUT_NAME main LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
|
||||
elseif(APPLE) # a Mac app: Fry3DX.app, content in Contents/Resources, MoltenVK in Contents/Frameworks
|
||||
set(icns ${PROJECT_SOURCE_DIR}/branding/fry3dx.icns)
|
||||
add_executable(fry3dx MACOSX_BUNDLE app.cpp icons.cpp ${icns})
|
||||
add_executable(fry3dx MACOSX_BUNDLE app.cpp commands.cpp icons.cpp ${icns})
|
||||
set_source_files_properties(${icns} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||
set_target_properties(fry3dx PROPERTIES OUTPUT_NAME Fry3DX MACOSX_BUNDLE_INFO_PLIST ${PROJECT_SOURCE_DIR}/cmake/Info.plist.in)
|
||||
# Vulkan on a Mac is MoltenVK (Vulkan on Metal): FRY_MOLTENVK=/path/to/libMoltenVK.dylib, or
|
||||
|
|
@ -28,7 +28,7 @@ elseif(APPLE) # a Mac app: Fry3DX.app, content in Contents/Resources, MoltenVK i
|
|||
message(WARNING "No MoltenVK found (set FRY_MOLTENVK): Fry3DX.app will only have OpenGL 4.1")
|
||||
endif()
|
||||
else()
|
||||
add_executable(fry3dx app.cpp icons.cpp)
|
||||
add_executable(fry3dx app.cpp commands.cpp icons.cpp)
|
||||
endif()
|
||||
if(WIN32) # a window program (no console window), with its icon
|
||||
target_sources(fry3dx PRIVATE fry3dx.rc)
|
||||
|
|
|
|||
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;
|
||||
|
|
|
|||
177
src/commands.cpp
Normal file
177
src/commands.cpp
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
#include "commands.h"
|
||||
|
||||
#include <imgui_internal.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
|
||||
namespace fry {
|
||||
namespace {
|
||||
|
||||
std::string lower(std::string s) {
|
||||
for (char& c : s) c = char(std::tolower(static_cast<unsigned char>(c)));
|
||||
return s;
|
||||
}
|
||||
|
||||
// The keys a chord can be made of (letters, numbers, F keys, the keypad and the rest).
|
||||
bool chordKey(ImGuiKey k) {
|
||||
return (k >= ImGuiKey_Tab && k <= ImGuiKey_KeypadEqual) && !(k >= ImGuiKey_LeftCtrl && k <= ImGuiKey_Menu) && k != ImGuiKey_Escape;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string chordName(const Chord& c) {
|
||||
if (!c) return "";
|
||||
std::string s;
|
||||
if (c.ctrl) s += "Ctrl+";
|
||||
if (c.shift) s += "Shift+";
|
||||
if (c.alt) s += "Alt+";
|
||||
return s + ImGui::GetKeyName(c.key);
|
||||
}
|
||||
|
||||
std::optional<Chord> parseChord(const std::string& text) {
|
||||
Chord c;
|
||||
std::string rest = text;
|
||||
for (;;) {
|
||||
if (rest.starts_with("Ctrl+")) c.ctrl = true, rest.erase(0, 5);
|
||||
else if (rest.starts_with("Shift+")) c.shift = true, rest.erase(0, 6);
|
||||
else if (rest.starts_with("Alt+")) c.alt = true, rest.erase(0, 4);
|
||||
else break;
|
||||
}
|
||||
for (int k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; ++k)
|
||||
if (chordKey(ImGuiKey(k)) && rest == ImGui::GetKeyName(ImGuiKey(k))) {
|
||||
c.key = ImGuiKey(k);
|
||||
return c;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Chord> pressedChord() {
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
for (int k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; ++k)
|
||||
if (chordKey(ImGuiKey(k)) && ImGui::IsKeyPressed(ImGuiKey(k), false)) return Chord{ImGuiKey(k), io.KeyCtrl, io.KeyShift, io.KeyAlt};
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int commandScore(const Command& c, const std::string& query) {
|
||||
const std::string name = lower(c.name), all = name + " " + lower(c.group) + " " + lower(c.words) + " " + lower(c.keys);
|
||||
int score = 1;
|
||||
size_t from = 0;
|
||||
bool any = false;
|
||||
const std::string q = lower(query);
|
||||
while (from < q.size()) {
|
||||
const size_t space = q.find(' ', from);
|
||||
const std::string word = q.substr(from, space == std::string::npos ? std::string::npos : space - from);
|
||||
from = space == std::string::npos ? q.size() : space + 1;
|
||||
if (word.empty()) continue;
|
||||
any = true;
|
||||
const size_t at = all.find(word);
|
||||
if (at == std::string::npos) return 0;
|
||||
const bool wordStart = at == 0 || !std::isalnum(static_cast<unsigned char>(all[at - 1]));
|
||||
score += (at < name.size() ? 20 : 6) + (wordStart ? 10 : 0) + (at == 0 ? 15 : 0);
|
||||
}
|
||||
if (any && name == q) score += 50;
|
||||
return score;
|
||||
}
|
||||
|
||||
int CommandSearch::draw(const std::vector<Command>& all, const std::vector<std::string>& recent) {
|
||||
if (!open) return -1;
|
||||
const ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
const float w = std::min(560.0f, vp->WorkSize.x - 32.0f);
|
||||
ImGui::SetNextWindowPos(ImVec2(vp->WorkPos.x + vp->WorkSize.x * 0.5f, vp->WorkPos.y + vp->WorkSize.y * 0.12f), ImGuiCond_Always, ImVec2(0.5f, 0.0f));
|
||||
ImGui::SetNextWindowSize(ImVec2(w, 0));
|
||||
if (!ImGui::IsPopupOpen("##commands")) ImGui::OpenPopup("##commands");
|
||||
int run = -1;
|
||||
if (!ImGui::BeginPopup("##commands", ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings)) {
|
||||
open = false;
|
||||
return -1;
|
||||
}
|
||||
if (focus) ImGui::SetKeyboardFocusHere(), focus = false;
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
ImGui::InputTextWithHint("##find", "Type what you want to do: round edges, hole, export STL...", text, sizeof text);
|
||||
// What fits, best first (nothing typed: the ones used lately, then the rest A to Z).
|
||||
std::vector<std::pair<int, int>> found; // (score, index)
|
||||
const std::string q = text;
|
||||
for (size_t i = 0; i < all.size(); ++i) {
|
||||
int s = q.empty() ? 1 : commandScore(all[i], q);
|
||||
if (!s) continue;
|
||||
if (const auto r = std::find(recent.begin(), recent.end(), all[i].id); r != recent.end()) s += 40 - int(r - recent.begin()); // (used lately: higher)
|
||||
if (!all[i].usable()) s -= 30;
|
||||
found.push_back({s, int(i)});
|
||||
}
|
||||
std::stable_sort(found.begin(), found.end(), [](const auto& a, const auto& b) { return a.first > b.first; });
|
||||
if (found.size() > 60) found.resize(60);
|
||||
at = std::clamp(at, 0, std::max(0, int(found.size()) - 1));
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) at = std::min(at + 1, int(found.size()) - 1);
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) at = std::max(at - 1, 0);
|
||||
if (ImGui::IsItemEdited()) at = 0;
|
||||
const bool enter = ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter);
|
||||
const float rowH = ImGui::GetTextLineHeightWithSpacing() + 4;
|
||||
ImGui::BeginChild("##list", ImVec2(0, std::min(float(found.size()), 12.0f) * rowH + 6), ImGuiChildFlags_None);
|
||||
for (int n = 0; n < int(found.size()); ++n) {
|
||||
const Command& c = all[size_t(found[size_t(n)].second)];
|
||||
ImGui::PushID(n);
|
||||
ImGui::BeginDisabled(!c.usable());
|
||||
const ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
if (ImGui::Selectable("##c", n == at, ImGuiSelectableFlags_None, ImVec2(0, rowH - 4))) run = found[size_t(n)].second;
|
||||
if (n == at && (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_UpArrow))) ImGui::SetScrollHereY();
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
const float ty = p.y + (rowH - 4 - ImGui::GetFontSize()) * 0.5f;
|
||||
dl->AddText(ImVec2(p.x + 6, ty), ImGui::GetColorU32(ImGuiCol_Text), c.name.c_str());
|
||||
const std::string right = c.keys.empty() ? c.group : c.group + " " + c.keys;
|
||||
const float rw = ImGui::CalcTextSize(right.c_str()).x;
|
||||
dl->AddText(ImVec2(p.x + ImGui::GetContentRegionAvail().x - rw - 6, ty), ImGui::GetColorU32(ImGuiCol_TextDisabled), right.c_str());
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (found.empty()) ImGui::TextDisabled("Nothing called that");
|
||||
ImGui::EndChild();
|
||||
if (enter && !found.empty()) run = found[size_t(at)].second;
|
||||
if (run >= 0 && !all[size_t(run)].usable()) run = -1;
|
||||
if (run >= 0 || ImGui::IsKeyPressed(ImGuiKey_Escape)) open = false;
|
||||
if (!open) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
return run;
|
||||
}
|
||||
|
||||
int pieMenu(const char* title, ImVec2 c, const std::vector<PieItem>& items, bool released) {
|
||||
ImDrawList* dl = ImGui::GetForegroundDrawList();
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
const int n = int(items.size());
|
||||
if (!n) return -2;
|
||||
const float radius = std::max(110.0f, ImGui::GetFontSize() * 8.5f), dead = 26.0f;
|
||||
const ImVec2 d(io.MousePos.x - c.x, io.MousePos.y - c.y);
|
||||
const float far = std::hypot(d.x, d.y);
|
||||
int hot = -1;
|
||||
if (far > dead) { // (the slice the mouse points into; the first at the top, going round clockwise)
|
||||
const float a = std::atan2(d.x, -d.y), slice = 2 * std::numbers::pi_v<float> / float(n);
|
||||
hot = int(std::floor((a + slice * 0.5f + 2 * std::numbers::pi_v<float>) / slice)) % n;
|
||||
}
|
||||
const ImU32 bg = ImGui::GetColorU32(ImGuiCol_PopupBg), text = ImGui::GetColorU32(ImGuiCol_Text), hi = ImGui::GetColorU32(ImGuiCol_ButtonActive),
|
||||
line = ImGui::GetColorU32(ImGuiCol_Border);
|
||||
dl->AddCircleFilled(c, dead, bg, 32);
|
||||
dl->AddCircle(c, dead, line, 32, 1.5f);
|
||||
if (far > dead) dl->AddLine(c, ImVec2(c.x + d.x / far * dead, c.y + d.y / far * dead), hi, 3.0f);
|
||||
const ImVec2 ts = ImGui::CalcTextSize(title);
|
||||
dl->AddText(ImVec2(c.x - ts.x * 0.5f, c.y + dead + 6), ImGui::GetColorU32(ImGuiCol_TextDisabled), title);
|
||||
const float h = ImGui::GetFrameHeight() + 6;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const float a = 2 * std::numbers::pi_v<float> * float(i) / float(n);
|
||||
const ImVec2 at(c.x + std::sin(a) * radius, c.y - std::cos(a) * radius);
|
||||
const float tw = ImGui::CalcTextSize(items[size_t(i)].name.c_str()).x, w = tw + (items[size_t(i)].hasIcon ? h : 0) + 20;
|
||||
const ImVec2 lo(at.x - w * 0.5f, at.y - h * 0.5f), up(at.x + w * 0.5f, at.y + h * 0.5f);
|
||||
const bool hover = i == hot || ImGui::IsMouseHoveringRect(lo, up, false);
|
||||
if (hover) hot = i;
|
||||
dl->AddRectFilled(lo, up, i == hot ? hi : bg, h * 0.5f);
|
||||
dl->AddRect(lo, up, items[size_t(i)].on ? hi : line, h * 0.5f, 0, items[size_t(i)].on ? 2.0f : 1.0f);
|
||||
float x = lo.x + 10;
|
||||
if (items[size_t(i)].hasIcon) drawIcon(dl, items[size_t(i)].icon, ImVec2(x, lo.y + 4), h - 8, text), x += h - 2;
|
||||
dl->AddText(ImVec2(x, at.y - ImGui::GetFontSize() * 0.5f), text, items[size_t(i)].name.c_str());
|
||||
}
|
||||
if (released || ImGui::IsMouseClicked(ImGuiMouseButton_Left)) return hot >= 0 ? hot : -2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
64
src/commands.h
Normal file
64
src/commands.h
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#pragma once
|
||||
// Commands: everything the app can do, by name. Found by typing (the command search, F3 or
|
||||
// Ctrl+Space), put on keys of your own (Keys and mouse), and offered round the mouse (pie menus).
|
||||
#include "icons.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fry {
|
||||
|
||||
struct Command {
|
||||
std::string id; // (stays the same: what keys of your own are kept against)
|
||||
std::string name; // what it's called ("Round edges")
|
||||
std::string group; // where it lives ("Shape", "View")
|
||||
std::string keys; // its own key, as shown ("B")
|
||||
std::function<void()> run;
|
||||
std::function<bool()> ok; // (can it be done now; none: always)
|
||||
std::string words; // more to find it by ("bevel chamfer")
|
||||
bool usable() const { return !ok || ok(); }
|
||||
};
|
||||
|
||||
// A key with what's held with it.
|
||||
struct Chord {
|
||||
ImGuiKey key = ImGuiKey_None;
|
||||
bool ctrl = false, shift = false, alt = false;
|
||||
bool operator==(const Chord&) const = default;
|
||||
explicit operator bool() const { return key != ImGuiKey_None; }
|
||||
};
|
||||
std::string chordName(const Chord& c);
|
||||
std::optional<Chord> parseChord(const std::string& text);
|
||||
// The chord pressed this frame (a key that isn't only Ctrl / Shift / Alt), or none.
|
||||
std::optional<Chord> pressedChord();
|
||||
|
||||
// How well `query` fits a command (0: not at all): every word of it in the name, group or
|
||||
// words; the start of a word, and the name, count most.
|
||||
int commandScore(const Command& c, const std::string& query);
|
||||
|
||||
// The command search: a box to type in and the commands that fit, best first; Enter or a click
|
||||
// runs one. `recent`: ids run lately (first when nothing's typed yet). True while it's open.
|
||||
struct CommandSearch {
|
||||
bool open = false;
|
||||
char text[128] = {};
|
||||
int at = 0;
|
||||
bool focus = false;
|
||||
void show() { open = true, focus = true, text[0] = 0, at = 0; }
|
||||
// Draws it; the command to run (its index), or -1.
|
||||
int draw(const std::vector<Command>& all, const std::vector<std::string>& recent);
|
||||
};
|
||||
|
||||
// A pie menu: the choices round the mouse, picked by moving towards one and letting go of the
|
||||
// key (or clicking). Draws it; the index picked, -1 while choosing, -2 when let go on nothing.
|
||||
struct PieItem {
|
||||
std::string name;
|
||||
Icon icon = Icon::Select;
|
||||
bool hasIcon = true;
|
||||
bool on = false;
|
||||
};
|
||||
int pieMenu(const char* title, ImVec2 centre, const std::vector<PieItem>& items, bool released);
|
||||
|
||||
} // namespace fry
|
||||
Loading…
Reference in a new issue