From c742c937c6a985b7f2b46cf6c1687eea7d3cc8f0 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Wed, 21 Oct 2026 07:37:32 +0200 Subject: [PATCH] Slide (Alt+G): picked points glide along their edges the way the mouse goes, click to keep, Esc to put back Co-Authored-By: Claude Opus 5.5 --- src/app.cpp | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index 9eeb4a7..b6ed04c 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -591,6 +591,10 @@ public: self->spinRise_ = a.size() > 3 ? std::stof(a[3]) / 1000 : 0.0f, self->spinWorld_ = a.size() > 4 && a[4] == "world"; self->spinSelected(); }, "Fry3DX: spin the picked shape round Z: degrees steps [rise mm] [world]"); + static keel::Cmd slideCmd("fry_slide", [](const keel::Args& a) { // (as though the mouse had gone dx, dy since) + self->startSlide(); + if (a.size() > 2) self->slide_.start = ImVec2(ImGui::GetIO().MousePos.x - std::stof(a[1]), ImGui::GetIO().MousePos.y - std::stof(a[2])); + }, "Fry3DX: start sliding the picked points, as though the mouse had moved dx dy"); 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(); @@ -4942,6 +4946,7 @@ private: void viewport(ImGuiIO& io) { cutHover_ = {-1, -1}; + if (slide_.active) return slideFrame(io); if (tab_ == Tab::Paint) return paintViewport(io); if (tab_ == Tab::Layout) return layoutViewport(io); if (tab_ == Tab::Print && printTool_ != PrintTool::None) return printViewport(io); @@ -5131,7 +5136,7 @@ private: // ───────────────────────────── keys ───────────────────────────── void shortcuts(ImGuiIO& io) { - if (io.WantTextInput) return; + if (io.WantTextInput || slide_.active) return; // (a slide has its own keys) 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(); @@ -5222,6 +5227,7 @@ private: } if (alt) { if (key(ImGuiKey_S) && mode_ == Mode::Edit) startPush(); + if (key(ImGuiKey_G) && mode_ == Mode::Edit) return startSlide(); if (key(ImGuiKey_A)) selectNone(); if (key(ImGuiKey_Z)) xray_ = !xray_; if (key(ImGuiKey_H)) @@ -5925,7 +5931,7 @@ private: 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.slide", "Slide the points along their edges...", "Shape", "Alt+G", [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; }); @@ -7860,7 +7866,76 @@ private: for (const std::string& d : did) text += (text.empty() ? "" : ", ") + d; say("Repaired: " + text + (still ? std::format(". {} still not closed (see the Print tab)", still) : ". Ready to print")); } - void startSlide() {} + // Slide (Alt+G): the picked points glide along their edges, each along the edge that goes + // most the way the mouse goes, never past its other end. Click: done. Esc / right click: as it was. + struct Slide { + bool active = false; + uint32_t object = 0; + Mesh from; + std::vector pts; + std::vector> rails; // (each point's neighbours that aren't picked: where it can go) + ImVec2 start{0, 0}; + } slide_; + void startSlide() { + Object* o = editing(); + if (!o) return say("Slide works on a shape being edited (Tab)"); + const std::set pts = selectedPoints(); + if (pts.empty()) return say("Pick points, edges or faces to slide first"); + std::vector> near(o->mesh.verts.size()); + for (const auto& e : adjacencyOf(*o, o->mesh).edges) + if (pts.count(e[0]) != pts.count(e[1])) near[size_t(e[0])].insert(e[1]), near[size_t(e[1])].insert(e[0]); + slide_ = {}; + for (int v : pts) + if (!near[size_t(v)].empty()) slide_.pts.push_back(v), slide_.rails.emplace_back(near[size_t(v)].begin(), near[size_t(v)].end()); + if (slide_.pts.empty()) return say("Nothing for those to slide along (every edge of theirs is picked too)"); + pushUndo(); + slide_.active = true, slide_.object = o->id, slide_.from = o->mesh, slide_.start = ImGui::GetIO().MousePos; + say("Sliding: move the mouse along the edges, click to keep it (Esc: as it was)"); + } + void slideFrame(const ImGuiIO& io) { + Object* o = find(slide_.object); + if (!o || mode_ != Mode::Edit) { + if (o) o->mesh = slide_.from, touched(o->id); + return void(slide_.active = false); + } + if (ImGui::IsKeyPressed(ImGuiKey_Escape) || ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { + o->mesh = slide_.from, touched(o->id), slide_.active = false; + takeBack(); + return say("Slide put back"); + } + const glm::mat4 m = objectMatrix(*o); + const ImVec2 d(io.MousePos.x - slide_.start.x, io.MousePos.y - slide_.start.y); + const float dl = std::hypot(d.x, d.y); + float shown = 0; + o->mesh.verts = slide_.from.verts; + if (dl > 1) { + for (size_t i = 0; i < slide_.pts.size(); ++i) { + const glm::vec3 p = slide_.from.verts[size_t(slide_.pts[i])]; + ImVec2 sp; + if (!project(glm::vec3(m * glm::vec4(p, 1.0f)), sp)) continue; + // (the edge whose way on the screen is most the mouse's, and how far along it the mouse has gone) + float best = 0.2f, t = 0; + int to = -1; + for (int r : slide_.rails[i]) { + ImVec2 sr; + if (!project(glm::vec3(m * glm::vec4(slide_.from.verts[size_t(r)], 1.0f)), sr)) continue; + const ImVec2 e(sr.x - sp.x, sr.y - sp.y); + const float el = std::hypot(e.x, e.y); + if (el < 1) continue; + const float along = (e.x * d.x + e.y * d.y) / el; // (pixels the mouse went that way) + if (const float k = along / dl; k > best) best = k, to = r, t = std::clamp(along / el, 0.0f, 0.98f); + } + if (to >= 0) o->mesh.verts[size_t(slide_.pts[i])] = glm::mix(p, slide_.from.verts[size_t(to)], t), shown = std::max(shown, t); + } + } + touched(o->id); + message_ = std::format("Sliding {:.0f}% of the way along (click: keep, Esc: as it was)", shown * 100), messageAt_ = now(); + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + slide_.active = false; + changedShape(*o, true); + say(std::format("Slid {} point{}", slide_.pts.size(), slide_.pts.size() == 1 ? "" : "s")); + } + } std::string workspaceDir() const { return keel::prefPath() + "workspaces/"; } void resetLayout() {