Compare commits

...
Author SHA1 Message Date
SoftFever aaab2d6d8b Merge branch 'main' into fix/gui-resource-leaks 2026-10-09 12:09:31 +08:00
ExPikaPakaandRodrigo Faselli 018c4f49ee Fix a crash on loading a 3MF with empty project settings (#16016)
* Fix a crash on loading a 3MF with empty project settings

opt_float() dereferences what option<>() returns without checking it, and
option<>() is called with create = false. Three CLI sites read printable_height
that way, so a 3mf whose Metadata/project_settings.config holds an empty object
takes the CLI down with a null dereference. Both models shipped in
resources/handy_models are such files, so `--info` on either of them segfaults.

Guard the three reads the way the neighbouring reads of
extruder_clearance_height_to_rod and friends already are. All three target
variables are initialised to 0 and the consumer tests for > 0, so an absent
setting already had a defined meaning and nothing changes for a project that
carries the setting.

* Add a CLI regression test for a project with empty settings

Runs --info over a copy of a shipped model whose Metadata/project_settings.config
has been rewritten to an empty object, so the test keeps covering the crash no
matter what settings the shipped models carry later.

Verified both ways: the test passes against this branch and fails with a
segmentation fault against a build without the guards.

---------

Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-09 12:07:03 +08:00
ExPikaPaka 4c74d84b94 Fail cleanly on a truncated or corrupt PNG (#16014)
libpng reports a bad file by longjmp()ing back to the buffer set with
setjmp(), and the frame it lands in must own nothing that needs
destroying: with exceptions enabled MSVC unwinds the stack as part of
longjmp, and returning from a frame unwound that way crashes. It did on
Windows while working everywhere else.

The read callback also returned quietly on a short read, leaving libpng
to decode whatever happened to be in the output buffer.

The calls that can fail now sit in two helpers that own nothing but
pointers, so every C++ object the decoders need stays in their own
frames, and a short read is reported through png_error().
2026-10-09 11:52:55 +08:00
HanifKoh 65978fc87a Cap Recursion Depth in the Plugin JSON Converters (#16255)
* Cap Recursion Depth in the Plugin JSON Converters

* Add Missing Includes to the Plugin JSON Depth Test
2026-10-09 11:39:46 +08:00
HanifKoh 9a9285a894 Log Instead of Showing Info and Substitution Dialogs When Opening a BambuStudio Project (#16153)
Every BambuStudio project opened with a "BambuStudio Project" info dialog
(or, from BambuStudio 2.8.2, one saying the file is newer than the
compatible version and to update the software), followed by the
configuration-substitution dialogs for the project and its embedded
presets. None of them asks anything and all of them fire for every
BambuStudio file.

For BambuStudio projects (untagged files newer than 2.3.2, the existing
test) log the version with the unrecognized settings, and each replaced
value, instead. The geometry-only, invalid-values and G-code safety
dialogs stay, and other 3MFs are unchanged.
2026-10-09 11:33:53 +08:00
HanifKoh ae59656fa4 Dismiss the Slice-Plate Popup Before the Filament Grouping Dialog Opens (#16284)
The Slice-plate hover popup (FilamentGroupPopup, a wxPopupTransientWindow)
takes the mouse capture while it is shown, and on macOS its OnIdle handler
reacquires that capture whenever the cursor sits outside the popup. If the
popup is still shown when the modal filament grouping dialog opens, wx routes
every dialog mouse event to the now-hidden popup, because WX_filterSendEvent
short-circuits to the capture window while GetCapture() is non-null. The
dialog's filament blocks never receive a mouse-down, so they can't be dragged
and the whole app looks frozen even though its modal loop is healthy and the
keyboard still works.

Dismiss the popup synchronously before the dialog opens: Dismiss() hides it
and releases the capture, and hiding it stops OnIdle from reacquiring. This is
a no-op where the popup is never shown (Linux, where the hover popup is
disabled, and any non-dual-nozzle printer).
2026-10-09 11:33:24 +08:00
Misterff1 627ae12dd3 Fix regression: bring back some machine specific BBL P2S start gcode (#15967)
Fix removal of custom code for machine_start_gcode for Bambu Lab P2S
2026-10-09 11:21:35 +08:00
Harm Berntsen b3490a1cdc Prevent segfault when X509_get_default_cert_file_env() environment variable is not set (#16128) 2026-10-08 22:39:26 -03:00
TheLegendTubaGuy c7e7fa2a0b Multi Object Vase Settings from Plate Settings Change (#16091) 2026-10-08 20:40:52 -03:00
lodriguez 16380e8560 add fallback to link spnav dynamiclly (#14223) 2026-10-08 20:35:47 -03:00
Ian Bassi 785a1946a6 Fix Adaptive and Support Cubic infill density with modifiers (#16295) 2026-10-08 18:28:56 -03:00
Kris Austin 8790b07773 fix: missing chamfers on STEP import since the OCCT 8.0.1 update (#16290) 2026-10-08 18:10:37 -03:00
mosfet80 1ec9b315f5 Update CMake minimum version to 3.10 (#16097) 2026-10-08 17:43:38 -03:00
Fernando Marino` e6be22dd4c Assign lib_name from $1 in has_host_runtime_library (#15912) 2026-10-08 17:22:57 -03:00
Rodrigo Faselli ee2c40ea85 Fix Mesh Boolean negative (cut) text object (#16275)
* Fix Mesh Boolean negative (cut) text object

* Update test_meshboolean.cpp

* missing headers

* Update test_meshboolean.cpp

* Refactor mcut difference test for source splits

* Apply suggestion from @raistlin7447
2026-10-08 16:00:28 -03:00
Rodrigo Faselli 3baacc6b75 Add memory header to GUI_Factories.cpp 2026-10-06 10:25:49 -03:00
Rodrigo Faselli 1a0af2fe57 Merge branch 'main' into fix/gui-resource-leaks 2026-10-06 10:18:20 -03:00
ExPikaPaka c017629d11 Release GL textures, menus and handlers the GUI was leaking
The filament legend held its texture id in a function local static while the
loader it calls generates a new texture every time, and BitmapCache deletes none,
so a dual nozzle printer leaked one texture per filament per frame while the
preview was open, along with an SVG read and a rasterize per frame. The same
pattern sat in menu_item_with_icon. Textures are now cached on what actually
varies and released with the rest of the ImGui resources. The canvas reuses
frames by hashing the draw commands, which include the texture id, so a fresh id
every frame also kept frame skipping from ever triggering.

TriangleSelectorPatch did not release its VAO and buffers in the destructor,
so every gizmo close leaked them. The base class already issues GL deletes on
that same path, so this adds no new context requirement.

bind_event_handlers() installed three lambdas that unbind_event_handlers() could
not remove, because wx matches handlers by functor address. They are member
functions now, so bind and unbind are symmetric, and the view switch stops
stacking handlers.

The five selection dependent menus were allocated with new on every popup and
never owned. PopupMenu is synchronous and none of them is used as a submenu, so
a single owner is enough.

remove_notification_of_type stopped at the first match, so clear_all() left the
later instances of multi-instance types alive with object ids from a project that
is gone. The measure gizmo cleared its raycaster map but not the map holding one
full mesh decomposition per volume.
2026-10-01 09:15:48 +02:00
43 changed files with 833 additions and 214 deletions
+2 -2
View File
@@ -1084,10 +1084,10 @@ endif ()
find_path(SPNAV_INCLUDE_DIR spnav.h)
if (SPNAV_INCLUDE_DIR)
find_library(SPNAV_LIB NAMES libspnav.a) # Force linking libspnav statically
find_library(SPNAV_LIB NAMES libspnav.a spnav)
if (SPNAV_LIB)
add_definitions(-DHAVE_SPNAV)
message(STATUS "SPNAV library found")
message(STATUS "SPNAV library found: ${SPNAV_LIB}")
else()
message(STATUS "SPNAV library NOT found, Spacenavd not supported")
endif()
+2 -2
View File
@@ -104,8 +104,8 @@ fi
CMAKE_VERSION=$(cmake --version | head -1 | sed 's/[^0-9]*\([0-9]*\).*/\1/')
if [ "$CMAKE_VERSION" -ge 4 ] 2>/dev/null; then
export CMAKE_POLICY_VERSION_MINIMUM=3.5
export CMAKE_POLICY_COMPAT="-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
export CMAKE_POLICY_VERSION_MINIMUM=3.10
export CMAKE_POLICY_COMPAT="-DCMAKE_POLICY_VERSION_MINIMUM=3.10"
echo "Detected CMake 4.x, adding compatibility flag (env + cmake arg)"
else
export CMAKE_POLICY_COMPAT=""
+4 -4
View File
@@ -1,5 +1,5 @@
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "4.0")
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "" FORCE)
set(CMAKE_POLICY_VERSION_MINIMUM 3.10 CACHE STRING "" FORCE)
endif()
#
@@ -24,7 +24,7 @@ endif()
# therefore, unfortunately, the installation cannot be copied/moved elsewhere without re-installing wxWidgets.
#
cmake_minimum_required(VERSION 3.2)
cmake_minimum_required(VERSION 3.10)
if (APPLE)
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts)
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
@@ -224,7 +224,7 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
${_source_dir_arg}
${_gen}
CMAKE_ARGS
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
-DCMAKE_POLICY_VERSION_MINIMUM=3.10
-DCMAKE_INSTALL_PREFIX:STRING=${DESTDIR}
-DCMAKE_MODULE_PATH:STRING=${PROJECT_SOURCE_DIR}/../cmake/modules
-DCMAKE_PREFIX_PATH:STRING=${DESTDIR}
@@ -279,7 +279,7 @@ else()
${_source_dir_arg}
${_gen}
CMAKE_ARGS
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
-DCMAKE_POLICY_VERSION_MINIMUM=3.10
-DCMAKE_INSTALL_PREFIX:STRING=${DESTDIR}
-DCMAKE_PREFIX_PATH:STRING=${DESTDIR}
-DCMAKE_IGNORE_PREFIX_PATH:STRING=${CMAKE_IGNORE_PREFIX_PATH}
@@ -0,0 +1,28 @@
diff --git a/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx b/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
index 6f63781..9b1c08e 100644
--- a/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
+++ b/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
@@ -210,10 +210,10 @@ private:
// Define two pcurves of the seam-edge.
occ::handle<Geom2d_Curve> aPC1, aPC2;
- double af, al;
+ double af, al, af1, al1;
aE.Orientation(TopAbs_FORWARD);
- aPC1 = BRep_Tool::CurveOnSurface(aE, aF, af, al);
+ aPC1 = BRep_Tool::CurveOnSurface(aE, aF, af1, al1);
aE.Orientation(TopAbs_REVERSED);
aPC2 = BRep_Tool::CurveOnSurface(aE, aF, af, al);
@@ -224,7 +224,9 @@ private:
}
// Select the correct pcurve of the seam-edge.
- const gp_Pnt2d& aFPntOfPC1 = aPC1->Value(aPC1->FirstParameter());
+ // Use the edge's first parameter. A Geom2d_Line's FirstParameter() is -Precision::Infinite(),
+ // where a direction of (2e-16, -1) from rounding error gives an X far outside the U range.
+ const gp_Pnt2d aFPntOfPC1 = aPC1->Value(af1);
if (std::abs(aLPntOfIPC1.X() - aFPntOfPC1.X()) > Precision::Confusion())
{
+7
View File
@@ -25,9 +25,16 @@ endif()
# shipped bytes. Windows ships only the DLLs libslic3r links, so the tab adds the TKFillet,
# TKOffset and TKBool DLLs. See docs/HLSD/design-tab.md.
if (IN_GIT_REPO)
set(OCCT_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OCCT-prefix/src/dep_OCCT)
endif ()
orcaslicer_add_cmake_project(OCCT
URL https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/V8_0_1.zip
URL_HASH SHA256=7c033d917ee8f040c0512d289dcc5f02c148889d5bac17c3e25639accb44f0da
# Makes BRepMesh triangulate cone faces whose seam pcurve is slightly tilted
# (Open-Cascade-SAS/OCCT#572); remove the patch once an OCCT release includes the fix.
PATCH_COMMAND git apply ${OCCT_DIRECTORY_FLAG} --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-BRepMesh-seam-pcurve-at-edge-parameter.patch
#DEPENDS dep_Boost
DEPENDS ${FREETYPE_PKG}
CMAKE_ARGS
+7
View File
@@ -79,6 +79,13 @@ the same `infill_body()`. The octree of the whole object is built only for an
object of a single body, or when some body received no triangles, which then
uses it.
The line spacing of an octree comes from the density, line width and multiline
count of a region, so a modifier or a part with its own density needs octrees of
its own. `adaptive_fill_line_spacing()` gives the spacing of each region, and
`FillAdaptive::RegionOctrees` holds one set of octrees per distinct spacing,
shared by the regions that have it. A set is built only for the bodies its
regions fill. The fill takes the set of its region, then the octree of its body.
## Patterns left out
Lightning grows its trees over the whole object, so moving a reference point
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -46,7 +46,7 @@ RUN set -eux; \
tar -xzf /tmp/assimp.tar.gz -C /tmp/assimp-src --strip-components=1; \
DESTDIR=/OrcaSlicer/deps/build/destdir/usr/local; \
cmake -S /tmp/assimp-src -B /tmp/assimp-build -G Ninja \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_POLICY_VERSION_MINIMUM=3.10 \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$DESTDIR" \
-DCMAKE_PREFIX_PATH="$DESTDIR" \
+6 -2
View File
@@ -1820,6 +1820,8 @@ int CLI::run(int argc, char **argv)
old_printable_width = static_cast<int>(old_printable_bbox.size().x());
old_printable_depth = static_cast<int>(old_printable_bbox.size().y());
}
// A 3mf can carry an empty project_settings.config - the models in
// resources/handy_models do - and opt_float() dereferences without checking.
if (config.option<ConfigOptionFloat>("printable_height"))
old_printable_height = (int)(config.opt_float("printable_height"));
@@ -2505,7 +2507,8 @@ int CLI::run(int argc, char **argv)
orig_printable_width = static_cast<int>(orig_printable_bbox.size().x());
orig_printable_depth = static_cast<int>(orig_printable_bbox.size().y());
}
orig_printable_height = (int)(config.opt_float("printable_height"));
if (config.option<ConfigOptionFloat>("printable_height"))
orig_printable_height = (int)(config.opt_float("printable_height"));
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(":%1%, check printable size: old_printable_width=%2%, orig_printable_width=%3%, old_printable_depth=%4%, orig_printable_depth=%5%, old_printable_height=%6%, orig_printable_height=%7%")
%__LINE__ %old_printable_width %orig_printable_width %old_printable_depth %orig_printable_depth %old_printable_height %orig_printable_height;
if ((orig_printable_width > 0) && (orig_printable_depth > 0) && (orig_printable_height > 0))
@@ -4617,7 +4620,8 @@ int CLI::run(int argc, char **argv)
BoundingBoxf temp_printable_bbox(temp_printable_area);
printer_plate.printable_width = static_cast<int>(temp_printable_bbox.size().x());
printer_plate.printable_depth = static_cast<int>(temp_printable_bbox.size().y());
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
if (config.option<ConfigOptionFloat>("printable_height"))
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
}
if (temp_exclude_area.size() >= 4) {
printer_plate.exclude_width = (int)(temp_exclude_area[2].x() - temp_exclude_area[0].x());
@@ -303,10 +303,11 @@ else
fi
has_host_runtime_library() {
local lib_name path
local lib_name="\$1"
local path
if command -v ldconfig >/dev/null 2>&1; then
if ldconfig -p 2>/dev/null | grep -Fq " \$lib_name"; then
if ldconfig -p 2>/dev/null | grep -Fq "\$lib_name ("; then
return 0
fi
fi
+4 -4
View File
@@ -1320,7 +1320,7 @@ void export_group_fills_to_svg(const char *path, const std::vector<SurfaceFill>
#endif
// friend to Layer
void Layer::make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator)
void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator)
{
for (LayerRegion *layerm : m_regions)
layerm->fills.clear();
@@ -1353,7 +1353,7 @@ void Layer::make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const
f->z = this->print_z;
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = surface_fill.params.pattern == ipSupportCubic ? support_fill_octrees : adaptive_fill_octrees;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
if (surface_fill.params.pattern == ipConcentricInternal) {
@@ -1516,7 +1516,7 @@ void Layer::make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const
* - For lightning/adaptive patterns, the respective generators are wired so their
* polylines match the final infill layout.
*/
Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator) const
Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator) const
{
LockRegionParam skin_inner_param;
std::vector<SurfaceFill> surface_fills = group_fills(*this, skin_inner_param);
@@ -1574,7 +1574,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
f->z = this->print_z;
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = surface_fill.params.pattern == ipSupportCubic ? support_fill_octrees : adaptive_fill_octrees;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
+14 -77
View File
@@ -301,88 +301,25 @@ void OctreeDeleter::operator()(Octree *p) {
delete p;
}
std::pair<double, double> adaptive_fill_line_spacing(const PrintObject &print_object)
std::vector<double> adaptive_fill_line_spacing(const PrintObject &print_object)
{
// Output, spacing for icAdaptiveCubic and icSupportCubic
double adaptive_line_spacing = 0.;
double support_line_spacing = 0.;
enum class Tristate {
Yes,
No,
Maybe
};
struct RegionFillData {
Tristate has_adaptive_infill;
Tristate has_support_infill;
double density;
double extrusion_width;
};
std::vector<RegionFillData> region_fill_data;
region_fill_data.reserve(print_object.num_printing_regions());
bool build_octree = false;
std::vector<double> line_spacing(print_object.num_printing_regions(), 0.);
const std::vector<double> &nozzle_diameters = print_object.print()->config().nozzle_diameter.values;
double max_nozzle_diameter = *std::max_element(nozzle_diameters.begin(), nozzle_diameters.end());
double default_infill_extrusion_width = Flow::auto_extrusion_width(FlowRole::frInfill, float(max_nozzle_diameter));
for (size_t region_id = 0; region_id < print_object.num_printing_regions(); ++ region_id) {
const PrintRegionConfig &config = print_object.printing_region(region_id).config();
bool nonempty = config.sparse_infill_density > 0;
bool has_adaptive_infill = nonempty && config.sparse_infill_pattern == ipAdaptiveCubic;
bool has_support_infill = nonempty && config.sparse_infill_pattern == ipSupportCubic;
double sparse_infill_line_width = config.sparse_infill_line_width.get_abs_value(max_nozzle_diameter);
region_fill_data.push_back(RegionFillData({
has_adaptive_infill ? Tristate::Maybe : Tristate::No,
has_support_infill ? Tristate::Maybe : Tristate::No,
config.sparse_infill_density,
sparse_infill_line_width != 0. ? sparse_infill_line_width : default_infill_extrusion_width
}));
build_octree |= has_adaptive_infill || has_support_infill;
for (size_t region_id = 0; region_id < line_spacing.size(); ++ region_id) {
const PrintRegionConfig &config = print_object.printing_region(region_id).config();
if (config.sparse_infill_density <= 0 || ! is_octree_infill_pattern(config.sparse_infill_pattern) ||
std::none_of(print_object.layers().begin(), print_object.layers().end(), [region_id](const Layer *layer) {
return region_id < layer->regions().size() && ! layer->regions()[region_id]->fill_surfaces.empty();
}))
continue;
double extrusion_width = config.sparse_infill_line_width.get_abs_value(max_nozzle_diameter);
if (extrusion_width == 0.)
extrusion_width = default_infill_extrusion_width;
line_spacing[region_id] = extrusion_width / ((config.sparse_infill_density / 100.0f) * 0.333333333f) * config.fill_multiline.value;
}
if (build_octree) {
// Compute the average of above parameters over all layers
for (const Layer *layer : print_object.layers())
for (size_t region_id = 0; region_id < layer->regions().size(); ++ region_id) {
RegionFillData &rd = region_fill_data[region_id];
if (rd.has_adaptive_infill == Tristate::Maybe && ! layer->regions()[region_id]->fill_surfaces.empty())
rd.has_adaptive_infill = Tristate::Yes;
if (rd.has_support_infill == Tristate::Maybe && ! layer->regions()[region_id]->fill_surfaces.empty())
rd.has_support_infill = Tristate::Yes;
}
double adaptive_fill_density = 0.;
double adaptive_infill_extrusion_width = 0.;
int adaptive_cnt = 0;
double support_fill_density = 0.;
double support_infill_extrusion_width = 0.;
int support_cnt = 0;
for (const RegionFillData &rd : region_fill_data) {
if (rd.has_adaptive_infill == Tristate::Yes) {
adaptive_fill_density += rd.density;
adaptive_infill_extrusion_width += rd.extrusion_width;
++ adaptive_cnt;
} else if (rd.has_support_infill == Tristate::Yes) {
support_fill_density += rd.density;
support_infill_extrusion_width += rd.extrusion_width;
++ support_cnt;
}
}
auto to_line_spacing = [](int cnt, double density, double extrusion_width) {
if (cnt) {
density /= double(cnt);
extrusion_width /= double(cnt);
return extrusion_width / ((density / 100.0f) * 0.333333333f);
} else
return 0.;
};
const int n_multiline = print_object.printing_region(0).config().fill_multiline.value;
adaptive_line_spacing = to_line_spacing(adaptive_cnt, adaptive_fill_density, adaptive_infill_extrusion_width) * n_multiline;
support_line_spacing = to_line_spacing(support_cnt, support_fill_density, support_infill_extrusion_width) * n_multiline;
}
return std::make_pair(adaptive_line_spacing, support_line_spacing);
return line_spacing;
}
// Context used by generate_infill_lines() when recursively traversing an octree in a DDA fashion
+16 -5
View File
@@ -59,11 +59,22 @@ struct Octrees
}
};
// Calculate line spacing for
// 1) adaptive cubic infill
// 2) adaptive internal support cubic infill
// Returns zero for a particular infill type if no such infill is to be generated.
std::pair<double, double> adaptive_fill_line_spacing(const PrintObject &print_object);
// Orca: The octrees of each line spacing the regions of an object fill with.
struct RegionOctrees
{
std::vector<Octrees> sets;
// Index into sets for each region, -1 for a region without adaptive or support cubic infill.
std::vector<int> region_set;
const Octrees *region(size_t region_id) const
{
return region_id < region_set.size() && region_set[region_id] >= 0 ? &sets[region_set[region_id]] : nullptr;
}
};
// Line spacing of the adaptive or support cubic infill of each region of the object,
// zero for a region that generates no such infill.
std::vector<double> adaptive_fill_line_spacing(const PrintObject &print_object);
// Rotation of the octree to stand on one of its corners.
Eigen::Quaterniond transform_to_world();
+4 -5
View File
@@ -33,7 +33,7 @@ class PrintObject;
class Print;
namespace FillAdaptive {
struct Octrees;
struct RegionOctrees;
};
namespace FillLightning {
@@ -207,10 +207,9 @@ public:
static bool is_perimeter_compatible(const Print& print, const PrintRegion& a, const PrintRegion& b);
void make_perimeters();
// Phony version of make_fills() without parameters for Perl integration only.
void make_fills() { this->make_fills(nullptr, nullptr); }
void make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator = nullptr);
Polylines generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::Octrees *adaptive_fill_octrees,
const FillAdaptive::Octrees *support_fill_octrees,
void make_fills() { this->make_fills(nullptr); }
void make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator = nullptr);
Polylines generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::RegionOctrees *fill_octrees,
FillLightning::Generator* lightning_generator) const;
void make_ironing();
// Returns the filament id (1-based) the region is ironed with, or -1 when the
+22 -1
View File
@@ -1045,7 +1045,28 @@ void do_boolean(McutMesh& srcMesh, const McutMesh& cutMesh, const std::string& b
// But we can force it to work by spliting the src mesh into disconnected components,
// and do booleans seperately, then merge all the results.
indexed_triangle_set all_its;
if (boolean_opts == "UNION" || boolean_opts == "A_NOT_B") {
if (boolean_opts == "A_NOT_B") {
// Each cut can leave the source with several disconnected components, which mcut rejects
// in the next dispatch, so re-split after every cut part (e.g. each letter of a text).
std::vector<indexed_triangle_set> parts = std::move(src_parts);
for (size_t j = 0; j < cut_parts.size(); j++) {
auto cut_part = triangle_mesh_to_mcut(cut_parts[j]);
std::vector<indexed_triangle_set> next_parts;
for (indexed_triangle_set &part : parts) {
auto src_part = triangle_mesh_to_mcut(part);
if (do_boolean_single(*src_part, *cut_part, boolean_opts)) {
TriangleMesh tri_part = mcut_to_triangle_mesh(*src_part);
std::vector<indexed_triangle_set> pieces = its_split(tri_part.its);
std::move(pieces.begin(), pieces.end(), std::back_inserter(next_parts));
} else
next_parts.emplace_back(std::move(part));
}
parts = std::move(next_parts);
}
for (const indexed_triangle_set &part : parts)
its_merge(all_its, part);
}
else if (boolean_opts == "UNION") {
for (size_t i = 0; i < src_parts.size(); i++) {
auto src_part = triangle_mesh_to_mcut(src_parts[i]);
for (size_t j = 0; j < cut_parts.size(); j++) {
+2 -2
View File
@@ -583,7 +583,7 @@ private:
void discover_horizontal_shells();
void combine_infill();
void _generate_support_material();
std::pair<FillAdaptive::Octrees, FillAdaptive::Octrees> prepare_adaptive_infill_data(
FillAdaptive::RegionOctrees prepare_adaptive_infill_data(
const std::vector<std::pair<const Surface*, const Layer*>>& surfaces_w_layer) const;
FillLightning::GeneratorPtr prepare_lightning_infill_data();
@@ -616,7 +616,7 @@ private:
// so that next call to make_perimeters() performs a union() before computing loops
bool m_typed_slices = false;
std::pair<FillAdaptive::Octrees, FillAdaptive::Octrees> m_adaptive_fill_octrees;
FillAdaptive::RegionOctrees m_adaptive_fill_octrees;
std::vector<BoundingBox> m_separated_body_bboxes;
FillLightning::GeneratorPtr m_lightning_generator;
+53 -35
View File
@@ -848,7 +848,7 @@ void PrintObject::infill()
[this](const tbb::blocked_range<size_t>& range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
m_print->throw_if_canceled();
m_layers[layer_idx]->make_fills(&m_adaptive_fill_octrees.first, &m_adaptive_fill_octrees.second, this->m_lightning_generator.get());
m_layers[layer_idx]->make_fills(&m_adaptive_fill_octrees, this->m_lightning_generator.get());
}
}
);
@@ -1168,14 +1168,27 @@ static std::vector<indexed_triangle_set> split_mesh_by_body(const PrintObject &o
return bodies;
}
std::pair<FillAdaptive::Octrees, FillAdaptive::Octrees> PrintObject::prepare_adaptive_infill_data(
FillAdaptive::RegionOctrees PrintObject::prepare_adaptive_infill_data(
const std::vector<std::pair<const Surface *, const Layer *>> &surfaces_w_layer) const
{
using namespace FillAdaptive;
auto [adaptive_line_spacing, support_line_spacing] = adaptive_fill_line_spacing(*this);
if ((adaptive_line_spacing == 0. && support_line_spacing == 0.) || this->layers().empty())
// Orca: Each region fills with the octrees of its own line spacing, shared by the regions of equal spacing.
const std::vector<double> line_spacing = adaptive_fill_line_spacing(*this);
std::vector<std::pair<double, bool>> spacings; // Line spacing, support cubic.
RegionOctrees octrees;
octrees.region_set.assign(line_spacing.size(), -1);
for (size_t region_id = 0; region_id < line_spacing.size(); ++ region_id)
if (line_spacing[region_id] > 0.) {
const std::pair<double, bool> spacing(line_spacing[region_id], this->printing_region(region_id).config().sparse_infill_pattern == ipSupportCubic);
const auto it = std::find(spacings.begin(), spacings.end(), spacing);
octrees.region_set[region_id] = int(it - spacings.begin());
if (it == spacings.end())
spacings.push_back(spacing);
}
if (spacings.empty() || this->layers().empty())
return {};
octrees.sets.resize(spacings.size());
indexed_triangle_set mesh = this->model_object()->raw_indexed_triangle_set();
// Rotate mesh and build octree on it with axis-aligned (standart base) cubes.
@@ -1198,44 +1211,50 @@ std::pair<FillAdaptive::Octrees, FillAdaptive::Octrees> PrintObject::prepare_ada
}
});
// Orca: Each body gets the octree it has when sliced on its own, from its own triangles.
std::pair<Octrees, Octrees> octrees;
const size_t num_bodies = m_separated_body_bboxes.size();
bool need_object = num_bodies <= 1;
// Orca: Each body gets the octree it has when sliced on its own, from its own triangles, for each line spacing
// its regions fill with. Body num_bodies stands for the whole object, which serves an object of a single body
// and the surfaces of bodies that have no octree of their own.
const size_t num_bodies = m_separated_body_bboxes.size();
std::vector<std::pair<size_t, size_t>> to_build; // Set, body.
std::vector<indexed_triangle_set> body_meshes;
std::vector<std::vector<Vec3d>> body_overhangs(num_bodies);
if (num_bodies > 1) {
const std::vector<indexed_triangle_set> body_meshes = split_mesh_by_body(*this, mesh, num_bodies);
need_object = std::any_of(body_meshes.begin(), body_meshes.end(), [](const indexed_triangle_set &its) { return its.indices.empty(); });
std::vector<std::vector<Vec3d>> body_overhangs(num_bodies);
body_meshes = split_mesh_by_body(*this, mesh, num_bodies);
for (size_t i = 0; i < surfaces_w_layer.size(); ++ i)
if (const int body = separated_body_at(*surfaces_w_layer[i].second, surfaces_w_layer[i].first->expolygon.contour.points.front()); body >= 0)
append(body_overhangs[body], overhangs[i]);
if (adaptive_line_spacing)
octrees.first.bodies.resize(num_bodies);
if (support_line_spacing)
octrees.second.bodies.resize(num_bodies);
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_bodies), [&, adaptive_spacing = adaptive_line_spacing, support_spacing = support_line_spacing](
const tbb::blocked_range<size_t> &range) {
for (size_t body = range.begin(); body < range.end(); ++ body) {
m_print->throw_if_canceled();
if (body_meshes[body].indices.empty())
continue;
if (adaptive_spacing)
octrees.first.bodies[body] = build_octree(body_meshes[body], body_overhangs[body], adaptive_spacing, false);
if (support_spacing)
octrees.second.bodies[body] = build_octree(body_meshes[body], body_overhangs[body], support_spacing, true);
}
});
}
std::vector<std::vector<char>> fills(spacings.size(), std::vector<char>(num_bodies + 1, false));
for (const Layer *layer : m_layers)
for (size_t region_id = 0; region_id < layer->regions().size() && region_id < octrees.region_set.size(); ++ region_id)
if (const int set = octrees.region_set[region_id]; set >= 0)
for (const Surface &surface : layer->regions()[region_id]->fill_surfaces) {
const int body = separated_body_at(*layer, surface.expolygon.contour.points.front());
fills[set][body >= 0 && ! body_meshes[body].indices.empty() ? size_t(body) : num_bodies] = true;
}
for (size_t set = 0; set < spacings.size(); ++ set) {
octrees.sets[set].bodies.resize(num_bodies);
for (size_t body = 0; body <= num_bodies; ++ body)
if (fills[set][body])
to_build.emplace_back(set, body);
}
} else
for (size_t set = 0; set < spacings.size(); ++ set)
to_build.emplace_back(set, num_bodies);
// and gather them.
for (size_t i = 1; i < overhangs.size(); ++ i)
append(overhangs.front(), std::move(overhangs[i]));
// Orca: The object's octree only serves bodies that have none of their own.
if (need_object && adaptive_line_spacing)
octrees.first.object = build_octree(mesh, overhangs.front(), adaptive_line_spacing, false);
if (need_object && support_line_spacing)
octrees.second.object = build_octree(mesh, overhangs.front(), support_line_spacing, true);
tbb::parallel_for(tbb::blocked_range<size_t>(0, to_build.size()), [&](const tbb::blocked_range<size_t> &range) {
for (size_t i = range.begin(); i < range.end(); ++ i) {
m_print->throw_if_canceled();
const auto [set, body] = to_build[i];
const bool object = body == num_bodies;
(object ? octrees.sets[set].object : octrees.sets[set].bodies[body]) =
build_octree(object ? mesh : body_meshes[body], object ? overhangs.front() : body_overhangs[body], spacings[set].first,
spacings[set].second);
}
});
return octrees;
}
@@ -3077,8 +3096,7 @@ void PrintObject::bridge_over_infill()
for (size_t job_idx = r.begin(); job_idx < r.end(); job_idx++) {
size_t lidx = layers_to_generate_infill[job_idx];
infill_lines.at(
lidx) = po->get_layer(lidx)->generate_sparse_infill_polylines_for_anchoring(&po->m_adaptive_fill_octrees.first,
&po->m_adaptive_fill_octrees.second,
lidx) = po->get_layer(lidx)->generate_sparse_infill_polylines_for_anchoring(&po->m_adaptive_fill_octrees,
po->m_lightning_generator.get());
}
});
+2
View File
@@ -58,6 +58,8 @@ public:
static bool parse_color(const std::string& scolor, unsigned char* rgb_out);
static bool parse_color4(const std::string& scolor, unsigned char* rgba_out);
// Rasterizes the SVG into a freshly generated GL texture; the caller owns it and has to delete
// it (ImGuiWrapper::svg_texture() caches the result for the whole session).
static bool load_from_svg_file_change_color(const std::string &filename, unsigned width, unsigned height, ImTextureID &texture_id, const char *hexColor);
+1 -1
View File
@@ -29,6 +29,7 @@ public:
FilamentGroupPopup(wxWindow *parent);
void tryPopup(Plater* plater,PartPlate* plate, bool slice_all);
void tryClose();
void Dismiss() override;
FilamentMapMode GetSelectedMode() const { return m_mode; }
private:
@@ -40,7 +41,6 @@ private:
void OnLeaveWindow(wxMouseEvent &);
void OnEnterWindow(wxMouseEvent &);
void OnTimer(wxTimerEvent &event);
void Dismiss();
void CreateBmps();
+28 -11
View File
@@ -3444,7 +3444,8 @@ void GLCanvas3D::load_sla_preview()
void GLCanvas3D::bind_event_handlers()
{
if (m_canvas != nullptr) {
// Every view switch binds, so binding twice would run each handler twice per event.
if (m_canvas != nullptr && !m_event_handlers_bound) {
m_canvas->Bind(wxEVT_SIZE, &GLCanvas3D::on_size, this);
m_canvas->Bind(wxEVT_IDLE, &GLCanvas3D::on_idle, this);
m_canvas->Bind(wxEVT_CHAR, &GLCanvas3D::on_char, this);
@@ -3454,9 +3455,9 @@ void GLCanvas3D::bind_event_handlers()
m_canvas->Bind(wxEVT_TIMER, &GLCanvas3D::on_timer, this);
m_canvas->Bind(EVT_GLCANVAS_RENDER_TIMER, &GLCanvas3D::on_render_timer, this);
m_toolbar_highlighter.set_timer_owner(m_canvas, 0);
m_canvas->Bind(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, [this](wxTimerEvent&) { m_toolbar_highlighter.blink(); });
m_canvas->Bind(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, &GLCanvas3D::on_toolbar_highlighter_timer, this);
m_gizmo_highlighter.set_timer_owner(m_canvas, 0);
m_canvas->Bind(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, [this](wxTimerEvent&) { m_gizmo_highlighter.blink(); });
m_canvas->Bind(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, &GLCanvas3D::on_gizmo_highlighter_timer, this);
m_canvas->Bind(wxEVT_LEFT_DOWN, &GLCanvas3D::on_mouse, this);
m_canvas->Bind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this);
m_canvas->Bind(wxEVT_MIDDLE_DOWN, &GLCanvas3D::on_mouse, this);
@@ -3471,14 +3472,7 @@ void GLCanvas3D::bind_event_handlers()
m_canvas->Bind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this);
m_canvas->Bind(wxEVT_PAINT, &GLCanvas3D::on_paint, this);
m_canvas->Bind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this);
m_canvas->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& evt) {
// The key-up that would commit a keyboard edit goes to whatever took the focus.
if (m_selection_edit.kind != SelectionEdit::None)
finish_selection_edit();
ImGui::SetWindowFocus(nullptr);
render();
evt.Skip();
});
m_canvas->Bind(wxEVT_KILL_FOCUS, &GLCanvas3D::on_kill_focus, this);
m_event_handlers_bound = true;
m_canvas->Bind(wxEVT_GESTURE_PAN, &GLCanvas3D::on_gesture, this);
@@ -3516,6 +3510,9 @@ void GLCanvas3D::unbind_event_handlers()
m_canvas->Unbind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this);
m_canvas->Unbind(wxEVT_PAINT, &GLCanvas3D::on_paint, this);
m_canvas->Unbind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this);
m_canvas->Unbind(wxEVT_KILL_FOCUS, &GLCanvas3D::on_kill_focus, this);
m_canvas->Unbind(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, &GLCanvas3D::on_toolbar_highlighter_timer, this);
m_canvas->Unbind(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, &GLCanvas3D::on_gizmo_highlighter_timer, this);
m_event_handlers_bound = false;
m_canvas->Unbind(wxEVT_GESTURE_PAN, &GLCanvas3D::on_gesture, this);
@@ -5036,6 +5033,26 @@ void GLCanvas3D::on_set_focus(wxFocusEvent& evt)
m_is_touchpad_navigation = wxGetApp().app_config->get_bool("camera_navigation_style");
}
void GLCanvas3D::on_kill_focus(wxFocusEvent& evt)
{
// The key-up that would commit a keyboard edit goes to whatever took the focus.
if (m_selection_edit.kind != SelectionEdit::None)
finish_selection_edit();
ImGui::SetWindowFocus(nullptr);
render();
evt.Skip();
}
void GLCanvas3D::on_toolbar_highlighter_timer(wxTimerEvent& evt)
{
m_toolbar_highlighter.blink();
}
void GLCanvas3D::on_gizmo_highlighter_timer(wxTimerEvent& evt)
{
m_gizmo_highlighter.blink();
}
bool GLCanvas3D::clicked_button_matches_action(const wxMouseEvent& evt, const MouseAction action, const std::map<MouseButton, MouseAction>& mappings) const
{
MouseButton clicked = MouseButton::None;
+3
View File
@@ -1207,6 +1207,9 @@ public:
void on_gesture(wxGestureEvent& evt);
void on_paint(wxPaintEvent& evt);
void on_set_focus(wxFocusEvent& evt);
void on_kill_focus(wxFocusEvent& evt);
void on_toolbar_highlighter_timer(wxTimerEvent& evt);
void on_gizmo_highlighter_timer(wxTimerEvent& evt);
void force_set_focus();
enum class MouseButton { None, Left, Middle, Right };
+13 -6
View File
@@ -25,6 +25,7 @@
#include <string>
#include <map>
#include <vector>
#include <memory>
#include "libslic3r/PrintConfig.hpp"
#include <wx/gdicmn.h>
#include "slic3r/GUI/wxExtensions.hpp"
@@ -2081,9 +2082,15 @@ wxMenu* MenuFactory::instance_menu()
return &m_instance_menu;
}
MenuWithSeparators* MenuFactory::new_transient_menu()
{
m_transient_menu = std::make_unique<MenuWithSeparators>();
return m_transient_menu.get();
}
wxMenu* MenuFactory::layer_menu()
{
MenuWithSeparators* menu = new MenuWithSeparators();
MenuWithSeparators* menu = new_transient_menu();
append_menu_item_settings(menu);
return menu;
@@ -2109,13 +2116,13 @@ wxMenu* MenuFactory::multi_selection_menu()
}
if (all_plates) {
wxMenu* menu = new MenuWithSeparators();
wxMenu* menu = new_transient_menu();
append_menu_item_replace_all_with_stl(menu);
return menu;
}
if (undefined_type)
return nullptr;
wxMenu* menu = new MenuWithSeparators();
wxMenu* menu = new_transient_menu();
if (!multi_volume) {
int index = 0;
if (obj_list()->can_merge_to_multipart_object()) {
@@ -2189,7 +2196,7 @@ wxMenu* MenuFactory::assemble_multi_selection_menu()
// show this menu only for Objects(Instances mixed with Objects)/Volumes selection
return nullptr;
wxMenu* menu = new MenuWithSeparators();
wxMenu* menu = new_transient_menu();
append_menu_item_set_visible(menu);
//append_menu_item_fix_through_cgal(menu);
//append_menu_item_simplify(menu);
@@ -2235,7 +2242,7 @@ wxMenu* MenuFactory::plate_menu()
wxMenu* MenuFactory::assemble_object_menu()
{
wxMenu* menu = new MenuWithSeparators();
wxMenu* menu = new_transient_menu();
// Set Visible
append_menu_item_set_visible(menu);
// Delete
@@ -2255,7 +2262,7 @@ wxMenu* MenuFactory::assemble_object_menu()
wxMenu* MenuFactory::assemble_part_menu()
{
wxMenu* menu = new MenuWithSeparators();
wxMenu* menu = new_transient_menu();
append_menu_item_set_visible(menu);
append_menu_item_delete(menu);
+10
View File
@@ -2,6 +2,7 @@
#define slic3r_GUI_Factories_hpp_
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <array>
@@ -121,6 +122,12 @@ private:
MenuWithSeparators m_assemble_part_menu;
wxMenu m_filament_action_menu;
// The selection dependent menus are rebuilt for every popup, so they cannot be members that
// outlive a build like the ones above; this owns the current one and destroys the previous.
// One slot is enough because PopupMenu() is synchronous: the menu a caller was handed is gone
// from the screen before anything can ask for the next one.
std::unique_ptr<MenuWithSeparators> m_transient_menu;
// Removed/Prepended Items according to the view mode
@@ -128,6 +135,9 @@ private:
std::array<wxMenuItem*, mtCount> items_decrease;
std::array<wxMenuItem*, mtCount> items_set_number_of_copies;
// Replaces m_transient_menu with an empty menu and returns it.
MenuWithSeparators* new_transient_menu();
void create_default_menu();
void create_common_object_menu(wxMenu *menu);
void create_object_menu();
+4
View File
@@ -2317,6 +2317,10 @@ void GLGizmoMeasure::update_measurement_result()
void GLGizmoMeasure::reset_all_pick()
{
std::map<GLVolume*, std::shared_ptr<PickRaycaster>>().swap(m_mesh_raycaster_map);
// register_single_mesh_pick() fills both maps in lockstep, so the measurings have to go with
// the raycasters; otherwise the entries keyed on the GLVolumes of the previous selection stay
// behind for the rest of the session.
std::map<GLVolume*, std::shared_ptr<Measure::Measuring>>().swap(m_mesh_measure_map);
reset_gripper_pick(GripperType::UNDEFINE,true);
}
+4 -3
View File
@@ -37,8 +37,7 @@ enum class PainterGizmoType {
FDM_SUPPORTS,
SEAM,
MM_SEGMENTATION,
FUZZY_SKIN,
TEXTURE_DISPLACEMENT
FUZZY_SKIN
};
class TriangleSelectorGUI : public TriangleSelector {
@@ -112,7 +111,9 @@ class TriangleSelectorPatch : public TriangleSelectorGUI {
public:
explicit TriangleSelectorPatch(const TriangleMesh& mesh, const std::vector<ColorRGBA> ebt_colors, float edge_limit = 0.6f)
: TriangleSelectorGUI(mesh, edge_limit), m_ebt_colors(ebt_colors) {}
virtual ~TriangleSelectorPatch() = default;
// Releases the VAO and the per-patch VBOs built by finalize_triangle_indices(). The base class
// already deletes GL buffers from its GLModel members here, so this needs no context of its own.
virtual ~TriangleSelectorPatch() { release_geometry(); }
// Render current selection. Transformation matrices are supposed
// to be already set.
+44 -4
View File
@@ -384,6 +384,7 @@ ImGuiWrapper::~ImGuiWrapper()
{
//destroy_fonts_texture();
destroy_font();
destroy_svg_textures();
ImGui::DestroyContext();
}
@@ -565,6 +566,12 @@ bool ImGuiWrapper::update_key_data(wxKeyEvent &evt)
return ret;
}
// SVG icons rasterized into GL textures, keyed on file name, size and recolor. Cleared as a whole
// from new_frame() once it grows past MAX_SVG_TEXTURES, which is safe there: the previous frame has
// been rendered and the frame about to be recorded asks for every icon it draws again.
static std::map<std::string, ImTextureID> s_svg_textures;
static const size_t MAX_SVG_TEXTURES = 256;
void ImGuiWrapper::new_frame()
{
if (m_new_frame_open) {
@@ -575,6 +582,11 @@ void ImGuiWrapper::new_frame()
init_font(true);
}
// Recolored icons accumulate one texture per color the session has shown; drop them before
// anything references them again. This frame recreates the handful it actually draws.
if (s_svg_textures.size() > MAX_SVG_TEXTURES)
destroy_svg_textures();
ImGuiIO& io = ImGui::GetIO();
ImGui::NewFrame();
@@ -1827,8 +1839,7 @@ bool menu_item_with_icon(const char *label, const char *shortcut, ImVec2 icon_si
if (icon_color != 0)
ImGui::RenderFrame(icon_pos, icon_pos + icon_size, icon_color);
else {
static ImTextureID transparent;
IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/transparent.svg", icon_size.x, icon_size.y, transparent);
ImTextureID transparent = ImGuiWrapper::svg_texture(Slic3r::resources_dir() + "/images/transparent.svg", icon_size.x, icon_size.y);
window->DrawList->AddImage(transparent, icon_pos, icon_pos + icon_size, { 0,0 }, { 1,1 }, ImGui::GetColorU32(ImVec4(1.f, 1.f, 1.f, 1.f)));
}
}
@@ -3408,6 +3419,36 @@ bool ImGuiWrapper::display_initialized() const
return io.DisplaySize.x >= 0.0f && io.DisplaySize.y >= 0.0f;
}
ImTextureID ImGuiWrapper::svg_texture(const std::string& filename, unsigned width, unsigned height, const char* hex_color)
{
std::string key = filename + "|" + std::to_string(width) + "x" + std::to_string(height);
if (hex_color != nullptr)
key += std::string("|") + hex_color;
const auto it = s_svg_textures.find(key);
if (it != s_svg_textures.end())
return it->second;
ImTextureID texture_id = nullptr;
const bool loaded = (hex_color != nullptr) ?
BitmapCache::load_from_svg_file_change_color(filename, width, height, texture_id, hex_color) :
IMTexture::load_from_svg_file(filename, width, height, texture_id);
if (!loaded)
return nullptr;
s_svg_textures.emplace(std::move(key), texture_id);
return texture_id;
}
void ImGuiWrapper::destroy_svg_textures()
{
for (const auto& texture : s_svg_textures) {
GLuint texture_id = (GLuint)(intptr_t)texture.second;
glsafe(::glDeleteTextures(1, &texture_id));
}
s_svg_textures.clear();
}
void ImGuiWrapper::destroy_font()
{
if (m_font_texture != 0) {
@@ -3487,7 +3528,6 @@ void ImGuiWrapper::filament_group(const std::string& filament_type, const char*
//ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
std::string id = std::to_string(static_cast<unsigned int> (filament_id + 1));
ImDrawList* draw_list = ImGui::GetWindowDrawList();
static ImTextureID transparent;
ImVec2 text_size = ImGui::CalcTextSize(filament_type.c_str());
// BBS image sizing based on text width (DPI scaling)
float img_width = ImGui::CalcTextSize("ABC").x;
@@ -3500,7 +3540,7 @@ void ImGuiWrapper::filament_group(const std::string& filament_type, const char*
if (rgba[3] == 0x00) {
svg_path = "/images/outlined_rect_transparent.svg";
}
BitmapCache::load_from_svg_file_change_color(Slic3r::resources_dir() + svg_path, img_size.x, img_size.y, transparent, hex_color);
ImTextureID transparent = svg_texture(Slic3r::resources_dir() + svg_path, img_size.x, img_size.y, hex_color);
ImGui::BeginGroup();
{
ImVec2 cursor_pos = ImGui::GetCursorScreenPos();
+8
View File
@@ -108,6 +108,14 @@ public:
// Hash of every draw list's vertices, indices and commands.
static ImGuiID draw_data_signature(const ImDrawData* draw_data);
// A GL texture holding an SVG icon rasterized at width x height, optionally recolored.
// Rasterizing an SVG is far too expensive to redo for every frame that draws the icon, and the
// texture the previous frame generated would leak, so the result is kept until the frame that
// finds the cache overgrown drops it (and rebuilds only what it still draws).
static ImTextureID svg_texture(const std::string& filename, unsigned width, unsigned height, const char* hex_color = nullptr);
// Deletes every texture svg_texture() handed out. Requires a current GL context.
static void destroy_svg_textures();
float scaled(float x) const { return x * m_font_size; }
ImVec2 scaled(float x, float y) const { return ImVec2(x * m_font_size, y * m_font_size); }
/// <summary>
+6
View File
@@ -2309,6 +2309,12 @@ wxBoxSizer* MainFrame::create_side_tools()
bool slice = true;
// The Slice-plate hover popup is a transient popup that keeps grabbing
// the mouse capture while shown. Left behind the modal grouping dialog it
// would starve that dialog of mouse events, so close it synchronously first.
if (m_filament_group_popup)
m_filament_group_popup->Dismiss();
auto curr_plate = m_plater->get_partplate_list().get_curr_plate();
#ifdef __linux__
PresetBundle* preset = wxGetApp().preset_bundle;
+2 -1
View File
@@ -2250,11 +2250,12 @@ void NotificationManager::close_and_delete_self(PopNotification * self)
}
void NotificationManager::remove_notification_of_type(const NotificationType type) {
// Seven notification types may have several instances alive at once, so erase every match:
// stopping at the first one leaves the rest (and the ObjectIDs they hold) behind.
for (auto it = m_pop_notifications.begin(); it != m_pop_notifications.end();) {
std::unique_ptr<PopNotification> &notification = *it;
if (notification->get_type() == type) {
it = m_pop_notifications.erase(it);
break;
} else
++it;
}
+3 -3
View File
@@ -3128,7 +3128,7 @@ void PartPlate::set_vase_mode_related_object_config(int obj_id) {
new_conf.set_key_value("detect_thin_wall", new ConfigOptionBool(false));
new_conf.set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
new_conf.set_key_value("overhang_reverse", new ConfigOptionBool(false));
auto applying_keys = global_config->diff(new_conf);
const auto applying_keys = global_config->diff(new_conf);
for (ModelObject* object : obj_ptrs) {
ModelConfigObject& config = object->config;
@@ -3137,8 +3137,8 @@ void PartPlate::set_vase_mode_related_object_config(int obj_id) {
config.set_key_value(opt_key, new_conf.option(opt_key)->clone());
}
applying_keys = config.get().diff(new_conf);
for (auto opt_key : applying_keys) {
const auto object_keys = config.get().diff(new_conf);
for (auto opt_key : object_keys) {
config.set_key_value(opt_key, new_conf.option(opt_key)->clone());
}
}
+33 -28
View File
@@ -8800,6 +8800,20 @@ void read_binary_stl(const std::string& filename, std::string& model_id, std::st
return;
}
// Logs what show_substitutions_info() would list, for loads that don't show the dialog.
static void log_substitutions(const ConfigSubstitutions& substitutions, const std::string& source)
{
for (const ConfigSubstitution& substitution : substitutions)
BOOST_LOG_TRIVIAL(warning) << "Loading " << source << ": " << substitution.opt_def->opt_key << " = \"" << substitution.old_value
<< "\" replaced with \"" << substitution.new_value->serialize() << "\"";
}
static void log_substitutions(const PresetsConfigSubstitutions& substitutions, const std::string& source)
{
for (const PresetConfigSubstitutions& preset : substitutions)
log_substitutions(preset.substitutions, source + " (preset " + preset.preset_name + ")");
}
// BBS: backup & restore
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files,
LoadStrategy strategy,
@@ -9101,7 +9115,6 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
Semver app_version = *(Semver::parse(SoftFever_VERSION));
const wxString load_3mf_title = _L("Load 3MF");
const wxString newer_3mf_title = _L("Newer 3MF version");
const wxString bambu_project_title = _L("BambuStudio Project");
const wxString msg_unsupported_geometry = _L("The 3MF is not supported by OrcaSlicer, loading geometry data only.");
const wxString msg_old_orca_geometry = _L("The 3MF file was generated by an old OrcaSlicer version, loading geometry data only.");
const wxString msg_older_geometry = _L("The 3MF file was generated by an older version, loading geometry data only.");
@@ -9112,6 +9125,8 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
<< boost::format("3MF import message [%1%]: %2% | file: %3%") % into_u8(title) % into_u8(text) % path.string();
show_info(q, text, title);
};
// Untagged files up to 2.3.2 may also come from OrcaSlicer, which only started tagging its 3MFs after it.
const bool is_bambu_studio_project = en_3mf_file_type == En3mfType::From_BBS && file_version > Semver(2, 3, 2);
if (en_3mf_file_type == En3mfType::From_Prusa) {
// do not reset the model config
load_config = false;
@@ -9171,8 +9186,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
}
else if (en_3mf_file_type == En3mfType::From_BBS) {
// No OrcaSlicer tag - check Bambu/Application version
Semver orca_tag_start_version(2, 3, 2);
if (file_version <= orca_tag_start_version) {
if (!is_bambu_studio_project) {
// Compatible old version (before OrcaSlicer tagging was introduced after 2.3.2).
// Any version prior or equal to 2.3.2 is older than the current one, no version warnings needed.
// Still apply migration fixes for known old versions.
@@ -9205,33 +9219,17 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
}
} else {
// BambuStudio project (version > 2.3.2 without OrcaSlicer tag)
// Report that a BambuStudio project is being imported and compare with SLIC3R_VERSION
Semver slic3r_version = *(Semver::parse(SLIC3R_VERSION));
if (load_config && config_loaded.empty()) {
load_config = false;
log_and_show_3mf_info(msg_bambu_geometry, load_3mf_title);
}
else if (load_config && (file_version > slic3r_version)) {
// BambuStudio file version is newer than our compatible SLIC3R_VERSION
if (config_substitutions.unrecogized_keys.size() > 0) {
wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Found unrecognized settings:"),
file_version.to_string(), slic3r_version.to_string());
text += "\n";
wxString context = text;
wxString append = _L("You should update your software.\n");
context += "\n\n";
context += append;
log_and_show_3mf_info(context, bambu_project_title);
} else {
wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Some settings may not be fully compatible."),
file_version.to_string(), slic3r_version.to_string());
text += "\n";
log_and_show_3mf_info(text, bambu_project_title);
}
} else if (load_config && !published_config.published) {
// BambuStudio version is older or same as our SLIC3R_VERSION
wxString text = _L("The 3MF was created by BambuStudio. Some settings may differ from OrcaSlicer.");
log_and_show_3mf_info(text, bambu_project_title);
else if (load_config) {
// Logged, not shown: it is the same for every BambuStudio project and needs no action.
std::string unrecognized;
for (const std::string& key : config_substitutions.unrecogized_keys)
unrecognized += (unrecognized.empty() ? "" : ", ") + key;
BOOST_LOG_TRIVIAL(info) << "BambuStudio " << file_version.to_string() << " project " << path.string()
<< ", unrecognized settings: " << (unrecognized.empty() ? "none" : unrecognized);
}
}
}
@@ -9320,7 +9318,10 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
PresetsConfigSubstitutions preset_substitutions;
PresetBundle & preset_bundle = *wxGetApp().preset_bundle;
preset_substitutions = preset_bundle.load_project_embedded_presets(project_presets, ForwardCompatibilitySubstitutionRule::Enable);
if (!preset_substitutions.empty()) show_substitutions_info(preset_substitutions);
if (is_bambu_studio_project)
log_substitutions(preset_substitutions, path.string());
else if (!preset_substitutions.empty())
show_substitutions_info(preset_substitutions);
}
if (project_presets.size() > 0) {
for (unsigned int i = 0; i < project_presets.size(); i++) { delete project_presets[i]; }
@@ -9358,7 +9359,11 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
notify_manager->bbl_show_3mf_warn_notification(error_message);
}
}
if (!config_substitutions.empty()) show_substitutions_info(config_substitutions.substitutions, filename.string());
// BambuStudio projects routinely carry values Orca replaces; log them rather than showing a dialog on every open.
if (is_bambu_studio_project)
log_substitutions(config_substitutions.substitutions, path.string());
else if (!config_substitutions.empty())
show_substitutions_info(config_substitutions.substitutions, filename.string());
// BBS
if (load_model && !load_config) {
+3 -4
View File
@@ -947,13 +947,12 @@ std::string Http::tls_global_init()
std::string Http::tls_system_cert_store()
{
std::string ret;
#ifdef OPENSSL_CERT_OVERRIDE
ret = ::getenv(X509_get_default_cert_file_env());
if (const char *cert_file = ::getenv(X509_get_default_cert_file_env()))
return cert_file;
#endif
return ret;
return {};
}
std::string Http::url_encode(const std::string &str)
+17 -6
View File
@@ -25,11 +25,19 @@ extern const char* const INSTALL_STATE_FILE;
// Plugin config and orca.host.ui payloads both cross the boundary as plain JSON-compatible
// values, so both go through these.
inline pybind11::object json_to_py(const nlohmann::json& j)
// Maximum nesting depth for JSON <-> Python conversion. A self-referential or pathologically
// deep value would otherwise recurse until the native C stack overflows, an uncatchable crash;
// past this bound we raise instead. 200 is far beyond any legitimate plugin config or UI payload.
inline constexpr int kMaxJsonConversionDepth = 200;
inline pybind11::object json_to_py(const nlohmann::json& j, int depth = 0)
{
namespace py = pybind11;
using json = nlohmann::json;
if (depth > kMaxJsonConversionDepth)
throw py::value_error("Plugin JSON value nested too deeply");
switch (j.type()) {
case json::value_t::null: return py::none();
case json::value_t::boolean: return py::bool_(j.get<bool>());
@@ -40,24 +48,27 @@ inline pybind11::object json_to_py(const nlohmann::json& j)
case json::value_t::array: {
py::list lst;
for (const auto& e : j)
lst.append(json_to_py(e));
lst.append(json_to_py(e, depth + 1));
return lst;
}
case json::value_t::object: {
py::dict d;
for (auto it = j.begin(); it != j.end(); ++it)
d[py::str(it.key())] = json_to_py(it.value());
d[py::str(it.key())] = json_to_py(it.value(), depth + 1);
return d;
}
default: return py::none();
}
}
inline nlohmann::json py_to_json(const pybind11::handle& o)
inline nlohmann::json py_to_json(const pybind11::handle& o, int depth = 0)
{
namespace py = pybind11;
using json = nlohmann::json;
if (depth > kMaxJsonConversionDepth)
throw py::value_error("Plugin value nested too deeply (possible cycle)");
if (o.is_none())
return json(nullptr);
if (py::isinstance<py::bool_>(o)) // bool before int (bool subclasses int in Python)
@@ -73,13 +84,13 @@ inline nlohmann::json py_to_json(const pybind11::handle& o)
if (py::isinstance<py::dict>(o)) {
json obj = json::object();
for (auto item : py::reinterpret_borrow<py::dict>(o))
obj[py::str(item.first).cast<std::string>()] = py_to_json(item.second);
obj[py::str(item.first).cast<std::string>()] = py_to_json(item.second, depth + 1);
return obj;
}
if (py::isinstance<py::list>(o) || py::isinstance<py::tuple>(o)) {
json arr = json::array();
for (auto e : o)
arr.push_back(py_to_json(e));
arr.push_back(py_to_json(e, depth + 1));
return arr;
}
return py::str(o).cast<std::string>(); // fallback: str()
+7
View File
@@ -11,6 +11,13 @@ endif ()
add_test(NAME cli_strict_mode
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_strict.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON})
add_test(NAME cli_empty_project_config
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_empty_project_config.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON} ${CMAKE_SOURCE_DIR})
set_tests_properties(cli_empty_project_config PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
TIMEOUT 300)
set_tests_properties(cli_strict_mode PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Regression check: a 3mf whose Metadata/project_settings.config carries no settings must load.
#
# The CLI reads printable_height out of the project config with opt_float(), which dereferences
# what option<>() returns. With create = false that is null when the key is absent, so a project
# saved without settings used to take the CLI down with a segfault. Both models in
# resources/handy_models are such files.
#
# usage: test_cli_empty_project_config.sh <orca-slicer binary> <python3> <source dir>
set -u
BIN="${1:-}"
PY="${2:-python3}"
SRC="${3:-}"
# 77 is the test's SKIP_RETURN_CODE.
[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; }
[ -f "$SRC/resources/handy_models/OrcaBadge.3mf" ] || { echo "SKIP: handy model not found"; exit 77; }
WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-emptycfg.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/datadir"
# Rewrite the project settings to an empty object, so the test holds no matter what the shipped
# models carry later on.
cp "$SRC/resources/handy_models/OrcaBadge.3mf" "$WORK/empty_config.3mf"
"$PY" - "$WORK/empty_config.3mf" <<'PYEOF'
import shutil, sys, zipfile
path = sys.argv[1]
entry = "Metadata/project_settings.config"
with zipfile.ZipFile(path) as src:
items = [(i, src.read(i.filename)) for i in src.infolist()]
with zipfile.ZipFile(path + ".new", "w", zipfile.ZIP_DEFLATED) as dst:
seen = False
for info, data in items:
if info.filename == entry:
data, seen = b"{\n}\n", True
dst.writestr(info, data)
if not seen:
dst.writestr(entry, b"{\n}\n")
shutil.move(path + ".new", path)
PYEOF
"$BIN" --datadir "$WORK/datadir" --info "$WORK/empty_config.3mf" > "$WORK/info.txt" 2>&1
rc=$?
if [ $rc -ne 0 ]; then
echo "FAIL: --info on a project with empty settings exited $rc"
tail -20 "$WORK/info.txt"
exit 1
fi
grep -q "size_x" "$WORK/info.txt" || { echo "FAIL: --info printed no geometry"; cat "$WORK/info.txt"; exit 1; }
echo "PASS: a project with empty settings loads"
+166
View File
@@ -0,0 +1,166 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('Open CASCADE Model'),'2;1');
FILE_NAME('Open CASCADE Shape Model','2026-10-08T09:57:33',('Author'),(
'Open CASCADE'),'Open CASCADE STEP processor 8.0','Open CASCADE 8.0'
,'Unknown');
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
ENDSEC;
DATA;
#1 = APPLICATION_PROTOCOL_DEFINITION('international standard',
'automotive_design',2000,#2);
#2 = APPLICATION_CONTEXT(
'core data for automotive mechanical design processes');
#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10);
#4 = PRODUCT_DEFINITION_SHAPE('','',#5);
#5 = PRODUCT_DEFINITION('design','',#6,#9);
#6 = PRODUCT_DEFINITION_FORMATION('','',#7);
#7 = PRODUCT('Open CASCADE STEP translator 8.0 1',
'Open CASCADE STEP translator 8.0 1','',(#8));
#8 = PRODUCT_CONTEXT('',#2,'mechanical');
#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#15),#121);
#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14);
#12 = CARTESIAN_POINT('',(0.,0.,0.));
#13 = DIRECTION('',(0.,0.,1.));
#14 = DIRECTION('',(1.,0.,-0.));
#15 = MANIFOLD_SOLID_BREP('',#16);
#16 = CLOSED_SHELL('',(#17,#113,#117));
#17 = ADVANCED_FACE('',(#18),#31,.T.);
#18 = FACE_BOUND('',#19,.T.);
#19 = EDGE_LOOP('',(#20,#58,#81,#112));
#20 = ORIENTED_EDGE('',*,*,#21,.F.);
#21 = EDGE_CURVE('',#22,#22,#24,.T.);
#22 = VERTEX_POINT('',#23);
#23 = CARTESIAN_POINT('',(-36.4,0.,25.));
#24 = SURFACE_CURVE('',#25,(#30,#42),.PCURVE_S1.);
#25 = CIRCLE('',#26,36.4);
#26 = AXIS2_PLACEMENT_3D('',#27,#28,#29);
#27 = CARTESIAN_POINT('',(0.,0.,25.));
#28 = DIRECTION('',(0.,0.,1.));
#29 = DIRECTION('',(1.,0.,-0.));
#30 = PCURVE('',#31,#36);
#31 = CONICAL_SURFACE('',#32,36.4,0.785398163397);
#32 = AXIS2_PLACEMENT_3D('',#33,#34,#35);
#33 = CARTESIAN_POINT('',(0.,0.,25.));
#34 = DIRECTION('',(0.,0.,1.));
#35 = DIRECTION('',(1.,0.,-0.));
#36 = DEFINITIONAL_REPRESENTATION('',(#37),#41);
#37 = LINE('',#38,#39);
#38 = CARTESIAN_POINT('',(-6.28318530718,3.552713678801E-15));
#39 = VECTOR('',#40,1.);
#40 = DIRECTION('',(1.,0.));
#41 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#42 = PCURVE('',#43,#48);
#43 = PLANE('',#44);
#44 = AXIS2_PLACEMENT_3D('',#45,#46,#47);
#45 = CARTESIAN_POINT('',(0.,0.,25.));
#46 = DIRECTION('',(-0.,-0.,-1.));
#47 = DIRECTION('',(-1.,0.,0.));
#48 = DEFINITIONAL_REPRESENTATION('',(#49),#57);
#49 = ( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#50,#51,#52,#53,#54,#55,#56),
.UNSPECIFIED.,.T.,.F.) B_SPLINE_CURVE_WITH_KNOTS((1,2,2,2,2,1),(
-2.094395102393,0.,2.094395102393,4.188790204786,6.28318530718,
8.377580409573),.UNSPECIFIED.) CURVE() GEOMETRIC_REPRESENTATION_ITEM()
RATIONAL_B_SPLINE_CURVE((1.,0.5,1.,0.5,1.,0.5,1.)) REPRESENTATION_ITEM(
'') );
#50 = CARTESIAN_POINT('',(-36.4,0.));
#51 = CARTESIAN_POINT('',(-36.4,63.046649395507));
#52 = CARTESIAN_POINT('',(18.2,31.523324697754));
#53 = CARTESIAN_POINT('',(72.8,8.915428697793E-15));
#54 = CARTESIAN_POINT('',(18.2,-31.52332469775));
#55 = CARTESIAN_POINT('',(-36.4,-63.0466493955));
#56 = CARTESIAN_POINT('',(-36.4,0.));
#57 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#58 = ORIENTED_EDGE('',*,*,#59,.T.);
#59 = EDGE_CURVE('',#22,#60,#62,.T.);
#60 = VERTEX_POINT('',#61);
#61 = CARTESIAN_POINT('',(-11.4,0.,3.552713678801E-15));
#62 = SEAM_CURVE('',#63,(#67,#74),.PCURVE_S1.);
#63 = LINE('',#64,#65);
#64 = CARTESIAN_POINT('',(-36.4,0.,25.));
#65 = VECTOR('',#66,1.);
#66 = DIRECTION('',(0.707106781187,8.659560562349E-17,-0.707106781187));
#67 = PCURVE('',#31,#68);
#68 = DEFINITIONAL_REPRESENTATION('',(#69),#73);
#69 = LINE('',#70,#71);
#70 = CARTESIAN_POINT('',(-3.14159265359,3.552713678801E-15));
#71 = VECTOR('',#72,1.);
#72 = DIRECTION('',(2.13162820728E-16,-1.));
#73 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#74 = PCURVE('',#31,#75);
#75 = DEFINITIONAL_REPRESENTATION('',(#76),#80);
#76 = LINE('',#77,#78);
#77 = CARTESIAN_POINT('',(3.14159265359,3.552713678801E-15));
#78 = VECTOR('',#79,1.);
#79 = DIRECTION('',(2.13162820728E-16,-1.));
#80 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#81 = ORIENTED_EDGE('',*,*,#82,.F.);
#82 = EDGE_CURVE('',#60,#60,#83,.T.);
#83 = SURFACE_CURVE('',#84,(#89,#96),.PCURVE_S1.);
#84 = CIRCLE('',#85,11.4);
#85 = AXIS2_PLACEMENT_3D('',#86,#87,#88);
#86 = CARTESIAN_POINT('',(0.,0.,3.552713678801E-15));
#87 = DIRECTION('',(0.,0.,-1.));
#88 = DIRECTION('',(1.,0.,0.));
#89 = PCURVE('',#31,#90);
#90 = DEFINITIONAL_REPRESENTATION('',(#91),#95);
#91 = LINE('',#92,#93);
#92 = CARTESIAN_POINT('',(6.28318530718,-25.));
#93 = VECTOR('',#94,1.);
#94 = DIRECTION('',(-1.,0.));
#95 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#96 = PCURVE('',#97,#102);
#97 = PLANE('',#98);
#98 = AXIS2_PLACEMENT_3D('',#99,#100,#101);
#99 = CARTESIAN_POINT('',(0.,0.,3.552713678801E-15));
#100 = DIRECTION('',(0.,0.,1.));
#101 = DIRECTION('',(1.,0.,-0.));
#102 = DEFINITIONAL_REPRESENTATION('',(#103),#111);
#103 = ( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#104,#105,#106,#107,#108,#109
,#110),.UNSPECIFIED.,.T.,.F.) B_SPLINE_CURVE_WITH_KNOTS((1,2,2,2,2,1),(
-2.094395102393,0.,2.094395102393,4.188790204786,6.28318530718,
8.377580409573),.UNSPECIFIED.) CURVE() GEOMETRIC_REPRESENTATION_ITEM()
RATIONAL_B_SPLINE_CURVE((1.,0.5,1.,0.5,1.,0.5,1.)) REPRESENTATION_ITEM(
'') );
#104 = CARTESIAN_POINT('',(11.4,0.));
#105 = CARTESIAN_POINT('',(11.4,-19.74537920628));
#106 = CARTESIAN_POINT('',(-5.7,-9.872689603143));
#107 = CARTESIAN_POINT('',(-22.8,-2.792194702056E-15));
#108 = CARTESIAN_POINT('',(-5.7,9.872689603143));
#109 = CARTESIAN_POINT('',(11.4,19.745379206285));
#110 = CARTESIAN_POINT('',(11.4,0.));
#111 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#112 = ORIENTED_EDGE('',*,*,#59,.F.);
#113 = ADVANCED_FACE('',(#114),#43,.F.);
#114 = FACE_BOUND('',#115,.F.);
#115 = EDGE_LOOP('',(#116));
#116 = ORIENTED_EDGE('',*,*,#21,.F.);
#117 = ADVANCED_FACE('',(#118),#97,.F.);
#118 = FACE_BOUND('',#119,.F.);
#119 = EDGE_LOOP('',(#120));
#120 = ORIENTED_EDGE('',*,*,#82,.F.);
#121 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#125)) GLOBAL_UNIT_ASSIGNED_CONTEXT
((#122,#123,#124)) REPRESENTATION_CONTEXT('Context #1',
'3D Context with UNIT and UNCERTAINTY') );
#122 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
#123 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
#124 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
#125 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#122,
'distance_accuracy_value','confusion accuracy');
#126 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7));
ENDSEC;
END-ISO-10303-21;
+36 -3
View File
@@ -1780,7 +1780,7 @@ TEST_CASE("Sparse plane-path anchors match the printed infill", "[Fill][Internal
const AABBTreeLines::LinesDistancer<Line> printed_tree(to_lines(printed));
// Orca: Exclude perimeter connections: anchoring and extrusion can trim those differently.
const Polylines anchors = intersection_pl(layer.generate_sparse_infill_polylines_for_anchoring(nullptr, nullptr, nullptr),
const Polylines anchors = intersection_pl(layer.generate_sparse_infill_polylines_for_anchoring(nullptr, nullptr),
shrink(to_polygons(layer.lslices), scale_(3.)));
REQUIRE_FALSE(anchors.empty());
double max_distance = 0.;
@@ -1792,8 +1792,9 @@ TEST_CASE("Sparse plane-path anchors match the printed infill", "[Fill][Internal
CHECK(unscale<double>(max_distance) <= config.opt_float("resolution"));
}
// Orca: Slices the meshes as the parts of one object, where they are.
static Print &slice_parts(Print &print, DynamicPrintConfig config, const std::vector<TriangleMesh> &parts)
// Orca: Slices the meshes as the parts of one object, where they are, with modifiers of their own config.
static Print &slice_parts(Print &print, DynamicPrintConfig config, const std::vector<TriangleMesh> &parts,
const std::vector<std::pair<TriangleMesh, DynamicPrintConfig>> &modifiers = {})
{
config.set_deserialize_strict({{"layer_height", 0.2},
{"initial_layer_print_height", 0.2},
@@ -1804,6 +1805,8 @@ static Print &slice_parts(Print &print, DynamicPrintConfig config, const std::ve
Slic3r::Test::init_print({parts.front()}, print, model, config, nullptr, false);
for (size_t i = 1; i < parts.size(); ++ i)
model.objects.front()->add_volume(TriangleMesh(parts[i]), ModelVolumeType::MODEL_PART, false);
for (const auto &[mesh, modifier_config] : modifiers)
model.objects.front()->add_volume(TriangleMesh(mesh), ModelVolumeType::PARAMETER_MODIFIER, false)->config.apply(modifier_config);
print.apply(model, config);
print.process();
return print;
@@ -1988,3 +1991,33 @@ TEST_CASE("Adaptive infill fills each body like the body sliced alone", "[Fill][
CHECK(unmatched.first < 0.02);
CHECK(unmatched.second < 0.02);
}
TEST_CASE("Adaptive infill of a modifier leaves the density of the other regions", "[Fill][Regression]")
{
const std::string pattern = GENERATE("adaptivecubic", "supportcubic");
CAPTURE(pattern);
auto config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "15%"},
{"top_shell_layers", 0},
{"bottom_shell_layers", 0}});
TriangleMesh bodies = make_cube(30, 30, 6), second = make_cube(30, 30, 6);
second.translate(40, 0, 0);
bodies.merge(second);
// Orca: A denser modifier over the right half of the second body.
TriangleMesh modifier = make_cube(20, 40, 10);
modifier.translate(55, -5, -2);
DynamicPrintConfig dense = config;
dense.set_deserialize_strict({{"sparse_infill_density", "60%"}});
Print print, print_sparse, print_dense;
slice_parts(print, config, {bodies}, {{modifier, dense}});
slice_parts(print_sparse, config, {bodies});
slice_parts(print_dense, dense, {bodies});
// Orca: Bed regions 3 mm inside the walls and the modifier, away from the links along them.
auto rect = [](double x0, double y0, double x1, double y1) {
return Polygon({Point::new_scale(x0, y0), Point::new_scale(x1, y0), Point::new_scale(x1, y1), Point::new_scale(x0, y1)});
};
CHECK(unmatched_between_prints(print, print_sparse, erInternalInfill, {rect(3, 3, 27, 27), rect(43, 3, 52, 27)}) < 0.02);
CHECK(unmatched_between_prints(print, print_dense, erInternalInfill, {rect(58, 3, 67, 27)}) < 0.02);
}
+1
View File
@@ -36,6 +36,7 @@ add_executable(${_TEST_NAME}_tests
test_geometry.cpp
test_multimaterial_segmentation.cpp
test_placeholder_parser.cpp
test_png_read_write.cpp
test_polygon.cpp
test_mutable_polygon.cpp
test_mutable_priority_queue.cpp
+39 -1
View File
@@ -1,9 +1,11 @@
#include <catch2/catch_all.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/catch_approx.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <libslic3r/TriangleMesh.hpp>
#include <libslic3r/MeshBoolean.hpp>
#include <vector>
using namespace Slic3r;
@@ -24,3 +26,39 @@ TEST_CASE("CGAL and TriangleMesh conversions", "[MeshBoolean]") {
REQUIRE(! MeshBoolean::cgal::does_self_intersect(M));
}
TEST_CASE("mcut difference handles source splits between cuts", "[MeshBoolean]") {
TriangleMesh body = make_cube(30., 10., 10.);
TriangleMesh tool;
// First cut splits the source into two disconnected components.
TriangleMesh slab = make_cube(2., 12., 20.);
slab.translate(Vec3f(14.f, -1.f, -5.f));
its_merge(tool.its, slab.its);
// These cuts must still be applied after the source has been split.
TriangleMesh left_hole = make_cube(4., 4., 20.);
left_hole.translate(Vec3f(3.f, 3.f, -5.f));
its_merge(tool.its, left_hole.its);
TriangleMesh right_hole = make_cube(4., 4., 20.);
right_hole.translate(Vec3f(21.f, 3.f, -5.f));
its_merge(tool.its, right_hole.its);
std::vector<TriangleMesh> result;
MeshBoolean::mcut::make_boolean(body, tool, result, "A_NOT_B");
REQUIRE(result.size() == 1);
const std::vector<indexed_triangle_set> components =
its_split(result.front().its);
REQUIRE(components.size() == 2);
// 3000 - 200 - 160 - 160 = 2480.
REQUIRE_THAT(
result.front().volume(),
Catch::Matchers::WithinRel(2480., 1e-3)
);
}
+83
View File
@@ -0,0 +1,83 @@
#include <catch2/catch_all.hpp>
#include <cstdint>
#include <fstream>
#include <iterator>
#include <vector>
#include <boost/filesystem.hpp>
#include "libslic3r/PNGReadWrite.hpp"
using namespace Slic3r;
// libpng reports a corrupt or truncated file by longjmp()ing out of the decoder, so the decoders have
// to come back with false rather than crash or hand back a half filled image.
namespace {
// A real PNG, produced by the writer next door, so the bytes are a file libpng accepts.
std::vector<uint8_t> encoded_png(size_t w, size_t h)
{
std::vector<uint8_t> pixels(w * h);
for (size_t i = 0; i < pixels.size(); ++ i)
pixels[i] = uint8_t((i * 7) % 256);
const boost::filesystem::path path = boost::filesystem::temp_directory_path() /
boost::filesystem::unique_path("png_rw_%%%%%%%%.png");
REQUIRE(png::write_gray_to_file(path.string(), w, h, pixels));
std::vector<uint8_t> bytes;
{
std::ifstream ifs(path.string(), std::ios::binary);
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
boost::system::error_code ec;
boost::filesystem::remove(path, ec);
REQUIRE(bytes.size() > 64);
return bytes;
}
png::ReadBuf buf_of(const std::vector<uint8_t> &bytes, size_t size)
{
return png::ReadBuf{ bytes.data(), size };
}
} // namespace
TEST_CASE("A whole PNG decodes", "[PNG]") {
const std::vector<uint8_t> bytes = encoded_png(24, 16);
png::ImageGreyscale grey;
REQUIRE(png::decode_png(buf_of(bytes, bytes.size()), grey));
CHECK(grey.cols == 24);
CHECK(grey.rows == 16);
CHECK(grey.buf.size() == 24 * 16);
}
TEST_CASE("A truncated PNG is refused instead of crashing", "[PNG]") {
const std::vector<uint8_t> bytes = encoded_png(64, 64);
// Cut past the signature: inside the header, and inside the pixel data. Not in the trailing
// chunks - decode_png() does not read those, so a file missing only its IEND still decodes, and
// that is the pre-existing contract rather than anything this change touches.
const size_t size = GENERATE_COPY(size_t(16), size_t(40), bytes.size() / 2, bytes.size() * 3 / 4);
REQUIRE(size < bytes.size());
png::ImageGreyscale grey;
CHECK_FALSE(png::decode_png(buf_of(bytes, size), grey));
png::ImageColorscale colour;
CHECK_FALSE(png::decode_colored_png(buf_of(bytes, size), colour));
}
TEST_CASE("A PNG whose body is garbage is refused", "[PNG]") {
std::vector<uint8_t> bytes = encoded_png(32, 32);
// Keep the signature, scribble over everything after it.
for (size_t i = 8; i < bytes.size(); ++ i)
bytes[i] = uint8_t(0xA5);
png::ImageGreyscale grey;
CHECK_FALSE(png::decode_png(buf_of(bytes, bytes.size()), grey));
png::ImageColorscale colour;
CHECK_FALSE(png::decode_colored_png(buf_of(bytes, bytes.size()), colour));
}
+18
View File
@@ -8,6 +8,7 @@
#include <catch2/generators/catch_generators.hpp>
#include "libslic3r/Model.hpp"
#include "libslic3r/Format/STEP.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "test_utils.hpp"
using namespace Slic3r;
@@ -73,6 +74,23 @@ TEST_CASE("A security classification assignment does not crash import", "[Step]"
CHECK(model.objects.front()->volumes.front()->mesh().facets_count() == 4); // a tetrahedron
}
// The fixture is a truncated cone whose seam pcurves have the direction (2.1e-16, -1).
TEST_CASE("A cone with a slightly tilted seam imports as a closed mesh", "[Step]")
{
const std::string path = std::string(TEST_DATA_DIR) + PATH_SEPARATOR "cone_tilted_seam_pcurve.step";
Model model;
bool cancel = false;
Step step(path);
REQUIRE(step.load() == Step::Step_Status::LOAD_SUCCESS);
REQUIRE(step.mesh(&model, cancel, false) == Step::Step_Status::MESH_SUCCESS);
REQUIRE(model.objects.size() == 1);
REQUIRE(model.objects.front()->volumes.size() == 1);
CHECK(its_num_open_edges(model.objects.front()->volumes.front()->mesh().its) == 0);
}
TEST_CASE("isUtf8 recognises two, three and four byte sequences", "[Step]")
{
CHECK(StepPreProcessor::isUtf8("\xC3\xA9")); // U+00E9
+1
View File
@@ -34,6 +34,7 @@ add_executable(${_TEST_NAME}_tests
test_plugin_sort.cpp
test_plugin_cloud_metadata.cpp
test_plugin_audit.cpp
test_plugin_json_depth.cpp
test_shortcuts.cpp
test_file_url.cpp
test_user_manager.cpp
@@ -0,0 +1,73 @@
#include <catch2/catch_all.hpp>
#include <catch2/catch_test_macros.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <slic3r/plugin/PythonInterpreter.hpp>
#include "plugin_test_utils.hpp"
#include <cstdint>
#include <exception>
#include <utility>
#include <nlohmann/json.hpp>
#include <pybind11/embed.h>
#include <pybind11/gil.h>
#include <pybind11/pytypes.h>
using namespace Slic3r;
namespace {
// Brings the embedded interpreter up for one test and tears it down before boost::log does,
// mirroring the ScopedPluginManager idiom in the other plugin tests.
struct ScopedPluginManager
{
ScopedDataDir python_data_dir{"plugin-json-depth"};
bool initialized = PluginManager::instance().initialize();
~ScopedPluginManager()
{
PluginManager::instance().shutdown();
PythonInterpreter::instance().shutdown();
}
};
} // namespace
TEST_CASE("py_to_json raises instead of overflowing on pathologically deep input", "[PluginHost][Python]")
{
ScopedPluginManager manager;
REQUIRE(manager.initialized);
namespace py = pybind11;
py::gil_scoped_acquire gil;
// [[[ ... 0 ... ]]] nested 300 deep: past the 200 conversion-depth cap, but shallow enough
// that the pre-fix code returns without crashing, so a regression fails cleanly rather than
// taking the process down. Built in C++ so the test does not depend on Python builtins.
py::object deep = py::int_(0);
for (int i = 0; i < 300; ++i) {
py::list wrapper;
wrapper.append(deep);
deep = std::move(wrapper);
}
CHECK_THROWS_AS(py_to_json(deep), std::exception);
}
TEST_CASE("py_to_json still converts reasonably nested input", "[PluginHost][Python]")
{
ScopedPluginManager manager;
REQUIRE(manager.initialized);
namespace py = pybind11;
py::gil_scoped_acquire gil;
py::dict d;
d["a"] = py::int_(1);
py::list inner;
inner.append(py::str("x"));
inner.append(py::int_(2));
d["b"] = inner;
const nlohmann::json j = py_to_json(d);
CHECK(j.at("a").get<std::int64_t>() == 1);
CHECK(j.at("b").at(0).get<std::string>() == "x");
CHECK(j.at("b").at(1).get<std::int64_t>() == 2);
}