mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-10 01:01:57 +00:00
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 <kris.austin@gmail.com>
This commit is contained in:
co-authored by
Rodrigo Faselli
Kris Austin
parent
b5ef24e7ff
commit
0f3e8fbf27
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -112,6 +112,8 @@ set(lisbslic3r_sources
|
||||
CommonDefs.hpp
|
||||
Config.cpp
|
||||
Config.hpp
|
||||
ConnectedBodies.cpp
|
||||
ConnectedBodies.hpp
|
||||
ContourZ.cpp
|
||||
CustomGCode.cpp
|
||||
CustomGCode.hpp
|
||||
|
||||
@@ -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 <tbb/blocked_range.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
std::vector<std::vector<size_t>> connected_bodies(const std::vector<const ExPolygons *> &layers, size_t &count,
|
||||
const std::function<void()> &throw_if_canceled)
|
||||
{
|
||||
// Union-find over the islands of all layers, numbered layer after layer.
|
||||
std::vector<size_t> first(layers.size() + 1, 0);
|
||||
for (size_t l = 0; l < layers.size(); ++l)
|
||||
first[l + 1] = first[l] + layers[l]->size();
|
||||
std::vector<size_t> 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<std::vector<BoundingBox>> 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<AABBTreeIndirect::BoundingBoxWrapper> 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<size_t> body(parent.size(), std::numeric_limits<size_t>::max());
|
||||
std::vector<std::vector<size_t>> 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<size_t>::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<size_t> 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<int, double> IslandLocator::find(const Point &point, bool strict) const
|
||||
{
|
||||
int nearest = -1;
|
||||
double distance = std::numeric_limits<double>::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<double>().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<SolidBody> solid_bodies(const std::vector<MeshInPlace> &solids, const std::vector<double> &densities,
|
||||
const std::vector<MeshInPlace> &negatives, size_t slabs)
|
||||
{
|
||||
assert(densities.size() == solids.size());
|
||||
double z_min = std::numeric_limits<double>::max();
|
||||
double z_max = std::numeric_limits<double>::lowest();
|
||||
for (const auto &[mesh, trafo] : solids)
|
||||
for (const stl_vertex &v : mesh->vertices) {
|
||||
const double z = (trafo * v.cast<double>()).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<float> 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<std::vector<ExPolygons>> slices;
|
||||
for (const MeshInPlace &solid : solids)
|
||||
slices.emplace_back(slice(solid));
|
||||
std::vector<ExPolygons> cut(slabs);
|
||||
for (const MeshInPlace &negative : negatives) {
|
||||
std::vector<ExPolygons> 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<ExPolygons> islands(slabs);
|
||||
std::vector<std::vector<Moments>> moments(slabs);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, slabs), [&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t k = range.begin(); k < range.end(); ++k) {
|
||||
ExPolygons all;
|
||||
for (const std::vector<ExPolygons> &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<const ExPolygons *> layers;
|
||||
layers.reserve(slabs);
|
||||
for (const ExPolygons &layer : islands)
|
||||
layers.emplace_back(&layer);
|
||||
size_t count = 0;
|
||||
const std::vector<std::vector<size_t>> bodies = connected_bodies(layers, count);
|
||||
std::vector<Moments> sums(count);
|
||||
std::vector<Points> outlines(count);
|
||||
std::vector<SolidBody> out(count);
|
||||
for (SolidBody &body : out) {
|
||||
body.z_min = std::numeric_limits<double>::max();
|
||||
body.z_max = std::numeric_limits<double>::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
|
||||
@@ -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 <admesh/stl.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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<std::vector<size_t>> connected_bodies(const std::vector<const ExPolygons *> &layers, size_t &count,
|
||||
const std::function<void()> &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<int, double> find(const Point &point, bool strict = false) const;
|
||||
const std::vector<BoundingBox> &boxes() const { return m_boxes; }
|
||||
|
||||
private:
|
||||
const ExPolygons *m_islands;
|
||||
std::vector<BoundingBox> m_boxes;
|
||||
std::vector<bool> m_alone;
|
||||
};
|
||||
|
||||
using MeshInPlace = std::pair<const indexed_triangle_set *, Transform3d>;
|
||||
|
||||
// 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<SolidBody> solid_bodies(const std::vector<MeshInPlace> &solids, const std::vector<double> &densities,
|
||||
const std::vector<MeshInPlace> &negatives, size_t slabs);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -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<coordf_t> print_zs;
|
||||
// Per layer, the body of each island and a locator whose boxes are widened for walls reaching past them.
|
||||
std::vector<std::vector<size_t>> bodies;
|
||||
std::vector<IslandLocator> islands;
|
||||
// Per instance, whether its widened box reaches another's, so that the box of an island proves nothing.
|
||||
std::vector<bool> crowded;
|
||||
};
|
||||
std::vector<Object> objects;
|
||||
std::vector<GCodeProcessorResult::ObjectMass> 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<std::vector<size_t>> 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<const ExPolygons *> 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<coord_t>(1.));
|
||||
}
|
||||
}
|
||||
if (objects.empty())
|
||||
return;
|
||||
std::vector<BoundingBox> 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<Hit>()](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<double>::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<double>().squaredNorm();
|
||||
const bool in = gap == 0.;
|
||||
const double d = in ? (box.center() - p).cast<double>().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<Hit> nearest;
|
||||
double distance = std::numeric_limits<double>::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();
|
||||
|
||||
@@ -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<std::optional<double>, 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<double>();
|
||||
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<unsigned int>(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<GCodeProcessorResult::ObjectMass> &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);
|
||||
|
||||
@@ -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<std::string> 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<Sum> 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<MoveVertex> 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<ObjectMass> object_masses;
|
||||
std::vector<ObjectMass> body_masses;
|
||||
// One per object instance, of its supports and raft.
|
||||
std::vector<ObjectMass> support_masses;
|
||||
// Positions of ends of lines of the final G-code this->filename after TimeProcessor::post_process() finalizes the G-code.
|
||||
std::vector<size_t> lines_ends;
|
||||
Pointfs printable_area;
|
||||
@@ -360,6 +399,10 @@ class Print;
|
||||
filename = std::forward<Other>(other).filename;
|
||||
id = std::forward<Other>(other).id;
|
||||
moves = std::forward<Other>(other).moves;
|
||||
plate_mass = std::forward<Other>(other).plate_mass;
|
||||
object_masses = std::forward<Other>(other).object_masses;
|
||||
body_masses = std::forward<Other>(other).body_masses;
|
||||
support_masses = std::forward<Other>(other).support_masses;
|
||||
lines_ends = std::forward<Other>(other).lines_ends;
|
||||
printable_area = std::forward<Other>(other).printable_area;
|
||||
bed_exclude_area = std::forward<Other>(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<MassLocation(const Vec3d &point, bool support)>;
|
||||
|
||||
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<float> m_remaining_volume;
|
||||
ExtruderTemps m_filament_nozzle_temp;
|
||||
@@ -1280,6 +1333,13 @@ class Print;
|
||||
const std::vector<std::set<int>>& 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<GCodeProcessorResult::ObjectMass> 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<MultiNozzleUtils::NozzleGroupResultBase>& 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.
|
||||
|
||||
@@ -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<size_t> 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<size_t> 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<AABBTreeIndirect::BoundingBoxWrapper> 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<size_t> body_of_root(nreg, size_t(-1));
|
||||
for (size_t i = 0; i < nl; ++ i) {
|
||||
std::vector<const ExPolygons *> islands;
|
||||
islands.reserve(m_layers.size());
|
||||
for (const Layer *layer : m_layers)
|
||||
islands.emplace_back(&layer->lslices);
|
||||
size_t bodies = 0;
|
||||
std::vector<std::vector<size_t>> 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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>();
|
||||
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<double>() - p0;
|
||||
const Vec3d b = its.vertices[face(1)].cast<double>() - p0;
|
||||
const Vec3d c = its.vertices[face(2)].cast<double>() - 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())
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <array>
|
||||
#include <cereal/specialize.hpp>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#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);
|
||||
|
||||
@@ -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<CustomGCode::Item>();
|
||||
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();
|
||||
|
||||
@@ -260,6 +260,10 @@ private:
|
||||
GCodeProcessorResult::SettingsIds m_settings_ids;
|
||||
|
||||
std::vector<CustomGCode::Item> m_custom_gcode_per_print_z;
|
||||
GCodeProcessorResult::ObjectMass m_plate_mass;
|
||||
std::vector<GCodeProcessorResult::ObjectMass> m_object_masses;
|
||||
std::vector<GCodeProcessorResult::ObjectMass> m_body_masses;
|
||||
std::vector<GCodeProcessorResult::ObjectMass> m_support_masses;
|
||||
|
||||
bool m_contained_in_bed{ true };
|
||||
mutable bool m_no_render_path { false };
|
||||
@@ -343,6 +347,10 @@ public:
|
||||
std::vector<float> get_layers_times() const { return m_viewer.get_layers_estimated_times(); }
|
||||
|
||||
const std::array<size_t,2> &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<GCodeProcessorResult::ObjectMass>& get_object_masses() const { return m_object_masses; }
|
||||
const std::vector<GCodeProcessorResult::ObjectMass>& get_body_masses() const { return m_body_masses; }
|
||||
const std::vector<GCodeProcessorResult::ObjectMass>& 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.
|
||||
|
||||
@@ -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 <tbb/spin_mutex.h>
|
||||
|
||||
#include <boost/functional/hash.hpp>
|
||||
#include <boost/format.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
|
||||
@@ -1000,6 +1003,325 @@ void GLCanvas3D::Labels::render(const std::vector<const ModelInstance*>& 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<double, 4> 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<const GLVolume*> volumes;
|
||||
};
|
||||
std::map<int, std::map<int, Instance>> 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<double> 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<int, Marker> plates;
|
||||
std::map<size_t, MassProperties> meshes;
|
||||
std::map<size_t, Bodies> 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<const GLVolume*> 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<MeshInPlace> solids;
|
||||
std::vector<double> densities;
|
||||
std::vector<MeshInPlace> negatives;
|
||||
std::vector<Bodies::Volume> 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<SolidBody>* 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<Marker> 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<GLModel::Geometry, 2> octants;
|
||||
for (size_t i = 0; i + 2 < sphere.indices_count(); i += 3) {
|
||||
const std::array<unsigned int, 3> 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<std::array<ColorRGBA, 2>, 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<Vec3d> ends = { center, center + marker_radii[kind] * scale * camera.get_dir_right() };
|
||||
const Points screen = CameraUtils::project(camera, ends);
|
||||
if ((screen[0].cast<double>() - mouse).norm() <= (screen[1] - screen[0]).cast<double>().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<Marker>& 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<unsigned int>(cnv_size.get_width()), static_cast<unsigned int>(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<double>())) {
|
||||
m_mouse.ignore_left_up = true;
|
||||
return;
|
||||
}
|
||||
|
||||
bool any_gizmo_active = m_gizmos.get_current() != nullptr;
|
||||
|
||||
std::map<MouseButton, MouseAction> 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
|
||||
|
||||
@@ -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 <cstdlib>
|
||||
#include <imgui.h>
|
||||
@@ -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<const ModelInstance*>& 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<std::vector<Marker>, mkCount>;
|
||||
|
||||
// The marker's two colors of alternating octants.
|
||||
std::array<GLModel, 2> m_octants;
|
||||
// Mass properties at unit density of each ModelVolume's mesh, by ModelVolume id, which a new mesh changes.
|
||||
std::map<size_t, MassProperties> 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<Volume> volumes;
|
||||
size_t slabs{ 0 };
|
||||
std::vector<SolidBody> bodies;
|
||||
};
|
||||
std::map<size_t, Bodies> 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<Markers, 2> 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<Pick> 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;
|
||||
|
||||
@@ -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<std::string> split_str(std::string src, std::string separator);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
#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 <algorithm>
|
||||
#include <cstddef>
|
||||
#include <fstream>
|
||||
#include <initializer_list>
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <sstream>
|
||||
@@ -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<std::pair<double, Vec3d>> 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<TriangleMesh> 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<std::vector<ConfigBase::SetDeserializeItem>> 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<const GCodeProcessorResult::ObjectMass *> 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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
#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 <cstddef>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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<std::vector<size_t>> 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<coord_t>(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<double>(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));
|
||||
}
|
||||
@@ -12,11 +12,16 @@
|
||||
#include <string>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
#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<double>()).cast<float>();
|
||||
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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user