mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
0.7: cutting inside a cut no longer crashes (the boolean's tree kept splitting a sliver by its own plane, forever) and cuts come out closed (points near an edge's end set into it, small gaps filled, a nudged retry); hidden edges stay hidden on big or cut-up shapes (visibility through a screen grid, no "all seen" past a size; the selection outline too); every panel has a least size, kept when docked, and the first layout fits small windows; eye and padlock buttons in Layers; the tabs fit narrow windows; a failed tool keeps your redo; view commands at start-up no longer crash; tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ec67538be0
commit
c1991d9f73
7 changed files with 336 additions and 64 deletions
208
src/app.cpp
208
src/app.cpp
|
|
@ -316,6 +316,7 @@ public:
|
|||
}, "Fry3DX: fry_orbit yaw pitch [distance]");
|
||||
// A sculpt stroke across the view (0..1 of it), for pictures of it: brush 1-6, then from and to.
|
||||
static keel::Cmd sculptStroke("fry_sculpt", [](const keel::Args& a) {
|
||||
if (!self->viewReady()) return self->say("fry_sculpt: the view isn't up yet (+wait 1 first)");
|
||||
if (a.size() < 6) return;
|
||||
const int brush = std::clamp(std::stoi(a[1]) - 1, 0, int(std::size(kSculptTools)) - 1);
|
||||
self->sculptBrush_ = kSculptTools[brush].brush;
|
||||
|
|
@ -390,6 +391,7 @@ public:
|
|||
};
|
||||
static auto vp = viewPoint;
|
||||
static keel::Cmd knife("fry_knife", [](const keel::Args& a) {
|
||||
if (!self->viewReady()) return self->say("fry_knife: the view isn't up yet (+wait 1 first)");
|
||||
if (a.size() < 5) return;
|
||||
if (a.size() > 5) self->knifeThrough_ = a[5] != "0";
|
||||
if (a.size() > 6) self->knifeAfter_ = std::stoi(a[6]);
|
||||
|
|
@ -432,6 +434,7 @@ public:
|
|||
static keel::Cmd panelsCmd("fry_panels", [](const keel::Args&) { self->panelsOpen_ = !self->panelsOpen_; }, "Fry3DX: phone layout: show / hide the panels");
|
||||
static keel::Cmd goCmd("fry_goto", [](const keel::Args&) { self->goTo(self->selected_); }, "Fry3DX: the camera to the picked");
|
||||
static keel::Cmd drawCmd("fry_draw", [](const keel::Args& a) { // fry_draw mode thick x y x y x y ... (view 0..1)
|
||||
if (!self->viewReady()) return self->say("fry_draw: the view isn't up yet (+wait 1 first)");
|
||||
if (a.size() < 9) return;
|
||||
self->setTool(Tool::Draw);
|
||||
self->drawOnMode_ = std::stoi(a[1]), self->drawThickness_ = std::stof(a[2]);
|
||||
|
|
@ -469,6 +472,7 @@ public:
|
|||
if (a.size() > 3) self->paintColor_ = {std::stof(a[1]), std::stof(a[2]), std::stof(a[3]), 1.0f};
|
||||
}, "Fry3DX: fry_color r g b (painting, 0..1)");
|
||||
static keel::Cmd stroke("fry_stroke", [](const keel::Args& a) {
|
||||
if (!self->viewReady()) return self->say("fry_stroke: the view isn't up yet (+wait 1 first)");
|
||||
if (a.size() < 5) return;
|
||||
const ImVec2 p0 = vp(a[1], a[2]), p1 = vp(a[3], a[4]);
|
||||
for (int i = 0; i <= 40; ++i) self->paintAt(ImVec2(p0.x + (p1.x - p0.x) * float(i) / 40, p0.y + (p1.y - p0.y) * float(i) / 40), i == 0);
|
||||
|
|
@ -488,6 +492,7 @@ public:
|
|||
if (const int i = self->owner_[o->id][size_t(y) * size_t(o->picture->w) + size_t(x)]; i > 0) self->unlockLayer(o->id, i - 1);
|
||||
}, "Fry3DX: fry_unlock x y: click the Picture panel there (picture pixels)");
|
||||
static keel::Cmd moveSticker("fry_sticker", [](const keel::Args& a) {
|
||||
if (!self->viewReady()) return self->say("fry_sticker: the view isn't up yet (+wait 1 first)");
|
||||
Object* o = self->find(self->ledit_.object);
|
||||
PictureLayer* l = self->editLayer();
|
||||
if (!o || !l || a.size() < 3) return;
|
||||
|
|
@ -527,6 +532,7 @@ public:
|
|||
if (a[1] == "middle") self->middleOfBed();
|
||||
if (a[1] == "printer" && a.size() > 2) keel::execute("fry_printer " + a[2]);
|
||||
if (a[1] == "size" && a.size() > 3) self->pushUndo(), self->printResize(std::stoi(a[2]), std::stof(a[3]));
|
||||
if ((a[1] == "measure" || a[1] == "flat") && !self->viewReady()) return self->say("fry_print: the view isn't up yet (+wait 1 first)");
|
||||
if (a[1] == "measure" && a.size() > 5) {
|
||||
self->printTool_ = PrintTool::Measure, self->measure_.clear();
|
||||
for (int i = 0; i < 2; ++i)
|
||||
|
|
@ -638,6 +644,7 @@ public:
|
|||
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);
|
||||
if (!touch()) keepMinSizes(ImGui::DockBuilderGetNode(dock));
|
||||
if (ImGuiDockNode* c = ImGui::DockBuilderGetCentralNode(dock)) viewMin_ = c->Pos, viewMax_ = ImVec2(c->Pos.x + c->Size.x, c->Pos.y + c->Size.y);
|
||||
updateCamera(io);
|
||||
menuBar();
|
||||
|
|
@ -681,6 +688,7 @@ private:
|
|||
std::set<std::pair<int, int>> edges;
|
||||
};
|
||||
std::vector<Snapshot> undo_, redo_;
|
||||
std::vector<Snapshot> redoKept_; // (what redo had before the last undo point: back if that step is dropped)
|
||||
|
||||
Mode mode_ = Mode::Object;
|
||||
Tool tool_ = Tool::Move;
|
||||
|
|
@ -966,9 +974,20 @@ private:
|
|||
void pushUndo() {
|
||||
undo_.push_back({scene_, selected_, active_, mode_, selVerts_, selFaces_, selEdges_});
|
||||
if (undo_.size() > 200) undo_.erase(undo_.begin());
|
||||
redo_.clear();
|
||||
redoKept_ = std::move(redo_), redo_.clear();
|
||||
dirty_ = true, ++edits_;
|
||||
}
|
||||
// The undo point just made, dropped (what it was for didn't happen): redo as it was before.
|
||||
void dropUndo() {
|
||||
if (!undo_.empty()) undo_.pop_back();
|
||||
redo_ = std::move(redoKept_), redoKept_.clear();
|
||||
}
|
||||
// Back to the undo point just made, and it dropped.
|
||||
void takeBack() {
|
||||
if (undo_.empty()) return;
|
||||
restore(undo_.back());
|
||||
dropUndo();
|
||||
}
|
||||
void restore(Snapshot& s) {
|
||||
// (Not the scripts: they're edited like text, not undone with the model.)
|
||||
std::vector<fry::SceneScript> scripts = std::move(scene_.scripts);
|
||||
|
|
@ -984,12 +1003,14 @@ private:
|
|||
redo_.push_back({scene_, selected_, active_, mode_, selVerts_, selFaces_, selEdges_});
|
||||
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_});
|
||||
restore(redo_.back());
|
||||
redo_.pop_back();
|
||||
redoKept_.clear();
|
||||
}
|
||||
|
||||
void newScene() {
|
||||
|
|
@ -1689,7 +1710,7 @@ private:
|
|||
pushUndo();
|
||||
const auto made = loopCut(o.mesh, edge, cuts_);
|
||||
if (made.empty()) {
|
||||
undo_.pop_back();
|
||||
dropUndo();
|
||||
return say("Nothing to cut there: loop cuts go across four-sided faces");
|
||||
}
|
||||
changedShape(o);
|
||||
|
|
@ -1707,7 +1728,7 @@ private:
|
|||
pushUndo();
|
||||
const glm::vec3 towards = glm::inverse(glm::mat3(objectMatrix(*o))) * (eye_ - pivotOfSelection());
|
||||
if (!makeFace(o->mesh, std::vector<int>(pts.begin(), pts.end()), towards)) {
|
||||
undo_.pop_back();
|
||||
dropUndo();
|
||||
return say("Pick 3 or more points (not in a line) to make a face");
|
||||
}
|
||||
changedShape(*o);
|
||||
|
|
@ -1771,7 +1792,7 @@ private:
|
|||
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)");
|
||||
if (made.empty()) return takeBack(), 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()));
|
||||
|
|
@ -1889,7 +1910,7 @@ private:
|
|||
pushUndo();
|
||||
const auto made = knifeCut(o->mesh, eye, path, knifeThrough_);
|
||||
if (made.empty()) {
|
||||
undo_.pop_back();
|
||||
dropUndo();
|
||||
return say(knifeThrough_ ? "The line didn't cross the shape" : "The line didn't cross any face you can see (Tool options: cut right through)");
|
||||
}
|
||||
clearElements();
|
||||
|
|
@ -1948,7 +1969,7 @@ private:
|
|||
holeTall_ *= across * scale / std::max(holeSize_, 1e-6f), holeSize_ = across * scale;
|
||||
if (holeMode_ == 2) { // just the outline: a new face, picked to extrude, bevel or paint
|
||||
const int made = imprintOutline(o->mesh, face, outline);
|
||||
if (made < 0) return undo_.pop_back(), say("The outline has to fit inside the face");
|
||||
if (made < 0) return dropUndo(), say("The outline has to fit inside the face");
|
||||
changedShape(*o);
|
||||
selected_ = {id}, active_ = id;
|
||||
if (mode_ != Mode::Edit) enterEdit();
|
||||
|
|
@ -1956,7 +1977,7 @@ private:
|
|||
return say(std::format("{} outline cut, {:.1f} mm across: its new face is picked (Extrude, Bevel, or paint it)", names[holeShape_], holeSize_ * 1000));
|
||||
}
|
||||
if (!cutHole(o->mesh, face, outline, holeMode_ == 0 ? 0.0f : holeDepth_ / scale)) {
|
||||
undo_.pop_back();
|
||||
dropUndo();
|
||||
return say(holeMode_ == 0 ? "The hole has to fit inside the face, and inside the face behind it (it goes through)" : "The hole has to fit inside the face");
|
||||
}
|
||||
changedShape(*o);
|
||||
|
|
@ -3270,6 +3291,16 @@ private:
|
|||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Delete", "Del")) selected_ = ids, deleteSelected();
|
||||
}
|
||||
// A row's show / hide or lock button: the picture of how it is now, dim when it's the usual way.
|
||||
bool rowToggle(const char* id, Icon icon, bool usual, const char* tip) {
|
||||
const ImVec2 at = ImGui::GetCursorScreenPos();
|
||||
const float h = ImGui::GetFrameHeight() - 4;
|
||||
const bool pressed = ImGui::InvisibleButton(id, ImVec2(h, h));
|
||||
const ImU32 col = ImGui::IsItemHovered() ? kHot : usual ? IM_COL32(200, 200, 205, 150) : ImGui::GetColorU32(accentText());
|
||||
drawIcon(ImGui::GetWindowDrawList(), icon, ImVec2(at.x + h * 0.1f, at.y + h * 0.1f), h * 0.8f, col);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tip);
|
||||
return pressed;
|
||||
}
|
||||
// The ⌖ button at the end of a row: brings the camera to it.
|
||||
bool goButton(const char* tip) {
|
||||
const ImVec2 at = ImGui::GetCursorScreenPos();
|
||||
|
|
@ -3300,11 +3331,9 @@ private:
|
|||
ImGui::PushID(int(id) + 1000000);
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::SmallButton(f->visible ? "o" : "-")) showFolder(*f, !f->visible);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip(f->visible ? "Hide all in it" : "Show all in it");
|
||||
if (rowToggle("##eye", f->visible ? Icon::Eye : Icon::EyeShut, f->visible, f->visible ? "Hide all in it" : "Show all in it")) showFolder(*f, !f->visible);
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::SmallButton(f->locked ? "L" : " ")) lockFolder(*f, !f->locked);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip(f->locked ? "Unlock all in it" : "Lock all in it");
|
||||
if (rowToggle("##lock", f->locked ? Icon::Lock : Icon::Unlock, !f->locked, f->locked ? "Unlock all in it" : "Lock all in it")) lockFolder(*f, !f->locked);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Indent(indent + 1);
|
||||
if (ImGui::ArrowButton("##open", f->open ? ImGuiDir_Down : ImGuiDir_Right)) f->open = !f->open;
|
||||
|
|
@ -3380,11 +3409,9 @@ private:
|
|||
ImGui::PushID(int(o.id));
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::SmallButton(o.visible ? "o" : "-")) o.visible = !o.visible, dirty_ = true;
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip(o.visible ? "Shown (click to hide)" : "Hidden (click to show)");
|
||||
if (rowToggle("##eye", o.visible ? Icon::Eye : Icon::EyeShut, o.visible, o.visible ? "Shown (click to hide)" : "Hidden (click to show)")) o.visible = !o.visible, dirty_ = true;
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::SmallButton(o.locked ? "L" : " ")) o.locked = !o.locked, dirty_ = true;
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip(o.locked ? "Locked: can't be moved or changed" : "Click to lock");
|
||||
if (rowToggle("##lock", o.locked ? Icon::Lock : Icon::Unlock, !o.locked, o.locked ? "Locked: can't be moved or changed (click to unlock)" : "Click to lock")) o.locked = !o.locked, dirty_ = true;
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Indent(indent + 1);
|
||||
if (renaming_ == o.id) {
|
||||
|
|
@ -3441,8 +3468,7 @@ private:
|
|||
Mesh m = b.hasFinish() ? finishedMesh(b) : b.mesh;
|
||||
transform(m, into * objectMatrix(b));
|
||||
if (!combine(result, m, op)) {
|
||||
undo();
|
||||
redo_.clear();
|
||||
takeBack();
|
||||
return say(op == Combine::Overlap ? "They don't overlap: nothing would be left" : "That didn't leave anything");
|
||||
}
|
||||
++used;
|
||||
|
|
@ -3774,6 +3800,8 @@ private:
|
|||
}
|
||||
|
||||
// World point → screen (false behind the eye).
|
||||
// The window and its view are up (commands given at the start run before the first frame).
|
||||
bool viewReady() const { return ImGui::GetCurrentContext() && viewMax_.x - viewMin_.x > 1 && viewMax_.y - viewMin_.y > 1; }
|
||||
bool project(const glm::vec3& p, ImVec2& out) const {
|
||||
const glm::vec4 c = vp_ * glm::vec4(p, 1.0f);
|
||||
if (c.w < 1e-4f) return false;
|
||||
|
|
@ -3839,38 +3867,64 @@ private:
|
|||
return h.t > len - std::max(len * 0.002f, 0.0005f);
|
||||
}
|
||||
// Which of an object's points the eye can see, worked out again only when it or the view
|
||||
// changes. (Very big meshes: all, rather than slow.)
|
||||
// changes. The triangles are sorted into a grid on the screen first: a point is tried only
|
||||
// against those over its own square (what could be between it and the eye).
|
||||
struct Visibility {
|
||||
uint64_t version = 0;
|
||||
glm::vec3 eye{1e30f};
|
||||
glm::mat4 vp{0.0f};
|
||||
std::vector<char> point;
|
||||
};
|
||||
std::map<uint32_t, Visibility> visibility_;
|
||||
const std::vector<char>& visiblePoints(const Object& o) {
|
||||
Visibility& v = visibility_[o.id];
|
||||
std::map<std::pair<uint32_t, bool>, Visibility> visibility_; // (an object's own mesh, or the one shown)
|
||||
const std::vector<char>& visiblePoints(const Object& o) { return visiblePoints(o, o.mesh); }
|
||||
const std::vector<char>& visiblePoints(const Object& o, const Mesh& mesh) {
|
||||
Visibility& v = visibility_[{o.id, &mesh != &o.mesh}];
|
||||
const uint64_t version = versions_[o.id];
|
||||
if (v.version == version && glm::distance(v.eye, eye_) < 1e-6f && v.point.size() == o.mesh.verts.size()) return v.point;
|
||||
v.version = version, v.eye = eye_;
|
||||
v.point.assign(o.mesh.verts.size(), 1);
|
||||
if (v.version == version && glm::distance(v.eye, eye_) < 1e-6f && v.vp == vp_ && v.point.size() == mesh.verts.size()) return v.point;
|
||||
v.version = version, v.eye = eye_, v.vp = vp_;
|
||||
v.point.assign(mesh.verts.size(), 1);
|
||||
const glm::mat4 m = objectMatrix(o);
|
||||
std::vector<glm::vec3> w(o.mesh.verts.size());
|
||||
for (size_t i = 0; i < w.size(); ++i) w[i] = glm::vec3(m * glm::vec4(o.mesh.verts[i], 1.0f));
|
||||
std::vector<glm::vec3> w(mesh.verts.size());
|
||||
std::vector<glm::vec4> clip(w.size());
|
||||
for (size_t i = 0; i < w.size(); ++i) w[i] = glm::vec3(m * glm::vec4(mesh.verts[i], 1.0f)), clip[i] = vp_ * glm::vec4(w[i], 1.0f);
|
||||
std::vector<std::array<int, 3>> tris;
|
||||
for (size_t f = 0; f < o.mesh.faces.size(); ++f)
|
||||
for (const auto& t : triangulate(o.mesh, int(f))) tris.push_back(t);
|
||||
if (w.size() * tris.size() > 4'000'000) return v.point;
|
||||
for (size_t f = 0; f < mesh.faces.size(); ++f)
|
||||
for (const auto& t : triangulate(mesh, int(f))) tris.push_back(t);
|
||||
constexpr int G = 64;
|
||||
auto cell = [](float ndc) { return std::clamp(int((ndc * 0.5f + 0.5f) * G), 0, G - 1); };
|
||||
std::vector<std::vector<int>> grid(G * G);
|
||||
std::vector<int> everywhere; // (reaching behind the eye: in every square)
|
||||
for (size_t t = 0; t < tris.size(); ++t) {
|
||||
glm::vec2 lo(1e30f), hi(-1e30f);
|
||||
bool behind = false;
|
||||
for (int k : tris[t]) {
|
||||
const glm::vec4 c = clip[size_t(k)];
|
||||
if (c.w < 1e-5f) { behind = true; break; }
|
||||
lo = glm::min(lo, glm::vec2(c) / c.w), hi = glm::max(hi, glm::vec2(c) / c.w);
|
||||
}
|
||||
if (behind) { everywhere.push_back(int(t)); continue; }
|
||||
if (hi.x < -1 || hi.y < -1 || lo.x > 1 || lo.y > 1) continue; // (off the screen: covers nothing on it)
|
||||
for (int y = cell(lo.y); y <= cell(hi.y); ++y)
|
||||
for (int x = cell(lo.x); x <= cell(hi.x); ++x) grid[size_t(y * G + x)].push_back(int(t));
|
||||
}
|
||||
for (size_t i = 0; i < w.size(); ++i) {
|
||||
const glm::vec4 c = clip[i];
|
||||
if (c.w < 1e-5f) continue; // (behind the eye: not drawn anyway)
|
||||
const glm::vec2 n = glm::vec2(c) / c.w;
|
||||
if (std::abs(n.x) > 1 || std::abs(n.y) > 1) continue;
|
||||
const glm::vec3 d = w[i] - eye_;
|
||||
const float len = glm::length(d);
|
||||
const Ray ray{eye_, d / len};
|
||||
const float limit = len - std::max(len * 0.002f, 0.0005f);
|
||||
for (const auto& t : tris) {
|
||||
if (t[0] == int(i) || t[1] == int(i) || t[2] == int(i)) continue;
|
||||
if (auto hit = hitTriangle(ray, w[size_t(t[0])], w[size_t(t[1])], w[size_t(t[2])]); hit && *hit < limit) {
|
||||
v.point[i] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto hides = [&](int t) {
|
||||
const auto& tri = tris[size_t(t)];
|
||||
if (tri[0] == int(i) || tri[1] == int(i) || tri[2] == int(i)) return false;
|
||||
auto hit = hitTriangle(ray, w[size_t(tri[0])], w[size_t(tri[1])], w[size_t(tri[2])]);
|
||||
return hit && *hit < limit;
|
||||
};
|
||||
if (std::any_of(everywhere.begin(), everywhere.end(), hides)) { v.point[i] = 0; continue; }
|
||||
const auto& here = grid[size_t(cell(n.y) * G + cell(n.x))];
|
||||
if (std::any_of(here.begin(), here.end(), hides)) v.point[i] = 0;
|
||||
}
|
||||
return v.point;
|
||||
}
|
||||
|
|
@ -4281,7 +4335,7 @@ private:
|
|||
void cancelDrag() {
|
||||
if (!drag_.active) return;
|
||||
scene_ = drag_.before;
|
||||
undo_.pop_back();
|
||||
dropUndo();
|
||||
touchAll();
|
||||
drag_.active = false;
|
||||
}
|
||||
|
|
@ -4461,16 +4515,16 @@ private:
|
|||
Mesh m = extrudeOutline(local, n, thick);
|
||||
drawPts_.clear();
|
||||
if (m.faces.empty()) return say("That shape doesn't work (points on top of each other?)");
|
||||
if (onto && drawOnMode_ > 0 && find(onto)->locked) return say("That object is locked");
|
||||
pushUndo();
|
||||
if (onto && drawOnMode_ > 0) {
|
||||
Object* o = find(onto);
|
||||
if (o->locked) return say("That object is locked");
|
||||
if (o->hasFinish()) o->mesh = finishedMesh(*o), o->mirror = 0, o->round = 0, o->smoothLevels = 0;
|
||||
Mesh shape = m;
|
||||
transform(shape, glm::inverse(objectMatrix(*o)) * glm::translate(glm::mat4(1.0f), c));
|
||||
Mesh result = o->mesh;
|
||||
if (!combine(result, shape, drawOnMode_ == 2 ? Combine::Subtract : Combine::Add)) {
|
||||
undo(), redo_.clear();
|
||||
takeBack();
|
||||
return say("That would leave nothing");
|
||||
}
|
||||
o = find(onto);
|
||||
|
|
@ -5047,8 +5101,10 @@ private:
|
|||
s = (s | facing) + 4;
|
||||
}
|
||||
}
|
||||
const std::vector<char>* vis = xray_ ? nullptr : &visiblePoints(o, mesh); // (not the edges of a pocket behind its rim)
|
||||
for (auto [e, s] : side) {
|
||||
const bool silhouette = (s & 3) == 3, open = (s >> 2) == 1 && (s & 1);
|
||||
if (vis && !((*vis)[size_t(e.first)] && (*vis)[size_t(e.second)])) continue;
|
||||
ImVec2 a, b;
|
||||
if ((silhouette || open) && project(w[size_t(e.first)], a) && project(w[size_t(e.second)], b)) dl->AddLine(a, b, col, width);
|
||||
}
|
||||
|
|
@ -5301,6 +5357,44 @@ private:
|
|||
ImGui::GetStyle().FontScaleDpi = 0.92f; // (the look itself: keel/ui, Niveus)
|
||||
}
|
||||
|
||||
// The least a panel can be and still read right (its words, its rows), by the font's size.
|
||||
static ImVec2 panelMin(std::string_view name) {
|
||||
const float f = ImGui::GetFontSize();
|
||||
if (name == "Toolbox") return {15.5f * f, 9 * f};
|
||||
if (name == "Tool options") return {15.5f * f, 8 * f};
|
||||
if (name == "Properties") return {23 * f, 12 * f};
|
||||
if (name == "Layers") return {20 * f, 8 * f};
|
||||
if (name == "Picture") return {18 * f, 14 * f};
|
||||
if (name == "Script") return {30 * f, 16 * f};
|
||||
return {0, 0};
|
||||
}
|
||||
// Docked panels kept at their least size: where a split leaves one side smaller, the split
|
||||
// moves (when the other side has room to give). ImGui's own splitters know only one least
|
||||
// size for everything. Gives what the node needs.
|
||||
ImVec2 keepMinSizes(ImGuiDockNode* n) {
|
||||
if (!n) return {0, 0};
|
||||
if (n->IsLeafNode()) {
|
||||
const float f = ImGui::GetFontSize();
|
||||
ImVec2 need = n->IsCentralNode() ? ImVec2(16 * f, 12 * f) : ImVec2(0, 0);
|
||||
for (ImGuiWindow* w : n->Windows) need = ImMax(need, panelMin(w->Name));
|
||||
if (need.y > 0 && !n->IsCentralNode()) need.y += ImGui::GetFrameHeight(); // (its tab)
|
||||
return need;
|
||||
}
|
||||
ImGuiDockNode *a = n->ChildNodes[0], *b = n->ChildNodes[1];
|
||||
const ImVec2 na = keepMinSizes(a), nb = keepMinSizes(b);
|
||||
if (!a->IsVisible) return nb;
|
||||
if (!b->IsVisible) return na;
|
||||
const int axis = n->SplitAxis;
|
||||
const float gap = ImGui::GetStyle().DockingSeparatorSize, room = n->Size[axis] - gap;
|
||||
float sa = a->Size[axis];
|
||||
if (sa < na[axis] && room - na[axis] >= nb[axis]) sa = na[axis];
|
||||
else if (room - sa < nb[axis] && room - nb[axis] >= na[axis]) sa = room - nb[axis];
|
||||
if (sa != a->Size[axis]) a->SizeRef[axis] = sa, b->SizeRef[axis] = room - sa;
|
||||
ImVec2 need = ImMax(na, nb);
|
||||
need[axis] = na[axis] + nb[axis] + gap;
|
||||
return need;
|
||||
}
|
||||
|
||||
void defaultLayout(ImGuiID dock) {
|
||||
layoutDone_ = true;
|
||||
ImGuiDockNode* node = ImGui::DockBuilderGetNode(dock);
|
||||
|
|
@ -5328,9 +5422,12 @@ private:
|
|||
ImGui::DockBuilderDockWindow("Picture", right);
|
||||
return ImGui::DockBuilderFinish(dock);
|
||||
}
|
||||
ImGuiID left = ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.14f, nullptr, ¢er);
|
||||
// (shares of the window, but never less than the panels' least sizes)
|
||||
const float wide = ImGui::GetMainViewport()->WorkSize.x, leftShare = std::min(0.3f, std::max(0.14f, (panelMin("Toolbox").x + 8) / wide));
|
||||
const float rightShare = std::min(0.45f, std::max(0.22f, (panelMin("Properties").x + 8) / (wide * (1 - leftShare))));
|
||||
ImGuiID left = ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, leftShare, nullptr, ¢er);
|
||||
const ImGuiID leftBottom = ImGui::DockBuilderSplitNode(left, ImGuiDir_Down, 0.5f, nullptr, &left);
|
||||
ImGuiID right = ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.22f, nullptr, ¢er);
|
||||
ImGuiID right = ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, rightShare, nullptr, ¢er);
|
||||
const ImGuiID rightBottom = ImGui::DockBuilderSplitNode(right, ImGuiDir_Down, 0.66f, nullptr, &right);
|
||||
const ImGuiID bottom = ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, 0.04f, nullptr, ¢er);
|
||||
const ImGuiID picture = ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.45f, nullptr, ¢er); // (the Paint tab's)
|
||||
|
|
@ -5517,22 +5614,27 @@ private:
|
|||
menus();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
// The tabs: what you're doing.
|
||||
const std::string file = (path_.empty() ? "Untitled" : fileName(path_)) + (dirty_ ? " *" : "");
|
||||
// The tabs: what you're doing (as wide as there's room for between the menus and the file's
|
||||
// name; the pictures alone when it's tight).
|
||||
{
|
||||
const char* names[] = {"Model", "Paint", "Layout", "Render", "Print"};
|
||||
const Icon icons[] = {Icon::Objects, Icon::PaintBrush, Icon::Layout, Icon::Render, Icon::Print};
|
||||
const char* tips[] = {"Make the shape (Ctrl+1)", "Paint pictures onto it (Ctrl+2)", "Change how its picture is laid out: move, turn and size its frames (Ctrl+3)",
|
||||
"Light it and save pictures of it (Ctrl+4)", "Get it ready for a 3D printer: size, checks, the printer's plate (Ctrl+5)"};
|
||||
const float tw = touch() ? 72.0f : 92.0f;
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX() + 20.0f, (ImGui::GetWindowWidth() - tw * 5 - 16) * 0.5f));
|
||||
const float h = ImGui::GetFrameHeight(), from = ImGui::GetCursorPosX() + 20.0f;
|
||||
const float room = ImGui::GetWindowWidth() - from - (touch() ? 8.0f : ImGui::CalcTextSize(file.c_str()).x + 32.0f);
|
||||
float tw = std::min(touch() ? 72.0f : 92.0f, room / 5 - 4);
|
||||
const bool words = tw >= h + 2 + ImGui::CalcTextSize("Layout").x + 6;
|
||||
if (!words) tw = h + 4;
|
||||
ImGui::SetCursorPosX(std::max(from, std::min((ImGui::GetWindowWidth() - (tw + 4) * 5) * 0.5f, from + room - (tw + 4) * 5)));
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
if (i) ImGui::SameLine(0, 4);
|
||||
ImGui::PushID(i);
|
||||
if (wideButton("##tab", icons[i], names[i], int(tab_) == i, tw, tips[i], ImGui::GetFrameHeight())) setTab(Tab(i));
|
||||
if (wideButton("##tab", icons[i], words ? names[i] : "", int(tab_) == i, tw, words ? tips[i] : std::format("{}: {}", names[i], tips[i]).c_str(), h)) setTab(Tab(i));
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
const std::string file = (path_.empty() ? "Untitled" : fileName(path_)) + (dirty_ ? " *" : "");
|
||||
if (touch()) return ImGui::EndMainMenuBar(); // (no room on a phone)
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::CalcTextSize(file.c_str()).x - 16);
|
||||
ImGui::TextDisabled("%s", file.c_str());
|
||||
|
|
@ -5599,6 +5701,7 @@ private:
|
|||
|
||||
void toolbox() {
|
||||
if (std::exchange(showToolbox_, false)) ImGui::SetNextWindowFocus();
|
||||
ImGui::SetNextWindowSizeConstraints(panelMin("Toolbox"), ImVec2(FLT_MAX, FLT_MAX));
|
||||
ImGui::Begin("Toolbox", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
const float w = ImGui::GetContentRegionAvail().x;
|
||||
if (tab_ == Tab::Paint) {
|
||||
|
|
@ -5810,6 +5913,7 @@ private:
|
|||
ImGui::TextDisabled("(the wheel while dragging too)");
|
||||
}
|
||||
void toolOptions() {
|
||||
ImGui::SetNextWindowSizeConstraints(panelMin("Tool options"), ImVec2(FLT_MAX, FLT_MAX));
|
||||
ImGui::Begin("Tool options", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
const float w = ImGui::GetContentRegionAvail().x;
|
||||
if (tab_ == Tab::Paint) {
|
||||
|
|
@ -6017,6 +6121,7 @@ private:
|
|||
}
|
||||
|
||||
void layersPanel() {
|
||||
ImGui::SetNextWindowSizeConstraints(panelMin("Layers"), ImVec2(FLT_MAX, FLT_MAX));
|
||||
ImGui::Begin("Layers", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
// Along the top: a new folder, the picked ones into one.
|
||||
if (ImGui::SmallButton("+ Folder")) {
|
||||
|
|
@ -6042,7 +6147,7 @@ private:
|
|||
ImGui::PushID(int(ref.id));
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::SmallButton(ref.visible ? "o" : "-")) ref.visible = !ref.visible;
|
||||
if (rowToggle("##eye", ref.visible ? Icon::Eye : Icon::EyeShut, ref.visible, ref.visible ? "Shown (click to hide)" : "Hidden (click to show)")) ref.visible = !ref.visible;
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TableNextColumn();
|
||||
const std::string label = "Picture: " + stdfs::path(ref.path).filename().string();
|
||||
|
|
@ -6111,8 +6216,7 @@ private:
|
|||
PictureLayer& l = o->layers[size_t(i)];
|
||||
const bool on = ledit_.object == o->id && ledit_.index == i;
|
||||
ImGui::PushID(i);
|
||||
if (ImGui::SmallButton(l.visible ? "o" : "-")) pushUndo(), l.visible = !l.visible;
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip(l.visible ? "Hide it" : "Show it");
|
||||
if (rowToggle("##eye", l.visible ? Icon::Eye : Icon::EyeShut, l.visible, l.visible ? "Hide it" : "Show it")) pushUndo(), l.visible = !l.visible;
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Selectable(std::format("{}{}", l.name, on ? " (moving)" : l.sticker ? "" : " (flat)").c_str(), on, 0, ImVec2(w - 92, 0)))
|
||||
on ? lockLayer() : unlockLayer(o->id, i);
|
||||
|
|
@ -6197,6 +6301,7 @@ private:
|
|||
|
||||
// The Paint tab's picture: the selected object's, flat, to paint on and place pictures on.
|
||||
void picturePanel() {
|
||||
ImGui::SetNextWindowSizeConstraints(panelMin("Picture"), ImVec2(FLT_MAX, FLT_MAX));
|
||||
ImGui::Begin("Picture", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
Object* o = selected_.empty() ? nullptr : find(active_);
|
||||
if (!o || !o->picture) {
|
||||
|
|
@ -6314,6 +6419,7 @@ private:
|
|||
}
|
||||
|
||||
void propertiesPanel() {
|
||||
ImGui::SetNextWindowSizeConstraints(panelMin("Properties"), ImVec2(FLT_MAX, FLT_MAX));
|
||||
ImGui::Begin("Properties", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
const float w = ImGui::GetContentRegionAvail().x;
|
||||
if (tab_ == Tab::Paint) {
|
||||
|
|
@ -6707,8 +6813,7 @@ private:
|
|||
touchAll();
|
||||
if (r.ok && frame) frameAll();
|
||||
if (!r.ok) {
|
||||
undo();
|
||||
redo_.clear();
|
||||
takeBack();
|
||||
scriptLine(r.error, true);
|
||||
showScript_ = true;
|
||||
return say(name + " stopped (nothing it did was kept): " + r.error.substr(0, r.error.find('\n')));
|
||||
|
|
@ -6955,6 +7060,7 @@ private:
|
|||
const ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(ImVec2(vp->WorkPos.x + vp->WorkSize.x * 0.5f, vp->WorkPos.y + vp->WorkSize.y * 0.5f), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowSize(ImVec2(std::min(780.0f, vp->WorkSize.x * 0.55f), vp->WorkSize.y * 0.7f), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSizeConstraints(panelMin("Script"), ImVec2(FLT_MAX, FLT_MAX));
|
||||
if (!ImGui::Begin("Script", &showScript_)) return ImGui::End();
|
||||
|
||||
// The file's scripts (saved in the .fry with it).
|
||||
|
|
|
|||
133
src/boolean.cpp
133
src/boolean.cpp
|
|
@ -7,6 +7,7 @@
|
|||
#include <cmath>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
namespace fry {
|
||||
namespace {
|
||||
|
|
@ -96,18 +97,28 @@ struct Node {
|
|||
if (front) front->all(out);
|
||||
if (back) back->all(out);
|
||||
}
|
||||
void build(const std::vector<Poly>& in) {
|
||||
void build(const std::vector<Poly>& in, int depth = 0) {
|
||||
if (in.empty()) return;
|
||||
if (!hasPlane) hasPlane = true, n = in[0].n, w = in[0].w;
|
||||
size_t first = 0;
|
||||
if (!hasPlane) { // (the piece the plane is taken from stays here: a thin sliver's own corners can
|
||||
// be off its rounded plane, and split by it again and again, forever)
|
||||
hasPlane = true, n = in[0].n, w = in[0].w;
|
||||
polys.push_back(in[0]);
|
||||
first = 1;
|
||||
}
|
||||
if (depth > 3000) { // (a tree this deep is gone wrong: the rest kept here, not a stack overflow)
|
||||
polys.insert(polys.end(), in.begin() + long(first), in.end());
|
||||
return;
|
||||
}
|
||||
std::vector<Poly> f, b;
|
||||
for (const Poly& p : in) split(n, w, p, polys, polys, f, b);
|
||||
for (size_t i = first; i < in.size(); ++i) split(n, w, in[i], polys, polys, f, b);
|
||||
if (!f.empty()) {
|
||||
if (!front) front = std::make_unique<Node>();
|
||||
front->build(f);
|
||||
front->build(f, depth + 1);
|
||||
}
|
||||
if (!b.empty()) {
|
||||
if (!back) back = std::make_unique<Node>();
|
||||
back->build(b);
|
||||
back->build(b, depth + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -171,7 +182,7 @@ Mesh meshOf(const std::vector<Poly>& polys) {
|
|||
const int v = *it;
|
||||
if (v == a || v == b) continue;
|
||||
const float t = glm::dot(m.verts[size_t(v)] - pa, d) / len2;
|
||||
if (t <= 1e-4f || t >= 1 - 1e-4f) continue;
|
||||
if (const float in = 2e-6f / std::sqrt(len2); t <= in || t >= 1 - in) continue; // (a µm or two from its ends: on a long edge, a part of it)
|
||||
if (const glm::vec3 off = pa + d * t - m.verts[size_t(v)]; glm::dot(off, off) < 1e-10f) on.push_back({t, v});
|
||||
}
|
||||
std::sort(on.begin(), on.end());
|
||||
|
|
@ -206,13 +217,91 @@ void removeSpikes(Mesh& m) {
|
|||
std::erase_if(m.faces, [](const std::vector<int>& f) { return f.size() < 3; });
|
||||
}
|
||||
|
||||
} // namespace
|
||||
// Gaps the cutting leaves (a tenth of a mm, where one side's rounding cut a piece the other
|
||||
// didn't): an edge with no face back along it. A small loop of them, or a sliver of next to no
|
||||
// area, gets a face of its own; the shape is closed again.
|
||||
void closeSmallGaps(Mesh& m) {
|
||||
// Two faces back to back on the same corners (a wall of no thickness): both go.
|
||||
std::map<std::vector<int>, std::vector<size_t>> byCorners;
|
||||
auto key = [](std::vector<int> f) {
|
||||
std::rotate(f.begin(), std::min_element(f.begin(), f.end()), f.end());
|
||||
return f;
|
||||
};
|
||||
for (size_t i = 0; i < m.faces.size(); ++i) byCorners[key(m.faces[i])].push_back(i);
|
||||
std::vector<bool> drop(m.faces.size(), false);
|
||||
for (size_t i = 0; i < m.faces.size(); ++i) {
|
||||
if (drop[i]) continue;
|
||||
std::vector<int> r(m.faces[i].rbegin(), m.faces[i].rend());
|
||||
if (auto it = byCorners.find(key(r)); it != byCorners.end())
|
||||
for (size_t j : it->second)
|
||||
if (j != i && !drop[j]) {
|
||||
drop[i] = drop[j] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (size_t i = m.faces.size(); i-- > 0;)
|
||||
if (drop[i]) m.faces.erase(m.faces.begin() + long(i));
|
||||
|
||||
bool combine(Mesh& a, const Mesh& b, Combine op) {
|
||||
std::set<std::pair<int, int>> have;
|
||||
for (const auto& f : m.faces)
|
||||
for (size_t i = 0; i < f.size(); ++i) have.insert({f[i], f[(i + 1) % f.size()]});
|
||||
std::map<int, std::vector<int>> next; // (the open edges, a to b)
|
||||
for (auto [a, b] : have)
|
||||
if (!have.contains({b, a})) next[a].push_back(b);
|
||||
auto fill = [&](std::vector<int> loop) {
|
||||
if (loop.size() < 3) return;
|
||||
float around = 0.0f;
|
||||
glm::vec3 area(0.0f);
|
||||
for (size_t i = 0; i < loop.size(); ++i) {
|
||||
const glm::vec3 p = m.verts[size_t(loop[i])], q = m.verts[size_t(loop[(i + 1) % loop.size()])];
|
||||
around += glm::distance(p, q), area += glm::cross(p, q);
|
||||
}
|
||||
// A small one, or one next to flat: a piece the cutting lost. Else a real opening: left.
|
||||
const glm::vec3 n = glm::length(area) > 0.0f ? glm::normalize(area) : glm::vec3(0.0f);
|
||||
float bulge = 0.0f;
|
||||
for (int v : loop) bulge = std::max(bulge, std::abs(glm::dot(m.verts[size_t(v)] - m.verts[size_t(loop[0])], n)));
|
||||
if (around > 2e-3f && glm::length(area) > 2e-9f && bulge > 1e-4f) return;
|
||||
std::reverse(loop.begin(), loop.end());
|
||||
m.faces.push_back(loop);
|
||||
};
|
||||
while (!next.empty()) {
|
||||
std::vector<int> loop{next.begin()->first};
|
||||
while (!loop.empty() && loop.size() <= 64) {
|
||||
auto it = next.find(loop.back());
|
||||
if (it == next.end()) break; // (an open run, not a loop)
|
||||
const int b = it->second.back();
|
||||
it->second.pop_back();
|
||||
if (it->second.empty()) next.erase(it);
|
||||
// Back at a point already passed: the loop from there closes (two gaps meeting at a corner, a figure 8).
|
||||
if (auto at = std::find(loop.begin(), loop.end(), b); at != loop.end()) {
|
||||
fill(std::vector<int>(at, loop.end()));
|
||||
loop.erase(at + 1, loop.end());
|
||||
if (loop.size() == 1 && !next.contains(loop[0])) break;
|
||||
} else
|
||||
loop.push_back(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every edge has as many faces along it one way as the other (closed; edges where two parts
|
||||
// only touch count too).
|
||||
bool sealed(const Mesh& m) {
|
||||
std::map<std::pair<int, int>, int> e;
|
||||
for (const auto& f : m.faces)
|
||||
for (size_t i = 0; i < f.size(); ++i) {
|
||||
const int a = f[i], b = f[(i + 1) % f.size()];
|
||||
a < b ? ++e[{a, b}] : --e[{b, a}];
|
||||
}
|
||||
for (auto [k, n] : e)
|
||||
if (n) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
Mesh combineOnce(const Mesh& am, const Mesh& b, Combine op) {
|
||||
Node A, B;
|
||||
A.build(polysOf(a));
|
||||
A.build(polysOf(am));
|
||||
B.build(polysOf(b));
|
||||
if (!A.hasPlane || !B.hasPlane) return false;
|
||||
if (!A.hasPlane || !B.hasPlane) return {};
|
||||
std::vector<Poly> bp;
|
||||
switch (op) {
|
||||
case Combine::Add:
|
||||
|
|
@ -230,13 +319,35 @@ bool combine(Mesh& a, const Mesh& b, Combine op) {
|
|||
}
|
||||
std::vector<Poly> out;
|
||||
A.all(out);
|
||||
a = meshOf(out);
|
||||
Mesh a = meshOf(out);
|
||||
// Points the cutting left a hair apart (closer than 0.03 mm: nothing a printer or a model
|
||||
// could show) are one; else they fold tiny faces back over their neighbours.
|
||||
mergeByDistance(a, 3e-5f);
|
||||
removeSpikes(a);
|
||||
mergeFlatFaces(a);
|
||||
removeSpikes(a);
|
||||
closeSmallGaps(a);
|
||||
return a;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool combine(Mesh& a, const Mesh& b, Combine op) {
|
||||
Mesh out = combineOnce(a, b, op);
|
||||
// Closed going in but not coming out: the rounding went wrong somewhere. Again with the
|
||||
// other shape moved a few µm (nothing that shows), and the first that comes out closed kept.
|
||||
if (!out.faces.empty() && !sealed(out) && sealed(a) && sealed(b)) {
|
||||
static const glm::vec3 nudge[] = {{3e-6f, 5e-6f, 7e-6f}, {-6e-6f, 4e-6f, -3e-6f}, {5e-6f, -7e-6f, 4e-6f}};
|
||||
for (const glm::vec3& d : nudge) {
|
||||
Mesh moved = b;
|
||||
for (glm::vec3& v : moved.verts) v += d;
|
||||
if (Mesh again = combineOnce(a, moved, op); !again.faces.empty() && sealed(again)) {
|
||||
out = std::move(again);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
a = std::move(out);
|
||||
return !a.faces.empty();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -213,6 +213,24 @@ void drawIcon(ImDrawList* dl, Icon icon, ImVec2 o, float s, ImU32 c) {
|
|||
line(0.62f, 0.34f, 0.84f, 0.16f, t * 0.8f), tri(0.92f, 0.08f, 0.78f, 0.12f, 0.88f, 0.22f);
|
||||
break;
|
||||
}
|
||||
case Icon::Eye: case Icon::EyeShut: // an eye: open, or shut with its lashes
|
||||
if (icon == Icon::Eye) {
|
||||
dl->AddBezierQuadratic(P(0.04f, 0.5f), P(0.5f, 0.02f), P(0.96f, 0.5f), c, t);
|
||||
dl->AddBezierQuadratic(P(0.04f, 0.5f), P(0.5f, 0.98f), P(0.96f, 0.5f), c, t);
|
||||
dot(0.5f, 0.5f, 0.15f);
|
||||
} else {
|
||||
dl->AddBezierQuadratic(P(0.04f, 0.42f), P(0.5f, 0.86f), P(0.96f, 0.42f), c, t);
|
||||
line(0.26f, 0.60f, 0.18f, 0.76f), line(0.5f, 0.66f, 0.5f, 0.84f), line(0.74f, 0.60f, 0.82f, 0.76f);
|
||||
}
|
||||
break;
|
||||
case Icon::Lock: case Icon::Unlock: { // a padlock: the shackle down in it, or up and open
|
||||
dl->AddRectFilled(P(0.24f, 0.46f), P(0.76f, 0.88f), c, s * 0.08f);
|
||||
const float lift = icon == Icon::Unlock ? 0.12f : 0.0f;
|
||||
dl->PathArcTo(P(0.5f, 0.38f - lift), 0.17f * s, 3.14159f, 3.14159f * 2.0f, 12);
|
||||
dl->PathStroke(c, 0, t);
|
||||
line(0.33f, 0.38f - lift, 0.33f, 0.47f - (lift > 0 ? lift : 0)), line(0.67f, 0.38f - lift, 0.67f, 0.47f);
|
||||
break;
|
||||
}
|
||||
case Icon::LayFlat:
|
||||
quad(0.22f, 0.40f, 0.62f, 0.22f, 0.86f, 0.50f, 0.46f, 0.68f, false, c);
|
||||
line(0.10f, 0.88f, 0.90f, 0.88f);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
namespace fry {
|
||||
|
||||
enum class Icon { Select, Move, Rotate, Scale, Draw, Extrude, Inset, Bevel, LoopCut, Objects, Shape, Points, Edges, Faces, Box, Brush, Lasso, Add, Knife, Hole, PaintBrush, Eraser, Fill, Picker, Picture, Render, Layout, Print, Ruler, LayFlat, Sculpt, Inflate, Smooth, Flatten, Pinch, Grab };
|
||||
enum class Icon { Select, Move, Rotate, Scale, Draw, Extrude, Inset, Bevel, LoopCut, Objects, Shape, Points, Edges, Faces, Box, Brush, Lasso, Add, Knife, Hole, PaintBrush, Eraser, Fill, Picker, Picture, Render, Layout, Print, Ruler, LayFlat, Sculpt, Inflate, Smooth, Flatten, Pinch, Grab, Eye, EyeShut, Lock, Unlock };
|
||||
|
||||
void drawIcon(ImDrawList* dl, Icon icon, ImVec2 topLeft, float size, ImU32 color);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue