mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
From Blender and Maya: soft selection in shape editing (Shift+O; near points follow, the wheel while dragging sets how far), push out / pull in along normals (Alt+S), smooth points, bridge two faces with a tunnel, show only the selected (/ or Shift+H); fry_shapeop for scripts and tests; tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3bcd50033d
commit
3f23371eac
4 changed files with 268 additions and 6 deletions
179
src/app.cpp
179
src/app.cpp
|
|
@ -347,8 +347,11 @@ public:
|
||||||
self->enterEdit();
|
self->enterEdit();
|
||||||
if (a.size() > 1) self->elem_ = a[1] == "vertex" ? Elem::Vertex : a[1] == "edge" ? Elem::Edge : Elem::Face;
|
if (a.size() > 1) self->elem_ = a[1] == "vertex" ? Elem::Vertex : a[1] == "edge" ? Elem::Edge : Elem::Face;
|
||||||
if (a.size() > 2 && a[2] == "all") self->selectAllElements();
|
if (a.size() > 2 && a[2] == "all") self->selectAllElements();
|
||||||
if (a.size() > 2 && a[2] != "all") self->selFaces_ = {std::stoi(a[2])};
|
if (a.size() > 2 && a[2] != "all") {
|
||||||
}, "Fry3DX: fry_edit [vertex|edge|face] [all|face number]");
|
self->selFaces_.clear();
|
||||||
|
for (size_t i = 2; i < a.size(); ++i) self->selFaces_.insert(std::stoi(a[i]));
|
||||||
|
}
|
||||||
|
}, "Fry3DX: fry_edit [vertex|edge|face] [all|face numbers...]");
|
||||||
static keel::Cmd tool("fry_tool", [](const keel::Args& a) {
|
static keel::Cmd tool("fry_tool", [](const keel::Args& a) {
|
||||||
for (const ToolInfo& t : kTools)
|
for (const ToolInfo& t : kTools)
|
||||||
if (a.size() > 1 && SDL_strcasecmp(a[1].c_str(), t.name) == 0) self->setTool(t.tool);
|
if (a.size() > 1 && SDL_strcasecmp(a[1].c_str(), t.name) == 0) self->setTool(t.tool);
|
||||||
|
|
@ -549,6 +552,32 @@ public:
|
||||||
static keel::Cmd closeAsk("fry_close", [](const keel::Args&) { if (self->allowQuit()) keel::execute("quit"); }, "Fry3DX: as the window's close button");
|
static keel::Cmd closeAsk("fry_close", [](const keel::Args&) { if (self->allowQuit()) keel::execute("quit"); }, "Fry3DX: as the window's close button");
|
||||||
static keel::Cmd recover("fry_recover", [](const keel::Args&) { self->findAutosaves(), self->showRecover_ = !self->recover_.empty(); }, "Fry3DX: show the autosaves to recover");
|
static keel::Cmd recover("fry_recover", [](const keel::Args&) { self->findAutosaves(), self->showRecover_ = !self->recover_.empty(); }, "Fry3DX: show the autosaves to recover");
|
||||||
static keel::Cmd scriptPanel("fry_scriptpanel", [](const keel::Args& a) { self->showScript_ = a.size() < 2 || a[1] != "0"; }, "Fry3DX: show (1) or hide (0) the Script panel");
|
static keel::Cmd scriptPanel("fry_scriptpanel", [](const keel::Args& a) { self->showScript_ = a.size() < 2 || a[1] != "0"; }, "Fry3DX: show (1) or hide (0) the Script panel");
|
||||||
|
static keel::Cmd shapeOp("fry_shapeop", [](const keel::Args& a) {
|
||||||
|
if (a.size() < 2) return;
|
||||||
|
if (a[1] == "smooth") self->smoothPointsNow();
|
||||||
|
else if (a[1] == "bridge") self->bridgeNow();
|
||||||
|
else if (a[1] == "push" && a.size() > 2) {
|
||||||
|
self->startPush();
|
||||||
|
if (Object* o = self->find(self->pushObject_); o && self->showPush_) {
|
||||||
|
self->pushBy_ = std::stof(a[2]);
|
||||||
|
pushPoints(o->mesh, self->pushPts_, self->pushBy_);
|
||||||
|
self->changedShape(*o, true);
|
||||||
|
self->showPush_ = false;
|
||||||
|
}
|
||||||
|
} else if (a[1] == "move" && a.size() > 4) { // the selection (and with soft selection, what's near) moved, world metres
|
||||||
|
Object* o = self->editing();
|
||||||
|
if (!o) return;
|
||||||
|
self->pushUndo();
|
||||||
|
self->drag_.soft.clear();
|
||||||
|
if (self->softSel_) self->findSoft();
|
||||||
|
const glm::mat4 m = objectMatrix(*o);
|
||||||
|
const glm::mat4 local = glm::inverse(m) * glm::translate(glm::mat4(1.0f), glm::vec3(std::stof(a[2]), std::stof(a[3]), std::stof(a[4]))) * m;
|
||||||
|
for (int v : self->selectedPoints()) o->mesh.verts[size_t(v)] = glm::vec3(local * glm::vec4(o->mesh.verts[size_t(v)], 1.0f));
|
||||||
|
for (const auto& [v, w] : self->drag_.soft) o->mesh.verts[size_t(v)] = glm::mix(o->mesh.verts[size_t(v)], glm::vec3(local * glm::vec4(o->mesh.verts[size_t(v)], 1.0f)), w);
|
||||||
|
self->drag_.soft.clear();
|
||||||
|
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 about("fry_about", [](const keel::Args&) { self->showAbout_ = true; }, "Fry3DX: show the About window");
|
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) {
|
static keel::Cmd plugins("fry_plugins", [](const keel::Args& a) {
|
||||||
if (a.size() > 1 && a[1] == "reload") self->loadPlugins();
|
if (a.size() > 1 && a[1] == "reload") self->loadPlugins();
|
||||||
|
|
@ -629,6 +658,7 @@ public:
|
||||||
commandForm();
|
commandForm();
|
||||||
copiesWindow();
|
copiesWindow();
|
||||||
thickenWindow();
|
thickenWindow();
|
||||||
|
pushWindow();
|
||||||
saveQuestion();
|
saveQuestion();
|
||||||
recoverWindow();
|
recoverWindow();
|
||||||
if (!askSave_ && !showRecover_) shortcuts(io);
|
if (!askSave_ && !showRecover_) shortcuts(io);
|
||||||
|
|
@ -708,7 +738,18 @@ private:
|
||||||
glm::vec3 startHit{0.0f};
|
glm::vec3 startHit{0.0f};
|
||||||
Scene before;
|
Scene before;
|
||||||
std::set<std::pair<int, int>> bevelEdges;
|
std::set<std::pair<int, int>> bevelEdges;
|
||||||
|
std::vector<std::pair<int, float>> soft; // points near the selection that follow it, and how much
|
||||||
} drag_;
|
} drag_;
|
||||||
|
// Soft selection (shape editing): points within `softReach_` of the selection follow it, less
|
||||||
|
// the further they are (Blender's proportional editing, Maya's soft select).
|
||||||
|
bool softSel_ = false;
|
||||||
|
float softReach_ = 0.3f;
|
||||||
|
// Push out / pull in (Alt+S): the points and the shape as it was when it started.
|
||||||
|
bool showPush_ = false;
|
||||||
|
float pushBy_ = 0.0f;
|
||||||
|
std::set<int> pushPts_;
|
||||||
|
Mesh pushFrom_;
|
||||||
|
uint32_t pushObject_ = 0;
|
||||||
bool boxing_ = false, painting_ = false, lassoing_ = false, tapDraw_ = false;
|
bool boxing_ = false, painting_ = false, lassoing_ = false, tapDraw_ = false;
|
||||||
ImVec2 boxStart_{0, 0};
|
ImVec2 boxStart_{0, 0};
|
||||||
std::vector<ImVec2> lasso_;
|
std::vector<ImVec2> lasso_;
|
||||||
|
|
@ -1682,6 +1723,59 @@ private:
|
||||||
mergeToCenter(o->mesh, pts);
|
mergeToCenter(o->mesh, pts);
|
||||||
changedShape(*o);
|
changedShape(*o);
|
||||||
}
|
}
|
||||||
|
void smoothPointsNow() {
|
||||||
|
Object* o = editing();
|
||||||
|
const std::set<int> pts = selectedPoints();
|
||||||
|
if (!o || pts.empty()) return say("Select points (or faces, edges) to smooth");
|
||||||
|
pushUndo();
|
||||||
|
smoothPoints(o->mesh, pts, 0.5f, 2);
|
||||||
|
changedShape(*o, true);
|
||||||
|
say("Smoothed (again for more)");
|
||||||
|
}
|
||||||
|
void startPush() {
|
||||||
|
Object* o = editing();
|
||||||
|
if (!o || selectedPoints().empty()) return say("Select points (or faces) to push out or pull in");
|
||||||
|
pushUndo();
|
||||||
|
pushPts_ = selectedPoints(), pushFrom_ = o->mesh, pushObject_ = o->id, pushBy_ = 0.0f, showPush_ = true;
|
||||||
|
}
|
||||||
|
void pushWindow() {
|
||||||
|
if (!showPush_) return;
|
||||||
|
Object* o = find(pushObject_);
|
||||||
|
if (!o || mode_ != Mode::Edit) return void(showPush_ = false);
|
||||||
|
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||||
|
bool open = true;
|
||||||
|
if (!ImGui::Begin("Push out, pull in", &open, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking)) return ImGui::End();
|
||||||
|
const float w = 260;
|
||||||
|
float mm = pushBy_ * 1000;
|
||||||
|
ImGui::SetNextItemWidth(w);
|
||||||
|
if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere();
|
||||||
|
if (ImGui::DragFloat("##by", &mm, 0.5f, -1e5f, 1e5f, "%.1f mm (- pulls in)")) {
|
||||||
|
pushBy_ = mm / 1000;
|
||||||
|
o->mesh = pushFrom_;
|
||||||
|
pushPoints(o->mesh, pushPts_, pushBy_);
|
||||||
|
changedShape(*o, true);
|
||||||
|
}
|
||||||
|
ImGui::TextWrapped("Each point along the surface's normal there: fatter or thinner, the same shape.");
|
||||||
|
pushAccent();
|
||||||
|
const bool done = ImGui::Button("Done", ImVec2(w, 0)) || ImGui::IsKeyPressed(ImGuiKey_Enter, false);
|
||||||
|
popAccent();
|
||||||
|
ImGui::End();
|
||||||
|
if (done) showPush_ = false;
|
||||||
|
if (!open) o->mesh = pushFrom_, changedShape(*o, true), showPush_ = false; // (closed: as it was)
|
||||||
|
}
|
||||||
|
void bridgeNow() {
|
||||||
|
Object* o = editing();
|
||||||
|
if (!o || selFaces_.size() != 2) return say("Select two faces to bridge (Faces, 3; Join two objects first if they're apart)");
|
||||||
|
const int a = *selFaces_.begin(), b = *selFaces_.rbegin();
|
||||||
|
if (o->mesh.faces[size_t(a)].size() != o->mesh.faces[size_t(b)].size())
|
||||||
|
return say(std::format("Those faces have {} and {} corners: bridging needs as many on each", o->mesh.faces[size_t(a)].size(), o->mesh.faces[size_t(b)].size()));
|
||||||
|
pushUndo();
|
||||||
|
const std::vector<int> made = bridgeFaces(o->mesh, a, b);
|
||||||
|
if (made.empty()) return undo(), redo_.clear(), say("Couldn't bridge those (they share a point)");
|
||||||
|
changedShape(*o);
|
||||||
|
selFaces_.insert(made.begin(), made.end());
|
||||||
|
say(std::format("Bridged: {} faces between them", made.size()));
|
||||||
|
}
|
||||||
void flipNow() {
|
void flipNow() {
|
||||||
Object* o = editing();
|
Object* o = editing();
|
||||||
if (!o || selFaces_.empty()) return say("Select faces to flip");
|
if (!o || selFaces_.empty()) return say("Select faces to flip");
|
||||||
|
|
@ -3662,7 +3756,11 @@ private:
|
||||||
const ImVec2 d = io.MouseDelta;
|
const ImVec2 d = io.MouseDelta;
|
||||||
if (orbiting_ && (d.x != 0 || d.y != 0)) orbitBy(d);
|
if (orbiting_ && (d.x != 0 || d.y != 0)) orbitBy(d);
|
||||||
if (panning_ && (d.x != 0 || d.y != 0)) panBy(d);
|
if (panning_ && (d.x != 0 || d.y != 0)) panBy(d);
|
||||||
if (inView && io.MouseWheel != 0) {
|
if (inView && io.MouseWheel != 0 && drag_.active && softSel_ && mode_ == Mode::Edit) { // the soft selection's reach, not zoom
|
||||||
|
softReach_ = std::clamp(softReach_ * std::pow(1.15f, io.MouseWheel), 0.001f, 1000.0f);
|
||||||
|
findSoft();
|
||||||
|
message_ = std::format("Soft selection reaches {:.3f} m (wheel)", softReach_), messageAt_ = now();
|
||||||
|
} else if (inView && io.MouseWheel != 0) {
|
||||||
// Towards what's under the mouse: the target slides that way a little.
|
// Towards what's under the mouse: the target slides that way a little.
|
||||||
const float k = std::pow(0.85f, io.MouseWheel);
|
const float k = std::pow(0.85f, io.MouseWheel);
|
||||||
const Ray ray = mouseRay(io.MousePos);
|
const Ray ray = mouseRay(io.MousePos);
|
||||||
|
|
@ -4110,6 +4208,7 @@ private:
|
||||||
drag_.startMouse = io.MousePos;
|
drag_.startMouse = io.MousePos;
|
||||||
drag_.before = scene_;
|
drag_.before = scene_;
|
||||||
pushUndo();
|
pushUndo();
|
||||||
|
if (softSel_ && (tool_ == Tool::Move || tool_ == Tool::Rotate || tool_ == Tool::Scale)) findSoft();
|
||||||
const Ray ray = mouseRay(io.MousePos);
|
const Ray ray = mouseRay(io.MousePos);
|
||||||
if (tool_ == Tool::Extrude || tool_ == Tool::Inset) {
|
if (tool_ == Tool::Extrude || tool_ == Tool::Inset) {
|
||||||
glm::vec3 at, n;
|
glm::vec3 at, n;
|
||||||
|
|
@ -4149,6 +4248,36 @@ private:
|
||||||
if (handle < 3) drag_.startT = closestOnAxis(ray, drag_.pivot, a[size_t(handle)]);
|
if (handle < 3) drag_.startT = closestOnAxis(ray, drag_.pivot, a[size_t(handle)]);
|
||||||
if (auto hit = hitPlane(ray, drag_.pivot, -viewDir())) drag_.startHit = *hit;
|
if (auto hit = hitPlane(ray, drag_.pivot, -viewDir())) drag_.startHit = *hit;
|
||||||
}
|
}
|
||||||
|
// 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() {
|
||||||
|
drag_.soft.clear();
|
||||||
|
const Object* o = editing();
|
||||||
|
if (!o) return;
|
||||||
|
const std::set<int> pts = selectedPoints();
|
||||||
|
if (pts.empty()) return;
|
||||||
|
const glm::mat4 m = objectMatrix(*o);
|
||||||
|
const float r = std::max(softReach_, 1e-5f);
|
||||||
|
auto cell = [&](const glm::vec3& p) { return glm::ivec3(glm::floor(p / r)); };
|
||||||
|
auto keyOf = [](glm::ivec3 c) { return (int64_t(c.x & 0x1FFFFF) << 42) | (int64_t(c.y & 0x1FFFFF) << 21) | int64_t(c.z & 0x1FFFFF); };
|
||||||
|
std::unordered_map<int64_t, std::vector<glm::vec3>> grid;
|
||||||
|
for (int v : pts) {
|
||||||
|
const glm::vec3 w(m * glm::vec4(o->mesh.verts[size_t(v)], 1.0f));
|
||||||
|
grid[keyOf(cell(w))].push_back(w);
|
||||||
|
}
|
||||||
|
for (size_t v = 0; v < o->mesh.verts.size(); ++v) {
|
||||||
|
if (pts.count(int(v))) continue;
|
||||||
|
const glm::vec3 w(m * glm::vec4(o->mesh.verts[v], 1.0f));
|
||||||
|
const glm::ivec3 c = cell(w);
|
||||||
|
float best = r;
|
||||||
|
for (int dx = -1; dx <= 1; ++dx)
|
||||||
|
for (int dy = -1; dy <= 1; ++dy)
|
||||||
|
for (int dz = -1; dz <= 1; ++dz)
|
||||||
|
if (auto it = grid.find(keyOf(c + glm::ivec3(dx, dy, dz))); it != grid.end())
|
||||||
|
for (const glm::vec3& q : it->second) best = std::min(best, glm::distance(w, q));
|
||||||
|
if (best < r) drag_.soft.push_back({int(v), sculptFalloff(best, r)});
|
||||||
|
}
|
||||||
|
}
|
||||||
void cancelDrag() {
|
void cancelDrag() {
|
||||||
if (!drag_.active) return;
|
if (!drag_.active) return;
|
||||||
scene_ = drag_.before;
|
scene_ = drag_.before;
|
||||||
|
|
@ -4249,6 +4378,10 @@ private:
|
||||||
if (Object* o = editing()) {
|
if (Object* o = editing()) {
|
||||||
const glm::mat4 m = objectMatrix(*o), local = glm::inverse(m) * T * m;
|
const glm::mat4 m = objectMatrix(*o), local = glm::inverse(m) * T * m;
|
||||||
for (int v : selectedPoints()) o->mesh.verts[size_t(v)] = glm::vec3(local * glm::vec4(o->mesh.verts[size_t(v)], 1.0f));
|
for (int v : selectedPoints()) o->mesh.verts[size_t(v)] = glm::vec3(local * glm::vec4(o->mesh.verts[size_t(v)], 1.0f));
|
||||||
|
for (const auto& [v, w] : drag_.soft) { // (part of the way, by how near)
|
||||||
|
glm::vec3& p = o->mesh.verts[size_t(v)];
|
||||||
|
p = glm::mix(p, glm::vec3(local * glm::vec4(p, 1.0f)), w);
|
||||||
|
}
|
||||||
handMade(*o);
|
handMade(*o);
|
||||||
touched(o->id);
|
touched(o->id);
|
||||||
return;
|
return;
|
||||||
|
|
@ -4628,6 +4761,7 @@ private:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (alt) {
|
if (alt) {
|
||||||
|
if (key(ImGuiKey_S) && mode_ == Mode::Edit) startPush();
|
||||||
if (key(ImGuiKey_A)) selectNone();
|
if (key(ImGuiKey_A)) selectNone();
|
||||||
if (key(ImGuiKey_Z)) xray_ = !xray_;
|
if (key(ImGuiKey_Z)) xray_ = !xray_;
|
||||||
if (key(ImGuiKey_H))
|
if (key(ImGuiKey_H))
|
||||||
|
|
@ -4646,7 +4780,7 @@ private:
|
||||||
if (key(ImGuiKey_B)) setTool(Tool::Bevel);
|
if (key(ImGuiKey_B)) setTool(Tool::Bevel);
|
||||||
if (key(ImGuiKey_C)) setTool(Tool::LoopCut);
|
if (key(ImGuiKey_C)) setTool(Tool::LoopCut);
|
||||||
if (key(ImGuiKey_K)) setTool(Tool::Knife);
|
if (key(ImGuiKey_K)) setTool(Tool::Knife);
|
||||||
if (key(ImGuiKey_O)) setTool(Tool::Hole);
|
if (key(ImGuiKey_O) && !shift) setTool(Tool::Hole);
|
||||||
if (key(ImGuiKey_Tab)) mode_ == Mode::Edit ? leaveEdit() : enterEdit();
|
if (key(ImGuiKey_Tab)) mode_ == Mode::Edit ? leaveEdit() : enterEdit();
|
||||||
if (key(ImGuiKey_Delete) || key(ImGuiKey_Backspace)) deleteSelected();
|
if (key(ImGuiKey_Delete) || key(ImGuiKey_Backspace)) deleteSelected();
|
||||||
if (key(ImGuiKey_F)) frameSelected();
|
if (key(ImGuiKey_F)) frameSelected();
|
||||||
|
|
@ -4668,9 +4802,12 @@ private:
|
||||||
if (key(ImGuiKey_2)) elem_ = Elem::Edge, clearElements();
|
if (key(ImGuiKey_2)) elem_ = Elem::Edge, clearElements();
|
||||||
if (key(ImGuiKey_3)) elem_ = Elem::Face, clearElements();
|
if (key(ImGuiKey_3)) elem_ = Elem::Face, clearElements();
|
||||||
}
|
}
|
||||||
if (key(ImGuiKey_H))
|
if (key(ImGuiKey_H) && shift && !selected_.empty()) showOnly(selected_); // (the rest hidden)
|
||||||
|
else if (key(ImGuiKey_H))
|
||||||
for (Object& o : scene_.objects)
|
for (Object& o : scene_.objects)
|
||||||
if (selected_.count(o.id) && mode_ == Mode::Object) o.visible = false;
|
if (selected_.count(o.id) && mode_ == Mode::Object) o.visible = false;
|
||||||
|
if ((key(ImGuiKey_Slash) || key(ImGuiKey_KeypadDivide)) && mode_ == Mode::Object && !selected_.empty()) showOnly(selected_);
|
||||||
|
if (key(ImGuiKey_O) && shift) softSel_ = !softSel_, say(softSel_ ? "Soft selection on: points near follow (the wheel while dragging: how far)" : "Soft selection off");
|
||||||
if (key(ImGuiKey_Keypad1)) setView(shift ? View::Back : View::Front);
|
if (key(ImGuiKey_Keypad1)) setView(shift ? View::Back : View::Front);
|
||||||
if (key(ImGuiKey_Keypad3)) setView(shift ? View::Right : View::Left);
|
if (key(ImGuiKey_Keypad3)) setView(shift ? View::Right : View::Left);
|
||||||
if (key(ImGuiKey_Keypad7)) setView(shift ? View::Bottom : View::Top);
|
if (key(ImGuiKey_Keypad7)) setView(shift ? View::Bottom : View::Top);
|
||||||
|
|
@ -5062,6 +5199,11 @@ private:
|
||||||
|
|
||||||
void gizmo(ImDrawList* dl, const ImGuiIO& io) {
|
void gizmo(ImDrawList* dl, const ImGuiIO& io) {
|
||||||
const int hot = mouseInView(io) || drag_.active ? hoverHandle(io.MousePos) : -1;
|
const int hot = mouseInView(io) || drag_.active ? hoverHandle(io.MousePos) : -1;
|
||||||
|
if (softSel_ && mode_ == Mode::Edit && (tool_ == Tool::Move || tool_ == Tool::Rotate || tool_ == Tool::Scale) && !selectedPoints().empty()) {
|
||||||
|
const glm::vec3 at = drag_.active ? drag_.pivot : pivotOfSelection();
|
||||||
|
ImVec2 c;
|
||||||
|
if (project(at, c)) dl->AddCircle(c, softReach_ / std::max(pixelSize(at), 1e-9f), keel::ui::accent(0.8f), 64, 1.5f);
|
||||||
|
}
|
||||||
if (tool_ == Tool::Extrude || tool_ == Tool::Inset) {
|
if (tool_ == Tool::Extrude || tool_ == Tool::Inset) {
|
||||||
glm::vec3 at, n;
|
glm::vec3 at, n;
|
||||||
if (!faceHandle(at, n)) return;
|
if (!faceHandle(at, n)) return;
|
||||||
|
|
@ -5290,6 +5432,11 @@ private:
|
||||||
if (ImGui::MenuItem("Merge points", "M")) mergeNow();
|
if (ImGui::MenuItem("Merge points", "M")) mergeNow();
|
||||||
if (ImGui::MenuItem("Make a face")) makeFaceNow();
|
if (ImGui::MenuItem("Make a face")) makeFaceNow();
|
||||||
if (ImGui::MenuItem("Flip faces")) flipNow();
|
if (ImGui::MenuItem("Flip faces")) flipNow();
|
||||||
|
ImGui::Separator();
|
||||||
|
if (ImGui::MenuItem("Smooth points")) smoothPointsNow();
|
||||||
|
if (ImGui::MenuItem("Push out / pull in...", "Alt+S")) startPush();
|
||||||
|
if (ImGui::MenuItem("Bridge two faces")) bridgeNow();
|
||||||
|
ImGui::MenuItem("Soft selection", "Shift+O", &softSel_);
|
||||||
if (ImGui::MenuItem("Make into its own object", "P")) separateSelected();
|
if (ImGui::MenuItem("Make into its own object", "P")) separateSelected();
|
||||||
if (ImGui::MenuItem("Delete", "Del")) deleteElements();
|
if (ImGui::MenuItem("Delete", "Del")) deleteElements();
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
|
|
@ -5319,6 +5466,7 @@ private:
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
if (ImGui::MenuItem("Frame selected", "F")) frameSelected();
|
if (ImGui::MenuItem("Frame selected", "F")) frameSelected();
|
||||||
if (ImGui::MenuItem("Frame everything", "Home")) frameAll();
|
if (ImGui::MenuItem("Frame everything", "Home")) frameAll();
|
||||||
|
if (ImGui::MenuItem("Show only the selected (again: all)", "/", false, !selected_.empty())) showOnly(selected_);
|
||||||
ImGui::EndMenu();
|
ImGui::EndMenu();
|
||||||
}
|
}
|
||||||
if (ImGui::BeginMenu("Script")) {
|
if (ImGui::BeginMenu("Script")) {
|
||||||
|
|
@ -5652,6 +5800,15 @@ private:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void softChoice(float w) {
|
||||||
|
if (mode_ != Mode::Edit) return;
|
||||||
|
ImGui::Checkbox("Soft selection (Shift+O)", &softSel_);
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Points near the selection follow it, less the further they are");
|
||||||
|
if (!softSel_) return;
|
||||||
|
ImGui::SetNextItemWidth(w);
|
||||||
|
ImGui::DragFloat("##reach", &softReach_, 0.002f, 0.001f, 1000.0f, "reaches %.3f m");
|
||||||
|
ImGui::TextDisabled("(the wheel while dragging too)");
|
||||||
|
}
|
||||||
void toolOptions() {
|
void toolOptions() {
|
||||||
ImGui::Begin("Tool options", nullptr, ImGuiWindowFlags_NoCollapse);
|
ImGui::Begin("Tool options", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||||
const float w = ImGui::GetContentRegionAvail().x;
|
const float w = ImGui::GetContentRegionAvail().x;
|
||||||
|
|
@ -5708,6 +5865,7 @@ private:
|
||||||
case Tool::Move:
|
case Tool::Move:
|
||||||
orientChoice();
|
orientChoice();
|
||||||
snapChoice(false);
|
snapChoice(false);
|
||||||
|
softChoice(w);
|
||||||
break;
|
break;
|
||||||
case Tool::Rotate: {
|
case Tool::Rotate: {
|
||||||
orientChoice();
|
orientChoice();
|
||||||
|
|
@ -5716,6 +5874,7 @@ private:
|
||||||
ImGui::SetNextItemWidth(w);
|
ImGui::SetNextItemWidth(w);
|
||||||
if (ImGui::Combo("##rpivot", &p, "The middle\0The origin\0")) rotatePivot_ = p ? Pivot::Origin : Pivot::Middle;
|
if (ImGui::Combo("##rpivot", &p, "The middle\0The origin\0")) rotatePivot_ = p ? Pivot::Origin : Pivot::Middle;
|
||||||
snapChoice(true);
|
snapChoice(true);
|
||||||
|
softChoice(w);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Tool::Scale: {
|
case Tool::Scale: {
|
||||||
|
|
@ -5728,6 +5887,7 @@ private:
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("The other side: drag a side, the opposite side stays (like stretching in a paint program).\nThe middle: both sides move.\nThe bottom: it stays standing on the ground.");
|
ImGui::SetTooltip("The other side: drag a side, the opposite side stays (like stretching in a paint program).\nThe middle: both sides move.\nThe bottom: it stays standing on the ground.");
|
||||||
snapChoice(false);
|
snapChoice(false);
|
||||||
|
softChoice(w);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Tool::Draw:
|
case Tool::Draw:
|
||||||
|
|
@ -6394,6 +6554,8 @@ private:
|
||||||
if (ImGui::Button("Flip", ImVec2((w - 6) / 2, 0))) flipNow();
|
if (ImGui::Button("Flip", ImVec2((w - 6) / 2, 0))) flipNow();
|
||||||
ImGui::SameLine(0, 6);
|
ImGui::SameLine(0, 6);
|
||||||
if (ImGui::Button("Make face", ImVec2(-1, 0))) makeFaceNow();
|
if (ImGui::Button("Make face", ImVec2(-1, 0))) makeFaceNow();
|
||||||
|
if (ImGui::Button("Bridge two faces", ImVec2(w, 0))) bridgeNow();
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Two faces with as many corners: taken out and joined by a tunnel");
|
||||||
if (ImGui::Button("Make into its own object (P)", ImVec2(w, 0))) separateSelected();
|
if (ImGui::Button("Make into its own object (P)", ImVec2(w, 0))) separateSelected();
|
||||||
ImGui::SeparatorText("Edges");
|
ImGui::SeparatorText("Edges");
|
||||||
ImGui::SetNextItemWidth(w * 0.45f);
|
ImGui::SetNextItemWidth(w * 0.45f);
|
||||||
|
|
@ -6406,6 +6568,10 @@ private:
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Round the selected edges (or the edges round the selected faces): width, steps");
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Round the selected edges (or the edges round the selected faces): width, steps");
|
||||||
ImGui::SeparatorText("Points");
|
ImGui::SeparatorText("Points");
|
||||||
if (ImGui::Button("Merge to middle (M)", ImVec2(w, 0))) mergeNow();
|
if (ImGui::Button("Merge to middle (M)", ImVec2(w, 0))) mergeNow();
|
||||||
|
if (ImGui::Button("Smooth", ImVec2((w - 6) / 2, 0))) smoothPointsNow();
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Even out bumps: each point towards its neighbours");
|
||||||
|
ImGui::SameLine(0, 6);
|
||||||
|
if (ImGui::Button("Push / pull (Alt+S)", ImVec2(-1, 0))) startPush();
|
||||||
ImGui::SetNextItemWidth(w * 0.45f);
|
ImGui::SetNextItemWidth(w * 0.45f);
|
||||||
ImGui::DragFloat("##md", &mergeDistance_, 0.0001f, 0.0f, 1.0f, "%.4f m");
|
ImGui::DragFloat("##md", &mergeDistance_, 0.0001f, 0.0f, 1.0f, "%.4f m");
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
@ -6917,7 +7083,8 @@ private:
|
||||||
rows("Objects and shape", {{"Tab / double-click", "Edit an object's shape"}, {"1 / 2 / 3", "Points / edges / faces (shape editing)"}, {"M", "Merge points"},
|
rows("Objects and shape", {{"Tab / double-click", "Edit an object's shape"}, {"1 / 2 / 3", "Points / edges / faces (shape editing)"}, {"M", "Merge points"},
|
||||||
{"P", "Selected faces into their own object"}, {"Shift + Scale drag", "Scale both sides (from the middle)"}, {"Ctrl+G", "Group into a folder"},
|
{"P", "Selected faces into their own object"}, {"Shift + Scale drag", "Scale both sides (from the middle)"}, {"Ctrl+G", "Group into a folder"},
|
||||||
{"Ctrl+Shift + / -", "Add together / take the others out (last picked kept)"}, {"Del", "Delete"}, {"Ctrl+D", "Duplicate"}, {"Ctrl+J", "Join objects into one"},
|
{"Ctrl+Shift + / -", "Add together / take the others out (last picked kept)"}, {"Del", "Delete"}, {"Ctrl+D", "Duplicate"}, {"Ctrl+J", "Join objects into one"},
|
||||||
{"H / Alt+H", "Hide / show all"}});
|
{"H / Alt+H", "Hide / show all"}, {"/ or Shift+H", "Show only the selected (again: all)"},
|
||||||
|
{"Shift+O", "Soft selection: near points follow (wheel while dragging: how far)"}, {"Alt+S", "Push points out / pull in (shape)"}});
|
||||||
rows("Files", {{"Ctrl+N / O / S", "New / open / save"}, {"Ctrl+Shift+S", "Save as"}, {"Ctrl+E", "Export (GLB, glTF, FBX, OBJ, 3MF, STL, PLY)"}, {"F12", "Save a picture of the view"},
|
rows("Files", {{"Ctrl+N / O / S", "New / open / save"}, {"Ctrl+Shift+S", "Save as"}, {"Ctrl+E", "Export (GLB, glTF, FBX, OBJ, 3MF, STL, PLY)"}, {"F12", "Save a picture of the view"},
|
||||||
{"Ctrl+Z / Ctrl+Y", "Undo / redo"}});
|
{"Ctrl+Z / Ctrl+Y", "Undo / redo"}});
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
|
|
|
||||||
11
src/mesh.h
11
src/mesh.h
|
|
@ -131,6 +131,17 @@ std::set<int> linkedVerts(const Mesh& m, const std::set<int>& verts);
|
||||||
// The points next to these (by an edge), and these.
|
// The points next to these (by an edge), and these.
|
||||||
std::set<int> growVerts(const Mesh& m, const std::set<int>& verts);
|
std::set<int> growVerts(const Mesh& m, const std::set<int>& verts);
|
||||||
|
|
||||||
|
// Points moved towards their neighbours' middle (`amount` 0..1, a few times for more): bumps
|
||||||
|
// evened out (Blender's Smooth Vertices, Maya's Average Vertices).
|
||||||
|
void smoothPoints(Mesh& m, const std::set<int>& verts, float amount = 0.5f, int times = 1);
|
||||||
|
// Points pushed out along the surface's normal there (negative: pulled in): fatter or thinner,
|
||||||
|
// keeping the shape (Blender's Shrink/Fatten).
|
||||||
|
void pushPoints(Mesh& m, const std::set<int>& verts, float distance);
|
||||||
|
// Two faces with as many corners each taken out and joined by a tunnel of faces between their
|
||||||
|
// edges, each corner to the nearest one round (Bridge, as in Blender and Maya). The new faces;
|
||||||
|
// empty (and nothing changed) if they aren't two faces with as many corners.
|
||||||
|
std::vector<int> bridgeFaces(Mesh& m, int a, int b);
|
||||||
|
|
||||||
// ── Whole shapes ──
|
// ── Whole shapes ──
|
||||||
|
|
||||||
// Faces next to each other lying flat together (normals within `angle` degrees) joined into
|
// Faces next to each other lying flat together (normals within `angle` degrees) joined into
|
||||||
|
|
|
||||||
|
|
@ -488,4 +488,57 @@ std::set<std::pair<int, int>> edgeRing(const Mesh& m, std::pair<int, int> edge)
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void smoothPoints(Mesh& m, const std::set<int>& verts, float amount, int times) {
|
||||||
|
std::vector<std::vector<int>> next(m.verts.size());
|
||||||
|
for (const auto& [a, b] : edges(m)) next[size_t(a)].push_back(b), next[size_t(b)].push_back(a);
|
||||||
|
for (int t = 0; t < times; ++t) {
|
||||||
|
std::vector<std::pair<int, glm::vec3>> moved; // (from where they all were before this pass)
|
||||||
|
for (int v : verts) {
|
||||||
|
if (v < 0 || size_t(v) >= m.verts.size() || next[size_t(v)].empty()) continue;
|
||||||
|
glm::vec3 mid(0.0f);
|
||||||
|
for (int n : next[size_t(v)]) mid += m.verts[size_t(n)];
|
||||||
|
mid /= float(next[size_t(v)].size());
|
||||||
|
moved.push_back({v, glm::mix(m.verts[size_t(v)], mid, std::clamp(amount, 0.0f, 1.0f))});
|
||||||
|
}
|
||||||
|
for (const auto& [v, p] : moved) m.verts[size_t(v)] = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void pushPoints(Mesh& m, const std::set<int>& verts, float distance) {
|
||||||
|
std::vector<glm::vec3> normal(m.verts.size(), glm::vec3(0.0f));
|
||||||
|
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||||
|
const glm::vec3 n = faceNormal(m, int(f)) * faceArea(m, int(f));
|
||||||
|
for (int v : m.faces[f]) normal[size_t(v)] += n;
|
||||||
|
}
|
||||||
|
for (int v : verts)
|
||||||
|
if (v >= 0 && size_t(v) < m.verts.size() && glm::length(normal[size_t(v)]) > 1e-20f) m.verts[size_t(v)] += glm::normalize(normal[size_t(v)]) * distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> bridgeFaces(Mesh& m, int a, int b) {
|
||||||
|
if (a == b || a < 0 || b < 0 || size_t(a) >= m.faces.size() || size_t(b) >= m.faces.size()) return {};
|
||||||
|
const std::vector<int> A = m.faces[size_t(a)];
|
||||||
|
std::vector<int> B = m.faces[size_t(b)];
|
||||||
|
const size_t n = A.size();
|
||||||
|
if (B.size() != n || n < 3) return {};
|
||||||
|
for (int v : A)
|
||||||
|
if (std::find(B.begin(), B.end(), v) != B.end()) return {}; // (sharing a point: nothing to bridge)
|
||||||
|
// B the other way round (seen down the tunnel, the same way as A), turned to fit A best.
|
||||||
|
std::reverse(B.begin(), B.end());
|
||||||
|
size_t shift = 0;
|
||||||
|
float best = INFINITY;
|
||||||
|
for (size_t s = 0; s < n; ++s) {
|
||||||
|
float d = 0.0f;
|
||||||
|
for (size_t i = 0; i < n; ++i) d += glm::distance(m.verts[size_t(A[i])], m.verts[size_t(B[(i + s) % n])]);
|
||||||
|
if (d < best) best = d, shift = s;
|
||||||
|
}
|
||||||
|
m.faces.erase(m.faces.begin() + std::max(a, b)), m.faces.erase(m.faces.begin() + std::min(a, b));
|
||||||
|
std::vector<int> made;
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
// Along A's edge the way A went (it takes A's place there; A's neighbour runs it the other way).
|
||||||
|
made.push_back(int(m.faces.size()));
|
||||||
|
m.faces.push_back({A[i], A[(i + 1) % n], B[(i + 1 + shift) % n], B[(i + shift) % n]});
|
||||||
|
}
|
||||||
|
return made;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace fry
|
} // namespace fry
|
||||||
|
|
|
||||||
|
|
@ -433,6 +433,37 @@ int main() {
|
||||||
const std::string text((std::istreambuf_iterator<char>(j)), {});
|
const std::string text((std::istreambuf_iterator<char>(j)), {});
|
||||||
CHECK(text.find("\"translation\":[2,3,1]") != std::string::npos, "in glTF's axes (Y up): the position is [2,3,1]");
|
CHECK(text.find("\"translation\":[2,3,1]") != std::string::npos, "in glTF's axes (Y up): the position is [2,3,1]");
|
||||||
|
|
||||||
|
{ // Bridge: two boxes' facing sides joined by a tunnel; one closed shape, every face outwards.
|
||||||
|
Mesh two = makeCube();
|
||||||
|
append(two, makeCube(), glm::translate(glm::mat4(1.0f), glm::vec3(3, 0.2f, 0)));
|
||||||
|
int fa = -1, fb = -1;
|
||||||
|
for (size_t f = 0; f < two.faces.size(); ++f) {
|
||||||
|
const glm::vec3 n = faceNormal(two, int(f)), c = faceCenter(two, int(f));
|
||||||
|
if (n.x > 0.9f && c.x < 1.0f) fa = int(f);
|
||||||
|
if (n.x < -0.9f && c.x > 2.0f) fb = int(f);
|
||||||
|
}
|
||||||
|
const std::vector<int> made = bridgeFaces(two, fa, fb);
|
||||||
|
const PrintCheck c = checkForPrint(two);
|
||||||
|
CHECK(made.size() == 4 && c.closed() && c.volume > 2.0f + 2.0f * 0.95f && c.pieces == 1, "bridge: one closed shape (volume %.2f, pieces %d, open %d crowded %d flipped %d)", c.volume, c.pieces, c.openEdges, c.crowdedEdges, c.flippedEdges);
|
||||||
|
Mesh tri = makeCube();
|
||||||
|
append(tri, makeCylinder(3, 0.5f, 1.0f), glm::translate(glm::mat4(1.0f), glm::vec3(3, 0, 0)));
|
||||||
|
CHECK(bridgeFaces(tri, 0, int(tri.faces.size()) - 1).empty() && tri.faces.size() == 6 + 5, "bridge: not between a 4 and a 3 cornered face");
|
||||||
|
}
|
||||||
|
{ // Smoothing points evens a bump; pushing a cube's points out makes it bigger all round.
|
||||||
|
Mesh p = makePlane(2.0f);
|
||||||
|
for (int i = 0; i < 3; ++i) subdivide(p, false);
|
||||||
|
int mid = 0;
|
||||||
|
for (size_t v = 0; v < p.verts.size(); ++v)
|
||||||
|
if (glm::length(glm::vec2(p.verts[v])) < glm::length(glm::vec2(p.verts[size_t(mid)]))) mid = int(v);
|
||||||
|
p.verts[size_t(mid)].z = 1.0f;
|
||||||
|
smoothPoints(p, {mid}, 1.0f);
|
||||||
|
CHECK(std::abs(p.verts[size_t(mid)].z) < 1e-5f, "smooth: the bump's point back to its neighbours' middle");
|
||||||
|
Mesh c = makeCube();
|
||||||
|
std::set<int> all;
|
||||||
|
for (size_t v = 0; v < c.verts.size(); ++v) all.insert(int(v));
|
||||||
|
pushPoints(c, all, 0.1f);
|
||||||
|
CHECK(std::abs(boundsMax(c).x - (0.5f + 0.1f / std::sqrt(3.0f))) < 1e-4f && checkForPrint(c).volume > 1.0f, "push: corners out along their normals");
|
||||||
|
}
|
||||||
{ // A gear with a screw hole (scripts/gear.lua): cutting leaves points a hair apart and
|
{ // A gear with a screw hole (scripts/gear.lua): cutting leaves points a hair apart and
|
||||||
// needles along edges; what comes out must still print (closed, nothing without size).
|
// needles along edges; what comes out must still print (closed, nothing without size).
|
||||||
for (int teeth : {10, 16})
|
for (int teeth : {10, 16})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue