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
+3
-25
@@ -47,7 +47,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
||||
u )
|
||||
export BUILD_TARGET="universal"
|
||||
;;
|
||||
h ) echo "Usage: ./build_release_macos.sh [-d] [-- CMAKE_ARGS...]"
|
||||
h ) echo "Usage: ./build_release_macos.sh [-d]"
|
||||
echo " -d: Build deps only"
|
||||
echo " -a: Set ARCHITECTURE (arm64 or x86_64 or universal)"
|
||||
echo " -s: Build slicer only"
|
||||
@@ -60,33 +60,13 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
||||
echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew"
|
||||
echo " -j: Set the number of parallel build jobs (CMAKE_BUILD_PARALLEL_LEVEL)"
|
||||
echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)"
|
||||
echo " -- CMAKE_ARGS...: pass everything after '--' to the cmake configure step of the deps (-d) and slicer (-s) builds,"
|
||||
echo " e.g. -DDESTDIR=<dir> for the deps, -DCMAKE_PREFIX_PATH=<dir>/usr/local -DSLIC3R_PCH=OFF for the slicer, --fresh."
|
||||
echo " They come last on the cmake command line. Use absolute paths: cmake runs in the build directory."
|
||||
exit 0
|
||||
;;
|
||||
: )
|
||||
echo "Option -${OPTARG} requires an argument, aborting."
|
||||
exit 1
|
||||
;;
|
||||
* )
|
||||
echo "Unknown option -${OPTARG}, aborting. CMake arguments must follow '--'."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# getopts stops at '--'; whatever follows is handed to cmake unchanged.
|
||||
if [[ ${OPTIND} -le $# ]] ; then
|
||||
separator_index=$((OPTIND-1))
|
||||
if [[ "${!separator_index}" != "--" ]] ; then
|
||||
echo "Unexpected argument '${!OPTIND}': cmake arguments must follow '--', aborting."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
shift $((OPTIND-1))
|
||||
EXTRA_CMAKE_ARGS=("$@")
|
||||
|
||||
# Set defaults
|
||||
|
||||
if [ -z "$ARCH" ]; then
|
||||
@@ -231,8 +211,7 @@ function build_deps() {
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||
${CMAKE_POLICY_COMPAT} \
|
||||
"${EXTRA_CMAKE_ARGS[@]}"
|
||||
${CMAKE_POLICY_COMPAT}
|
||||
fi
|
||||
cmake --build . --config "$BUILD_CONFIG" --target deps
|
||||
)
|
||||
@@ -335,8 +314,7 @@ function build_slicer() {
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||
${CMAKE_POLICY_COMPAT} \
|
||||
"${EXTRA_CMAKE_ARGS[@]}"
|
||||
${CMAKE_POLICY_COMPAT}
|
||||
fi
|
||||
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -528,6 +528,81 @@ AuditDecision PluginAuditManager::check_open(const std::string& path_str, const
|
||||
return check_path_access(boost::filesystem::path(path_str), is_write);
|
||||
}
|
||||
|
||||
bool PluginAuditManager::request_filesystem_read_permissions(const std::string& plugin_key,
|
||||
const std::vector<std::string>& paths)
|
||||
{
|
||||
if (plugin_key.empty() || paths.empty())
|
||||
return true;
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
|
||||
return false;
|
||||
|
||||
PluginInstallState state;
|
||||
read_install_state(boost::filesystem::path(descriptor.plugin_root), state);
|
||||
|
||||
std::vector<std::string> missing;
|
||||
for (const std::string& path : paths) {
|
||||
if (std::find(state.permissions.fs_read.begin(), state.permissions.fs_read.end(), path) == state.permissions.fs_read.end())
|
||||
missing.push_back(path);
|
||||
}
|
||||
if (missing.empty())
|
||||
return true;
|
||||
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
|
||||
return false;
|
||||
|
||||
auto show_dialog = [&descriptor, &missing]() {
|
||||
wxString requested_paths;
|
||||
for (const std::string& path : missing)
|
||||
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
|
||||
|
||||
wxMessageDialog dialog(
|
||||
nullptr,
|
||||
wxString::Format(_L("Plugin \"%s\" requests filesystem read access to:\n%s"),
|
||||
wxString::FromUTF8(descriptor.name.c_str()), requested_paths),
|
||||
_L("Plugin permissions"),
|
||||
wxYES_NO | wxICON_WARNING);
|
||||
return dialog.ShowModal() == wxID_YES;
|
||||
};
|
||||
|
||||
bool granted = false;
|
||||
if (wxIsMainThread()) {
|
||||
granted = show_dialog();
|
||||
} else {
|
||||
auto result = std::make_shared<std::promise<bool>>();
|
||||
auto future = result->get_future();
|
||||
GUI::wxGetApp().CallAfter([result, descriptor_name = descriptor.name, missing]() {
|
||||
wxString requested_paths;
|
||||
for (const std::string& path : missing)
|
||||
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
|
||||
|
||||
wxMessageDialog dialog(
|
||||
nullptr,
|
||||
wxString::Format(_L("Plugin \"%s\" requests filesystem read access to:\n%s"),
|
||||
wxString::FromUTF8(descriptor_name.c_str()), requested_paths),
|
||||
_L("Plugin permissions"),
|
||||
wxYES_NO | wxICON_WARNING);
|
||||
result->set_value(dialog.ShowModal() == wxID_YES);
|
||||
});
|
||||
granted = future.get();
|
||||
}
|
||||
|
||||
if (!granted)
|
||||
return false;
|
||||
|
||||
if (state.plugin_name.empty()) {
|
||||
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
|
||||
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
|
||||
state.plugin_name = descriptor.name;
|
||||
state.cloud_uuid = descriptor.cloud_uuid();
|
||||
state.enabled = true;
|
||||
}
|
||||
|
||||
state.permissions.fs_read.insert(state.permissions.fs_read.end(), missing.begin(), missing.end());
|
||||
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
|
||||
}
|
||||
|
||||
void PluginAuditManager::report_violation(const AuditViolation& violation)
|
||||
{
|
||||
m_last_violation = violation;
|
||||
@@ -763,7 +838,7 @@ std::vector<std::string> audit_targets(const std::string& event_name, AuditEvent
|
||||
// Thread creation exposes no user-supplied target. Use a fixed sentinel so the grant
|
||||
// persists per plugin: the permission list matches targets by exact string, and the
|
||||
// started function's repr embeds an address that changes every run.
|
||||
targets.emplace_back(kThreadPermissionTarget);
|
||||
targets.emplace_back("thread");
|
||||
return targets;
|
||||
default:
|
||||
break;
|
||||
@@ -819,20 +894,6 @@ bool has_permission(const std::vector<std::string>& granted, const std::string&
|
||||
return std::find(granted.begin(), granted.end(), target) != granted.end();
|
||||
}
|
||||
|
||||
// A sandbox plugin may not have a sidecar yet. Seed the small amount of install metadata the
|
||||
// writer needs so clicking Yes still creates the JSON file.
|
||||
void seed_install_metadata(const PluginDescriptor& descriptor, PluginInstallState& state)
|
||||
{
|
||||
if (!state.plugin_name.empty())
|
||||
return;
|
||||
|
||||
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
|
||||
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
|
||||
state.plugin_name = descriptor.name;
|
||||
state.cloud_uuid = descriptor.cloud_uuid();
|
||||
state.enabled = true;
|
||||
}
|
||||
|
||||
bool persist_permission(const std::string& plugin_key,
|
||||
PluginInstallState& state,
|
||||
std::vector<std::string>& permission_list,
|
||||
@@ -842,7 +903,15 @@ bool persist_permission(const std::string& plugin_key,
|
||||
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
|
||||
return false;
|
||||
|
||||
seed_install_metadata(descriptor, state);
|
||||
// A sandbox plugin may not have a sidecar yet. Seed the small amount of install metadata
|
||||
// needed by the writer so clicking Yes still creates the JSON file.
|
||||
if (state.plugin_name.empty()) {
|
||||
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
|
||||
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
|
||||
state.plugin_name = descriptor.name;
|
||||
state.cloud_uuid = descriptor.cloud_uuid();
|
||||
state.enabled = true;
|
||||
}
|
||||
|
||||
if (!has_permission(permission_list, target))
|
||||
permission_list.push_back(target);
|
||||
@@ -928,13 +997,6 @@ bool prompt_for_targets(AuditEventCategory category, const std::string& plugin_n
|
||||
return dialog.ShowModal() == wxID_YES;
|
||||
}
|
||||
|
||||
// Shows a prompt whose body may cover several permission buckets. Must run on the GUI thread.
|
||||
bool prompt_for_permission_message(const wxString& message)
|
||||
{
|
||||
wxMessageDialog dialog(nullptr, message, _L("Plugin permissions"), wxYES_NO | wxICON_WARNING);
|
||||
return dialog.ShowModal() == wxID_YES;
|
||||
}
|
||||
|
||||
// Records a grant in the plugin's sidecar so it is not asked again. Reads the install state
|
||||
// freshly because the async prompt outlives the caller's stack copy of it.
|
||||
void persist_grant(const std::string& plugin_key, AuditEventCategory category, bool by_event, const std::vector<std::string>& keys)
|
||||
@@ -961,7 +1023,7 @@ static PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing;
|
||||
// SlicingPipeline hook runs on the slicing worker thread (see PluginHooks.cpp), and
|
||||
// BackgroundSlicingProcess::stop()/stop_internal() park the UI thread until that worker stops.
|
||||
// Blocking the worker on a marshaled modal -- which is safe for the plugin-load worker that
|
||||
// request_permissions runs on -- would therefore deadlock the application (the
|
||||
// request_filesystem_read_permissions runs on -- would therefore deadlock the application (the
|
||||
// invariant PluginHostUi.cpp documents for slicing-hook UI calls). Off the main thread the prompt
|
||||
// is therefore posted asynchronously and the current event denied (fail closed, like an unanswered
|
||||
// prompt); the grant is persisted once the user accepts, so a later attempt succeeds without
|
||||
@@ -1085,105 +1147,6 @@ int audit_plugin_event(PluginAuditManager& mgr, const std::string& event_name, A
|
||||
|
||||
} // namespace PluginAuditDetail
|
||||
|
||||
bool PluginAuditManager::request_permissions(const std::string& plugin_key, const PluginPermissions& requested)
|
||||
{
|
||||
if (plugin_key.empty())
|
||||
return true;
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
|
||||
return false;
|
||||
|
||||
PluginInstallState state;
|
||||
read_install_state(boost::filesystem::path(descriptor.plugin_root), state);
|
||||
|
||||
// Per bucket, keep only the targets that no earlier grant already covers. Dropping these
|
||||
// before prompting keeps a repeated declaration from asking again for what is on disk.
|
||||
struct MissingBucket {
|
||||
AuditEventCategory category;
|
||||
std::vector<std::string> targets;
|
||||
};
|
||||
std::vector<MissingBucket> missing_buckets;
|
||||
|
||||
const auto collect_missing = [&missing_buckets](AuditEventCategory category,
|
||||
const std::vector<std::string>& requested_targets,
|
||||
const std::vector<std::string>& granted_targets) {
|
||||
MissingBucket bucket{category, {}};
|
||||
for (const std::string& target : requested_targets) {
|
||||
if (target.empty())
|
||||
continue;
|
||||
if (std::find(granted_targets.begin(), granted_targets.end(), target) == granted_targets.end() &&
|
||||
std::find(bucket.targets.begin(), bucket.targets.end(), target) == bucket.targets.end())
|
||||
bucket.targets.push_back(target);
|
||||
}
|
||||
if (!bucket.targets.empty())
|
||||
missing_buckets.push_back(std::move(bucket));
|
||||
};
|
||||
|
||||
collect_missing(AuditEventCategory::FsRead, requested.fs_read, state.permissions.fs_read);
|
||||
collect_missing(AuditEventCategory::FsReadWrite, requested.fs_readwrite, state.permissions.fs_readwrite);
|
||||
collect_missing(AuditEventCategory::Http, requested.network_http, state.permissions.network_http);
|
||||
collect_missing(AuditEventCategory::Socket, requested.network_socket, state.permissions.network_socket);
|
||||
collect_missing(AuditEventCategory::ProcessCreate, requested.process, state.permissions.process);
|
||||
collect_missing(AuditEventCategory::Threading, requested.threading, state.permissions.threading);
|
||||
|
||||
if (missing_buckets.empty())
|
||||
return true;
|
||||
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
|
||||
return false;
|
||||
|
||||
const auto build_message = [](const std::string& plugin_name, const std::vector<MissingBucket>& buckets) {
|
||||
wxString message;
|
||||
for (const MissingBucket& bucket : buckets) {
|
||||
wxString targets;
|
||||
for (const std::string& target : bucket.targets)
|
||||
targets += wxString::FromUTF8(target.c_str()) + "\n";
|
||||
|
||||
if (!message.IsEmpty())
|
||||
message += "\n\n";
|
||||
message += PluginAuditDetail::audit_message(bucket.category, wxString::FromUTF8(plugin_name.c_str()),
|
||||
wxEmptyString, targets, bucket.targets.size());
|
||||
}
|
||||
return message;
|
||||
};
|
||||
|
||||
bool granted = false;
|
||||
if (wxIsMainThread()) {
|
||||
granted = PluginAuditDetail::prompt_for_permission_message(build_message(descriptor.name, missing_buckets));
|
||||
} else {
|
||||
auto result = std::make_shared<std::promise<bool>>();
|
||||
auto future = result->get_future();
|
||||
GUI::wxGetApp().CallAfter([result, plugin_name = descriptor.name, missing_buckets, build_message]() {
|
||||
result->set_value(PluginAuditDetail::prompt_for_permission_message(build_message(plugin_name, missing_buckets)));
|
||||
});
|
||||
granted = future.get();
|
||||
}
|
||||
|
||||
if (!granted)
|
||||
return false;
|
||||
|
||||
// The dialog can stay open while another grant is persisted; re-read before saving so this
|
||||
// request adds to the current state instead of replacing it.
|
||||
PluginInstallState latest_state;
|
||||
if (read_install_state(boost::filesystem::path(descriptor.plugin_root), latest_state))
|
||||
state = std::move(latest_state);
|
||||
|
||||
PluginAuditDetail::seed_install_metadata(descriptor, state);
|
||||
|
||||
for (const MissingBucket& bucket : missing_buckets) {
|
||||
std::vector<std::string>* permission_list =
|
||||
PluginAuditDetail::permission_list_for(bucket.category, /*by_event=*/false, state.permissions);
|
||||
if (!permission_list)
|
||||
continue;
|
||||
for (const std::string& target : bucket.targets)
|
||||
if (std::find(permission_list->begin(), permission_list->end(), target) == permission_list->end())
|
||||
permission_list->push_back(target);
|
||||
}
|
||||
|
||||
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
|
||||
}
|
||||
|
||||
void PluginAuditManager::record_root_frame()
|
||||
{
|
||||
PyObject* frame = reinterpret_cast<PyObject*>(PyEval_GetFrame()); // borrowed reference
|
||||
|
||||
@@ -13,12 +13,6 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct PluginPermissions;
|
||||
|
||||
// Thread creation exposes no user-supplied target, so both the runtime audit event and a
|
||||
// request_permissions(threading=True) declaration use this fixed target.
|
||||
inline constexpr const char* kThreadPermissionTarget = "thread";
|
||||
|
||||
struct AuditDecision {
|
||||
bool allowed = true;
|
||||
std::string reason;
|
||||
@@ -136,12 +130,12 @@ public:
|
||||
AuditDecision check_path_access(const boost::filesystem::path& candidate, bool is_write);
|
||||
AuditDecision check_open(const std::string& path, const std::string& mode);
|
||||
|
||||
// Ask the user to grant the requested permissions. The request may originate on a plugin
|
||||
// load worker, so the implementation marshals the modal dialog to the wx main thread.
|
||||
// Targets already persisted for the plugin are not shown or asked for again. Returns true
|
||||
// only when every missing target was granted and persisted; denial aborts the plugin load
|
||||
// without adding a permission.
|
||||
bool request_permissions(const std::string& plugin_key, const PluginPermissions& requested);
|
||||
// Ask the user to grant the requested filesystem-read paths. The request may originate on a
|
||||
// plugin load worker, so the implementation marshals the modal dialog to the wx main thread.
|
||||
// Returns true only when every missing path was granted and persisted; denial aborts the plugin
|
||||
// load without adding a permission.
|
||||
bool request_filesystem_read_permissions(const std::string& plugin_key,
|
||||
const std::vector<std::string>& paths);
|
||||
|
||||
void report_violation(const AuditViolation& violation);
|
||||
bool audit_denial_pending() const;
|
||||
|
||||
@@ -49,7 +49,7 @@ thread_local std::string g_active_plugin_key;
|
||||
std::mutex g_registry_mutex;
|
||||
std::unordered_map<std::string, std::vector<py::object>> g_pending_capabilities;
|
||||
std::unordered_map<std::string, py::object> g_pending_package;
|
||||
std::unordered_map<std::string, PluginPermissions> g_pending_permissions;
|
||||
std::unordered_map<std::string, std::vector<std::string>> g_pending_fs_read_permissions;
|
||||
struct PluginInstanceHandle
|
||||
{
|
||||
// The C++ plugin interface points into a Python object. Keep both alive through one
|
||||
@@ -88,7 +88,7 @@ void discard_pending_capture_without_python(const std::string& plugin_key)
|
||||
g_pending_package.erase(package);
|
||||
}
|
||||
|
||||
g_pending_permissions.erase(plugin_key);
|
||||
g_pending_fs_read_permissions.erase(plugin_key);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -113,7 +113,7 @@ void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key)
|
||||
// for this same entry path.
|
||||
g_pending_capabilities.erase(plugin_key);
|
||||
g_pending_package.erase(plugin_key);
|
||||
g_pending_permissions.erase(plugin_key);
|
||||
g_pending_fs_read_permissions.erase(plugin_key);
|
||||
}
|
||||
// From now until finalize/cancel, @orca.plugin and register_capability() calls made by
|
||||
// Python code on this thread are attributed to this plugin.
|
||||
@@ -144,7 +144,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
g_pending_capabilities.erase(capture_key);
|
||||
g_pending_package.erase(capture_key);
|
||||
g_pending_permissions.erase(capture_key);
|
||||
g_pending_fs_read_permissions.erase(capture_key);
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -177,27 +177,23 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
|
||||
// Permission declarations are collected while register_capabilities() runs so the
|
||||
// plugin can describe its needs without touching wx from the Python load worker. Ask
|
||||
// only after registration returns, before capability instances are materialized.
|
||||
PluginPermissions requested_permissions;
|
||||
std::vector<std::string> fs_read_permissions;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
auto it = g_pending_permissions.find(capture_key);
|
||||
if (it != g_pending_permissions.end()) {
|
||||
requested_permissions = std::move(it->second);
|
||||
g_pending_permissions.erase(it);
|
||||
auto it = g_pending_fs_read_permissions.find(capture_key);
|
||||
if (it != g_pending_fs_read_permissions.end()) {
|
||||
fs_read_permissions = std::move(it->second);
|
||||
g_pending_fs_read_permissions.erase(it);
|
||||
}
|
||||
}
|
||||
const bool has_permission_request =
|
||||
!requested_permissions.fs_read.empty() || !requested_permissions.fs_readwrite.empty() ||
|
||||
!requested_permissions.network_http.empty() || !requested_permissions.network_socket.empty() ||
|
||||
!requested_permissions.process.empty() || !requested_permissions.threading.empty();
|
||||
if (has_permission_request) {
|
||||
if (!fs_read_permissions.empty()) {
|
||||
bool granted = false;
|
||||
{
|
||||
py::gil_scoped_release release;
|
||||
granted = PluginAuditManager::instance().request_permissions(plugin_key, requested_permissions);
|
||||
granted = PluginAuditManager::instance().request_filesystem_read_permissions(plugin_key, fs_read_permissions);
|
||||
}
|
||||
if (!granted) {
|
||||
error = _u8L("Plugin permission request denied");
|
||||
error = _u8L("Plugin filesystem read permission request denied");
|
||||
BOOST_LOG_TRIVIAL(warning) << error << " for key " << plugin_key;
|
||||
discard_pending_for_key();
|
||||
clear_active_key();
|
||||
@@ -326,7 +322,7 @@ void PythonPluginBridge::cancel_plugin_capture(const std::string& plugin_key)
|
||||
// may already have registered under this key.
|
||||
g_pending_capabilities.erase(plugin_key);
|
||||
g_pending_package.erase(plugin_key);
|
||||
g_pending_permissions.erase(plugin_key);
|
||||
g_pending_fs_read_permissions.erase(plugin_key);
|
||||
}
|
||||
|
||||
if (g_active_plugin_key == plugin_key)
|
||||
@@ -353,7 +349,7 @@ void PythonPluginBridge::clear_pending_captures()
|
||||
(void) pkg.release();
|
||||
}
|
||||
g_pending_package.clear();
|
||||
g_pending_permissions.clear();
|
||||
g_pending_fs_read_permissions.clear();
|
||||
g_active_plugin_key.clear();
|
||||
return;
|
||||
}
|
||||
@@ -362,7 +358,7 @@ void PythonPluginBridge::clear_pending_captures()
|
||||
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)";
|
||||
g_pending_capabilities.clear();
|
||||
g_pending_package.clear();
|
||||
g_pending_permissions.clear();
|
||||
g_pending_fs_read_permissions.clear();
|
||||
g_active_plugin_key.clear();
|
||||
}
|
||||
|
||||
@@ -562,54 +558,24 @@ void bind_python_api(pybind11::module_& m)
|
||||
|
||||
m.def(
|
||||
"request_permissions",
|
||||
[](const std::vector<std::string>& fs_read,
|
||||
const std::vector<std::string>& fs_readwrite,
|
||||
const std::vector<std::string>& network_http,
|
||||
const std::vector<std::string>& network_socket,
|
||||
const std::vector<std::string>& process,
|
||||
bool threading) {
|
||||
[](const std::vector<std::string>& fs_read) {
|
||||
if (g_active_plugin_key.empty())
|
||||
throw py::value_error("request_permissions() called outside plugin discovery context");
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
PluginPermissions& requested = g_pending_permissions[g_active_plugin_key];
|
||||
|
||||
const auto add_targets = [](std::vector<std::string>& bucket, const char* name,
|
||||
const std::vector<std::string>& targets) {
|
||||
for (const std::string& target : targets) {
|
||||
if (target.empty())
|
||||
throw py::value_error(std::string("request_permissions(") + name +
|
||||
"=...) does not accept empty values");
|
||||
if (std::find(bucket.begin(), bucket.end(), target) == bucket.end())
|
||||
bucket.push_back(target);
|
||||
}
|
||||
};
|
||||
|
||||
add_targets(requested.fs_read, "fs_read", fs_read);
|
||||
add_targets(requested.fs_readwrite, "fs_readwrite", fs_readwrite);
|
||||
add_targets(requested.network_http, "network_http", network_http);
|
||||
add_targets(requested.network_socket, "network_socket", network_socket);
|
||||
add_targets(requested.process, "process", process);
|
||||
|
||||
if (threading && std::find(requested.threading.begin(), requested.threading.end(),
|
||||
kThreadPermissionTarget) == requested.threading.end())
|
||||
requested.threading.emplace_back(kThreadPermissionTarget);
|
||||
auto& requested = g_pending_fs_read_permissions[g_active_plugin_key];
|
||||
for (const std::string& path : fs_read) {
|
||||
if (path.empty())
|
||||
throw py::value_error("request_permissions(fs_read=...) does not accept empty paths");
|
||||
if (std::find(requested.begin(), requested.end(), path) == requested.end())
|
||||
requested.push_back(path);
|
||||
}
|
||||
},
|
||||
py::arg("fs_read") = std::vector<std::string>{},
|
||||
py::arg("fs_readwrite") = std::vector<std::string>{},
|
||||
py::arg("network_http") = std::vector<std::string>{},
|
||||
py::arg("network_socket") = std::vector<std::string>{},
|
||||
py::arg("process") = std::vector<std::string>{},
|
||||
py::arg("threading") = false,
|
||||
R"pbdoc(Request permissions while OrcaSlicer loads your module.
|
||||
R"pbdoc(Request filesystem read permissions while OrcaSlicer loads your module.
|
||||
|
||||
Each bucket takes the target strings the plugin expects the corresponding audit event to
|
||||
report: filesystem paths for fs_read and fs_readwrite, URLs for network_http, socket addresses
|
||||
for network_socket, and commands for process. threading=True requests permission to start
|
||||
threads. Targets already granted to the plugin are not asked for again.
|
||||
|
||||
The host presents one request to the user after register_capabilities() returns and before any
|
||||
capability instance is created. Denying the request aborts the plugin load.)pbdoc");
|
||||
The host presents the request to the user after register_capabilities() returns. Denying the
|
||||
request aborts the plugin load.)pbdoc");
|
||||
|
||||
m.def("plugin", [](py::object cls) {
|
||||
if (g_active_plugin_key.empty())
|
||||
|
||||
@@ -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