mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
Crease (Shift+E): picked edges stay sharp when the shape's smoothed (carried through each level and the mirror; saved); shown in magenta; tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
67866ed0fb
commit
ab554f277f
6 changed files with 108 additions and 6 deletions
34
src/app.cpp
34
src/app.cpp
|
|
@ -5431,6 +5431,7 @@ private:
|
||||||
if (key(ImGuiKey_F12)) dialog(5);
|
if (key(ImGuiKey_F12)) dialog(5);
|
||||||
if (key(ImGuiKey_Q)) setTool(Tool::Select);
|
if (key(ImGuiKey_Q)) setTool(Tool::Select);
|
||||||
if (key(ImGuiKey_W)) setTool(Tool::Move);
|
if (key(ImGuiKey_W)) setTool(Tool::Move);
|
||||||
|
if (key(ImGuiKey_E) && shift && mode_ == Mode::Edit) return creaseSelected();
|
||||||
if (key(ImGuiKey_E)) setTool(Tool::Rotate);
|
if (key(ImGuiKey_E)) setTool(Tool::Rotate);
|
||||||
if (key(ImGuiKey_R)) setTool(Tool::Scale);
|
if (key(ImGuiKey_R)) setTool(Tool::Scale);
|
||||||
if (key(ImGuiKey_D)) setTool(Tool::Draw);
|
if (key(ImGuiKey_D)) setTool(Tool::Draw);
|
||||||
|
|
@ -5790,6 +5791,16 @@ private:
|
||||||
lines(*ed, shownMesh(*ed), IM_COL32(20, 20, 25, 70), 1.0f, false);
|
lines(*ed, shownMesh(*ed), IM_COL32(20, 20, 25, 70), 1.0f, false);
|
||||||
lines(*ed, ed->mesh, shading() == Shading::Wire ? IM_COL32(230, 230, 235, 230) : IM_COL32(12, 12, 15, 235), 1.3f, true);
|
lines(*ed, ed->mesh, shading() == Shading::Wire ? IM_COL32(230, 230, 235, 230) : IM_COL32(12, 12, 15, 235), 1.3f, true);
|
||||||
}
|
}
|
||||||
|
if (ed && !ed->creases.empty()) { // (creased edges: smoothing keeps them sharp)
|
||||||
|
const glm::mat4 m = objectMatrix(*ed);
|
||||||
|
const int n = int(ed->mesh.verts.size());
|
||||||
|
for (auto [a, b] : ed->creases) {
|
||||||
|
ImVec2 sa, sb;
|
||||||
|
if (a < n && b < n && (xray_ || (pointSeen(*ed, a) && pointSeen(*ed, b))) && project(glm::vec3(m * glm::vec4(ed->mesh.verts[size_t(a)], 1.0f)), sa) &&
|
||||||
|
project(glm::vec3(m * glm::vec4(ed->mesh.verts[size_t(b)], 1.0f)), sb))
|
||||||
|
dl->AddLine(sa, sb, IM_COL32(230, 70, 200, 255), 2.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (Object* o = ed) {
|
if (Object* o = ed) {
|
||||||
const glm::mat4 m = objectMatrix(*o);
|
const glm::mat4 m = objectMatrix(*o);
|
||||||
if (elem_ == Elem::Vertex) {
|
if (elem_ == Elem::Vertex) {
|
||||||
|
|
@ -6151,6 +6162,7 @@ private:
|
||||||
add("shape.bridge", "Bridge two faces", "Shape", "", [this] { bridgeNow(); }, editing, "tunnel connect");
|
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.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("shape.separate", "Make into its own object", "Shape", "P", [this] { separateSelected(); }, editing, "separate split");
|
||||||
|
add("shape.crease", "Crease the picked edges (stay sharp when smoothed)", "Shape", "Shift+E", [this] { creaseSelected(); }, editing, "sharp hard edge subdivision smooth");
|
||||||
add("shape.slide", "Slide the points along their edges...", "Shape", "Alt+G", [this] { startSlide(); }, editing, "edge slide vertex slide");
|
add("shape.slide", "Slide the points along their edges...", "Shape", "Alt+G", [this] { startSlide(); }, editing, "edge slide vertex slide");
|
||||||
add("shape.mirror", "Mirror editing (both sides change together)", "Shape", "", [this] { liveMirror_ = !liveMirror_, say(liveMirror_ ? "Mirror editing on: both sides change together" : "Mirror editing off"); }, {}, "symmetry symmetric x");
|
add("shape.mirror", "Mirror editing (both sides change together)", "Shape", "", [this] { liveMirror_ = !liveMirror_, say(liveMirror_ ? "Mirror editing on: both sides change together" : "Mirror editing off"); }, {}, "symmetry symmetric x");
|
||||||
add("view.solid", "Show: solid", "View", "Z", [this] { shading_ = Shading::Solid; });
|
add("view.solid", "Show: solid", "View", "Z", [this] { shading_ = Shading::Solid; });
|
||||||
|
|
@ -8110,6 +8122,28 @@ private:
|
||||||
for (const std::string& d : did) text += (text.empty() ? "" : ", ") + d;
|
for (const std::string& d : did) text += (text.empty() ? "" : ", ") + d;
|
||||||
say("Repaired: " + text + (still ? std::format(". {} still not closed (see the Print tab)", still) : ". Ready to print"));
|
say("Repaired: " + text + (still ? std::format(". {} still not closed (see the Print tab)", still) : ". Ready to print"));
|
||||||
}
|
}
|
||||||
|
// Crease (Shift+E): the picked edges (or those between picked points / round picked faces)
|
||||||
|
// kept sharp when the shape's smoothed; again: not.
|
||||||
|
void creaseSelected() {
|
||||||
|
Object* o = editing();
|
||||||
|
if (!o) return say("Crease works on a shape being edited (Tab)");
|
||||||
|
std::set<std::pair<int, int>> picked;
|
||||||
|
if (elem_ == Elem::Edge)
|
||||||
|
for (auto [a, b] : selEdges_) picked.insert({std::min(a, b), std::max(a, b)});
|
||||||
|
else {
|
||||||
|
const std::set<int> pts = selectedPoints();
|
||||||
|
for (const auto& e : adjacencyOf(*o, o->mesh).edges)
|
||||||
|
if (pts.count(e[0]) && pts.count(e[1])) picked.insert({e[0], e[1]});
|
||||||
|
}
|
||||||
|
if (picked.empty()) return say("Pick the edges to crease first (Edges, 2)");
|
||||||
|
pushUndo();
|
||||||
|
const bool all = std::all_of(picked.begin(), picked.end(), [&](const auto& e) { return o->creases.count(e) > 0; });
|
||||||
|
for (const auto& e : picked) all ? (void)o->creases.erase(e) : (void)o->creases.insert(e);
|
||||||
|
touched(o->id);
|
||||||
|
say(all ? std::format("{} edge{} smooth again", picked.size(), picked.size() == 1 ? "" : "s")
|
||||||
|
: std::format("{} edge{} creased: smoothing keeps {} sharp{}", picked.size(), picked.size() == 1 ? "" : "s", picked.size() == 1 ? "it" : "them",
|
||||||
|
o->smoothLevels ? "" : " (turn on Smooth out to see)"));
|
||||||
|
}
|
||||||
// Slide (Alt+G): the picked points glide along their edges, each along the edge that goes
|
// Slide (Alt+G): the picked points glide along their edges, each along the edge that goes
|
||||||
// most the way the mouse goes, never past its other end. Click: done. Esc / right click: as it was.
|
// most the way the mouse goes, never past its other end. Click: done. Esc / right click: as it was.
|
||||||
struct Slide {
|
struct Slide {
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,9 @@ void bevelEdges(Mesh& m, const std::set<std::pair<int, int>>& edges, float width
|
||||||
// Edges where the faces meet sharper than `angle` degrees.
|
// Edges where the faces meet sharper than `angle` degrees.
|
||||||
std::set<std::pair<int, int>> sharpEdges(const Mesh& m, float angle);
|
std::set<std::pair<int, int>> sharpEdges(const Mesh& m, float angle);
|
||||||
// Every face cut into four-sided pieces; `smooth` rounds it (Catmull–Clark) — edges sharper
|
// Every face cut into four-sided pieces; `smooth` rounds it (Catmull–Clark) — edges sharper
|
||||||
// than `keepAngle` degrees stay sharp (180: none do).
|
// than `keepAngle` degrees stay sharp (180: none do), and the `creases` (point pairs, the lower
|
||||||
void subdivide(Mesh& m, bool smooth, float keepAngle = 180.0f);
|
// first) too: they're made the new edges along them, for the next time round.
|
||||||
|
void subdivide(Mesh& m, bool smooth, float keepAngle = 180.0f, std::set<std::pair<int, int>>* creases = nullptr);
|
||||||
// The ring of four-sided faces across an edge, cut round the middle `cuts` times. Returns the new
|
// The ring of four-sided faces across an edge, cut round the middle `cuts` times. Returns the new
|
||||||
// points (empty if the edge has no four-sided face).
|
// points (empty if the edge has no four-sided face).
|
||||||
std::vector<int> loopCut(Mesh& m, std::pair<int, int> edge, int cuts = 1);
|
std::vector<int> loopCut(Mesh& m, std::pair<int, int> edge, int cuts = 1);
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,9 @@ void applyMaterial(Object& o, const MaterialPreset& m) {
|
||||||
|
|
||||||
Mesh finishedMesh(const Object& o, bool mirrorOnly) {
|
Mesh finishedMesh(const Object& o, bool mirrorOnly) {
|
||||||
Mesh m = o.mesh;
|
Mesh m = o.mesh;
|
||||||
|
std::set<std::pair<int, int>> creases;
|
||||||
|
for (const auto& [a, b] : o.creases) // (only those still edges of its points)
|
||||||
|
if (a >= 0 && b >= 0 && a < int(m.verts.size()) && b < int(m.verts.size())) creases.insert({std::min(a, b), std::max(a, b)});
|
||||||
for (int axis = 0; axis < 3; ++axis) {
|
for (int axis = 0; axis < 3; ++axis) {
|
||||||
if (!(o.mirror & (1 << axis))) continue;
|
if (!(o.mirror & (1 << axis))) continue;
|
||||||
glm::vec3 s(1.0f);
|
glm::vec3 s(1.0f);
|
||||||
|
|
@ -127,6 +130,10 @@ Mesh finishedMesh(const Object& o, bool mirrorOnly) {
|
||||||
if (std::abs(m.verts[i][axis]) < eps) to[i] = int(i - before);
|
if (std::abs(m.verts[i][axis]) < eps) to[i] = int(i - before);
|
||||||
for (auto& f : m.faces)
|
for (auto& f : m.faces)
|
||||||
for (int& v : f) v = to[size_t(v)];
|
for (int& v : f) v = to[size_t(v)];
|
||||||
|
for (const auto& [a, b] : std::set(creases)) { // (the creases mirrored with it)
|
||||||
|
const int p = to[size_t(a) + before], q = to[size_t(b) + before];
|
||||||
|
creases.insert({std::min(p, q), std::max(p, q)});
|
||||||
|
}
|
||||||
std::erase_if(m.faces, [](const std::vector<int>& f) { return std::set<int>(f.begin(), f.end()).size() < 3; });
|
std::erase_if(m.faces, [](const std::vector<int>& f) { return std::set<int>(f.begin(), f.end()).size() < 3; });
|
||||||
// (a face lying on the mirror line would now be there twice, back to back: both go)
|
// (a face lying on the mirror line would now be there twice, back to back: both go)
|
||||||
std::map<std::vector<int>, int> seen;
|
std::map<std::vector<int>, int> seen;
|
||||||
|
|
@ -140,11 +147,23 @@ Mesh finishedMesh(const Object& o, bool mirrorOnly) {
|
||||||
std::sort(k.begin(), k.end());
|
std::sort(k.begin(), k.end());
|
||||||
return seen[k] > 1;
|
return seen[k] > 1;
|
||||||
});
|
});
|
||||||
|
{ // (the creases numbered as removeUnused is about to number the points)
|
||||||
|
std::vector<int> num(m.verts.size(), -1);
|
||||||
|
for (const auto& f : m.faces)
|
||||||
|
for (int v : f) num[size_t(v)] = 0;
|
||||||
|
int next = 0;
|
||||||
|
for (int& n : num)
|
||||||
|
if (n == 0) n = next++;
|
||||||
|
std::set<std::pair<int, int>> now;
|
||||||
|
for (const auto& [a, b] : creases)
|
||||||
|
if (num[size_t(a)] >= 0 && num[size_t(b)] >= 0) now.insert({std::min(num[size_t(a)], num[size_t(b)]), std::max(num[size_t(a)], num[size_t(b)])});
|
||||||
|
creases = std::move(now);
|
||||||
|
}
|
||||||
removeUnused(m);
|
removeUnused(m);
|
||||||
}
|
}
|
||||||
if (mirrorOnly) return m;
|
if (mirrorOnly) return m;
|
||||||
if (o.round > 0.0f) bevelEdges(m, sharpEdges(m, o.roundAngle), o.round, o.roundSteps);
|
if (o.round > 0.0f) bevelEdges(m, sharpEdges(m, o.roundAngle), o.round, o.roundSteps), creases.clear(); // (rounded: its points renumbered)
|
||||||
for (int i = 0; i < std::min(o.smoothLevels, 4); ++i) subdivide(m, true, o.smoothKeepSharp ? 30.0f : 180.0f);
|
for (int i = 0; i < std::min(o.smoothLevels, 4); ++i) subdivide(m, true, o.smoothKeepSharp ? 30.0f : 180.0f, &creases);
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,6 +248,11 @@ std::string sceneText(const Scene& scene) {
|
||||||
if (o.mirror) out << " mirror " << o.mirror << "\n";
|
if (o.mirror) out << " mirror " << o.mirror << "\n";
|
||||||
if (o.round > 0.0f) out << " round " << o.round << " " << o.roundSteps << " " << o.roundAngle << "\n";
|
if (o.round > 0.0f) out << " round " << o.round << " " << o.roundSteps << " " << o.roundAngle << "\n";
|
||||||
if (o.smoothLevels) out << " subdivide " << o.smoothLevels << " " << o.smoothKeepSharp << "\n";
|
if (o.smoothLevels) out << " subdivide " << o.smoothLevels << " " << o.smoothKeepSharp << "\n";
|
||||||
|
if (!o.creases.empty()) {
|
||||||
|
out << " crease";
|
||||||
|
for (const auto& [a, b] : o.creases) out << " " << a << " " << b;
|
||||||
|
out << "\n";
|
||||||
|
}
|
||||||
if (!o.pictureFile.empty()) out << " picture " << quoted(o.pictureFile) << "\n";
|
if (!o.pictureFile.empty()) out << " picture " << quoted(o.pictureFile) << "\n";
|
||||||
for (const PictureLayer& l : o.layers) {
|
for (const PictureLayer& l : o.layers) {
|
||||||
if (l.file.empty()) continue;
|
if (l.file.empty()) continue;
|
||||||
|
|
@ -356,6 +380,8 @@ std::optional<Scene> parseScene(std::istream& in, const std::string& path, std::
|
||||||
} else if (word == "mirror") l >> o->mirror;
|
} else if (word == "mirror") l >> o->mirror;
|
||||||
else if (word == "round") l >> o->round >> o->roundSteps >> o->roundAngle;
|
else if (word == "round") l >> o->round >> o->roundSteps >> o->roundAngle;
|
||||||
else if (word == "subdivide") l >> o->smoothLevels >> o->smoothKeepSharp;
|
else if (word == "subdivide") l >> o->smoothLevels >> o->smoothKeepSharp;
|
||||||
|
else if (word == "crease")
|
||||||
|
for (int a, b; l >> a >> b;) o->creases.insert({std::min(a, b), std::max(a, b)});
|
||||||
else if (word == "picture") o->pictureFile = readQuoted(l);
|
else if (word == "picture") o->pictureFile = readQuoted(l);
|
||||||
else if (word == "layer") {
|
else if (word == "layer") {
|
||||||
PictureLayer p;
|
PictureLayer p;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
@ -64,6 +65,7 @@ struct Object {
|
||||||
int roundSteps = 3;
|
int roundSteps = 3;
|
||||||
int smoothLevels = 0;
|
int smoothLevels = 0;
|
||||||
bool smoothKeepSharp = false;
|
bool smoothKeepSharp = false;
|
||||||
|
std::set<std::pair<int, int>> creases; // edges (points, the lower first) smoothing keeps sharp
|
||||||
bool hasFinish() const { return mirror || round > 0.0f || smoothLevels > 0; }
|
bool hasFinish() const { return mirror || round > 0.0f || smoothLevels > 0; }
|
||||||
// A painted picture on it: where each face corner sits on it (0..1), and the picture
|
// A painted picture on it: where each face corner sits on it (0..1), and the picture
|
||||||
// (shared with the undo steps until it's painted on again). Saved beside the .fry as a PNG.
|
// (shared with the undo steps until it's painted on again). Saved beside the .fry as a PNG.
|
||||||
|
|
|
||||||
|
|
@ -267,11 +267,11 @@ void bevelEdges(Mesh& m, const std::set<std::pair<int, int>>& want, float width,
|
||||||
|
|
||||||
// ── Subdivide (Catmull–Clark) ──
|
// ── Subdivide (Catmull–Clark) ──
|
||||||
|
|
||||||
void subdivide(Mesh& m, bool smooth, float keepAngle) {
|
void subdivide(Mesh& m, bool smooth, float keepAngle, std::set<std::pair<int, int>>* creases) {
|
||||||
if (m.faces.empty()) return;
|
if (m.faces.empty()) return;
|
||||||
const auto d = directed(m);
|
const auto d = directed(m);
|
||||||
const std::set<Edge> sharp = keepAngle < 179.9f ? sharpEdges(m, keepAngle) : std::set<Edge>{};
|
const std::set<Edge> sharp = keepAngle < 179.9f ? sharpEdges(m, keepAngle) : std::set<Edge>{};
|
||||||
auto crease = [&](int a, int b) { return !d.count({a, b}) || !d.count({b, a}) || sharp.count(key(a, b)); };
|
auto crease = [&](int a, int b) { return !d.count({a, b}) || !d.count({b, a}) || sharp.count(key(a, b)) || (creases && creases->count(key(a, b))); };
|
||||||
Mesh out;
|
Mesh out;
|
||||||
out.verts = m.verts;
|
out.verts = m.verts;
|
||||||
std::vector<int> facePt(m.faces.size());
|
std::vector<int> facePt(m.faces.size());
|
||||||
|
|
@ -317,6 +317,23 @@ void subdivide(Mesh& m, bool smooth, float keepAngle) {
|
||||||
for (size_t i = 0; i < n; ++i)
|
for (size_t i = 0; i < n; ++i)
|
||||||
out.faces.push_back({fv[i], edgePt[key(fv[i], fv[(i + 1) % n])], facePt[f], edgePt[key(fv[(i + n - 1) % n], fv[i])]});
|
out.faces.push_back({fv[i], edgePt[key(fv[i], fv[(i + 1) % n])], facePt[f], edgePt[key(fv[(i + n - 1) % n], fv[i])]});
|
||||||
}
|
}
|
||||||
|
if (creases) { // (each crease is now two edges, through its new middle point; numbered as removeUnused will)
|
||||||
|
std::vector<int> to(out.verts.size(), -1);
|
||||||
|
int next = 0;
|
||||||
|
std::vector<bool> used(out.verts.size());
|
||||||
|
for (const auto& f : out.faces)
|
||||||
|
for (int v : f) used[size_t(v)] = true;
|
||||||
|
for (size_t v = 0; v < used.size(); ++v)
|
||||||
|
if (used[v]) to[v] = next++;
|
||||||
|
std::set<std::pair<int, int>> now;
|
||||||
|
for (const auto& [a, b] : *creases) {
|
||||||
|
const auto it = edgePt.find(key(a, b));
|
||||||
|
if (it == edgePt.end()) continue; // (not an edge any more)
|
||||||
|
for (const auto& [p, q] : {std::pair{a, it->second}, std::pair{it->second, b}})
|
||||||
|
if (to[size_t(p)] >= 0 && to[size_t(q)] >= 0) now.insert(key(to[size_t(p)], to[size_t(q)]));
|
||||||
|
}
|
||||||
|
*creases = std::move(now);
|
||||||
|
}
|
||||||
m = std::move(out);
|
m = std::move(out);
|
||||||
removeUnused(m);
|
removeUnused(m);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -449,6 +449,28 @@ int main() {
|
||||||
CHECK(checkForPrint(o).closed() && checkForPrint(o).volume < 0.05f * 0.04f * 0.01f, "an O has its hole (%g m3)", checkForPrint(o).volume);
|
CHECK(checkForPrint(o).closed() && checkForPrint(o).volume < 0.05f * 0.04f * 0.01f, "an O has its hole (%g m3)", checkForPrint(o).volume);
|
||||||
CHECK(makeText({}, "x", st, &err).faces.empty() && !err.empty(), "no font: nothing, and why");
|
CHECK(makeText({}, "x", st, &err).faces.empty() && !err.empty(), "no font: nothing, and why");
|
||||||
}
|
}
|
||||||
|
// Creases: a cube smoothed with all its edges creased stays a cube; with none it rounds off;
|
||||||
|
// mirrored, the copy's creases hold too; and they're saved.
|
||||||
|
{
|
||||||
|
Object o;
|
||||||
|
o.id = 1, o.mesh = makeCube(1.0f), o.smoothLevels = 2;
|
||||||
|
const float round = checkForPrint(finishedMesh(o)).volume;
|
||||||
|
for (auto [a, b] : edges(o.mesh)) o.creases.insert({a, b});
|
||||||
|
const float kept = checkForPrint(finishedMesh(o)).volume;
|
||||||
|
CHECK(round < 0.8f && std::abs(kept - 1.0f) < 1e-4f, "creased all round: still a cube (%.3f; smooth %.3f)", kept, round);
|
||||||
|
Object half;
|
||||||
|
half.id = 2, half.mesh = makeCube(1.0f), half.smoothLevels = 2, half.mirror = 1;
|
||||||
|
transform(half.mesh, glm::translate(glm::mat4(1), glm::vec3(0.5f, 0, 0)));
|
||||||
|
for (auto [a, b] : edges(half.mesh)) half.creases.insert({a, b});
|
||||||
|
const float both = checkForPrint(finishedMesh(half)).volume;
|
||||||
|
CHECK(std::abs(both - 2.0f) < 1e-3f, "mirrored, creased: two cubes' worth, square (%.3f)", both);
|
||||||
|
Scene sc;
|
||||||
|
sc.objects.push_back(o);
|
||||||
|
const auto file = (std::filesystem::temp_directory_path() / "fry3dx_test" / "crease.fry").string();
|
||||||
|
std::filesystem::create_directories(std::filesystem::path(file).parent_path());
|
||||||
|
auto back = saveScene(file, sc) ? loadScene(file) : std::nullopt;
|
||||||
|
CHECK(back && back->objects.size() == 1 && back->objects[0].creases == o.creases, "creases saved and back");
|
||||||
|
}
|
||||||
// A slicer's layer: a 1 m cube cut halfway is a 4 m outline, a cylinder's a circle's.
|
// A slicer's layer: a 1 m cube cut halfway is a 4 m outline, a cylinder's a circle's.
|
||||||
{
|
{
|
||||||
float len = 0;
|
float len = 0;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue