mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
Editable cuts: Object > Cut out, editable: the cutters stay objects (see-through red), moved or changed and the hole follows; they go with what they cut when it's moved; Apply makes one or all for good; saved; exported and printed with the cut made; tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f9ba30ab3d
commit
5394eeb3d8
4 changed files with 202 additions and 14 deletions
141
src/app.cpp
141
src/app.cpp
|
|
@ -29,6 +29,7 @@
|
||||||
|
|
||||||
#include <glm/gtc/matrix_transform.hpp>
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
#include <glm/gtc/type_ptr.hpp>
|
#include <glm/gtc/type_ptr.hpp>
|
||||||
|
#include <glm/gtx/matrix_decompose.hpp>
|
||||||
#include <glm/gtx/quaternion.hpp>
|
#include <glm/gtx/quaternion.hpp>
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <imgui_internal.h>
|
#include <imgui_internal.h>
|
||||||
|
|
@ -1117,7 +1118,7 @@ private:
|
||||||
void restore(Snapshot& s) {
|
void restore(Snapshot& s) {
|
||||||
// (Not the scripts: they're edited like text, not undone with the model.)
|
// (Not the scripts: they're edited like text, not undone with the model.)
|
||||||
std::vector<fry::SceneScript> scripts = std::move(scene_.scripts);
|
std::vector<fry::SceneScript> scripts = std::move(scene_.scripts);
|
||||||
scene_ = s.scene, selected_ = s.selected, active_ = s.active, mode_ = s.mode;
|
scene_ = s.scene, selected_ = s.selected, active_ = s.active, mode_ = s.mode, followAt_.clear();
|
||||||
scene_.scripts = std::move(scripts);
|
scene_.scripts = std::move(scripts);
|
||||||
selVerts_ = s.verts, selFaces_ = s.faces, selEdges_ = s.edges;
|
selVerts_ = s.verts, selFaces_ = s.faces, selEdges_ = s.edges;
|
||||||
if (mode_ == Mode::Edit && !find(active_)) mode_ = Mode::Object;
|
if (mode_ == Mode::Edit && !find(active_)) mode_ = Mode::Object;
|
||||||
|
|
@ -1140,7 +1141,7 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
void newScene() {
|
void newScene() {
|
||||||
scene_ = {};
|
scene_ = {}, followAt_.clear();
|
||||||
script_ = 0, scriptPath_.clear();
|
script_ = 0, scriptPath_.clear();
|
||||||
path_.clear();
|
path_.clear();
|
||||||
undo_.clear(), redo_.clear(), selected_.clear();
|
undo_.clear(), redo_.clear(), selected_.clear();
|
||||||
|
|
@ -1299,7 +1300,7 @@ private:
|
||||||
auto s = loadPackage(path, &err);
|
auto s = loadPackage(path, &err);
|
||||||
if (!s) return say("Couldn't open: " + err);
|
if (!s) return say("Couldn't open: " + err);
|
||||||
dropAutosave();
|
dropAutosave();
|
||||||
scene_ = std::move(*s);
|
scene_ = std::move(*s), followAt_.clear();
|
||||||
// (an old plain-text .fry: its pictures are PNGs beside it)
|
// (an old plain-text .fry: its pictures are PNGs beside it)
|
||||||
const stdfs::path here = stdfs::path(path).parent_path();
|
const stdfs::path here = stdfs::path(path).parent_path();
|
||||||
for (Object& o : scene_.objects) {
|
for (Object& o : scene_.objects) {
|
||||||
|
|
@ -1362,7 +1363,7 @@ private:
|
||||||
for (size_t i = 0; i < scene_.objects.size(); ++i)
|
for (size_t i = 0; i < scene_.objects.size(); ++i)
|
||||||
if (const Object& o = scene_.objects[i]; o.picture && o.uvsFit()) pngs[i] = pngBytes(o.layers.empty() ? *o.picture : composePicture(o));
|
if (const Object& o = scene_.objects[i]; o.picture && o.uvsFit()) pngs[i] = pngBytes(o.layers.empty() ? *o.picture : composePicture(o));
|
||||||
std::string err;
|
std::string err;
|
||||||
if (!exportModel(path, scene_, pngs, &err)) return say("Couldn't export: " + err);
|
if (!exportModel(path, fry::withCutsMade(scene_), pngs, &err)) return say("Couldn't export: " + err);
|
||||||
say("Exported " + stdfs::path(path).filename().string());
|
say("Exported " + stdfs::path(path).filename().string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3070,7 +3071,7 @@ private:
|
||||||
}
|
}
|
||||||
if (p.key == key) return p;
|
if (p.key == key) return p;
|
||||||
p.key = key;
|
p.key = key;
|
||||||
p.world = o.hasFinish() ? finishedMesh(o) : o.mesh;
|
p.world = cutKeys_.count(o.id) ? fry::cutMesh(scene_, o) : o.hasFinish() ? finishedMesh(o) : o.mesh;
|
||||||
transform(p.world, objectMatrix(o));
|
transform(p.world, objectMatrix(o));
|
||||||
p.lo = boundsMin(p.world), p.hi = boundsMax(p.world);
|
p.lo = boundsMin(p.world), p.hi = boundsMax(p.world);
|
||||||
p.check = checkForPrint(p.world, overhangDeg_, 0.0f);
|
p.check = checkForPrint(p.world, overhangDeg_, 0.0f);
|
||||||
|
|
@ -3126,7 +3127,7 @@ private:
|
||||||
std::vector<Object*> printTargets() { // (what's picked, or everything shown)
|
std::vector<Object*> printTargets() { // (what's picked, or everything shown)
|
||||||
std::vector<Object*> out;
|
std::vector<Object*> out;
|
||||||
for (Object& o : scene_.objects)
|
for (Object& o : scene_.objects)
|
||||||
if (o.visible && (selected_.empty() || selected_.count(o.id))) out.push_back(&o);
|
if (o.visible && !isCutter(o) && (selected_.empty() || selected_.count(o.id))) out.push_back(&o);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
void drawPrintBed(keel::Renderer& r, const keel::ModelLighting& light) {
|
void drawPrintBed(keel::Renderer& r, const keel::ModelLighting& light) {
|
||||||
|
|
@ -3160,14 +3161,14 @@ private:
|
||||||
if (showOverhangs_) {
|
if (showOverhangs_) {
|
||||||
const keel::ModelLighting flat{{glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f)}, 0.0f};
|
const keel::ModelLighting flat{{glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f)}, 0.0f};
|
||||||
for (const Object& o : scene_.objects)
|
for (const Object& o : scene_.objects)
|
||||||
if (o.visible)
|
if (o.visible && !isCutter(o))
|
||||||
if (const PrintInfo& p = printInfo(o); p.hasOverhang)
|
if (const PrintInfo& p = printInfo(o); p.hasOverhang)
|
||||||
r.drawModel(p.overhang, glm::scale(glm::mat4(1.0f), glm::vec3(kUnits)), glm::vec4(0.95f, 0.15f, 0.1f, 0.55f), nullptr, &flat, false);
|
r.drawModel(p.overhang, glm::scale(glm::mat4(1.0f), glm::vec3(kUnits)), glm::vec4(0.95f, 0.15f, 0.1f, 0.55f), nullptr, &flat, false);
|
||||||
}
|
}
|
||||||
if (showThin_) {
|
if (showThin_) {
|
||||||
const keel::ModelLighting flat{{glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f)}, 0.0f};
|
const keel::ModelLighting flat{{glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f)}, 0.0f};
|
||||||
for (const Object& o : scene_.objects)
|
for (const Object& o : scene_.objects)
|
||||||
if (o.visible)
|
if (o.visible && !isCutter(o))
|
||||||
if (const PrintInfo& p = printInfo(o); p.thinFaces)
|
if (const PrintInfo& p = printInfo(o); p.thinFaces)
|
||||||
r.drawModel(p.thin, glm::scale(glm::mat4(1.0f), glm::vec3(kUnits)), glm::vec4(1.0f, 0.8f, 0.1f, 0.7f), nullptr, &flat, false);
|
r.drawModel(p.thin, glm::scale(glm::mat4(1.0f), glm::vec3(kUnits)), glm::vec4(1.0f, 0.8f, 0.1f, 0.7f), nullptr, &flat, false);
|
||||||
}
|
}
|
||||||
|
|
@ -4388,14 +4389,96 @@ private:
|
||||||
|
|
||||||
// The mesh as seen: with its rounding, smoothing and mirror — but while its shape is being
|
// The mesh as seen: with its rounding, smoothing and mirror — but while its shape is being
|
||||||
// edited, only the mirror (the rest would hide the points you're moving).
|
// edited, only the mirror (the rest would hide the points you're moving).
|
||||||
|
// Editable cuts show (but not while its own shape's being edited: the points you move).
|
||||||
const Mesh& shownMesh(const Object& o) {
|
const Mesh& shownMesh(const Object& o) {
|
||||||
const bool editingThis = mode_ == Mode::Edit && o.id == active_;
|
const bool editingThis = mode_ == Mode::Edit && o.id == active_;
|
||||||
if (!o.hasFinish() || (editingThis && !o.mirror)) return o.mesh;
|
const bool cut = !editingThis && cutKeys_.count(o.id);
|
||||||
|
if ((!o.hasFinish() && !cut) || (editingThis && !o.mirror)) return o.mesh;
|
||||||
Shown& s = shown_[o.id];
|
Shown& s = shown_[o.id];
|
||||||
const uint64_t v = versions_[o.id];
|
const uint64_t v = versions_[o.id];
|
||||||
if (!(s.version == v && v && s.mirrorOnly == editingThis)) s.mesh = finishedMesh(o, editingThis), s.version = v, s.mirrorOnly = editingThis;
|
if (!(s.version == v && v && s.mirrorOnly == editingThis))
|
||||||
|
s.mesh = cut ? fry::cutMesh(scene_, o) : finishedMesh(o, editingThis), s.version = v, s.mirrorOnly = editingThis;
|
||||||
return s.mesh;
|
return s.mesh;
|
||||||
}
|
}
|
||||||
|
// Each object something's cut out of, and what its cutters are (their shapes, where they are):
|
||||||
|
// a change, and it's made again (the caches follow its version).
|
||||||
|
std::map<uint32_t, uint64_t> cutKeys_;
|
||||||
|
std::map<uint32_t, glm::mat4> followAt_; // (where each cut object and cutter was last frame)
|
||||||
|
void followCuts() {
|
||||||
|
// What's cut moved, turned or sized (not by a drag: that takes them itself), and its cutter
|
||||||
|
// didn't: the cutter goes the same way, so the hole stays where it was on it.
|
||||||
|
std::map<uint32_t, glm::mat4> at;
|
||||||
|
for (Object& c : scene_.objects) {
|
||||||
|
const Object* t = fry::cutTarget(scene_, c);
|
||||||
|
if (!t) continue;
|
||||||
|
const glm::mat4 mt = objectMatrix(*t);
|
||||||
|
const auto was = followAt_.find(t->id), cWas = followAt_.find(c.id);
|
||||||
|
if (!drag_.active && was != followAt_.end() && cWas != followAt_.end() && was->second != mt && cWas->second == objectMatrix(c) && !c.locked) {
|
||||||
|
const glm::mat4 m = mt * glm::inverse(was->second) * objectMatrix(c);
|
||||||
|
glm::vec3 skew;
|
||||||
|
glm::vec4 persp;
|
||||||
|
glm::vec3 scale, pos;
|
||||||
|
glm::quat rot;
|
||||||
|
if (glm::decompose(m, scale, rot, pos, skew, persp)) c.position = pos, c.rotation = glm::normalize(rot), c.scale = scale, touched(c.id);
|
||||||
|
}
|
||||||
|
at[t->id] = mt, at[c.id] = objectMatrix(c);
|
||||||
|
}
|
||||||
|
followAt_ = std::move(at);
|
||||||
|
std::map<uint32_t, uint64_t> now;
|
||||||
|
for (const Object& c : scene_.objects) {
|
||||||
|
const Object* t = fry::cutTarget(scene_, c);
|
||||||
|
if (!t || c.mesh.faces.empty()) continue;
|
||||||
|
uint64_t& key = now.emplace(t->id, 1469598103934665603ull).first->second;
|
||||||
|
key = (key ^ versions_[c.id] ^ (uint64_t(c.id) << 40)) * 1099511628211ull;
|
||||||
|
const glm::mat4 m = objectMatrix(c);
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
uint32_t b;
|
||||||
|
std::memcpy(&b, &m[i / 4][i % 4], 4);
|
||||||
|
key = (key ^ b) * 1099511628211ull;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& [id, key] : now)
|
||||||
|
if (auto it = cutKeys_.find(id); it == cutKeys_.end() || it->second != key) versions_[id] = ++versionClock_;
|
||||||
|
for (const auto& [id, key] : cutKeys_)
|
||||||
|
if (!now.count(id)) versions_[id] = ++versionClock_;
|
||||||
|
cutKeys_ = std::move(now);
|
||||||
|
}
|
||||||
|
bool isCutter(const Object& o) const { return fry::cutTarget(scene_, o) != nullptr; }
|
||||||
|
// Editable cut: the last picked has the others cut out of it, and they stay objects to move.
|
||||||
|
void cutEditable() {
|
||||||
|
if (mode_ == Mode::Edit) leaveEdit();
|
||||||
|
Object* a = find(active_);
|
||||||
|
if (!a || selected_.size() < 2) return say("Pick the cutters, then (Shift) what they cut last: they stay, to move and change, and the hole follows");
|
||||||
|
pushUndo();
|
||||||
|
int n = 0;
|
||||||
|
for (Object& o : scene_.objects)
|
||||||
|
if (selected_.count(o.id) && o.id != a->id) o.cuts = a->id, ++n;
|
||||||
|
for (Object& o : scene_.objects) // (one that was cutting the cutters: not a loop of cuts)
|
||||||
|
if (o.id == a->id && o.cuts && selected_.count(o.cuts)) o.cuts = 0;
|
||||||
|
selected_.erase(a->id), active_ = *selected_.rbegin();
|
||||||
|
dirty_ = true;
|
||||||
|
say(std::format("{} cut out of {} (editable: move or change {}; Apply makes it for good)", n == 1 ? "1 object" : std::format("{} objects", n), find(a->id)->name, n == 1 ? "it" : "them"));
|
||||||
|
}
|
||||||
|
// The cut made for good: one cutter (or all of what's cut out of it) into the shape, gone.
|
||||||
|
void applyCuts(uint32_t target, uint32_t only = 0) {
|
||||||
|
Object* t = find(target);
|
||||||
|
if (!t) return;
|
||||||
|
pushUndo();
|
||||||
|
Scene part = scene_; // (just the cutters wanted)
|
||||||
|
for (Object& o : part.objects)
|
||||||
|
if (o.cuts == target && only && o.id != only) o.cuts = 0;
|
||||||
|
Object* pt = &*std::find_if(part.objects.begin(), part.objects.end(), [&](const Object& o) { return o.id == target; });
|
||||||
|
t->mesh = fry::cutMesh(part, *pt);
|
||||||
|
t->mirror = 0, t->round = 0, t->smoothLevels = 0, t->creases.clear();
|
||||||
|
std::set<uint32_t> gone;
|
||||||
|
for (const Object& o : scene_.objects)
|
||||||
|
if (o.cuts == target && (!only || o.id == only)) gone.insert(o.id);
|
||||||
|
std::erase_if(scene_.objects, [&](const Object& o) { return gone.count(o.id) > 0; });
|
||||||
|
for (uint32_t id : gone) selected_.erase(id);
|
||||||
|
selected_.insert(target), active_ = target;
|
||||||
|
changedShape(*find(target));
|
||||||
|
say(std::format("Cut made for good in {} ({} faces)", find(target)->name, find(target)->mesh.faces.size()));
|
||||||
|
}
|
||||||
|
|
||||||
// ───────────────────────────── picking ─────────────────────────────
|
// ───────────────────────────── picking ─────────────────────────────
|
||||||
|
|
||||||
|
|
@ -5110,7 +5193,8 @@ private:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (Object& o : scene_.objects) {
|
for (Object& o : scene_.objects) {
|
||||||
if (!selected_.count(o.id) || o.locked) continue;
|
const bool along = !selected_.count(o.id) && o.cuts && selected_.count(o.cuts) && isCutter(o); // (an editable cutter goes with what it cuts)
|
||||||
|
if ((!selected_.count(o.id) && !along) || o.locked) continue;
|
||||||
o.position = glm::vec3(T * glm::vec4(o.position, 1.0f));
|
o.position = glm::vec3(T * glm::vec4(o.position, 1.0f));
|
||||||
if (drag_.tool == Tool::Rotate) o.rotation = glm::normalize(glm::quat_cast(glm::mat3(T)) * o.rotation);
|
if (drag_.tool == Tool::Rotate) o.rotation = glm::normalize(glm::quat_cast(glm::mat3(T)) * o.rotation);
|
||||||
if (drag_.tool == Tool::Scale) {
|
if (drag_.tool == Tool::Scale) {
|
||||||
|
|
@ -5607,7 +5691,7 @@ private:
|
||||||
keel::execute(want);
|
keel::execute(want);
|
||||||
}
|
}
|
||||||
// Painted, and shown as painted (with rounding or smoothing added since, it isn't).
|
// Painted, and shown as painted (with rounding or smoothing added since, it isn't).
|
||||||
bool painted(const Object& o) const { return o.picture && o.uvsFit() && !o.hasFinish(); }
|
bool painted(const Object& o) const { return o.picture && o.uvsFit() && !o.hasFinish() && !cutKeys_.count(o.id); }
|
||||||
|
|
||||||
keel::ModelId modelOf(const Object& o) {
|
keel::ModelId modelOf(const Object& o) {
|
||||||
const bool editingThis = mode_ == Mode::Edit && o.id == active_;
|
const bool editingThis = mode_ == Mode::Edit && o.id == active_;
|
||||||
|
|
@ -5688,6 +5772,7 @@ private:
|
||||||
|
|
||||||
void draw3D(keel::Renderer& r) {
|
void draw3D(keel::Renderer& r) {
|
||||||
settleView();
|
settleView();
|
||||||
|
followCuts();
|
||||||
keel::Camera cam;
|
keel::Camera cam;
|
||||||
cam.origin = eye_ * kUnits, cam.yaw = yaw_, cam.pitch = pitch_, cam.fovY = fov_, cam.shift = shift_, cam.zNear = nearFar_.x, cam.zFar = nearFar_.y;
|
cam.origin = eye_ * kUnits, cam.yaw = yaw_, cam.pitch = pitch_, cam.fovY = fov_, cam.shift = shift_, cam.zNear = nearFar_.x, cam.zFar = nearFar_.y;
|
||||||
r.setCamera(cam);
|
r.setCamera(cam);
|
||||||
|
|
@ -5712,11 +5797,15 @@ private:
|
||||||
if (o.picture) uploadPicture(o, !(paint_.active && paint_.object == o.id));
|
if (o.picture) uploadPicture(o, !(paint_.active && paint_.object == o.id));
|
||||||
if (shading() != Shading::Wire)
|
if (shading() != Shading::Wire)
|
||||||
for (const Object& o : scene_.objects) {
|
for (const Object& o : scene_.objects) {
|
||||||
if (!o.visible) continue;
|
if (!o.visible || isCutter(o)) continue;
|
||||||
const glm::vec4 tint = painted(o) ? glm::vec4(1.0f) : glm::vec4(glm::pow(o.color, glm::vec3(2.2f)), 1.0f); // (colours are picked as seen: sRGB)
|
const glm::vec4 tint = painted(o) ? glm::vec4(1.0f) : glm::vec4(glm::pow(o.color, glm::vec3(2.2f)), 1.0f); // (colours are picked as seen: sRGB)
|
||||||
const keel::Surface surface{o.metal, o.rough};
|
const keel::Surface surface{o.metal, o.rough};
|
||||||
r.drawModel(modelOf(o), toUnits * objectMatrix(o), tint, nullptr, &light, rendered, nullptr, keel::LiquidStyle::None, &surface);
|
r.drawModel(modelOf(o), toUnits * objectMatrix(o), tint, nullptr, &light, rendered, nullptr, keel::LiquidStyle::None, &surface);
|
||||||
}
|
}
|
||||||
|
// Editable cutters: see-through, so the hole they make shows (not in a saved picture).
|
||||||
|
if (shading() != Shading::Wire && !capturing())
|
||||||
|
for (const Object& o : scene_.objects)
|
||||||
|
if (o.visible && isCutter(o)) r.drawModel(modelOf(o), toUnits * objectMatrix(o), glm::vec4(0.9f, 0.35f, 0.3f, 0.3f), nullptr, &light, false);
|
||||||
// Rendered: a ground to stand on and catch the shadows (not part of what you make).
|
// Rendered: a ground to stand on and catch the shadows (not part of what you make).
|
||||||
if (rendered && look.ground) {
|
if (rendered && look.ground) {
|
||||||
if (!haveGround_) {
|
if (!haveGround_) {
|
||||||
|
|
@ -5858,6 +5947,10 @@ private:
|
||||||
if (!o.visible) continue;
|
if (!o.visible) continue;
|
||||||
const bool sel = selected_.count(o.id) > 0 && !clean && tab_ != Tab::Paint;
|
const bool sel = selected_.count(o.id) > 0 && !clean && tab_ != Tab::Paint;
|
||||||
if (ed && o.id == ed->id) continue;
|
if (ed && o.id == ed->id) continue;
|
||||||
|
if (isCutter(o)) {
|
||||||
|
if (!clean) lines(o, shownMesh(o), sel ? kAccent : IM_COL32(235, 95, 85, 150), sel ? 1.6f : 1.0f, false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (shading() == Shading::Wire || xray_) lines(o, shownMesh(o), sel ? kAccent : IM_COL32(200, 200, 205, 150), sel ? 1.6f : 1.0f, false);
|
if (shading() == Shading::Wire || xray_) lines(o, shownMesh(o), sel ? kAccent : IM_COL32(200, 200, 205, 150), sel ? 1.6f : 1.0f, false);
|
||||||
else if (sel) outline(o, kAccent, 2.5f);
|
else if (sel) outline(o, kAccent, 2.5f);
|
||||||
}
|
}
|
||||||
|
|
@ -6286,6 +6379,7 @@ private:
|
||||||
add("shape.separate", "Make into its own object", "Shape", "P", [this] { separateSelected(); }, editing, "separate split");
|
add("shape.separate", "Make into its own object", "Shape", "P", [this] { separateSelected(); }, editing, "separate split");
|
||||||
add("paint.mirror", "Paint both sides (mirrored)", "Paint", "", [this] { paintMirror_ = !paintMirror_, say(paintMirror_ ? "Painting both sides" : "Painting one side"); }, {}, "symmetry symmetric");
|
add("paint.mirror", "Paint both sides (mirrored)", "Paint", "", [this] { paintMirror_ = !paintMirror_, say(paintMirror_ ? "Painting both sides" : "Painting one side"); }, {}, "symmetry symmetric");
|
||||||
add("paint.stencil", "Draw a paint stencil (paint only inside it)", "Paint", "", [this] { setTab(Tab::Paint), drawingStencil_ = true, stencil_.clear(); }, {}, "mask masking tape area");
|
add("paint.stencil", "Draw a paint stencil (paint only inside it)", "Paint", "", [this] { setTab(Tab::Paint), drawingStencil_ = true, stencil_.clear(); }, {}, "mask masking tape area");
|
||||||
|
add("object.cutEditable", "Cut out, editable (the cutters stay, the hole follows them)", "Object", "", [this] { cutEditable(); }, many, "boolean subtract difference hole non-destructive live");
|
||||||
add("shape.crease", "Crease the picked edges (stay sharp when smoothed)", "Shape", "Shift+E", [this] { creaseSelected(); }, editing, "sharp hard edge subdivision smooth");
|
add("shape.crease", "Crease the picked edges (stay sharp when smoothed)", "Shape", "Shift+E", [this] { creaseSelected(); }, editing, "sharp hard edge subdivision smooth");
|
||||||
add("shape.slide", "Slide the points along their edges...", "Shape", "Alt+G", [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_, say(liveMirror_ ? "Mirror editing on: both sides change together" : "Mirror editing off"); }, {}, "symmetry symmetric x");
|
add("shape.mirror", "Mirror editing (both sides change together)", "Shape", "", [this] { liveMirror_ = !liveMirror_, say(liveMirror_ ? "Mirror editing on: both sides change together" : "Mirror editing off"); }, {}, "symmetry symmetric x");
|
||||||
|
|
@ -6455,6 +6549,8 @@ private:
|
||||||
ImGui::TextDisabled("Two or more: the last picked is kept");
|
ImGui::TextDisabled("Two or more: the last picked is kept");
|
||||||
if (ImGui::MenuItem("Add them together", "Ctrl+Shift++", false, many)) combineSelected(Combine::Add);
|
if (ImGui::MenuItem("Add them together", "Ctrl+Shift++", false, many)) combineSelected(Combine::Add);
|
||||||
if (ImGui::MenuItem("Take the others out of it", "Ctrl+Shift+-", false, many)) combineSelected(Combine::Subtract);
|
if (ImGui::MenuItem("Take the others out of it", "Ctrl+Shift+-", false, many)) combineSelected(Combine::Subtract);
|
||||||
|
if (ImGui::MenuItem("Cut out, editable", nullptr, false, many)) cutEditable();
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("The others cut into the last picked but stay objects: move or change them and the hole follows (Apply makes it for good)");
|
||||||
if (ImGui::MenuItem("Keep only where they overlap", nullptr, false, many)) combineSelected(Combine::Overlap);
|
if (ImGui::MenuItem("Keep only where they overlap", nullptr, false, many)) combineSelected(Combine::Overlap);
|
||||||
if (ImGui::MenuItem("Join (just as they are)", "Ctrl+J", false, many)) joinSelected();
|
if (ImGui::MenuItem("Join (just as they are)", "Ctrl+J", false, many)) joinSelected();
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
@ -7567,6 +7663,25 @@ private:
|
||||||
}
|
}
|
||||||
if (o->hasFinish() && ImGui::Button("Make it part of the shape", ImVec2(w, 0))) applyFinish(*o);
|
if (o->hasFinish() && ImGui::Button("Make it part of the shape", ImVec2(w, 0))) applyFinish(*o);
|
||||||
if (o->hasFinish() && ImGui::IsItemHovered()) ImGui::SetTooltip("Bake the rounding, smoothing and mirror into the points (so you can edit them)");
|
if (o->hasFinish() && ImGui::IsItemHovered()) ImGui::SetTooltip("Bake the rounding, smoothing and mirror into the points (so you can edit them)");
|
||||||
|
if (const Object* t = fry::cutTarget(scene_, *o)) { // (an editable cutter)
|
||||||
|
ImGui::SeparatorText("Cut");
|
||||||
|
ImGui::TextColored(accentText(), "Cuts into %s", t->name.c_str());
|
||||||
|
ImGui::TextDisabled("Move it, turn it, change its shape: the hole follows.");
|
||||||
|
const uint32_t target = t->id, me = o->id;
|
||||||
|
if (ImGui::Button("Apply: make the cut for good", ImVec2(w, 0))) return applyCuts(target, me);
|
||||||
|
if (ImGui::Button("Stop cutting (keep it as an object)", ImVec2(w, 0))) pushUndo(), o->cuts = 0, dirty_ = true;
|
||||||
|
} else if (cutKeys_.count(o->id)) {
|
||||||
|
int n = 0;
|
||||||
|
for (const Object& c : scene_.objects) n += c.cuts == o->id && isCutter(c);
|
||||||
|
ImGui::SeparatorText("Cut");
|
||||||
|
ImGui::TextDisabled("%d editable cut%s in it", n, n == 1 ? "" : "s");
|
||||||
|
if (ImGui::Button(n == 1 ? "Apply: make the cut for good" : "Apply: make them all for good", ImVec2(w, 0))) return applyCuts(o->id);
|
||||||
|
if (ImGui::Button("Pick the cutters", ImVec2(w, 0))) {
|
||||||
|
selected_.clear();
|
||||||
|
for (const Object& c : scene_.objects)
|
||||||
|
if (c.cuts == o->id) selected_.insert(c.id), active_ = c.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
ImGui::SeparatorText("Do");
|
ImGui::SeparatorText("Do");
|
||||||
if (ImGui::Button("Edit shape (Tab)", ImVec2(w, 0))) enterEdit();
|
if (ImGui::Button("Edit shape (Tab)", ImVec2(w, 0))) enterEdit();
|
||||||
if (ImGui::Button("Apply turn and size", ImVec2(w, 0))) applyTransform(*o);
|
if (ImGui::Button("Apply turn and size", ImVec2(w, 0))) applyTransform(*o);
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,7 @@ std::string sceneText(const Scene& scene) {
|
||||||
for (const Object& o : scene.objects) {
|
for (const Object& o : scene.objects) {
|
||||||
out << "object " << quoted(o.name) << "\n";
|
out << "object " << quoted(o.name) << "\n";
|
||||||
if (folderAt.count(o.folder)) out << " folder " << folderAt[o.folder] << "\n";
|
if (folderAt.count(o.folder)) out << " folder " << folderAt[o.folder] << "\n";
|
||||||
|
if (const Object* t = cutTarget(scene, o)) out << " cuts " << (t - scene.objects.data()) + 1 << "\n"; // (its place in the file)
|
||||||
out << " position " << o.position.x << " " << o.position.y << " " << o.position.z << "\n";
|
out << " position " << o.position.x << " " << o.position.y << " " << o.position.z << "\n";
|
||||||
out << " rotation " << o.rotation.w << " " << o.rotation.x << " " << o.rotation.y << " " << o.rotation.z << "\n"; // (a quaternion: w x y z)
|
out << " rotation " << o.rotation.w << " " << o.rotation.x << " " << o.rotation.y << " " << o.rotation.z << "\n"; // (a quaternion: w x y z)
|
||||||
out << " scale " << o.scale.x << " " << o.scale.y << " " << o.scale.z << "\n";
|
out << " scale " << o.scale.x << " " << o.scale.y << " " << o.scale.z << "\n";
|
||||||
|
|
@ -373,7 +374,7 @@ std::optional<Scene> parseScene(std::istream& in, const std::string& path, std::
|
||||||
size_t at = 0;
|
size_t at = 0;
|
||||||
l >> at;
|
l >> at;
|
||||||
o->folder = uint32_t(at); // (a place in the file for now)
|
o->folder = uint32_t(at); // (a place in the file for now)
|
||||||
} else if (word == "visible") l >> o->visible;
|
} else if (word == "cuts") l >> o->cuts; // (a place in the file for now) else if (word == "visible") l >> o->visible;
|
||||||
else if (word == "locked") l >> o->locked;
|
else if (word == "locked") l >> o->locked;
|
||||||
else if (word == "recipe") {
|
else if (word == "recipe") {
|
||||||
l >> o->recipe;
|
l >> o->recipe;
|
||||||
|
|
@ -422,9 +423,46 @@ std::optional<Scene> parseScene(std::istream& in, const std::string& path, std::
|
||||||
p = std::find_if(s.folders.begin(), s.folders.end(), [&](const Folder& g) { return g.id == p; })->parent;
|
p = std::find_if(s.folders.begin(), s.folders.end(), [&](const Folder& g) { return g.id == p; })->parent;
|
||||||
}
|
}
|
||||||
for (Object& o : s.objects) o.folder = idAt(o.folder);
|
for (Object& o : s.objects) o.folder = idAt(o.folder);
|
||||||
|
for (Object& o : s.objects) o.cuts = o.cuts >= 1 && o.cuts <= s.objects.size() && &s.objects[o.cuts - 1] != &o ? s.objects[o.cuts - 1].id : 0u;
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Object* cutTarget(const Scene& s, const Object& cutter) {
|
||||||
|
if (!cutter.cuts || cutter.cuts == cutter.id) return nullptr;
|
||||||
|
for (const Object& o : s.objects)
|
||||||
|
if (o.id == cutter.cuts) return &o;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
bool hasCuts(const Scene& s, const Object& o) {
|
||||||
|
for (const Object& c : s.objects)
|
||||||
|
if (c.cuts == o.id && c.id != o.id && !c.mesh.faces.empty()) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Mesh cutMesh(const Scene& s, const Object& o, bool mirrorOnly) {
|
||||||
|
Mesh m = o.hasFinish() ? finishedMesh(o, mirrorOnly) : o.mesh;
|
||||||
|
const glm::mat4 into = glm::inverse(objectMatrix(o));
|
||||||
|
for (const Object& c : s.objects) {
|
||||||
|
if (c.cuts != o.id || c.id == o.id || c.mesh.faces.empty()) continue;
|
||||||
|
Mesh cutter = c.hasFinish() ? finishedMesh(c) : c.mesh;
|
||||||
|
transform(cutter, into * objectMatrix(c));
|
||||||
|
if (Mesh cut = m; combine(cut, cutter, Combine::Subtract)) m = std::move(cut); // (one that takes everything, or fails: left out)
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
Scene withCutsMade(const Scene& s) {
|
||||||
|
Scene out = s;
|
||||||
|
for (size_t i = 0; i < out.objects.size(); ++i) {
|
||||||
|
Object& o = out.objects[i];
|
||||||
|
if (hasCuts(s, s.objects[i])) {
|
||||||
|
o.mesh = cutMesh(s, s.objects[i]);
|
||||||
|
o.mirror = 0, o.round = 0, o.smoothLevels = 0, o.creases.clear();
|
||||||
|
o.uvs.clear(); // (its painting doesn't fit the new faces)
|
||||||
|
}
|
||||||
|
if (cutTarget(s, s.objects[i])) o.visible = false;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// ── glTF ──
|
// ── glTF ──
|
||||||
|
|
||||||
bool exportGltf(const std::string& path, const Scene& scene, const std::vector<std::vector<uint8_t>>& pngs, bool binary, std::string* error) {
|
bool exportGltf(const std::string& path, const Scene& scene, const std::vector<std::vector<uint8_t>>& pngs, bool binary, std::string* error) {
|
||||||
|
|
|
||||||
11
src/scene.h
11
src/scene.h
|
|
@ -66,6 +66,9 @@ struct Object {
|
||||||
int smoothLevels = 0;
|
int smoothLevels = 0;
|
||||||
bool smoothKeepSharp = false;
|
bool smoothKeepSharp = false;
|
||||||
std::set<std::pair<int, int>> creases; // edges (points, the lower first) smoothing keeps sharp
|
std::set<std::pair<int, int>> creases; // edges (points, the lower first) smoothing keeps sharp
|
||||||
|
// An editable cut: this object is taken out of that one (its id), and stays an object you can
|
||||||
|
// move and change (the hole follows); shown see-through, not exported or printed itself.
|
||||||
|
uint32_t cuts = 0;
|
||||||
bool hasFinish() const { return mirror || round > 0.0f || smoothLevels > 0; }
|
bool hasFinish() const { return mirror || round > 0.0f || smoothLevels > 0; }
|
||||||
// A painted picture on it: where each face corner sits on it (0..1), and the picture
|
// A painted picture on it: where each face corner sits on it (0..1), and the picture
|
||||||
// (shared with the undo steps until it's painted on again). Saved beside the .fry as a PNG.
|
// (shared with the undo steps until it's painted on again). Saved beside the .fry as a PNG.
|
||||||
|
|
@ -138,6 +141,14 @@ struct Scene {
|
||||||
uint32_t nextId = 1;
|
uint32_t nextId = 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Editable cuts: whether something's cut out of it, and it with them cut out (and its finishing
|
||||||
|
// touches; only the mirror with mirrorOnly) in its own space. The scene with each cut made for
|
||||||
|
// good and its cutters hidden, for exporting.
|
||||||
|
const Object* cutTarget(const Scene& s, const Object& cutter); // (what it cuts, if it's there)
|
||||||
|
bool hasCuts(const Scene& s, const Object& o);
|
||||||
|
Mesh cutMesh(const Scene& s, const Object& o, bool mirrorOnly = false);
|
||||||
|
Scene withCutsMade(const Scene& s);
|
||||||
|
|
||||||
// Materials to start from: a colour (unless `keepColor`: only how metal and rough it is) and
|
// Materials to start from: a colour (unless `keepColor`: only how metal and rough it is) and
|
||||||
// how metal and rough. Shown in the view (Solid and Rendered) and exported with the object.
|
// how metal and rough. Shown in the view (Solid and Rendered) and exported with the object.
|
||||||
struct MaterialPreset {
|
struct MaterialPreset {
|
||||||
|
|
|
||||||
|
|
@ -709,6 +709,30 @@ int main() {
|
||||||
CHECK(!held.empty() && std::abs(boundsMax(g).z - 0.8f) < 0.01f && closed(g), "grab pulls the middle up with it (%.3f)", boundsMax(g).z);
|
CHECK(!held.empty() && std::abs(boundsMax(g).z - 0.8f) < 0.01f && closed(g), "grab pulls the middle up with it (%.3f)", boundsMax(g).z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{ // editable cuts: a box with a bar through it, the bar still an object
|
||||||
|
Scene sc;
|
||||||
|
Object box, bar;
|
||||||
|
box.id = 1, box.name = "Box", box.mesh = makeCube(2.0f);
|
||||||
|
bar.id = 2, bar.name = "Bar", bar.mesh = makeCube(1.0f), bar.scale = {3, 1, 1}, bar.cuts = 1;
|
||||||
|
sc.objects = {box, bar};
|
||||||
|
sc.nextId = 3;
|
||||||
|
CHECK(cutTarget(sc, sc.objects[1]) == &sc.objects[0] && hasCuts(sc, sc.objects[0]) && !hasCuts(sc, sc.objects[1]), "the bar cuts the box");
|
||||||
|
const float v = checkForPrint(cutMesh(sc, sc.objects[0])).volume;
|
||||||
|
CHECK(std::abs(v - 6.0f) < 1e-3f, "a 1x1 tunnel through a 2-box: 6 left (%.3f)", v);
|
||||||
|
sc.objects[1].position = {0, 0, 0.5f};
|
||||||
|
const float moved = checkForPrint(cutMesh(sc, sc.objects[0])).volume;
|
||||||
|
CHECK(std::abs(moved - 6.0f) < 1e-3f && boundsMax(cutMesh(sc, sc.objects[0])).z == 1.0f, "moved up, a slot at the top instead (%.3f)", moved);
|
||||||
|
const Scene made = withCutsMade(sc);
|
||||||
|
CHECK(!made.objects[1].visible && std::abs(checkForPrint(made.objects[0].mesh).volume - 6.0f) < 1e-3f, "exporting: the cut made, the bar hidden");
|
||||||
|
sc.objects[1].cuts = 2;
|
||||||
|
CHECK(!cutTarget(sc, sc.objects[1]) && !hasCuts(sc, sc.objects[0]), "cutting itself: no cut");
|
||||||
|
sc.objects[1].cuts = 1;
|
||||||
|
const auto file = (std::filesystem::temp_directory_path() / "fry3dx_test" / "cuts.fry").string();
|
||||||
|
std::filesystem::create_directories(std::filesystem::path(file).parent_path());
|
||||||
|
auto back = saveScene(file, sc) ? loadScene(file) : std::nullopt;
|
||||||
|
CHECK(back && back->objects.size() == 2 && back->objects[1].cuts == back->objects[0].id && back->objects[0].cuts == 0, "cuts saved and back (to the new ids)");
|
||||||
|
}
|
||||||
|
|
||||||
{ // copies along a path: a square's outline, a line, a cube's biggest face
|
{ // copies along a path: a square's outline, a line, a cube's biggest face
|
||||||
Mesh sq;
|
Mesh sq;
|
||||||
sq.verts = {{0, 0, 0}, {2, 0, 0}, {2, 2, 0}, {0, 2, 0}};
|
sq.verts = {{0, 0, 0}, {2, 0, 0}, {2, 2, 0}, {0, 2, 0}};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue