From e0b35f9ec9cb5191158033969696d596597691aa Mon Sep 17 00:00:00 2001 From: ExPikaPaka <112851715+ExPikaPaka@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:08:43 +0200 Subject: [PATCH 1/5] Fix garbled G-code preview when a post-processing script is used (#15005) * Rebuild the G-code line offsets after post-processing scripts run in place * Clamp the G-code window reads to the mapped file size * Add tests for rebuilding the G-code line offsets * Include , and boost/filesystem/operations.hpp where they are used * Keep the preview's G-code lines and highlight in step with post-processing scripts --------- Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com> Co-authored-by: SoftFever --- src/libslic3r/Print.cpp | 25 +++++++++ src/libslic3r/Print.hpp | 5 ++ src/slic3r/GUI/BackgroundSlicingProcess.cpp | 9 ++-- src/slic3r/GUI/GCodeViewer.cpp | 10 +++- tests/fff_print/test_gcodeprocessor.cpp | 60 +++++++++++++++++++++ 5 files changed, 103 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 6d810a922d..5603716064 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -5340,6 +5340,31 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces } } +void Print::reload_gcode_moves(GCodeProcessorResult* result) const +{ + GCodeProcessor processor; + GCodeProcessor::s_IsBBLPrinter = is_BBL_printer(); + const Vec3d origin = this->get_plate_origin(); + processor.set_xy_offset(origin(0), origin(1)); + // Estimate the per-move times with the same nozzle-grouping slot context as the export. + if (result->nozzle_group_result) + processor.initialize_from_context(result->nozzle_group_result); + try { + processor.process_file(result->filename); + } catch (const std::exception& ex) { + // The edited file is what gets printed, so failing to preview it must not fail the slice. + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": cannot re-read the G-code file " << result->filename << ": " << ex.what(); + std::lock_guard lock(result->result_mutex); + result->lines_ends.clear(); + return; + } + + GCodeProcessorResult& reloaded = processor.result(); + std::lock_guard lock(result->result_mutex); + result->moves = std::move(reloaded.moves); + result->lines_ends = std::move(reloaded.lines_ends); +} + std::tuple Print::object_skirt_offset(double margin_height) const { if (config().skirt_loops == 0 || config().skirt_type != stPerObject || m_objects.empty()) diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 062a8eb3a3..b536d73e8f 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -1287,6 +1287,11 @@ public: void set_gcode_file_ready(); void set_gcode_file_invalidated(); void export_gcode_from_previous_file(const std::string& file, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); + // Re-reads the moves and line offsets of `result` from its G-code file after the file was rewritten in + // place (post-processing scripts or plugins), so the preview and its G-code window follow the file on + // disk. Everything else in `result` was computed while slicing and is kept. If the file cannot be + // re-read, the moves are kept and the line offsets are cleared, which hides the G-code window. + void reload_gcode_moves(GCodeProcessorResult* result) const; //BBS: add modify_count logic int get_modified_count() const {return m_modified_count;} //BBS: add status for whether support used diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index 50c69f40dc..ba205887ec 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp @@ -283,11 +283,12 @@ void BackgroundSlicingProcess::process_fff() m_temp_output_path = this->get_current_plate()->get_tmp_gcode_path(); m_fff_print->export_gcode(m_temp_output_path, m_gcode_result, [this](const ThumbnailsParams& params) { return this->render_thumbnails(params); }); - // Orca: BBL printers post-process the g-code in place here and never re-parse it into a fresh - // GCodeProcessorResult, so m_gcode_result->nozzle_group_result (consumed by the H2C print-dispatch - // nozzle mapping) survives post-processing. No preservation guard is needed on this path. + // Orca: BBL printers post-process the g-code in place here, in the file the G-code viewer maps, so + // the preview re-reads its moves and line offsets from the edited file. The rest of m_gcode_result, + // including nozzle_group_result (consumed by the H2C print-dispatch nozzle mapping), is kept. if (m_fff_print->is_BBL_printer()) { - run_post_process_scripts(m_temp_output_path, false, "File", m_temp_output_path, m_fff_print->full_print_config()); + if (run_post_process_scripts(m_temp_output_path, false, "File", m_temp_output_path, m_fff_print->full_print_config())) + m_fff_print->reload_gcode_moves(m_gcode_result); } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": export gcode finished"); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index aa4b3ca2bb..03bef82d18 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -895,10 +895,16 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, f auto update_lines = [this](uint64_t start_id, uint64_t end_id) { std::vector ret; ret.reserve(end_id - start_id + 1); + // Orca: m_lines_ends indexes into a memory mapping, so it must be clamped to the mapping. If the + // file was modified behind our back (an in-place post-processing script that shrank it), an + // unchecked read is an access violation, which the caller's try/catch cannot catch on Windows. + const size_t file_size = m_file.size(); for (uint64_t id = start_id; id <= end_id; ++id) { // read line from file - const size_t start = id == 1 ? 0 : m_lines_ends[id - 2]; - const size_t original_len = m_lines_ends[id - 1] - start; + // Keep one entry per id: render() indexes m_lines by (id - start_id). + const size_t start = id == 1 ? 0 : std::min(m_lines_ends[id - 2], file_size); + const size_t end = std::min(m_lines_ends[id - 1], file_size); + const size_t original_len = end > start ? end - start : 0; // A character is four bytes at most, so 55 of them always fit in 220. const size_t len = std::min(original_len, (size_t) 55 * 4); std::string gline(m_file.data() + start, len); diff --git a/tests/fff_print/test_gcodeprocessor.cpp b/tests/fff_print/test_gcodeprocessor.cpp index 2d6bafd0a9..4bdc416002 100644 --- a/tests/fff_print/test_gcodeprocessor.cpp +++ b/tests/fff_print/test_gcodeprocessor.cpp @@ -9,6 +9,7 @@ #include "libslic3r/Config.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/Print.hpp" #include "libslic3r/Utils.hpp" #include "test_helpers.hpp" @@ -221,3 +222,62 @@ TEST_CASE("Line ends of the exported G-code mark every newline in the file", "[G INFO("first difference at line " << difference.first - result.lines_ends.begin() + 1); CHECK(difference.first == result.lines_ends.end()); } + +TEST_CASE("Reloaded moves name their lines in G-code a script rewrote in place", "[GCodeProcessor]") +{ + Print print; + Model model; + Test::init_print({ Test::cube(20) }, print, model); + GCodeProcessorResult result; + const std::string gcode = Test::gcode(print, &result); + const auto exported_moves = result.moves; + + // A script that prepends one comment and, writing in text mode on Windows, turns every LF into CRLF. + const std::string prepended = ";EDITED\r\n"; + std::string edited = prepended; + for (const char c : gcode) { + if (c == '\n') + edited += '\r'; + edited += c; + } + ScopedTemporaryFile temp(".gcode"); + save_string_file(temp.path(), edited); + result.filename = temp.string(); + print.reload_gcode_moves(&result); + + std::vector newline_ends; + for (size_t i = edited.find('\n'); i != std::string::npos; i = edited.find('\n', i + 1)) + newline_ends.push_back(i + 1); + CHECK(result.lines_ends == newline_ends); + + // Every move that came from a line now names the same line one further down. + REQUIRE(result.moves.size() == exported_moves.size()); + const auto difference = std::mismatch(exported_moves.begin(), exported_moves.end(), result.moves.begin(), + [](const auto &exported, const auto &reloaded) { + return reloaded.gcode_id == (exported.gcode_id == 0 ? 0 : exported.gcode_id + 1); + }); + INFO("first difference at move " << difference.first - exported_moves.begin()); + CHECK(difference.first == exported_moves.end()); +} + +TEST_CASE("Rewritten G-code that cannot be re-read keeps the moves and hides the G-code window", "[GCodeProcessor]") +{ + Print print; + Model model; + Test::init_print({ Test::cube(20) }, print, model); + GCodeProcessorResult result; + const std::string gcode = Test::gcode(print, &result); + const auto exported_moves = result.moves; + + // A script that strips the trailing config block, which the G-code reader needs. + const size_t config_block = gcode.find("; CONFIG_BLOCK_START"); + REQUIRE(config_block != std::string::npos); + ScopedTemporaryFile temp(".gcode"); + save_string_file(temp.path(), gcode.substr(0, config_block)); + result.filename = temp.string(); + print.reload_gcode_moves(&result); + + CHECK(result.lines_ends.empty()); + REQUIRE(result.moves.size() == exported_moves.size()); + CHECK(result.moves.back().gcode_id == exported_moves.back().gcode_id); +} From b5f5b501572627b3886638f768c9673e13c49a5e Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Fri, 9 Oct 2026 12:36:09 -0300 Subject: [PATCH 2/5] Adaptive TPMS (#16005) Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> --- docs/HLSD/adaptive-tpms-infill.md | 221 ++++++ src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/Fill/Fill.cpp | 39 +- src/libslic3r/Fill/FillBase.hpp | 9 + src/libslic3r/Fill/FillGyroid.cpp | 31 +- src/libslic3r/Fill/FillTpmsAdaptive.cpp | 823 ++++++++++++++++++++ src/libslic3r/Fill/FillTpmsAdaptive.hpp | 143 ++++ src/libslic3r/Fill/FillTpmsD.cpp | 61 +- src/libslic3r/Fill/FillTpmsFK.cpp | 48 +- src/libslic3r/Layer.cpp | 8 +- src/libslic3r/Preset.cpp | 3 + src/libslic3r/Print.hpp | 4 + src/libslic3r/PrintConfig.cpp | 87 +++ src/libslic3r/PrintConfig.hpp | 27 + src/libslic3r/PrintObject.cpp | 37 + src/slic3r/GUI/ConfigManipulation.cpp | 12 +- src/slic3r/GUI/GUI_Factories.cpp | 3 + src/slic3r/GUI/Tab.cpp | 3 + tests/fff_print/test_fill.cpp | 332 ++++++++ tests/libslic3r/CMakeLists.txt | 1 + tests/libslic3r/test_fill_tpms_adaptive.cpp | 292 +++++++ 21 files changed, 2138 insertions(+), 48 deletions(-) create mode 100644 docs/HLSD/adaptive-tpms-infill.md create mode 100644 src/libslic3r/Fill/FillTpmsAdaptive.cpp create mode 100644 src/libslic3r/Fill/FillTpmsAdaptive.hpp create mode 100644 tests/libslic3r/test_fill_tpms_adaptive.cpp diff --git a/docs/HLSD/adaptive-tpms-infill.md b/docs/HLSD/adaptive-tpms-infill.md new file mode 100644 index 0000000000..5fc578f7e2 --- /dev/null +++ b/docs/HLSD/adaptive-tpms-infill.md @@ -0,0 +1,221 @@ +# Adaptive TPMS infill — High Level Design + +## Purpose and scope + +`tpms_adaptive` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK +patterns inside the object: the cells grow continuously from the surface +towards the center of the object. `distance_warp`, `smooth_blend` and +`stepped_shells` follow the distance to the nearest surface, including the top +and bottom, like concentric shells; `lobes` follows the whole 3D shape towards +the center of each lobe of the object; `normal_z`, `normal_y` and `normal_x` +follow each section of the object normal to that axis, so the grading does not +change along the axis, as suits a profile extruded along it. +`sparse_infill_density` is the density at the surface, `tpms_interior_density` +the density at the center, and `tpms_adaptive_gradient` picks how the density +goes from one to the other. Only internal sparse infill is graded; the Gyroid +Z-buckling optimization does not apply to it. + +The design has two parts: a field built once per object, and a pattern made +from it, warped around the center of each lobe of a body so that its cell size +follows the field, or, in the modes following the distance to the surface, +split into shells or blended between densities. + +## Radial field + +`TpmsRadialField` gives every point of an object the center of its lobe and a +radial coordinate: 0 at the center, 1 at the surface along the ray from the +center. `PrintObject::prepare_tpms_radial_fields()` builds it in +`bridge_over_infill()`, next to the adaptive cubic octree, because the anchoring +infill generated there has to match the printed infill. A field is built for +every mode a region uses, and is shared by the regions using that mode: the +field depends on the geometry only, the densities are applied per region in the +fill. An object thinner than the grid cells has no body in the field; no field +is kept then, and the infill falls back to the regular pattern. In the modes +following the distance to the surface, the field also gives the depth of every +point (see below). + +A regular 3D grid of cubic cells is rasterized from the `lslices` of the layers, +so the field follows what is printed: negative volumes, the union of +overlapping parts and holes are taken into account, and the mesh does not need +to be closed. A padding node around the grid is always outside. The grid is +capped at about a million nodes, with cells no smaller than 0.5 mm. + +- Bodies are the connected inside nodes. Each is graded on its own, so separate + parts of one object each get their own sparse center. +- A body is split into lobes around the local maxima of the depth, by an exact + Euclidean distance transform (Felzenszwalb and Huttenlocher, one pass per + axis). Two maxima are in separate lobes when the depth along the segment + between them drops below 0.8 of the shallower one, like at the neck between + two united spheres; maxima shallower than 0.3 of the deepest one are ignored. + A maximum joins the first lobe whose first maximum it sees without a neck. + The lobes are made one at a time, the remaining maxima tested against the + first one in parallel, as a plate has a whole plane of them. + Where the depth ties along a line or a plane, as in a tall box, the lobe's + center is the node nearest to the middle of the tied nodes, so the center is + in the middle of the height and not a column. +- A point belongs to the lobe it is nearest to relative to their depths, so the + side between two lobes is nearer to the smaller one. Near that side, within a + tenth of that relative distance, the patterns of the lobes morph into each + other, so the lines stay continuous. Every lobe in that range takes part, up + to four, so the morph is also continuous where three or four lobes meet. +- The reach of a lobe is the distance from its center to the first exit along + 24 x 48 latitude-longitude directions, smoothed twice over neighbouring + directions in log space. Towards a neighbouring lobe it stops at twice the + distance to the side between them, so that side is graded half way, as deep + as a neck is, rather than as sparse as the center or as dense as the surface. + Only the lobes whose centers are near enough to be nearer at the current + distance are compared along a ray, so many lobes, as in a perforated plate, + stay cheap. + The radial coordinate of a point is its distance to the center over the reach + in its direction. Behind a gap, as across the hole + of a ring, the radial coordinate is above 1 and the infill keeps the surface + density. +- Every outside node belongs to its nearest body, so points near a surface find + their body without a search. With a single body, all nodes belong to it. + +In the 2D modes, every plane of nodes normal to the axis is a field of its own: +the distance transform skips the axis, bodies, lobes and the nearest body are +found within the plane, and the reach is sampled on a circle of 48 directions. +A point is looked up in the two planes around it, the weights of their lobes +interpolated along the axis, so the grading does not step between planes; a +plane without a body uses the nearest one that has one. The planes are a cell +apart, not a layer: where the sections change abruptly, as at a step, the +patterns of the two planes morph into each other over that cell. + +A distance to the nearest surface would be the obvious field, but no smooth map +follows it. By the divergence theorem, the mean scale of a map over a body is +fixed by its values on the surface: a map that keeps the full density along the +whole surface, as the distance would ask under the top and bottom, has the mean +density of the uniform infill, the sparser core being paid for by lines crowding +along the walls. The layers of a plate at different depths would also need +different line spacings in the same directions, which no continuous map allows +without shearing across the plate. Following the distance needs changes of the +topology of the lattice (see below). The radial coordinate instead grades what a +single map can: towards one point. + +## Warped pattern + +The pattern is evaluated on warped coordinates: + + TPMS(f_surface * m(t) * (p - center)) + +where `m` scales the pattern around the center of the lobe: its frequency is +`m + t * m'` along the ray and `m` across it. `m(t)` is the mean of the target +scale over the ball of radius `t`, `3 / t^3 * integral of s^2 * target(s) ds`, so +the mean of the three, and with it the density, follows the gradient. The cells +are round at the center; near the surface they are flattened, with the lines +running parallel to it. Beyond the surface the target is the surface scale, so +the warp extends continuously outside. + +In the 2D modes only the coordinates within the plane are warped, and `m(t)` is +the mean over the disc, `2 / t^2 * integral of s * target(s) ds`. Along the axis +the pattern keeps the interior frequency: scaling it with `m` would shear the +pattern by the distance along the axis times the gradient of `m`, without bound +on a long object. The cells are round at the center and stretched along the +axis near the surface. With Normal Z the layers are graded exactly, since +the lines of a layer follow its in-plane frequencies; normal to X or Y, the +layers near the sides are as dense as the larger of the two frequencies in the +layer, which is the surface one. + +Evaluating a TPMS at a frequency that varies with the position without such a +map distorts it wherever the frequency changes, because the phase also changes +with the gradient of the frequency times the distance from the origin. Fitting a +smooth map to a varying isotropic scale in the least-squares sense (a Poisson +problem per axis) cannot grade strongly: its divergence is the target scale plus +a harmonic function pinned by the surface, which keeps the scale in the core +near two thirds of the surface one. Following a distance exactly needs the +lattice to change its topology, by blending lattices of different densities or +filling shells of equal distance with them, as the modes following the distance +to the surface do. + +The target scale at depth `d = 1 - t`, with `S` the surface and `I` the interior +frequency, both from each pattern's own density calibration: + +| Gradient | Scale | +|-------------|------------------------| +| Linear | `1 + (I / S - 1) * d` | +| Quadratic | `1 + (I / S - 1) * d^2`| +| Exponential | `(I / S)^d` | + +With a denser surface, quadratic keeps the surface density deepest and +exponential drops fastest. A denser interior works the same way. + +The zero level is extracted with marching squares like the regular TPMS-FK, on +a sampling grid fixed in the fill frame like the optimized Gyroid, so that every +region of a layer connects its lines the same way at the saddles of the pattern. +Loops narrower than two lines (shorter than `2 * PI * spacing`) are dropped, as +they would print as blobs. The fill works in a frame rotated by the infill +angle, so the radial field is looked up at the point rotated back into the +object frame, and the center rotated into the fill frame. Both use the middle of +the layer. + +## Modes following the distance to the surface + +`distance_warp`, `smooth_blend` and `stepped_shells` grade by the distance to +the nearest surface, including the top and bottom, as concentric shells do. The +depth of a point is that distance over the distance of the deepest point of its +body, from 0 at the surface to 1; the field keeps it for every node and +interpolates it between them. A tall box so keeps its whole axis as sparse as +its center, and a plate is graded through its thickness. + +No single smooth pattern follows that depth without distortion (see above), so +the three modes trade differently: + +- Distance warp keeps the lobes and the warp of Lobes, but its radial profile + comes from the depth. For every direction of a lobe, the mean depth over the + ball along the ray is sampled at 33 radii up to the reach, then smoothed over + the neighbouring directions like the reach. The radial coordinate is the one + of the linear profile with the same mean depth, `t = 4/3 * (1 - mean depth)`, + so with a linear gradient the mean cell size follows the depth exactly, and + with the others approximately. In a sphere or a cube, where the depth falls + linearly along every ray, it is Lobes. Elsewhere the profile changes with the + direction, and the warp shears where neighbouring directions differ, as in + plates and long bodies; right under the top of a long body the cells are + sparser within the layer, the warp moving their density into the height. + The profiles are smoothed over the directions like the reach, as sharper + ones shear the pattern across the layer, which adds lines. A long body is so + graded partly along its length, between Lobes and the distance. +- Smooth blend evaluates the regular patterns of the two levels around the + target of every point and blends them by a smoothstep over the whole gap + between the levels, here at most 2.5 times apart. The density follows the + depth without steps, but where two lattices blend, part of their lines run + along the blend, so fewer levels print fewer extra lines. From 25% to 5%, + three levels print about 0.45 of the uniform infill in a deep core whose + levels alone would print 0.3; levels 1.5 times apart print 0.6 to 0.7, and a + single blend from the surface to the interior 0.6. +- Stepped shells split each region of a layer into shells and fill every shell + with the regular pattern at its density. The densities are levels from the + surface to the interior density at most 1.5 times apart, five from 25% to 5%, + and a point takes the level nearest to its target on that geometric scale. + The shells are traced by marching squares of the continuous level over the + layer on a 0.5 mm grid fixed in the object, so every region of a layer gets + the same shells, and clipped to the region. Each shell is shrunk by half a + line, like a filled region, and its regular filler connects its lines along + that boundary, so the connections of two neighbouring shells lie side by side + instead of on top of each other. The pattern is never + distorted, but its lines end at every shell, and thin parts get thin shells. + The connections add lines: in the core of a 60 mm cube, about a third more + than the target. + +Stepped shells and Smooth blend need neither lobes nor reaches, which are not +built for them. + +## Constraints + +- With `tpms_adaptive` disabled, or for other patterns, the fill parameters are reset + to their defaults, so they neither change the infill nor split fill batches. +- `Layer::get_sparse_infill_max_void_area()` uses the sparser of the two + densities, as the voids at the center are that large. +- At a sparse infill density of 100% the sparse infill is turned into solid + infill, so there is nothing to grade: the options are hidden and no field is + built. +- The adaptive options invalidate `posPrepareInfill`, which rebuilds the field + and the anchoring infill. +- An elongated body without a neck has one center, so its far ends are graded + as the outer part of the body, and the warp shears where the reach changes + quickly with the direction. A concave body, like an L, may be split into lobes + where its maxima cannot see each other in a straight line. +- Across a ray, the scale is the mean of the gradient from the center, so the + layers right under the top and above the bottom are sparser than the surface + density in their middle, and a plate is graded from its middle outwards rather + than through its thickness. diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index c909d7e96a..ca95cfb364 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -180,6 +180,8 @@ set(lisbslic3r_sources Fill/FillPlanePath.hpp Fill/FillRectilinear.cpp Fill/FillRectilinear.hpp + Fill/FillTpmsAdaptive.cpp + Fill/FillTpmsAdaptive.hpp Fill/FillTpmsD.cpp Fill/FillTpmsD.hpp Fill/FillTpmsFK.cpp diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 2f23917be6..146d3ee7b4 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -311,6 +311,11 @@ struct SurfaceFillParams // For Gyroid: when true, use the parameterized "optimized" wave. bool gyroid_optimized = false; + // For TPMS: grade the density from the surface to the interior of the object. + TpmsAdaptiveMode tpms_adaptive = TpmsAdaptiveMode::Disabled; + float tpms_interior_density = 0.f; + TpmsAdaptiveGradient tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear; + // Orca: corner smoothing factor in the range [0, 1]. double smooth_factor { 0. }; @@ -353,6 +358,9 @@ struct SurfaceFillParams RETURN_COMPARE_NON_EQUAL(skin_infill_depth); RETURN_COMPARE_NON_EQUAL(infill_overhang_angle); RETURN_COMPARE_NON_EQUAL(gyroid_optimized); + RETURN_COMPARE_NON_EQUAL(tpms_adaptive); + RETURN_COMPARE_NON_EQUAL(tpms_interior_density); + RETURN_COMPARE_NON_EQUAL(tpms_adaptive_gradient); RETURN_COMPARE_NON_EQUAL(smooth_factor); RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern); RETURN_COMPARE_NON_EQUAL(separated_infills); @@ -386,6 +394,9 @@ struct SurfaceFillParams this->center_of_surface_pattern == rhs.center_of_surface_pattern && this->separated_infills == rhs.separated_infills && this->gyroid_optimized == rhs.gyroid_optimized && + this->tpms_adaptive == rhs.tpms_adaptive && + this->tpms_interior_density == rhs.tpms_interior_density && + this->tpms_adaptive_gradient == rhs.tpms_adaptive_gradient && this->smooth_factor == rhs.smooth_factor && this->fill_order == rhs.fill_order; } @@ -994,15 +1005,23 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p // Orca: apply fill multiline only for sparse infill params.multiline = params.extrusion_role == erInternalInfill ? int(region_config.fill_multiline) : 1; - // Pass through gyroid_optimized only when the effective pattern is Gyroid, - // so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag - // (which would unnecessarily split fill batching). - // Stored on SurfaceFillParams; copied to FillParams during conversion. - params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized; - // Orca: Likewise separated_infills only where it can move the pattern. + // Orca: Pass through separated_infills only where it can move the pattern. params.separated_infills = region_config.separated_infills && is_separable_infill_pattern(params.pattern) && params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface; + // Orca: only the TPMS sparse infill is graded; reset otherwise, as params is reused. + params.tpms_adaptive = is_tpms_adaptive_pattern(params.pattern) && params.extrusion_role == erInternalInfill ? + region_config.tpms_adaptive.value : TpmsAdaptiveMode::Disabled; + const bool tpms_adaptive = params.tpms_adaptive != TpmsAdaptiveMode::Disabled; + params.tpms_interior_density = tpms_adaptive ? std::max(1.f, float(region_config.tpms_interior_density)) : 0.f; + params.tpms_adaptive_gradient = tpms_adaptive ? region_config.tpms_adaptive_gradient.value : TpmsAdaptiveGradient::Linear; + + // Pass through gyroid_optimized only when the effective pattern is Gyroid, + // so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag + // (which would unnecessarily split fill batching). Adaptive density replaces it. + // Stored on SurfaceFillParams; copied to FillParams during conversion. + params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !tpms_adaptive; + if (params.extrusion_role == erInternalInfill) { params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value, region_config.sparse_infill_rotate_template.value); @@ -1354,6 +1373,7 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh f->angle = surface_fill.params.angle; f->fixed_angle = surface_fill.params.fixed_angle; const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr; + f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive); f->print_config = &this->object()->print()->config(); f->print_object_config = &this->object()->config(); if (surface_fill.params.pattern == ipConcentricInternal) { @@ -1403,6 +1423,9 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2; params.infill_overhang_angle = surface_fill.params.infill_overhang_angle; params.gyroid_optimized = surface_fill.params.gyroid_optimized; + params.tpms_adaptive = surface_fill.params.tpms_adaptive; + params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density); + params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient; params.smooth_factor = surface_fill.params.smooth_factor; // BBS @@ -1575,6 +1598,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti f->angle = surface_fill.params.angle; f->fixed_angle = surface_fill.params.fixed_angle; const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr; + f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive); f->print_config = &this->object()->print()->config(); f->print_object_config = &this->object()->config(); @@ -1614,6 +1638,9 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti params.infill_overhang_angle = surface_fill.params.infill_overhang_angle; params.multiline = surface_fill.params.multiline; params.gyroid_optimized = surface_fill.params.gyroid_optimized; + params.tpms_adaptive = surface_fill.params.tpms_adaptive; + params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density); + params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient; params.smooth_factor = surface_fill.params.smooth_factor; // Orca: Match make_fills() when choosing the origin of plane-path patterns. // Without the sparse extrusion role, the filler uses each surface's bounds diff --git a/src/libslic3r/Fill/FillBase.hpp b/src/libslic3r/Fill/FillBase.hpp index 45dcfc87e8..e0308c9450 100644 --- a/src/libslic3r/Fill/FillBase.hpp +++ b/src/libslic3r/Fill/FillBase.hpp @@ -33,6 +33,7 @@ namespace Slic3r { class ExtrusionEntityCollection; } namespace Slic3r { class Surface; +class TpmsRadialField; enum InfillPattern : int; namespace FillAdaptive { @@ -91,6 +92,11 @@ struct FillParams // For Gyroid: when true, use the parameterized "optimized" variant. bool gyroid_optimized { false }; + // For TPMS: grade the density from the surface to the interior of the object. Density fraction. + TpmsAdaptiveMode tpms_adaptive { TpmsAdaptiveMode::Disabled }; + float tpms_interior_density { 0.f }; + TpmsAdaptiveGradient tpms_adaptive_gradient { TpmsAdaptiveGradient::Linear }; + // Orca: corner smoothing factor in the range [0, 1]. double smooth_factor { 0. }; @@ -155,6 +161,9 @@ public: // Octree builds on mesh for usage in the adaptive cubic infill FillAdaptive::Octree* adapt_fill_octree = nullptr; + // Radial coordinate inside the object for the adaptive TPMS infill + const TpmsRadialField* tpms_radial_field = nullptr; + // PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring). // Orca: also used by gap fill function. const PrintConfig *print_config = nullptr; diff --git a/src/libslic3r/Fill/FillGyroid.cpp b/src/libslic3r/Fill/FillGyroid.cpp index 3b2803738c..06cb55f70e 100644 --- a/src/libslic3r/Fill/FillGyroid.cpp +++ b/src/libslic3r/Fill/FillGyroid.cpp @@ -17,6 +17,17 @@ #include "libslic3r/Polyline.hpp" #include "FillGyroid.hpp" #include "libslic3r/Polygon.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "FillTpmsAdaptive.hpp" + +namespace Slic3r { + +static float gyroid(float x, float y, float z) +{ + return std::sin(x) * std::cos(y) + std::sin(y) * std::cos(z) + std::sin(z) * std::cos(x); +} + +} // namespace Slic3r // --------------------------------------------------------------------------- // Marching-squares scalar field for the optimized gyroid branch. @@ -62,10 +73,7 @@ struct GyroidField float get_scalar(coordf_t x, coordf_t y, coordf_t z_arg) const { - const float a = fx * float(x); - const float b = fy * float(y); - const float c = fz * float(z_arg); - return std::sin(a) * std::cos(b) + std::sin(b) * std::cos(c) + std::sin(c) * std::cos(a); + return gyroid(fx * float(x), fy * float(y), fz * float(z_arg)); } float get_scalar(Coord p) const @@ -307,6 +315,14 @@ void FillGyroid::_fill_surface_single( ExPolygon expolygon, Polylines &polylines_out) { + if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) { + fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing, + [&](const FillParams &shell_params, const ExPolygon &shell) { + this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out); + }); + return; + } + auto infill_angle = float(this->angle + (CorrectionAngle * 2*M_PI) / 360.); if(std::abs(infill_angle) >= EPSILON) expolygon.rotate(-infill_angle); @@ -326,7 +342,12 @@ void FillGyroid::_fill_surface_single( // generate pattern Polylines polylines; - if (params.gyroid_optimized) { + if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) { + // Radians per mm of the regular pattern at a density. + auto frequency = [¶ms, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); }; + polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient}, + *this->tpms_radial_field, bb, this->z, params.layer_height, this->spacing, infill_angle); + } else if (params.gyroid_optimized) { // Marching-squares path on the gyroid implicit field. Base period matches // the standard parametric path's wavelength: 2*pi * spacing / density_adj. // omega >= 1 always, so fz >= baseline -> shorter vertical wavelength -> diff --git a/src/libslic3r/Fill/FillTpmsAdaptive.cpp b/src/libslic3r/Fill/FillTpmsAdaptive.cpp new file mode 100644 index 0000000000..2dcd2930b9 --- /dev/null +++ b/src/libslic3r/Fill/FillTpmsAdaptive.cpp @@ -0,0 +1,823 @@ +#include "FillTpmsAdaptive.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "../BoundingBox.hpp" +#include "../ClipperUtils.hpp" +#include "../ExPolygon.hpp" +#include "FillBase.hpp" +#include "../Execution/ExecutionTBB.hpp" +#include "../MarchingSquares.hpp" +#include "../Point.hpp" +#include "../Polygon.hpp" +#include "../Polyline.hpp" +#include "../PrintConfig.hpp" +#include "../libslic3r.h" + +namespace Slic3r { + +namespace { + +// At most 4 MB of body indices; finer cells would not change the grading. +constexpr double MaxNodes = double(1 << 20); +constexpr double MinCellSize = 0.5; + +// Two deepest points are in separate lobes when the depth between them drops below this ratio of the shallower one. +constexpr double NeckRatio = 0.8; +// Lobes shallower than this ratio of the deepest one of their body are graded as part of it. +constexpr double MinLobeRatio = 0.3; +// A lobe reaches twice as far as the side towards its neighbour, so that the side is half way to the surface. +constexpr double LobeReach = 2.; +// Width of the morph between the patterns of two lobes, in their distance to the center over its depth. +constexpr double LobeMorph = 0.1; +// Densities of the levels of Stepped shells, and of Smooth blend, are at most these ratios apart. Fewer levels blend +// with fewer lines running along the blends. +constexpr double ShellRatio = 1.5; +constexpr double BlendRatio = 2.5; +// Distance warp: samples of the mean depth along a ray. +constexpr int Samples = 32; + +constexpr float InfF = std::numeric_limits::infinity(); +constexpr double InfD = std::numeric_limits::infinity(); + +// Squared distance transform of a line (Felzenszwalb & Huttenlocher); infinite samples are no sites. +void distance_transform_line(const float *f, float *d, int n, int *v, double *s) +{ + int k = -1; + for (int q = 0; q < n; ++q) { + if (f[q] == InfF) + continue; + double x = -InfD; + while (k >= 0) { + x = (f[q] + double(q) * q - f[v[k]] - double(v[k]) * v[k]) / (2. * (q - v[k])); + if (x > s[k]) + break; + --k; + } + if (k < 0) + x = -InfD; + v[++k] = q; + s[k] = x; + s[k + 1] = InfD; + } + if (k < 0) { + std::fill(d, d + n, InfF); + return; + } + for (int q = 0, j = 0; q < n; ++q) { + while (s[j + 1] < q) + ++j; + d[q] = float(sqr(double(q - v[j])) + f[v[j]]); + } +} + +void distance_transform_axis(std::vector &grid, const Vec3i32 &size, int axis, const std::function &throw_if_canceled) +{ + const int n = size[axis]; + const int a1 = (axis + 1) % 3; + const int a2 = (axis + 2) % 3; + const size_t stride = axis == 0 ? 1 : axis == 1 ? size_t(size.x()) : size_t(size.x()) * size.y(); + tbb::parallel_for(tbb::blocked_range(0, size_t(size[a1]) * size[a2]), [&](const tbb::blocked_range &range) { + std::vector f(n), d(n); + std::vector v(n); + std::vector s(n + 1); + for (size_t line = range.begin(); line < range.end(); ++line) { + Vec3i32 idx; + idx[axis] = 0; + idx[a1] = int(line % size[a1]); + idx[a2] = int(line / size[a1]); + const size_t first = (size_t(idx.z()) * size.y() + idx.y()) * size.x() + idx.x(); + for (int i = 0; i < n; ++i) + f[i] = grid[first + i * stride]; + distance_transform_line(f.data(), d.data(), n, v.data(), s.data()); + for (int i = 0; i < n; ++i) + grid[first + i * stride] = d[i]; + } + throw_if_canceled(); + }); +} + +// Marks the nodes inside the expolygons with infinity, by even-odd scanlines. +void rasterize(const ExPolygons &expolygons, const Vec2d &origin, double cell, int nx, int ny, float *nodes) +{ + std::vector> crossings(ny); + auto add_crossings = [&](const Polygon &polygon) { + const Points &pts = polygon.points; + for (size_t i = 0; i < pts.size(); ++i) { + const Vec2d a = unscaled(pts[i]); + const Vec2d b = unscaled(pts[i + 1 == pts.size() ? 0 : i + 1]); + if (a.y() == b.y()) + continue; + const auto [lo, hi] = std::minmax(a.y(), b.y()); + const int j0 = std::max(0, int(std::ceil((lo - origin.y()) / cell))); + const int j1 = std::min(ny, int(std::ceil((hi - origin.y()) / cell))); + for (int j = j0; j < j1; ++j) { + const double y = origin.y() + j * cell; + crossings[j].push_back(a.x() + (b.x() - a.x()) * (y - a.y()) / (b.y() - a.y())); + } + } + }; + for (const ExPolygon &expolygon : expolygons) { + add_crossings(expolygon.contour); + for (const Polygon &hole : expolygon.holes) + add_crossings(hole); + } + for (int j = 0; j < ny; ++j) { + std::vector &xs = crossings[j]; + std::sort(xs.begin(), xs.end()); + for (size_t k = 0; k + 1 < xs.size(); k += 2) { + const int i0 = std::max(0, int(std::ceil((xs[k] - origin.x()) / cell))); + const int i1 = std::min(nx, int(std::ceil((xs[k + 1] - origin.x()) / cell))); + std::fill(nodes + size_t(j) * nx + std::min(i0, i1), nodes + size_t(j) * nx + i1, InfF); + } + } +} + +// Scale of the pattern relative to the surface at a depth from 0 at the surface to 1 at the deepest point. +double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth) +{ + switch (gradient) { + case TpmsAdaptiveGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth; + case TpmsAdaptiveGradient::Exponential: return std::pow(ratio, depth); + default: return 1. + (ratio - 1.) * depth; + } +} + +// Levels of Stepped shells and Smooth blend, geometric from the surface at 0 to the interior at count, and the +// continuous level of the target of a depth. +struct DensityLevels +{ + DensityLevels(double ratio, double step, TpmsAdaptiveGradient gradient) + : ratio(ratio), gradient(gradient), count(int(std::ceil(std::abs(std::log(ratio)) / std::log(step) - EPSILON))) + {} + + double scale(int level) const { return count == 0 ? 1. : std::pow(ratio, double(level) / count); } + double level(double depth) const + { + return count == 0 ? 0. : count * std::log(target_scale(ratio, gradient, depth)) / std::log(ratio); + } + + double ratio; + TpmsAdaptiveGradient gradient; + int count; +}; + +// Scale of the pattern around the center at a radial coordinate t. The mean cell scale over the ball of radius t, +// t^-3 * integral of 3 t'^2 * target(t'), or over the disc in 2D, follows the gradient; beyond the surface the target +// is the surface scale. +class RadialScale +{ +public: + RadialScale(const AdaptiveTpms &tpms, int dimensions) : m_dimensions(dimensions) + { + const double ratio = std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3); + auto target = [&tpms, ratio](double depth) { return target_scale(ratio, tpms.gradient, depth); }; + m_scale[0] = target(1.); + double volume = 0.; + for (size_t i = 1; i < m_scale.size(); ++i) { + const double t0 = double(i - 1) / double(m_scale.size() - 1); + const double t1 = double(i) / double(m_scale.size() - 1); + volume += (std::pow(t1, m_dimensions) - std::pow(t0, m_dimensions)) * target(1. - 0.5 * (t0 + t1)); + m_scale[i] = volume / std::pow(t1, m_dimensions); + } + } + + double operator()(double t) const + { + if (t >= 1.) { + const double volume = std::pow(t, m_dimensions); + return (m_scale.back() + volume - 1.) / volume; + } + const double x = t * double(m_scale.size() - 1); + const size_t i = std::min(size_t(x), m_scale.size() - 2); + return m_scale[i] + (m_scale[i + 1] - m_scale[i]) * (x - double(i)); + } + +private: + int m_dimensions; + std::array m_scale; +}; + +} // namespace + +TpmsRadialField::TpmsRadialField(const std::vector &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode, + const std::function &throw_if_canceled) + : m_mode(mode) + , m_axis(mode == TpmsAdaptiveMode::NormalX ? 0 : mode == TpmsAdaptiveMode::NormalY ? 1 : mode == TpmsAdaptiveMode::NormalZ ? 2 : -1) +{ + assert(!slices.empty() && mode != TpmsAdaptiveMode::Disabled); + const Vec3d min(unscaled(bbox.min.x()), unscaled(bbox.min.y()), slices.front().bottom_z); + const Vec3d extent = Vec3d(unscaled(bbox.max.x()), unscaled(bbox.max.y()), slices.back().top_z) - min; + + // Padded by a node on each side, so that the border of the grid is outside. + m_cell = std::max(MinCellSize, std::cbrt(extent.prod() / MaxNodes)); + auto nodes = [this](double length) { return int(std::ceil(length / m_cell)) + 3; }; + while (double(nodes(extent.x())) * nodes(extent.y()) * nodes(extent.z()) > MaxNodes) + m_cell *= 1.1; + m_size = Vec3i32(nodes(extent.x()), nodes(extent.y()), nodes(extent.z())); + m_origin = min - Vec3d::Constant(m_cell); + + const size_t sy = size_t(m_size.x()); + const size_t sz = sy * m_size.y(); + std::vector depth(sz * m_size.z(), 0.f); + tbb::parallel_for(tbb::blocked_range(0, m_size.z()), [&](const tbb::blocked_range &range) { + for (int k = range.begin(); k < range.end(); ++k) { + const double z = m_origin.z() + k * m_cell; + auto it = std::lower_bound(slices.begin(), slices.end(), z, [](const Slice &s, double z) { return s.top_z < z; }); + if (it != slices.end() && z > it->bottom_z) + rasterize(*it->expolygons, m_origin.head<2>(), m_cell, m_size.x(), m_size.y(), depth.data() + k * sz); + } + throw_if_canceled(); + }); + for (int axis = 0; axis < 3; ++axis) + if (axis != m_axis) + distance_transform_axis(depth, m_size, axis, throw_if_canceled); + + auto position = [this, sy, sz](size_t i) { + return Vec3d(m_origin + m_cell * Vec3d(double(i % sy), double(i / sy % m_size.y()), double(i / sz))); + }; + const std::array steps{1, -1, std::ptrdiff_t(sy), -std::ptrdiff_t(sy), std::ptrdiff_t(sz), -std::ptrdiff_t(sz)}; + + auto node_of = [this, sy, sz](const Vec3d &pt) -> std::ptrdiff_t { + const Vec3d f = (pt - m_origin) / m_cell; + const long x = std::lround(f.x()), y = std::lround(f.y()), z = std::lround(f.z()); + if (x < 0 || y < 0 || z < 0 || x >= m_size.x() || y >= m_size.y() || z >= m_size.z()) + return -1; + return std::ptrdiff_t(size_t(z) * sz + size_t(y) * sy + size_t(x)); + }; + // In the 2D modes, the steps within a section. + auto in_section = [this](size_t step) { return int(step / 2) != m_axis; }; + std::vector neighbours; + for (int dz = -1; dz <= 1; ++dz) + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if ((dx != 0 || dy != 0 || dz != 0) && (m_axis < 0 || Vec3i32(dx, dy, dz)[m_axis] == 0)) + neighbours.push_back(std::ptrdiff_t(dz) * std::ptrdiff_t(sz) + std::ptrdiff_t(dy) * std::ptrdiff_t(sy) + dx); + + // Bodies are the connected inside nodes, none of which is on the border. The deepest nodes of a body are the + // centers of its lobes, unless the depth between them stays above NeckRatio; where the depth ties, the center + // is the node nearest to the middle of the tied nodes. + const bool lobes = m_mode != TpmsAdaptiveMode::SteppedShells && m_mode != TpmsAdaptiveMode::SmoothBlend; + m_body.assign(depth.size(), -1); + std::vector body_nodes; + for (size_t seed = 0; seed < depth.size(); ++seed) { + if (depth[seed] == 0.f || m_body[seed] >= 0) + continue; + const int id = int(m_bodies.size()); + body_nodes.assign(1, seed); + m_body[seed] = id; + float max_depth = 0.f; + for (size_t k = 0; k < body_nodes.size(); ++k) { + max_depth = std::max(max_depth, depth[body_nodes[k]]); + for (size_t s = 0; s < steps.size(); ++s) + if (const size_t j = body_nodes[k] + steps[s]; in_section(s) && depth[j] > 0.f && m_body[j] < 0) { + m_body[j] = id; + body_nodes.push_back(j); + } + } + m_bodies.push_back({m_lobes.size(), 0, (std::sqrt(double(max_depth)) - 0.5) * m_cell}); + if (!lobes) + continue; + + std::vector peaks; + for (size_t i : body_nodes) + if (depth[i] >= sqr(MinLobeRatio) * max_depth && + std::all_of(neighbours.begin(), neighbours.end(), [&](std::ptrdiff_t n) { return depth[i + n] <= depth[i]; })) + peaks.push_back(i); + std::sort(peaks.begin(), peaks.end(), [&depth](size_t a, size_t b) { return depth[a] > depth[b] || (depth[a] == depth[b] && a < b); }); + auto necked = [&](size_t a, size_t b) { + const Vec3d pa = position(a), pb = position(b); + const double limit = sqr(NeckRatio) * std::min(depth[a], depth[b]); + const int samples = int(std::ceil((pb - pa).norm() / (0.5 * m_cell))); + for (int s = 1; s < samples; ++s) + if (depth[node_of(pa + (pb - pa) * (double(s) / samples))] < limit) + return true; + return false; + }; + // A peak joins the first lobe it sees without a neck, if it is as deep. The lobes are made one at a time, + // from the first remaining peak, testing the others in parallel. + std::vector> ties; + while (!peaks.empty()) { + const size_t front = peaks.front(); + std::vector joins(peaks.size(), 0); + tbb::parallel_for(tbb::blocked_range(1, peaks.size()), [&](const tbb::blocked_range &range) { + for (size_t k = range.begin(); k < range.end(); ++k) + if (!necked(front, peaks[k])) + joins[k] = std::sqrt(depth[peaks[k]]) >= std::sqrt(depth[front]) - 1.f ? 1 : 2; + }); + std::vector &tied = ties.emplace_back(1, front); + std::vector remaining; + for (size_t k = 1; k < peaks.size(); ++k) + if (joins[k] == 0) + remaining.push_back(peaks[k]); + else if (joins[k] == 1) + tied.push_back(peaks[k]); + peaks = std::move(remaining); + } + m_bodies.back().lobes = ties.size(); + for (const std::vector &tied : ties) { + Vec3d middle(0., 0., 0.); + for (size_t i : tied) + middle += position(i); + middle /= double(tied.size()); + Vec3d center = position(tied.front()); + for (size_t i : tied) + if ((position(i) - middle).squaredNorm() < (center - middle).squaredNorm()) + center = position(i); + m_lobes.push_back({center, (std::sqrt(double(depth[tied.front()])) - 0.5) * m_cell, {}}); + } + } + throw_if_canceled(); + + if (m_mode == TpmsAdaptiveMode::SteppedShells || m_mode == TpmsAdaptiveMode::SmoothBlend || m_mode == TpmsAdaptiveMode::DistanceWarp) { + m_depth.assign(depth.size(), 0.f); + for (size_t i = 0; i < depth.size(); ++i) + if (m_body[i] >= 0) + m_depth[i] = float(std::min(1., std::max(0., std::sqrt(double(depth[i])) - 0.5) * m_cell / m_bodies[m_body[i]].depth)); + } + + // The reach of a lobe is the first exit along each direction from its center, smoothed over the directions. + // It is shortened towards a neighbouring lobe, from where the point is nearer to the other lobe relative to their depths. + std::vector lobe_body(m_lobes.size()); + for (size_t id = 0; id < m_bodies.size(); ++id) + std::fill_n(lobe_body.begin() + m_bodies[id].first_lobe, m_bodies[id].lobes, int(id)); + const int rows = this->directions() / Azimuth; + auto direction = [this](int i, int j) { + const double azimuth = j * 2. * PI / Azimuth; + Vec3d dir = Vec3d::Zero(); + if (m_axis < 0) { + const double polar = (i + 0.5) * PI / Polar; + dir = Vec3d(std::sin(polar) * std::cos(azimuth), std::sin(polar) * std::sin(azimuth), std::cos(polar)); + } else { + dir[(m_axis + 1) % 3] = std::cos(azimuth); + dir[(m_axis + 2) % 3] = std::sin(azimuth); + } + return dir; + }; + // Two passes of a box filter over the neighbouring directions, for each of the values of a direction. + auto smooth = [rows](std::vector &values, size_t count) { + for (int pass = 0; pass < 2; ++pass) { + std::vector smoothed(values.size(), 0.); + for (int i = 0; i < rows; ++i) + for (int j = 0; j < Azimuth; ++j) + for (size_t k = 0; k < count; ++k) { + double &sum = smoothed[size_t(i * Azimuth + j) * count + k]; + for (int di = -1; di <= 1; ++di) + for (int dj = -1; dj <= 1; ++dj) + sum += values[size_t(std::clamp(i + di, 0, rows - 1) * Azimuth + (j + dj + Azimuth) % Azimuth) * count + k]; + sum /= 9.; + } + values = std::move(smoothed); + } + }; + tbb::parallel_for(tbb::blocked_range(0, m_lobes.size()), [&](const tbb::blocked_range &range) { + for (size_t l = range.begin(); l < range.end(); ++l) { + const int id = lobe_body[l]; + const Body &body = m_bodies[id]; + Lobe &lobe = m_lobes[l]; + // The other lobes of the body, by the distance from the center beyond which they may be nearer. + std::vector> others; + for (size_t k = body.first_lobe; k < body.first_lobe + body.lobes; ++k) + if (k != l) + others.emplace_back((m_lobes[k].center - lobe.center).norm() / (1. + m_lobes[k].depth / lobe.depth), k); + std::sort(others.begin(), others.end()); + auto nearer_lobe = [&](const Vec3d &pt, double r) { + for (auto it = others.begin(); it != others.end() && it->first < r; ++it) + if ((pt - m_lobes[it->second].center).norm() / m_lobes[it->second].depth < r / lobe.depth) + return true; + return false; + }; + const double step = 0.5 * m_cell; + std::vector log_reach(this->directions()); + for (int i = 0; i < rows; ++i) + for (int j = 0; j < Azimuth; ++j) { + const Vec3d dir = direction(i, j); + double r = 0.; + double limit = InfD; + for (;;) { + const Vec3d pt = lobe.center + (r + step) * dir; + const std::ptrdiff_t n = node_of(pt); + if (n < 0 || depth[n] == 0.f || m_body[n] != id || r + step >= limit) + break; + if (limit == InfD && nearer_lobe(pt, r + step)) + limit = LobeReach * (r + step); + r += step; + } + log_reach[i * Azimuth + j] = std::log(std::min(r + 0.5 * step, limit)); + } + smooth(log_reach, 1); + lobe.reach.resize(log_reach.size()); + std::transform(log_reach.begin(), log_reach.end(), lobe.reach.begin(), [](double v) { return float(std::exp(v)); }); + + if (m_mode == TpmsAdaptiveMode::DistanceWarp) { + std::vector mean(log_reach.size() * (Samples + 1)); + for (size_t d = 0; d < log_reach.size(); ++d) { + const Vec3d dir = direction(int(d) / Azimuth, int(d) % Azimuth); + double integral = 0.; + double previous = this->depth(lobe.center); + mean[d * (Samples + 1)] = previous; + for (int k = 1; k <= Samples; ++k) { + // Exact for a depth linear between the samples, a + b * tau, weighted by tau^2. + const double t0 = double(k - 1) / Samples; + const double t1 = double(k) / Samples; + const double d1 = this->depth(lobe.center + t1 * lobe.reach[d] * dir); + const double b = (d1 - previous) * Samples; + const double a = previous - b * t0; + integral += a * (std::pow(t1, 3) - std::pow(t0, 3)) / 3. + b * (std::pow(t1, 4) - std::pow(t0, 4)) / 4.; + previous = d1; + mean[d * (Samples + 1) + k] = 3. * integral / std::pow(t1, 3); + } + } + // Smoothed like the reach: sharper profiles shear the pattern across the layer, adding lines. + smooth(mean, Samples + 1); + lobe.mean_depth.assign(mean.begin(), mean.end()); + } + } + throw_if_canceled(); + }); + + // Every other node belongs to its nearest body, within its section in the 2D modes. + if (m_axis >= 0) { + std::vector has_body(m_size[m_axis], false); + for (size_t i = 0; i < m_body.size(); ++i) + if (m_body[i] >= 0) + has_body[m_axis == 0 ? i % sy : m_axis == 1 ? i / sy % m_size.y() : i / sz] = true; + m_section.assign(m_size[m_axis], -1); + for (int k = 0; k < m_size[m_axis]; ++k) + for (int d = 0; d < m_size[m_axis] && m_section[k] < 0; ++d) + if (k - d >= 0 && has_body[k - d]) + m_section[k] = k - d; + else if (k + d < m_size[m_axis] && has_body[k + d]) + m_section[k] = k + d; + } + // Stepped shells and Smooth blend only look up the depth. + if (!lobes) { + m_body = {}; + return; + } + if (m_bodies.size() == 1) { + std::fill(m_body.begin(), m_body.end(), 0); + return; + } + std::deque queue; + for (size_t i = 0; i < m_body.size(); ++i) + if (m_body[i] >= 0) + queue.push_back(i); + while (!queue.empty()) { + const size_t i = queue.front(); + queue.pop_front(); + const size_t x = i % sy, y = i / sy % m_size.y(), z = i / sz; + const std::array valid{x + 1 < sy, x > 0, y + 1 < size_t(m_size.y()), y > 0, z + 1 < size_t(m_size.z()), z > 0}; + for (size_t k = 0; k < steps.size(); ++k) + if (valid[k] && in_section(k) && m_body[i + steps[k]] < 0) { + m_body[i + steps[k]] = m_body[i]; + queue.push_back(i + steps[k]); + } + } +} + +double TpmsRadialField::depth(const Vec3d &pt) const +{ + assert(!m_depth.empty()); + const Vec3d f = (pt - m_origin) / m_cell; + std::array n0; + std::array w; + for (int a = 0; a < 3; ++a) { + n0[a] = std::clamp(int(std::floor(f[a])), 0, m_size[a] - 2); + w[a] = std::clamp(f[a] - n0[a], 0., 1.); + } + const size_t sy = size_t(m_size.x()); + const size_t sz = sy * size_t(m_size.y()); + double value = 0.; + for (int c = 0; c < 8; ++c) { + const size_t n = size_t(n0[2] + (c >> 2)) * sz + size_t(n0[1] + (c >> 1 & 1)) * sy + size_t(n0[0] + (c & 1)); + value += (c & 1 ? w[0] : 1. - w[0]) * (c >> 1 & 1 ? w[1] : 1. - w[1]) * (c >> 2 ? w[2] : 1. - w[2]) * m_depth[n]; + } + return value; +} + +Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d ¢er) const +{ + Vec3d d = pt - center; + if (m_axis >= 0) + d[m_axis] = 0.; + return d; +} + +double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const +{ + const Vec3d d = this->offset(pt, lobe.center); + const double r = d.norm(); + // Distance warp: the radial coordinate of the linear profile with the same mean depth, which is 1 - 3/4 of it. + auto warp = [](double mean_depth) { return std::max(0., 4. / 3. * (1. - mean_depth)); }; + if (r < EPSILON) + return lobe.mean_depth.empty() ? 0. : warp(lobe.mean_depth.front()); + int i = 0; + double fi = 0.; + double azimuth; + if (m_axis < 0) { + const double polar = std::clamp(std::acos(std::clamp(d.z() / r, -1., 1.)) / PI * Polar - 0.5, 0., double(Polar - 1)); + i = std::min(int(polar), Polar - 2); + fi = polar - i; + azimuth = std::atan2(d.y(), d.x()); + } else + azimuth = std::atan2(d[(m_axis + 2) % 3], d[(m_axis + 1) % 3]); + azimuth *= Azimuth / (2. * PI); + if (azimuth < 0.) + azimuth += Azimuth; + const int j0 = int(azimuth) % Azimuth; + const int j1 = (j0 + 1) % Azimuth; + const double fj = azimuth - std::floor(azimuth); + auto at = [&lobe](int i, int j) { return double(lobe.reach[i * Azimuth + j]); }; + double reach = at(i, j0) * (1. - fj) + at(i, j1) * fj; + if (fi > 0.) + reach = reach * (1. - fi) + (at(i + 1, j0) * (1. - fj) + at(i + 1, j1) * fj) * fi; + const double tau = r / reach; + if (lobe.mean_depth.empty()) + return tau; + // Beyond the surface, the depth is zero. + auto mean_at = [&lobe, tau](int i, int j) { + const float *mean = lobe.mean_depth.data() + size_t(i * Azimuth + j) * (Samples + 1); + if (tau >= 1.) + return double(mean[Samples]) / (tau * tau * tau); + const double x = tau * Samples; + const int k = std::min(int(x), Samples - 1); + return mean[k] + (mean[k + 1] - mean[k]) * (x - k); + }; + double mean = mean_at(i, j0) * (1. - fj) + mean_at(i, j1) * fj; + if (fi > 0.) + mean = mean * (1. - fi) + (mean_at(i + 1, j0) * (1. - fj) + mean_at(i + 1, j1) * fj) * fi; + return warp(mean); +} + +size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const +{ + assert(!this->empty()); + Vec3i32 idx; + for (int axis = 0; axis < 3; ++axis) + idx[axis] = std::clamp(int(std::lround((pt[axis] - m_origin[axis]) / m_cell)), 0, m_size[axis] - 1); + auto node = [this](const Vec3i32 &idx) { return (size_t(idx.z()) * m_size.y() + idx.y()) * m_size.x() + idx.x(); }; + if (m_axis < 0) + return this->body_radial(node(idx), pt, 1.f, out.data()); + + // The sections around pt, or the nearest ones with a body. + const double f = std::clamp((pt[m_axis] - m_origin[m_axis]) / m_cell, 0., double(m_size[m_axis] - 1)); + const int k = std::min(int(f), m_size[m_axis] - 2); + const float w = float(f - k); + size_t count = 0; + if (w < 1.f) { + idx[m_axis] = m_section[k]; + count += this->body_radial(node(idx), pt, 1.f - w, out.data()); + } + if (w > 0.f) { + idx[m_axis] = m_section[k + 1]; + count += this->body_radial(node(idx), pt, w, out.data() + count); + } + return count; +} + +size_t TpmsRadialField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const +{ + const Body &body = m_bodies[m_body[node]]; + if (body.lobes == 1) { + const Lobe &lobe = m_lobes[body.first_lobe]; + out[0] = {lobe.center, radial(lobe, pt), weight}; + return 1; + } + + // The lobes nearest relative to their depth; they morph into each other near the sides where they are as near. + std::array, MaxMorph> nearest; + size_t count = 0; + for (size_t l = body.first_lobe; l < body.first_lobe + body.lobes; ++l) { + const double d = this->offset(pt, m_lobes[l].center).norm() / m_lobes[l].depth; + if (count < MaxMorph) + nearest[count++] = {d, l}; + else if (d < nearest.back().first) + nearest.back() = {d, l}; + else + continue; + for (size_t k = count - 1; k > 0 && nearest[k].first < nearest[k - 1].first; --k) + std::swap(nearest[k], nearest[k - 1]); + } + std::array blend; + double total = 0.; + size_t morphs = 0; + for (; morphs < count; ++morphs) { + const double u = 0.5 - (nearest[morphs].first - nearest[0].first) / LobeMorph; + if (u <= 0.) + break; + blend[morphs] = u * u * (3. - 2. * u); + total += blend[morphs]; + } + for (size_t k = 0; k < morphs; ++k) { + const Lobe &lobe = m_lobes[nearest[k].second]; + out[k] = {lobe.center, radial(lobe, pt), float(weight * blend[k] / total)}; + } + return morphs; +} + +} // namespace Slic3r + +namespace marchsq { +using namespace Slic3r; + +struct AdaptiveTpmsField +{ + static constexpr float gsizef = 0.40f; // grid cell size in mm (roughly line segment length). + static constexpr float rsizef = 0.004f; // raster pixel size in mm (roughly point accuracy). + const coord_t rsize = scaled(rsizef); + const long gsize = std::lround(gsizef / rsizef); + + const AdaptiveTpms &tpms; + const TpmsRadialField &radial_field; + RadialScale scale; + DensityLevels levels; + Point size; + Point offs; + double z; + double cos_angle; + double sin_angle; + + AdaptiveTpmsField(const AdaptiveTpms &tpms, const TpmsRadialField &radial_field, const BoundingBox &bbox, coordf_t z, float angle) + : tpms(tpms), radial_field(radial_field), scale(tpms, radial_field.axis() < 0 ? 3 : 2) + , levels(std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3), BlendRatio, tpms.gradient) + , size(bbox.size()), offs(bbox.min), z(z) + , cos_angle(std::cos(angle)), sin_angle(std::sin(angle)) + {} + + // The pattern is scaled around the center of the lobe, morphing into the pattern of a neighbouring lobe near the + // side between them. In the 2D modes only within the section, with the interior frequency along the axis. + // The radial field is in the object frame, the fill is rotated by -angle. + float get_scalar(const Coord &p) const + { + const Point pt = to_Point(p); + const double x = unscaled(pt.x()); + const double y = unscaled(pt.y()); + const Vec3d obj(cos_angle * x - sin_angle * y, sin_angle * x + cos_angle * y, z); + if (radial_field.mode() == TpmsAdaptiveMode::SmoothBlend) { + // The regular patterns of the two levels around the target of the depth, blended by a smoothstep. + auto lattice = [this, x, y](int level) { + const double frequency = tpms.surface_frequency * levels.scale(level); + return tpms.equation(float(frequency * x), float(frequency * y), float(frequency * z)); + }; + if (levels.count == 0) + return lattice(0); + const double c = std::clamp(levels.level(radial_field.depth(obj)), 0., double(levels.count)); + const int k = std::min(int(c), levels.count - 1); + const double u = c - k; + const double w = u * u * (3. - 2. * u); + return float((w < 1. ? (1. - w) * lattice(k) : 0.) + (w > 0. ? w * lattice(k + 1) : 0.)); + } + const int axis = radial_field.axis(); + TpmsRadialField::Radials radials; + const size_t count = radial_field.radial(obj, radials); + float value = 0.f; + for (size_t i = 0; i < count; ++i) { + const auto &[center, t, weight] = radials[i]; + Vec3d q = tpms.surface_frequency * scale(t) * (obj - center); + if (axis >= 0) + q[axis] = tpms.interior_frequency * obj[axis]; + value += weight * tpms.equation(float(cos_angle * q.x() + sin_angle * q.y()), float(cos_angle * q.y() - sin_angle * q.x()), float(q.z())); + } + return value; + } + + inline coord_t to_coord(long x) const { return x * rsize; } + inline long to_coordr(coord_t x) const { return x / rsize; } + inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); } +}; + +template<> struct _RasterTraits +{ + using ValueType = float; + static float get(const AdaptiveTpmsField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); } + static size_t rows(const AdaptiveTpmsField &sf) { return sf.to_coordr(sf.size.y()); } + static size_t cols(const AdaptiveTpmsField &sf) { return sf.to_coordr(sf.size.x()); } +}; + +// Continuous density level of the depth over a layer, in the object frame. +struct TpmsLevelField +{ + static constexpr float gsizef = 0.5f; + static constexpr float rsizef = 0.05f; + const coord_t rsize = scaled(rsizef); + const long gsize = std::lround(gsizef / rsizef); + + const TpmsRadialField &field; + const DensityLevels &levels; + Point size; + Point offs; + double z; + + TpmsLevelField(const TpmsRadialField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z) + : field(field), levels(levels), size(bbox.size()), offs(bbox.min), z(z) + {} + + float get_scalar(const Coord &p) const + { + const Point pt = to_Point(p); + return float(levels.level(field.depth(Vec3d(unscaled(pt.x()), unscaled(pt.y()), z)))); + } + + inline coord_t to_coord(long x) const { return x * rsize; } + inline long to_coordr(coord_t x) const { return x / rsize; } + inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); } +}; + +template<> struct _RasterTraits +{ + using ValueType = float; + static float get(const TpmsLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); } + static size_t rows(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.y()); } + static size_t cols(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.x()); } +}; + +} // namespace marchsq + +namespace Slic3r { + +Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox, + coordf_t z, coordf_t layer_height, coordf_t spacing, float angle) +{ + // A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region. + const coord_t cell = scaled(marchsq::AdaptiveTpmsField::gsizef); + bbox.offset(cell); + bbox.merge(align_to_grid(bbox.min, Point(cell, cell))); + const marchsq::AdaptiveTpmsField raster(tpms, field, bbox, z - 0.5 * layer_height, angle); + const std::vector rings = marchsq::execute_with_policy(ex_tbb, raster, 0.f, {raster.gsize, raster.gsize}); + + // Loops narrower than two lines print as blobs. + const double min_loop_length = scaled(2. * PI * spacing); + Polylines polylines; + polylines.reserve(rings.size()); + for (const marchsq::Ring &ring : rings) { + Polyline polyline; + polyline.points.reserve(ring.size() + 1); + for (const marchsq::Coord &crd : ring) + polyline.points.emplace_back(raster.to_Point(crd)); + polyline.points.push_back(polyline.points.front()); + polyline.simplify(SCALED_SPARSE_INFILL_RESOLUTION); + if (polyline.length() >= min_loop_length) + polylines.push_back(std::move(polyline)); + } + return polylines; +} + +std::vector make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, + float surface_density, float interior_density, TpmsAdaptiveGradient gradient) +{ + const DensityLevels levels(interior_density / surface_density, ShellRatio, gradient); + if (levels.count == 0) + return {{surface_density, {expolygon}}}; + // A fixed sampling grid, so that every region of a layer gets the same shells. + const coord_t cell = scaled(marchsq::TpmsLevelField::gsizef); + BoundingBox bbox = get_extents(expolygon); + bbox.offset(cell); + bbox.merge(align_to_grid(bbox.min, Point(cell, cell))); + const marchsq::TpmsLevelField raster(field, levels, bbox, z); + + // Each level takes the part deeper than the middle between it and the previous one. + std::vector shells; + ExPolygons remaining{expolygon}; + for (int level = 0; level < levels.count && !remaining.empty(); ++level) { + Polygons deeper; + for (const marchsq::Ring &ring : marchsq::execute_with_policy(ex_tbb, raster, float(level + 0.5), {raster.gsize, raster.gsize})) { + Polygon &polygon = deeper.emplace_back(); + polygon.points.reserve(ring.size()); + for (const marchsq::Coord &crd : ring) + polygon.points.emplace_back(raster.to_Point(crd)); + } + ExPolygons inner = intersection_ex(union_ex(deeper), remaining); + shells.push_back({float(surface_density * levels.scale(level)), diff_ex(remaining, inner)}); + remaining = std::move(inner); + } + if (!remaining.empty()) + shells.push_back({interior_density, std::move(remaining)}); + return shells; +} + +void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, coordf_t spacing, + const std::function &fill_shell) +{ + FillParams shell_params = params; + shell_params.tpms_adaptive = TpmsAdaptiveMode::Disabled; + for (const TpmsShell &shell : make_tpms_shells(field, expolygon, z, params.density, params.tpms_interior_density, params.tpms_adaptive_gradient)) { + shell_params.density = shell.density; + for (const ExPolygon &part : offset_ex(shell.expolygons, -float(scale_(0.5 * spacing)))) + fill_shell(shell_params, part); + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/Fill/FillTpmsAdaptive.hpp b/src/libslic3r/Fill/FillTpmsAdaptive.hpp new file mode 100644 index 0000000000..8e7b9db6cd --- /dev/null +++ b/src/libslic3r/Fill/FillTpmsAdaptive.hpp @@ -0,0 +1,143 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../libslic3r.h" +#include "FillBase.hpp" +#include "../BoundingBox.hpp" +#include "../ExPolygon.hpp" +#include "../Point.hpp" +#include "../Polyline.hpp" +#include "../PrintConfig.hpp" + +namespace Slic3r { + +// Radial coordinate inside the lobes of the bodies of an object, sampled from its slices: 0 at the center of a +// lobe, 1 at its surface. Lobes are parts of a body separated by a neck, like two spheres united. In the 2D modes, +// every section normal to the axis has its own bodies and lobes, and distances are measured within the section. The +// modes following the distance to the surface use the depth of every point instead; Distance warp also the lobes. +class TpmsRadialField +{ +public: + struct Slice + { + coordf_t bottom_z; + coordf_t top_z; + const ExPolygons *expolygons; + }; + + struct Radial + { + Vec3d center; + double t; + float weight; + }; + + // Slices sorted by z, in the XY coordinates of the fill and the print Z. + TpmsRadialField(const std::vector &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode, + const std::function &throw_if_canceled); + + // Lobes blended near the sides between them, from a body or from each of the two sections around a point. + static constexpr size_t MaxMorph = 4; + using Radials = std::array; + + // Without a body, as when the object is thinner than the grid cells. + bool empty() const { return m_bodies.empty(); } + + // Radial coordinates of pt in unscaled coordinates towards the lobe it belongs to, and towards the neighbouring + // lobes near the sides between them, with weights summing to 1. In the 2D modes, those of the two sections around + // pt. Returns their count. + size_t radial(const Vec3d &pt, Radials &out) const; + + // Axis normal to the sections in the 2D modes, -1 in the modes graded in 3D. + int axis() const { return m_axis; } + TpmsAdaptiveMode mode() const { return m_mode; } + + // In the modes following the distance to the surface: depth relative to the deepest point of the body, from 0 at + // the surface to 1. + double depth(const Vec3d &pt) const; + +private: + struct Lobe + { + Vec3d center; + double depth; + // Distance from the center to the surface on a latitude-longitude grid of directions. + std::vector reach; + // Distance warp: mean depth over the ball along each direction, sampled up to the reach. + std::vector mean_depth; + }; + + struct Body + { + size_t first_lobe; + size_t lobes; + // Distance from the deepest point to the surface. + double depth; + }; + + double radial(const Lobe &lobe, const Vec3d &pt) const; + size_t body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const; + // Offset of pt from a center, within the section in the 2D modes. + Vec3d offset(const Vec3d &pt, const Vec3d ¢er) const; + int directions() const { return m_axis < 0 ? Polar * Azimuth : Azimuth; } + + // Directions of the reach of a lobe, on a latitude-longitude grid, or a circle in the 2D modes. + static constexpr int Polar = 24; + static constexpr int Azimuth = 48; + + TpmsAdaptiveMode m_mode; + int m_axis; + Vec3d m_origin; + double m_cell; + Vec3i32 m_size; + // Nearest body of every grid node. + std::vector m_body; + std::vector m_bodies; + std::vector m_lobes; + // In the 2D modes, the nearest section with a body to every section. + std::vector m_section; + // In the modes following the distance to the surface, the depth of every grid node. + std::vector m_depth; +}; + +using TpmsRadialFieldPtr = std::unique_ptr; +// A field for every adaptive mode in use, indexed by the mode. +using TpmsRadialFields = std::array; + +struct AdaptiveTpms +{ + // Implicit TPMS equation with a period of 2 PI. + float (*equation)(float x, float y, float z); + // Pattern frequencies at the surface and at the center, in radians per mm. + double surface_frequency; + double interior_frequency; + TpmsAdaptiveGradient gradient; +}; + +// Infill lines in the fill frame, the object frame rotated by -angle; z is the print_z of the layer. +Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox, + coordf_t z, coordf_t layer_height, coordf_t spacing, float angle); + +struct TpmsShell +{ + float density; + ExPolygons expolygons; +}; + +// Stepped shells: the parts of an expolygon in the object frame at each density, from the surface inwards; z is the +// middle of the layer. +std::vector make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, + float surface_density, float interior_density, TpmsAdaptiveGradient gradient); + +// Stepped shells: fills every shell with fill_shell at its density, each shrunk by half a line like a filled region, +// so the lines connected along the boundaries of two shells don't overlap. +void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, coordf_t spacing, + const std::function &fill_shell); + +} // namespace Slic3r diff --git a/src/libslic3r/Fill/FillTpmsD.cpp b/src/libslic3r/Fill/FillTpmsD.cpp index bb1a338691..04f8222ca5 100644 --- a/src/libslic3r/Fill/FillTpmsD.cpp +++ b/src/libslic3r/Fill/FillTpmsD.cpp @@ -14,8 +14,10 @@ #include "libslic3r/Fill/FillBase.hpp" #include "libslic3r/Point.hpp" #include "libslic3r/Polygon.hpp" +#include "libslic3r/PrintConfig.hpp" #include "libslic3r/libslic3r.h" #include "FillTpmsD.hpp" +#include "FillTpmsAdaptive.hpp" namespace Slic3r { @@ -23,6 +25,11 @@ static double scaled_floor(double x,double scale){ return std::floor(x/scale)*scale; } +static float schwarz_d(float x, float y, float z) +{ + return std::sin(x) * std::sin(y) * std::sin(z) - std::cos(x) * std::cos(y) * std::cos(z); +} + static Polylines make_waves(double gridZ, double density_adjusted, double line_spacing, double width, double height) { const double scaleFactor = scale_(line_spacing) / density_adjusted; @@ -110,31 +117,49 @@ void FillTpmsD::_fill_surface_single( ExPolygon expolygon, Polylines &polylines_out) { + if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) { + fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing, + [&](const FillParams &shell_params, const ExPolygon &shell) { + this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out); + }); + return; + } + auto infill_angle = float(this->angle + (CorrectionAngle * 2*M_PI) / 360.); if(std::abs(infill_angle) >= EPSILON) expolygon.rotate(-infill_angle); - BoundingBox bb = expolygon.contour.bounding_box(); - // Density adjusted to have a good %of weight. - double density_adjusted = std::max(0., params.density * DensityAdjust / params.multiline); - // Distance between the gyroid waves in scaled coordinates. - coord_t distance = coord_t(scale_(this->spacing) / density_adjusted); + Polylines polylines; + if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) { + // Radians per mm of the regular pattern at a density. + auto frequency = [¶ms, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); }; + BoundingBox bbox = expolygon.contour.bounding_box(); + bbox.offset(scale_((params.multiline + 1) * this->spacing)); + polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient}, + *this->tpms_radial_field, bbox, this->z, params.layer_height, this->spacing, infill_angle); + } else { + BoundingBox bb = expolygon.contour.bounding_box(); + // Density adjusted to have a good %of weight. + double density_adjusted = std::max(0., params.density * DensityAdjust / params.multiline); + // Distance between the gyroid waves in scaled coordinates. + coord_t distance = coord_t(scale_(this->spacing) / density_adjusted); - // align bounding box to a multiple of our grid module - bb.merge(align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance))); + // align bounding box to a multiple of our grid module + bb.merge(align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance))); - // generate pattern - Polylines polylines = make_waves( - scale_(this->z), - density_adjusted, - this->spacing, - ceil(bb.size()(0) / distance) + 1., - ceil(bb.size()(1) / distance) + 1.); + // generate pattern + polylines = make_waves( + scale_(this->z), + density_adjusted, + this->spacing, + ceil(bb.size()(0) / distance) + 1., + ceil(bb.size()(1) / distance) + 1.); + + // shift the polyline to the grid origin + for (Polyline &pl : polylines) + pl.translate(bb.min); + } - // shift the polyline to the grid origin - for (Polyline &pl : polylines) - pl.translate(bb.min); - // Apply multiline offset if needed multiline_fill(polylines, params, spacing); diff --git a/src/libslic3r/Fill/FillTpmsFK.cpp b/src/libslic3r/Fill/FillTpmsFK.cpp index 164e240280..819ef2998e 100644 --- a/src/libslic3r/Fill/FillTpmsFK.cpp +++ b/src/libslic3r/Fill/FillTpmsFK.cpp @@ -8,6 +8,7 @@ #include "libslic3r/Fill/FillBase.hpp" #include "libslic3r/ExPolygon.hpp" #include "FillTpmsFK.hpp" +#include "FillTpmsAdaptive.hpp" #include #include #include @@ -17,6 +18,18 @@ #include #include #include "libslic3r/Polygon.hpp" +#include "libslic3r/PrintConfig.hpp" + +namespace Slic3r { + +// Fischer - Koch S equation: +// cos(2x)sin(y)cos(z) + cos(2y)sin(z)cos(x) + cos(2z)sin(x)cos(y) = 0 +static float fischer_koch(float x, float y, float z) +{ + return cosf(2 * x) * sinf(y) * cosf(z) + cosf(2 * y) * sinf(z) * cosf(x) + cosf(2 * z) * sinf(x) * cosf(y); +} + +} // namespace Slic3r namespace marchsq { using namespace Slic3r; @@ -42,16 +55,7 @@ struct ScalarField {} // Get the scalar field value at x,y,z in coordf_t coordinates. - float get_scalar(coordf_t x, coordf_t y, coordf_t z) const - { - const float fx = freq * x; - const float fy = freq * y; - const float fz = freq * z; - - // Fischer - Koch S equation: - // cos(2x)sin(y)cos(z) + cos(2y)sin(z)cos(x) + cos(2z)sin(x)cos(y) = 0 - return cosf(2 * fx) * sinf(fy) * cosf(fz) + cosf(2 * fy) * sinf(fz) * cosf(fx) + cosf(2 * fz) * sinf(fx) * cosf(fy); - } + float get_scalar(coordf_t x, coordf_t y, coordf_t z) const { return fischer_koch(freq * x, freq * y, freq * z); } // Get the scalar field value at a Coord for the current z value. float get_scalar(Coord p) const @@ -128,20 +132,34 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params, ExPolygon expolygon, Polylines& polylines_out) { + if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) { + fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing, + [&](const FillParams &shell_params, const ExPolygon &shell) { + this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out); + }); + return; + } + auto infill_angle = float(this->angle + (CorrectionAngle * 2 * M_PI) / 360.); if (std::abs(infill_angle) >= EPSILON) expolygon.rotate(-infill_angle); - float density_factor = std::min(0.9f, params.density); // Density (field period) adjusted to have a good %of weight. - const float vari_T = 4.18f * spacing * params.multiline / density_factor; + auto period = [¶ms, this](float density) { return 4.18f * spacing * params.multiline / std::min(0.9f, density); }; BoundingBox bbox = expolygon.contour.bounding_box(); // Enlarge the bounding box by the multi-line width to avoid artifacts at the edges. bbox.offset(scale_((params.multiline + 1) * spacing)); - marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, vari_T); - // Get simplified lines using coarse tolerance of 0.1mm (this is infill). - Polylines polylines = marchsq::get_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION); + Polylines polylines; + if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) { + polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.tpms_interior_density), + params.tpms_adaptive_gradient}, + *this->tpms_radial_field, bbox, this->z, params.layer_height, spacing, infill_angle); + } else { + marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, period(params.density)); + // Get simplified lines using coarse tolerance of 0.1mm (this is infill). + polylines = marchsq::get_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION); + } // Apply multiline offset if needed multiline_fill(polylines, params, spacing); diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index 610dd959d2..20eff0446f 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -437,10 +437,14 @@ coordf_t Layer::get_sparse_infill_max_void_area() double max_void_area = 0.; for (auto layerm : m_regions) { Flow flow = layerm->flow(frInfill); - float density = layerm->region().config().sparse_infill_density; - InfillPattern pattern = layerm->region().config().sparse_infill_pattern; + const PrintRegionConfig &config = layerm->region().config(); + float density = config.sparse_infill_density; + InfillPattern pattern = config.sparse_infill_pattern; if (density == 0.) return -1; + // Orca: the adaptive TPMS infill is as sparse as its interior density. + if (density < 100.f && config.tpms_adaptive != TpmsAdaptiveMode::Disabled && is_tpms_adaptive_pattern(pattern)) + density = std::min(density, std::max(1.f, float(config.tpms_interior_density))); //BBS: rough estimation and need to be optimized double spacing = flow.scaled_spacing() * (100 - density) / density; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index bd357ef59e..5b8127b122 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1181,6 +1181,9 @@ static std::vector s_Preset_print_options{ "is_infill_first", "sparse_infill_density", "fill_multiline", + "tpms_adaptive", + "tpms_interior_density", + "tpms_adaptive_gradient", "gyroid_optimized", "sparse_infill_pattern", "sparse_infill_smooth_factor", diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index b536d73e8f..45e8703168 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -12,6 +12,7 @@ #include "PrintBase.hpp" #include "Fill/FillAdaptive.hpp" #include "Fill/FillLightning.hpp" +#include "Fill/FillTpmsAdaptive.hpp" #include "BoundingBox.hpp" #include "ExtrusionEntityCollection.hpp" @@ -406,6 +407,7 @@ public: double max_z() const { return m_max_z; } // Centering offset of the sliced mesh from the scaled and rotated mesh of the model. const Point& center_offset() const { return m_center_offset; } + const TpmsRadialField* tpms_radial_field(TpmsAdaptiveMode mode) const { return m_tpms_radial_fields[size_t(mode)].get(); } // BBS void generate_support_preview(); @@ -650,6 +652,7 @@ private: FillAdaptive::RegionOctrees prepare_adaptive_infill_data( const std::vector>& surfaces_w_layer) const; FillLightning::GeneratorPtr prepare_lightning_infill_data(); + TpmsRadialFields prepare_tpms_radial_fields() const; // BBS SupportNecessaryType is_support_necessary(); @@ -700,6 +703,7 @@ private: FillAdaptive::RegionOctrees m_adaptive_fill_octrees; std::vector m_separated_body_bboxes; FillLightning::GeneratorPtr m_lightning_generator; + TpmsRadialFields m_tpms_radial_fields; std::vector < VolumeSlices > firstLayerObjSliceByVolume; std::vector firstLayerObjSliceByGroups; diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index ce27b0eb93..cf9b79367e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -364,6 +364,26 @@ static t_config_enum_values s_keys_map_SurfaceFillOrder{ }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SurfaceFillOrder) +//Orca +static t_config_enum_values s_keys_map_TpmsAdaptiveMode{ + { "disabled", int(TpmsAdaptiveMode::Disabled) }, + { "distance_warp", int(TpmsAdaptiveMode::DistanceWarp) }, + { "smooth_blend", int(TpmsAdaptiveMode::SmoothBlend) }, + { "stepped_shells", int(TpmsAdaptiveMode::SteppedShells) }, + { "lobes", int(TpmsAdaptiveMode::Lobes) }, + { "normal_z", int(TpmsAdaptiveMode::NormalZ) }, + { "normal_y", int(TpmsAdaptiveMode::NormalY) }, + { "normal_x", int(TpmsAdaptiveMode::NormalX) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveMode) + +static t_config_enum_values s_keys_map_TpmsAdaptiveGradient{ + { "linear", int(TpmsAdaptiveGradient::Linear) }, + { "quadratic", int(TpmsAdaptiveGradient::Quadratic) }, + { "exponential", int(TpmsAdaptiveGradient::Exponential) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveGradient) + //BBS static t_config_enum_values s_keys_map_PrintSequence { { "by layer", int(PrintSequence::ByLayer) }, @@ -3620,6 +3640,73 @@ void PrintConfigDef::init_fff_params() def->max = 10; // Maximum number of lines for infill pattern def->set_default_value(new ConfigOptionInt(1)); + def = this->add("tpms_adaptive", coEnum); + def->label = L("Adaptive density (experimental)"); + def->category = L("Strength"); + def->tooltip = L("Grades the Gyroid and TPMS infill inside the object: its cells grow from the surface of the " + "object towards its center. The sparse infill density is used at the surface and the interior " + "density at the center.\n" + "Distance warp, Smooth blend and Stepped shells follow the distance to the nearest surface, " + "including the top and bottom, with the interior density at the point farthest from it:\n" + " - Distance warp: one continuous pattern, stretched and sheared where the distance changes " + "across directions, as in plates and long parts.\n" + " - Smooth blend: the patterns of neighbouring densities blended into each other, with small " + "loops where they meet.\n" + " - Stepped shells: shells of the regular pattern at densities about 1.5 times apart, their " + "lines joined along the shell boundaries.\n" + " - Lobes: follows the 3D shape of the object, including its top and bottom. Every lobe, a part " + "joined to the rest by a narrower neck, is graded towards its own center.\n" + " - Normal Z, Y or X: follows the sections of the object normal to that axis, so the density " + "does not change along it."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("disabled"); + def->enum_values.push_back("distance_warp"); + def->enum_values.push_back("smooth_blend"); + def->enum_values.push_back("stepped_shells"); + def->enum_values.push_back("lobes"); + def->enum_values.push_back("normal_z"); + def->enum_values.push_back("normal_y"); + def->enum_values.push_back("normal_x"); + def->enum_labels.push_back(L("Disabled")); + def->enum_labels.push_back(L("Distance warp")); + def->enum_labels.push_back(L("Smooth blend")); + def->enum_labels.push_back(L("Stepped shells")); + def->enum_labels.push_back(L("Lobes")); + def->enum_labels.push_back(L("Normal Z")); + def->enum_labels.push_back(L("Normal Y")); + def->enum_labels.push_back(L("Normal X")); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(TpmsAdaptiveMode::Disabled)); + + def = this->add("tpms_interior_density", coPercent); + def->label = L("Interior density"); + def->category = L("Strength"); + def->tooltip = L("Density of the adaptive infill at the center of the object."); + def->sidetext = "%"; + def->min = 1; + def->max = 100; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionPercent(5)); + + def = this->add("tpms_adaptive_gradient", coEnum); + def->label = L("Adaptive gradient"); + def->category = L("Strength"); + def->tooltip = L("How the density changes from the surface to the center of the object.\n" + "Linear: the density changes at a constant rate.\n" + "Quadratic: the density stays close to the sparse infill density near the surface and " + "changes faster towards the center.\n" + "Exponential: the density changes quickly just below the surface and levels off towards " + "the center."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("linear"); + def->enum_values.push_back("quadratic"); + def->enum_values.push_back("exponential"); + def->enum_labels.push_back(L("Linear")); + def->enum_labels.push_back(L("Quadratic")); + def->enum_labels.push_back(L("Exponential")); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(TpmsAdaptiveGradient::Linear)); + // Z-buckling bias optimization (experimental). Tightens the gyroid wave along the Z // (vertical) axis at low infill density to shorten the effective column length under // Z-axis compression. Filament use at the same `sparse_infill_density` setting is diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 7c9633ff78..0e1e146c72 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -172,6 +172,8 @@ inline bool is_separable_infill_pattern(InfillPattern pattern) // Orca: Infill patterns laid out by an octree, which each connected body always gets of its own. inline bool is_octree_infill_pattern(InfillPattern pattern) { return pattern == ipAdaptiveCubic || pattern == ipSupportCubic; } +// Orca: Infill patterns graded by the "tpms_adaptive" option. +inline bool is_tpms_adaptive_pattern(InfillPattern pattern) { return pattern == ipGyroid || pattern == ipTpmsD || pattern == ipTpmsFK; } // Orca: Infill patterns that round their corners by the "sparse_infill_smooth_factor" option. // Grid, Triangles and Tri-hexagon only do so in their trapezoidal form, which is generated with more @@ -257,6 +259,26 @@ enum class SurfaceFillOrder { Count, }; +// Orca: what the adaptive TPMS density follows: the 3D shape of the object, or its 2D sections normal to an axis. +enum class TpmsAdaptiveMode { + Disabled, + DistanceWarp, + SmoothBlend, + SteppedShells, + Lobes, + NormalZ, + NormalY, + NormalX, + Count, +}; + +// Orca: how the adaptive TPMS density changes from the object surface to its deepest point. +enum class TpmsAdaptiveGradient { + Linear, + Quadratic, + Exponential, +}; + //BBS enum class PrintSequence { ByLayer, @@ -774,6 +796,8 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PerimeterGeneratorType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ToolChangeOrderingType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PowerLossRecoveryMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TpmsAdaptiveMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TpmsAdaptiveGradient) #undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS @@ -1450,6 +1474,9 @@ PRINT_CONFIG_CLASS_DEFINE( // Orca: ((ConfigOptionFloatOrPercent, infill_combination_max_layer_height)) ((ConfigOptionInt, fill_multiline)) + ((ConfigOptionEnum, tpms_adaptive)) + ((ConfigOptionPercent, tpms_interior_density)) + ((ConfigOptionEnum, tpms_adaptive_gradient)) ((ConfigOptionBool, gyroid_optimized)) // Ironing options ((ConfigOptionEnum, ironing_type)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 3387d22ba8..5ba7882df3 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -31,6 +31,7 @@ #include "Fill/FillAdaptive.hpp" #include "Fill/Fill.hpp" #include "Fill/FillLightning.hpp" +#include "Fill/FillTpmsAdaptive.hpp" #include "format.hpp" #include "AABBTreeIndirect.hpp" #include "AABBTreeLines.hpp" @@ -1297,6 +1298,38 @@ FillLightning::GeneratorPtr PrintObject::prepare_lightning_infill_data() return has_lightning_infill ? FillLightning::build_generator(std::as_const(*this), [this]() -> void { this->throw_if_canceled(); }) : FillLightning::GeneratorPtr(); } +TpmsRadialFields PrintObject::prepare_tpms_radial_fields() const +{ + TpmsRadialFields fields; + std::array modes{}; + for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) + if (const PrintRegionConfig &config = this->printing_region(region_id).config(); + config.sparse_infill_density > 0 && config.sparse_infill_density < 100 && is_tpms_adaptive_pattern(config.sparse_infill_pattern)) + modes[size_t(config.tpms_adaptive.value)] = true; + modes[size_t(TpmsAdaptiveMode::Disabled)] = false; + if (std::find(modes.begin(), modes.end(), true) == modes.end() || m_layers.empty()) + return fields; + + std::vector slices; + slices.reserve(m_layers.size()); + BoundingBox bbox; + for (const Layer *layer : m_layers) { + slices.push_back({layer->bottom_z(), layer->print_z, &layer->lslices}); + bbox.merge(get_extents(layer->lslices)); + } + if (!bbox.defined) + return fields; + for (size_t mode = 0; mode < modes.size(); ++mode) { + if (!modes[mode]) + continue; + // Without a field, the infill falls back to the regular pattern. + auto field = std::make_unique(slices, bbox, TpmsAdaptiveMode(mode), [this]() { m_print->throw_if_canceled(); }); + if (!field->empty()) + fields[mode] = std::move(field); + } + return fields; +} + void PrintObject::clear_layers() { if (!m_shared_object) { @@ -1700,6 +1733,9 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "infill_overhang_angle") { steps.emplace_back(posInfill); } else if (opt_key == "sparse_infill_pattern" + || opt_key == "tpms_adaptive" + || opt_key == "tpms_interior_density" + || opt_key == "tpms_adaptive_gradient" // Orca: Body centering now also determines bridge anchors during preparation. // Invalidating preparation also invalidates infill, including top/bottom surfaces. || opt_key == "center_of_surface_pattern" @@ -3207,6 +3243,7 @@ void PrintObject::bridge_over_infill() } this->m_adaptive_fill_octrees = this->prepare_adaptive_infill_data(surfaces_w_layer); + this->m_tpms_radial_fields = this->prepare_tpms_radial_fields(); std::vector layers_to_generate_infill; for (const auto &pair : surfaces_by_layer) { diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index b156f32cb8..cfd985b64b 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -843,9 +843,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in bool have_multiline_infill_pattern = pattern == ipGyroid || pattern == ipGrid || pattern == ipRectilinear || pattern == ipTpmsD || pattern == ipTpmsFK || pattern == ipCrossHatch || pattern == ipHoneycomb || pattern == ipLateralLattice || pattern == ipLateralHoneycomb || pattern == ipConcentric || pattern == ipCubic || pattern == ipStars || pattern == ipAlignedRectilinear || pattern == ipLightning || pattern == ip3DHoneycomb || pattern == ipAdaptiveCubic || pattern == ipSupportCubic|| pattern == ipTriangles || pattern == ipQuarterCubic|| pattern == ipArchimedeanChords || pattern == ipHilbertCurve || pattern == ipOctagramSpiral; - // gyroid_optimized only applies when the sparse infill pattern is gyroid; + // The sparse infill density is the surface density of the adaptive TPMS infill; at 100% the infill is solid. + bool have_tpms_infill = have_infill && config->option("sparse_infill_density")->value < 100 && + is_tpms_adaptive_pattern(pattern); + toggle_line("tpms_adaptive", have_tpms_infill); + bool have_tpms_adaptive = have_tpms_infill && config->opt_enum("tpms_adaptive") != TpmsAdaptiveMode::Disabled; + toggle_line("tpms_interior_density", have_tpms_adaptive); + toggle_line("tpms_adaptive_gradient", have_tpms_adaptive); + + // gyroid_optimized only applies when the sparse infill pattern is gyroid without adaptive density; // hide the whole line otherwise. - toggle_line("gyroid_optimized", have_infill && pattern == ipGyroid); + toggle_line("gyroid_optimized", have_infill && pattern == ipGyroid && !have_tpms_adaptive); // If there is infill, enable/disable fill_multiline according to whether the pattern supports multiline infill. if (have_infill) { diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 28c0c1ceb4..5cbda924b5 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -148,6 +148,9 @@ std::map> SettingsFactory::PART_CATE {"sparse_infill_density", "", 1}, {"fill_multiline", "", 1}, {"sparse_infill_pattern", "", 1}, + {"tpms_adaptive", "", 1}, + {"tpms_interior_density", "", 1}, + {"tpms_adaptive_gradient", "", 1}, {"sparse_infill_smooth_factor", "", 1}, {"lateral_lattice_angle_1", "", 1}, {"lateral_lattice_angle_2", "", 1}, diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 49457e8bc8..7c9f486426 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2961,6 +2961,9 @@ void TabPrint::build() optgroup->append_single_option_line("sparse_infill_density", "strength_settings_infill#sparse-infill-density"); optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline"); optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern"); + optgroup->append_single_option_line("tpms_adaptive", "strength_settings_patterns#adaptive-density"); + optgroup->append_single_option_line("tpms_interior_density", "strength_settings_patterns#interior-density"); + optgroup->append_single_option_line("tpms_adaptive_gradient", "strength_settings_patterns#adaptive-gradient"); optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized"); optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_infill#sparse-infill-smooth-factor"); optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction"); diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index e1d93c5e15..1fbedcda8f 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -37,6 +37,7 @@ #include "libslic3r/Fill/Fill.hpp" #include "libslic3r/Fill/FillAdaptive.hpp" #include "libslic3r/Fill/FillGyroid.hpp" +#include "libslic3r/Fill/FillTpmsAdaptive.hpp" #include "libslic3r/Flow.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/IntersectionPoints.hpp" @@ -2021,3 +2022,334 @@ TEST_CASE("Adaptive infill of a modifier leaves the density of the other regions 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); } + +// Length of the sparse infill of the first object over the layers with print_z in [z_min, z_max], inside clip. +static double sparse_infill_length(const Print &print, coordf_t z_min, coordf_t z_max, const Polygons &clip) +{ + Polylines polylines; + for (const Layer *layer : print.objects().front()->layers()) + if (layer->print_z >= z_min && layer->print_z <= z_max) + for (const LayerRegion *region : layer->regions()) + for (const ExtrusionEntity *entity : region->fills.flatten().entities) + if (entity->role() == erInternalInfill) + entity->collect_polylines(polylines); + return unscale(total_length(intersection_pl(polylines, clip))); +} + +static Polygons centered_square(double half) +{ + return {Polygon({Point::new_scale(-half, -half), Point::new_scale(half, -half), Point::new_scale(half, half), Point::new_scale(-half, half)})}; +} + +TEST_CASE("Adaptive TPMS infill thins out from the surface to the interior density", "[Fill]") +{ + // The modes following the distance print more lines than their target where its levels meet: along the shells, or + // where the patterns blend. + const auto row = GENERATE(table({{"tpmsd", "lobes", 0.5}, + {"tpmsfk", "lobes", 0.5}, + {"gyroid", "lobes", 0.5}, + {"gyroid", "stepped_shells", 0.6}, + {"tpmsfk", "smooth_blend", 0.6}, + {"tpmsd", "distance_warp", 0.5}})); + const std::string pattern = std::get<0>(row); + const std::string mode = std::get<1>(row); + const double max_core = std::get<2>(row); + CAPTURE(pattern, mode); + // A 60 mm cube in 0.4 mm layers, centered on the origin in XY. Its center is 30 mm from every face. + auto slice = [&pattern](const std::string &adaptive, Print &print) { + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(60)}, print, + {{"sparse_infill_pattern", pattern}, + {"sparse_infill_density", "25%"}, + {"tpms_adaptive", adaptive}, + {"tpms_interior_density", "5%"}, + {"tpms_adaptive_gradient", "linear"}, + {"layer_height", 0.4}, + {"initial_layer_print_height", 0.4}}); + }; + Print uniform, adaptive; + slice("disabled", uniform); + slice(mode, adaptive); + + // The middle of the cube, at most 10 mm from the center: at most 5% + 20% * 10 / 30 = 11.7% dense. + const Polygons core = centered_square(10.); + // Along the sides at mid height, 24 to 28 mm from the center: at least 5% + 20% * 24 / 30 = 21% dense. + const Polygons shell = diff(centered_square(28.), centered_square(24.)); + const double core_uniform = sparse_infill_length(uniform, 25., 35., core); + const double shell_uniform = sparse_infill_length(uniform, 25., 35., shell); + REQUIRE(core_uniform > 0.); + REQUIRE(shell_uniform > 0.); + CHECK(sparse_infill_length(adaptive, 25., 35., core) < max_core * core_uniform); + CHECK(sparse_infill_length(adaptive, 25., 35., shell) > 0.75 * shell_uniform); +} + +TEST_CASE("Adaptive TPMS infill of a tall object is sparsest at its middle height", "[Fill]") +{ + // A 30 x 30 x 90 mm box in 0.4 mm layers: its center is 45 mm high, the middle of its core, not a column. + Print print; + Slic3r::Test::init_and_process_print({make_cube(30., 30., 90.)}, print, + {{"sparse_infill_pattern", "tpmsd"}, + {"sparse_infill_density", "25%"}, + {"tpms_adaptive", "lobes"}, + {"tpms_interior_density", "5%"}, + {"tpms_adaptive_gradient", "linear"}, + {"layer_height", 0.4}, + {"initial_layer_print_height", 0.4}}); + const Polygons core = centered_square(5.); + const double middle = sparse_infill_length(print, 40., 50., core); + const double low = sparse_infill_length(print, 10., 20., core); + REQUIRE(low > 0.); + CHECK(middle < 0.7 * low); +} + +TEST_CASE("2D adaptive TPMS infill does not change along its axis", "[Fill]") +{ + // A box of 30 x 30 mm sections, 90 mm long along the axis, centered on the origin in XY. + const std::string mode = GENERATE("normal_x", "normal_z"); + CAPTURE(mode); + const bool along_x = mode == "normal_x"; + auto slice = [&](const std::string &adaptive, Print &print) { + Slic3r::Test::init_and_process_print({along_x ? make_cube(90., 30., 30.) : make_cube(30., 30., 90.)}, print, + {{"sparse_infill_pattern", "gyroid"}, + {"sparse_infill_density", "25%"}, + {"tpms_adaptive", adaptive}, + {"tpms_interior_density", "5%"}, + {"tpms_adaptive_gradient", "linear"}, + {"layer_height", 0.4}, + {"initial_layer_print_height", 0.4}}); + }; + Print uniform, adaptive; + slice("disabled", uniform); + slice(mode, adaptive); + + // The core of the sections near an end and at the middle of the box, at most 5 mm from their center. + auto rectangle = [](double x0, double x1) { + return Polygons{Polygon({Point::new_scale(x0, -5.), Point::new_scale(x1, -5.), Point::new_scale(x1, 5.), Point::new_scale(x0, 5.)})}; + }; + auto core = [&](const Print &print, bool end) { + return along_x ? sparse_infill_length(print, 10., 20., rectangle(end ? -40. : -5., end ? -30. : 5.)) : + sparse_infill_length(print, end ? 10. : 40., end ? 20. : 50., centered_square(5.)); + }; + const double end = core(adaptive, true); + const double middle = core(adaptive, false); + REQUIRE(end > 0.); + CHECK(middle > 0.8 * end); + CHECK(middle < 1.25 * end); + CHECK(middle < 0.7 * core(uniform, false)); +} + +TEST_CASE("Adaptive TPMS shells and blend keep the whole deep core of a tall object sparse", "[Fill]") +{ + // Distance warp grades it partly along the height, like Lobes, as its warp is centered on the middle. + const std::string mode = GENERATE("stepped_shells", "smooth_blend"); + CAPTURE(mode); + // A 30 x 30 x 90 mm box: from 15 to 75 mm high its axis is 15 mm deep, as deep as its center. + auto slice = [](const std::string &adaptive, Print &print) { + Slic3r::Test::init_and_process_print({make_cube(30., 30., 90.)}, print, + {{"sparse_infill_pattern", "tpmsd"}, + {"sparse_infill_density", "25%"}, + {"tpms_adaptive", adaptive}, + {"tpms_interior_density", "5%"}, + {"tpms_adaptive_gradient", "linear"}, + {"layer_height", 0.4}, + {"initial_layer_print_height", 0.4}}); + }; + Print uniform, distance; + slice("disabled", uniform); + slice(mode, distance); + // At 15 to 25 mm high the axis is as sparse as at the middle, where Lobes grades it towards the bottom. + const Polygons core = centered_square(5.); + const double uniform_low = sparse_infill_length(uniform, 15., 25., core); + const double low = sparse_infill_length(distance, 15., 25., core); + const double middle = sparse_infill_length(distance, 40., 50., core); + CAPTURE(uniform_low, low, middle); + REQUIRE(middle > 0.); + CHECK(low < 0.7 * uniform_low); + CHECK(low < 1.25 * middle); +} + +TEST_CASE("Adaptive TPMS infill is sparse at the center of both of two united spheres", "[Fill]") +{ + // Two spheres of 20 mm, their centers 32 mm apart at 20 mm high: centered on the origin, they are at x = -16 and 16. + TriangleMesh spheres = make_sphere(20., 2. * PI / 90.); + spheres.translate(-16.f, 0.f, 20.f); + TriangleMesh second = make_sphere(20., 2. * PI / 90.); + second.translate(16.f, 0.f, 20.f); + spheres.merge(second); + auto slice = [&spheres](const std::string &adaptive, Print &print) { + Slic3r::Test::init_and_process_print({TriangleMesh(spheres)}, print, + {{"sparse_infill_pattern", "gyroid"}, + {"sparse_infill_density", "25%"}, + {"tpms_adaptive", adaptive}, + {"tpms_interior_density", "5%"}, + {"tpms_adaptive_gradient", "linear"}, + {"layer_height", 0.4}, + {"initial_layer_print_height", 0.4}}); + }; + Print uniform, adaptive; + slice("disabled", uniform); + slice("lobes", adaptive); + // Within 5 mm of either center: at most 5% + 20% * 7.1 / 20 = 12.1% dense, where one center for both + // would leave the other sphere at more than 60% of the uniform density. + for (const double x : {-16., 16.}) { + CAPTURE(x); + Polygons core = centered_square(5.); + for (Polygon &square : core) + square.translate(Point::new_scale(x, 0.)); + const double core_uniform = sparse_infill_length(uniform, 18., 22., core); + REQUIRE(core_uniform > 0.); + CHECK(sparse_infill_length(adaptive, 18., 22., core) < 0.45 * core_uniform); + } +} + +TEST_CASE("Adaptive TPMS gradients keep the surface density deeper in the order quadratic, linear, exponential", "[Fill]") +{ + // With a denser surface, t^2 <= t and the geometric interpolation is below the linear one at every depth. + auto length_for = [](const std::string &gradient) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(40)}, print, + {{"sparse_infill_pattern", "tpmsd"}, + {"sparse_infill_density", "25%"}, + {"tpms_adaptive", "lobes"}, + {"tpms_interior_density", "5%"}, + {"tpms_adaptive_gradient", gradient}, + {"layer_height", 0.4}, + {"initial_layer_print_height", 0.4}}); + return sparse_infill_length(print, 0., 40., centered_square(20.)); + }; + const double quadratic = length_for("quadratic"); + const double linear = length_for("linear"); + const double exponential = length_for("exponential"); + CHECK(quadratic > linear); + CHECK(linear > exponential); +} + +TEST_CASE("Adaptive TPMS settings leave the infill unchanged when they do not apply", "[Fill]") +{ + // Adaptive density turned off, or turned on for a pattern that is no TPMS. + const auto [pattern, adaptive] = GENERATE( + table({{"tpmsd", "disabled"}, {"tpmsfk", "disabled"}, {"gyroid", "disabled"}, {"grid", "lobes"}, {"grid", "stepped_shells"}, {"grid", "smooth_blend"}, {"grid", "distance_warp"}, {"grid", "normal_z"}})); + CAPTURE(pattern, adaptive); + Print reference, tuned; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, reference, + {{"sparse_infill_pattern", pattern}, {"sparse_infill_density", "20%"}, {"layer_height", 0.2}}); + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, tuned, + {{"sparse_infill_pattern", pattern}, + {"sparse_infill_density", "20%"}, + {"layer_height", 0.2}, + {"tpms_adaptive", adaptive}, + {"tpms_interior_density", "40%"}, + {"tpms_adaptive_gradient", "exponential"}}); + const SparseInfillShape expected = sparse_infill_shape(reference); + REQUIRE(expected.path_count > 0); + CHECK(sparse_infill_shape(tuned).sequence == expected.sequence); +} + +TEST_CASE("Adaptive gyroid infill ignores the Z-buckling optimization", "[Fill]") +{ + auto shape_for = [](const std::string &gyroid_optimized) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "gyroid"}, + {"sparse_infill_density", "10%"}, + {"gyroid_optimized", gyroid_optimized}, + {"tpms_adaptive", "lobes"}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + const SparseInfillShape expected = shape_for("0"); + REQUIRE(expected.path_count > 0); + CHECK(shape_for("1").sequence == expected.sequence); +} + +TEST_CASE("Adaptive TPMS infill of a region matches the infill of a larger region of the same object", "[Fill]") +{ + const InfillPattern pattern = GENERATE(ipGyroid, ipTpmsD, ipTpmsFK); + CAPTURE(pattern); + // An 80 x 80 x 40 mm box around both regions, which share its radial field. + const ExPolygons box{ExPolygon(Points{Point::new_scale(60., 20.), Point::new_scale(140., 20.), Point::new_scale(140., 100.), + Point::new_scale(60., 100.)})}; + std::vector slices; + for (int i = 0; i < 200; ++i) + slices.push_back({0.2 * i, 0.2 * (i + 1), &box}); + const TpmsRadialField field(slices, get_extents(box), TpmsAdaptiveMode::Lobes, [] {}); + + auto circle = [](double radius) { + Polygon contour = make_circle_num_segments(scale_(radius), 120); + contour.translate(Point::new_scale(100., 60.)); + return ExPolygon(std::move(contour)); + }; + const ExPolygon region = circle(20.); + const ExPolygon larger = circle(30.); + auto fill = [pattern, &field](const ExPolygon &expolygon, double z) { + std::unique_ptr filler(Fill::new_from_type(pattern)); + filler->spacing = 0.45; + filler->angle = float(M_PI / 7.); + filler->z = z; + filler->tpms_radial_field = &field; + + FillParams params; + params.density = 0.2f; + params.tpms_adaptive = TpmsAdaptiveMode::Lobes; + params.tpms_interior_density = 0.05f; + params.tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear; + params.layer_height = 0.2; + params.dont_adjust = true; + Surface surface(stInternal, expolygon); + return filler->fill_surface(&surface, params); + }; + // Away from the boundary of the region, where both are clipped and connected the same way. + const Polygons inner = shrink(to_polygons(region), scale_(1.)); + auto farthest = [&inner](const Polylines &from, const Polylines &to) { + const AABBTreeLines::LinesDistancer tree(to_lines(to)); + double distance = 0.; + for (const Polyline &path : intersection_pl(from, inner)) + for (const Point &point : path.equally_spaced_points(scale_(0.2))) + distance = std::max(distance, tree.distance_from_lines(point)); + return unscale(distance); + }; + // Marching squares simplifies rings that start elsewhere in each region. At 15.325 mm TPMS-FK has a saddle, + // whose lines connect one way or the other with the sampling grid. + const double tolerance = SPARSE_INFILL_RESOLUTION + 0.01; + for (const double z : {5.1, 12.3, 15.325, 20.1, 27.9, 34.7}) { + CAPTURE(z); + const Polylines paths = fill(region, z); + REQUIRE_FALSE(paths.empty()); + const Polylines reference = fill(larger, z); + CHECK(farthest(reference, paths) < tolerance); + CHECK(farthest(paths, reference) < tolerance); + } +} + +TEST_CASE("Adaptive TPMS anchors match the printed infill", "[Fill][InternalBridge]") +{ + const std::string pattern = GENERATE("tpmsd", "tpmsfk", "gyroid"); + CAPTURE(pattern); + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(30)}, print, + {{"sparse_infill_pattern", pattern}, + {"sparse_infill_density", "25%"}, + {"tpms_adaptive", "lobes"}, + {"tpms_interior_density", "5%"}, + {"layer_height", 0.2}, + {"initial_layer_print_height", 0.2}, + {"resolution", 0.012}}); + + const Layer &layer = *print.objects().front()->get_layer(40); + Polylines printed; + for (const LayerRegion *region : layer.regions()) + for (const ExtrusionEntity *entity : region->fills.flatten().entities) + if (entity->role() == erInternalInfill) + entity->collect_polylines(printed); + REQUIRE_FALSE(printed.empty()); + const AABBTreeLines::LinesDistancer printed_tree(to_lines(printed)); + + // Exclude the perimeter connections, which anchoring and extrusion trim differently. + 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.; + for (const Polyline &path : anchors) + for (const Point &point : path.equally_spaced_points(scale_(0.25))) + max_distance = std::max(max_distance, printed_tree.distance_from_lines(point)); + CHECK(unscale(max_distance) <= 0.012); +} diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index ef96e45445..21aa27bf29 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -35,6 +35,7 @@ add_executable(${_TEST_NAME}_tests test_fill_corner_smoothing.cpp test_filament_mixer.cpp test_fill_plane_path.cpp + test_fill_tpms_adaptive.cpp test_geometry.cpp test_multimaterial_segmentation.cpp test_placeholder_parser.cpp diff --git a/tests/libslic3r/test_fill_tpms_adaptive.cpp b/tests/libslic3r/test_fill_tpms_adaptive.cpp new file mode 100644 index 0000000000..d150ae8d7b --- /dev/null +++ b/tests/libslic3r/test_fill_tpms_adaptive.cpp @@ -0,0 +1,292 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/ExPolygon.hpp" +#include "libslic3r/Fill/FillTpmsAdaptive.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/Polygon.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/libslic3r.h" + +using namespace Slic3r; +using Catch::Matchers::WithinAbs; + +namespace { + +ExPolygon rectangle(double x0, double y0, double x1, double y1) +{ + return ExPolygon(Points{Point::new_scale(x0, y0), Point::new_scale(x1, y0), Point::new_scale(x1, y1), Point::new_scale(x0, y1)}); +} + +// The expolygons stacked in 0.2 mm layers from z = 0 to height. +TpmsRadialField radial_field(const ExPolygons &expolygons, double height, TpmsAdaptiveMode mode = TpmsAdaptiveMode::Lobes) +{ + std::vector slices; + for (int i = 0; 0.2 * (i + 1) < height + EPSILON; ++i) + slices.push_back({0.2 * i, 0.2 * (i + 1), &expolygons}); + return TpmsRadialField(slices, get_extents(expolygons), mode, [] {}); +} + +double radial(const TpmsRadialField &field, const Vec3d &pt) +{ + TpmsRadialField::Radials radials; + field.radial(pt, radials); + return radials[0].t; +} + +Vec3d center(const TpmsRadialField &field, const Vec3d &pt) +{ + TpmsRadialField::Radials radials; + field.radial(pt, radials); + return radials[0].center; +} + +// The grid cells are 0.5 mm, so a radial coordinate over 10 mm is accurate to about a twentieth. +constexpr double Tolerance = 0.075; + +} // namespace + +TEST_CASE("TPMS radial field is zero at the center of a cube and one at its faces", "[FillTpmsAdaptive]") +{ + // A 20 mm cube, 10 mm from its center to every face. + const ExPolygons square{rectangle(0., 0., 20., 20.)}; + const TpmsRadialField field = radial_field(square, 20.); + + const Vec3d c = center(field, {10., 10., 10.}); + CHECK_THAT(c.x(), WithinAbs(10., 0.5)); + CHECK_THAT(c.y(), WithinAbs(10., 0.5)); + CHECK_THAT(c.z(), WithinAbs(10., 0.5)); + CHECK_THAT(radial(field, {10., 10., 10.}), WithinAbs(0., Tolerance)); + for (const Vec3d &face : {Vec3d(0., 10., 10.), Vec3d(20., 10., 10.), Vec3d(10., 0., 10.), Vec3d(10., 10., 0.), Vec3d(10., 10., 20.)}) { + CAPTURE(face.x(), face.y(), face.z()); + CHECK_THAT(radial(field, face), WithinAbs(1., Tolerance)); + } +} + +TEST_CASE("TPMS radial field grows linearly from the center of a cube to its faces", "[FillTpmsAdaptive]") +{ + const ExPolygons square{rectangle(0., 0., 20., 20.)}; + const TpmsRadialField field = radial_field(square, 20.); + for (double d = 1.; d < 10.; d += 1.) { + CAPTURE(d); + CHECK_THAT(radial(field, {10. - d, 10., 10.}), WithinAbs(d / 10., Tolerance)); + CHECK_THAT(radial(field, {10., 10., 10. + d}), WithinAbs(d / 10., Tolerance)); + } +} + +TEST_CASE("TPMS radial field of a tall box is centered at its middle height", "[FillTpmsAdaptive]") +{ + // 20 x 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom. + const ExPolygons square{rectangle(0., 0., 20., 20.)}; + const TpmsRadialField field = radial_field(square, 60.); + + CHECK_THAT(center(field, {10., 10., 45.}).z(), WithinAbs(30., 0.5)); + CHECK_THAT(radial(field, {10., 10., 15.}), WithinAbs(0.5, Tolerance)); + CHECK_THAT(radial(field, {10., 10., 45.}), WithinAbs(0.5, Tolerance)); + CHECK_THAT(radial(field, {15., 10., 30.}), WithinAbs(0.5, Tolerance)); +} + +TEST_CASE("TPMS radial field grades every body towards its own center", "[FillTpmsAdaptive]") +{ + const ExPolygons squares{rectangle(0., 0., 20., 20.), rectangle(30., 0., 50., 20.)}; + const TpmsRadialField field = radial_field(squares, 20.); + + CHECK_THAT(center(field, {5., 10., 10.}).x(), WithinAbs(10., 0.5)); + CHECK_THAT(center(field, {45., 10., 10.}).x(), WithinAbs(40., 0.5)); + CHECK_THAT(radial(field, {40., 10., 10.}), WithinAbs(0., Tolerance)); + CHECK_THAT(radial(field, {30., 10., 10.}), WithinAbs(1., Tolerance)); +} + +TEST_CASE("TPMS radial field is beyond one outside of the object", "[FillTpmsAdaptive]") +{ + const ExPolygons square{rectangle(0., 0., 20., 20.)}; + const TpmsRadialField field = radial_field(square, 20.); + CHECK(radial(field, {-5., 10., 10.}) > 1.); + CHECK(radial(field, {10., 10., 30.}) > 1.); +} + +TEST_CASE("TPMS radial field grades every lobe of a body towards its own center", "[FillTpmsAdaptive]") +{ + // Two spheres of 10 mm united, their centers 16 mm apart: the neck between them is 6 mm deep. + const Vec3d c1(10., 10., 10.), c2(26., 10., 10.); + std::vector layers; + std::vector slices; + for (int i = 0; i < 100; ++i) { + const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.))); + Polygons circles; + for (const Vec3d &c : {c1, c2}) { + Polygon &circle = circles.emplace_back(); + for (int k = 0; k < 90; ++k) + circle.points.push_back(Point::new_scale(c.x() + r * std::cos(k * 2. * PI / 90.), c.y() + r * std::sin(k * 2. * PI / 90.))); + } + layers.push_back(union_ex(circles)); + } + for (int i = 0; i < 100; ++i) + slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]}); + const TpmsRadialField field(slices, get_extents(layers[50]), TpmsAdaptiveMode::Lobes, [] {}); + + for (const Vec3d &c : {c1, c2}) { + CAPTURE(c.x()); + CHECK_THAT(center(field, c).x(), WithinAbs(c.x(), 0.5)); + CHECK_THAT(radial(field, c), WithinAbs(0., Tolerance)); + CHECK_THAT(radial(field, c + Vec3d(0., 0., 9.5)), WithinAbs(1., 2. * Tolerance)); + } + // The side between the lobes is half way to the surface, where both patterns morph into each other. + TpmsRadialField::Radials radials; + REQUIRE(field.radial(0.5 * (c1 + c2), radials) == 2); + for (size_t i = 0; i < 2; ++i) { + CHECK_THAT(radials[i].t, WithinAbs(0.5, 2. * Tolerance)); + CHECK_THAT(radials[i].weight, WithinAbs(0.5, 0.05)); + } +} + +TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously", "[FillTpmsAdaptive]") +{ + // Three spheres of 10 mm united, their centers on a triangle of 16 mm sides: the necks meet at its middle. + const std::array centers{Vec3d(10., 10., 10.), Vec3d(26., 10., 10.), Vec3d(18., 10. + 8. * std::sqrt(3.), 10.)}; + std::vector layers; + std::vector slices; + for (int i = 0; i < 100; ++i) { + const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.))); + Polygons circles; + for (const Vec3d &c : centers) { + Polygon &circle = circles.emplace_back(); + for (int k = 0; k < 90; ++k) + circle.points.push_back(Point::new_scale(c.x() + r * std::cos(k * 2. * PI / 90.), c.y() + r * std::sin(k * 2. * PI / 90.))); + } + layers.push_back(union_ex(circles)); + } + for (int i = 0; i < 100; ++i) + slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]}); + const TpmsRadialField field(slices, get_extents(layers[50]), TpmsAdaptiveMode::Lobes, [] {}); + + // Around the junction the nearest lobes swap, but the weight of every lobe changes smoothly. + const Vec3d junction = (centers[0] + centers[1] + centers[2]) / 3.; + size_t max_count = 0; + double max_jump = 0.; + double max_error = 0.; + for (int row = 0; row <= 100; ++row) { + std::array previous{}; + for (int step = 0; step <= 200; ++step) { + TpmsRadialField::Radials radials; + const size_t count = field.radial(junction + Vec3d(0.01 * step - 1., 0.02 * row - 1., 0.), radials); + max_count = std::max(max_count, count); + std::array weights{}; + for (size_t i = 0; i < count; ++i) { + auto nearest = std::min_element(centers.begin(), centers.end(), [&](const Vec3d &a, const Vec3d &b) { + return (a - radials[i].center).norm() < (b - radials[i].center).norm(); + }); + weights[nearest - centers.begin()] += radials[i].weight; + } + max_error = std::max(max_error, std::abs(weights[0] + weights[1] + weights[2] - 1.)); + if (step > 0) + for (size_t k = 0; k < 3; ++k) + max_jump = std::max(max_jump, double(std::abs(weights[k] - previous[k]))); + previous = weights; + } + } + CHECK(max_count == 3); + CHECK(max_error < 1e-5); + CHECK(max_jump < 0.05); +} + +TEST_CASE("TPMS radial field is empty when the object is thinner than the grid cells", "[FillTpmsAdaptive]") +{ + // A 0.3 mm square bar between the nodes of a grid sized by a 200 mm bounding box, with cells of 0.5 mm or more. + const ExPolygons bar{rectangle(0.1, 0.1, 0.4, 0.4)}; + std::vector slices; + for (int i = 0; i < 1000; ++i) + slices.push_back({0.2 * i, 0.2 * (i + 1), &bar}); + const TpmsRadialField field(slices, BoundingBox(Point::new_scale(0., 0.), Point::new_scale(200., 200.)), + TpmsAdaptiveMode::Lobes, [] {}); + CHECK(field.empty()); +} + +TEST_CASE("TPMS depth follows the distance to the surface relative to the deepest point", "[FillTpmsAdaptive]") +{ + // 20 x 20 x 60 mm: from 10 to 50 mm high the axis is 10 mm deep, as deep as the center. + const ExPolygons square{rectangle(0., 0., 20., 20.)}; + const TpmsRadialField field = radial_field(square, 60., TpmsAdaptiveMode::SmoothBlend); + for (double z : {15., 30., 45.}) { + CAPTURE(z); + CHECK_THAT(field.depth({10., 10., z}), WithinAbs(1., Tolerance)); + } + CHECK_THAT(field.depth({5., 10., 30.}), WithinAbs(0.5, Tolerance)); + CHECK_THAT(field.depth({10., 10., 55.}), WithinAbs(0.5, Tolerance)); + CHECK_THAT(field.depth({0., 10., 30.}), WithinAbs(0., Tolerance)); +} + +TEST_CASE("TPMS radial field in Distance warp mode follows the distance to the surface", "[FillTpmsAdaptive]") +{ + // In a cube the depth falls linearly along every ray from the center, so Distance warp matches Lobes. + const ExPolygons square{rectangle(0., 0., 20., 20.)}; + const TpmsRadialField cube = radial_field(square, 20., TpmsAdaptiveMode::DistanceWarp); + CHECK_THAT(radial(cube, {10., 10., 10.}), WithinAbs(0., Tolerance)); + CHECK_THAT(radial(cube, {15., 10., 10.}), WithinAbs(0.5, Tolerance)); + CHECK_THAT(radial(cube, {10., 10., 20.}), WithinAbs(1., Tolerance)); + + // 20 x 20 x 60 mm: Lobes grades the axis towards the top and the bottom, Distance warp keeps it deep. + const TpmsRadialField lobes = radial_field(square, 60.); + const TpmsRadialField warp = radial_field(square, 60., TpmsAdaptiveMode::DistanceWarp); + for (double z : {15., 45.}) { + CAPTURE(z); + CHECK_THAT(radial(lobes, {10., 10., z}), WithinAbs(0.5, Tolerance)); + CHECK(radial(warp, {10., 10., z}) < 0.25); + } + CHECK_THAT(radial(warp, {0., 10., 30.}), WithinAbs(1., Tolerance)); +} + +TEST_CASE("TPMS stepped shells split a layer by depth from the surface inwards", "[FillTpmsAdaptive]") +{ + // The middle layer of a 40 mm cube, 20 mm from its center to every face, 20% at the surface to 5% inside. + const ExPolygons square{rectangle(0., 0., 40., 40.)}; + const TpmsRadialField field = radial_field(square, 40., TpmsAdaptiveMode::SteppedShells); + const std::vector shells = make_tpms_shells(field, square.front(), 20., 0.2f, 0.05f, TpmsAdaptiveGradient::Linear); + REQUIRE(shells.size() == 5); + CHECK_THAT(shells.front().density, WithinAbs(0.2, 1e-6)); + CHECK_THAT(shells.back().density, WithinAbs(0.05, 1e-6)); + double area = 0.; + for (size_t i = 0; i < shells.size(); ++i) { + CAPTURE(i); + if (i > 0) + CHECK(shells[i].density < shells[i - 1].density); + for (const ExPolygon &expolygon : shells[i].expolygons) + area += expolygon.area(); + } + CHECK_THAT(area / square.front().area(), WithinAbs(1., 0.01)); + const Point center = Point::new_scale(20., 20.); + CHECK(std::any_of(shells.back().expolygons.begin(), shells.back().expolygons.end(), + [¢er](const ExPolygon &expolygon) { return expolygon.contains(center); })); +} + +TEST_CASE("TPMS radial field in 2D grades every section normal to the axis on its own", "[FillTpmsAdaptive]") +{ + // 20 x 20 x 60 mm: every section normal to Z is 10 mm from its center to the sides, whatever its height. + const ExPolygons square{rectangle(0., 0., 20., 20.)}; + const TpmsRadialField normal_z = radial_field(square, 60., TpmsAdaptiveMode::NormalZ); + for (double z : {5., 30., 55.}) { + CAPTURE(z); + CHECK_THAT(radial(normal_z, {10., 10., z}), WithinAbs(0., Tolerance)); + CHECK_THAT(radial(normal_z, {15., 10., z}), WithinAbs(0.5, Tolerance)); + CHECK_THAT(radial(normal_z, {10., 0., z}), WithinAbs(1., Tolerance)); + } + + // Normal to X, the sections are 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom. + const TpmsRadialField normal_x = radial_field(square, 60., TpmsAdaptiveMode::NormalX); + for (double x : {3., 10., 17.}) { + CAPTURE(x); + CHECK_THAT(radial(normal_x, {x, 15., 30.}), WithinAbs(0.5, Tolerance)); + CHECK_THAT(radial(normal_x, {x, 10., 45.}), WithinAbs(0.5, Tolerance)); + } +} From e72ace164bb865d8dd47ab46cb248ffbbc9ec13c Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 9 Oct 2026 18:35:10 +0800 Subject: [PATCH 3/5] feat(speed-dial): add plugin page capabilities as actions Plugin Pages capabilities (top-level notebook tabs) were missing from the Speed Dial because ActionRegistry only ingested Script capabilities. Enumerate and subscribe to Pages as well. Launching a page action switches the notebook to that page, swapping it into the visible slot first when it lives behind the overflow dropdown. --- src/slic3r/GUI/ActionRegistry.cpp | 100 +++++++++++++++++++++++-- src/slic3r/GUI/ActionRegistry.hpp | 1 + src/slic3r/plugin/host/PluginPages.cpp | 11 +++ src/slic3r/plugin/host/PluginPages.hpp | 2 + 4 files changed, 109 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index 9d70bdb934..5b1e6f5957 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -149,6 +149,56 @@ std::unique_ptr make_action(const std::string& plugin_key, const std: return std::make_unique(plugin_key, capability, source_name); } +// A plugin page capability exposed as a speed-dial action. source_key = plugin_key +// (identity), so a plugin display-name change does not re-key the action. +struct PluginPageAction : AppAction +{ + static constexpr const char* kIdPrefix = "plugin_page_action"; + + std::string plugin_key; + std::string capability; + + // The id an action for (plugin_key, capability) would have - lets refresh_page_capability + // remove a gone capability without materialising the action. + static std::string id_for(const std::string& plugin_key, const std::string& capability) + { return AppAction::compose_id(kIdPrefix, capability.empty() ? plugin_key : capability, plugin_key); } + + PluginPageAction(std::string plugin_key_in, std::string capability_in, std::string source_name) + : AppAction(kIdPrefix, + capability_in.empty() ? plugin_key_in : capability_in, // title + plugin_key_in, // source_key + std::move(source_name)) + , plugin_key(std::move(plugin_key_in)) + , capability(std::move(capability_in)) + { + // Stay classified as a plugin: grouped under "Plugins" and gated by the same run-confirm. + this->kind = AppActionKind::Plugin; + // Icon is left empty on purpose: the webview builds resources/images/.svg, which a + // plugin filesystem icon path would not resolve to. + } + + AppActionRunResult run(const std::string& /*param*/) const override + { + MainFrame* mf = wxGetApp().mainframe; + if (mf) + mf->plugin_pages().select_page({PluginCapabilityType::Pages, capability, plugin_key}); + return {AppActionRunResult::Level::Success}; + } +}; + +// Builds an action for a page capability, or nullptr if it is not a currently-loaded, +// enabled page capability. +std::unique_ptr make_page_action(const std::string& plugin_key, const std::string& capability, const std::string& source_name) +{ + PluginManager& manager = PluginManager::instance(); + if (!manager.is_plugin_loaded(plugin_key)) + return nullptr; + // only_enabled defaults true, so a disabled capability resolves to nullptr here. + if (!manager.get_plugin_capability({PluginCapabilityType::Pages, capability, plugin_key})) + return nullptr; + return std::make_unique(plugin_key, capability, source_name); +} + // ---- built-in command actions (the speed dial "commands" section) ------ constexpr const char* kSettingPrefix = "orca_setting"; @@ -311,13 +361,20 @@ void ActionRegistry::init() }); }; auto on_capability = [this](const PluginCapabilityId& capability, ActionChange change) { - if (capability.type != PluginCapabilityType::Script || !wxTheApp || wxGetApp().is_closing()) + if (capability.type != PluginCapabilityType::Script && capability.type != PluginCapabilityType::Pages) return; - const std::string plugin_key = capability.plugin_key; - const std::string name = capability.name; - wxGetApp().CallAfter([this, plugin_key, name, change] { - if (!wxGetApp().is_closing()) + if (!wxTheApp || wxGetApp().is_closing()) + return; + const PluginCapabilityType type = capability.type; + const std::string plugin_key = capability.plugin_key; + const std::string name = capability.name; + wxGetApp().CallAfter([this, type, plugin_key, name, change] { + if (wxGetApp().is_closing()) + return; + if (type == PluginCapabilityType::Script) this->refresh_capability(plugin_key, name, change); + else + this->refresh_page_capability(plugin_key, name, change); }); }; @@ -347,6 +404,16 @@ void ActionRegistry::init() upsert(std::move(action)); } + for (const auto& capability : manager.get_plugin_capabilities("", PluginCapabilityType::Pages)) { + if (!capability) + continue; + const std::string& key = capability->audit_plugin_key(); + auto it = source_names.find(key); + const std::string& source_name = it == source_names.end() ? key : it->second; + if (auto action = make_page_action(key, capability->name(), source_name)) + upsert(std::move(action)); + } + // Built-in palette commands (Save/Load, Preferences, Mode switch, Slice/Preview, Go to layer). // Register after plugins so the plugin ids win on any (unlikely) id collision - ids are distinct // by prefix, so this is order-independent. The catalog (and its thin AppAction adapter) lives in @@ -401,6 +468,12 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange if (auto action = make_action(plugin_key, capability->name(), source_name)) upsert(std::move(action)); } + for (const auto& capability : manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::Pages)) { + if (!capability) + continue; + if (auto action = make_page_action(plugin_key, capability->name(), source_name)) + upsert(std::move(action)); + } } void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change) @@ -420,6 +493,23 @@ void ActionRegistry::refresh_capability(const std::string& plugin_key, const std remove(id); } +void ActionRegistry::refresh_page_capability(const std::string& plugin_key, const std::string& capability, ActionChange change) +{ + assert(wxThread::IsMain()); + + const std::string id = PluginPageAction::id_for(plugin_key, capability); + if (change == ActionChange::Removed) { + remove(id); + return; + } + + PluginManager& manager = PluginManager::instance(); + if (auto action = make_page_action(plugin_key, capability, find_loaded_source_name(manager, plugin_key))) + upsert(std::move(action)); + else + remove(id); +} + void ActionRegistry::upsert(std::unique_ptr action) { assert(wxThread::IsMain()); diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index 61cdbffd25..4d5800907f 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -239,6 +239,7 @@ private: // one plugin's whole action set; refresh_capability touches a single capability. void refresh_source(const std::string& plugin_key, ActionChange change); void refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change); + void refresh_page_capability(const std::string& plugin_key, const std::string& capability, ActionChange change); bool m_started = false; // init() runs exactly once; guards double-subscription std::unordered_map> m_actions; // UI-thread confined; no lock diff --git a/src/slic3r/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp index f5d0bf8f91..cf4ed6a6a8 100644 --- a/src/slic3r/plugin/host/PluginPages.cpp +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -400,6 +400,17 @@ void PluginPages::relayout() } } +void PluginPages::select_page(const PluginCapabilityId& id) +{ + if (m_parent == nullptr || m_pages.find(id) == m_pages.end()) + return; + + // Swap the page into the visible slot first when it lives behind the overflow menu. + m_swapped_in_id = id; + relayout(); + m_parent->SelectPageByName(page_tab_id(id)); +} + void PluginPages::show_overflow_menu() { const int visible_slots = std::max(1, m_visible_page_count); diff --git a/src/slic3r/plugin/host/PluginPages.hpp b/src/slic3r/plugin/host/PluginPages.hpp index 962e02ecd0..61c64f53d5 100644 --- a/src/slic3r/plugin/host/PluginPages.hpp +++ b/src/slic3r/plugin/host/PluginPages.hpp @@ -66,6 +66,8 @@ public: void relayout(); + void select_page(const PluginCapabilityId& id); + private: std::shared_ptr get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const; bool create_page(const PluginCapabilityId& id); From 8585eae816da96b27a0c1e8344266307af12a04b Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Sat, 10 Oct 2026 00:26:02 +0800 Subject: [PATCH 4/5] fix: hide symbols of the bundled static openssl (#16317) * fix: hide symbols of the bundled static openssl * fix: hide the bundled static OpenSSL symbols on Linux * fix: relink _ssl/_hashlib when OpenSSL recipe changes --- deps/OpenSSL/OpenSSL.cmake | 29 +++++++++++++++++++++++++++++ deps/python3/python3.cmake | 24 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/deps/OpenSSL/OpenSSL.cmake b/deps/OpenSSL/OpenSSL.cmake index 2bbf2b81b7..35b17ed50e 100644 --- a/deps/OpenSSL/OpenSSL.cmake +++ b/deps/OpenSSL/OpenSSL.cmake @@ -44,6 +44,18 @@ else() if(APPLE) set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}) else() + # A static library that is embedded into a shared object must not export + # its symbols. On Linux the running process also loads the system OpenSSL + # 3.x (WebKitGTK/gnutls pull in libcrypto.so.3), and CPython's _ssl and + # _hashlib are dlopened (RTLD_LOCAL) DSOs that each embed this OpenSSL. + # With default visibility their unversioned OpenSSL references are + # preempted by that global 3.x copy, mixing the 1.1.1 and 3.x ABIs and + # corrupting the heap (ssl.create_default_context() aborts). Hidden + # visibility makes each embedded copy self-contained. Linux-only: macOS + # binds dylibs with a two-level namespace (no interposition) and ships no + # OpenSSL, and Windows has no equivalent flag and no system OpenSSL to + # collide with. + set(_openssl_extra_cflags -fvisibility=hidden) set(_conf_cmd env "CC=${CMAKE_C_COMPILER}" "LDFLAGS=${CMAKE_EXE_LINKER_FLAGS}" "./config") endif() set(_cross_comp_prefix_line "") @@ -102,3 +114,20 @@ ExternalProject_Add_Step(dep_OpenSSL install_cmake_files COMMAND ${CMAKE_COMMAND} -E copy_directory openssl "${DESTDIR}${CMAKE_INSTALL_LIBDIR}/cmake/openssl" WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" ) + +if (NOT WIN32 AND NOT APPLE) + # OpenSSL's object rules do not depend on CFLAGS, so reconfiguring it (for + # example to add -fvisibility=hidden) relinks the archives from stale + # objects instead of recompiling them, and the change silently has no + # effect. Drop the objects whenever this recipe changes so the next build + # actually recompiles them. + ExternalProject_Get_Property(dep_OpenSSL SOURCE_DIR) + ExternalProject_Add_Step(dep_OpenSSL clean_objects + DEPENDEES configure + DEPENDERS build + COMMAND make clean + WORKING_DIRECTORY "${SOURCE_DIR}" + DEPENDS "${CMAKE_CURRENT_LIST_FILE}" + COMMENT "OpenSSL: cleaning objects after a recipe change" + ) +endif () diff --git a/deps/python3/python3.cmake b/deps/python3/python3.cmake index 6b4443ef3a..b0415f2bb5 100644 --- a/deps/python3/python3.cmake +++ b/deps/python3/python3.cmake @@ -299,3 +299,27 @@ endif() if(TARGET dep_ZLIB) add_dependencies(dep_python3 dep_ZLIB) endif() + +if (NOT WIN32 AND NOT APPLE) + # CPython's Makefile rules for _ssl and _hashlib depend only on their own + # sources, not on the OpenSSL archives, so a rebuilt OpenSSL does not make + # them relink and they keep the previous symbols. On an incremental tree, + # drop the built modules and relink them against the current OpenSSL; a + # fresh build is left alone (its PGO target builds them). "make" alone is a + # no-op once PGO has run, so sharedmods is invoked explicitly. + ExternalProject_Get_Property(dep_python3 SOURCE_DIR) + file(GLOB _python_ssl_modules + "${SOURCE_DIR}/Modules/_ssl*.so" + "${SOURCE_DIR}/Modules/_hashlib*.so") + if (_python_ssl_modules) + ExternalProject_Add_Step(dep_python3 relink_ssl_extensions + DEPENDEES configure + DEPENDERS build + COMMAND sh -c "rm -f '${SOURCE_DIR}'/Modules/_ssl*.so '${SOURCE_DIR}'/Modules/_hashlib*.so && make -j${NPROC} sharedmods" + WORKING_DIRECTORY "${SOURCE_DIR}" + COMMENT "CPython: relinking _ssl/_hashlib against the current OpenSSL" + DEPENDS "${CMAKE_CURRENT_LIST_FILE}" + "${CMAKE_CURRENT_LIST_DIR}/../OpenSSL/OpenSSL.cmake" + ) + endif () +endif () From b5ef24e7ffa1b928071a22195657e8dc1db9360c Mon Sep 17 00:00:00 2001 From: Misterff1 Date: Fri, 9 Oct 2026 18:32:46 +0200 Subject: [PATCH 5/5] Fix regression: Arc Fitting setting for BBL P2S (#16319) Disable Arc Fitting for BBL P2S --- .../BBL/process/0.08mm High Quality @BBL P2S 0.2 nozzle.json | 1 + resources/profiles/BBL/process/0.08mm High Quality @BBL P2S.json | 1 + .../BBL/process/0.10mm Standard @BBL P2S 0.2 nozzle.json | 1 + .../BBL/process/0.12mm Balanced Quality @BBL P2S 0.2 nozzle.json | 1 + resources/profiles/BBL/process/0.12mm High Quality @BBL P2S.json | 1 + resources/profiles/BBL/process/0.16mm High Quality @BBL P2S.json | 1 + resources/profiles/BBL/process/0.16mm Standard @BBL P2S.json | 1 + .../BBL/process/0.18mm Balanced Quality @BBL P2S 0.6 nozzle.json | 1 + resources/profiles/BBL/process/0.20mm High Quality @BBL P2S.json | 1 + resources/profiles/BBL/process/0.20mm Standard @BBL P2S.json | 1 + .../BBL/process/0.24mm Balanced Quality @BBL P2S 0.6 nozzle.json | 1 + .../BBL/process/0.24mm Balanced Quality @BBL P2S 0.8 nozzle.json | 1 + resources/profiles/BBL/process/0.24mm Standard @BBL P2S.json | 1 + .../BBL/process/0.30mm Standard @BBL P2S 0.6 nozzle.json | 1 + .../BBL/process/0.32mm Balanced Quality @BBL P2S 0.8 nozzle.json | 1 + .../BBL/process/0.40mm Standard @BBL P2S 0.8 nozzle.json | 1 + 16 files changed, 16 insertions(+) diff --git a/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S 0.2 nozzle.json index 8ca40003f3..44bd17e125 100644 --- a/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S 0.2 nozzle.json @@ -10,6 +10,7 @@ "50", "50" ], + "enable_arc_fitting": "0", "default_acceleration": [ "4000", "4000" diff --git a/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S.json b/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S.json index bd399e65a1..1867783ab5 100644 --- a/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S.json +++ b/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S.json @@ -10,6 +10,7 @@ "50", "50" ], + "enable_arc_fitting": "0", "default_acceleration": [ "4000", "4000", diff --git a/resources/profiles/BBL/process/0.10mm Standard @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/process/0.10mm Standard @BBL P2S 0.2 nozzle.json index 02ec0007ec..2d00b3d04e 100644 --- a/resources/profiles/BBL/process/0.10mm Standard @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/process/0.10mm Standard @BBL P2S 0.2 nozzle.json @@ -9,6 +9,7 @@ "50", "50" ], + "enable_arc_fitting": "0", "default_acceleration": [ "4000", "4000" diff --git a/resources/profiles/BBL/process/0.12mm Balanced Quality @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/process/0.12mm Balanced Quality @BBL P2S 0.2 nozzle.json index e3471e88b0..37194e5627 100644 --- a/resources/profiles/BBL/process/0.12mm Balanced Quality @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/process/0.12mm Balanced Quality @BBL P2S 0.2 nozzle.json @@ -9,6 +9,7 @@ "50", "50" ], + "enable_arc_fitting": "0", "default_acceleration": [ "4000", "4000" diff --git a/resources/profiles/BBL/process/0.12mm High Quality @BBL P2S.json b/resources/profiles/BBL/process/0.12mm High Quality @BBL P2S.json index a9c9eb7bdf..2847f1cae2 100644 --- a/resources/profiles/BBL/process/0.12mm High Quality @BBL P2S.json +++ b/resources/profiles/BBL/process/0.12mm High Quality @BBL P2S.json @@ -10,6 +10,7 @@ "50", "50" ], + "enable_arc_fitting": "0", "default_acceleration": [ "4000", "4000", diff --git a/resources/profiles/BBL/process/0.16mm High Quality @BBL P2S.json b/resources/profiles/BBL/process/0.16mm High Quality @BBL P2S.json index 4d6e809633..cdd946b32f 100644 --- a/resources/profiles/BBL/process/0.16mm High Quality @BBL P2S.json +++ b/resources/profiles/BBL/process/0.16mm High Quality @BBL P2S.json @@ -11,6 +11,7 @@ "50", "50" ], + "enable_arc_fitting": "0", "default_acceleration": [ "4000", "4000", diff --git a/resources/profiles/BBL/process/0.16mm Standard @BBL P2S.json b/resources/profiles/BBL/process/0.16mm Standard @BBL P2S.json index 657b0b7c38..41bc2ba0f0 100644 --- a/resources/profiles/BBL/process/0.16mm Standard @BBL P2S.json +++ b/resources/profiles/BBL/process/0.16mm Standard @BBL P2S.json @@ -11,6 +11,7 @@ "50", "50" ], + "enable_arc_fitting": "0", "default_acceleration": [ "10000", "10000", diff --git a/resources/profiles/BBL/process/0.18mm Balanced Quality @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/process/0.18mm Balanced Quality @BBL P2S 0.6 nozzle.json index 4d2722df04..2103b9cc04 100644 --- a/resources/profiles/BBL/process/0.18mm Balanced Quality @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/process/0.18mm Balanced Quality @BBL P2S 0.6 nozzle.json @@ -11,6 +11,7 @@ "30", "30" ], + "enable_arc_fitting": "0", "default_acceleration": [ "10000", "10000", diff --git a/resources/profiles/BBL/process/0.20mm High Quality @BBL P2S.json b/resources/profiles/BBL/process/0.20mm High Quality @BBL P2S.json index 2a76fcba00..46a148408c 100644 --- a/resources/profiles/BBL/process/0.20mm High Quality @BBL P2S.json +++ b/resources/profiles/BBL/process/0.20mm High Quality @BBL P2S.json @@ -11,6 +11,7 @@ "50", "50" ], + "enable_arc_fitting": "0", "default_acceleration": [ "4000", "4000", diff --git a/resources/profiles/BBL/process/0.20mm Standard @BBL P2S.json b/resources/profiles/BBL/process/0.20mm Standard @BBL P2S.json index 13c9f1e722..54ec7a734a 100644 --- a/resources/profiles/BBL/process/0.20mm Standard @BBL P2S.json +++ b/resources/profiles/BBL/process/0.20mm Standard @BBL P2S.json @@ -11,6 +11,7 @@ "50", "50" ], + "enable_arc_fitting": "0", "default_acceleration": [ "10000", "10000", diff --git a/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.6 nozzle.json index 24f35fc875..6ca9a556a9 100644 --- a/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.6 nozzle.json @@ -11,6 +11,7 @@ "30", "30" ], + "enable_arc_fitting": "0", "default_acceleration": [ "10000", "10000", diff --git a/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.8 nozzle.json index 64c46e6414..47bcce8c51 100644 --- a/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.8 nozzle.json @@ -10,6 +10,7 @@ "30", "30" ], + "enable_arc_fitting": "0", "default_acceleration": [ "10000", "10000" diff --git a/resources/profiles/BBL/process/0.24mm Standard @BBL P2S.json b/resources/profiles/BBL/process/0.24mm Standard @BBL P2S.json index bec3147a16..90ef61fe8c 100644 --- a/resources/profiles/BBL/process/0.24mm Standard @BBL P2S.json +++ b/resources/profiles/BBL/process/0.24mm Standard @BBL P2S.json @@ -10,6 +10,7 @@ "50", "50" ], + "enable_arc_fitting": "0", "default_acceleration": [ "10000", "10000", diff --git a/resources/profiles/BBL/process/0.30mm Standard @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/process/0.30mm Standard @BBL P2S 0.6 nozzle.json index d745625988..22171f7b4f 100644 --- a/resources/profiles/BBL/process/0.30mm Standard @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/process/0.30mm Standard @BBL P2S 0.6 nozzle.json @@ -11,6 +11,7 @@ "30", "30" ], + "enable_arc_fitting": "0", "default_acceleration": [ "10000", "10000", diff --git a/resources/profiles/BBL/process/0.32mm Balanced Quality @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/process/0.32mm Balanced Quality @BBL P2S 0.8 nozzle.json index 979edcca6e..b6f59a2ce1 100644 --- a/resources/profiles/BBL/process/0.32mm Balanced Quality @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/process/0.32mm Balanced Quality @BBL P2S 0.8 nozzle.json @@ -10,6 +10,7 @@ "30", "30" ], + "enable_arc_fitting": "0", "default_acceleration": [ "10000", "10000" diff --git a/resources/profiles/BBL/process/0.40mm Standard @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/process/0.40mm Standard @BBL P2S 0.8 nozzle.json index d3aaa84887..843a06b9a9 100644 --- a/resources/profiles/BBL/process/0.40mm Standard @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/process/0.40mm Standard @BBL P2S 0.8 nozzle.json @@ -10,6 +10,7 @@ "30", "30" ], + "enable_arc_fitting": "0", "default_acceleration": [ "10000", "10000"