mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 12:34:27 +00:00
Painting both sides (mirrored across the object's own Y, as sculpting) and a stencil (drag round an area: paint only inside it); a layer's see-through in its menu too; fry_stencil
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2f68430354
commit
e0831563b7
1 changed files with 57 additions and 4 deletions
61
src/app.cpp
61
src/app.cpp
|
|
@ -478,6 +478,11 @@ public:
|
|||
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);
|
||||
self->endStroke();
|
||||
}, "Fry3DX: fry_stroke x0 y0 x1 y1: a brush stroke across the view (0..1)");
|
||||
static keel::Cmd stencil("fry_stencil", [](const keel::Args& a) { // a box stencil, 0..1 of the view
|
||||
if (a.size() < 5 || !self->viewReady()) return;
|
||||
const ImVec2 p0 = vp(a[1], a[2]), p1 = vp(a[3], a[4]);
|
||||
self->stencil_ = {p0, ImVec2(p1.x, p0.y), p1, ImVec2(p0.x, p1.y)}, self->stencilOn_ = true;
|
||||
}, "Fry3DX: fry_stencil x0 y0 x1 y1: a box stencil for painting (0..1 of the view)");
|
||||
static keel::Cmd paste("fry_pastefile", [](const keel::Args& a) { if (a.size() > 1) self->floatFromFile(a[1]); }, "Fry3DX: place a picture file onto the selected object");
|
||||
static keel::Cmd corner("fry_corner", [](const keel::Args& a) {
|
||||
if (PictureLayer* l = self->editLayer(); l && a.size() > 3) l->corners[size_t(std::clamp(std::stoi(a[1]), 0, 3))] = {std::stof(a[2]), std::stof(a[3])};
|
||||
|
|
@ -885,6 +890,8 @@ private:
|
|||
bool sculpting_ = false;
|
||||
SculptBrush sculptBrush_ = SculptBrush::Draw;
|
||||
float sculptStrength_ = 0.5f, sculptPx_ = 60.0f; // (its own brush size: sculpting wants a bigger one than painting)
|
||||
bool paintMirror_ = false, stencilOn_ = false, drawingStencil_ = false;
|
||||
std::vector<ImVec2> stencil_; // (on the screen)
|
||||
bool sculptMirror_ = false; // the same on the other side (left and right: the object's Y)
|
||||
struct SculptStroke {
|
||||
bool active = false, haveLast = false, waiting = false;
|
||||
|
|
@ -2242,9 +2249,14 @@ private:
|
|||
}
|
||||
// One dab of the brush on the model: on every face near it, into the picture where that face
|
||||
// lies (so it carries on across the seams between pieces of the layout).
|
||||
void dab3D(Object& o, const glm::vec3& at, float radius) {
|
||||
// One dab of paint at a point of the object (the world), on the faces towards `eye` (the
|
||||
// mirrored dab: towards the eye mirrored). The stencil: only where the surface shows inside
|
||||
// it on the screen (the mirrored dab: where its mirror image does, `back` taking it there).
|
||||
void dab3D(Object& o, const glm::vec3& at, float radius, const glm::vec3* eyeAt = nullptr, const glm::mat4* back = nullptr) {
|
||||
Image& im = *paint_.image;
|
||||
const glm::mat4 m = objectMatrix(o);
|
||||
const glm::vec3 eye = eyeAt ? *eyeAt : eye_;
|
||||
const bool stencil = stencilOn_ && stencil_.size() >= 3;
|
||||
const glm::vec2 size(float(im.w), float(im.h));
|
||||
const float inner = std::clamp(hardness_, 0.0f, 0.999f) * radius;
|
||||
for (size_t f = 0; f < o.mesh.faces.size(); ++f) {
|
||||
|
|
@ -2260,7 +2272,7 @@ private:
|
|||
if (glm::any(glm::lessThan(at, lo)) || glm::any(glm::greaterThan(at, hi))) continue;
|
||||
const glm::vec3 n = glm::cross(w[1] - w[0], w[2] - w[0]);
|
||||
const float area3 = glm::length(n) * 0.5f;
|
||||
if (area3 < 1e-12f || glm::dot(n, eye_ - w[0]) <= 0 || std::abs(glm::dot(glm::normalize(n), at - w[0])) > radius) continue;
|
||||
if (area3 < 1e-12f || glm::dot(n, eye - w[0]) <= 0 || std::abs(glm::dot(glm::normalize(n), at - w[0])) > radius) continue;
|
||||
const float areaPx = std::abs((px[1].x - px[0].x) * (px[2].y - px[0].y) - (px[2].x - px[0].x) * (px[1].y - px[0].y)) * 0.5f;
|
||||
if (areaPx < 1e-6f) continue;
|
||||
// The dab's middle on the picture, and how far it reaches there.
|
||||
|
|
@ -2287,6 +2299,10 @@ private:
|
|||
const glm::vec3 p = w[0] * l[0] + w[1] * l[1] + w[2] * l[2];
|
||||
const float d = glm::length(p - at);
|
||||
if (d >= radius) continue;
|
||||
if (stencil) {
|
||||
ImVec2 sp;
|
||||
if (!project(back ? glm::vec3(*back * glm::vec4(p, 1.0f)) : p, sp) || !insidePolygon(sp, stencil_)) continue;
|
||||
}
|
||||
const float s = d <= inner ? 1.0f : 1.0f - (d - inner) / (radius - inner);
|
||||
paint_.stroke.put(im, x, y, s * s * (3 - 2 * s));
|
||||
}
|
||||
|
|
@ -2316,11 +2332,18 @@ private:
|
|||
const glm::vec3 at = ray.origin + ray.dir * h.t;
|
||||
const float radius = brushPx_ * pixelSize(at);
|
||||
const float gap = glm::distance(at, paint_.last);
|
||||
// (painting both sides: the same dab mirrored across the object's own Y, its left and right)
|
||||
const glm::mat4 m = objectMatrix(*o), mirror = m * glm::scale(glm::mat4(1.0f), glm::vec3(1, -1, 1)) * glm::inverse(m);
|
||||
const glm::vec3 eyeMirrored = glm::vec3(mirror * glm::vec4(eye_, 1.0f));
|
||||
auto dab = [&](const glm::vec3& p) {
|
||||
dab3D(*o, p, radius);
|
||||
if (paintMirror_) dab3D(*o, glm::vec3(mirror * glm::vec4(p, 1.0f)), radius, &eyeMirrored, &mirror);
|
||||
};
|
||||
if (paint_.haveLast && gap < radius * 8) {
|
||||
const int steps = std::max(1, int(std::ceil(gap / (radius * 0.3f))));
|
||||
for (int i = 1; i <= steps; ++i) dab3D(*o, glm::mix(paint_.last, at, float(i) / float(steps)), radius);
|
||||
for (int i = 1; i <= steps; ++i) dab(glm::mix(paint_.last, at, float(i) / float(steps)));
|
||||
} else
|
||||
dab3D(*o, at, radius);
|
||||
dab(at);
|
||||
paint_.last = at, paint_.haveLast = true;
|
||||
}
|
||||
void pickColor(const Object& o, glm::vec2 uv) {
|
||||
|
|
@ -5294,6 +5317,16 @@ private:
|
|||
// Painting: on the model under the mouse.
|
||||
void paintViewport(ImGuiIO& io) {
|
||||
if (sculpting_) return sculptViewport(io);
|
||||
if (drawingStencil_) { // (drawing the stencil: a drag round the area)
|
||||
if (ImGui::IsMouseDown(ImGuiMouseButton_Left) && mouseInView(io)) {
|
||||
if (stencil_.empty() || std::hypot(stencil_.back().x - io.MousePos.x, stencil_.back().y - io.MousePos.y) > 3) stencil_.push_back(io.MousePos);
|
||||
} else if (!stencil_.empty()) {
|
||||
drawingStencil_ = false, stencilOn_ = stencil_.size() >= 3;
|
||||
if (!stencilOn_) stencil_.clear();
|
||||
say(stencilOn_ ? "Stencil on: paint goes only inside it" : "Too small for a stencil: drag round an area");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (paint_.waiting) {
|
||||
if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) paint_.waiting = false;
|
||||
else if (pickObject(mouseRay(io.MousePos), 0, true).object) paint_.waiting = false, paintAt(io.MousePos, true);
|
||||
|
|
@ -5793,6 +5826,10 @@ private:
|
|||
}
|
||||
}
|
||||
}
|
||||
if (stencil_.size() >= 2) { // (the stencil's edge; where paint can't go, shaded)
|
||||
dl->AddPolyline(stencil_.data(), int(stencil_.size()), stencilOn_ || drawingStencil_ ? kAccent : IM_COL32(200, 200, 205, 120), drawingStencil_ ? 0 : ImDrawFlags_Closed, 2.0f);
|
||||
if (stencilOn_ && !drawingStencil_ && stencil_.size() >= 3) dl->AddConcavePolyFilled(stencil_.data(), int(stencil_.size()), IM_COL32(255, 160, 90, 28));
|
||||
}
|
||||
if (sculpting_) {
|
||||
if (mouseInView(io)) dl->AddCircle(io.MousePos, sculptPx_, io.KeyCtrl ? IM_COL32(120, 180, 255, 220) : IM_COL32(255, 255, 255, 200), 40, 1.5f);
|
||||
} else if (mouseInView(io) && !editLayer() && paintTool_ != PaintTool::Picture)
|
||||
|
|
@ -6193,6 +6230,8 @@ private:
|
|||
add("shape.bridge", "Bridge two faces", "Shape", "", [this] { bridgeNow(); }, editing, "tunnel connect");
|
||||
add("shape.soft", "Soft selection on / off", "Shape", "Shift+O", [this] { softSel_ = !softSel_; }, {}, "proportional falloff");
|
||||
add("shape.separate", "Make into its own object", "Shape", "P", [this] { separateSelected(); }, editing, "separate split");
|
||||
add("paint.mirror", "Paint both sides (mirrored)", "Paint", "", [this] { paintMirror_ = !paintMirror_, say(paintMirror_ ? "Painting both sides" : "Painting one side"); }, {}, "symmetry symmetric");
|
||||
add("paint.stencil", "Draw a paint stencil (paint only inside it)", "Paint", "", [this] { setTab(Tab::Paint), drawingStencil_ = true, stencil_.clear(); }, {}, "mask masking tape area");
|
||||
add("shape.crease", "Crease the picked edges (stay sharp when smoothed)", "Shape", "Shift+E", [this] { creaseSelected(); }, editing, "sharp hard edge subdivision smooth");
|
||||
add("shape.slide", "Slide the points along their edges...", "Shape", "Alt+G", [this] { startSlide(); }, editing, "edge slide vertex slide");
|
||||
add("shape.mirror", "Mirror editing (both sides change together)", "Shape", "", [this] { liveMirror_ = !liveMirror_, say(liveMirror_ ? "Mirror editing on: both sides change together" : "Mirror editing off"); }, {}, "symmetry symmetric x");
|
||||
|
|
@ -6705,6 +6744,18 @@ private:
|
|||
ImGui::SliderFloat("##hard", &hardness_, 0.0f, 1.0f, "hard edge %.2f");
|
||||
ImGui::SetNextItemWidth(w);
|
||||
ImGui::SliderFloat("##strength", &paintColor_.a, 0.02f, 1.0f, "strength %.2f");
|
||||
ImGui::Checkbox("Both sides (mirrored, X)", &paintMirror_);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("What you paint on one side goes on the other too, mirrored (left and right: the object's own Y)");
|
||||
ImGui::SeparatorText("Stencil");
|
||||
if (stencil_.size() >= 3) {
|
||||
ImGui::Checkbox("Paint only inside it", &stencilOn_);
|
||||
if (ImGui::Button("Draw it again", ImVec2(w * 0.5f - 2, 0))) drawingStencil_ = true, stencil_.clear();
|
||||
ImGui::SameLine(0, 4);
|
||||
if (ImGui::Button("Take it off", ImVec2(w * 0.5f - 2, 0))) stencil_.clear(), stencilOn_ = false;
|
||||
} else {
|
||||
if (ImGui::Button(drawingStencil_ ? "Drawing: drag round the area..." : "Draw a stencil", ImVec2(w, 0))) drawingStencil_ = !drawingStencil_, stencil_.clear();
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Drag round an area on the screen: paint goes only inside it (it stays put on the screen as you turn the view)");
|
||||
}
|
||||
}
|
||||
if (paintTool_ == PaintTool::Fill) {
|
||||
ImGui::SetNextItemWidth(w);
|
||||
|
|
@ -7096,6 +7147,8 @@ private:
|
|||
if (ImGui::SmallButton("x")) drop = i;
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Delete it");
|
||||
if (ImGui::BeginPopupContextItem("menu")) {
|
||||
ImGui::SetNextItemWidth(180);
|
||||
undoable([&] { return ImGui::SliderFloat("##op", &l.opacity, 0.0f, 1.0f, "see-through %.2f"); });
|
||||
if (ImGui::MenuItem("Paint it in for good")) merge = i;
|
||||
if (ImGui::MenuItem("Delete")) drop = i;
|
||||
ImGui::EndPopup();
|
||||
|
|
|
|||
Loading…
Reference in a new issue