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
|
|
@ -1,5 +1,5 @@
|
||||||
cmake_minimum_required(VERSION 3.25)
|
cmake_minimum_required(VERSION 3.25)
|
||||||
project(Fry3DX VERSION 0.6.0 LANGUAGES C CXX)
|
project(Fry3DX VERSION 0.7.0 LANGUAGES C CXX)
|
||||||
set(CMAKE_CXX_SCAN_FOR_MODULES OFF) # (no C++ modules; the scan can pick up a different clang than the NDK's)
|
set(CMAKE_CXX_SCAN_FOR_MODULES OFF) # (no C++ modules; the scan can pick up a different clang than the NDK's)
|
||||||
if(UNIX AND NOT APPLE)
|
if(UNIX AND NOT APPLE)
|
||||||
add_link_options(-rdynamic) # function names in crash reports
|
add_link_options(-rdynamic) # function names in crash reports
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,7 @@ fry.frame()`,
|
||||||
},
|
},
|
||||||
|
|
||||||
versions: [
|
versions: [
|
||||||
|
{ v: "0.7", date: "2026-10-21", items: ["Steadier and tidier", "Cutting a shape inside an earlier cut no longer crashes, and cuts come out closed", "Edges hidden behind the surface stay hidden on big or cut-up shapes", "Every panel has a least size, so it stays readable; the first layout fits small windows", "Eye and padlock buttons in Layers; the tabs fit narrow windows", "A failed tool keeps your redo"] },
|
||||||
{ v: "0.6", date: "2026-10-20", items: ["Sculpting, scripts and a Mac app", "Sculpting: raise, puff out, smooth, flatten, pinch and grab", "Materials shown in the view, sixteen to start from", "Scripts: Lua and a Script panel, with examples, kept in the .fry", "Plug-ins: menu commands and materials, three to start with", "Your own materials", "Soft selection, bridge, push / pull, smooth points, show only the selected", "macOS: one app for Apple silicon and Intel", "A new look (Niveus): light and dark, following the desktop", "A new logo", "Drop a file on the window to open it"] },
|
{ v: "0.6", date: "2026-10-20", items: ["Sculpting, scripts and a Mac app", "Sculpting: raise, puff out, smooth, flatten, pinch and grab", "Materials shown in the view, sixteen to start from", "Scripts: Lua and a Script panel, with examples, kept in the .fry", "Plug-ins: menu commands and materials, three to start with", "Your own materials", "Soft selection, bridge, push / pull, smooth points, show only the selected", "macOS: one app for Apple silicon and Intel", "A new look (Niveus): light and dark, following the desktop", "A new logo", "Drop a file on the window to open it"] },
|
||||||
{ v: "0.5", date: "2026-10-19", items: ["Builds for Windows, Linux (AppImage) and Android", "A phone layout: big buttons, finger gestures, the phone's file picker", "Apache License 2.0"] },
|
{ v: "0.5", date: "2026-10-19", items: ["Builds for Windows, Linux (AppImage) and Android", "A phone layout: big buttons, finger gestures, the phone's file picker", "Apache License 2.0"] },
|
||||||
{ v: "0.4", date: "2026-10-19", items: ["Combine objects: add, subtract, overlap", "Draw shapes onto other objects", "Copies in a row or circle, thicken and hollow", "FBX export", "Folders and groups in Layers, go-to buttons", "Shift scales both sides, Shift+click loops and rings", "A crash screen of its own"] },
|
{ v: "0.4", date: "2026-10-19", items: ["Combine objects: add, subtract, overlap", "Draw shapes onto other objects", "Copies in a row or circle, thicken and hollow", "FBX export", "Folders and groups in Layers, go-to buttons", "Shift scales both sides, Shift+click loops and rings", "A crash screen of its own"] },
|
||||||
|
|
|
||||||
208
src/app.cpp
208
src/app.cpp
|
|
@ -316,6 +316,7 @@ public:
|
||||||
}, "Fry3DX: fry_orbit yaw pitch [distance]");
|
}, "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.
|
// 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) {
|
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;
|
if (a.size() < 6) return;
|
||||||
const int brush = std::clamp(std::stoi(a[1]) - 1, 0, int(std::size(kSculptTools)) - 1);
|
const int brush = std::clamp(std::stoi(a[1]) - 1, 0, int(std::size(kSculptTools)) - 1);
|
||||||
self->sculptBrush_ = kSculptTools[brush].brush;
|
self->sculptBrush_ = kSculptTools[brush].brush;
|
||||||
|
|
@ -390,6 +391,7 @@ public:
|
||||||
};
|
};
|
||||||
static auto vp = viewPoint;
|
static auto vp = viewPoint;
|
||||||
static keel::Cmd knife("fry_knife", [](const keel::Args& a) {
|
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) return;
|
||||||
if (a.size() > 5) self->knifeThrough_ = a[5] != "0";
|
if (a.size() > 5) self->knifeThrough_ = a[5] != "0";
|
||||||
if (a.size() > 6) self->knifeAfter_ = std::stoi(a[6]);
|
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 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 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)
|
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;
|
if (a.size() < 9) return;
|
||||||
self->setTool(Tool::Draw);
|
self->setTool(Tool::Draw);
|
||||||
self->drawOnMode_ = std::stoi(a[1]), self->drawThickness_ = std::stof(a[2]);
|
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};
|
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)");
|
}, "Fry3DX: fry_color r g b (painting, 0..1)");
|
||||||
static keel::Cmd stroke("fry_stroke", [](const keel::Args& a) {
|
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;
|
if (a.size() < 5) return;
|
||||||
const ImVec2 p0 = vp(a[1], a[2]), p1 = vp(a[3], a[4]);
|
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);
|
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);
|
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)");
|
}, "Fry3DX: fry_unlock x y: click the Picture panel there (picture pixels)");
|
||||||
static keel::Cmd moveSticker("fry_sticker", [](const keel::Args& a) {
|
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);
|
Object* o = self->find(self->ledit_.object);
|
||||||
PictureLayer* l = self->editLayer();
|
PictureLayer* l = self->editLayer();
|
||||||
if (!o || !l || a.size() < 3) return;
|
if (!o || !l || a.size() < 3) return;
|
||||||
|
|
@ -527,6 +532,7 @@ public:
|
||||||
if (a[1] == "middle") self->middleOfBed();
|
if (a[1] == "middle") self->middleOfBed();
|
||||||
if (a[1] == "printer" && a.size() > 2) keel::execute("fry_printer " + a[2]);
|
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] == "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) {
|
if (a[1] == "measure" && a.size() > 5) {
|
||||||
self->printTool_ = PrintTool::Measure, self->measure_.clear();
|
self->printTool_ = PrintTool::Measure, self->measure_.clear();
|
||||||
for (int i = 0; i < 2; ++i)
|
for (int i = 0; i < 2; ++i)
|
||||||
|
|
@ -638,6 +644,7 @@ public:
|
||||||
const ImGuiID dock = ImGui::DockSpaceOverViewport(0, nullptr, ImGuiDockNodeFlags_PassthruCentralNode);
|
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 (touch() && (io.DisplaySize.y > io.DisplaySize.x) != portrait_) portrait_ = !portrait_, layoutDone_ = false; // (the phone turned: its layout)
|
||||||
if (!layoutDone_) defaultLayout(dock);
|
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);
|
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);
|
updateCamera(io);
|
||||||
menuBar();
|
menuBar();
|
||||||
|
|
@ -681,6 +688,7 @@ private:
|
||||||
std::set<std::pair<int, int>> edges;
|
std::set<std::pair<int, int>> edges;
|
||||||
};
|
};
|
||||||
std::vector<Snapshot> undo_, redo_;
|
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;
|
Mode mode_ = Mode::Object;
|
||||||
Tool tool_ = Tool::Move;
|
Tool tool_ = Tool::Move;
|
||||||
|
|
@ -966,9 +974,20 @@ private:
|
||||||
void pushUndo() {
|
void pushUndo() {
|
||||||
undo_.push_back({scene_, selected_, active_, mode_, selVerts_, selFaces_, selEdges_});
|
undo_.push_back({scene_, selected_, active_, mode_, selVerts_, selFaces_, selEdges_});
|
||||||
if (undo_.size() > 200) undo_.erase(undo_.begin());
|
if (undo_.size() > 200) undo_.erase(undo_.begin());
|
||||||
redo_.clear();
|
redoKept_ = std::move(redo_), redo_.clear();
|
||||||
dirty_ = true, ++edits_;
|
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) {
|
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);
|
||||||
|
|
@ -984,12 +1003,14 @@ private:
|
||||||
redo_.push_back({scene_, selected_, active_, mode_, selVerts_, selFaces_, selEdges_});
|
redo_.push_back({scene_, selected_, active_, mode_, selVerts_, selFaces_, selEdges_});
|
||||||
restore(undo_.back());
|
restore(undo_.back());
|
||||||
undo_.pop_back();
|
undo_.pop_back();
|
||||||
|
redoKept_.clear();
|
||||||
}
|
}
|
||||||
void redo() {
|
void redo() {
|
||||||
if (redo_.empty()) return say("Nothing to redo");
|
if (redo_.empty()) return say("Nothing to redo");
|
||||||
undo_.push_back({scene_, selected_, active_, mode_, selVerts_, selFaces_, selEdges_});
|
undo_.push_back({scene_, selected_, active_, mode_, selVerts_, selFaces_, selEdges_});
|
||||||
restore(redo_.back());
|
restore(redo_.back());
|
||||||
redo_.pop_back();
|
redo_.pop_back();
|
||||||
|
redoKept_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void newScene() {
|
void newScene() {
|
||||||
|
|
@ -1689,7 +1710,7 @@ private:
|
||||||
pushUndo();
|
pushUndo();
|
||||||
const auto made = loopCut(o.mesh, edge, cuts_);
|
const auto made = loopCut(o.mesh, edge, cuts_);
|
||||||
if (made.empty()) {
|
if (made.empty()) {
|
||||||
undo_.pop_back();
|
dropUndo();
|
||||||
return say("Nothing to cut there: loop cuts go across four-sided faces");
|
return say("Nothing to cut there: loop cuts go across four-sided faces");
|
||||||
}
|
}
|
||||||
changedShape(o);
|
changedShape(o);
|
||||||
|
|
@ -1707,7 +1728,7 @@ private:
|
||||||
pushUndo();
|
pushUndo();
|
||||||
const glm::vec3 towards = glm::inverse(glm::mat3(objectMatrix(*o))) * (eye_ - pivotOfSelection());
|
const glm::vec3 towards = glm::inverse(glm::mat3(objectMatrix(*o))) * (eye_ - pivotOfSelection());
|
||||||
if (!makeFace(o->mesh, std::vector<int>(pts.begin(), pts.end()), towards)) {
|
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");
|
return say("Pick 3 or more points (not in a line) to make a face");
|
||||||
}
|
}
|
||||||
changedShape(*o);
|
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()));
|
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();
|
pushUndo();
|
||||||
const std::vector<int> made = bridgeFaces(o->mesh, a, b);
|
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);
|
changedShape(*o);
|
||||||
selFaces_.insert(made.begin(), made.end());
|
selFaces_.insert(made.begin(), made.end());
|
||||||
say(std::format("Bridged: {} faces between them", made.size()));
|
say(std::format("Bridged: {} faces between them", made.size()));
|
||||||
|
|
@ -1889,7 +1910,7 @@ private:
|
||||||
pushUndo();
|
pushUndo();
|
||||||
const auto made = knifeCut(o->mesh, eye, path, knifeThrough_);
|
const auto made = knifeCut(o->mesh, eye, path, knifeThrough_);
|
||||||
if (made.empty()) {
|
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)");
|
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();
|
clearElements();
|
||||||
|
|
@ -1948,7 +1969,7 @@ private:
|
||||||
holeTall_ *= across * scale / std::max(holeSize_, 1e-6f), holeSize_ = across * scale;
|
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
|
if (holeMode_ == 2) { // just the outline: a new face, picked to extrude, bevel or paint
|
||||||
const int made = imprintOutline(o->mesh, face, outline);
|
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);
|
changedShape(*o);
|
||||||
selected_ = {id}, active_ = id;
|
selected_ = {id}, active_ = id;
|
||||||
if (mode_ != Mode::Edit) enterEdit();
|
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));
|
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)) {
|
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");
|
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);
|
changedShape(*o);
|
||||||
|
|
@ -3270,6 +3291,16 @@ private:
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
if (ImGui::MenuItem("Delete", "Del")) selected_ = ids, deleteSelected();
|
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.
|
// The ⌖ button at the end of a row: brings the camera to it.
|
||||||
bool goButton(const char* tip) {
|
bool goButton(const char* tip) {
|
||||||
const ImVec2 at = ImGui::GetCursorScreenPos();
|
const ImVec2 at = ImGui::GetCursorScreenPos();
|
||||||
|
|
@ -3300,11 +3331,9 @@ private:
|
||||||
ImGui::PushID(int(id) + 1000000);
|
ImGui::PushID(int(id) + 1000000);
|
||||||
ImGui::TableNextRow();
|
ImGui::TableNextRow();
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
if (ImGui::SmallButton(f->visible ? "o" : "-")) showFolder(*f, !f->visible);
|
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);
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip(f->visible ? "Hide all in it" : "Show all in it");
|
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
if (ImGui::SmallButton(f->locked ? "L" : " ")) lockFolder(*f, !f->locked);
|
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);
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip(f->locked ? "Unlock all in it" : "Lock all in it");
|
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
ImGui::Indent(indent + 1);
|
ImGui::Indent(indent + 1);
|
||||||
if (ImGui::ArrowButton("##open", f->open ? ImGuiDir_Down : ImGuiDir_Right)) f->open = !f->open;
|
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::PushID(int(o.id));
|
||||||
ImGui::TableNextRow();
|
ImGui::TableNextRow();
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
if (ImGui::SmallButton(o.visible ? "o" : "-")) o.visible = !o.visible, dirty_ = true;
|
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;
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip(o.visible ? "Shown (click to hide)" : "Hidden (click to show)");
|
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
if (ImGui::SmallButton(o.locked ? "L" : " ")) o.locked = !o.locked, dirty_ = true;
|
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;
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip(o.locked ? "Locked: can't be moved or changed" : "Click to lock");
|
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
ImGui::Indent(indent + 1);
|
ImGui::Indent(indent + 1);
|
||||||
if (renaming_ == o.id) {
|
if (renaming_ == o.id) {
|
||||||
|
|
@ -3441,8 +3468,7 @@ private:
|
||||||
Mesh m = b.hasFinish() ? finishedMesh(b) : b.mesh;
|
Mesh m = b.hasFinish() ? finishedMesh(b) : b.mesh;
|
||||||
transform(m, into * objectMatrix(b));
|
transform(m, into * objectMatrix(b));
|
||||||
if (!combine(result, m, op)) {
|
if (!combine(result, m, op)) {
|
||||||
undo();
|
takeBack();
|
||||||
redo_.clear();
|
|
||||||
return say(op == Combine::Overlap ? "They don't overlap: nothing would be left" : "That didn't leave anything");
|
return say(op == Combine::Overlap ? "They don't overlap: nothing would be left" : "That didn't leave anything");
|
||||||
}
|
}
|
||||||
++used;
|
++used;
|
||||||
|
|
@ -3774,6 +3800,8 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
// World point → screen (false behind the eye).
|
// 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 {
|
bool project(const glm::vec3& p, ImVec2& out) const {
|
||||||
const glm::vec4 c = vp_ * glm::vec4(p, 1.0f);
|
const glm::vec4 c = vp_ * glm::vec4(p, 1.0f);
|
||||||
if (c.w < 1e-4f) return false;
|
if (c.w < 1e-4f) return false;
|
||||||
|
|
@ -3839,38 +3867,64 @@ private:
|
||||||
return h.t > len - std::max(len * 0.002f, 0.0005f);
|
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
|
// 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 {
|
struct Visibility {
|
||||||
uint64_t version = 0;
|
uint64_t version = 0;
|
||||||
glm::vec3 eye{1e30f};
|
glm::vec3 eye{1e30f};
|
||||||
|
glm::mat4 vp{0.0f};
|
||||||
std::vector<char> point;
|
std::vector<char> point;
|
||||||
};
|
};
|
||||||
std::map<uint32_t, Visibility> visibility_;
|
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) {
|
const std::vector<char>& visiblePoints(const Object& o) { return visiblePoints(o, o.mesh); }
|
||||||
Visibility& v = visibility_[o.id];
|
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];
|
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;
|
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.version = version, v.eye = eye_, v.vp = vp_;
|
||||||
v.point.assign(o.mesh.verts.size(), 1);
|
v.point.assign(mesh.verts.size(), 1);
|
||||||
const glm::mat4 m = objectMatrix(o);
|
const glm::mat4 m = objectMatrix(o);
|
||||||
std::vector<glm::vec3> w(o.mesh.verts.size());
|
std::vector<glm::vec3> w(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::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;
|
std::vector<std::array<int, 3>> tris;
|
||||||
for (size_t f = 0; f < o.mesh.faces.size(); ++f)
|
for (size_t f = 0; f < mesh.faces.size(); ++f)
|
||||||
for (const auto& t : triangulate(o.mesh, int(f))) tris.push_back(t);
|
for (const auto& t : triangulate(mesh, int(f))) tris.push_back(t);
|
||||||
if (w.size() * tris.size() > 4'000'000) return v.point;
|
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) {
|
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 glm::vec3 d = w[i] - eye_;
|
||||||
const float len = glm::length(d);
|
const float len = glm::length(d);
|
||||||
const Ray ray{eye_, d / len};
|
const Ray ray{eye_, d / len};
|
||||||
const float limit = len - std::max(len * 0.002f, 0.0005f);
|
const float limit = len - std::max(len * 0.002f, 0.0005f);
|
||||||
for (const auto& t : tris) {
|
auto hides = [&](int t) {
|
||||||
if (t[0] == int(i) || t[1] == int(i) || t[2] == int(i)) continue;
|
const auto& tri = tris[size_t(t)];
|
||||||
if (auto hit = hitTriangle(ray, w[size_t(t[0])], w[size_t(t[1])], w[size_t(t[2])]); hit && *hit < limit) {
|
if (tri[0] == int(i) || tri[1] == int(i) || tri[2] == int(i)) return false;
|
||||||
v.point[i] = 0;
|
auto hit = hitTriangle(ray, w[size_t(tri[0])], w[size_t(tri[1])], w[size_t(tri[2])]);
|
||||||
break;
|
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;
|
return v.point;
|
||||||
}
|
}
|
||||||
|
|
@ -4281,7 +4335,7 @@ private:
|
||||||
void cancelDrag() {
|
void cancelDrag() {
|
||||||
if (!drag_.active) return;
|
if (!drag_.active) return;
|
||||||
scene_ = drag_.before;
|
scene_ = drag_.before;
|
||||||
undo_.pop_back();
|
dropUndo();
|
||||||
touchAll();
|
touchAll();
|
||||||
drag_.active = false;
|
drag_.active = false;
|
||||||
}
|
}
|
||||||
|
|
@ -4461,16 +4515,16 @@ private:
|
||||||
Mesh m = extrudeOutline(local, n, thick);
|
Mesh m = extrudeOutline(local, n, thick);
|
||||||
drawPts_.clear();
|
drawPts_.clear();
|
||||||
if (m.faces.empty()) return say("That shape doesn't work (points on top of each other?)");
|
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();
|
pushUndo();
|
||||||
if (onto && drawOnMode_ > 0) {
|
if (onto && drawOnMode_ > 0) {
|
||||||
Object* o = find(onto);
|
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;
|
if (o->hasFinish()) o->mesh = finishedMesh(*o), o->mirror = 0, o->round = 0, o->smoothLevels = 0;
|
||||||
Mesh shape = m;
|
Mesh shape = m;
|
||||||
transform(shape, glm::inverse(objectMatrix(*o)) * glm::translate(glm::mat4(1.0f), c));
|
transform(shape, glm::inverse(objectMatrix(*o)) * glm::translate(glm::mat4(1.0f), c));
|
||||||
Mesh result = o->mesh;
|
Mesh result = o->mesh;
|
||||||
if (!combine(result, shape, drawOnMode_ == 2 ? Combine::Subtract : Combine::Add)) {
|
if (!combine(result, shape, drawOnMode_ == 2 ? Combine::Subtract : Combine::Add)) {
|
||||||
undo(), redo_.clear();
|
takeBack();
|
||||||
return say("That would leave nothing");
|
return say("That would leave nothing");
|
||||||
}
|
}
|
||||||
o = find(onto);
|
o = find(onto);
|
||||||
|
|
@ -5047,8 +5101,10 @@ private:
|
||||||
s = (s | facing) + 4;
|
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) {
|
for (auto [e, s] : side) {
|
||||||
const bool silhouette = (s & 3) == 3, open = (s >> 2) == 1 && (s & 1);
|
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;
|
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);
|
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)
|
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) {
|
void defaultLayout(ImGuiID dock) {
|
||||||
layoutDone_ = true;
|
layoutDone_ = true;
|
||||||
ImGuiDockNode* node = ImGui::DockBuilderGetNode(dock);
|
ImGuiDockNode* node = ImGui::DockBuilderGetNode(dock);
|
||||||
|
|
@ -5328,9 +5422,12 @@ private:
|
||||||
ImGui::DockBuilderDockWindow("Picture", right);
|
ImGui::DockBuilderDockWindow("Picture", right);
|
||||||
return ImGui::DockBuilderFinish(dock);
|
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);
|
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 rightBottom = ImGui::DockBuilderSplitNode(right, ImGuiDir_Down, 0.66f, nullptr, &right);
|
||||||
const ImGuiID bottom = ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, 0.04f, nullptr, ¢er);
|
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)
|
const ImGuiID picture = ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.45f, nullptr, ¢er); // (the Paint tab's)
|
||||||
|
|
@ -5517,22 +5614,27 @@ private:
|
||||||
menus();
|
menus();
|
||||||
ImGui::EndMenu();
|
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 char* names[] = {"Model", "Paint", "Layout", "Render", "Print"};
|
||||||
const Icon icons[] = {Icon::Objects, Icon::PaintBrush, Icon::Layout, Icon::Render, Icon::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)",
|
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)"};
|
"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;
|
const float h = ImGui::GetFrameHeight(), from = ImGui::GetCursorPosX() + 20.0f;
|
||||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX() + 20.0f, (ImGui::GetWindowWidth() - tw * 5 - 16) * 0.5f));
|
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) {
|
for (int i = 0; i < 5; ++i) {
|
||||||
if (i) ImGui::SameLine(0, 4);
|
if (i) ImGui::SameLine(0, 4);
|
||||||
ImGui::PushID(i);
|
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();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const std::string file = (path_.empty() ? "Untitled" : fileName(path_)) + (dirty_ ? " *" : "");
|
|
||||||
if (touch()) return ImGui::EndMainMenuBar(); // (no room on a phone)
|
if (touch()) return ImGui::EndMainMenuBar(); // (no room on a phone)
|
||||||
ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::CalcTextSize(file.c_str()).x - 16);
|
ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::CalcTextSize(file.c_str()).x - 16);
|
||||||
ImGui::TextDisabled("%s", file.c_str());
|
ImGui::TextDisabled("%s", file.c_str());
|
||||||
|
|
@ -5599,6 +5701,7 @@ private:
|
||||||
|
|
||||||
void toolbox() {
|
void toolbox() {
|
||||||
if (std::exchange(showToolbox_, false)) ImGui::SetNextWindowFocus();
|
if (std::exchange(showToolbox_, false)) ImGui::SetNextWindowFocus();
|
||||||
|
ImGui::SetNextWindowSizeConstraints(panelMin("Toolbox"), ImVec2(FLT_MAX, FLT_MAX));
|
||||||
ImGui::Begin("Toolbox", nullptr, ImGuiWindowFlags_NoCollapse);
|
ImGui::Begin("Toolbox", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||||
const float w = ImGui::GetContentRegionAvail().x;
|
const float w = ImGui::GetContentRegionAvail().x;
|
||||||
if (tab_ == Tab::Paint) {
|
if (tab_ == Tab::Paint) {
|
||||||
|
|
@ -5810,6 +5913,7 @@ private:
|
||||||
ImGui::TextDisabled("(the wheel while dragging too)");
|
ImGui::TextDisabled("(the wheel while dragging too)");
|
||||||
}
|
}
|
||||||
void toolOptions() {
|
void toolOptions() {
|
||||||
|
ImGui::SetNextWindowSizeConstraints(panelMin("Tool options"), ImVec2(FLT_MAX, FLT_MAX));
|
||||||
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;
|
||||||
if (tab_ == Tab::Paint) {
|
if (tab_ == Tab::Paint) {
|
||||||
|
|
@ -6017,6 +6121,7 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
void layersPanel() {
|
void layersPanel() {
|
||||||
|
ImGui::SetNextWindowSizeConstraints(panelMin("Layers"), ImVec2(FLT_MAX, FLT_MAX));
|
||||||
ImGui::Begin("Layers", nullptr, ImGuiWindowFlags_NoCollapse);
|
ImGui::Begin("Layers", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||||
// Along the top: a new folder, the picked ones into one.
|
// Along the top: a new folder, the picked ones into one.
|
||||||
if (ImGui::SmallButton("+ Folder")) {
|
if (ImGui::SmallButton("+ Folder")) {
|
||||||
|
|
@ -6042,7 +6147,7 @@ private:
|
||||||
ImGui::PushID(int(ref.id));
|
ImGui::PushID(int(ref.id));
|
||||||
ImGui::TableNextRow();
|
ImGui::TableNextRow();
|
||||||
ImGui::TableNextColumn();
|
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();
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
const std::string label = "Picture: " + stdfs::path(ref.path).filename().string();
|
const std::string label = "Picture: " + stdfs::path(ref.path).filename().string();
|
||||||
|
|
@ -6111,8 +6216,7 @@ private:
|
||||||
PictureLayer& l = o->layers[size_t(i)];
|
PictureLayer& l = o->layers[size_t(i)];
|
||||||
const bool on = ledit_.object == o->id && ledit_.index == i;
|
const bool on = ledit_.object == o->id && ledit_.index == i;
|
||||||
ImGui::PushID(i);
|
ImGui::PushID(i);
|
||||||
if (ImGui::SmallButton(l.visible ? "o" : "-")) pushUndo(), l.visible = !l.visible;
|
if (rowToggle("##eye", l.visible ? Icon::Eye : Icon::EyeShut, l.visible, l.visible ? "Hide it" : "Show it")) pushUndo(), l.visible = !l.visible;
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip(l.visible ? "Hide it" : "Show it");
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::Selectable(std::format("{}{}", l.name, on ? " (moving)" : l.sticker ? "" : " (flat)").c_str(), on, 0, ImVec2(w - 92, 0)))
|
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);
|
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.
|
// The Paint tab's picture: the selected object's, flat, to paint on and place pictures on.
|
||||||
void picturePanel() {
|
void picturePanel() {
|
||||||
|
ImGui::SetNextWindowSizeConstraints(panelMin("Picture"), ImVec2(FLT_MAX, FLT_MAX));
|
||||||
ImGui::Begin("Picture", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
ImGui::Begin("Picture", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||||
Object* o = selected_.empty() ? nullptr : find(active_);
|
Object* o = selected_.empty() ? nullptr : find(active_);
|
||||||
if (!o || !o->picture) {
|
if (!o || !o->picture) {
|
||||||
|
|
@ -6314,6 +6419,7 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
void propertiesPanel() {
|
void propertiesPanel() {
|
||||||
|
ImGui::SetNextWindowSizeConstraints(panelMin("Properties"), ImVec2(FLT_MAX, FLT_MAX));
|
||||||
ImGui::Begin("Properties", nullptr, ImGuiWindowFlags_NoCollapse);
|
ImGui::Begin("Properties", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||||
const float w = ImGui::GetContentRegionAvail().x;
|
const float w = ImGui::GetContentRegionAvail().x;
|
||||||
if (tab_ == Tab::Paint) {
|
if (tab_ == Tab::Paint) {
|
||||||
|
|
@ -6707,8 +6813,7 @@ private:
|
||||||
touchAll();
|
touchAll();
|
||||||
if (r.ok && frame) frameAll();
|
if (r.ok && frame) frameAll();
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
undo();
|
takeBack();
|
||||||
redo_.clear();
|
|
||||||
scriptLine(r.error, true);
|
scriptLine(r.error, true);
|
||||||
showScript_ = true;
|
showScript_ = true;
|
||||||
return say(name + " stopped (nothing it did was kept): " + r.error.substr(0, r.error.find('\n')));
|
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();
|
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::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::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();
|
if (!ImGui::Begin("Script", &showScript_)) return ImGui::End();
|
||||||
|
|
||||||
// The file's scripts (saved in the .fry with it).
|
// 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 <cmath>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
namespace fry {
|
namespace fry {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
@ -96,18 +97,28 @@ struct Node {
|
||||||
if (front) front->all(out);
|
if (front) front->all(out);
|
||||||
if (back) back->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 (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;
|
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 (!f.empty()) {
|
||||||
if (!front) front = std::make_unique<Node>();
|
if (!front) front = std::make_unique<Node>();
|
||||||
front->build(f);
|
front->build(f, depth + 1);
|
||||||
}
|
}
|
||||||
if (!b.empty()) {
|
if (!b.empty()) {
|
||||||
if (!back) back = std::make_unique<Node>();
|
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;
|
const int v = *it;
|
||||||
if (v == a || v == b) continue;
|
if (v == a || v == b) continue;
|
||||||
const float t = glm::dot(m.verts[size_t(v)] - pa, d) / len2;
|
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});
|
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());
|
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; });
|
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;
|
Node A, B;
|
||||||
A.build(polysOf(a));
|
A.build(polysOf(am));
|
||||||
B.build(polysOf(b));
|
B.build(polysOf(b));
|
||||||
if (!A.hasPlane || !B.hasPlane) return false;
|
if (!A.hasPlane || !B.hasPlane) return {};
|
||||||
std::vector<Poly> bp;
|
std::vector<Poly> bp;
|
||||||
switch (op) {
|
switch (op) {
|
||||||
case Combine::Add:
|
case Combine::Add:
|
||||||
|
|
@ -230,13 +319,35 @@ bool combine(Mesh& a, const Mesh& b, Combine op) {
|
||||||
}
|
}
|
||||||
std::vector<Poly> out;
|
std::vector<Poly> out;
|
||||||
A.all(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
|
// 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.
|
// could show) are one; else they fold tiny faces back over their neighbours.
|
||||||
mergeByDistance(a, 3e-5f);
|
mergeByDistance(a, 3e-5f);
|
||||||
removeSpikes(a);
|
removeSpikes(a);
|
||||||
mergeFlatFaces(a);
|
mergeFlatFaces(a);
|
||||||
removeSpikes(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();
|
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);
|
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;
|
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:
|
case Icon::LayFlat:
|
||||||
quad(0.22f, 0.40f, 0.62f, 0.22f, 0.86f, 0.50f, 0.46f, 0.68f, false, c);
|
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);
|
line(0.10f, 0.88f, 0.90f, 0.88f);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
namespace fry {
|
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);
|
void drawIcon(ImDrawList* dl, Icon icon, ImVec2 topLeft, float size, ImU32 color);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -362,6 +362,42 @@ int main() {
|
||||||
CHECK(combine(a, drill, Combine::Subtract) && checkForPrint(a).closed() && checkForPrint(a).volume < 1.0f && checkForPrint(a).volume > 0.85f,
|
CHECK(combine(a, drill, Combine::Subtract) && checkForPrint(a).closed() && checkForPrint(a).volume < 1.0f && checkForPrint(a).volume > 0.85f,
|
||||||
"a cylinder drilled through a cube: closed, %g m3", checkForPrint(a).volume);
|
"a cylinder drilled through a cube: closed, %g m3", checkForPrint(a).volume);
|
||||||
}
|
}
|
||||||
|
// Shapes drawn and cut in, again and again inside the last cut, on a turned object (the shape
|
||||||
|
// carried into its own space, as the Draw tool does). This once built its tree forever and
|
||||||
|
// crashed; and the cutting's rounding left small gaps.
|
||||||
|
{
|
||||||
|
uint32_t seed = 12345;
|
||||||
|
auto rnd = [&] { return (seed = seed * 1664525u + 1013904223u) / 4294967296.0f * 2 - 1; }; // (the same everywhere: -1..1)
|
||||||
|
int cuts = 0, sealedCuts = 0;
|
||||||
|
for (int run = 0; run < 12; ++run) {
|
||||||
|
Mesh m = makeCube(1.0f);
|
||||||
|
const glm::vec3 axis = glm::normalize(glm::vec3(rnd(), rnd(), rnd() + 2));
|
||||||
|
const glm::mat4 M = glm::translate(glm::mat4(1), glm::vec3(rnd(), rnd(), 0.5f)) * glm::rotate(glm::mat4(1), rnd() * 3, axis);
|
||||||
|
float top = 0.5f; // (the floor of the last cut, the object's own space)
|
||||||
|
for (int k = 0; k < 5; ++k) {
|
||||||
|
const glm::vec3 P = glm::vec3(M * glm::vec4(rnd() * 0.03f, rnd() * 0.03f, top, 1)), N = glm::normalize(glm::mat3(M) * glm::vec3(0, 0, 1));
|
||||||
|
const glm::vec3 X = glm::normalize(glm::cross(N, glm::vec3(0.3f, 0.2f, 0.9f))), Y = glm::cross(N, X);
|
||||||
|
const int corners = 3 + k % 4;
|
||||||
|
const float r = 0.25f - 0.04f * float(k);
|
||||||
|
std::vector<glm::vec3> pts;
|
||||||
|
glm::vec3 c(0.0f);
|
||||||
|
for (int i = 0; i < corners; ++i) {
|
||||||
|
const float t = 6.2831f * float(i) / float(corners) + rnd() * 0.3f;
|
||||||
|
pts.push_back(P + X * std::cos(t) * r + Y * std::sin(t) * r), c += pts.back();
|
||||||
|
}
|
||||||
|
c /= float(corners);
|
||||||
|
for (glm::vec3& q : pts) q -= c;
|
||||||
|
const float thick = 0.05f + 0.0002f;
|
||||||
|
Mesh shape = extrudeOutline(pts, N, thick);
|
||||||
|
transform(shape, glm::inverse(M) * glm::translate(glm::mat4(1), c + N * (0.0001f - thick / 2)));
|
||||||
|
Mesh was = m;
|
||||||
|
if (!combine(m, shape, Combine::Subtract)) m = was;
|
||||||
|
++cuts, sealedCuts += checkForPrint(m).closed();
|
||||||
|
top -= 0.05f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CHECK(sealedCuts >= cuts - 2, "cuts drawn inside cuts: no crash, %d of %d closed after", sealedCuts, cuts);
|
||||||
|
}
|
||||||
// An edge's ring: across a cube's side, the four edges going round it.
|
// An edge's ring: across a cube's side, the four edges going round it.
|
||||||
{
|
{
|
||||||
const Mesh c = makeCube(1.0f);
|
const Mesh c = makeCube(1.0f);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue