mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-20 14:08:20 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37dcc65752 | ||
|
|
cdb7eda54f | ||
|
|
e7d6cc32a7 | ||
|
|
069bd402d9 |
No files matched your search
+25
-3
@@ -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]"
|
||||
h ) echo "Usage: ./build_release_macos.sh [-d] [-- CMAKE_ARGS...]"
|
||||
echo " -d: Build deps only"
|
||||
echo " -a: Set ARCHITECTURE (arm64 or x86_64 or universal)"
|
||||
echo " -s: Build slicer only"
|
||||
@@ -60,13 +60,33 @@ 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
|
||||
@@ -211,7 +231,8 @@ function build_deps() {
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||
${CMAKE_POLICY_COMPAT}
|
||||
${CMAKE_POLICY_COMPAT} \
|
||||
"${EXTRA_CMAKE_ARGS[@]}"
|
||||
fi
|
||||
cmake --build . --config "$BUILD_CONFIG" --target deps
|
||||
)
|
||||
@@ -314,7 +335,8 @@ function build_slicer() {
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||
${CMAKE_POLICY_COMPAT}
|
||||
${CMAKE_POLICY_COMPAT} \
|
||||
"${EXTRA_CMAKE_ARGS[@]}"
|
||||
fi
|
||||
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
|
||||
)
|
||||
|
||||
Vendored
+6
-2
@@ -59,9 +59,13 @@ else ()
|
||||
set(_cross_compile_arg --host=${TOOLCHAIN_PREFIX})
|
||||
endif ()
|
||||
|
||||
# 6.3.0 is the first release whose arm64 assembly avoids x18, which macOS
|
||||
# reserves and zeroes on every return to user space; 6.2.1 crashes on Apple
|
||||
# silicon whenever CGAL falls back to exact arithmetic.
|
||||
ExternalProject_Add(dep_GMP
|
||||
URL https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.2.1/gmp-6.2.1.tar.bz2
|
||||
URL_HASH SHA256=eae9326beb4158c386e39a356818031bd28f3124cf915f8c5b1dc4c7a36b4d7c
|
||||
URL https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz
|
||||
https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.3.0/gmp-6.3.0.tar.xz
|
||||
URL_HASH SHA256=a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/GMP
|
||||
PATCH_COMMAND git apply ${GMP_DIRECTORY_FLAG} --verbose ${CMAKE_CURRENT_LIST_DIR}/0001-GMP_GCC15.patch
|
||||
BUILD_IN_SOURCE ON
|
||||
|
||||
@@ -226,10 +226,10 @@ modules:
|
||||
sha256: f353e7b99bd0cbfc27ac9082de51acf32a8bc0b3e21ff9661ecca6f205ec1d81
|
||||
dest: external-packages/OpenVDB
|
||||
|
||||
# GMP 6.2.1
|
||||
# GMP 6.3.0
|
||||
- type: file
|
||||
url: https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.2.1/gmp-6.2.1.tar.bz2
|
||||
sha256: eae9326beb4158c386e39a356818031bd28f3124cf915f8c5b1dc4c7a36b4d7c
|
||||
url: https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz
|
||||
sha256: a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898
|
||||
dest: external-packages/GMP
|
||||
|
||||
# MPFR 4.2.2
|
||||
|
||||
@@ -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.
|
||||
// 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();
|
||||
Points bedpts = get_shrink_bedpts(print_cfg, params);
|
||||
BoundingBox bedbb = Polygon(bedpts).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,30 +144,14 @@ 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);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
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,29 +213,13 @@ 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(), which the gizmo uses.
|
||||
const Transform3d rotation = lay_on_face_rotation(normal, trafo.get_matrix_no_offset());
|
||||
// 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()));
|
||||
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,10 +46,4 @@ 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,17 +771,6 @@ 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;
|
||||
|
||||
@@ -881,13 +870,12 @@ 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. Plates are never recycled here: arranging
|
||||
// must not remove a plate the user created, even when the arrange left it empty.
|
||||
//BBS: reload all objects due to arrange
|
||||
if (only_on_partplate) {
|
||||
plate_list.rebuild_plates_after_arrangement(false, true, current_plate_index);
|
||||
plate_list.rebuild_plates_after_arrangement(!only_on_partplate, true, current_plate_index);
|
||||
}
|
||||
else {
|
||||
plate_list.rebuild_plates_after_arrangement(false, true);
|
||||
plate_list.rebuild_plates_after_arrangement(!only_on_partplate, true);
|
||||
}
|
||||
|
||||
// unlock the plates we just locked
|
||||
|
||||
@@ -248,6 +248,7 @@ 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();
|
||||
@@ -259,8 +260,6 @@ 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,14 +7048,19 @@ 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,17 +11034,7 @@ void Plater::priv::split_volume()
|
||||
void Plater::priv::scale_selection_to_fit_print_volume()
|
||||
{
|
||||
#if ENABLE_ENHANCED_PRINT_VOLUME_FIT
|
||||
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);
|
||||
this->view3D->get_canvas3d()->get_selection().scale_to_fit_print_volume(this->bed.build_volume());
|
||||
#else
|
||||
this->view3D->get_canvas3d()->get_selection().scale_to_fit_print_volume(*config);
|
||||
#endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
#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>
|
||||
@@ -1473,9 +1472,11 @@ void Selection::flattening_rotate(const Vec3d& normal)
|
||||
|
||||
for (unsigned int i : m_list) {
|
||||
GLVolume& v = *(*m_volumes)[i];
|
||||
// Additional rotation to align the face normal with the down vector in the world coordinate space.
|
||||
// Normal transformed from the object coordinate space to the world coordinate space.
|
||||
const Geometry::Transformation& old_inst_trafo = v.get_instance_transformation();
|
||||
const Transform3d rotation_matrix = lay_on_face_rotation(normal, old_inst_trafo.get_matrix_no_offset());
|
||||
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()));
|
||||
v.set_instance_transformation(old_inst_trafo.get_offset_matrix() * rotation_matrix * old_inst_trafo.get_matrix_no_offset());
|
||||
}
|
||||
|
||||
@@ -1495,7 +1496,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, const Vec2d& bed_center, const BoundingBoxf3& print_volume)
|
||||
void Selection::scale_to_fit_print_volume(const BuildVolume& volume)
|
||||
{
|
||||
auto fit = [this](double s, Vec3d offset) {
|
||||
if (s <= 0.0 || s == 1.0)
|
||||
@@ -1525,7 +1526,8 @@ void Selection::scale_to_fit_print_volume(const BuildVolume& volume, const Vec2d
|
||||
//wxGetApp().obj_manipul()->set_dirty();
|
||||
};
|
||||
|
||||
auto fit_rectangle = [this, fit, &print_volume]() {
|
||||
auto fit_rectangle = [this, fit](const BuildVolume& volume) {
|
||||
const BoundingBoxf3 print_volume = volume.bounding_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
|
||||
@@ -1539,7 +1541,7 @@ void Selection::scale_to_fit_print_volume(const BuildVolume& volume, const Vec2d
|
||||
fit(std::min(sx, std::min(sy, sz)), print_volume.center() - get_bounding_box().center());
|
||||
};
|
||||
|
||||
auto fit_circle = [this, fit, &bed_center](const BuildVolume& volume) {
|
||||
auto fit_circle = [this, fit](const BuildVolume& volume) {
|
||||
const Geometry::Circled& print_circle = volume.circle();
|
||||
double print_circle_radius = unscale<double>(print_circle.radius);
|
||||
|
||||
@@ -1570,9 +1572,7 @@ void Selection::scale_to_fit_print_volume(const BuildVolume& volume, const Vec2d
|
||||
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);
|
||||
// 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;
|
||||
const Vec3d print_center = { unscale<double>(print_circle.center.x()), unscale<double>(print_circle.center.y()), 0.5 * volume.printable_height() };
|
||||
fit(s, print_center - (sel_center + offset));
|
||||
};
|
||||
|
||||
@@ -1581,7 +1581,7 @@ void Selection::scale_to_fit_print_volume(const BuildVolume& volume, const Vec2d
|
||||
|
||||
switch (volume.type())
|
||||
{
|
||||
case BuildVolume_Type::Rectangle: { fit_rectangle(); break; }
|
||||
case BuildVolume_Type::Rectangle: { fit_rectangle(volume); break; }
|
||||
case BuildVolume_Type::Circle: { fit_circle(volume); break; }
|
||||
default: { break; }
|
||||
}
|
||||
|
||||
@@ -354,10 +354,7 @@ public:
|
||||
void flattening_rotate(const Vec3d& normal);
|
||||
void scale(const Vec3d& scale, TransformationType transformation_type);
|
||||
#if ENABLE_ENHANCED_PRINT_VOLUME_FIT
|
||||
// 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);
|
||||
void scale_to_fit_print_volume(const BuildVolume& volume);
|
||||
#else
|
||||
void scale_to_fit_print_volume(const DynamicPrintConfig& config);
|
||||
#endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT
|
||||
|
||||
@@ -353,16 +353,18 @@ void PluginAuditManager::add_denied_filename(const std::string& filename)
|
||||
std::vector<std::string> PluginAuditManager::default_denied_filenames()
|
||||
{
|
||||
// AppConfig::config_path() picks .conf vs .ini on the USE_JSON_CONFIG ifdef and the app key
|
||||
// on its mode, and is a non-static member we cannot call without an instance. Denying all
|
||||
// on its mode, and is a non-static member we cannot call without an instance. Denying all
|
||||
// four names is cheaper and more robust than replicating that; the two unused names cost one
|
||||
// string comparison each. Single-sourced here so install_hook() and the tests seed from the
|
||||
// same list and cannot drift apart.
|
||||
// string comparison each. Keep the plugin install-state sidecar here too: it controls which
|
||||
// packages load on startup and must remain host-managed. Single-sourced so install_hook() and
|
||||
// the tests seed from the same list and cannot drift apart.
|
||||
return {
|
||||
SLIC3R_APP_KEY ".conf",
|
||||
GCODEVIEWER_APP_KEY ".conf",
|
||||
SLIC3R_APP_KEY ".ini",
|
||||
GCODEVIEWER_APP_KEY ".ini",
|
||||
secret_constants::USER_SECRET_FILENAME,
|
||||
".install_state.json",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -498,6 +500,12 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
|
||||
candidate = absolute_candidate;
|
||||
}
|
||||
|
||||
// Importing Python modules reads their source and may inspect package directories. These
|
||||
// reads are normal interpreter work, not plugin data access; keep the exemption read-only
|
||||
// and exclude third-party packages from the stdlib roots.
|
||||
if (!is_write && is_stdlib_path(candidate))
|
||||
return {true, ""};
|
||||
|
||||
// A root that doesn't allow writes only matches a read-shaped request; a write/create/
|
||||
// delete-shaped one falls through to "outside allowed root" for that root even though the
|
||||
// path is physically inside it.
|
||||
@@ -1015,6 +1023,22 @@ void persist_grant(const std::string& plugin_key, AuditEventCategory category, b
|
||||
|
||||
static PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing;
|
||||
|
||||
static const char* audit_category_name(AuditEventCategory category)
|
||||
{
|
||||
switch (category) {
|
||||
case AuditEventCategory::FsRead: return "FsRead";
|
||||
case AuditEventCategory::FsReadWrite: return "FsReadWrite";
|
||||
case AuditEventCategory::FsCreate: return "FsCreate";
|
||||
case AuditEventCategory::FsDelete: return "FsDelete";
|
||||
case AuditEventCategory::Http: return "Http";
|
||||
case AuditEventCategory::Socket: return "Socket";
|
||||
case AuditEventCategory::ProcessCreate: return "ProcessCreate";
|
||||
case AuditEventCategory::ProcessReplace: return "ProcessReplace";
|
||||
case AuditEventCategory::Threading: return "Threading";
|
||||
default: return "None";
|
||||
}
|
||||
}
|
||||
|
||||
// Asks the user to grant an audited event, returning true when they approve an inline prompt.
|
||||
// `keys` are what a Yes saves: the unresolved targets, or the event-grant key when the event has
|
||||
// no target (by_event), which the prompt does not show as a target.
|
||||
@@ -1032,6 +1056,16 @@ bool request_permission(AuditEventCategory category, const std::string& plugin_k
|
||||
const std::string& event_name, bool by_event, const std::vector<std::string>& keys)
|
||||
{
|
||||
const std::vector<std::string> shown = by_event ? std::vector<std::string>{} : keys;
|
||||
std::string targets_text = by_event ? "<none>" : "";
|
||||
for (const auto& target : shown) {
|
||||
if (!targets_text.empty())
|
||||
targets_text += ", ";
|
||||
targets_text += target;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "[AUDIT PROMPT] plugin=" << plugin_key << " event=" << event_name
|
||||
<< " category=" << audit_category_name(category) << " targets=[" << targets_text << "]"
|
||||
<< (by_event && !keys.empty() ? " grant=" + keys.front() : std::string{});
|
||||
|
||||
if (s_permission_prompt_for_testing)
|
||||
return s_permission_prompt_for_testing(event_name, shown);
|
||||
@@ -1198,6 +1232,18 @@ bool PluginAuditManager::is_stdlib_file(const std::string& filename)
|
||||
return it->second;
|
||||
}
|
||||
|
||||
bool PluginAuditManager::is_stdlib_path(const boost::filesystem::path& path)
|
||||
{
|
||||
if (path.empty())
|
||||
return false;
|
||||
|
||||
const auto inside_any = [&path](const std::vector<boost::filesystem::path>& dirs) {
|
||||
return std::any_of(dirs.begin(), dirs.end(),
|
||||
[&path](const boost::filesystem::path& dir) { return is_inside_allowed_root(path, dir); });
|
||||
};
|
||||
return inside_any(PluginAuditDetail::s_stdlib_dirs) && !inside_any(PluginAuditDetail::s_site_package_dirs);
|
||||
}
|
||||
|
||||
void PluginAuditManager::release_frames(std::vector<PyObject*>& frames)
|
||||
{
|
||||
if (frames.empty())
|
||||
@@ -1266,7 +1312,7 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user
|
||||
|
||||
void PluginAuditManager::install_hook()
|
||||
{
|
||||
// Tells the stdlib's own frames apart, for the operation-root frames (see record_root_frame).
|
||||
// Identifies stdlib frames for operation roots and stdlib paths for read-only access.
|
||||
PluginAuditDetail::collect_stdlib_dirs();
|
||||
|
||||
// data_dir() is the primary globally-allowed root during plugin execution: read+write. It
|
||||
@@ -1283,8 +1329,9 @@ void PluginAuditManager::install_hook()
|
||||
// "cert" denied-path keyword seeded below.
|
||||
add_global_allowed_root(resources_dir(), /*allow_write=*/false);
|
||||
|
||||
// The user's app config and cloud credentials live directly inside data_dir(), so the
|
||||
// root just granted would otherwise expose them to any plugin. Deny them by name.
|
||||
// The user's app config and cloud credentials live directly inside data_dir(), and plugin
|
||||
// install state lives beneath it, so the root just granted would otherwise expose them to
|
||||
// plugins. Deny them by name.
|
||||
//
|
||||
// Seeded here rather than by each secret's owner because install_hook() runs during lazy
|
||||
// interpreter init and therefore provably precedes any plugin bytecode, whereas
|
||||
|
||||
@@ -173,6 +173,11 @@ private:
|
||||
|
||||
static int audit_hook(const char* event, PyObject* args, void* user_data);
|
||||
|
||||
// True for paths inside the interpreter's stdlib, excluding site-packages. Unlike
|
||||
// is_stdlib_file(), this query is not cached because filesystem paths may resolve through
|
||||
// symlinks that can change.
|
||||
static bool is_stdlib_path(const boost::filesystem::path& path);
|
||||
|
||||
// Drops the references in `frames`, keeping the caller's error and denial state intact.
|
||||
void release_frames(std::vector<PyObject*>& frames);
|
||||
|
||||
|
||||
@@ -220,39 +220,11 @@ 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,23 +187,6 @@ 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;
|
||||
|
||||
@@ -69,7 +69,7 @@ void seed_denied_keywords()
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit]")
|
||||
TEST_CASE("Plugin audit denies host-managed filenames anywhere", "[audit]")
|
||||
{
|
||||
seed_denied_names();
|
||||
const PluginAuditManager& mgr = PluginAuditManager::instance();
|
||||
@@ -81,6 +81,7 @@ TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit
|
||||
CHECK(mgr.is_denied_filename(fs::path(SLIC3R_APP_KEY ".ini")));
|
||||
CHECK(mgr.is_denied_filename(fs::path(GCODEVIEWER_APP_KEY ".ini")));
|
||||
CHECK(mgr.is_denied_filename(fs::path(secret_constants::USER_SECRET_FILENAME)));
|
||||
CHECK(mgr.is_denied_filename(fs::path(".install_state.json")));
|
||||
}
|
||||
|
||||
SECTION("companions holding the same secrets are denied by the prefix rule")
|
||||
@@ -136,6 +137,7 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
|
||||
|
||||
const fs::path conf = fs::path(data_dir()) / (SLIC3R_APP_KEY ".conf");
|
||||
const fs::path token = fs::path(data_dir()) / secret_constants::USER_SECRET_FILENAME;
|
||||
const fs::path install_state = fs::path(data_dir()) / "plugins" / "example" / ".install_state.json";
|
||||
|
||||
SECTION("a non-denied file inside the allowed root is writable (root really grants writes)")
|
||||
{
|
||||
@@ -176,6 +178,17 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
|
||||
CHECK_FALSE(decision.allowed);
|
||||
}
|
||||
|
||||
SECTION("plugin install state is blocked for reads and writes despite the allowed root")
|
||||
{
|
||||
AuditDecision read_decision = mgr.check_open(install_state.string(), "r");
|
||||
CHECK_FALSE(read_decision.allowed);
|
||||
CHECK(read_decision.reason == "denied filename");
|
||||
|
||||
AuditDecision write_decision = mgr.check_open(install_state.string(), "w");
|
||||
CHECK_FALSE(write_decision.allowed);
|
||||
CHECK(write_decision.reason == "denied filename");
|
||||
}
|
||||
|
||||
SECTION("a traversal path resolving to the config is blocked")
|
||||
{
|
||||
const fs::path traversal = fs::path(data_dir()) / "plugins" / ".." / (SLIC3R_APP_KEY ".conf");
|
||||
@@ -1119,3 +1132,37 @@ TEST_CASE("Stdlib code is recognized through a symlinked Python home", "[audit][
|
||||
CHECK_FALSE(PluginAuditManager::is_stdlib_file((harness.outside.path() / "plugin.py").string()));
|
||||
CHECK_FALSE(PluginAuditManager::is_stdlib_file("<string>"));
|
||||
}
|
||||
|
||||
TEST_CASE("A plugin may read the CPython stdlib without a prompt, but not site-packages or for writing", "[audit][Python]")
|
||||
{
|
||||
AuditHarness harness;
|
||||
if (!harness.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
harness.add_plugin(PLUGIN);
|
||||
py::gil_scoped_acquire gil;
|
||||
PythonSide python(harness);
|
||||
|
||||
const py::dict paths = py::module_::import("sysconfig").attr("get_paths")();
|
||||
const fs::path stdlib = py::str(paths["stdlib"]).cast<std::string>();
|
||||
const fs::path purelib = py::str(paths["purelib"]).cast<std::string>();
|
||||
// os.py ships with every CPython installation. A site-packages path is third-party code even
|
||||
// when the directory is empty: the prompt must come before the missing file is noticed.
|
||||
const fs::path stdlib_file = stdlib / "os.py";
|
||||
REQUIRE(fs::exists(stdlib_file));
|
||||
|
||||
python.globals["STDLIB_FILE"] = stdlib_file.string();
|
||||
python.globals["SITE_PACKAGE_FILE"] = (purelib / "regression_probe.py").string();
|
||||
python.run_as(PLUGIN, R"PY(
|
||||
read_outcome = attempt(lambda: open(STDLIB_FILE).read())
|
||||
write_outcome = attempt(lambda: open(STDLIB_FILE, "r+").close())
|
||||
site_packages_outcome = attempt(lambda: open(SITE_PACKAGE_FILE).close())
|
||||
)PY");
|
||||
|
||||
CHECK(python.str("read_outcome") == "ok");
|
||||
CHECK(python.str("write_outcome") == "denied");
|
||||
CHECK(python.str("site_packages_outcome") == "denied");
|
||||
// Only the write and the site-packages read are asked about; the stdlib read is implicit.
|
||||
const std::string write_prompt = "open: " + python.str("STDLIB_FILE") + "\n";
|
||||
const std::string site_packages_prompt = "open: " + python.str("SITE_PACKAGE_FILE") + "\n";
|
||||
CHECK(describe(harness.prompts) == write_prompt + site_packages_prompt);
|
||||
}
|
||||
Reference in new issue
Block a user