From 0f3e8fbf279adf9aa3c115ae75278ac9584efcac Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Fri, 9 Oct 2026 16:48:48 -0300 Subject: [PATCH] Add center of mass markers to Prepare and Preview (#16291) Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Co-authored-by: Kris Austin --- docs/HLSD/center-of-mass.md | 172 +++++++++ docs/HLSD/separated-infills.md | 4 +- src/libslic3r/AppConfig.cpp | 3 + src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/ConnectedBodies.cpp | 292 +++++++++++++++ src/libslic3r/ConnectedBodies.hpp | 61 ++++ src/libslic3r/GCode.cpp | 152 ++++++++ src/libslic3r/GCode/GCodeProcessor.cpp | 77 +++- src/libslic3r/GCode/GCodeProcessor.hpp | 62 ++++ src/libslic3r/PrintObject.cpp | 75 +--- src/libslic3r/TriangleMesh.cpp | 34 ++ src/libslic3r/TriangleMesh.hpp | 15 + src/slic3r/GUI/GCodeViewer.cpp | 12 + src/slic3r/GUI/GCodeViewer.hpp | 8 + src/slic3r/GUI/GLCanvas3D.cpp | 338 ++++++++++++++++++ src/slic3r/GUI/GLCanvas3D.hpp | 68 ++++ src/slic3r/GUI/GUI_App.hpp | 3 + src/slic3r/GUI/Plater.cpp | 3 + tests/fff_print/test_gcodeprocessor.cpp | 245 ++++++++++++- tests/libslic3r/CMakeLists.txt | 1 + tests/libslic3r/test_connected_bodies.cpp | 154 ++++++++ tests/libslic3r/test_indexed_triangle_set.cpp | 77 ++++ 22 files changed, 1781 insertions(+), 77 deletions(-) create mode 100644 docs/HLSD/center-of-mass.md create mode 100644 src/libslic3r/ConnectedBodies.cpp create mode 100644 src/libslic3r/ConnectedBodies.hpp create mode 100644 tests/libslic3r/test_connected_bodies.cpp diff --git a/docs/HLSD/center-of-mass.md b/docs/HLSD/center-of-mass.md new file mode 100644 index 0000000000..f857497e5f --- /dev/null +++ b/docs/HLSD/center-of-mass.md @@ -0,0 +1,172 @@ +# Center of mass markers — High Level Design + +## Purpose and scope + +The "Center of mass" item of the canvas toolbar menu, in the bottom left corner of the 3D view, marks +where the mass of the plate, of each object instance and of each body of an assembly is centered, +in Prepare and in Preview. It helps judge how parts will rest on the plate, for instance whether a +tall or leaning part could tip. It is a view setting: it changes nothing in the model, the slice or +the project file, and it does not reach plate thumbnails. The choice is kept in the app config as +`show_center_of_mass`, off by default. The assembly view and the Design tab have no markers. + +Three kinds of marker share one shape, a sphere whose octants alternate between two colors: + +- each plate, black and white, for everything on it; +- each object instance, light blue and white; +- each body of an assembly, red and yellow; +- in Preview, the supports and raft of each object instance, green and black. + +A click on a marker opens a box beside it with the weight and volume of what it stands for, where its +center lies in that thing's bounding box and the size of the box, and its moments of inertia about +axes through the center parallel to x, y and z. + +An assembly is an object of several parts or with negative volumes. Its bodies are the connected +solids its parts make once united, the bodies the separated infills option centers its infill on +(see separated-infills.md): parts that overlap or touch are one body, parts apart are separate +bodies. An object of one body, and every object of a single part, has no body markers, as its object +marker says it all. + +Mass is volume times density, the `filament_density` of the filament that prints it, or 1.245 g/cm³ +(`DEFAULT_FILAMENT_DENSITY`) for a filament without one. Prepare has the model only, so it takes each +part as a solid of the density of its filament, the part's own or else its object's. It reads each +filament's density from the filament's selected preset, edits not yet saved included, as slicing does: +the plater's own config holds the values of the filament edited last only. Preview has +what will be printed, so its markers come from the toolpaths, whose mass depends on walls, infill +and flow as well. There the solid markers are for what is printed up to the top layer the layer +slider shows: for the plate with brim, raft and supports, where the weight rests at that point of +the print; for an object or a body, its own extrusions. Each has a faded twin for the same at the +end of the print, so the slider shows the weight moving toward where it ends, and at the top layer +the two meet. In Prepare the plate has the model alone, as brim, raft and supports exist only once +sliced. + +## Prepare: from the meshes + +An object of one part takes the mass properties of its mesh at unit density, times its density, from +`its_mass_properties()`, which handles a mesh in a single pass. Each triangle and a fixed vertex of +the mesh span a tetrahedron whose signed volume is `V = a · (b × c) / 6`, with `a`, `b`, `c` taken +relative to that vertex. By the divergence theorem these volumes add up to the volume of a closed +mesh, their volume-weighted centroids to its center of mass, and their second moments +`V (a aᵀ + b bᵀ + c cᵀ + s sᵀ) / 20`, with `s = a + b + c`, to its own. Shells facing inward, such as +a cavity, subtract themselves, and flipping every triangle changes nothing. The sums are kept in +double precision and relative to a vertex of the mesh rather than the origin, which keeps them exact +for meshes far from it. The result keeps the spread of the mass about its center, +`(x - c)(x - c)ᵀ` averaged over the mass, from which the moments of inertia follow. + +The CGAL routines that look alike do not compute this. `CGAL::centroid` weighs tetrahedra by their +unsigned volume, so it fails on cavities and on any shell that is not star-shaped from the fan's +apex; over triangles it returns the centroid of the surface, and over points the average of the +vertices, which depends on the tessellation. `CGAL::barycenter` with the signed volumes as weights +gives the same answer, but only after copying every tetrahedron into a vector of weighted points, +and takes two and a half times as long. + +A mesh's result is in its own coordinates. Each `GLVolume` maps it to the world with its world +matrix `M`, and weighs it by the volume times the absolute determinant of that matrix: a center of +mass moves with any affine map, and the spread becomes `L S Lᵀ` for the linear part `L` of `M`, so no +mesh is ever transformed. The `GLVolume`'s matrices, rather than the +model's, let the markers follow an object while it is dragged, before the model is updated. Results +are cached by `ModelVolume` id; a `ModelVolume` takes a new id whenever its mesh changes, which is +the rule `reload_scene()` relies on to rebuild a `GLVolume`'s geometry, so a cached result never +outlives its mesh. + +An assembly's parts overlap or touch, which the mesh formula would count twice, so `solid_bodies()` +slices them instead, in the object's coordinates. It cuts the height into 500 slabs, 100 while a part +is dragged, slices every part and negative volume at the middle of each slab, unites the parts and +cuts the negative volumes away, and links the islands of neighboring slabs that overlap into bodies +with `connected_bodies()`. Each island adds a prism of the slab's thickness at the density: its +area, and its first and second moments of area, from the same sums over the outline as the area, +with the slab's height for z. Where parts of different densities overlap, the later volume of the object counts, as +slicing clips every part by the parts after it; each part then weighs the region it prints, which is +credited to the island holding it. Each body also keeps the convex hull of its islands and the height +they span, whose corners, once transformed, give its bounding box, tight while the instance turns +about z only. The object is the sum of its bodies, its box that of its parts, as the object's size +shows it, and each plate the sum of the object instances `PartPlateList::find_instance()` puts on it, +so that an instance on no plate counts in none. The bodies are cached by `ModelObject` id with the volumes, types, densities and +transformations they were sliced from. + +## Preview: from the toolpaths + +`GCodeProcessor` sums the masses while it processes the G-code, in the same pass that builds the +moves, and leaves them in `GCodeProcessorResult`; `GCodeViewer` keeps a copy of them when it loads a +result. Nothing is stored per move. + +Each extrusion weighs the volume of filament its E extrudes times the density of the filament that +extrudes it, so a print of several materials weighs each as it is. Flow ratio, line widths, ironing +and purging into infill all count +as printed. Its mass spreads evenly along the segment the bead's center runs, half the layer height +below the nozzle, in the frame of the stored moves: plate offset added, Z offset removed. Such a +segment from `a` to `b` adds `m (a + b) / 2` to the moments and `m (a² + a b + b²) / 3` to the second +moments along each axis, and its box widened by half the bead's height to the bounding box; not by +half its width, which the processor only estimates, so that a box runs along the walls' center lines. Arcs are already split into segments by the processor. Walls, infill, top and bottom surfaces, ironing +and gap fill make the parts. The brim and the support roles, the raft among them, count only in what +the plate prints. The skirt, the prime tower and custom G-code count nowhere. + +The plate takes every extrusion, so it needs nothing more. The objects and bodies need the sliced +objects, which the G-code does not describe, so the G-code export hands the processor a locator +built from the `Print`; G-code opened from a file, or from a project sliced earlier, has no `Print` +behind it, and so shows the plate alone. Object labels would not do: profiles turn the four kinds +Orca writes on and off in every combination, and none of them tells the bodies apart. + +The locator numbers the object instances and, for each assembly, the bodies of every instance. It +takes the bodies `PrintObject::prepare_infill()` found for separated infills, or, when that option +did not need them, links the islands (`Layer::lslices`) of neighboring layers into bodies with the +same `connected_bodies()`. For each extrusion of a part, it finds the first layer printed at or above +its height, as spiral vase rises through each layer, and the island holding it with an +`IslandLocator`, the one `solid_bodies()` credits its regions with: by the island's box, widened by +1 mm for walls reaching past it, with a polygon test only where boxes overlap, and the nearest +outline where none holds the point. The boxes of one layer's islands say nothing of the other +instances, so an instance whose widened box reaches another's, as copies placed side by side do, +tests the outlines alone, and outside them the nearest outline of all such instances wins. The island +gives both the instance and the body. The island found last is tried first, as extrusions mostly follow each other on one +island. Brim, raft and supports lie outside the islands. The brim counts in the plate only; a support +or raft extrusion goes to the instance whose footprint, the box of its widened islands, holds it, the +one whose center is nearest among several, or else the nearest footprint, as supports stand below and +around their object. + +Each mass holds, for each layer id, the running total of what is printed up to that layer, the last +of which is the faded marker's, so the solid marker for any slider position is a single lookup. The layer ids are those +the moves carry, which are also the layers of libvgcode and of the slider; in a print by object they +follow the order of printing, so the solid markers show the objects printed so far as they are. + +## Drawing + +`smooth_sphere()` with a resolution divisible by four leaves every triangle within one octant, so it +splits into two models drawn with the `gouraud_light` shader in each kind's two colors. The radius is +9 pixels for the plate, 7 for the objects, 6 for the supports and 5 for the bodies, scaled like the canvas toolbar for the +display's DPI and kept constant on screen through the camera's inverse zoom. They are drawn in that +order, so that markers at one place show as rings. The faded markers are the same spheres at 40% +opacity, drawn before all the solid ones, which show over them where both meet. + +The centers usually lie inside the objects, so the markers are drawn without the depth test and show +through the objects and anything in front of them. Back face culling keeps the far half of a sphere +from covering the near one. They are drawn after the ambient occlusion pass, which would otherwise +darken them as the surface behind them, and before FXAA, which smooths their edges. + +The markers are part of the cached scene, so toggling them, or changing a filament's density while +they are shown, marks the scene dirty, and moving the layer slider redraws the scene with the solid +markers where they belong. In Prepare they are hidden while any gizmo other than Move, Rotate, Scale +and Lay on face is open, since the others work on the surface a marker would cover, and a hidden +object has no markers. + +## Details + +The markers drawn last are kept, and a left click is tested against them before it selects: each +center and a point a radius to its right are projected to the screen, and the click hits a marker +within that distance. The solid markers are tested before the faded ones and the smaller kinds +before the larger, the order in which they cover each other. A hit opens the details of that marker +and keeps the click from changing the selection; a click anywhere else closes them. The box is an +ImGui window beside the marker, redrawn with the overlay from the markers of the last scene, so it +follows a dragged object, and in Preview the layer slider. It closes when its marker is gone, or when +the number of markers of its kind changes, as then it may stand for something else. + +Its title says what the marker stands for: the plate, an object, an assembly or a part, the body of +an assembly. The G-code export lists the object instances for the processor, marking assemblies, as +it hands it the locator. + +Each marker carries its sums: mass, volume, first moments and the second moments about the origin +along each axis, `Σ m x²`, `Σ m y²` and `Σ m z²`, which add up from parts to objects to plates. The +moment of inertia about the axis through the center parallel to x is then +`m (σy² + σz²)`, with `σ² = Σ m x² / m - c²` along each axis, and likewise for y and z. Masses are +kept in mg, volume times density in g/cm³, and shown in g, volumes in cm³ and moments of inertia in +g·mm². In Preview the box tells the finished print from what is printed up to the layer shown, the +two weighing differently, and both are placed in the bounding box of everything the marker holds +by the end. diff --git a/docs/HLSD/separated-infills.md b/docs/HLSD/separated-infills.md index 10742eb042..4b6227d82a 100644 --- a/docs/HLSD/separated-infills.md +++ b/docs/HLSD/separated-infills.md @@ -18,8 +18,8 @@ each body on its own (see Octree infill). ## Bodies `PrintObject::prepare_infill()` groups the islands of every layer (`lslices`) -into 3D connected bodies before bridges are detected, so bridge anchors and -printed infill share one origin. Islands on adjacent layers belong to one body +into 3D connected bodies with `connected_bodies()` before bridges are detected, +so bridge anchors and printed infill share one origin. Islands on adjacent layers belong to one body when their slices overlap. Parts that touch or overlap form one body. Separate parts, disconnected islands of one mesh, and interleaved parts that never touch, such as chain links, each form their own. Every island stores the index of its diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 984524b0ba..d7aaac9280 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -411,6 +411,9 @@ void AppConfig::set_defaults() if (get("show_overhang").empty()) set_bool("show_overhang", false); + if (get("show_center_of_mass").empty()) + set_bool("show_center_of_mass", false); + #ifdef _WIN32 //#ifdef SUPPORT_3D_CONNEXION diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index ca95cfb364..1f9d4b7c53 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -112,6 +112,8 @@ set(lisbslic3r_sources CommonDefs.hpp Config.cpp Config.hpp + ConnectedBodies.cpp + ConnectedBodies.hpp ContourZ.cpp CustomGCode.cpp CustomGCode.hpp diff --git a/src/libslic3r/ConnectedBodies.cpp b/src/libslic3r/ConnectedBodies.cpp new file mode 100644 index 0000000000..8cfe5a3e43 --- /dev/null +++ b/src/libslic3r/ConnectedBodies.cpp @@ -0,0 +1,292 @@ +#include "ConnectedBodies.hpp" + +#include "AABBTreeIndirect.hpp" +#include "BoundingBox.hpp" +#include "ClipperUtils.hpp" +#include "ExPolygon.hpp" +#include "Geometry/ConvexHull.hpp" +#include "Point.hpp" +#include "Polygon.hpp" +#include "TriangleMesh.hpp" +#include "TriangleMeshSlicer.hpp" +#include "libslic3r.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + +std::vector> connected_bodies(const std::vector &layers, size_t &count, + const std::function &throw_if_canceled) +{ + // Union-find over the islands of all layers, numbered layer after layer. + std::vector first(layers.size() + 1, 0); + for (size_t l = 0; l < layers.size(); ++l) + first[l + 1] = first[l] + layers[l]->size(); + std::vector parent(first.back()); + for (size_t i = 0; i < parent.size(); ++i) + parent[i] = i; + const auto find = [&parent](size_t i) { + while (parent[i] != i) + i = parent[i] = parent[parent[i]]; + return i; + }; + + std::vector> boxes(layers.size()); + for (size_t l = 0; l < layers.size(); ++l) + for (const ExPolygon &island : *layers[l]) + boxes[l].emplace_back(get_extents(island)); + for (size_t l = 0; l + 1 < layers.size(); ++l) { + if (throw_if_canceled) + throw_if_canceled(); + // Index the smaller of the two layers, so that a fragmented layer is not scanned island by island. + size_t a_layer = l; + size_t b_layer = l + 1; + if (layers[a_layer]->size() < layers[b_layer]->size()) + std::swap(a_layer, b_layer); + if (layers[b_layer]->empty()) + continue; + using IslandTree = AABBTreeIndirect::Tree<2, coord_t>; + std::vector wrappers; + wrappers.reserve(boxes[b_layer].size()); + for (size_t b = 0; b < boxes[b_layer].size(); ++b) + wrappers.emplace_back(b, boxes[b_layer][b]); + IslandTree tree; + tree.build_modify_input(wrappers); + for (size_t a = 0; a < boxes[a_layer].size(); ++a) { + const IslandTree::BoundingBox query(boxes[a_layer][a].min, boxes[a_layer][a].max); + AABBTreeIndirect::traverse( + tree, [&query](const IslandTree::Node &node) { return node.bbox.intersects(query); }, + [&](const IslandTree::Node &node) { + // The tree's boxes are widened by an epsilon, and islands already joined need no clipping. + const size_t b = node.idx; + if (boxes[a_layer][a].overlap(boxes[b_layer][b]) && find(first[a_layer] + a) != find(first[b_layer] + b) && + !intersection_ex((*layers[a_layer])[a], (*layers[b_layer])[b]).empty()) + parent[find(first[a_layer] + a)] = find(first[b_layer] + b); + return true; + }); + } + } + + std::vector body(parent.size(), std::numeric_limits::max()); + std::vector> out(layers.size()); + count = 0; + for (size_t l = 0; l < layers.size(); ++l) + for (size_t i = 0; i < layers[l]->size(); ++i) { + size_t &b = body[find(first[l] + i)]; + if (b == std::numeric_limits::max()) + b = count++; + out[l].emplace_back(b); + } + return out; +} + +IslandLocator::IslandLocator(const ExPolygons &islands, coord_t margin) : m_islands(&islands), m_alone(islands.size(), true) +{ + m_boxes.reserve(islands.size()); + for (const ExPolygon &island : islands) + m_boxes.emplace_back(get_extents(island).inflated(margin)); + // Sweep the boxes along x, so that only those reaching each other are compared. + std::vector order(m_boxes.size()); + for (size_t i = 0; i < order.size(); ++i) + order[i] = i; + std::sort(order.begin(), order.end(), [this](size_t l, size_t r) { return m_boxes[l].min.x() < m_boxes[r].min.x(); }); + for (size_t a = 0; a < order.size(); ++a) + for (size_t b = a + 1; b < order.size() && m_boxes[order[b]].min.x() <= m_boxes[order[a]].max.x(); ++b) + if (m_boxes[order[a]].overlap(m_boxes[order[b]])) + m_alone[order[a]] = m_alone[order[b]] = false; +} + +bool IslandLocator::holds(size_t island, const Point &point, bool strict) const +{ + return m_boxes[island].contains(point) && ((m_alone[island] && !strict) || (*m_islands)[island].contains(point)); +} + +std::pair IslandLocator::find(const Point &point, bool strict) const +{ + int nearest = -1; + double distance = std::numeric_limits::max(); + for (size_t i = 0; i < m_boxes.size(); ++i) + if (m_boxes[i].contains(point)) { + if ((m_alone[i] && !strict) || (*m_islands)[i].contains(point)) + return { int(i), 0. }; + if (const double d = ((*m_islands)[i].point_projection(point) - point).cast().squaredNorm(); d < distance) { + distance = d; + nearest = int(i); + } + } + return { nearest, distance }; +} + +// The area of polygons and their first and second moments of area, which holes, running clockwise, subtract. +struct AreaMoments +{ + double area{ 0. }; + Vec2d first{ Vec2d::Zero() }; + // Of x^2, y^2 and xy. + Vec3d second{ Vec3d::Zero() }; + + void add(const Polygon &polygon) + { + if (polygon.points.size() < 3) + return; + Vec2d p1 = unscaled(polygon.points.back()); + for (const Point &point : polygon.points) { + const Vec2d p2 = unscaled(point); + const double a = cross2(p1, p2); + area += a / 2.; + first += a / 6. * (p1 + p2); + second += a / 12. * + Vec3d(p1.x() * p1.x() + p1.x() * p2.x() + p2.x() * p2.x(), p1.y() * p1.y() + p1.y() * p2.y() + p2.y() * p2.y(), + p1.x() * p1.y() + p2.x() * p2.y() + 0.5 * (p1.x() * p2.y() + p2.x() * p1.y())); + p1 = p2; + } + } +}; + +// Mass, volume and the first and second moments of mass about the origin. +struct Moments +{ + double mass{ 0. }; + double volume{ 0. }; + Vec3d first{ Vec3d::Zero() }; + Matrix3d second{ Matrix3d::Zero() }; + + void add(const Moments &other) + { + mass += other.mass; + volume += other.volume; + first += other.first; + second += other.second; + } +}; + +BoundingBoxf3 SolidBody::bounding_box(const Transform3d &trafo) const +{ + BoundingBoxf3 box; + for (const Point &point : hull.points) + for (const double z : { z_min, z_max }) + box.merge(trafo * Vec3d(unscaled(point.x()), unscaled(point.y()), z)); + return box; +} + +std::vector solid_bodies(const std::vector &solids, const std::vector &densities, + const std::vector &negatives, size_t slabs) +{ + assert(densities.size() == solids.size()); + double z_min = std::numeric_limits::max(); + double z_max = std::numeric_limits::lowest(); + for (const auto &[mesh, trafo] : solids) + for (const stl_vertex &v : mesh->vertices) { + const double z = (trafo * v.cast()).z(); + z_min = std::min(z_min, z); + z_max = std::max(z_max, z); + } + if (z_min >= z_max || slabs == 0) + return {}; + + // Each slab sliced at its middle. + const double thickness = (z_max - z_min) / double(slabs); + std::vector zs(slabs); + for (size_t k = 0; k < slabs; ++k) + zs[k] = float(z_min + (double(k) + 0.5) * thickness); + + MeshSlicingParamsEx params; + const auto slice = [&zs, ¶ms](const MeshInPlace &mesh) { + params.trafo = mesh.second; + return slice_mesh_ex(*mesh.first, zs, params); + }; + std::vector> slices; + for (const MeshInPlace &solid : solids) + slices.emplace_back(slice(solid)); + std::vector cut(slabs); + for (const MeshInPlace &negative : negatives) { + std::vector slices_negative = slice(negative); + for (size_t k = 0; k < slabs; ++k) + append(cut[k], std::move(slices_negative[k])); + } + + // The islands of each slab, and the moments of what each solid prints of them with its density. + const bool uniform = std::all_of(densities.begin(), densities.end(), [&densities](double d) { return d == densities.front(); }); + std::vector islands(slabs); + std::vector> moments(slabs); + tbb::parallel_for(tbb::blocked_range(0, slabs), [&](const tbb::blocked_range &range) { + for (size_t k = range.begin(); k < range.end(); ++k) { + ExPolygons all; + for (const std::vector &solid : slices) + append(all, solid[k]); + islands[k] = diff_ex(union_ex(all), cut[k]); + moments[k].assign(islands[k].size(), {}); + const double z = zs[k]; + const auto add = [&](const ExPolygon ®ion, double density, size_t island) { + AreaMoments area; + area.add(region.contour); + for (const Polygon &hole : region.holes) + area.add(hole); + if (area.area <= 0.) + return; + // A prism of the slab's thickness. + Matrix3d second; + second << area.second.x(), area.second.z(), area.first.x() * z, area.second.z(), area.second.y(), area.first.y() * z, + area.first.x() * z, area.first.y() * z, area.area * (z * z + thickness * thickness / 12.); + moments[k][island].add({ density * area.area * thickness, area.area * thickness, + density * thickness * Vec3d(area.first.x(), area.first.y(), area.area * z), density * thickness * second }); + }; + if (uniform) { + for (size_t j = 0; j < islands[k].size(); ++j) + add(islands[k][j], densities.front(), j); + continue; + } + // A later solid prints where it overlaps an earlier one, and each region it prints lies in one island. + const IslandLocator locator(islands[k], 10); + ExPolygons later = cut[k]; + for (size_t i = solids.size(); i-- > 0;) + if (!slices[i][k].empty()) { + for (const ExPolygon ®ion : diff_ex(slices[i][k], later)) + if (const int island = locator.find(region.contour.points.front()).first; island >= 0) + add(region, densities[i], size_t(island)); + later = union_ex(later, slices[i][k]); + } + } + }); + + std::vector layers; + layers.reserve(slabs); + for (const ExPolygons &layer : islands) + layers.emplace_back(&layer); + size_t count = 0; + const std::vector> bodies = connected_bodies(layers, count); + std::vector sums(count); + std::vector outlines(count); + std::vector out(count); + for (SolidBody &body : out) { + body.z_min = std::numeric_limits::max(); + body.z_max = std::numeric_limits::lowest(); + } + for (size_t k = 0; k < slabs; ++k) + for (size_t j = 0; j < islands[k].size(); ++j) { + const size_t body = bodies[k][j]; + sums[body].add(moments[k][j]); + append(outlines[body], islands[k][j].contour.points); + out[body].z_min = std::min(out[body].z_min, zs[k] - 0.5 * thickness); + out[body].z_max = std::max(out[body].z_max, zs[k] + 0.5 * thickness); + } + for (size_t body = 0; body < count; ++body) + if (const Moments &sum = sums[body]; sum.mass > 0.) { + const Vec3d center = sum.first / sum.mass; + MassProperties &solid = out[body]; + solid = { sum.mass, sum.volume, center, sum.second / sum.mass - center * center.transpose() }; + out[body].hull = Geometry::convex_hull(std::move(outlines[body])); + } + return out; +} + +} // namespace Slic3r diff --git a/src/libslic3r/ConnectedBodies.hpp b/src/libslic3r/ConnectedBodies.hpp new file mode 100644 index 0000000000..a420401d28 --- /dev/null +++ b/src/libslic3r/ConnectedBodies.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include "BoundingBox.hpp" +#include "ExPolygon.hpp" +#include "Point.hpp" +#include "Polygon.hpp" +#include "TriangleMesh.hpp" +#include "libslic3r.h" + +#include + +#include +#include +#include +#include + +namespace Slic3r { + +// The connected body of each island of each layer: islands of adjacent layers whose slices overlap are one body. +// Bodies are numbered from 0 in the order of their first island. +std::vector> connected_bodies(const std::vector &layers, size_t &count, + const std::function &throw_if_canceled = nullptr); + +// Finds the island of a layer that a point lies in, testing the polygons only where the boxes of several islands hold it. +class IslandLocator +{ +public: + // The islands must outlive the locator. Their boxes are widened by the margin, for points reaching past an outline. + IslandLocator(const ExPolygons &islands, coord_t margin); + // Whether the island holds the point, or its box does where no other box reaches unless strict. + bool holds(size_t island, const Point &point, bool strict = false) const; + // The island holding the point as above, else the nearest one whose box holds it, with the squared distance to it; -1 + // for none. + std::pair find(const Point &point, bool strict = false) const; + const std::vector &boxes() const { return m_boxes; } + +private: + const ExPolygons *m_islands; + std::vector m_boxes; + std::vector m_alone; +}; + +using MeshInPlace = std::pair; + +// A connected body of solids, with its outline seen from above as a convex hull and the height it spans. +struct SolidBody : MassProperties +{ + Polygon hull; + double z_min{ 0. }; + double z_max{ 0. }; + + // Its box once transformed, tight for a transformation that rotates about z only. + BoundingBoxf3 bounding_box(const Transform3d &trafo) const; +}; + +// Each connected body of the union of the solids less the negatives, sliced into slabs, each solid weighing its density. +// Where solids overlap, the later one counts, as slicing prints it. +std::vector solid_bodies(const std::vector &solids, const std::vector &densities, + const std::vector &negatives, size_t slabs); + +} // namespace Slic3r diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index d29363e639..accb67b3da 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -24,6 +24,7 @@ #include "Polygon.hpp" #include "Polyline.hpp" #include "PrintBase.hpp" +#include "ConnectedBodies.hpp" #include "PrintConfig.hpp" #include "enum_bitmask.hpp" #include "libslic3r.h" @@ -2590,6 +2591,156 @@ WipeTowerType GCode::wipe_tower_type() return WipeTowerType::Type2; } +// Numbers the object instances and the connected bodies of the instances of several, for the processor to find those an +// extrusion lies in. +static void set_mass_locator(GCodeProcessor &processor, const Print &print) +{ + struct Object + { + const PrintObject *object; + int first_instance; + // No bodies for an object of one. + size_t bodies_count; + int first_body; + std::vector print_zs; + // Per layer, the body of each island and a locator whose boxes are widened for walls reaching past them. + std::vector> bodies; + std::vector islands; + // Per instance, whether its widened box reaches another's, so that the box of an island proves nothing. + std::vector crowded; + }; + std::vector objects; + std::vector object_masses; + int bodies_total = 0; + for (const PrintObject *object : print.objects()) { + const auto layers = object->layers(); + if (layers.empty()) + continue; + // Bodies for assemblies only, as the Prepare tab counts them: those separated infills found, if it needed them. + const ModelVolumePtrs &volumes = object->model_object()->volumes; + const bool assembly = std::count_if(volumes.begin(), volumes.end(), [](const ModelVolume *v) { return v->is_model_part(); }) > 1 || + std::any_of(volumes.begin(), volumes.end(), [](const ModelVolume *v) { return v->is_negative_volume(); }); + size_t count = 0; + std::vector> bodies; + if (assembly) { + count = object->separated_body_bboxes().size(); + if (count > 0 && std::all_of(layers.begin(), layers.end(), [](const Layer *l) { return l->lslices_separated_component_ids.size() == l->lslices.size(); })) + for (const Layer *layer : layers) + bodies.emplace_back(layer->lslices_separated_component_ids); + else { + std::vector islands; + for (const Layer *layer : layers) + islands.emplace_back(&layer->lslices); + bodies = connected_bodies(islands, count); + } + } + if (count < 2) { + count = 0; + bodies.assign(layers.size(), {}); + } + Object &o = objects.emplace_back(Object{ object, int(object_masses.size()), count, bodies_total, {}, std::move(bodies), {}, {} }); + object_masses.resize(object_masses.size() + object->instances().size()); + for (size_t instance = 0; instance < object->instances().size(); ++instance) + object_masses[o.first_instance + instance].assembly = assembly; + bodies_total += int(count * object->instances().size()); + for (const Layer *layer : layers) { + o.print_zs.emplace_back(layer->print_z); + o.islands.emplace_back(layer->lslices, scaled(1.)); + } + } + if (objects.empty()) + return; + std::vector boxes; + for (const Object &o : objects) { + BoundingBox box; + for (const IslandLocator &islands : o.islands) + for (const BoundingBox &island : islands.boxes()) + box.merge(island); + for (const PrintInstance &instance : o.object->instances()) { + BoundingBox &moved = boxes.emplace_back(box); + moved.translate(instance.shift); + } + } + for (Object &o : objects) + for (size_t instance = 0; instance < o.object->instances().size(); ++instance) { + const size_t i = o.first_instance + instance; + o.crowded.emplace_back(false); + for (size_t j = 0; j < boxes.size() && !o.crowded.back(); ++j) + o.crowded.back() = j != i && boxes[i].overlap(boxes[j]); + } + + struct Hit + { + size_t object{ 0 }, instance{ 0 }, layer{ 0 }, island{ 0 }; + }; + auto locate = [objects = std::move(objects), footprints = std::move(boxes), + last = std::optional()](const Vec3d &point, bool support) mutable -> GCodeProcessor::MassLocation { + // Supports stand below and around their object: the instance whose footprint holds the point, the one whose center + // is nearest among several, else the nearest footprint. + if (support) { + const Point p(scaled(point.x()), scaled(point.y())); + int found = -1; + bool inside = false; + double best = std::numeric_limits::max(); + for (size_t i = 0; i < footprints.size(); ++i) { + const BoundingBox &box = footprints[i]; + const double gap = Point((box.min - p).cwiseMax(p - box.max).cwiseMax(0)).cast().squaredNorm(); + const bool in = gap == 0.; + const double d = in ? (box.center() - p).cast().squaredNorm() : gap; + if ((in && !inside) || (in == inside && d < best)) { + found = int(i); + inside = in; + best = d; + } + } + return { found, -1 }; + } + constexpr double z_tolerance = 0.002; + const auto local = [&point, &objects](size_t object, size_t instance) { + return Point(Point(scaled(point.x()), scaled(point.y())) - objects[object].object->instances()[instance].shift); + }; + const auto location = [&objects, &last](const Hit &hit) { + last = hit; + const Object &o = objects[hit.object]; + return GCodeProcessor::MassLocation{ o.first_instance + int(hit.instance), + o.bodies_count == 0 ? -1 : o.first_body + int(hit.instance * o.bodies_count + o.bodies[hit.layer][hit.island]) }; + }; + // A point lies on the first layer at or above it, as spiral vase rises through each layer. + // Extrusions mostly follow each other on one island. + if (last) { + const Object &o = objects[last->object]; + if (point.z() <= o.print_zs[last->layer] + z_tolerance && + (last->layer == 0 || point.z() > o.print_zs[last->layer - 1] + z_tolerance) && + o.islands[last->layer].holds(last->island, local(last->object, last->instance), o.crowded[last->instance])) + return location(*last); + } + // Outside the islands of instances crowding each other, the nearest outline. + std::optional nearest; + double distance = std::numeric_limits::max(); + for (size_t object = 0; object < objects.size(); ++object) { + const Object &o = objects[object]; + const auto z = std::lower_bound(o.print_zs.begin(), o.print_zs.end(), point.z() - z_tolerance); + if (z == o.print_zs.end()) + continue; + const size_t layer = size_t(z - o.print_zs.begin()); + for (size_t instance = 0; instance < o.object->instances().size(); ++instance) { + const auto [island, d] = o.islands[layer].find(local(object, instance), o.crowded[instance]); + if (island < 0) + continue; + const Hit hit{ object, instance, layer, size_t(island) }; + if (d == 0. || !o.crowded[instance]) + return location(hit); + if (d < distance) { + distance = d; + nearest = hit; + } + } + } + return nearest ? location(*nearest) : GCodeProcessor::MassLocation{}; + }; + processor.set_mass_locator(std::move(locate), std::move(object_masses)); +} + void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb) { PROFILE_CLEAR(); @@ -3112,6 +3263,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // modifies m_silent_time_estimator_enabled DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled, print.get_layered_nozzle_group_result()); + set_mass_locator(m_processor, print); const bool is_bbl_printers = print.is_BBL_printer(); const bool skip_config_block = print.config().gcode_skip_config_block; const WipeTowerType wipe_tower_type = print.wipe_tower_type(); diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index ac5a435bcd..8e7d5e7557 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -89,7 +89,6 @@ static const float DEFAULT_TRAVEL_ACCELERATION = 1250.0f; static const size_t MIN_EXTRUDERS_COUNT = 5; static const float DEFAULT_FILAMENT_DIAMETER = 1.75f; static const int DEFAULT_FILAMENT_HRC = 0; -static const float DEFAULT_FILAMENT_DENSITY = 1.245f; static const float DEFAULT_FILAMENT_COST = 29.99f; static const int DEFAULT_FILAMENT_VITRIFICATION_TEMPERATURE = 0; static const Slic3r::Vec3f DEFAULT_EXTRUDER_OFFSET = Slic3r::Vec3f::Zero(); @@ -2604,6 +2603,10 @@ void GCodeProcessorResult::reset() { lock(); moves.clear(); + plate_mass = {}; + object_masses.clear(); + body_masses.clear(); + support_masses.clear(); lines_ends.clear(); printable_area = Pointfs(); //BBS: add bed exclude area @@ -3702,6 +3705,7 @@ void GCodeProcessor::reset() m_g1_line_id = 0; m_layer_id = 0; m_cp_color.reset(); + m_mass_locator = nullptr; m_producer = EProducer::Unknown; @@ -3841,6 +3845,7 @@ void GCodeProcessor::process_buffer(const std::string &buffer) void GCodeProcessor::finalize(bool post_process) { m_result.z_offset = m_z_offset; + finalize_object_masses(); // update width/height of wipe moves for (GCodeProcessorResult::MoveVertex& move : m_result.moves) { @@ -5469,6 +5474,9 @@ void GCodeProcessor::process_G1(const std::array, 4>& axes m_seams_detector.set_first_vertex(m_result.moves.back().position - m_extruder_offsets[filament_id] - plate_offset); } + if (type == EMoveType::Extrude) + add_object_mass(filament_id, area_filament_cross_section * delta_pos[E]); + // store move store_move_vertex(type); } @@ -7277,6 +7285,73 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type, } } +void GCodeProcessorResult::ObjectMass::add(const Sum &sum, const BoundingBoxf3 &extent, size_t layer) +{ + box.merge(extent); + if (printed_up_to_layer.size() <= layer) + printed_up_to_layer.resize(layer + 1); + printed_up_to_layer[layer].add(sum); +} + +void GCodeProcessor::add_object_mass(int filament_id, float volume) +{ + // Skirt, prime tower and custom G-code belong to no object. + const ExtrusionRole role = m_extrusion_role; + if (volume <= 0.f || role == erNone || role == erSkirt || role == erWipeTower || role == erCustom || role == erMixed) + return; + + const bool has_density = size_t(filament_id) < m_result.filament_densities.size() && m_result.filament_densities[filament_id] > 0.f; + const double mass = double(volume) * (has_density ? m_result.filament_densities[filament_id] : DEFAULT_FILAMENT_DENSITY); + // In the frame of the stored moves, the bead's center half its height below the nozzle, from the move's start to its end. + const Vec3d half_height = 0.5 * double(m_height) * Vec3d::UnitZ(); + const Vec3d offset = Vec3d(m_x_offset, m_y_offset, -m_z_offset) - half_height + m_extruder_offsets[filament_id].cast(); + const Vec3d start = Vec3d(m_start_position[X], m_start_position[Y], m_start_position[Z]) + offset; + const Vec3d end = Vec3d(m_end_position[X], m_end_position[Y], m_end_position[Z]) + offset; + // The second moments of a uniform segment. + const GCodeProcessorResult::ObjectMass::Sum sum{ mass, double(volume), 0.5 * mass * (start + end), + mass / 3. * (start.cwiseProduct(start) + start.cwiseProduct(end) + end.cwiseProduct(end)) }; + // Of the bead's center line and its height, as its width is only estimated. Merged, as a wall along an axis is flat. + BoundingBoxf3 extent; + extent.merge(start.cwiseMin(end) - half_height); + extent.merge(start.cwiseMax(end) + half_height); + const bool part = role != erBrim && !is_support(role); + const size_t layer = std::max(1, m_layer_id) - 1; + + m_result.plate_mass.add(sum, extent, layer); + // The brim belongs to the plate alone. + if (role == erBrim || !m_mass_locator) + return; + const auto add = [&sum, &extent, layer](std::vector &masses, int index) { + if (index < 0) + return; + if (masses.size() <= size_t(index)) + masses.resize(index + 1); + masses[index].add(sum, extent, layer); + }; + // At the nozzle's height, which the layers print at. + const MassLocation location = m_mass_locator(0.5 * (start + end) + half_height, !part); + if (part) { + add(m_result.object_masses, location.object); + add(m_result.body_masses, location.body); + } else + add(m_result.support_masses, location.object); +} + +void GCodeProcessor::finalize_object_masses() +{ + const auto accumulate = [](GCodeProcessorResult::ObjectMass &object) { + for (size_t i = 1; i < object.printed_up_to_layer.size(); ++i) + object.printed_up_to_layer[i].add(object.printed_up_to_layer[i - 1]); + }; + accumulate(m_result.plate_mass); + for (GCodeProcessorResult::ObjectMass &object : m_result.object_masses) + accumulate(object); + for (GCodeProcessorResult::ObjectMass &body : m_result.body_masses) + accumulate(body); + for (GCodeProcessorResult::ObjectMass &support : m_result.support_masses) + accumulate(support); +} + void GCodeProcessor::set_extrusion_role(ExtrusionRole role) { m_used_filaments.process_role_cache(this); diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 9b3571223c..f5b9985f0e 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -3,6 +3,7 @@ #include "libslic3r/CommonDefs.hpp" #include "libslic3r/libslic3r.h" +#include "libslic3r/BoundingBox.hpp" #include "libslic3r/Polygon.hpp" #include "libslic3r/Config.hpp" #include "libslic3r/ArcFitter.hpp" @@ -35,6 +36,9 @@ namespace Slic3r { class Print; +// For a filament whose density is not set, in g/cm³. +inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f; + // slice warnings enum strings #define NOZZLE_HRC_CHECKER "the_actual_nozzle_hrc_smaller_than_the_required_nozzle_hrc" #define BED_TEMP_TOO_HIGH_THAN_FILAMENT "bed_temperature_too_high_than_filament" @@ -270,9 +274,44 @@ class Print; std::vector params; // extra msg info }; + // Material extruded for the plate, one object instance or one connected body of it, for their centers of mass. + struct ObjectMass + { + struct Sum + { + double mass{ 0. }; + double volume{ 0. }; + Vec3d moment{ Vec3d::Zero() }; + // Of the mass about the origin along each axis, the sums of m x^2, m y^2 and m z^2. + Vec3d second{ Vec3d::Zero() }; + + void add(const Sum &other) + { + mass += other.mass; + volume += other.volume; + moment += other.moment; + second += other.second; + } + }; + // Everything printed up to each layer id, the plate's with brim, raft and supports, and the box it fills. + std::vector printed_up_to_layer; + BoundingBoxf3 box; + // Of an object, whether it is an assembly. + bool assembly{ false }; + + Sum total() const { return printed_up_to_layer.empty() ? Sum{} : printed_up_to_layer.back(); } + void add(const Sum &sum, const BoundingBoxf3 &extent, size_t layer); + }; + std::string filename; unsigned int id; std::vector moves; + ObjectMass plate_mass; + // One per object instance, and one per connected body of the instances of several, when the sliced objects were at hand. + std::vector object_masses; + std::vector body_masses; + // One per object instance, of its supports and raft. + std::vector support_masses; // Positions of ends of lines of the final G-code this->filename after TimeProcessor::post_process() finalizes the G-code. std::vector lines_ends; Pointfs printable_area; @@ -360,6 +399,10 @@ class Print; filename = std::forward(other).filename; id = std::forward(other).id; moves = std::forward(other).moves; + plate_mass = std::forward(other).plate_mass; + object_masses = std::forward(other).object_masses; + body_masses = std::forward(other).body_masses; + support_masses = std::forward(other).support_masses; lines_ends = std::forward(other).lines_ends; printable_area = std::forward(other).printable_area; bed_exclude_area = std::forward(other).bed_exclude_area; @@ -1099,6 +1142,15 @@ class Print; }; #endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + // The object instance and the connected body of an instance of several that a point lies in, -1 for none. + struct MassLocation + { + int object{ -1 }; + int body{ -1 }; + }; + // For a support, the object instance only. + using MassLocator = std::function; + private: CommandProcessor m_command_processor; GCodeReader m_parser; @@ -1126,6 +1178,7 @@ class Print; bool m_skippable{false}; SkipType m_skippable_type{SkipType::stNone}; int m_object_label_id{-1}; + MassLocator m_mass_locator; float m_print_z{0.0f}; std::vector m_remaining_volume; ExtruderTemps m_filament_nozzle_temp; @@ -1280,6 +1333,13 @@ class Print; const std::vector>& unprintable_filament_types ); void apply_config(const PrintConfig& config); void set_print(Print* print) { m_print = print; } + // Locates extrusions in the objects and bodies it numbers, those objects listed beforehand. + void set_mass_locator(MassLocator locator, std::vector objects) + { + m_mass_locator = std::move(locator); + m_result.support_masses.assign(objects.size(), {}); + m_result.object_masses = std::move(objects); + } // Hand the nozzle grouping context to the estimator BEFORE the streaming replay, so the // per-slot machine-limit resolution can follow the active nozzle. Null is fine (slot 0). void initialize_from_context(const std::shared_ptr& nozzle_group_result) { @@ -1534,6 +1594,8 @@ class Print; //BBS: different path_type is only used for arc move void store_move_vertex(EMoveType type, EMovePathType path_type = EMovePathType::Noop_move, bool internal_only = false); + void add_object_mass(int filament_id, float volume); + void finalize_object_masses(); void set_extrusion_role(ExtrusionRole role); // Resolve the SKIPPABLE_TYPE payload to a SkipType. diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 5ba7882df3..9da74d7986 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -12,6 +12,7 @@ #include "BoundingBox.hpp" #include "ClipperUtils.hpp" +#include "ConnectedBodies.hpp" #include "Geometry.hpp" #include "I18N.hpp" #include "Layer.hpp" @@ -747,69 +748,19 @@ void PrintObject::prepare_infill() for (Layer *layer : m_layers) layer->lslices_separated_component_ids.clear(); if (needs_separated_components) { - const size_t nl = m_layers.size(); - std::vector offset(nl + 1, 0); // Orca: flat index of the first island of each layer - for (size_t i = 0; i < nl; ++ i) - offset[i + 1] = offset[i] + m_layers[i]->lslices.size(); - const size_t nreg = offset[nl]; - // Orca: Union-find over every (layer, island). - std::vector parent(nreg); - for (size_t i = 0; i < nreg; ++ i) parent[i] = i; - auto find = [&parent](size_t x) { - while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } - return x; - }; - auto unite = [&](size_t a, size_t b) { a = find(a); b = find(b); if (a != b) parent[a] = b; }; - // Orca: Index the smaller of two consecutive layers instead of scanning every - // pair of islands. The tree prunes distant boxes on fragmented models; exact - // polygon intersections still decide connectivity for the remaining candidates. - for (size_t i = 0; i + 1 < nl; ++ i) { - m_print->throw_if_canceled(); - size_t layer_a = i, layer_b = i + 1; - if (m_layers[layer_a]->lslices.size() < m_layers[layer_b]->lslices.size()) - std::swap(layer_a, layer_b); - const Layer *la = m_layers[layer_a], *lb = m_layers[layer_b]; - if (lb->lslices.empty()) - continue; - - using IslandTree = AABBTreeIndirect::Tree<2, coord_t>; - std::vector bboxes; - bboxes.reserve(lb->lslices.size()); - for (size_t b = 0; b < lb->lslices.size(); ++ b) - bboxes.emplace_back(b, lb->lslices_bboxes[b]); - IslandTree tree; - tree.build_modify_input(bboxes); - for (size_t a = 0; a < la->lslices.size(); ++ a) { - const IslandTree::BoundingBox query(la->lslices_bboxes[a].min, la->lslices_bboxes[a].max); - AABBTreeIndirect::traverse(tree, - [&query](const IslandTree::Node &node) { return node.bbox.intersects(query); }, - [&](const IslandTree::Node &node) { - const size_t b = node.idx; - // Orca: Tree boxes include an epsilon, so retain the original box - // filter. Already-connected islands cannot change the partition - // and need no further polygon intersection. - if (la->lslices_bboxes[a].overlap(lb->lslices_bboxes[b]) && - find(offset[layer_a] + a) != find(offset[layer_b] + b) && - ! intersection_ex(la->lslices[a], lb->lslices[b]).empty()) - unite(offset[layer_a] + a, offset[layer_b] + b); - return true; - }); - } - } - // Orca: Number the bodies by their first island and merge the bounding boxes of their islands. - std::vector body_of_root(nreg, size_t(-1)); - for (size_t i = 0; i < nl; ++ i) { + std::vector islands; + islands.reserve(m_layers.size()); + for (const Layer *layer : m_layers) + islands.emplace_back(&layer->lslices); + size_t bodies = 0; + std::vector> ids = connected_bodies(islands, bodies, [this]() { m_print->throw_if_canceled(); }); + // Orca: Merge the bounding boxes of the islands of each body. + m_separated_body_bboxes.assign(bodies, BoundingBox()); + for (size_t i = 0; i < m_layers.size(); ++ i) { Layer *layer = m_layers[i]; - layer->lslices_separated_component_ids.resize(layer->lslices.size()); - for (size_t a = 0; a < layer->lslices.size(); ++ a) { - size_t &body = body_of_root[find(offset[i] + a)]; - if (body == size_t(-1)) { - body = m_separated_body_bboxes.size(); - m_separated_body_bboxes.emplace_back(); - } - m_separated_body_bboxes[body].merge(layer->lslices_bboxes[a]); - layer->lslices_separated_component_ids[a] = body; - } + for (size_t a = 0; a < layer->lslices.size(); ++ a) + m_separated_body_bboxes[ids[i][a]].merge(layer->lslices_bboxes[a]); + layer->lslices_separated_component_ids = std::move(ids[i]); } } diff --git a/src/libslic3r/TriangleMesh.cpp b/src/libslic3r/TriangleMesh.cpp index c0623ac824..8da29fdb30 100644 --- a/src/libslic3r/TriangleMesh.cpp +++ b/src/libslic3r/TriangleMesh.cpp @@ -1495,6 +1495,40 @@ float its_volume(const indexed_triangle_set &its) return volume; } +MassProperties MassProperties::transformed(const Transform3d &trafo) const +{ + const Matrix3d linear = trafo.linear(); + const double scale = std::abs(linear.determinant()); + return { mass * scale, volume * scale, trafo * center, linear * spread * linear.transpose() }; +} + +MassProperties its_mass_properties(const indexed_triangle_set &its) +{ + if (its.indices.empty()) + return {}; + + // Signed tetrahedra fanned from a mesh vertex, not the origin, to keep the sums precise far from it. + const Vec3d p0 = its.vertices.front().cast(); + double volume6 = 0.; + Vec3d moment24 = Vec3d::Zero(); + Matrix3d second120 = Matrix3d::Zero(); + for (const stl_triangle_vertex_indices &face : its.indices) { + const Vec3d a = its.vertices[face(0)].cast() - p0; + const Vec3d b = its.vertices[face(1)].cast() - p0; + const Vec3d c = its.vertices[face(2)].cast() - p0; + const Vec3d s = a + b + c; + const double v = a.dot(b.cross(c)); + volume6 += v; + moment24 += v * s; + second120 += v * (a * a.transpose() + b * b.transpose() + c * c.transpose() + s * s.transpose()); + } + if (volume6 == 0.) + return {}; + const Vec3d center = moment24 / (4. * volume6); + const double volume = std::abs(volume6) / 6.; + return { volume, volume, p0 + center, second120 / (20. * volume6) - center * center.transpose() }; +} + float its_average_edge_length(const indexed_triangle_set &its) { if (its.indices.empty()) diff --git a/src/libslic3r/TriangleMesh.hpp b/src/libslic3r/TriangleMesh.hpp index 8c82bac52f..c775b8ef38 100644 --- a/src/libslic3r/TriangleMesh.hpp +++ b/src/libslic3r/TriangleMesh.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "BoundingBox.hpp" #include "Line.hpp" @@ -324,6 +325,20 @@ inline stl_normal its_unnormalized_normal(const indexed_triangle_set &its, } float its_volume(const indexed_triangle_set &its); +// Mass, volume and center of mass of a solid, and the mean over its mass of (x - center)(x - center)^T, from which its +// moments of inertia about axes through the center follow. +struct MassProperties +{ + double mass{ 0. }; + double volume{ 0. }; + Vec3d center{ Vec3d::Zero() }; + Matrix3d spread{ Matrix3d::Zero() }; + + // Under an affine map, which scales mass and volume by its determinant. + MassProperties transformed(const Transform3d &trafo) const; +}; +// The solid a closed mesh bounds at unit density, whichever way its faces turn; nothing for a zero volume. +MassProperties its_mass_properties(const indexed_triangle_set &its); float its_average_edge_length(const indexed_triangle_set &its); void its_merge(indexed_triangle_set &A, const indexed_triangle_set &B); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 03bef82d18..988fe4d22d 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1441,6 +1441,10 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const wxGetApp().plater()->schedule_background_process(); return; } + m_plate_mass = gcode_result.plate_mass; + m_object_masses = gcode_result.object_masses; + m_body_masses = gcode_result.body_masses; + m_support_masses = gcode_result.support_masses; // convert data from PrusaSlicer format to libvgcode format. // Belt printers: when the designed (upright) view is active, back-transform @@ -1876,6 +1880,10 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const void GCodeViewer::load_as_preview(libvgcode::GCodeInputData&& data) { m_loaded_as_preview = true; + m_plate_mass = {}; + m_object_masses.clear(); + m_body_masses.clear(); + m_support_masses.clear(); m_move_type_counts.fill(0); for (auto& move_type_times : m_move_type_times) @@ -1955,6 +1963,10 @@ void GCodeViewer::reset() m_move_type_distances.fill(0.0f); m_print_statistics.reset(); m_custom_gcode_per_print_z = std::vector(); + m_plate_mass = {}; + m_object_masses.clear(); + m_body_masses.clear(); + m_support_masses.clear(); m_left_extruder_filament.clear(); m_right_extruder_filament.clear(); m_sequential_view.gcode_window.reset(); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index c038aec671..6c8892d868 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -260,6 +260,10 @@ private: GCodeProcessorResult::SettingsIds m_settings_ids; std::vector m_custom_gcode_per_print_z; + GCodeProcessorResult::ObjectMass m_plate_mass; + std::vector m_object_masses; + std::vector m_body_masses; + std::vector m_support_masses; bool m_contained_in_bed{ true }; mutable bool m_no_render_path { false }; @@ -343,6 +347,10 @@ public: std::vector get_layers_times() const { return m_viewer.get_layers_estimated_times(); } const std::array &get_layers_z_range() const { return m_viewer.get_layers_view_range(); } + const GCodeProcessorResult::ObjectMass& get_plate_mass() const { return m_plate_mass; } + const std::vector& get_object_masses() const { return m_object_masses; } + const std::vector& get_body_masses() const { return m_body_masses; } + const std::vector& get_support_masses() const { return m_support_masses; } size_t get_vertices_count() const { return m_viewer.get_vertices_count(); } size_t get_layers_count() const { return m_viewer.get_layers_count(); } // ORCA: realistic view. Changes whenever the toolpaths casting shadows do. diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 2cc6c6747f..e1c8b9935c 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -85,7 +85,9 @@ #include "3DScene.hpp" #include "BackgroundSlicingProcess.hpp" #include "CameraUtils.hpp" +#include "GLModel.hpp" #include "GLShader.hpp" +#include "libslic3r/ConnectedBodies.hpp" #include "GUI.hpp" #include "Tab.hpp" #include "GUI_Preview.hpp" @@ -136,6 +138,7 @@ #include #include +#include #include #include @@ -1000,6 +1003,325 @@ void GLCanvas3D::Labels::render(const std::vector& sorted_ } } +// The sums a solid adds to a marker. +static GCodeProcessorResult::ObjectMass::Sum mass_sum(const MassProperties& solid) +{ + return { solid.mass, solid.volume, solid.mass * solid.center, + solid.mass * (solid.spread.diagonal() + solid.center.cwiseProduct(solid.center)) }; +} + +// On screen, of the plates, the objects, the supports and the bodies, each smaller than the one before, so that markers at +// one place still show. +static constexpr std::array marker_radii{ 9., 7., 6., 5. }; + +// As the canvas toolbar scales for the display's DPI. +static double marker_scale(const GLCanvas3D& canvas) +{ + double scale = canvas.get_scale(); +#ifdef WIN32 + scale *= double(get_dpi_for_window(wxGetApp().GetTopWindow())) / double(DPI_DEFAULT); +#endif // WIN32 + return scale; +} + +GLCanvas3D::CenterOfMass::Markers GLCanvas3D::CenterOfMass::model_markers(const GLCanvas3D& canvas) +{ + Markers markers; + if (canvas.get_model() == nullptr) + return markers; + + struct Instance + { + Transform3d trafo; + std::vector volumes; + }; + std::map> objects; + const ModelObjectPtrs& model_objects = canvas.get_model()->objects; + for (const GLVolume* volume : canvas.get_volumes().volumes) { + const int obj_idx = volume->object_idx(); + const int vol_idx = volume->volume_idx(); + if (!volume->is_active || volume->is_wipe_tower || obj_idx < 0 || obj_idx >= int(model_objects.size()) || vol_idx < 0 || + vol_idx >= int(model_objects[obj_idx]->volumes.size())) + continue; + Instance& instance = objects[obj_idx][volume->instance_idx()]; + instance.trafo = volume->get_instance_transformation().get_matrix(); + instance.volumes.emplace_back(volume); + } + + // From the filament presets, as the plater config holds the values of the last filament edited only. + const PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + std::vector filament_densities; + for (const std::string& name : preset_bundle.filament_presets) + filament_densities.emplace_back(preset_bundle.filaments.find_preset(name, true)->config.opt_float("filament_density", 0)); + const auto density = [&filament_densities](const ModelVolume& volume) { + const size_t filament = size_t(std::max(1, volume.extruder_id())); + const double density = filament <= filament_densities.size() ? filament_densities[filament - 1] : 0.; + return density > 0. ? density : double(DEFAULT_FILAMENT_DENSITY); + }; + + // One per plate, of the instances on it. + PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list(); + std::map plates; + std::map meshes; + std::map bodies; + for (const auto& [obj_idx, instances] : objects) { + const ModelObject& object = *model_objects[obj_idx]; + // An assembly is sliced, so that its overlapping parts are united and its negative volumes cut away, in the + // order of its volumes, as the later one prints where two overlap. + std::vector volumes = instances.begin()->second.volumes; + std::sort(volumes.begin(), volumes.end(), [](const GLVolume* l, const GLVolume* r) { return l->volume_idx() < r->volume_idx(); }); + std::vector solids; + std::vector densities; + std::vector negatives; + std::vector sliced; + for (const GLVolume* volume : volumes) { + const ModelVolume& model_volume = *object.volumes[volume->volume_idx()]; + if (!model_volume.is_model_part() && !model_volume.is_negative_volume()) + continue; + const Transform3d trafo = volume->get_volume_transformation().get_matrix(); + if (model_volume.is_model_part()) { + solids.emplace_back(&model_volume.mesh().its, trafo); + densities.emplace_back(density(model_volume)); + } else + negatives.emplace_back(&model_volume.mesh().its, trafo); + sliced.push_back({ model_volume.id().id, model_volume.is_negative_volume(), model_volume.is_model_part() ? densities.back() : 0., trafo }); + } + const std::vector* assembly = nullptr; + if (solids.size() > 1 || (!solids.empty() && !negatives.empty())) { + // Coarser while a part is dragged. + const size_t slabs = canvas.is_dragging() ? 100 : 500; + const auto cached = m_bodies.find(object.id().id); + const bool valid = cached != m_bodies.end() && cached->second.slabs >= slabs && cached->second.volumes == sliced; + Bodies& entry = bodies[object.id().id]; + entry = valid ? std::move(cached->second) : Bodies{ std::move(sliced), slabs, solid_bodies(solids, densities, negatives, slabs) }; + assembly = &entry.bodies; + } + + for (const auto& [inst_idx, instance] : instances) { + // The box of its parts, which the object's size shows. + Marker object_marker; + object_marker.assembly = assembly != nullptr; + for (const GLVolume* volume : instance.volumes) + if (object.volumes[volume->volume_idx()]->is_model_part()) + object_marker.box.merge(volume->transformed_convex_hull_bounding_box()); + if (assembly != nullptr) { + std::vector parts; + for (const SolidBody& body : *assembly) + if (body.mass > 0.) { + parts.push_back({ mass_sum(body.transformed(instance.trafo)), body.bounding_box(instance.trafo) }); + object_marker.sum.add(parts.back().sum); + } + if (parts.size() > 1) + append(markers[mkBody], std::move(parts)); + } else + for (const GLVolume* volume : instance.volumes) { + // The parts the object info's volume sums. + const ModelVolume& model_volume = *object.volumes[volume->volume_idx()]; + if (!model_volume.is_model_part()) + continue; + const auto [it, inserted] = meshes.try_emplace(model_volume.id().id); + if (inserted) { + const auto cached = m_meshes.find(it->first); + it->second = cached != m_meshes.end() ? cached->second : its_mass_properties(model_volume.mesh().its); + } + MassProperties part = it->second.transformed(volume->world_matrix()); + part.mass *= density(model_volume); + object_marker.sum.add(mass_sum(part)); + } + if (object_marker.sum.mass > 0.) { + if (const int plate = plate_list.find_instance(obj_idx, inst_idx); plate >= 0) { + plates[plate].sum.add(object_marker.sum); + plates[plate].box.merge(object_marker.box); + } + markers[mkObject].emplace_back(std::move(object_marker)); + } + } + } + m_meshes = std::move(meshes); + m_bodies = std::move(bodies); + for (auto& [plate, marker] : plates) + markers[mkPlate].emplace_back(std::move(marker)); + return markers; +} + +void GLCanvas3D::CenterOfMass::render(GLCanvas3D& canvas) +{ + m_drawn = {}; + const bool preview = canvas.m_canvas_type == ECanvasType::CanvasPreview; + // The other gizmos work on the surface the marker would cover. + const GLGizmosManager::EType gizmo = canvas.get_gizmos_manager().get_current_type(); + if (!wxGetApp().show_center_of_mass() || canvas.m_design_canvas || + !(canvas.m_canvas_type == ECanvasType::CanvasView3D || (preview && canvas.m_render_preview)) || + (gizmo != GLGizmosManager::Undefined && gizmo != GLGizmosManager::Move && gizmo != GLGizmosManager::Rotate && + gizmo != GLGizmosManager::Scale && gizmo != GLGizmosManager::Flatten)) + return; + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + if (shader == nullptr) + return; + + // Preview adds markers for what is printed up to the top layer shown. + if (preview) { + const GCodeViewer& gcode_viewer = canvas.get_gcode_viewer(); + m_top_layer = gcode_viewer.get_layers_z_range()[1]; + const auto add = [this](const GCodeProcessorResult::ObjectMass& mass, MarkerKind kind) { + if (const Sum total = mass.total(); total.mass > 0.) + m_drawn[0][kind].push_back({ total, mass.box, mass.assembly }); + if (!mass.printed_up_to_layer.empty()) + if (const Sum& sum = mass.printed_up_to_layer[std::min(m_top_layer, mass.printed_up_to_layer.size() - 1)]; sum.mass > 0.) + m_drawn[1][kind].push_back({ sum, mass.box, mass.assembly }); + }; + add(gcode_viewer.get_plate_mass(), mkPlate); + for (const GCodeProcessorResult::ObjectMass& object : gcode_viewer.get_object_masses()) + add(object, mkObject); + for (const GCodeProcessorResult::ObjectMass& body : gcode_viewer.get_body_masses()) + add(body, mkBody); + for (const GCodeProcessorResult::ObjectMass& support : gcode_viewer.get_support_masses()) + add(support, mkSupport); + } else + m_drawn[0] = model_markers(canvas); + if (std::all_of(m_drawn.begin(), m_drawn.end(), + [](const Markers& markers) { return std::all_of(markers.begin(), markers.end(), [](const auto& kind) { return kind.empty(); }); })) + return; + + if (!m_octants[0].is_initialized()) { + // A resolution divisible by 4 puts every triangle within one octant. + const GLModel::Geometry sphere = smooth_sphere(32, 1.f); + std::array octants; + for (size_t i = 0; i + 2 < sphere.indices_count(); i += 3) { + const std::array ids = { sphere.extract_index(i), sphere.extract_index(i + 1), sphere.extract_index(i + 2) }; + const Vec3f c = sphere.extract_position_3(ids[0]) + sphere.extract_position_3(ids[1]) + sphere.extract_position_3(ids[2]); + GLModel::Geometry& octant = octants[c.x() * c.y() * c.z() > 0.f ? 0 : 1]; + for (const unsigned int id : ids) + octant.add_vertex(sphere.extract_position_3(id), sphere.extract_normal_3(id)); + const auto n = (unsigned int)octant.vertices_count(); + octant.add_triangle(n - 3, n - 2, n - 1); + } + for (size_t i = 0; i < octants.size(); ++i) + m_octants[i].init_from(std::move(octants[i])); + } + + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d& view_matrix = camera.get_view_matrix(); + const double scale = marker_scale(canvas) * camera.get_inv_zoom(); + + // Seen through the object it lies in; culling keeps the sphere's far half behind its near one. + glsafe(::glDisable(GL_DEPTH_TEST)); + glsafe(::glEnable(GL_CULL_FACE)); + shader->start_using(); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + shader->set_uniform("view_normal_matrix", (Matrix3d)view_matrix.matrix().block(0, 0, 3, 3)); + shader->set_uniform("emission_factor", 0.1f); + const std::array, mkCount> colors = { { + { ColorRGBA(0.1f, 0.1f, 0.1f, 1.f), ColorRGBA::WHITE() }, + { ColorRGBA(0x5A / 255.f, 0x9B / 255.f, 0xD4 / 255.f, 1.f), ColorRGBA::WHITE() }, + { ColorRGBA(0.f, 0.6f, 0.f, 1.f), ColorRGBA(0.1f, 0.1f, 0.1f, 1.f) }, + { ColorRGBA(0.7f, 0.f, 0.f, 1.f), ColorRGBA::YELLOW() }, + } }; + const auto draw = [&](const Markers& markers, float alpha) { + for (size_t kind = 0; kind < mkCount; ++kind) + for (const Marker& marker : markers[kind]) { + shader->set_uniform("view_model_matrix", view_matrix * Geometry::translation_transform(marker.center()) * + Geometry::scale_transform(marker_radii[kind] * scale)); + for (size_t i = 0; i < m_octants.size(); ++i) { + ColorRGBA color = colors[kind][i]; + color.a(alpha); + m_octants[i].set_color(color); + m_octants[i].render(); + } + } + }; + // Preview fades the finished parts' markers under those of what is printed so far. + draw(m_drawn[0], preview ? 0.4f : 1.f); + draw(m_drawn[1], 1.f); + shader->stop_using(); + glsafe(::glEnable(GL_DEPTH_TEST)); +} + +bool GLCanvas3D::CenterOfMass::on_left_down(GLCanvas3D& canvas, const Vec2d& mouse) +{ + const bool shown = m_picked.has_value(); + const Camera& camera = wxGetApp().plater()->get_camera(); + const double scale = marker_scale(canvas) * camera.get_inv_zoom(); + m_picked.reset(); + // In the order they cover each other: what is printed so far over the finished print, smaller kinds over larger ones. + for (size_t set = m_drawn.size(); set-- > 0 && !m_picked;) + for (size_t kind = mkCount; kind-- > 0 && !m_picked;) + for (size_t index = 0; index < m_drawn[set][kind].size(); ++index) { + const Vec3d center = m_drawn[set][kind][index].center(); + const std::vector ends = { center, center + marker_radii[kind] * scale * camera.get_dir_right() }; + const Points screen = CameraUtils::project(camera, ends); + if ((screen[0].cast() - mouse).norm() <= (screen[1] - screen[0]).cast().norm()) { + m_picked = Pick{ set, kind, index, m_drawn[set][kind].size() }; + break; + } + } + if (shown || m_picked) + canvas._set_overlay_as_dirty(); + return m_picked.has_value(); +} + +void GLCanvas3D::CenterOfMass::render_details(GLCanvas3D& canvas) +{ + if (!m_picked) + return; + const Pick& pick = *m_picked; + const std::vector& markers = m_drawn[pick.set][pick.kind]; + // Gone with the markers, or with what it stood for. + if (markers.size() != pick.count) { + m_picked.reset(); + return; + } + const Marker& marker = markers[pick.index]; + const Sum& sum = marker.sum; + const Vec3d center = marker.center(); + // About the axes through the center, from how far the mass spreads along the two others. + const Vec3d spread = (sum.second / sum.mass - center.cwiseProduct(center)).cwiseMax(0.); + const Vec3d inertia = sum.mass * Vec3d(spread.y() + spread.z(), spread.x() + spread.z(), spread.x() + spread.y()); + + // Beside the marker. + const Point screen = CameraUtils::project(wxGetApp().plater()->get_camera(), center); + ImGuiWrapper& imgui = *wxGetApp().imgui(); + imgui.set_next_window_pos(float(screen.x() + 2. * marker_radii[pick.kind] * marker_scale(canvas)), float(screen.y()), ImGuiCond_Always, 0.f, 0.5f); + const std::string title = pick.kind == mkPlate ? _u8L("Plate center of mass") : + pick.kind == mkBody ? _u8L("Part center of mass") : + pick.kind == mkSupport ? _u8L("Support center of mass") : + marker.assembly ? _u8L("Assembly center of mass") : + _u8L("Object center of mass"); + bool open = true; + imgui.begin(title + "###center_of_mass", &open, + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings); + if (ImGui::IsWindowAppearing()) + imgui.set_requires_extra_frame(); + if (canvas.get_canvas_type() == ECanvasType::CanvasPreview) + imgui.text(pick.set == 0 ? _u8L("Finished print") : (boost::format(_u8L("Printed up to layer %1%")) % (m_top_layer + 1)).str()); + // Masses are in mg, volumes in mm³. + const auto xyz = [](const Vec3d& v, const char* format, const std::string& unit) { + return (boost::format(format) % v.x() % v.y() % v.z()).str() + " " + unit; + }; + if (ImGui::BeginTable("##center_of_mass_details", 2)) { + const auto row = [](const std::string& label, const std::string& value) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGuiWrapper::text_colored(ImGuiWrapper::COL_ORCA, label); + ImGui::TableSetColumnIndex(1); + ImGuiWrapper::text(value); + }; + row(_u8L("Weight"), (boost::format("%.2f g") % (sum.mass / 1000.)).str()); + row(_u8L("Volume"), (boost::format(u8"%.2f cm³") % (sum.volume / 1000.)).str()); + if (marker.box.defined) { + row(_u8L("Center in bounding box"), xyz(center - marker.box.min, "X: %.2f, Y: %.2f, Z: %.2f", _u8L("mm"))); + row(_u8L("Bounding box size"), xyz(marker.box.size(), "X: %.2f, Y: %.2f, Z: %.2f", _u8L("mm"))); + } + row(_u8L("Moment of inertia"), xyz(inertia / 1000., "X: %.0f, Y: %.0f, Z: %.0f", u8"g·mm²")); + ImGui::EndTable(); + } + imgui.end(); + if (!open) { + m_picked.reset(); + canvas._set_overlay_as_dirty(); + } +} + void GLCanvas3D::Tooltip::set_text(const std::string& text) { // If the mouse is inside an ImGUI dialog, then the tooltip is suppressed. @@ -2520,6 +2842,9 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size) m_frame_profiler.mark("ssao"); } + // After the occlusion pass, which would shade it as the surface behind it. + m_center_of_mass.render(*this); + if (_is_fxaa_enabled()) { _render_fxaa_pass(static_cast(cnv_size.get_width()), static_cast(cnv_size.get_height())); m_frame_profiler.mark("fxaa"); @@ -4513,6 +4838,12 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) return; } + // A click on a center of mass marker shows its details instead of selecting. + if (evt.LeftDown() && !mouse_in_layer_editing && m_center_of_mass.on_left_down(*this, pos.cast())) { + m_mouse.ignore_left_up = true; + return; + } + bool any_gizmo_active = m_gizmos.get_current() != nullptr; std::map button_mappings; @@ -9363,6 +9694,7 @@ void GLCanvas3D::_render_overlays() }*/ } m_labels.render(sorted_instances); + m_center_of_mass.render_details(*this); _render_3d_navigator(); @@ -10350,6 +10682,12 @@ void GLCanvas3D::_render_canvas_toolbar() [p]{p->show_view3D_labels(!p->are_view3D_labels_shown());} ); + create_menu_item( _utf8(L("Center of mass")), + m_canvas_type != ECanvasType::CanvasAssembleView && !m_design_canvas, // work on prepare and preview + wxGetApp().show_center_of_mass(), + [this]{wxGetApp().toggle_show_center_of_mass(); m_dirty = true;} + ); + // Belt printers, G-code preview only: show the raw machine-frame G-code instead of // the designed (upright) view. This menu is the only place the toggle lives (plus // its shortcut); the reload is deferred (CallAfter) so the preview is not rebuilt diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 23fdfbb66d..70ef0d52b3 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -2,6 +2,9 @@ #define slic3r_GLCanvas3D_hpp_ #include "libslic3r/Point.hpp" +#include "libslic3r/ConnectedBodies.hpp" +#include "libslic3r/GCode/GCodeProcessor.hpp" +#include "libslic3r/TriangleMesh.hpp" #include "slic3r/GUI/3DScene.hpp" #include #include @@ -30,6 +33,7 @@ #include "Gizmos/GLGizmosManager.hpp" #include "GUI_ObjectLayers.hpp" #include "GLSelectionRectangle.hpp" +#include "GLModel.hpp" #include "MeshUtils.hpp" #include "GCodeViewer.hpp" #include "Camera.hpp" @@ -477,6 +481,69 @@ class GLCanvas3D void render(const std::vector& sorted_instances) const; }; + class CenterOfMass + { + using Sum = GCodeProcessorResult::ObjectMass::Sum; + enum MarkerKind : size_t { mkPlate, mkObject, mkSupport, mkBody, mkCount }; + // A marker's mass and the box of what it stands for. + struct Marker + { + Sum sum; + BoundingBoxf3 box; + // Of an object, whether it is an assembly. + bool assembly{ false }; + + Vec3d center() const { return sum.moment / sum.mass; } + }; + // The plates', each object instance's, its supports' and each body of an assembly's. + using Markers = std::array, mkCount>; + + // The marker's two colors of alternating octants. + std::array m_octants; + // Mass properties at unit density of each ModelVolume's mesh, by ModelVolume id, which a new mesh changes. + std::map m_meshes; + // The connected bodies of each assembly in its own coordinates, by ModelObject id, with the volumes they were sliced from. + struct Bodies + { + struct Volume + { + size_t id; + bool negative; + double density; + Transform3d trafo; + + bool operator==(const Volume& other) const + { + return id == other.id && negative == other.negative && density == other.density && trafo.matrix() == other.trafo.matrix(); + } + }; + std::vector volumes; + size_t slabs{ 0 }; + std::vector bodies; + }; + std::map m_bodies; + // The markers drawn last: of the finished print and, in Preview, of what is printed up to the top layer shown. + std::array m_drawn; + size_t m_top_layer{ 0 }; + // The marker whose details are shown, with the number of its kind then. + struct Pick + { + size_t set; + size_t kind; + size_t index; + size_t count; + }; + std::optional m_picked; + + Markers model_markers(const GLCanvas3D& canvas); + + public: + void render(GLCanvas3D& canvas); + // Shows the details of the marker under the mouse, else hides them; whether it hit one. + bool on_left_down(GLCanvas3D& canvas, const Vec2d& mouse); + void render_details(GLCanvas3D& canvas); + }; + class Tooltip { std::string m_text; @@ -733,6 +800,7 @@ private: int m_selected_extruder; Labels m_labels; + CenterOfMass m_center_of_mass; Tooltip m_tooltip; bool m_tooltip_enabled{ true }; Slope m_slope; diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 8b9ad1bae7..361693ed42 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -435,6 +435,9 @@ public: bool show_outline() const { return app_config->get_bool("show_outline"); } void toggle_show_outline() const { app_config->set_bool("show_outline", !show_outline()); } + bool show_center_of_mass() const { return app_config->get_bool("show_center_of_mass"); } + void toggle_show_center_of_mass() const { app_config->set_bool("show_center_of_mass", !show_center_of_mass()); } + wxString get_inf_dialog_contect () {return m_info_dialog_content;}; std::vector split_str(std::string src, std::string separator); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 51a0c85909..f87659cf48 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -21230,6 +21230,9 @@ void Plater::on_config_change(const DynamicPrintConfig &config) opt_key == "top_surface_filament_id" || opt_key == "bottom_surface_filament_id") { update_scheduled = true; } + // Orca: the center of mass markers weigh the parts by it. + else if (opt_key == "filament_density" && wxGetApp().show_center_of_mass()) + p->view3D->get_canvas3d()->set_as_dirty(); } if (bed_shape_changed) diff --git a/tests/fff_print/test_gcodeprocessor.cpp b/tests/fff_print/test_gcodeprocessor.cpp index 4bdc416002..0be8cc1c12 100644 --- a/tests/fff_print/test_gcodeprocessor.cpp +++ b/tests/fff_print/test_gcodeprocessor.cpp @@ -6,10 +6,12 @@ #include #include #include "libslic3r/libslic3r.h" +#include "libslic3r/BoundingBox.hpp" #include "libslic3r/Config.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/Print.hpp" +#include "libslic3r/TriangleMesh.hpp" #include "libslic3r/Utils.hpp" #include "test_helpers.hpp" @@ -18,6 +20,7 @@ #include #include #include +#include #include "libslic3r/PrintConfig.hpp" #include "libslic3r/Point.hpp" #include @@ -99,6 +102,22 @@ TEST_CASE("Reserved keyword detection reports every offending line", "[GCodeProc namespace { +void process_gcode(const std::string &gcode, GCodeProcessorResult &result) +{ + FullPrintConfig config; + config.gcode_flavor.value = gcfMarlinFirmware; + // s_IsBBLPrinter selects the "; FEATURE: " role tags the G-code uses. + const bool was_bbl_printer = GCodeProcessor::s_IsBBLPrinter; + const ScopeGuard restore_bbl_printer([was_bbl_printer] { GCodeProcessor::s_IsBBLPrinter = was_bbl_printer; }); + GCodeProcessor::s_IsBBLPrinter = true; + ScopedTemporaryFile temp(".gcode"); + std::ofstream(temp.string()) << gcode; + GCodeProcessor processor; + processor.apply_config(config); + processor.process_file(temp.string()); + result = std::move(processor.extract_result()); +} + // Closed outer-wall squares, each after a fast travel and before an inner-wall move, so the processor // records seams and inserts actual speed moves. virtual_moves adds a VG1 move after each square. void process_squares(int squares, GCodeProcessorResult &result, bool virtual_moves = false) @@ -114,18 +133,42 @@ void process_squares(int squares, GCodeProcessorResult &result, bool virtual_mov if (virtual_moves) gcode << "VG1 X20 Y30 F12000\n"; } - FullPrintConfig config; - config.gcode_flavor.value = gcfMarlinFirmware; - // s_IsBBLPrinter selects the "; FEATURE: " role tags this G-code uses. - const bool was_bbl_printer = GCodeProcessor::s_IsBBLPrinter; - const ScopeGuard restore_bbl_printer([was_bbl_printer] { GCodeProcessor::s_IsBBLPrinter = was_bbl_printer; }); - GCodeProcessor::s_IsBBLPrinter = true; - ScopedTemporaryFile temp(".gcode"); - std::ofstream(temp.string()) << gcode.str(); - GCodeProcessor processor; - processor.apply_config(config); - processor.process_file(temp.string()); - result = std::move(processor.extract_result()); + process_gcode(gcode.str(), result); +} + +// Objects A and B on the first layer and A again on the second, with A's brim and support. The skirt and the +// prime tower belong to neither. +void process_two_objects(GCodeProcessorResult &result) +{ + std::ostringstream gcode; + gcode << "M83\nG90\n" + << "; CHANGE_LAYER\n; LAYER_HEIGHT: 0.2\nG1 Z0.2 F12000\n" + << "; FEATURE: Skirt\nG1 X0 Y100 E5 F3000\n" + << "; FEATURE: Brim\nG1 X8 Y8 F12000\nG1 X12 Y8 E1 F3000\n" + << "; FEATURE: Support\nG1 X10 Y20 F12000\nG1 X10 Y30 E1 F3000\n" + << "; FEATURE: Outer wall\nG1 X10 Y10 F12000\nG1 X20 Y10 E1 F3000\n" + << "; FEATURE: Outer wall\nG1 X50 Y50 F12000\nG1 X60 Y50 E2 F3000\n" + << "; FEATURE: Prime tower\nG1 X80 Y80 F12000\nG1 X90 Y80 E1 F3000\n" + << "; CHANGE_LAYER\n; LAYER_HEIGHT: 0.2\nG1 Z0.4 F12000\n" + << "; FEATURE: Outer wall\nG1 X10 Y10 F12000\nG1 X20 Y10 E1 F3000\n"; + process_gcode(gcode.str(), result); +} + +// Bead centers of process_two_objects(), half the 0.2 mm layer below the nozzle. +const Vec3d a_brim(10., 8., 0.1), a_support(10., 25., 0.1), a_wall_0(15., 10., 0.1), a_wall_1(15., 10., 0.3), b_wall(55., 50., 0.1); + +Vec3d center_of(const GCodeProcessorResult::ObjectMass::Sum &sum) { return sum.moment / sum.mass; } + +// One filament, so each bead weighs as much as the E it was extruded with. +Vec3d weighted_center(std::initializer_list> beads) +{ + double mass = 0.; + Vec3d moment = Vec3d::Zero(); + for (const auto &[e, center] : beads) { + mass += e; + moment += e * center; + } + return moment / mass; } bool is_block_move(const GCodeProcessorResult::MoveVertex &move) @@ -281,3 +324,181 @@ TEST_CASE("Rewritten G-code that cannot be re-read keeps the moves and hides the REQUIRE(result.moves.size() == exported_moves.size()); CHECK(result.moves.back().gcode_id == exported_moves.back().gcode_id); } + +TEST_CASE("The plate's center of mass takes every extrusion of G-code without a print behind it", "[GCodeProcessor]") +{ + GCodeProcessorResult result; + process_two_objects(result); + + CHECK(result.object_masses.empty()); + CHECK(result.body_masses.empty()); + const GCodeProcessorResult::ObjectMass &plate = result.plate_mass; + REQUIRE(plate.printed_up_to_layer.size() == 2); + CHECK_THAT((center_of(plate.printed_up_to_layer.front()) - + weighted_center({ { 1., a_brim }, { 1., a_support }, { 1., a_wall_0 }, { 2., b_wall } })).norm(), + Catch::Matchers::WithinAbs(0., 1e-5)); + CHECK_THAT((center_of(plate.printed_up_to_layer.back()) - + weighted_center({ { 1., a_brim }, { 1., a_support }, { 1., a_wall_0 }, { 2., b_wall }, { 1., a_wall_1 } })).norm(), + Catch::Matchers::WithinAbs(0., 1e-5)); + + // Each bead weighs its volume at the default density and spreads along its move, (a^2 + ab + b^2) / 3 for one from + // a to b: the brim from x 8 to 12 at y 8, the support at x 10 from y 20 to 30, A's walls from x 10 to 20 at y 10 and + // B's from x 50 to 60 at y 50 with twice the filament, all at z 0.1 but A's second wall at 0.3. + const GCodeProcessorResult::ObjectMass::Sum total = plate.total(); + CHECK_THAT(total.mass / total.volume, Catch::Matchers::WithinRel(double(DEFAULT_FILAMENT_DENSITY), 1e-6)); + const Vec3d second = total.second / total.mass; + CHECK_THAT(second.x(), Catch::Matchers::WithinRel((304. / 3. + 100. + 2. * 700. / 3. + 2. * 9100. / 3.) / 6., 1e-6)); + CHECK_THAT(second.y(), Catch::Matchers::WithinRel((64. + 1900. / 3. + 2. * 100. + 2. * 2500.) / 6., 1e-6)); + CHECK_THAT(second.z(), Catch::Matchers::WithinRel((5. * 0.01 + 0.09) / 6., 1e-5)); + // The beads' center lines, brim and support included, from the first layer's bottom to the second's top. + CHECK_THAT((plate.box.min - Vec3d(8., 8., 0.)).norm(), Catch::Matchers::WithinAbs(0., 1e-5)); + CHECK_THAT((plate.box.max - Vec3d(60., 50., 0.4)).norm(), Catch::Matchers::WithinAbs(0., 1e-5)); +} + +TEST_CASE("Each sliced cube's center of mass is its center, and the brim lowers the plate's printed one", "[GCodeProcessor]") +{ + const bool copies = GENERATE(false, true); + INFO((copies ? "two copies of one cube" : "two cubes")); + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ { "skirt_loops", 0 }, { "brim_type", "outer_only" }, { "brim_width", 5 }, { "combine_brims", 0 } }); + std::vector cubes{ Test::cube(20) }; + if (!copies) + cubes.emplace_back(Test::cube(20)); + Print print; + Model model; + Test::init_print(std::move(cubes), print, model, config, nullptr, true, copies ? 2 : 1); + GCodeProcessorResult result; + Test::gcode(print, &result); + + CHECK(result.body_masses.empty()); + REQUIRE(result.object_masses.size() == 2); + for (const ModelObject *object : model.objects) + for (size_t instance = 0; instance < object->instances.size(); ++instance) { + const Vec3d center = object->instance_bounding_box(instance).center(); + const auto mass = std::min_element(result.object_masses.begin(), result.object_masses.end(), [¢er](const auto &l, const auto &r) { + return (center_of(l.total()) - center).squaredNorm() < (center_of(r.total()) - center).squaredNorm(); + }); + // Off the center only by the infill's alignment and the top and bottom shells. + const Vec3d part = center_of(mass->total()); + CHECK_THAT(part.x(), Catch::Matchers::WithinAbs(center.x(), 0.5)); + CHECK_THAT(part.y(), Catch::Matchers::WithinAbs(center.y(), 0.5)); + CHECK_THAT(part.z(), Catch::Matchers::WithinAbs(center.z(), 1.)); + // The outer walls' center lines run half a line inside the cube's sides, of copies touching each other too. + const BoundingBoxf3 box = object->instance_bounding_box(instance); + for (int axis = 0; axis < 3; ++axis) { + CHECK_THAT(mass->box.min[axis], Catch::Matchers::WithinAbs(box.min[axis], 0.3)); + CHECK_THAT(mass->box.max[axis], Catch::Matchers::WithinAbs(box.max[axis], 0.3)); + } + } + GCodeProcessorResult::ObjectMass::Sum objects; + for (const GCodeProcessorResult::ObjectMass &object : result.object_masses) + objects.add(object.total()); + const GCodeProcessorResult::ObjectMass::Sum plate = result.plate_mass.total(); + CHECK(plate.mass > objects.mass); + CHECK(center_of(plate).z() < center_of(objects).z()); +} + +TEST_CASE("Each cube's raft is its support, centered below it", "[GCodeProcessor]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ { "skirt_loops", 0 }, { "brim_type", "no_brim" }, { "raft_layers", 3 } }); + Print print; + Model model; + Test::init_print({ Test::cube(20), Test::cube(20) }, print, model, config); + GCodeProcessorResult result; + Test::gcode(print, &result); + + REQUIRE(result.support_masses.size() == 2); + for (size_t i = 0; i < 2; ++i) { + const GCodeProcessorResult::ObjectMass::Sum support = result.support_masses[i].total(); + const Vec3d object = center_of(result.object_masses[i].total()); + REQUIRE(support.mass > 0.); + CHECK_THAT(center_of(support).x(), Catch::Matchers::WithinAbs(object.x(), 1.)); + CHECK_THAT(center_of(support).y(), Catch::Matchers::WithinAbs(object.y(), 1.)); + CHECK(center_of(support).z() < 1.); + } +} + +TEST_CASE("A spiral vase cube counts all its extrusions, rising through each layer", "[GCodeProcessor]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ { "skirt_loops", 0 }, { "brim_type", "no_brim" }, { "spiral_mode", 1 }, { "wall_loops", 1 }, + { "top_shell_layers", 0 }, { "sparse_infill_density", 0 } }); + Print print; + Model model; + Test::init_print({ Test::cube(20) }, print, model, config); + GCodeProcessorResult result; + Test::gcode(print, &result); + + REQUIRE(result.object_masses.size() == 1); + CHECK_THAT(result.object_masses.front().total().mass, Catch::Matchers::WithinRel(result.plate_mass.total().mass, 1e-6)); +} + +TEST_CASE("Each separate part of an assembly gets its center of mass, overlapping parts one", "[GCodeProcessor]") +{ + const bool overlapping = GENERATE(false, true); + // Separated infills finds the bodies first, which the G-code export then takes. + const bool separated = GENERATE(false, true); + INFO((overlapping ? "overlapping parts" : "separate parts") << (separated ? ", separated infills" : "")); + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ { "skirt_loops", 0 }, { "brim_type", "no_brim" }, { "separated_infills", separated ? 1 : 0 } }); + TriangleMesh first = make_cube(20, 20, 20); + TriangleMesh second = make_cube(20, 20, 20); + first.translate(50, 50, 0); + second.translate(overlapping ? 60 : 90, 50, 0); + Print print; + Model model; + Test::init_print({ first }, print, model, config, nullptr, false); + model.objects.front()->add_volume(std::move(second), ModelVolumeType::MODEL_PART, false); + print.apply(model, config); + GCodeProcessorResult result; + Test::gcode(print, &result); + + REQUIRE(result.object_masses.size() == 1); + CHECK(result.object_masses.front().assembly); + // One body is the object itself. + if (overlapping) { + CHECK(result.body_masses.empty()); + return; + } + REQUIRE(result.body_masses.size() == 2); + CHECK(print.objects().front()->separated_body_bboxes().size() == (separated ? 2 : 0)); + const ModelObject &object = *model.objects.front(); + for (const ModelVolume *volume : object.volumes) { + const Vec3d center = volume->mesh().transformed_bounding_box(object.instances.front()->get_matrix() * volume->get_matrix()).center(); + const auto body = std::min_element(result.body_masses.begin(), result.body_masses.end(), [¢er](const auto &l, const auto &r) { + return (center_of(l.total()) - center).squaredNorm() < (center_of(r.total()) - center).squaredNorm(); + }); + const Vec3d part = center_of(body->total()); + CHECK_THAT(part.x(), Catch::Matchers::WithinAbs(center.x(), 0.5)); + CHECK_THAT(part.y(), Catch::Matchers::WithinAbs(center.y(), 0.5)); + CHECK_THAT(part.z(), Catch::Matchers::WithinAbs(center.z(), 1.)); + } +} + +TEST_CASE("Each extrusion weighs its filament's density", "[GCodeProcessor]") +{ + // Two like cubes, the second's filament three times as dense. + DynamicPrintConfig config = Test::multifilament_config(2, { { "filament_density", "1,3" }, { "skirt_loops", 0 }, { "brim_type", "no_brim" } }); + const std::vector> overrides{ { { "extruder", 1 } }, { { "extruder", 2 } } }; + Print print; + Model model; + Test::init_print({ Test::cube(20), Test::cube(20) }, print, model, config, &overrides); + GCodeProcessorResult result; + Test::gcode(print, &result); + + REQUIRE(result.object_masses.size() == 2); + std::vector masses; + for (const ModelObject *object : model.objects) { + const Vec3d center = object->instance_bounding_box(0).center(); + masses.emplace_back(&*std::min_element(result.object_masses.begin(), result.object_masses.end(), [¢er](const auto &l, const auto &r) { + return (center_of(l.total()) - center).squaredNorm() < (center_of(r.total()) - center).squaredNorm(); + })); + } + CHECK_THAT(masses[1]->total().mass / masses[0]->total().mass, Catch::Matchers::WithinRel(3., 0.02)); + // The plate's center lies three quarters of the way to the dense cube. + const Vec3d plate = center_of(result.plate_mass.total()); + const Vec3d light = center_of(masses[0]->total()); + const Vec3d dense = center_of(masses[1]->total()); + CHECK_THAT((plate - light).dot(dense - light) / (dense - light).squaredNorm(), Catch::Matchers::WithinAbs(0.75, 0.01)); +} diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 21aa27bf29..c04019c179 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -65,6 +65,7 @@ add_executable(${_TEST_NAME}_tests test_ordering_strategies.cpp # test_png_io.cpp test_indexed_triangle_set.cpp + test_connected_bodies.cpp test_texture_displacement.cpp test_instance_lock.cpp ../libnest2d/printer_parts.cpp diff --git a/tests/libslic3r/test_connected_bodies.cpp b/tests/libslic3r/test_connected_bodies.cpp new file mode 100644 index 0000000000..b4e897ffe3 --- /dev/null +++ b/tests/libslic3r/test_connected_bodies.cpp @@ -0,0 +1,154 @@ +#include +#include +#include + +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/ExPolygon.hpp" +#include "libslic3r/Geometry.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/Polygon.hpp" +#include "libslic3r/ConnectedBodies.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/libslic3r.h" + +#include +#include +#include + +using namespace Slic3r; +using Catch::Matchers::WithinAbs; +using Catch::Matchers::WithinRel; + +namespace { + +ExPolygon rectangle(double x, double width) { return ExPolygon(Polygon::new_scale({ { x, 0. }, { x + width, 0. }, { x + width, 10. }, { x, 10. } })); } + +} // namespace + +TEST_CASE("Islands overlapping their neighbors' make one body", "[ConnectedBodies]") +{ + const ExPolygons apart = { rectangle(0., 10.), rectangle(20., 10.) }; + const ExPolygons bridge = { rectangle(0., 30.) }; + const ExPolygons left = { rectangle(0., 10.) }; + const ExPolygons right = { rectangle(20., 10.) }; + size_t count = 0; + + const std::vector> stacked = connected_bodies({ &apart, &apart }, count); + CHECK(count == 2); + CHECK(stacked[1][0] == stacked[0][0]); + CHECK(stacked[1][1] == stacked[0][1]); + + connected_bodies({ &apart, &bridge, &apart }, count); + CHECK(count == 1); + + // Neighbors that do not overlap stay apart even with one island a layer. + connected_bodies({ &left, &right }, count); + CHECK(count == 2); +} + +TEST_CASE("The island locator tests the outlines only where boxes overlap", "[ConnectedBodies]") +{ + const auto square = [](double from, double to) { + return Polygon::new_scale({ { from, from }, { to, from }, { to, to }, { from, to } }); + }; + Polygon hole = square(5., 25.); + hole.make_clockwise(); + ExPolygon ring(square(0., 30.)); + ring.holes.emplace_back(hole); + ExPolygon alone(square(40., 50.)); + const ExPolygons islands = { ring, ExPolygon(square(10., 20.)), alone }; + const IslandLocator locator(islands, scaled(1.)); + const auto at = [](double x, double y) { return Point::new_scale(x, y); }; + + CHECK(locator.find(at(2., 2.)).first == 0); + CHECK(locator.find(at(15., 15.)).first == 1); + // In the ring's hole, outside the island within it, the nearest outline counts. + CHECK(locator.find(at(7., 15.)).first == 0); + CHECK(locator.find(at(9.5, 15.)).first == 1); + // An island no other box reaches takes the margin past its outline. + CHECK(locator.find(at(50.5, 45.)).first == 2); + // Unless strict, as for an island whose neighbor is another instance's: then it is only the nearest, 0.5 mm away. + const auto [nearest, distance] = locator.find(at(50.5, 45.), true); + CHECK(nearest == 2); + CHECK_THAT(distance, WithinRel(sqr(scaled(0.5)), 1e-6)); + CHECK_FALSE(locator.holds(2, at(50.5, 45.), true)); + CHECK(locator.holds(2, at(50.5, 45.))); + CHECK_FALSE(locator.holds(1, at(7., 15.))); + CHECK(locator.find(at(35., 45.)).first == -1); +} + +TEST_CASE("Separate solids are separate bodies", "[ConnectedBodies]") +{ + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + const auto bodies = solid_bodies({ { &cube, Transform3d::Identity() }, { &cube, Geometry::translation_transform({ 20., 0., 0. }) } }, { 1., 1. }, {}, 100); + + REQUIRE(bodies.size() == 2); + CHECK_THAT(bodies[0].mass, WithinRel(1000., 1e-4)); + CHECK_THAT((bodies[0].center - Vec3d(5., 5., 5.)).norm(), WithinAbs(0., 1e-4)); + CHECK_THAT(bodies[1].mass, WithinRel(1000., 1e-4)); + CHECK_THAT((bodies[1].center - Vec3d(25., 5., 5.)).norm(), WithinAbs(0., 1e-4)); +} + +TEST_CASE("Overlapping solids are one body that counts the overlap once", "[ConnectedBodies]") +{ + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + const auto bodies = solid_bodies({ { &cube, Transform3d::Identity() }, { &cube, Geometry::translation_transform({ 5., 0., 0. }) } }, { 1., 1. }, {}, 100); + + // Their union is a 15 x 10 x 10 box, which spreads a^2 / 12 along each side a. + REQUIRE(bodies.size() == 1); + const SolidBody &body = bodies.front(); + CHECK_THAT(body.mass, WithinRel(1500., 1e-4)); + CHECK_THAT(body.volume, WithinRel(1500., 1e-4)); + CHECK_THAT((body.center - Vec3d(7.5, 5., 5.)).norm(), WithinAbs(0., 1e-4)); + const Matrix3d spread = Vec3d(225., 100., 100.).asDiagonal() * (1. / 12.); + CHECK_THAT((body.spread - spread).norm(), WithinAbs(0., 1e-4)); + + // Turned a quarter about z, the box spans what was its y in -x. + const BoundingBoxf3 box = body.bounding_box(Geometry::rotation_transform({ 0., 0., 0.5 * PI })); + CHECK_THAT((box.min - Vec3d(-10., 0., 0.)).norm(), WithinAbs(0., 1e-4)); + CHECK_THAT((box.max - Vec3d(0., 15., 10.)).norm(), WithinAbs(0., 1e-4)); +} + +TEST_CASE("A negative solid is cut away from the body", "[ConnectedBodies]") +{ + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + const indexed_triangle_set notch = its_make_cube(4., 4., 4.); + const auto bodies = solid_bodies({ { &cube, Transform3d::Identity() } }, { 1. }, { { ¬ch, Transform3d::Identity() } }, 100); + // A 10 mm cube centered at 5 less a 4 mm cube centered at 2, in each axis alike. + const double expected = (1000. * 5. - 64. * 2.) / (1000. - 64.); + + REQUIRE(bodies.size() == 1); + CHECK_THAT(bodies[0].mass, WithinRel(1000. - 64., 1e-4)); + CHECK_THAT((bodies[0].center - Vec3d(expected, expected, expected)).norm(), WithinAbs(0., 1e-4)); +} + +TEST_CASE("Each solid weighs its density, the later of two overlapping ones the overlap", "[ConnectedBodies]") +{ + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + const MeshInPlace left{ &cube, Transform3d::Identity() }; + const MeshInPlace right{ &cube, Geometry::translation_transform({ 5., 0., 0. }) }; + + // The right cube, three times as dense, prints the overlap from x 5 to 10. + auto bodies = solid_bodies({ left, right }, { 1., 3. }, {}, 100); + REQUIRE(bodies.size() == 1); + CHECK_THAT(bodies[0].mass, WithinRel(500. + 3. * 1000., 1e-4)); + CHECK_THAT(bodies[0].volume, WithinRel(1500., 1e-4)); + CHECK_THAT(bodies[0].center.x(), WithinAbs((500. * 2.5 + 3000. * 10.) / 3500., 1e-4)); + + // Listed the other way round, the left cube prints it. + bodies = solid_bodies({ right, left }, { 3., 1. }, {}, 100); + REQUIRE(bodies.size() == 1); + CHECK_THAT(bodies[0].mass, WithinRel(1000. + 3. * 500., 1e-4)); + CHECK_THAT(bodies[0].center.x(), WithinAbs((1000. * 5. + 1500. * 12.5) / 2500., 1e-4)); +} + +TEST_CASE("Separate solids weigh their own densities", "[ConnectedBodies]") +{ + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + const auto bodies = solid_bodies({ { &cube, Transform3d::Identity() }, { &cube, Geometry::translation_transform({ 20., 0., 0. }) } }, + { 1.24, 2. }, {}, 100); + + REQUIRE(bodies.size() == 2); + CHECK_THAT(bodies[0].mass, WithinRel(1240., 1e-4)); + CHECK_THAT(bodies[1].mass, WithinRel(2000., 1e-4)); +} diff --git a/tests/libslic3r/test_indexed_triangle_set.cpp b/tests/libslic3r/test_indexed_triangle_set.cpp index c6e5007c4b..202c1ad889 100644 --- a/tests/libslic3r/test_indexed_triangle_set.cpp +++ b/tests/libslic3r/test_indexed_triangle_set.cpp @@ -12,11 +12,16 @@ #include #include +#include +#include +#include "libslic3r/Geometry.hpp" #include "libslic3r/TriangleMesh.hpp" #include "test_utils.hpp" using namespace Slic3r; +using Catch::Matchers::WithinAbs; +using Catch::Matchers::WithinRel; TEST_CASE("Split empty mesh", "[its_split][its]") { @@ -317,3 +322,75 @@ TEST_CASE("Simplified cube should not be empty.", "[its]") its_quadric_edge_collapse(its, wanted_count, &max_error); CHECK(!its.indices.empty()); } + +TEST_CASE("A box far from the origin has its center of mass at its center and spreads as a box", "[its]") +{ + indexed_triangle_set box = its_make_cube(10., 20., 30.); + for (Vec3f &v : box.vertices) + v += Vec3f(1000.f, 2000.f, 300.f); + const MassProperties solid = its_mass_properties(box); + CHECK_THAT(solid.volume, WithinRel(10. * 20. * 30., 1e-6)); + CHECK_THAT(solid.mass, WithinRel(solid.volume, 1e-12)); + CHECK_THAT(solid.center.x(), WithinAbs(1005., 1e-6)); + CHECK_THAT(solid.center.y(), WithinAbs(2010., 1e-6)); + CHECK_THAT(solid.center.z(), WithinAbs(315., 1e-6)); + // A box of side a spreads a^2 / 12 along it. + const Matrix3d spread = Vec3d(100., 400., 900.).asDiagonal() * (1. / 12.); + CHECK_THAT((solid.spread - spread).norm(), WithinAbs(0., 1e-6)); +} + +TEST_CASE("The center of mass of a cone lies a quarter of its height above the base", "[its]") +{ + // Neither the surface centroid nor the vertex average lands there. + const double h = 40.; + const MassProperties solid = its_mass_properties(its_make_cone(10., h)); + CHECK(solid.volume > 0.); + CHECK_THAT(solid.center.z(), WithinAbs(h / 4., 1e-4)); + CHECK_THAT(solid.center.x(), WithinAbs(0., 1e-4)); + CHECK_THAT(solid.center.y(), WithinAbs(0., 1e-4)); + // 3 h^2 / 80 along the axis. + CHECK_THAT(solid.spread(2, 2), WithinRel(3. * h * h / 80., 1e-4)); +} + +TEST_CASE("A cavity moves the center of mass away from it", "[its]") +{ + indexed_triangle_set solid = its_make_cube(20., 20., 20.); + indexed_triangle_set cavity = its_make_cube(10., 10., 8.); + for (Vec3f &v : cavity.vertices) + v += Vec3f(5.f, 5.f, 10.f); + its_flip_triangles(cavity); + its_merge(solid, cavity); + const MassProperties hollow = its_mass_properties(solid); + // A 20 mm cube centered at z 10 less a 10x10x8 mm cavity centered at z 14. + CHECK_THAT(hollow.volume, WithinRel(8000. - 800., 1e-6)); + CHECK_THAT(hollow.center.x(), WithinAbs(10., 1e-6)); + CHECK_THAT(hollow.center.y(), WithinAbs(10., 1e-6)); + CHECK_THAT(hollow.center.z(), WithinAbs((8000. * 10. - 800. * 14.) / (8000. - 800.), 1e-6)); +} + +TEST_CASE("The mass properties follow an affine transformation of the mesh", "[its]") +{ + indexed_triangle_set cone = its_make_cone(10., 40.); + const MassProperties solid = its_mass_properties(cone); + const Transform3d trafo = Geometry::translation_transform({ 50., -20., 7. }) * Geometry::rotation_transform({ 0.3, -0.5, 1.2 }) * + Geometry::scale_transform({ 2., 0.5, 1.5 }); + for (Vec3f &v : cone.vertices) + v = (trafo * v.cast()).cast(); + const MassProperties moved = its_mass_properties(cone); + const MassProperties expected = solid.transformed(trafo); + CHECK_THAT(moved.volume, WithinRel(expected.volume, 1e-5)); + CHECK_THAT(moved.mass, WithinRel(expected.mass, 1e-5)); + CHECK_THAT((moved.center - expected.center).norm(), WithinAbs(0., 1e-4)); + CHECK_THAT((moved.spread - expected.spread).norm(), WithinAbs(0., 1e-3)); +} + +TEST_CASE("Flipped faces keep the mass properties", "[its]") +{ + indexed_triangle_set cone = its_make_cone(10., 40.); + const MassProperties solid = its_mass_properties(cone); + its_flip_triangles(cone); + const MassProperties flipped = its_mass_properties(cone); + CHECK_THAT(flipped.volume, WithinRel(solid.volume, 1e-9)); + CHECK_THAT((flipped.center - solid.center).norm(), WithinAbs(0., 1e-9)); + CHECK_THAT((flipped.spread - solid.spread).norm(), WithinAbs(0., 1e-9)); +}