mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
Make copies: along a path (the last picked: a drawn shape's outline, a line, a ring), spread evenly by length, turned as it turns (or not); fry::pathOf / alongPath; tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b9b6932d0e
commit
f9ba30ab3d
4 changed files with 171 additions and 11 deletions
56
src/app.cpp
56
src/app.cpp
|
|
@ -421,7 +421,8 @@ public:
|
|||
static keel::Cmd copiesCmd("fry_copies", [](const keel::Args& a) { // fry_copies n row dx dy dz | n round angle [joined]
|
||||
if (a.size() < 3) return;
|
||||
auto& c = self->copies_;
|
||||
c.count = std::stoi(a[1]), c.round = a[2] == "round";
|
||||
c.count = std::stoi(a[1]), c.round = a[2] == "round", c.path = a[2] == "path";
|
||||
c.follow = std::find(a.begin(), a.end(), "still") == a.end();
|
||||
if (c.round && a.size() > 3) c.angle = std::stof(a[3]), c.centre = glm::vec3(0.0f);
|
||||
if (!c.round && a.size() > 5) c.step = {std::stof(a[3]), std::stof(a[4]), std::stof(a[5])};
|
||||
c.joined = a.back() == "joined";
|
||||
|
|
@ -940,7 +941,7 @@ private:
|
|||
std::string captureUri_; // (a picture of the view going into a picked file)
|
||||
struct CopiesSetting {
|
||||
int count = 3, axis = 2;
|
||||
bool round = false, joined = false;
|
||||
bool round = false, joined = false, path = false, follow = true; // (path: along the last picked object)
|
||||
glm::vec3 step{0.3f, 0.0f, 0.0f}, centre{0.0f};
|
||||
float angle = 360.0f;
|
||||
} copies_;
|
||||
|
|
@ -3842,21 +3843,45 @@ private:
|
|||
void makeCopies() {
|
||||
if (mode_ == Mode::Edit) leaveEdit();
|
||||
if (selected_.empty()) return say("Pick what to copy first");
|
||||
pushUndo();
|
||||
const CopiesSetting& c = copies_;
|
||||
// Along a path: the last one picked is the path (its outline, or its line), the others go along it.
|
||||
std::vector<fry::PathPoint> along;
|
||||
int pathSkip = 0;
|
||||
if (c.path) {
|
||||
const Object* path = find(active_);
|
||||
if (!path || selected_.size() < 2) return say("Pick what to copy, then (Shift) the path last: a drawn shape, a line, a ring...");
|
||||
bool loop = false;
|
||||
Mesh m = path->mesh;
|
||||
transform(m, objectMatrix(*path));
|
||||
const std::vector<glm::vec3> line = fry::pathOf(m, loop);
|
||||
along = fry::alongPath(line, loop, c.count);
|
||||
if (along.empty()) return say("The path has no length to go along");
|
||||
// (one already on the path's start: that's the first, the copies after it)
|
||||
const float near = glm::distance(boundsMin(m), boundsMax(m)) * 1e-3f + 1e-6f;
|
||||
for (uint32_t id : selected_)
|
||||
if (const Object* o = find(id); o && id != active_ && glm::distance(o->position, along[0].at) < near) {
|
||||
along = fry::alongPath(line, loop, c.count + 1);
|
||||
pathSkip = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
pushUndo();
|
||||
std::set<uint32_t> made = selected_;
|
||||
const std::vector<Object> originals = [&] {
|
||||
std::vector<Object> v;
|
||||
for (const Object& o : scene_.objects)
|
||||
if (selected_.count(o.id)) v.push_back(o);
|
||||
if (selected_.count(o.id) && !(c.path && o.id == active_)) v.push_back(o);
|
||||
return v;
|
||||
}();
|
||||
const glm::vec3 axis = c.axis == 0 ? glm::vec3(1, 0, 0) : c.axis == 1 ? glm::vec3(0, 1, 0) : glm::vec3(0, 0, 1);
|
||||
for (const Object& o : originals) {
|
||||
Object* joinInto = c.joined ? find(o.id) : nullptr;
|
||||
for (int k = 1; k <= c.count; ++k) {
|
||||
for (int k = c.path ? pathSkip : 1; k <= (c.path ? int(along.size()) - 1 : c.count); ++k) {
|
||||
glm::mat4 T(1.0f);
|
||||
if (c.round) {
|
||||
if (c.path) { // (its middle onto the point; turned as the path turns from its start)
|
||||
const glm::quat turn = c.follow ? glm::rotation(along[0].along, along[size_t(k)].along) : glm::quat(1, 0, 0, 0);
|
||||
T = glm::translate(glm::mat4(1.0f), along[size_t(k)].at) * glm::mat4_cast(turn) * glm::translate(glm::mat4(1.0f), -o.position);
|
||||
} else if (c.round) {
|
||||
const float a = glm::radians(c.angle) * float(k) / float(c.angle >= 359.99f ? c.count + 1 : c.count);
|
||||
T = glm::translate(glm::mat4(1.0f), c.centre) * glm::rotate(glm::mat4(1.0f), a, axis) * glm::translate(glm::mat4(1.0f), -c.centre);
|
||||
} else
|
||||
|
|
@ -3872,14 +3897,16 @@ private:
|
|||
const std::string name = n.name;
|
||||
n = o, n.id = id, n.name = name, n.locked = false;
|
||||
n.position = glm::vec3(T * glm::vec4(o.position, 1.0f));
|
||||
if (c.round) n.rotation = glm::normalize(glm::quat_cast(glm::mat3(T)) * o.rotation);
|
||||
if (c.round || c.path) n.rotation = glm::normalize(glm::quat_cast(glm::mat3(T)) * o.rotation);
|
||||
made.insert(id);
|
||||
}
|
||||
if (joinInto) changedShape(*joinInto);
|
||||
}
|
||||
selected_ = made;
|
||||
touchAll();
|
||||
say(std::format("{} cop{} of each made{}", c.count, c.count == 1 ? "y" : "ies", c.joined ? ", joined into it" : ""));
|
||||
if (c.path) selected_.erase(active_), active_ = selected_.empty() ? 0 : *selected_.rbegin(); // (the path isn't one of them)
|
||||
const size_t n = c.path ? along.size() - size_t(pathSkip) : size_t(c.count);
|
||||
say(std::format("{} cop{} of each made{}{}", n, n == 1 ? "y" : "ies", c.path ? " along the path" : "", c.joined ? ", joined into it" : ""));
|
||||
}
|
||||
void copiesWindow() {
|
||||
if (!showCopies_) return;
|
||||
|
|
@ -3889,10 +3916,17 @@ private:
|
|||
const float w = 280;
|
||||
ImGui::SetNextItemWidth(w);
|
||||
ImGui::SliderInt("##count", &c.count, 1, 100, "%d copies");
|
||||
int kind = c.round ? 1 : 0;
|
||||
int kind = c.path ? 2 : c.round ? 1 : 0;
|
||||
ImGui::SetNextItemWidth(w);
|
||||
if (ImGui::Combo("##kind", &kind, "In a row\0Round in a circle\0")) c.round = kind == 1;
|
||||
if (!c.round) {
|
||||
if (ImGui::Combo("##kind", &kind, "In a row\0Round in a circle\0Along a path\0")) c.round = kind == 1, c.path = kind == 2;
|
||||
if (c.path) {
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + w);
|
||||
ImGui::TextDisabled("Pick what to copy, then the path last (Shift+click): a drawn shape's outline, a line, a ring. The copies are spread evenly along it.");
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Checkbox("Turn them as the path turns", &c.follow);
|
||||
if (const Object* p = selected_.size() >= 2 ? find(active_) : nullptr) ImGui::TextColored(accentText(), "Path: %s", p->name.c_str());
|
||||
else ImGui::TextColored(roleColor(keel::ui::Role::DangerText), "No path picked yet");
|
||||
} else if (!c.round) {
|
||||
glm::vec3 mm = c.step * 1000.0f;
|
||||
ImGui::SetNextItemWidth(w);
|
||||
if (ImGui::DragFloat3("##step", &mm.x, 1.0f, -1e6f, 1e6f, "%.1f mm")) c.step = mm / 1000.0f;
|
||||
|
|
|
|||
11
src/mesh.h
11
src/mesh.h
|
|
@ -156,6 +156,17 @@ void thicken(Mesh& m, float thickness);
|
|||
// edges (or a flat shape's outline) sweep into the walls; a flat shape is its two ends, when the
|
||||
// turn doesn't come back round on itself.
|
||||
Mesh spin(const Mesh& m, const glm::vec3& origin, const glm::vec3& axis, float degrees, int steps, float rise = 0.0f);
|
||||
|
||||
// A shape as a path to put things along: its open edges chained up (the longest run; a flat
|
||||
// shape's outline, a line's points), or, a closed solid, its biggest face's outline. `closed`:
|
||||
// it comes back round to its start.
|
||||
std::vector<glm::vec3> pathOf(const Mesh& m, bool& closed);
|
||||
struct PathPoint {
|
||||
glm::vec3 at, along; // (the way the path goes there, unit length)
|
||||
};
|
||||
// `count` points evenly spaced along it by length (ends included on an open one; on a closed one
|
||||
// the last isn't on top of the first).
|
||||
std::vector<PathPoint> alongPath(const std::vector<glm::vec3>& path, bool closed, int count);
|
||||
// Two shapes (in the same space) made one: `b` added to `a`, taken out of it, or only where they
|
||||
// overlap kept. The result goes in `a`; false if nothing's left.
|
||||
enum class Combine { Add, Subtract, Overlap };
|
||||
|
|
|
|||
|
|
@ -153,3 +153,95 @@ Mesh spin(const Mesh& m, const glm::vec3& origin, const glm::vec3& axis, float d
|
|||
}
|
||||
|
||||
} // namespace fry
|
||||
|
||||
namespace fry {
|
||||
|
||||
std::vector<glm::vec3> pathOf(const Mesh& m, bool& closed) {
|
||||
closed = false;
|
||||
std::map<std::pair<int, int>, int> count;
|
||||
for (const auto& f : m.faces)
|
||||
if (f.size() == 2) count[{std::min(f[0], f[1]), std::max(f[0], f[1])}] = 1; // (a line's piece)
|
||||
else
|
||||
for (size_t i = 0; i < f.size(); ++i) ++count[{std::min(f[i], f[(i + 1) % f.size()]), std::max(f[i], f[(i + 1) % f.size()])}];
|
||||
std::map<int, std::vector<int>> next;
|
||||
for (const auto& [e, n] : count)
|
||||
if (n == 1 && e.first != e.second) next[e.first].push_back(e.second), next[e.second].push_back(e.first);
|
||||
if (next.empty()) { // (closed all round: the biggest face's outline)
|
||||
int best = -1;
|
||||
float area = 0;
|
||||
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||
if (m.faces[f].size() < 3) continue;
|
||||
glm::vec3 n(0.0f);
|
||||
const auto& fv = m.faces[f];
|
||||
for (size_t i = 0; i < fv.size(); ++i) n += glm::cross(m.verts[size_t(fv[i])], m.verts[size_t(fv[(i + 1) % fv.size()])]);
|
||||
if (glm::length(n) > area) area = glm::length(n), best = int(f);
|
||||
}
|
||||
std::vector<glm::vec3> out;
|
||||
if (best >= 0)
|
||||
for (int v : m.faces[size_t(best)]) out.push_back(m.verts[size_t(v)]);
|
||||
closed = out.size() >= 3;
|
||||
return out;
|
||||
}
|
||||
// Each run of edges, walked from an end (or anywhere on a loop); the longest kept.
|
||||
std::set<std::pair<int, int>> used;
|
||||
std::vector<glm::vec3> best;
|
||||
bool bestClosed = false;
|
||||
float bestLength = -1;
|
||||
auto walk = [&](int from) {
|
||||
std::vector<int> chain{from};
|
||||
int at = from, prev = -1;
|
||||
bool loop = false;
|
||||
for (;;) {
|
||||
int go = -1;
|
||||
for (int n : next[at])
|
||||
if (n != prev && !used.count({std::min(at, n), std::max(at, n)})) {
|
||||
go = n;
|
||||
break;
|
||||
}
|
||||
if (go < 0) break;
|
||||
used.insert({std::min(at, go), std::max(at, go)});
|
||||
if (go == from) {
|
||||
loop = true;
|
||||
break;
|
||||
}
|
||||
chain.push_back(go), prev = at, at = go;
|
||||
}
|
||||
float length = 0;
|
||||
for (size_t i = 1; i < chain.size(); ++i) length += glm::distance(m.verts[size_t(chain[i - 1])], m.verts[size_t(chain[i])]);
|
||||
if (loop) length += glm::distance(m.verts[size_t(chain.back())], m.verts[size_t(from)]);
|
||||
if (chain.size() >= 2 && length > bestLength) {
|
||||
bestLength = length, bestClosed = loop, best.clear();
|
||||
for (int v : chain) best.push_back(m.verts[size_t(v)]);
|
||||
}
|
||||
};
|
||||
for (const auto& [v, ns] : next)
|
||||
if (ns.size() == 1) walk(v); // (ends first: open runs from their start)
|
||||
for (const auto& [v, ns] : next)
|
||||
for (int n : ns)
|
||||
if (!used.count({std::min(v, n), std::max(v, n)})) walk(v);
|
||||
closed = bestClosed;
|
||||
return best;
|
||||
}
|
||||
|
||||
std::vector<PathPoint> alongPath(const std::vector<glm::vec3>& path, bool closed, int count) {
|
||||
std::vector<PathPoint> out;
|
||||
if (path.size() < 2 || count < 1) return out;
|
||||
std::vector<glm::vec3> p = path;
|
||||
if (closed) p.push_back(path.front());
|
||||
std::vector<float> at(p.size(), 0.0f); // (the length to each point)
|
||||
for (size_t i = 1; i < p.size(); ++i) at[i] = at[i - 1] + glm::distance(p[i - 1], p[i]);
|
||||
const float total = at.back();
|
||||
if (total < 1e-12f) return out;
|
||||
for (int k = 0; k < count; ++k) {
|
||||
const float want = closed ? total * float(k) / float(count) : count == 1 ? 0.0f : total * float(k) / float(count - 1);
|
||||
size_t i = size_t(std::upper_bound(at.begin(), at.end(), want) - at.begin());
|
||||
i = std::clamp<size_t>(i, 1, p.size() - 1);
|
||||
while (i < p.size() - 1 && at[i] - at[i - 1] < 1e-12f) ++i; // (no way along a piece with no length)
|
||||
const float piece = std::max(at[i] - at[i - 1], 1e-12f), t = std::clamp((want - at[i - 1]) / piece, 0.0f, 1.0f);
|
||||
const glm::vec3 d = p[i] - p[i - 1];
|
||||
out.push_back({glm::mix(p[i - 1], p[i], t), glm::length(d) > 1e-12f ? glm::normalize(d) : glm::vec3(1, 0, 0)});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace fry
|
||||
|
|
|
|||
Loading…
Reference in a new issue