mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-20 14:08:20 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c71e456b95 | ||
|
|
f048ede640 | ||
|
|
07fa713f0c | ||
|
|
4c85d0e80a |
No files matched your search
@@ -122,8 +122,8 @@ void update_arrange_params(ArrangeParams& params, const DynamicPrintConfig* prin
|
||||
|
||||
void update_selected_items_inflation(ArrangePolygons& selected, const DynamicPrintConfig* print_cfg, ArrangeParams& params) {
|
||||
// do not inflate brim_width. Objects are allowed to have overlapped brim.
|
||||
Points bedpts = get_shrink_bedpts(print_cfg, params);
|
||||
BoundingBox bedbb = Polygon(bedpts).bounding_box();
|
||||
// A brim reaching the printable area's edge is fine, so measure against the full bed.
|
||||
BoundingBox bedbb = Polygon(get_bed_shape(*print_cfg)).bounding_box();
|
||||
// set obj distance for auto seq_print
|
||||
if (params.is_seq_print) {
|
||||
bool all_objects_are_short = std::all_of(selected.begin(), selected.end(), [&](ArrangePolygon& ap) { return ap.height < params.nozzle_height; });
|
||||
@@ -144,14 +144,30 @@ void update_selected_items_inflation(ArrangePolygons& selected, const DynamicPri
|
||||
// 3. otherwise, use each object's own brim width
|
||||
ap.inflation = params.min_obj_distance != 0 ? params.min_obj_distance / 2 :
|
||||
plate_has_tree_support ? scaled(brim_max / 2) : scaled(ap.brim_width);
|
||||
BoundingBox apbb = ap.poly.contour.bounding_box();
|
||||
auto diffx = bedbb.size().x() - apbb.size().x() - 5;
|
||||
auto diffy = bedbb.size().y() - apbb.size().y() - 5;
|
||||
if (diffx > 0 && diffy > 0) {
|
||||
auto min_diff = std::min(diffx, diffy);
|
||||
ap.inflation = std::min(min_diff / 2, ap.inflation);
|
||||
}
|
||||
if (params.min_obj_distance != 0)
|
||||
return; // an explicit object distance is the user's call, not ours to shrink
|
||||
// An item cannot be larger than the printable area, inflation included. Inflating
|
||||
// an almost bed filling item would leave it too big to ever be placed.
|
||||
BoundingBox apbb = ap.poly.contour.bounding_box();
|
||||
auto diffx = bedbb.size().x() - apbb.size().x();
|
||||
auto diffy = bedbb.size().y() - apbb.size().y();
|
||||
ap.inflation = std::max<coord_t>(0, std::min({ ap.inflation, diffx / 2, diffy / 2 }));
|
||||
});
|
||||
|
||||
// Give the bed margin, which the arrange bed keeps from the printable area, back
|
||||
// where the largest item needs it. The skirt keeps its room: pushing a skirt off
|
||||
// the bed is worse than saying the item does not fit.
|
||||
if (!selected.empty()) {
|
||||
coord_t max_width = 0, max_height = 0;
|
||||
for (const ArrangePolygon &ap : selected) {
|
||||
BoundingBox apbb = ap.poly.contour.bounding_box();
|
||||
max_width = std::max(max_width, apbb.size().x() + 2 * ap.inflation);
|
||||
max_height = std::max(max_height, apbb.size().y() + 2 * ap.inflation);
|
||||
}
|
||||
auto margin = [](coord_t bed, coord_t item) { return std::max(0., unscale<double>(bed - item) / 2.); };
|
||||
params.bed_shrink_x = std::min<double>(params.bed_shrink_x, std::max<double>(params.brim_skirt_distance, margin(bedbb.size().x(), max_width)));
|
||||
params.bed_shrink_y = std::min<double>(params.bed_shrink_y, std::max<double>(params.brim_skirt_distance, margin(bedbb.size().y(), max_height)));
|
||||
}
|
||||
}
|
||||
|
||||
void update_unselected_items_inflation(ArrangePolygons& unselected, const DynamicPrintConfig* print_cfg, const ArrangeParams& params)
|
||||
|
||||
@@ -213,13 +213,29 @@ int find_plane_at_point(const std::vector<LayOnFacePlane> &planes, const Transfo
|
||||
return -1;
|
||||
}
|
||||
|
||||
Transform3d lay_on_face_rotation(const Vec3d &normal, const Transform3d &instance_matrix_no_offset)
|
||||
{
|
||||
// Normal in instance coordinates: a normal transforms by the inverse transpose.
|
||||
const Vec3d tnormal = (instance_matrix_no_offset.matrix().block(0, 0, 3, 3).inverse().transpose() * normal).normalized();
|
||||
const Vec3d down = -Vec3d::UnitZ();
|
||||
|
||||
// A face already pointing straight up needs a half turn, and every horizontal axis does
|
||||
// that equally well. setFromTwoVectors() resolves the choice through an SVD whose null
|
||||
// space is the whole XY plane, so the axis follows rounding noise and the object lands at
|
||||
// an arbitrary angle in the plate plane. Turn it over about X instead, which keeps the
|
||||
// footprint the object already had.
|
||||
if (tnormal.dot(down) < -1. + 1e-9)
|
||||
return Transform3d(Eigen::AngleAxisd(PI, Vec3d::UnitX()));
|
||||
|
||||
return Transform3d(Eigen::Quaterniond().setFromTwoVectors(tnormal, down));
|
||||
}
|
||||
|
||||
void lay_on_face(ModelObject &object, size_t instance_idx, const Vec3d &normal)
|
||||
{
|
||||
ModelInstance &instance = *object.instances[instance_idx];
|
||||
const Geometry::Transformation &trafo = instance.get_transformation();
|
||||
// Same rotation as Selection::flattening_rotate(): turn the transformed normal to point down.
|
||||
const Vec3d tnormal = trafo.get_matrix().matrix().block(0, 0, 3, 3).inverse().transpose() * normal;
|
||||
const Transform3d rotation = Transform3d(Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ()));
|
||||
// Same rotation as Selection::flattening_rotate(), which the gizmo uses.
|
||||
const Transform3d rotation = lay_on_face_rotation(normal, trafo.get_matrix_no_offset());
|
||||
instance.set_transformation(Geometry::Transformation(trafo.get_offset_matrix() * rotation * trafo.get_matrix_no_offset()));
|
||||
// Drop this instance only: ensure_on_bed() skips instances without auto_drop and measures the first instance.
|
||||
object.translate_instance(instance_idx, -object.instance_bounding_box(instance_idx).min.z() * Vec3d::UnitZ());
|
||||
|
||||
@@ -46,4 +46,10 @@ int find_plane_at_point(const std::vector<LayOnFacePlane> &planes, const Transfo
|
||||
// the gizmo applies, then drops the instance so its lowest point is at z = 0.
|
||||
void lay_on_face(ModelObject &object, size_t instance_idx, const Vec3d &normal);
|
||||
|
||||
// The rotation that turns `normal` (object coordinates) to point down, given the instance's
|
||||
// rotation, scale and mirror. A face that already points straight up is turned over about X:
|
||||
// the axis setFromTwoVectors() picks there is arbitrary, and it lands the object at a random
|
||||
// angle in the plate plane.
|
||||
Transform3d lay_on_face_rotation(const Vec3d &normal, const Transform3d &instance_matrix_no_offset);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -771,6 +771,17 @@ void ArrangeJob::finalize(bool canceled, std::exception_ptr &eptr) {
|
||||
if (canceled || eptr)
|
||||
return;
|
||||
|
||||
// Nothing could be placed: leave the plates and the instances exactly as they were.
|
||||
// Rebuilding the plates would only re-home the instances by their bounds, which moves
|
||||
// a too large item to whichever plate it happens to reach over.
|
||||
const bool nothing_arranged = !m_selected.empty() &&
|
||||
std::all_of(m_selected.begin(), m_selected.end(), [](const ArrangePolygon &ap) { return !ap.is_arranged(); });
|
||||
if (nothing_arranged) {
|
||||
m_plater->get_notification_manager()->close_notification_of_type(NotificationType::ArrangeOngoing);
|
||||
m_plater->m_arrange_running.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Unprintable items go to the last virtual bed
|
||||
int beds = 0;
|
||||
|
||||
@@ -870,12 +881,13 @@ void ArrangeJob::finalize(bool canceled, std::exception_ptr &eptr) {
|
||||
}
|
||||
m_plater->get_notification_manager()->close_notification_of_type(NotificationType::ArrangeOngoing);
|
||||
|
||||
//BBS: reload all objects due to arrange
|
||||
//BBS: reload all objects due to arrange. Plates are never recycled here: arranging
|
||||
// must not remove a plate the user created, even when the arrange left it empty.
|
||||
if (only_on_partplate) {
|
||||
plate_list.rebuild_plates_after_arrangement(!only_on_partplate, true, current_plate_index);
|
||||
plate_list.rebuild_plates_after_arrangement(false, true, current_plate_index);
|
||||
}
|
||||
else {
|
||||
plate_list.rebuild_plates_after_arrangement(!only_on_partplate, true);
|
||||
plate_list.rebuild_plates_after_arrangement(false, true);
|
||||
}
|
||||
|
||||
// unlock the plates we just locked
|
||||
|
||||
@@ -248,7 +248,6 @@ void FillBedJob::process(Ctl &ctl)
|
||||
if (m_object_idx == -1 || m_selected.empty()) return;
|
||||
|
||||
update_arrange_params(params, m_plater->config(), m_selected);
|
||||
m_bedpts = get_shrink_bedpts(m_plater->config(), params);
|
||||
|
||||
auto &partplate_list = m_plater->get_partplate_list();
|
||||
auto &print = wxGetApp().plater()->get_partplate_list().get_current_fff_print();
|
||||
@@ -260,6 +259,8 @@ void FillBedJob::process(Ctl &ctl)
|
||||
|
||||
update_selected_items_inflation(m_selected, m_plater->config(), params);
|
||||
update_unselected_items_inflation(m_unselected, m_plater->config(), params);
|
||||
// After the inflation, which may give back part of the bed margin.
|
||||
m_bedpts = get_shrink_bedpts(m_plater->config(), params);
|
||||
|
||||
bool do_stop = false;
|
||||
params.stopcondition = [&ctl, &do_stop]() {
|
||||
|
||||
@@ -7048,19 +7048,14 @@ void PartPlateList::postprocess_arrange_polygon(arrangement::ArrangePolygon& arr
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": bed_idx %1%, selected %2%, translation(x) %3%, (y) %4%") % arrange_polygon.bed_idx % selected % unscale<double>(arrange_polygon.translation(X)) % unscale<double>(arrange_polygon.translation(Y));
|
||||
|
||||
// UNARRANGED means the item could not be placed, so it has no bed to map onto and
|
||||
// must be left where the user had it. Moving it to a virtual bed here would shift
|
||||
// it off the plate, because no such plate is created for it afterwards.
|
||||
if (arrange_polygon.bed_idx == arrangement::UNARRANGED)
|
||||
return;
|
||||
|
||||
if ((selected) || (arrange_polygon.bed_idx != PartPlateList::MAX_PLATES_COUNT))
|
||||
{
|
||||
if (arrange_polygon.bed_idx == -1)
|
||||
{
|
||||
// outarea for large object
|
||||
arrange_polygon.bed_idx = m_plate_list.size();
|
||||
BoundingBox apbox = get_extents(arrange_polygon.transformed_poly()); // the item may have been rotated
|
||||
auto apbox_size = apbox.size();
|
||||
|
||||
arrange_polygon.translation(X) = 0.5 * apbox_size[0];
|
||||
arrange_polygon.translation(Y) = scaled<double>(static_cast<double>(m_plate_depth)) - 0.5 * apbox_size[1];
|
||||
}
|
||||
|
||||
arrange_polygon.row = arrange_polygon.bed_idx / m_plate_cols;
|
||||
arrange_polygon.col = arrange_polygon.bed_idx % m_plate_cols;
|
||||
arrange_polygon.translation(X) += scaled<double>(plate_stride_x() * arrange_polygon.col);
|
||||
|
||||
@@ -11034,7 +11034,17 @@ void Plater::priv::split_volume()
|
||||
void Plater::priv::scale_selection_to_fit_print_volume()
|
||||
{
|
||||
#if ENABLE_ENHANCED_PRINT_VOLUME_FIT
|
||||
this->view3D->get_canvas3d()->get_selection().scale_to_fit_print_volume(this->bed.build_volume());
|
||||
const BuildVolume volume = this->bed.build_volume();
|
||||
// Every plate shares the bed size, but each sits at its own origin: fit onto the
|
||||
// plate the selection is on instead of always onto the first one.
|
||||
PartPlate* plate = this->partplate_list.get_curr_plate();
|
||||
const Vec3d bed_center = plate != nullptr ? plate->get_center_origin() :
|
||||
Vec3d(volume.bed_center().x(), volume.bed_center().y(), 0.);
|
||||
// On a multi-nozzle printer each nozzle reaches only part of the bed, so a model fitted
|
||||
// to the bed can still exceed a nozzle's printable range. Fit into the volume every
|
||||
// nozzle shares, which is also the area Arrange places items in.
|
||||
const BoundingBoxf3 print_volume = plate != nullptr ? plate->get_build_volume(true) : volume.bounding_volume();
|
||||
this->view3D->get_canvas3d()->get_selection().scale_to_fit_print_volume(volume, Vec2d(bed_center.x(), bed_center.y()), print_volume);
|
||||
#else
|
||||
this->view3D->get_canvas3d()->get_selection().scale_to_fit_print_volume(*config);
|
||||
#endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/LayOnFace.hpp"
|
||||
#include "slic3r/GUI/GLModel.hpp"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -1472,11 +1473,9 @@ void Selection::flattening_rotate(const Vec3d& normal)
|
||||
|
||||
for (unsigned int i : m_list) {
|
||||
GLVolume& v = *(*m_volumes)[i];
|
||||
// Normal transformed from the object coordinate space to the world coordinate space.
|
||||
// Additional rotation to align the face normal with the down vector in the world coordinate space.
|
||||
const Geometry::Transformation& old_inst_trafo = v.get_instance_transformation();
|
||||
const Vec3d tnormal = old_inst_trafo.get_matrix().matrix().block(0, 0, 3, 3).inverse().transpose() * normal;
|
||||
// Additional rotation to align tnormal with the down vector in the world coordinate space.
|
||||
const Transform3d rotation_matrix = Transform3d(Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ()));
|
||||
const Transform3d rotation_matrix = lay_on_face_rotation(normal, old_inst_trafo.get_matrix_no_offset());
|
||||
v.set_instance_transformation(old_inst_trafo.get_offset_matrix() * rotation_matrix * old_inst_trafo.get_matrix_no_offset());
|
||||
}
|
||||
|
||||
@@ -1496,7 +1495,7 @@ void Selection::scale(const Vec3d& scale, TransformationType transformation_type
|
||||
}
|
||||
|
||||
#if ENABLE_ENHANCED_PRINT_VOLUME_FIT
|
||||
void Selection::scale_to_fit_print_volume(const BuildVolume& volume)
|
||||
void Selection::scale_to_fit_print_volume(const BuildVolume& volume, const Vec2d& bed_center, const BoundingBoxf3& print_volume)
|
||||
{
|
||||
auto fit = [this](double s, Vec3d offset) {
|
||||
if (s <= 0.0 || s == 1.0)
|
||||
@@ -1526,8 +1525,7 @@ void Selection::scale_to_fit_print_volume(const BuildVolume& volume)
|
||||
//wxGetApp().obj_manipul()->set_dirty();
|
||||
};
|
||||
|
||||
auto fit_rectangle = [this, fit](const BuildVolume& volume) {
|
||||
const BoundingBoxf3 print_volume = volume.bounding_volume();
|
||||
auto fit_rectangle = [this, fit, &print_volume]() {
|
||||
const Vec3d print_volume_size = print_volume.size();
|
||||
|
||||
// adds 1/100th of a mm on all sides to avoid false out of print volume detections due to floating-point roundings
|
||||
@@ -1541,7 +1539,7 @@ void Selection::scale_to_fit_print_volume(const BuildVolume& volume)
|
||||
fit(std::min(sx, std::min(sy, sz)), print_volume.center() - get_bounding_box().center());
|
||||
};
|
||||
|
||||
auto fit_circle = [this, fit](const BuildVolume& volume) {
|
||||
auto fit_circle = [this, fit, &bed_center](const BuildVolume& volume) {
|
||||
const Geometry::Circled& print_circle = volume.circle();
|
||||
double print_circle_radius = unscale<double>(print_circle.radius);
|
||||
|
||||
@@ -1572,7 +1570,9 @@ void Selection::scale_to_fit_print_volume(const BuildVolume& volume)
|
||||
const double s = std::min(print_circle_radius / circle_radius, volume.printable_height() / max_z);
|
||||
const Vec3d sel_center = get_bounding_box().center();
|
||||
const Vec3d offset = s * (Vec3d(unscale<double>(circle.center.x()), unscale<double>(circle.center.y()), 0.5 * max_z) - sel_center);
|
||||
const Vec3d print_center = { unscale<double>(print_circle.center.x()), unscale<double>(print_circle.center.y()), 0.5 * volume.printable_height() };
|
||||
// The active plate's bed is the same circle, moved by its centre.
|
||||
const Vec3d plate_shift = Vec3d(bed_center.x() - volume.bed_center().x(), bed_center.y() - volume.bed_center().y(), 0.);
|
||||
const Vec3d print_center = Vec3d(unscale<double>(print_circle.center.x()), unscale<double>(print_circle.center.y()), 0.5 * volume.printable_height()) + plate_shift;
|
||||
fit(s, print_center - (sel_center + offset));
|
||||
};
|
||||
|
||||
@@ -1581,7 +1581,7 @@ void Selection::scale_to_fit_print_volume(const BuildVolume& volume)
|
||||
|
||||
switch (volume.type())
|
||||
{
|
||||
case BuildVolume_Type::Rectangle: { fit_rectangle(volume); break; }
|
||||
case BuildVolume_Type::Rectangle: { fit_rectangle(); break; }
|
||||
case BuildVolume_Type::Circle: { fit_circle(volume); break; }
|
||||
default: { break; }
|
||||
}
|
||||
|
||||
@@ -354,7 +354,10 @@ public:
|
||||
void flattening_rotate(const Vec3d& normal);
|
||||
void scale(const Vec3d& scale, TransformationType transformation_type);
|
||||
#if ENABLE_ENHANCED_PRINT_VOLUME_FIT
|
||||
void scale_to_fit_print_volume(const BuildVolume& volume);
|
||||
// bed_center is the centre of the plate the selection is on: every plate shares the
|
||||
// bed size, but each sits at its own origin. print_volume is the box the selection must
|
||||
// fit in, which is the area every nozzle reaches rather than the whole bed.
|
||||
void scale_to_fit_print_volume(const BuildVolume& volume, const Vec2d& bed_center, const BoundingBoxf3& print_volume);
|
||||
#else
|
||||
void scale_to_fit_print_volume(const DynamicPrintConfig& config);
|
||||
#endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT
|
||||
|
||||
@@ -220,11 +220,39 @@ TEST_CASE("An item larger than the bed cannot be placed", "[Arrange]")
|
||||
ArrangePolygons items;
|
||||
items.emplace_back(make_square(scaled(20.)));
|
||||
items.emplace_back(make_square(scaled(400.))); // far bigger than the bed
|
||||
// An item that cannot be placed keeps the position it came in with, so the
|
||||
// caller can leave it where the user had it.
|
||||
items[1].translation = {scaled(10.), scaled(20.)};
|
||||
|
||||
arrange(items, bed(200, 200), quiet_params(scaled(1.)));
|
||||
|
||||
REQUIRE(items[0].bed_idx == 0);
|
||||
REQUIRE(items[1].bed_idx == UNARRANGED);
|
||||
REQUIRE(items[1].translation.x() == scaled(10.));
|
||||
REQUIRE(items[1].translation.y() == scaled(20.));
|
||||
}
|
||||
|
||||
TEST_CASE("An item filling the printable area is arranged", "[Arrange]")
|
||||
{
|
||||
// The arrange bed keeps a margin from the printable area and the item is widened by
|
||||
// its brim. Neither may keep an item that fits the printable area from being placed.
|
||||
DynamicPrintConfig cfg = bed_config(); // 200 x 200
|
||||
ArrangeParams p = quiet_params();
|
||||
|
||||
ArrangePolygons items = squares(1, 197.);
|
||||
items.front().brim_width = 1.;
|
||||
update_arrange_params(p, &cfg, items);
|
||||
update_selected_items_inflation(items, &cfg, p);
|
||||
arrange(items, get_shrink_bedpts(&cfg, p), p);
|
||||
REQUIRE(items.front().bed_idx == 0);
|
||||
|
||||
// Larger than the printable area is still refused.
|
||||
ArrangeParams p_large = quiet_params();
|
||||
ArrangePolygons large = squares(1, 201.);
|
||||
update_arrange_params(p_large, &cfg, large);
|
||||
update_selected_items_inflation(large, &cfg, p_large);
|
||||
arrange(large, get_shrink_bedpts(&cfg, p_large), p_large);
|
||||
REQUIRE(large.front().bed_idx == UNARRANGED);
|
||||
}
|
||||
|
||||
TEST_CASE("Items overflowing one bed spill onto virtual beds", "[Arrange]")
|
||||
|
||||
@@ -187,6 +187,23 @@ TEST_CASE("A part rests on its largest hull face even when parallel inner faces
|
||||
check_on_bed(plate);
|
||||
}
|
||||
|
||||
TEST_CASE("A box laid on its top face is turned over about X", "[LayOnFace]")
|
||||
{
|
||||
// A face pointing straight up leaves the half turn's axis open to every horizontal
|
||||
// direction. It must be the fixed X flip both times, keep the footprint and not follow
|
||||
// rounding noise to an arbitrary angle.
|
||||
Model model;
|
||||
ModelObject &box = add_box_object(model, { 40, 20, 10 });
|
||||
lay_on_face(box, 0, { 0, 0, 1 });
|
||||
|
||||
check_size(box, { 40, 20, 10 });
|
||||
check_on_bed(box);
|
||||
const Matrix3d rotation = box.instances.front()->get_matrix_no_offset().linear();
|
||||
CHECK_THAT(rotation(0, 0), WithinAbs(1., 1e-9));
|
||||
CHECK_THAT(rotation(1, 1), WithinAbs(-1., 1e-9));
|
||||
CHECK_THAT(rotation(2, 2), WithinAbs(-1., 1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("Faces are selected in object coordinates whatever the instance rotation", "[LayOnFace]")
|
||||
{
|
||||
Model model;
|
||||
|
||||
Reference in new issue
Block a user