Compare commits

..
Author SHA1 Message Date
peachismomo 0bf0901a95 fix: use WEBKIT_DISABLE_DMABUF_RENDERER 1 when using WebKitGTK >= 2.46 2026-10-09 03:31:44 +08:00
Error404JoyNotFound 9d32c1c545 docs: correct layer_num index tooltip to 0-based (fixes #10340) (#15984) 2026-10-08 15:44:03 -03:00
Ian Chua 67a16dabca fix: prompt for permission when plugin tries to create a thread (#16248)
* fix: prompt for permission when plugin tries to create a thread

* fix: request permission on main thread
2026-10-08 23:49:33 +08:00
Ian Bassi 59fc97fb28 Extend Separated Infills (#16274) 2026-10-08 09:43:46 -03:00
LixNix 7d44b60ae4 H2D X2D Multi Nozzle Size Printing Support (#14125) 2026-10-08 20:28:32 +08:00
ExPikaPakaandExPikaPaka 776ca6a3e4 build_linux.sh: add -J to build several dependencies at once (#16282)
* build_linux.sh: add -J to build several dependencies at once

The top-level deps build is fixed at -j1, so one dependency compiles at a
time while the small ones leave most cores idle. -J N raises that level.

-j still applies in full to each dependency, so the worst case is -J times
-j compile jobs: ninja has no job server to share a pool across the nested
builds. Without -J nothing changes.

* Quote the job count for shellcheck (SC2086)

---------

Co-authored-by: ExPikaPaka <mrfsfyt@gmail.com>
2026-10-08 18:56:02 +08:00
SoftFeverandxxxsam ee26e94170 Fix H2D prints rejected for a missing Filament Track Switch
Fixes #15927

Co-authored-by: xxxsam <31843242+xxxsam92123@users.noreply.github.com>
2026-10-08 17:49:04 +08:00
46 changed files with 938 additions and 424 deletions
@@ -61,8 +61,10 @@ available; GTK2/WebKit1 is an opt-out configuration the GUI does not support.
11. A web host that re-themes in place handles `EVT_WEBVIEW_RECREATED` without `Skip()`; one that
needs a reload lets it through. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
12. Keep `WebViewWebKit`'s destructor removing the `"wx"` handler. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond Orca's default-path XWayland case, and never
make a page's progress depend only on a C++→JS callback. §[WebKitGTK on Linux](#webkitgtk-on-linux-sessions)
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond Orca's default-path XWayland case. Disable the
WebKit DMA-BUF renderer only for the NVIDIA proprietary driver on native Wayland (GNOME/gtk#8056), and
never make a page's progress depend only on a C++→JS callback.
§[WebKitGTK on Linux](#webkitgtk-on-linux-sessions)
14. GL attribute lists: legacy `int[]` lists end with `0` and spell out `WX_GL_RGBA` and
`WX_GL_DOUBLEBUFFER`; `wxGLAttributes`/`wxGLContextAttrs` end with `EndList()`; MSAA is requested
explicitly. §[wxGLCanvas](#wxglcanvas-and-wxglcontext)
@@ -599,6 +601,25 @@ Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_
Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit`
`setTimeout("JumpToTarget()", …)`), 9446030079 (the `GDK_BACKEND=x11` opt-in branch).
**DMA-BUF renderer / explicit sync (NVIDIA + Wayland).**
- **Rule:** On the Wayland default path, set `WEBKIT_DISABLE_DMABUF_RENDERER=1` non-replacing, gated to
`__linux__` + native Wayland (not the forced-X11 fallback) + `/proc/driver/nvidia/version` +
`WebView::WebKitAtLeast(2, 46)`. The version gate uses the runtime `webkit_get_major_version()` /
`webkit_get_minor_version()` getters (no GTK init needed), not a compile-time macro, and 2.46 is the
oldest release with a confirmed crash report — narrower gates (e.g. 2.54) would miss 2.46–2.52 victims.
**Why:** WebKitGTK's DMA-BUF renderer arms `linux-drm-syncobj-v1` on the toplevel surface; GTK3 then
commits its first shared-memory frame without an acquire point, and KWin/Mutter reject the commit with
`Error 71 (Protocol error)` — GDK turns that into `_exit(1)` before Orca logs anything. The defect is
GTK3's `gdk_wayland_window_attach_image()` (GNOME/gtk#8056); WebKit only triggers it, the NVIDIA
proprietary driver has no implicit-sync fallback (Mesa tolerates it), and no upstream fix or PR exists.
This is a different variable from `WEBKIT_DISABLE_COMPOSITING_MODE` and does not replace it.
```cpp
// Wayland default path, before GTK init
if (!x11_backend && wayland && ::access("/proc/driver/nvidia/version", F_OK) == 0 &&
WebView::WebKitAtLeast(2, 46))
::setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", /* replace */ false);
```
## wxGLCanvas and wxGLContext
**Attributes.**
+36 -3
View File
@@ -8,9 +8,10 @@ SCRIPT_PATH=$(dirname "$(readlink -f "${0}")")
pushd "${SCRIPT_PATH}" > /dev/null
function usage() {
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-p][-r][-s][-t][-u][-l][-L]"
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-J N][-p][-r][-s][-t][-u][-l][-L]"
echo " -1: limit builds to one core (where possible)"
echo " -j N: limit builds to N cores (where possible)"
echo " -J N: build up to N dependencies at a time, each still using -j jobs (default: 1)"
echo " -b: build in Debug mode"
echo " -c: force a clean build"
echo " -C: enable ANSI-colored compile output (GNU/Clang only)"
@@ -36,12 +37,13 @@ function usage() {
}
SLIC3R_PRECOMPILED_HEADERS="ON"
DEPS_PARALLEL=""
unset name
BUILD_DIR=build
BUILD_CONFIG=Release
FORWARDED_ARGS=()
while getopts ":1j:bcCdDeFghiprstulL" opt ; do
while getopts ":1j:J:bcCdDeFghiprstulL" opt ; do
case ${opt} in
1 )
export CMAKE_BUILD_PARALLEL_LEVEL=1
@@ -51,6 +53,10 @@ while getopts ":1j:bcCdDeFghiprstulL" opt ; do
export CMAKE_BUILD_PARALLEL_LEVEL=$OPTARG
FORWARDED_ARGS+=("-j" "$OPTARG")
;;
J )
DEPS_PARALLEL=$OPTARG
FORWARDED_ARGS+=("-J" "$OPTARG")
;;
b )
BUILD_DIR=build-dbg
BUILD_CONFIG=Debug
@@ -135,6 +141,11 @@ if [[ -n "${CLEAN_DOCKER_IMAGE}" ]] && [[ -z "${USE_DOCKER}" ]] ; then
exit 1
fi
if [[ -n "${DEPS_PARALLEL}" ]] && ! [[ "${DEPS_PARALLEL}" =~ ^[1-9][0-9]*$ ]] ; then
echo "Error: -J expects a positive integer."
exit 1
fi
function check_available_memory_and_disk() {
FREE_MEM_GB=$(free --gibi --total | grep 'Mem' | rev | cut --delimiter=" " --fields=1 | rev)
MIN_MEM_GB=10
@@ -537,7 +548,29 @@ if [[ -n "${BUILD_DEPS}" ]] ; then
fi
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}"
print_and_run cmake --build deps/$BUILD_DIR -j1
# The top-level build runs one dependency at a time by default, which keeps the console
# output readable and lets that dependency's own build use all of CMAKE_BUILD_PARALLEL_LEVEL.
# -J raises the top level instead, and -j still applies in full to every dependency, so the
# worst case is -J times -j compile jobs at once. Ninja has no job server to share a pool
# across the nested builds, so that ceiling is not enforced anywhere: pick -J to suit the RAM.
DEPS_JOBS=1
if [[ -n "${DEPS_PARALLEL}" ]] ; then
DEPS_JOBS=${DEPS_PARALLEL}
SAVED_PARALLEL_LEVEL=${CMAKE_BUILD_PARALLEL_LEVEL-}
export CMAKE_BUILD_PARALLEL_LEVEL=${CMAKE_BUILD_PARALLEL_LEVEL:-$(nproc)}
echo "Building up to ${DEPS_JOBS} dependencies at a time, ${CMAKE_BUILD_PARALLEL_LEVEL} jobs each: up to $(( DEPS_JOBS * CMAKE_BUILD_PARALLEL_LEVEL )) compile jobs at once."
fi
print_and_run cmake --build deps/$BUILD_DIR -j"${DEPS_JOBS}"
if [[ -n "${DEPS_PARALLEL}" ]] ; then
# Give the whole -j back to the OrcaSlicer build below.
if [[ -n "${SAVED_PARALLEL_LEVEL}" ]] ; then
export CMAKE_BUILD_PARALLEL_LEVEL=${SAVED_PARALLEL_LEVEL}
else
unset CMAKE_BUILD_PARALLEL_LEVEL
fi
fi
fi
if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
+90
View File
@@ -0,0 +1,90 @@
# Separated infills — High Level Design
## Purpose and scope
An object's infill patterns are laid out from one reference point, the center
of the object. When an object groups several parts that do not touch, every
part cuts the same object-wide pattern at a different place, so equal parts get
different infill. `separated_infills` lays the infill of every connected body
out from the center of that body instead, as if the body were sliced on its own.
The option covers sparse infill, internal solid infill and bridges. Top and
bottom surfaces are left to `center_of_surface_pattern`, which centers the
Archimedean Chords and Octagram Spiral surface patterns. The option is off by
default; with it off, or for an object made of a single body, no fill changes.
Adaptive Cubic and Support Cubic do not depend on the option: they always fill
each body on its own (see Octree infill).
## Bodies
`PrintObject::prepare_infill()` groups the islands of every layer (`lslices`)
into 3D connected bodies before bridges are detected, so bridge anchors and
printed infill share one origin. Islands on adjacent layers belong to one body
when their slices overlap. Parts that touch or overlap form one body. Separate
parts, disconnected islands of one mesh, and interleaved parts that never touch,
such as chain links, each form their own. Every island stores the index of its
body in `Layer::lslices_separated_component_ids`, and
`PrintObject::separated_body_bboxes()` holds the bounding box of each body over
all its layers.
The pass runs when a region uses separated infills, per-model surface centering
or an octree infill pattern. It is skipped when the object has one model part
that cannot be split, since a single body already shares the object center.
## Centering a fill
`infill_body()` matches each fill region to the island it overlaps most, among
the islands whose bounding boxes overlap it, and the filler takes the bounding
box of that island's body instead of the object's. The box covers every layer
of the body, which is the box the body gets when sliced alone, so patterns that
depend on its extent as well as its center come out the same too. Bridge
anchoring (`Layer::generate_sparse_infill_polylines_for_anchoring()`) makes the
same choice, so the anchors match the printed infill.
The patterns follow the body's box in one of two ways:
- Rectilinear and its variants, Line, Grid, Triangles, Tri-hexagon, Cubic,
Quarter Cubic, Lateral Lattice, Lateral Honeycomb and the plane-path patterns
(Hilbert Curve, Archimedean Chords, Octagram Spiral) are laid out from the
box: they phase their lines through its center, and Hilbert Curve and the Zig
Zag links start from its corner. `Fill::extended_object_bounding_box()`
extends the box about its center, so it also serves a box that is not
centered on the origin.
- Honeycomb, 3D Honeycomb, Cross Hatch, Gyroid, TPMS-D and TPMS-FK are laid out
from the coordinate origin, which is the object center. They return true from
`Fill::aligned_to_origin()`, and `Fill::fill_surface()` moves each region so
that the box center lands on the origin, fills it, and moves the paths back.
With the default box the center is the origin, so nothing moves.
`is_separable_infill_pattern()` lists these patterns. The settings show the
option only when the sparse infill pattern is one of them.
## Octree infill
Adaptive Cubic and Support Cubic take their lines from an octree, laid out from
the center of the mesh it is built from and refined near its surfaces. An
octree of the whole object would lay every part out from the object's center
and refine it near the other parts, so these patterns
(`is_octree_infill_pattern()`) always fill each body on its own, and the
settings hide the option for them.
For an object of several bodies, `PrintObject::prepare_adaptive_infill_data()`
builds one octree per body (`FillAdaptive::Octrees`) from the triangles of that
body only, which is the octree the body gets when sliced alone. Each connected
component of the mesh goes to the body that most of a few sampled triangles lie
on. A sample is taken a layer height inside the solid, behind the triangle, and
looked up in the islands of the nearest layer. Each internal bridge surface goes
to the body of its island. The fill takes the octree of the region's body, from
the same `infill_body()`. The octree of the whole object is built only for an
object of a single body, or when some body received no triangles, which then
uses it.
## Patterns left out
Lightning grows its trees over the whole object, so moving a reference point
cannot center it on one body. Concentric and Spiral Inset follow the outline of
each region and need no centering.
Solid infill at full density spaces its lines over the extent of each region,
so it is already independent of the other bodies. Only bridges, which keep
their line spacing, and the plane-path solid patterns depend on the center.
+35
View File
@@ -165,6 +165,7 @@ using json = nlohmann::json;
#ifdef SLIC3R_GUI
#include "slic3r/GUI/GUI_Init.hpp"
#include "slic3r/GUI/Widgets/WebView.hpp"
// BBLPrinterAgent::from_orca_filament_id(); the map and its lookups live in libslic3r_gui,
// which only a SLIC3R_GUI build links (see target_link_libraries(OrcaSlicer libslic3r_gui)
// in CMakeLists).
@@ -1155,6 +1156,12 @@ int CLI::run(int argc, char **argv)
// unset preserves WebKit hardware acceleration on Device / Setup
// Wizard / login / store. The default path still applies it on
// XWayland sessions as a conservative fallback for older WebKit.
//
// The default path also disables WebKit's DMA-BUF renderer when the
// NVIDIA proprietary driver is in use on native Wayland. That is a
// separate switch from compositing and is needed because GTK3 commits
// a shared-memory frame onto an explicit-sync surface (GNOME/gtk#8056),
// which KWin/Mutter reject with "Error 71 (Protocol error)".
// ------------------------------------------------------------------
{
const char* gdk_backend = ::getenv("GDK_BACKEND");
@@ -1218,6 +1225,34 @@ int CLI::run(int argc, char **argv)
}
}
// WebKitGTK's DMA-BUF renderer arms explicit sync
// (linux-drm-syncobj-v1) on the toplevel Wayland surface. If the
// first painted frame is a shared-memory buffer, GTK3's
// gdk_wayland_window_attach_image() commits it without an acquire
// point, and compositors that enforce the protocol (KWin, Mutter)
// terminate the client with "Error 71 (Protocol error)" before the
// app can log anything (GNOME/gtk#8056). NVIDIA's proprietary
// driver has no implicit-sync fallback, so the crash only happens
// there; Mesa tolerates the missing point. Scope the workaround to
// NVIDIA on native Wayland, on WebKitGTK >= 2.46 (the oldest release
// with a confirmed report; the DMA-BUF renderer predates it).
// Non-replacing, so a user who does not hit the bug can opt back in
// with WEBKIT_DISABLE_DMABUF_RENDERER=0.
#if defined(__linux__)
{
const char* gdk_backend_wk = ::getenv("GDK_BACKEND");
// The EGL-less fallback above may have just forced X11.
const bool x11_backend_wk = gdk_backend_wk && boost::starts_with(gdk_backend_wk, "x11");
const char* wayland_env_dmabuf = ::getenv("WAYLAND_DISPLAY");
if (!x11_backend_wk && wayland_env_dmabuf && *wayland_env_dmabuf &&
::access("/proc/driver/nvidia/version", F_OK) == 0 &&
WebView::WebKitAtLeast(2, 46)) {
BOOST_LOG_TRIVIAL(info) << "NVIDIA proprietary driver on Wayland: disabling the WebKit DMA-BUF renderer (GNOME/gtk#8056 workaround).";
::setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", /* replace */ false);
}
}
#endif
// XInitThreads is needed before GStreamer may use Xlib. On
// native Wayland without DISPLAY, GStreamer uses waylandsink
// (no Xlib involved), so the call is skipped.
+31 -25
View File
@@ -29,6 +29,7 @@
#include "ExtrusionEntity.hpp"
#include "Fill.hpp"
#include "libslic3r/Fill/FillBase.hpp"
#include "FillAdaptive.hpp"
#include "FillRectilinear.hpp"
#include "FillLightning.hpp"
#include "FillConcentricInternal.hpp"
@@ -926,7 +927,6 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.lateral_lattice_angle_2 = region_config.lateral_lattice_angle_2;
params.infill_overhang_angle = region_config.infill_overhang_angle;
params.center_of_surface_pattern = region_config.center_of_surface_pattern;
params.separated_infills = region_config.separated_infills;
if (params.pattern == ipLockedZag) {
params.infill_lock_depth = scale_(region_config.infill_lock_depth);
params.skin_infill_depth = scale_(region_config.skin_infill_depth);
@@ -999,6 +999,9 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
// (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.
params.separated_infills = region_config.separated_infills && is_separable_infill_pattern(params.pattern) &&
params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface;
if (params.extrusion_role == erInternalInfill) {
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
@@ -1073,7 +1076,7 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
//get locked region param
if (params.pattern == ipLockedZag){
const PrintObject *object = layerm.layer()->object();
auto nozzle_diameter = float(object->print()->config().nozzle_diameter.get_at(layerm.region().extruder(extrusion_role) - 1));
auto nozzle_diameter = float(nozzle_diameter_for_filament(object->print()->config(), layerm.region().extruder(extrusion_role), object->print()->is_BBL_printer()));
Flow skin_flow = params.bridge ? params.flow : Flow::new_from_config_width(extrusion_role, region_config.skin_infill_line_width, nozzle_diameter, float((surface.thickness == -1) ? layer.height : surface.thickness));
//add skin flow
append_flow_param(lock_param.skin_flow_params, skin_flow, surface.expolygon);
@@ -1271,29 +1274,28 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
// Orca: Anchors and printed infill must share the same body origin. Keep the choice
// here so per-model surface centering and separated sparse infill cannot drift apart.
static BoundingBox infill_bounding_box(const Layer &layer, const SurfaceFill &fill, const ExPolygon &expoly, BoundingBox bbox)
// Returns the connected body the fill region is laid out on, or -1 to keep the object's origin.
static int infill_body(const Layer &layer, const SurfaceFill &fill, const ExPolygon &expoly)
{
const auto &params = fill.params;
const auto &config = layer.regions()[fill.region_id]->region().config();
const bool external = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface;
const bool per_model = external && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model &&
const bool per_model = (params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface) &&
params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model &&
(params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral);
const bool separate = !external && params.separated_infills &&
(is_separable_infill_pattern(params.pattern) || !config.solid_infill_rotate_template.value.empty() ||
!config.sparse_infill_rotate_template.value.empty());
if (per_model || separate) {
double best_overlap = 0.;
for (size_t i = 0; i < layer.lslices.size() && i < layer.lslices_separated_component_bboxes.size(); ++i) {
int body = -1;
if (per_model || params.separated_infills || is_octree_infill_pattern(params.pattern)) {
const BoundingBox box = get_extents(expoly);
double best_overlap = 0.;
for (size_t i = 0; i < layer.lslices.size() && i < layer.lslices_separated_component_ids.size(); ++i) {
if (! layer.lslices_bboxes[i].overlap(box))
continue;
const double overlap = area(intersection_ex(layer.lslices[i], expoly));
if (overlap > best_overlap) {
best_overlap = overlap;
const Point center = layer.lslices_separated_component_bboxes[i].center();
bbox = layer.object()->bounding_box();
bbox.translate(center.x(), center.y());
body = int(layer.lslices_separated_component_ids[i]);
}
}
}
return bbox;
return body;
}
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
@@ -1318,7 +1320,7 @@ void export_group_fills_to_svg(const char *path, const std::vector<SurfaceFill>
#endif
// friend to Layer
void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive::Octree* support_fill_octree, FillLightning::Generator* lightning_generator)
void Layer::make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator)
{
for (LayerRegion *layerm : m_regions)
layerm->fills.clear();
@@ -1351,7 +1353,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
f->z = this->print_z;
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
f->adapt_fill_octree = (surface_fill.params.pattern == ipSupportCubic) ? support_fill_octree : adaptive_fill_octree;
const FillAdaptive::Octrees *octrees = surface_fill.params.pattern == ipSupportCubic ? support_fill_octrees : adaptive_fill_octrees;
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
if (surface_fill.params.pattern == ipConcentricInternal) {
@@ -1443,8 +1445,10 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
params.can_reverse = false;
for (ExPolygon& expoly : surface_fill.expolygons) {
// Orca: Reuse the body origin used for bridge anchoring, resetting it for each surface.
f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox));
// Orca: Reuse the body box and octree used for bridge anchoring, resetting them for each surface.
const int body = infill_body(*this, surface_fill, expoly);
f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox);
f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr;
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
if (params.symmetric_infill_y_axis) {
@@ -1512,7 +1516,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
* - For lightning/adaptive patterns, the respective generators are wired so their
* polylines match the final infill layout.
*/
Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive::Octree* support_fill_octree, FillLightning::Generator* lightning_generator) const
Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator) const
{
LockRegionParam skin_inner_param;
std::vector<SurfaceFill> surface_fills = group_fills(*this, skin_inner_param);
@@ -1570,7 +1574,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
f->z = this->print_z;
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
f->adapt_fill_octree = (surface_fill.params.pattern == ipSupportCubic) ? support_fill_octree : adaptive_fill_octree;
const FillAdaptive::Octrees *octrees = surface_fill.params.pattern == ipSupportCubic ? support_fill_octrees : adaptive_fill_octrees;
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
@@ -1617,8 +1621,10 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
params.extrusion_role = surface_fill.params.extrusion_role;
for (ExPolygon &expoly : surface_fill.expolygons) {
// Orca: Match the per-body origin of make_fills() before generating physical anchors.
f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox));
// Orca: Match the per-body box and octree of make_fills() before generating physical anchors.
const int body = infill_body(*this, surface_fill, expoly);
f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox);
f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr;
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
f->spacing = surface_fill.params.spacing;
surface_fill.surface.expolygon = std::move(expoly);
@@ -1788,7 +1794,7 @@ void Layer::make_ironing()
// Create the ironing extrusions for regions <i, j)
ExPolygons ironing_areas;
double nozzle_dmr = this->object()->print()->config().nozzle_diameter.get_at(ironing_params.extruder - 1);
double nozzle_dmr = nozzle_diameter_for_filament(this->object()->print()->config(), ironing_params.extruder, this->object()->print()->is_BBL_printer());
if (ironing_params.just_infill) {
//TODO just_infill is currently not used.
// Just infill.
+1
View File
@@ -25,6 +25,7 @@ public:
// pattern is placed on top of previous layers
bool use_bridge_flow() const override { return false; }
bool is_self_crossing() override { return false; }
bool aligned_to_origin() const override { return true; }
protected:
void _fill_surface_single(
+22
View File
@@ -14,6 +14,7 @@
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "FillBase.hpp"
#include <cstddef>
#include <memory>
#include <utility>
#include <Eigen/Geometry>
@@ -37,6 +38,27 @@ struct Octree;
struct OctreeDeleter { void operator()(Octree *p); };
using OctreePtr = std::unique_ptr<Octree, OctreeDeleter>;
// Orca: One octree per body (see Layer::lslices_separated_component_ids), and one of the whole object
// for objects of a single body or with a body that has none of its own.
struct Octrees
{
OctreePtr object;
std::vector<OctreePtr> bodies;
// A body without an octree, or body -1, uses the object's, or any body's when the object has none.
Octree *get(int body) const
{
if (body >= 0 && size_t(body) < bodies.size() && bodies[body])
return bodies[body].get();
if (object)
return object.get();
for (const OctreePtr &octree : bodies)
if (octree)
return octree.get();
return nullptr;
}
};
// Calculate line spacing for
// 1) adaptive cubic infill
// 2) adaptive internal support cubic infill
+12 -2
View File
@@ -128,6 +128,9 @@ Polylines Fill::fill_surface(const Surface *surface, const FillParams &params)
{
// Perform offset.
Slic3r::ExPolygons expp = offset_ex(surface->expolygon, float(scale_(this->overlap - 0.5 * this->spacing)));
// Orca: Separated infills move the box center onto each body; origin-aligned patterns follow it.
const Point shift = this->aligned_to_origin() && ! empty(this->bounding_box) ? this->bounding_box.center() : Point::Zero();
translate(expp, -shift);
// Create the infills for each of the regions.
Polylines polylines_out;
for (size_t i = 0; i < expp.size(); ++ i)
@@ -137,6 +140,8 @@ Polylines Fill::fill_surface(const Surface *surface, const FillParams &params)
_infill_direction(surface),
std::move(expp[i]),
polylines_out);
for (Polyline &pl : polylines_out)
pl.translate(shift);
return polylines_out;
}
@@ -1591,13 +1596,18 @@ BoundaryInfillGraph create_boundary_infill_graph(const Polylines &infill_ordered
// The extended bounding box of the whole object that covers any rotation of every layer.
BoundingBox Fill::extended_object_bounding_box() const
{
BoundingBox out = bounding_box;
// Orca: Extend about the box center, which separated infills move off the origin.
const Point c = this->bounding_box.center();
BoundingBox out = this->bounding_box;
out.translate(-c.x(), -c.y());
out.merge(Point(out.min.y(), out.min.x()));
out.merge(Point(out.max.y(), out.max.x()));
// The bounding box is scaled by sqrt(2.) to ensure that the bounding box
// covers any possible rotations.
return out.scaled(sqrt(2.));
out = out.scaled(sqrt(2.));
out.translate(c.x(), c.y());
return out;
}
void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Polygon*> &boundary_src, const BoundingBox &bbox, Polylines &polylines_out, const double spacing, const FillParams &params)
+3
View File
@@ -188,6 +188,9 @@ public:
// Return true if infill has a consistent pattern between layers.
virtual bool has_consistent_pattern() const { return false; }
// Orca: Is the pattern laid out from the origin instead of the bounding box center?
virtual bool aligned_to_origin() const { return false; }
// Perform the fill.
virtual Polylines fill_surface(const Surface *surface, const FillParams &params);
virtual ThickPolylines fill_surface_arachne(const Surface* surface, const FillParams& params);
+1
View File
@@ -19,6 +19,7 @@ public:
Fill *clone() const override { return new FillCrossHatch(*this); };
~FillCrossHatch() override {}
bool is_self_crossing() override { return false; }
bool aligned_to_origin() const override { return true; }
protected:
void _fill_surface_single(
+1
View File
@@ -20,6 +20,7 @@ public:
// require bridge flow since most of this pattern hangs in air
bool use_bridge_flow() const override { return false; }
bool is_self_crossing() override { return false; }
bool aligned_to_origin() const override { return true; }
// Correction applied to regular infill angle to maximize printing
// speed in default configuration (degrees)
+1
View File
@@ -20,6 +20,7 @@ class FillHoneycomb : public Fill
public:
~FillHoneycomb() override {}
bool is_self_crossing() override { return false; }
bool aligned_to_origin() const override { return true; }
protected:
Fill* clone() const override { return new FillHoneycomb(*this); };
+7 -18
View File
@@ -2750,23 +2750,6 @@ static void polylines_from_paths(const std::vector<MonotonicRegionLink> &path, c
}
}
// The extended bounding box of the whole object that covers any rotation of every layer.
BoundingBox FillRectilinear::extended_object_bounding_box() const {
// Build the extension around the box center. The transpose merge and the sqrt(2.) scaling
// (which covers any possible rotation) are both defined about the origin, so a box that is not
// origin-centered — e.g. a separated-infill box re-centered on a single assembly part — would be
// distorted. Shift to the origin first and back afterwards; for the default origin-centered box
// the two translations cancel and this is identical to the original behavior.
const Point c = this->bounding_box.center();
BoundingBox out = this->bounding_box;
out.translate(-c.x(), -c.y());
out.merge(Point(out.min.y(), out.min.x()));
out.merge(Point(out.max.y(), out.max.x()));
out = out.scaled(sqrt(2.));
out.translate(c.x(), c.y());
return out;
}
bool FillRectilinear::fill_surface_by_lines(const Surface *surface, const FillParams &params, float angleBase, float pattern_shift, Polylines &polylines_out)
{
// At the end, only the new polylines will be rotated back.
@@ -2801,7 +2784,13 @@ bool FillRectilinear::fill_surface_by_lines(const Surface *surface, const FillPa
// For infill that needs to be consistent between layers (like Zig Zag),
// we use bounding box of whole object to match vertical lines between layers.
BoundingBox bounding_box_src = poly_with_offset.bounding_box_src();
BoundingBox bounding_box = this->has_consistent_pattern() ? this->extended_object_bounding_box() : bounding_box_src;
BoundingBox bounding_box = bounding_box_src;
if (this->has_consistent_pattern()) {
// Orca: The polygons are rotated about the origin, so follow the box center to where it was rotated.
const Point c = this->bounding_box.center();
bounding_box = this->extended_object_bounding_box();
bounding_box.translate(c.rotated(- rotate_vector.first) - c);
}
// define flow spacing according to requested density
if (params.full_infill() && !params.dont_adjust) {
-3
View File
@@ -42,9 +42,6 @@ protected:
};
bool fill_surface_by_multilines(const Surface *surface, FillParams params, const std::initializer_list<SweepParams> &sweep_params, Polylines &polylines_out);
bool fill_surface_trapezoidal(const Surface *surface, FillParams params, Polylines &polylines_out, int Pattern_type);
// The extended bounding box of the whole object that covers any rotation of every layer.
BoundingBox extended_object_bounding_box() const;
};
class FillAlignedRectilinear : public FillRectilinear
+1
View File
@@ -31,6 +31,7 @@ public:
Polylines& polylines_out) override;
bool is_self_crossing() override { return false; }
bool aligned_to_origin() const override { return true; }
// Density adjustment to have a good %of weight.
static constexpr double DensityAdjust = 2.1;
+1
View File
@@ -31,6 +31,7 @@ public:
Polylines& polylines_out) override;
bool is_self_crossing() override { return false; }
bool aligned_to_origin() const override { return true; }
};
+4 -4
View File
@@ -241,14 +241,14 @@ Flow support_material_flow(const PrintObject *object, float layer_height)
// The width parameter accepted by new_from_config_width is of type ConfigOptionFloatOrPercent, the Flow class takes care of the percent to value substitution.
(object->config().support_line_width.value > 0) ? object->config().support_line_width : object->config().line_width,
// if object->config().support_filament == 0 (which means to not trigger tool change, but use the current extruder instead), get_at will return the 0th component.
float(object->print()->config().nozzle_diameter.get_at(object->config().support_filament-1)),
float(nozzle_diameter_for_filament(object->print()->config(), object->config().support_filament, object->print()->is_BBL_printer())),
(layer_height > 0.f) ? layer_height : float(object->config().layer_height.value));
}
//BBS
Flow support_transition_flow(const PrintObject* object)
{
//BBS: support transition of tree support is bridge flow
float dmr = float(object->print()->config().nozzle_diameter.get_at(object->config().support_filament - 1));
float dmr = float(nozzle_diameter_for_filament(object->print()->config(), object->config().support_filament, object->print()->is_BBL_printer()));
return Flow::bridging_flow(dmr, dmr);
}
@@ -260,7 +260,7 @@ Flow support_material_1st_layer_flow(const PrintObject *object, float layer_heig
frSupportMaterial,
// The width parameter accepted by new_from_config_width is of type ConfigOptionFloatOrPercent, the Flow class takes care of the percent to value substitution.
(width.value > 0) ? width : object->config().line_width,
float(print_config.nozzle_diameter.get_at(object->config().support_filament-1)),
float(nozzle_diameter_for_filament(print_config, object->config().support_filament, object->print()->is_BBL_printer())),
(layer_height > 0.f) ? layer_height : float(print_config.initial_layer_print_height.value));
}
@@ -271,7 +271,7 @@ Flow support_material_interface_flow(const PrintObject *object, float layer_heig
// The width parameter accepted by new_from_config_width is of type ConfigOptionFloatOrPercent, the Flow class takes care of the percent to value substitution.
(object->config().support_line_width > 0) ? object->config().support_line_width : object->config().line_width,
// if object->config().support_interface_filament == 0 (which means to not trigger tool change, but use the current extruder instead), get_at will return the 0th component.
float(object->print()->config().nozzle_diameter.get_at(object->config().support_interface_filament-1)),
float(nozzle_diameter_for_filament(object->print()->config(), object->config().support_interface_filament, object->print()->is_BBL_printer())),
(layer_height > 0.f) ? layer_height : float(object->config().layer_height.value));
}
+41 -64
View File
@@ -154,14 +154,9 @@ static const float g_purge_volume_one_time = 135.f;
static const int g_max_flush_count = 4;
static const size_t g_max_label_object = 64;
static bool is_bambu_x2d_printer(const FullPrintConfig &config)
{
return config.printer_model.value == "Bambu Lab X2D";
}
// Multi-nozzle printer predicate: an extruder carries a nozzle cluster (extruder_max_nozzle_count
// entry > 1). Today only H2C profiles trip it, so every existing single- and dual-extruder printer
// is excluded and keeps its historic placeholder values.
// is excluded and keeps its historic first-filament marker.
static bool is_multi_nozzle_printer(const FullPrintConfig &config)
{
return std::any_of(config.extruder_max_nozzle_count.values.begin(),
@@ -169,33 +164,20 @@ static bool is_multi_nozzle_printer(const FullPrintConfig &config)
[](int v) { return v > 1; });
}
static int hotend_id_for_gcode_placeholder(const FullPrintConfig &config, int hotend_id)
// current_hotend / next_hotend value. On a BBL printer: the real nozzle id only while the print uses a
// dynamic nozzle map (a filament moves between nozzles across layers), else -1. Bambu firmware reads an
// explicit hotend index as a request for the Filament Track Switch and rejects the job on a printer
// without one. group_result may be null on slicing paths that don't populate it, which resolves to -1.
// Any other printer has no firmware hotend selection and gets the filament's extruder index, whatever
// its nozzle map, so existing custom G-code keeps its values.
static int hotend_id_for_gcode_placeholder(const std::shared_ptr<MultiNozzleUtils::LayeredNozzleGroupResult> &group_result,
bool is_bbl_printer, int filament_id, int extruder_id, int layer_id = -1)
{
return is_bambu_x2d_printer(config) ? -1 : hotend_id;
}
// current_hotend / next_hotend value. For multi-nozzle printers a dynamic nozzle map yields the real
// nozzle id, a static map yields -1:
// - multi-nozzle (H2C): dynamic nozzle map -> real nozzle id; static -> -1.
// The dynamic branch is dormant today: the selector create() overload that sets the flag has no
// callers yet (deferred with the nozzle-assignment pipeline), so H2C currently resolves to -1.
// - X2D: keeps its historic -1 (single-nozzle -> falls through to the fallback helper).
// - every other (existing single-nozzle) printer: keeps its historic extruder-id value, so
// existing g-code stays byte-identical.
// group_result may be null on slicing paths that don't populate it -> the dynamic branch is simply
// skipped, so we never dereference null.
static int hotend_id_for_gcode_placeholder(const FullPrintConfig &config,
const std::shared_ptr<MultiNozzleUtils::LayeredNozzleGroupResult> &group_result,
int filament_id,
int extruder_id,
int layer_id = -1)
{
if (is_multi_nozzle_printer(config)) {
if (group_result && group_result->is_support_dynamic_nozzle_map() && filament_id >= 0)
return group_result->get_nozzle_id(filament_id, layer_id);
return -1;
}
return hotend_id_for_gcode_placeholder(config, extruder_id);
if (!is_bbl_printer)
return extruder_id;
if (group_result && group_result->is_support_dynamic_nozzle_map() && filament_id >= 0)
return group_result->get_nozzle_id(filament_id, layer_id);
return -1;
}
// Logical nozzle id for the *_nozzle_id placeholders. Null-safe: falls back to the
@@ -210,24 +192,20 @@ static int nozzle_id_for_gcode_placeholder(const std::shared_ptr<MultiNozzleUtil
// Init variants: the start-gcode init sites (first_non_support_hotend / initial_no_support_hotend /
// current_hotend / initial_nozzle_id / filament_start current_nozzle_id) use get_first_nozzle_for_filament
// (the nozzle a filament FIRST uses) rather than the layer-based get_nozzle_id. Same hotend-value semantics
// as hotend_id_for_gcode_placeholder above (multi-nozzle static -> -1; dynamic branch dormant;
// existing printers -> extruder id; X2D -> -1); they differ from the layer-based helper only on the dormant
// dynamic path for a filament first used after layer 0.
static int first_hotend_id_for_gcode_placeholder(const FullPrintConfig &config,
const std::shared_ptr<MultiNozzleUtils::LayeredNozzleGroupResult> &group_result,
int filament_id,
int extruder_id)
// (the nozzle a filament FIRST uses) rather than the layer-based get_nozzle_id. Same hotend-value rule
// as hotend_id_for_gcode_placeholder above; they differ from the layer-based helper only on the dynamic
// path for a filament first used after layer 0.
static int first_hotend_id_for_gcode_placeholder(const std::shared_ptr<MultiNozzleUtils::LayeredNozzleGroupResult> &group_result,
bool is_bbl_printer, int filament_id, int extruder_id)
{
if (is_multi_nozzle_printer(config)) {
if (group_result && group_result->is_support_dynamic_nozzle_map() && filament_id >= 0) {
auto nozzle = group_result->get_first_nozzle_for_filament(filament_id);
if (nozzle)
return nozzle->group_id;
}
return -1;
if (!is_bbl_printer)
return extruder_id;
if (group_result && group_result->is_support_dynamic_nozzle_map() && filament_id >= 0) {
auto nozzle = group_result->get_first_nozzle_for_filament(filament_id);
if (nozzle)
return nozzle->group_id;
}
return hotend_id_for_gcode_placeholder(config, extruder_id);
return -1;
}
static int first_nozzle_id_for_gcode_placeholder(const std::shared_ptr<MultiNozzleUtils::LayeredNozzleGroupResult> &group_result,
@@ -1214,12 +1192,11 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
config.set_key_value("previous_extruder", new ConfigOptionInt(old_filament_id));
config.set_key_value("next_extruder", new ConfigOptionInt(new_filament_id));
// current_hotend/next_hotend (see hotend_id_for_gcode_placeholder): multi-nozzle H2C -> -1
// (static; dynamic branch dormant), X2D -> -1, existing printers -> extruder id.
const bool is_bbl_printer = gcodegen.m_print->is_BBL_printer();
config.set_key_value("current_hotend", new ConfigOptionInt(
hotend_id_for_gcode_placeholder(gcodegen.m_config, group_result, old_filament_id, old_extruder_id, m_layer_idx)));
hotend_id_for_gcode_placeholder(group_result, is_bbl_printer, old_filament_id, old_extruder_id, m_layer_idx)));
config.set_key_value("next_hotend", new ConfigOptionInt(
hotend_id_for_gcode_placeholder(gcodegen.m_config, group_result, new_filament_id, (int) gcodegen.get_extruder_id(new_filament_id), m_layer_idx)));
hotend_id_for_gcode_placeholder(group_result, is_bbl_printer, new_filament_id, (int) gcodegen.get_extruder_id(new_filament_id), m_layer_idx)));
config.set_key_value("current_nozzle_id", new ConfigOptionInt(old_nozzle_id));
config.set_key_value("next_nozzle_id", new ConfigOptionInt(next_nozzle_id));
config.set_key_value("current_filament_id", new ConfigOptionInt(old_filament_id));
@@ -1498,7 +1475,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
gcodegen.placeholder_parser().set("current_nozzle_id",
nozzle_id_for_gcode_placeholder(group_result, new_filament_id, new_extruder_id, m_layer_idx));
gcodegen.placeholder_parser().set("current_hotend",
hotend_id_for_gcode_placeholder(gcodegen.m_config, group_result, new_filament_id, new_extruder_id, m_layer_idx));
hotend_id_for_gcode_placeholder(group_result, gcodegen.m_print->is_BBL_printer(), new_filament_id, new_extruder_id, m_layer_idx));
{
size_t fi = gcodegen.get_filament_config_index(new_filament_id);
gcodegen.placeholder_parser().set("retraction_distance_when_cut", gcodegen.m_config.retraction_distances_when_cut.get_at(fi));
@@ -3309,20 +3286,19 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
first_non_support_hotends.reserve(first_non_support_filaments.size());
for (int filament_id : first_non_support_filaments)
first_non_support_hotends.push_back(filament_id < 0 ? -1 :
first_hotend_id_for_gcode_placeholder(m_config, group_result, filament_id, (int) get_extruder_id(filament_id)));
first_hotend_id_for_gcode_placeholder(group_result, is_bbl_printers, filament_id, (int) get_extruder_id(filament_id)));
this->placeholder_parser().set("first_non_support_tools", new ConfigOptionInts(first_non_support_filaments));
this->placeholder_parser().set("first_non_support_filaments", new ConfigOptionInts(first_non_support_filaments));
this->placeholder_parser().set("first_non_support_hotend", new ConfigOptionInts(first_non_support_hotends));
this->placeholder_parser().set("initial_no_support_tool", initial_non_support_extruder_id);
this->placeholder_parser().set("initial_no_support_extruder", initial_non_support_extruder_id);
// initial_no_support_hotend/current_hotend (see first_hotend_id_for_gcode_placeholder): multi-nozzle
// H2C -> -1 (static; dynamic branch dormant), X2D -> -1, existing printers -> extruder id.
this->placeholder_parser().set("initial_no_support_hotend",
first_hotend_id_for_gcode_placeholder(m_config, group_result, (int) initial_non_support_extruder_id, (int) get_extruder_id(initial_non_support_extruder_id)));
first_hotend_id_for_gcode_placeholder(group_result, is_bbl_printers, (int) initial_non_support_extruder_id,
(int) get_extruder_id(initial_non_support_extruder_id)));
this->placeholder_parser().set("current_extruder", initial_extruder_id);
this->placeholder_parser().set("current_hotend",
first_hotend_id_for_gcode_placeholder(m_config, group_result, (int) initial_extruder_id, extruder_id));
first_hotend_id_for_gcode_placeholder(group_result, is_bbl_printers, (int) initial_extruder_id, extruder_id));
this->placeholder_parser().set("current_filament_id", (int) initial_extruder_id);
this->placeholder_parser().set("current_extruder_id", extruder_id);
this->placeholder_parser().set("current_nozzle_id",
@@ -3831,7 +3807,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
file.writeln(filament_start_gcode);
// Mark the first filament used in print. Multi-nozzle printers (H2C) get ";VT%d H%d" where
// H = dynamic ? nozzle_id : -1; existing single-nozzle printers keep the bare ";VT%d" so their
// g-code stays byte-identical. (The dynamic branch is dormant, so H2C currently emits H-1.)
// g-code stays byte-identical.
if (is_multi_nozzle_printer(m_config)) {
int initial_nozzle_id = -1;
if (group_result && group_result->is_support_dynamic_nozzle_map()) {
@@ -9615,6 +9591,9 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
this->placeholder_parser().set("current_extruder_id", new_extruder_id);
this->placeholder_parser().set("current_nozzle_id",
nozzle_id_for_gcode_placeholder(m_print->get_layered_nozzle_group_result(), (int) new_filament_id, new_extruder_id, m_layer_index));
this->placeholder_parser().set("current_hotend",
hotend_id_for_gcode_placeholder(m_print->get_layered_nozzle_group_result(), m_print->is_BBL_printer(), (int) new_filament_id,
new_extruder_id, m_layer_index));
{
size_t fi = get_filament_config_index(new_filament_id);
this->placeholder_parser().set("retraction_distance_when_ec", m_config.retraction_distances_when_ec.get_at(fi));
@@ -9793,12 +9772,10 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
dyn_config.set_key_value("outer_wall_volumetric_speed", new ConfigOptionFloat(outer_wall_volumetric_speed));
dyn_config.set_key_value("previous_extruder", new ConfigOptionInt(old_filament_id));
dyn_config.set_key_value("next_extruder", new ConfigOptionInt((int)new_filament_id));
// current_hotend/next_hotend (see hotend_id_for_gcode_placeholder): multi-nozzle H2C -> -1
// (static; dynamic branch dormant), X2D -> -1, existing printers -> extruder id.
dyn_config.set_key_value("current_hotend", new ConfigOptionInt(
hotend_id_for_gcode_placeholder(m_config, group_result, old_filament_id, old_extruder_id, m_layer_index)));
hotend_id_for_gcode_placeholder(group_result, m_print->is_BBL_printer(), old_filament_id, old_extruder_id, m_layer_index)));
dyn_config.set_key_value("next_hotend", new ConfigOptionInt(
hotend_id_for_gcode_placeholder(m_config, group_result, (int) new_filament_id, new_extruder_id, m_layer_index)));
hotend_id_for_gcode_placeholder(group_result, m_print->is_BBL_printer(), (int) new_filament_id, new_extruder_id, m_layer_index)));
dyn_config.set_key_value("current_nozzle_id", new ConfigOptionInt(old_nozzle_id));
dyn_config.set_key_value("next_nozzle_id", new ConfigOptionInt(next_nozzle_id));
dyn_config.set_key_value("current_filament_id", new ConfigOptionInt(old_filament_id));
@@ -9976,7 +9953,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
this->placeholder_parser().set("current_extruder", new_filament_id);
this->placeholder_parser().set("current_hotend",
hotend_id_for_gcode_placeholder(m_config, group_result, (int) new_filament_id, new_extruder_id, m_layer_index));
hotend_id_for_gcode_placeholder(group_result, m_print->is_BBL_printer(), (int) new_filament_id, new_extruder_id, m_layer_index));
// Orca: keep the global current-tool identity coherent for later contexts (see append_tcr).
this->placeholder_parser().set("current_filament_id", (int) new_filament_id);
this->placeholder_parser().set("current_extruder_id", new_extruder_id);
+8 -8
View File
@@ -33,7 +33,7 @@ class PrintObject;
class Print;
namespace FillAdaptive {
struct Octree;
struct Octrees;
};
namespace FillLightning {
@@ -170,10 +170,10 @@ public:
ExPolygons lslices;
ExPolygons lslices_extrudable; // BBS: the extrudable part of lslices used for tree support
std::vector<BoundingBox> lslices_bboxes;
// Orca: for separated infills / per-model centering. Aligned with lslices: for each island, the
// full bounding box of the 3D connected body (across all layers) it belongs to. Populated by
// PrintObject::infill() only when the feature is used; empty otherwise.
std::vector<BoundingBox> lslices_separated_component_bboxes;
// Orca: for separated infills / per-model centering / octree infills. Aligned with lslices: for each
// island, the 3D connected body (across all layers) it belongs to, indexing
// PrintObject::separated_body_bboxes(). Populated by PrintObject::prepare_infill() only when needed.
std::vector<size_t> lslices_separated_component_ids;
// BBS
ExPolygons loverhangs;
@@ -208,9 +208,9 @@ public:
void make_perimeters();
// Phony version of make_fills() without parameters for Perl integration only.
void make_fills() { this->make_fills(nullptr, nullptr); }
void make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive::Octree* support_fill_octree, FillLightning::Generator* lightning_generator = nullptr);
Polylines generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Octree *adaptive_fill_octree,
FillAdaptive::Octree *support_fill_octree,
void make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator = nullptr);
Polylines generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::Octrees *adaptive_fill_octrees,
const FillAdaptive::Octrees *support_fill_octrees,
FillLightning::Generator* lightning_generator) const;
void make_ironing();
// Returns the filament id (1-based) the region is ironed with, or -1 when the
+1 -1
View File
@@ -62,7 +62,7 @@ Flow LayerRegion::bridging_flow(FlowRole role, bool thick_bridge) const
const PrintObject &print_object = *this->layer()->object();
Flow bridge_flow;
// Here this->extruder(role) - 1 may underflow to MAX_INT, but then the get_at() will fall back to zero'th element, so everything is all right.
auto nozzle_diameter = float(print_object.print()->config().nozzle_diameter.get_at(region.extruder(role) - 1));
auto nozzle_diameter = float(nozzle_diameter_for_filament(print_object.print()->config(), region.extruder(role), print_object.print()->is_BBL_printer()));
const ConfigOptionFloatOrPercent& bridge_width_opt = region_config.bridge_line_width;
const double bridge_width = bridge_width_opt.get_abs_value(nozzle_diameter);
const bool has_bridge_width = bridge_width > 0.;
+3
View File
@@ -1696,6 +1696,9 @@ indexed_triangle_set ModelObject::raw_indexed_triangle_set() const
size_t j = out.indices.size();
append(out.vertices, v->mesh().its.vertices);
append(out.indices, v->mesh().its.indices);
// Orca: Point the volume's triangles at its own vertices, which follow those of the volumes before it.
for (size_t k = j; k < out.indices.size(); ++ k)
out.indices[k] += stl_triangle_vertex_indices::Constant(int(i));
const Transform3d& m = v->get_matrix();
for (; i < out.vertices.size(); ++ i)
out.vertices[i] = (m * out.vertices[i].cast<double>()).cast<float>().eval();
+9 -8
View File
@@ -2229,7 +2229,7 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
for (const PrintRegion &region : object->all_regions()) {
const auto &bridge_width_opt = region.config().bridge_line_width;
for (FlowRole bridge_role : { frPerimeter, frInfill, frSolidInfill, frTopSolidInfill }) {
const double nozzle_diameter = m_config.nozzle_diameter.get_at(region.extruder(bridge_role) - 1);
const double nozzle_diameter = nozzle_diameter_for_filament(m_config, region.extruder(bridge_role), this->is_BBL_printer());
const double bridge_width = bridge_width_opt.get_abs_value(nozzle_diameter);
if (bridge_width <= 0.)
continue;
@@ -2606,7 +2606,7 @@ Flow Print::brim_flow() const
frPerimeter,
// Flow::new_from_config_width takes care of the percent to value substitution
width,
(float)m_config.nozzle_diameter.get_at(m_print_regions.front()->config().outer_wall_filament_id-1),
(float)nozzle_diameter_for_filament(m_config, m_print_regions.front()->config().outer_wall_filament_id, this->is_BBL_printer()),
(float)this->skirt_first_layer_height());
}
@@ -2623,12 +2623,13 @@ Flow Print::skirt_flow() const
extruders and take the one with, say, the smallest index;
The same logic should be applied to the code that selects the extruder during G-code
generation as well. */
return Flow::new_from_config_width(frPerimeter,
// Flow::new_from_config_width takes care of the percent to value substitution
width,
(float) m_config.nozzle_diameter.get_at(
m_objects.empty() ? 0 : m_objects.front()->config().support_filament - 1),
(float) this->skirt_first_layer_height());
return Flow::new_from_config_width(
frPerimeter,
// Flow::new_from_config_width takes care of the percent to value substitution
width,
// ORCA: resolve the actual nozzle the support filament is printed with (dual-nozzle printers).
(float)nozzle_diameter_for_filament(m_config, m_objects.empty() ? 0 : m_objects.front()->config().support_filament, this->is_BBL_printer()),
(float)this->skirt_first_layer_height());
}
bool Print::has_support_material() const
+6 -3
View File
@@ -375,6 +375,8 @@ public:
Transform3d trafo_centered() const
{ Transform3d t = this->trafo(); t.pretranslate(Vec3d(- unscale<double>(m_center_offset.x()), - unscale<double>(m_center_offset.y()), 0)); return t; }
const PrintInstances& instances() const { return m_instances; }
// Orca: Bounding box of each connected body, indexed by Layer::lslices_separated_component_ids.
const std::vector<BoundingBox>& separated_body_bboxes() const { return m_separated_body_bboxes; }
PrintInstances &instances() { return m_instances; }
// Whoever will get a non-const pointer to PrintObject will be able to modify its layers.
@@ -581,8 +583,8 @@ private:
void discover_horizontal_shells();
void combine_infill();
void _generate_support_material();
std::pair<FillAdaptive::OctreePtr, FillAdaptive::OctreePtr> prepare_adaptive_infill_data(
const std::vector<std::pair<const Surface*, float>>& surfaces_w_bottom_z) const;
std::pair<FillAdaptive::Octrees, FillAdaptive::Octrees> prepare_adaptive_infill_data(
const std::vector<std::pair<const Surface*, const Layer*>>& surfaces_w_layer) const;
FillLightning::GeneratorPtr prepare_lightning_infill_data();
// BBS
@@ -614,7 +616,8 @@ private:
// so that next call to make_perimeters() performs a union() before computing loops
bool m_typed_slices = false;
std::pair<FillAdaptive::OctreePtr, FillAdaptive::OctreePtr> m_adaptive_fill_octrees;
std::pair<FillAdaptive::Octrees, FillAdaptive::Octrees> m_adaptive_fill_octrees;
std::vector<BoundingBox> m_separated_body_bboxes;
FillLightning::GeneratorPtr m_lightning_generator;
std::vector < VolumeSlices > firstLayerObjSliceByVolume;
+12 -3
View File
@@ -133,6 +133,15 @@ size_t get_extruder_index(const GCodeConfig& config, unsigned int filament_id)
return 0;
}
double nozzle_diameter_for_filament(const PrintConfig& config, int filament_id, bool is_bbl_printer)
{
int extruder = filament_id;
if (is_bbl_printer && config.nozzle_diameter.size() > 1 &&
filament_id >= 1 && static_cast<size_t>(filament_id - 1) < config.filament_map.size())
extruder = config.filament_map.get_at(filament_id - 1);
return config.nozzle_diameter.get_at(extruder - 1);
}
// Orca: input shaping values types by flavor
std::vector<std::string> get_shaper_type_values_for_flavor(GCodeFlavor flavor)
@@ -7602,8 +7611,8 @@ void PrintConfigDef::init_fff_params()
"whole assembly. Parts that touch or overlap are treated as one body and share a center; separate parts "
"(or distinct 3D objects) each get their own.\n"
"Useful when an assembly groups several objects that should each keep a consistent, self-centered infill.\n"
"Affects line and grid patterns and rotation-template infills.\n"
"Patterns locked to global coordinates (Gyroid, Honeycomb, TPMS, ...) are unaffected.");
"Adaptive Cubic and Support Cubic always center each part on itself, and Lightning infill is generated for "
"the whole object and is unaffected.");
def->mode = comExpert;
def->set_default_value(new ConfigOptionBool(false));
@@ -12990,7 +12999,7 @@ CustomGcodeSpecificConfigDef::CustomGcodeSpecificConfigDef()
// Common Defs
def = this->add("layer_num", coInt);
def->label = L("Layer number");
def->tooltip = L("Index of the current layer. One-based (i.e. first layer is number 1).");
def->tooltip = L("Index of the current layer. Zero-based (i.e. first layer is number 0), except in extrusion role change G-code, where it is one-based.");
def = this->add("layer_z", coFloat);
def->label = L("Layer Z");
+15 -4
View File
@@ -136,25 +136,31 @@ enum InfillPattern : int {
ipCount,
};
// Orca: Infill patterns whose alignment origin follows the fill bounding box, so the
// "separated_infills" option can re-center them per connected body. Patterns evaluated in
// absolute/global coordinates (Gyroid, TPMS, Honeycomb, CrossHatch, ...) or that are shape-relative
// (Concentric) ignore that bounding box and are therefore excluded.
// Orca: Infill patterns that the "separated_infills" option can center on each connected body.
inline bool is_separable_infill_pattern(InfillPattern pattern)
{
switch (pattern) {
case ipMonotonic:
case ipMonotonicLine:
case ipRectilinear:
case ipAlignedRectilinear:
case ipZigZag:
case ipCrossZag:
case ipLockedZag:
case ipLine:
case ipGrid:
case ipTriangles:
case ipStars: // tri-hexagon
case ipCubic:
case ipQuarterCubic:
case ipHoneycomb:
case ip3DHoneycomb:
case ipLateralHoneycomb:
case ipLateralLattice:
case ipCrossHatch:
case ipTpmsD:
case ipTpmsFK:
case ipGyroid:
case ipHilbertCurve:
case ipArchimedeanChords:
case ipOctagramSpiral:
@@ -164,6 +170,9 @@ 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 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
// than one line per infill wall; a single line makes them plain crossing lines with nothing to round.
@@ -2566,6 +2575,8 @@ static bool has_zero_flush_volume_for_used_filaments(const std::vector<T> &fv_ma
size_t get_extruder_index(const GCodeConfig& config, unsigned int filament_id);
double nozzle_diameter_for_filament(const PrintConfig& config, int filament_id, bool is_bbl_printer);
} // namespace Slic3r
// Serialization through the Cereal library
+125 -34
View File
@@ -67,6 +67,7 @@
#include <utility>
#include <boost/log/trivial.hpp>
#include <Eigen/Core>
#include <tbb/parallel_for.h>
#include <tbb/spin_mutex.h>
@@ -719,7 +720,8 @@ void PrintObject::prepare_infill()
bool needs_separated_components = false;
for (size_t i = 0; i < this->num_printing_regions(); ++ i) {
const PrintRegionConfig &rc = this->printing_region(i).config();
if (rc.separated_infills || rc.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model) {
if (rc.separated_infills || rc.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model ||
(rc.sparse_infill_density > 0 && is_octree_infill_pattern(rc.sparse_infill_pattern))) {
needs_separated_components = true;
break;
}
@@ -736,8 +738,9 @@ void PrintObject::prepare_infill()
if (parts <= 1 && ! (first_part != nullptr && first_part->is_splittable()))
needs_separated_components = false;
}
m_separated_body_bboxes.clear();
for (Layer *layer : m_layers)
layer->lslices_separated_component_bboxes.clear();
layer->lslices_separated_component_ids.clear();
if (needs_separated_components) {
const size_t nl = m_layers.size();
std::vector<size_t> offset(nl + 1, 0); // Orca: flat index of the first island of each layer
@@ -788,17 +791,20 @@ void PrintObject::prepare_infill()
});
}
}
// Orca: Full bounding box of each body, indexed by its union-find root.
std::vector<BoundingBox> body_bbox(nreg);
for (size_t i = 0; i < nl; ++ i)
for (size_t a = 0; a < m_layers[i]->lslices.size(); ++ a)
body_bbox[find(offset[i] + a)].merge(m_layers[i]->lslices_bboxes[a]);
// Orca: Store the body bbox for every island.
// Orca: Number the bodies by their first island and merge the bounding boxes of their islands.
std::vector<size_t> body_of_root(nreg, size_t(-1));
for (size_t i = 0; i < nl; ++ i) {
Layer *layer = m_layers[i];
layer->lslices_separated_component_bboxes.resize(layer->lslices.size());
for (size_t a = 0; a < layer->lslices.size(); ++ a)
layer->lslices_separated_component_bboxes[a] = body_bbox[find(offset[i] + a)];
layer->lslices_separated_component_ids.resize(layer->lslices.size());
for (size_t a = 0; a < layer->lslices.size(); ++ a) {
size_t &body = body_of_root[find(offset[i] + a)];
if (body == size_t(-1)) {
body = m_separated_body_bboxes.size();
m_separated_body_bboxes.emplace_back();
}
m_separated_body_bboxes[body].merge(layer->lslices_bboxes[a]);
layer->lslices_separated_component_ids[a] = body;
}
}
}
@@ -836,16 +842,13 @@ void PrintObject::infill()
if (this->set_started(posInfill)) {
m_print->set_status(35, L("Generating infill toolpath"));
const auto& adaptive_fill_octree = this->m_adaptive_fill_octrees.first;
const auto& support_fill_octree = this->m_adaptive_fill_octrees.second;
BOOST_LOG_TRIVIAL(debug) << "Filling layers in parallel - start";
tbb::parallel_for(
tbb::blocked_range<size_t>(0, m_layers.size()),
[this, &adaptive_fill_octree = adaptive_fill_octree, &support_fill_octree = support_fill_octree](const tbb::blocked_range<size_t>& range) {
[this](const tbb::blocked_range<size_t>& range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
m_print->throw_if_canceled();
m_layers[layer_idx]->make_fills(adaptive_fill_octree.get(), support_fill_octree.get(), this->m_lightning_generator.get());
m_layers[layer_idx]->make_fills(&m_adaptive_fill_octrees.first, &m_adaptive_fill_octrees.second, this->m_lightning_generator.get());
}
}
);
@@ -1110,14 +1113,69 @@ void PrintObject::simplify_extrusion_path()
}
}
std::pair<FillAdaptive::OctreePtr, FillAdaptive::OctreePtr> PrintObject::prepare_adaptive_infill_data(
const std::vector<std::pair<const Surface *, float>> &surfaces_w_bottom_z) const
// Orca: Separated body of the island containing a point of a layer, else of the island outline nearest within 1 mm, or -1.
static int separated_body_at(const Layer &layer, const Point &point)
{
int body = -1;
double best = scaled<double>(1.);
for (size_t i = 0; i < layer.lslices.size() && i < layer.lslices_separated_component_ids.size() && best > 0.; ++ i) {
BoundingBox bbox = layer.lslices_bboxes[i];
bbox.offset(coord_t(best));
if (! bbox.contains(point))
continue;
const double dist = layer.lslices[i].contains(point) ? 0. : (layer.lslices[i].point_projection(point) - point).cast<double>().norm();
if (dist < best) {
best = dist;
body = int(layer.lslices_separated_component_ids[i]);
}
}
return body;
}
// Orca: The object mesh in the octree frame split by separated body. Each connected component goes to the body
// most of its sampled triangles lie on, sampled a layer height inside the solid at the layer nearest to them.
static std::vector<indexed_triangle_set> split_mesh_by_body(const PrintObject &object, const indexed_triangle_set &mesh, size_t num_bodies)
{
const Eigen::Matrix3d to_object = FillAdaptive::transform_to_world().toRotationMatrix();
const double inset = object.config().layer_height.value;
std::vector<indexed_triangle_set> bodies(num_bodies);
for (const indexed_triangle_set &component : its_split(mesh)) {
std::vector<size_t> votes(num_bodies, 0);
const size_t step = std::max<size_t>(1, component.indices.size() / 8);
for (size_t i = 0; i < component.indices.size(); i += step) {
const stl_triangle_vertex_indices &tri = component.indices[i];
const Vec3d a = component.vertices[tri[0]].cast<double>(), b = component.vertices[tri[1]].cast<double>(),
d = component.vertices[tri[2]].cast<double>();
const Vec3d normal = (b - a).cross(d - a);
const double area2 = normal.norm();
const Vec3d c = to_object * ((a + b + d) / 3. - (area2 > 0. ? Vec3d(normal * (inset / area2)) : Vec3d::Zero()));
size_t lo = 0, hi = object.layer_count();
while (lo < hi) {
const size_t mid = (lo + hi) / 2;
if (object.get_layer(int(mid))->slice_z < c.z())
lo = mid + 1;
else
hi = mid;
}
if (lo == object.layer_count() || (lo > 0 && c.z() - object.get_layer(int(lo) - 1)->slice_z < object.get_layer(int(lo))->slice_z - c.z()))
-- lo;
if (const int body = separated_body_at(*object.get_layer(int(lo)), Point(scaled<coord_t>(c.x()), scaled<coord_t>(c.y()))); body >= 0)
++ votes[body];
}
if (const auto best = std::max_element(votes.begin(), votes.end()); *best > 0)
its_merge(bodies[best - votes.begin()], component);
}
return bodies;
}
std::pair<FillAdaptive::Octrees, FillAdaptive::Octrees> PrintObject::prepare_adaptive_infill_data(
const std::vector<std::pair<const Surface *, const Layer *>> &surfaces_w_layer) const
{
using namespace FillAdaptive;
auto [adaptive_line_spacing, support_line_spacing] = adaptive_fill_line_spacing(*this);
if ((adaptive_line_spacing == 0. && support_line_spacing == 0.) || this->layers().empty())
return std::make_pair(OctreePtr(), OctreePtr());
return {};
indexed_triangle_set mesh = this->model_object()->raw_indexed_triangle_set();
// Rotate mesh and build octree on it with axis-aligned (standart base) cubes.
@@ -1125,27 +1183,60 @@ std::pair<FillAdaptive::OctreePtr, FillAdaptive::OctreePtr> PrintObject::prepare
its_transform(mesh, to_octree * this->trafo_centered(), true);
// Triangulate internal bridging surfaces.
std::vector<std::vector<Vec3d>> overhangs(std::max(surfaces_w_bottom_z.size(), size_t(1)));
std::vector<std::vector<Vec3d>> overhangs(std::max(surfaces_w_layer.size(), size_t(1)));
// ^ make sure vector is not empty, even with no briding surfaces we still want to build the adaptive trees later, some continue normally
tbb::parallel_for(tbb::blocked_range<int>(0, surfaces_w_bottom_z.size()),
[this, &to_octree, &overhangs, &surfaces_w_bottom_z](const tbb::blocked_range<int> &range) {
tbb::parallel_for(tbb::blocked_range<int>(0, surfaces_w_layer.size()),
[this, &to_octree, &overhangs, &surfaces_w_layer](const tbb::blocked_range<int> &range) {
PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT);
for (int surface_idx = range.begin(); surface_idx < range.end(); ++surface_idx) {
std::vector<Vec3d> &out = overhangs[surface_idx];
m_print->throw_if_canceled();
append(out, triangulate_expolygon_3d(surfaces_w_bottom_z[surface_idx].first->expolygon,
surfaces_w_bottom_z[surface_idx].second));
append(out, triangulate_expolygon_3d(surfaces_w_layer[surface_idx].first->expolygon,
float(surfaces_w_layer[surface_idx].second->bottom_z())));
for (Vec3d &p : out)
p = (to_octree * p).eval();
}
});
// Orca: Each body gets the octree it has when sliced on its own, from its own triangles.
std::pair<Octrees, Octrees> octrees;
const size_t num_bodies = m_separated_body_bboxes.size();
bool need_object = num_bodies <= 1;
if (num_bodies > 1) {
const std::vector<indexed_triangle_set> body_meshes = split_mesh_by_body(*this, mesh, num_bodies);
need_object = std::any_of(body_meshes.begin(), body_meshes.end(), [](const indexed_triangle_set &its) { return its.indices.empty(); });
std::vector<std::vector<Vec3d>> body_overhangs(num_bodies);
for (size_t i = 0; i < surfaces_w_layer.size(); ++ i)
if (const int body = separated_body_at(*surfaces_w_layer[i].second, surfaces_w_layer[i].first->expolygon.contour.points.front()); body >= 0)
append(body_overhangs[body], overhangs[i]);
if (adaptive_line_spacing)
octrees.first.bodies.resize(num_bodies);
if (support_line_spacing)
octrees.second.bodies.resize(num_bodies);
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_bodies), [&, adaptive_spacing = adaptive_line_spacing, support_spacing = support_line_spacing](
const tbb::blocked_range<size_t> &range) {
for (size_t body = range.begin(); body < range.end(); ++ body) {
m_print->throw_if_canceled();
if (body_meshes[body].indices.empty())
continue;
if (adaptive_spacing)
octrees.first.bodies[body] = build_octree(body_meshes[body], body_overhangs[body], adaptive_spacing, false);
if (support_spacing)
octrees.second.bodies[body] = build_octree(body_meshes[body], body_overhangs[body], support_spacing, true);
}
});
}
// and gather them.
for (size_t i = 1; i < overhangs.size(); ++ i)
append(overhangs.front(), std::move(overhangs[i]));
return std::make_pair(
adaptive_line_spacing ? build_octree(mesh, overhangs.front(), adaptive_line_spacing, false) : OctreePtr(),
support_line_spacing ? build_octree(mesh, overhangs.front(), support_line_spacing, true) : OctreePtr());
// Orca: The object's octree only serves bodies that have none of their own.
if (need_object && adaptive_line_spacing)
octrees.first.object = build_octree(mesh, overhangs.front(), adaptive_line_spacing, false);
if (need_object && support_line_spacing)
octrees.second.object = build_octree(mesh, overhangs.front(), support_line_spacing, true);
return octrees;
}
FillLightning::GeneratorPtr PrintObject::prepare_lightning_infill_data()
@@ -2963,14 +3054,14 @@ void PrintObject::bridge_over_infill()
std::map<size_t, Polylines> infill_lines;
// SECTION to generate infill polylines
{
std::vector<std::pair<const Surface *, float>> surfaces_w_bottom_z;
std::vector<std::pair<const Surface *, const Layer *>> surfaces_w_layer;
for (const auto &pair : surfaces_by_layer) {
for (const CandidateSurface &c : pair.second) {
surfaces_w_bottom_z.emplace_back(c.original_surface, c.region->m_layer->bottom_z());
surfaces_w_layer.emplace_back(c.original_surface, c.region->m_layer);
}
}
this->m_adaptive_fill_octrees = this->prepare_adaptive_infill_data(surfaces_w_bottom_z);
this->m_adaptive_fill_octrees = this->prepare_adaptive_infill_data(surfaces_w_layer);
std::vector<size_t> layers_to_generate_infill;
for (const auto &pair : surfaces_by_layer) {
@@ -2986,8 +3077,8 @@ void PrintObject::bridge_over_infill()
for (size_t job_idx = r.begin(); job_idx < r.end(); job_idx++) {
size_t lidx = layers_to_generate_infill[job_idx];
infill_lines.at(
lidx) = po->get_layer(lidx)->generate_sparse_infill_polylines_for_anchoring(po->m_adaptive_fill_octrees.first.get(),
po->m_adaptive_fill_octrees.second.get(),
lidx) = po->get_layer(lidx)->generate_sparse_infill_polylines_for_anchoring(&po->m_adaptive_fill_octrees.first,
&po->m_adaptive_fill_octrees.second,
po->m_lightning_generator.get());
}
});
@@ -4467,8 +4558,8 @@ void PrintObject::combine_infill()
// Limit the number of combined layers to the maximum height allowed by this regions' nozzle.
//FIXME limit the layer height to max_layer_height
double nozzle_diameter = std::min(
this->print()->config().nozzle_diameter.get_at(region.config().sparse_infill_filament_id.value - 1),
this->print()->config().nozzle_diameter.get_at(region.config().internal_solid_filament_id.value - 1));
nozzle_diameter_for_filament(this->print()->config(), region.config().sparse_infill_filament_id.value, this->print()->is_BBL_printer()),
nozzle_diameter_for_filament(this->print()->config(), region.config().internal_solid_filament_id.value, this->print()->is_BBL_printer()));
//Orca: Limit combination of infill to up to infill_combination_max_layer_height
const double infill_combination_max_layer_height = region.config().infill_combination_max_layer_height.get_abs_value(nozzle_diameter);
+1 -1
View File
@@ -57,7 +57,7 @@ Flow PrintRegion::flow(const PrintObject &object, FlowRole role, double layer_he
// Get the configured nozzle_diameter for the extruder associated to the flow role requested.
// Here this->extruder(role) - 1 may underflow to MAX_INT, but then the get_at() will follback to zero'th element, so everything is all right.
auto nozzle_diameter = float(print_config.nozzle_diameter.get_at(this->extruder(role) - 1));
auto nozzle_diameter = float(nozzle_diameter_for_filament(print_config, this->extruder(role), object.print()->is_BBL_printer()));
return Flow::new_from_config_width(role, config_width, nozzle_diameter, float(layer_height));
}
+1 -1
View File
@@ -2180,7 +2180,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::top_contact_layers(
// check if the sharp tails should be extended higher
bool detect_first_sharp_tail_only = false;
const coordf_t extrusion_width = m_object_config->line_width.get_abs_value(object.print()->config().nozzle_diameter.get_at(object.config().support_interface_filament-1));
const coordf_t extrusion_width = m_object_config->line_width.get_abs_value(nozzle_diameter_for_filament(object.print()->config(), object.config().support_interface_filament, object.print()->is_BBL_printer()));
const coordf_t extrusion_width_scaled = scale_(extrusion_width);
if (is_auto(m_object_config->support_type.value) && g_config_support_sharp_tails && !detect_first_sharp_tail_only) {
for (size_t layer_nr = layer_id_start; layer_nr < num_layers; layer_nr++) {
+1 -1
View File
@@ -1399,7 +1399,7 @@ void TreeSupport::generate_toolpaths()
{
const PrintObjectConfig &object_config = m_object->config();
coordf_t support_extrusion_width = m_support_params.support_extrusion_width;
coordf_t nozzle_diameter = m_print_config->nozzle_diameter.get_at(object_config.support_filament - 1);
coordf_t nozzle_diameter = nozzle_diameter_for_filament(*m_print_config, object_config.support_filament, m_object->print()->is_BBL_printer());
coordf_t layer_height = object_config.layer_height.value;
const size_t wall_count = object_config.tree_support_wall_count.value;
+8 -2
View File
@@ -1012,7 +1012,10 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0);
if (machine_diameter == 0.0f && preset_bundle) {
const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter");
if (opt) machine_diameter = static_cast<const ConfigOptionFloats *>(opt)->values[0];
if (opt) {
const auto &nd = static_cast<const ConfigOptionFloats *>(opt)->values;
if (!nd.empty()) machine_diameter = nd.size() > 1 ? nd[1] : nd[0];
}
}
stream << std::fixed << std::setprecision(1) << machine_diameter;
std::string nozzle_diameter_str = stream.str();
@@ -1291,7 +1294,10 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0);
if (machine_diameter == 0.0f) {
const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter");
if (opt) machine_diameter = static_cast<const ConfigOptionFloats *>(opt)->values[0];
if (opt) {
const auto &nd = static_cast<const ConfigOptionFloats *>(opt)->values;
if (!nd.empty()) machine_diameter = nd.size() > 1 ? nd[1] : nd[0];
}
}
stream << std::fixed << std::setprecision(1) << machine_diameter;
}
+1 -4
View File
@@ -871,10 +871,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
toggle_line("center_of_surface_pattern", has_centered_surface);
// Orca: separate infills
bool is_internal_infill_separable = is_separable_infill_pattern(config->option<ConfigOptionEnum<InfillPattern>>("sparse_infill_pattern")->value) ||
config->opt_string("sparse_infill_rotate_template") != "" ||
config->opt_string("solid_infill_rotate_template") != "";
toggle_line("separated_infills", is_internal_infill_separable);
toggle_line("separated_infills", is_separable_infill_pattern(pattern));
// Fill order is only meaningful for the center-based surface fill patterns; hide it otherwise.
auto is_centered_fill = [](InfillPattern p) { return p == ipConcentric || p == ipSpiralInset || p == ipArchimedeanChords || p == ipOctagramSpiral; };
@@ -523,15 +523,7 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
// volume for a shaded pass that then draws nothing is what made the model vanish - most obviously
// with zero layers, but equally with a layer that has no texture picked yet.
const bool use_shaded = m_use_shaded_preview && m_shaded_preview_glmodel.is_initialized() && shaded_preview_ready();
// Checker/Distortion are built from the *base* patch and drawn with a polygon offset, which biases
// depth values - it does not move the geometry. It therefore cannot win against a surface that
// genuinely stands in front, and the displaced preview does exactly that: it rises above the base
// surface by the layer's depth. Drawn underneath a UV-check overlay it simply occludes it, which is
// why those two modes looked like they did nothing. Leave it out and let the undisplaced volume show
// through instead (toggle_model_objects_visibility below) - that one *is* coincident with the
// overlay, which is what the offset assumes, and it is the surface whose mapping is being inspected.
const bool use_true_preview = !use_shaded && m_uv_check_mode == UVCheckMode::None &&
m_preview_glmodel.is_initialized();
const bool use_true_preview = !use_shaded && m_preview_glmodel.is_initialized();
// In Checker/Distortion mode the UV-check overlay *is* the surface visualization the user is
// looking at, so the opaque paint-selection highlight must not be drawn on top of it - same
// reasoning as skipping it for the shaded preview (see bug #12). Without this the painted area
@@ -559,13 +551,7 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
render_triangles(selection);
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
}
} else {
// render_triangles() *is* the model in a painter gizmo (it draws every model-part volume with the
// selector's colours), not an overlay on top of one - so it still has to run under a UV-check
// overlay, or nothing draws the surface at all and the checker floats alone over an empty scene.
// Deliberately without the depth bias the branch above applies: the checker/heatmap is drawn later
// with its own -1 offset and has to win against this. Biasing both by the same amount is what made
// the painted area cover the checker and is why this call used to be skipped outright.
} else if (show_paint_overlay) {
render_triangles(selection);
}
@@ -5756,20 +5742,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
const int cur_mode = m_use_shaded_preview ? 1 :
m_uv_check_mode == UVCheckMode::Checker ? 2 :
m_uv_check_mode == UVCheckMode::Distortion ? 3 : 0;
int new_mode = cur_mode;
bool wf_toggle = false;
bool open_uv_editor = false;
// Checker and Distortion both draw *the unwrap* - the first the texture grid laid over it, the second
// its stretch - so they only mean anything for a layer mapped with Unwrap (LSCM). On the default
// triplanar mapping (or cylindrical / spherical / from view) they are faded out with the reason in the
// tooltip, rather than being offered and then showing nothing.
const wxString uv_view_na = active == nullptr ? _L("Add a layer first.") :
active->projection_method != TextureProjectionMethod::LSCM ?
_L("Only for a layer mapped with Unwrap (LSCM) - set the "
"active layer's Mapping to Unwrap to use this view.") :
wxString();
// Either view over a layer that stopped being an unwrap shows nothing at all, so fall back to Normal.
if ((cur_mode == 2 || cur_mode == 3) && !uv_view_na.empty())
int new_mode = cur_mode;
bool wf_toggle = false;
const wxString distortion_na = active == nullptr ? _L("Add a layer first.") :
active->projection_method != TextureProjectionMethod::LSCM ?
_L("Needs the active layer mapped with Unwrap (LSCM).") :
wxString();
// Distortion over a layer that stopped being an unwrap shows nothing at all, so fall back to Normal.
if (cur_mode == 3 && !distortion_na.empty())
new_mode = 0;
const float x0 = ImGui::GetCursorPosX();
@@ -5788,20 +5768,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
ImGui::SameLine(0.f, gap_s);
if (icon_toggle(703, "texture_displacement_checker.svg", cur_mode == 2, icon_md, _L("Checker"),
_L("Checker - a test grid instead of the texture. Where the squares stay square the "
"texture is undistorted; where they stretch, it will too. Opens the UV editor if "
"it is closed"),
uv_view_na)) {
new_mode = 2;
open_uv_editor = true;
}
"texture is undistorted; where they stretch, it will too")))
new_mode = 2;
ImGui::SameLine(0.f, gap_s);
if (icon_toggle(704, "texture_displacement_distortion.svg", cur_mode == 3, icon_md, _L("Distortion"),
_L("Distortion - blue-to-red stretch heatmap over the unwrap. Opens the UV editor if "
"it is closed"),
uv_view_na)) {
new_mode = 3;
open_uv_editor = true;
}
_L("Distortion - blue-to-red stretch heatmap over the unwrap"), distortion_na))
new_mode = 3;
vsep(icon_md);
if (icon_toggle(705, "texture_displacement_wireframe.svg", m_wireframe_overlay, icon_md, _L("Wireframe"),
_L("Wireframe - overlay the mesh edges; independent of the view above")))
@@ -5816,13 +5788,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
hover_tip(_u8L("Rebuilds the preview as soon as anything changes. Turn it off on a heavy model if painting "
"or dragging a slider starts to stutter - the preview then waits until you let go."));
// Both are views of the unwrap, so picking one brings the UV editor up with it - including when that
// view is already the active one and only the pane is missing.
if (open_uv_editor && !m_show_uv_editor) {
m_show_uv_editor = true;
if (new_mode == cur_mode)
update_uv_editor(); // otherwise apply_view_mode() below does it
}
if (new_mode != cur_mode)
apply_view_mode(new_mode);
if (wf_toggle) {
+21 -21
View File
@@ -1862,27 +1862,22 @@ bool Sidebar::priv::switch_diameter(bool single)
auto diameter_left = left_extruder->combo_diameter->GetValue();
auto diameter_right = right_extruder->combo_diameter->GetValue();
if (diameter_left != diameter_right) {
std::string printer_type = wxGetApp().preset_bundle->printers.get_edited_preset().get_printer_type(wxGetApp().preset_bundle);
auto left_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::SentenceCase));
auto right_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::SentenceCase));
MessageDialog dlg(this->plater,
_L("The software does not support using different diameter of nozzles for one print. "
"If the left and right nozzles are inconsistent, we can only proceed with single-head printing. "
"Please confirm which nozzle you would like to use for this project."),
_L("Switch diameter"), wxYES_NO | wxNO_DEFAULT);
dlg.SetButtonLabel(wxID_YES, wxString::Format("%s: %smm", left_name, diameter_left));
dlg.SetButtonLabel(wxID_NO, wxString::Format("%s: %smm", right_name, diameter_right));
int result = dlg.ShowModal();
if (result == wxID_YES)
diameter = diameter_left;
else if (result == wxID_NO)
diameter = diameter_right;
else
double left_value = 0.0, right_value = 0.0;
if (!diameter_left.ToCDouble(&left_value) || !diameter_right.ToCDouble(&right_value))
return false;
Tab* printer_tab = wxGetApp().get_tab(Preset::TYPE_PRINTER);
DynamicPrintConfig new_conf = wxGetApp().preset_bundle->printers.get_edited_preset().config;
auto* nozzle_diameter_opt = new_conf.option<ConfigOptionFloats>("nozzle_diameter");
if (printer_tab == nullptr || nozzle_diameter_opt == nullptr || nozzle_diameter_opt->size() < 2)
return false;
nozzle_diameter_opt->values[0] = left_value;
nozzle_diameter_opt->values[1] = right_value;
printer_tab->load_config(new_conf);
return true;
}
else {
diameter = diameter_left;
}
diameter = diameter_left;
}
return switch_diameter_to(diameter);
@@ -1892,15 +1887,20 @@ bool Sidebar::priv::switch_diameter_to(const wxString &diameter)
{
// ORCA: Check if the selected diameter matches the current nozzle diameter in the config
Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset();
auto* nozzle_diameter = dynamic_cast<const ConfigOptionFloats*>(printer_preset.config.option("nozzle_diameter"));
// ORCA: the left/right combos of a BBL multi-nozzle printer set the nozzles apart without leaving
// the preset (see switch_diameter), so there the preset is only kept while every nozzle matches.
const bool nozzles_apart = nozzle_diameter && nozzle_diameter->size() > 1 && wxGetApp().preset_bundle->is_bbl_vendor() &&
std::any_of(nozzle_diameter->values.begin(), nozzle_diameter->values.end(),
[&diameter](double value) { return get_diameter_string(value) != diameter.ToStdString(); });
// The combo lists printer variants, and the variant of a mixed-nozzle machine ("0.4+0.6") is no
// single extruder's diameter, so the preset's own variant answers first.
const std::string &printer_variant = printer_preset.config.opt_string("printer_variant");
if (printer_variant == diameter.ToStdString()) {
if (printer_variant == diameter.ToStdString() && !nozzles_apart) {
return true;
}
// A named variant ("0.4 High Flow") shares its diameter with the standard profile, which selecting
// the plain diameter switches back to, so only a preset naming no variant is kept by its diameter.
auto* nozzle_diameter = dynamic_cast<const ConfigOptionFloats*>(printer_preset.config.option("nozzle_diameter"));
if (printer_variant.empty() && nozzle_diameter && nozzle_diameter->size() > 0) {
auto current_nozzle_dia = get_diameter_string(nozzle_diameter->values[0]);
// If the selected diameter is the same as current nozzle, don't switch profiles
+2 -1
View File
@@ -5833,7 +5833,8 @@ if (is_marlin_flavor)
// The page edits the extruder selected on the variant switch.
const size_t extruder_idx = size_t(get_current_active_extruder());
bool is_SEMM = m_config->opt_bool("single_extruder_multi_material");
if (is_SEMM && m_extruders_count > 1 && boost::starts_with(opt_key, "nozzle_diameter"))
bool independent_nozzles = m_preset_bundle && m_preset_bundle->is_bbl_vendor() && m_extruders_count > 1;
if (is_SEMM && m_extruders_count > 1 && !independent_nozzles && boost::starts_with(opt_key, "nozzle_diameter"))
{
SuppressBackgroundProcessingUpdate sbpu;
const double new_nd = boost::any_cast<double>(value);
+30 -138
View File
@@ -199,7 +199,6 @@ UVEditorCanvas::UVEditorCanvas(wxWindow *parent)
Bind(wxEVT_MIDDLE_DOWN, &UVEditorCanvas::on_mouse, this);
Bind(wxEVT_MIDDLE_UP, &UVEditorCanvas::on_mouse, this);
Bind(wxEVT_MOTION, &UVEditorCanvas::on_mouse, this);
Bind(wxEVT_MOUSE_CAPTURE_LOST, &UVEditorCanvas::on_capture_lost, this);
Bind(wxEVT_MOUSEWHEEL, &UVEditorCanvas::on_mouse, this);
Bind(wxEVT_LEAVE_WINDOW, &UVEditorCanvas::on_leave, this);
Bind(wxEVT_KEY_DOWN, &UVEditorCanvas::on_key, this);
@@ -751,47 +750,10 @@ void UVEditorCanvas::end_gesture()
m_rot_raw_deg = 0.f;
m_rot_applied_deg = 0.f;
m_modal_scale_accum = 1.f;
drop_mouse();
}
void UVEditorCanvas::cancel_gesture()
{
// Undo what the gesture already applied live, as the Esc path does, and commit nothing.
if (m_on_island_edit) {
if (m_gesture == Gesture::RotateIslandModal && m_rot_applied_deg != 0.f)
m_on_island_edit(m_selected_island, Vec2f::Zero(), -m_rot_applied_deg, 1.f, false);
if (m_gesture == Gesture::ScaleIslandModal && m_modal_scale_accum != 1.f)
m_on_island_edit(m_selected_island, Vec2f::Zero(), 0.f, 1.f / m_modal_scale_accum, false);
}
m_gesture = Gesture::None;
m_rot_raw_deg = 0.f;
m_rot_applied_deg = 0.f;
m_modal_scale_accum = 1.f;
m_vertex_edit_moved = false;
}
void UVEditorCanvas::grab_mouse()
{
if (!HasCapture())
CaptureMouse();
}
void UVEditorCanvas::drop_mouse()
{
if (HasCapture())
ReleaseMouse();
}
// The capture was taken from us (a dialog opened, another application grabbed the pointer). wx
// requires this to cancel the gesture: no commit, no Skip(), and no ReleaseMouse() - the capture is
// already gone, and releasing it again would unbalance the stack.
void UVEditorCanvas::on_capture_lost(wxMouseCaptureLostEvent &)
{
cancel_gesture();
Refresh();
}
void UVEditorCanvas::on_key(wxKeyEvent &evt)
{
const int key = evt.GetKeyCode();
@@ -970,7 +932,7 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
}
m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan;
}
grab_mouse();
CaptureMouse();
Refresh();
} else if (type == wxEVT_RIGHT_DOWN && m_selected_island >= 0 && m_select_mode == SelectMode::Island) {
const Vec2f rel = screen_to_uv(pos) - island_centroid(m_selected_island);
@@ -980,16 +942,12 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
m_rot_base_deg = island_rotation_deg(m_selected_island);
m_rot_display_deg = m_rot_base_deg;
m_gesture_last_angle = std::atan2(rel.y(), rel.x());
grab_mouse();
CaptureMouse();
} else if (type == wxEVT_MIDDLE_DOWN) {
m_gesture = Gesture::Pan;
m_drag_last_px = pos;
grab_mouse();
CaptureMouse();
} else if (type == wxEVT_LEFT_UP || type == wxEVT_RIGHT_UP || type == wxEVT_MIDDLE_UP) {
// The drag is over either way. A modal R/S keeps running until a click confirms it, but it
// tracks the pointer over this canvas and needs no capture to do so, so the capture goes back
// here rather than waiting for that click - which may never come.
drop_mouse();
if (m_gesture != Gesture::RotateIslandModal && m_gesture != Gesture::ScaleIslandModal) {
end_gesture();
Refresh();
@@ -1777,7 +1735,7 @@ public:
bool toggle, bool accent = false, int size_dip = 26)
: wxWindow(parent, id, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE | wxFULL_REPAINT_ON_RESIZE)
, m_icon_name(icon), m_icon_dip(size_dip >= 26 ? 16 : 14), m_label(label), m_toggle(toggle), m_accent(accent)
, m_size_dip(size_dip), m_tip(tip)
, m_size_dip(size_dip)
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetToolTip(tip);
@@ -1790,7 +1748,7 @@ public:
Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent &) { m_hover = true; Refresh(); });
Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &) { m_hover = false; m_pressed = false; Refresh(); });
Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &) {
if (usable()) {
if (IsEnabled()) {
m_pressed = true;
Refresh();
}
@@ -1799,7 +1757,7 @@ public:
const bool was_pressed = m_pressed;
m_pressed = false;
Refresh();
if (!was_pressed || !usable() || !GetClientRect().Contains(e.GetPosition()))
if (!was_pressed || !IsEnabled() || !GetClientRect().Contains(e.GetPosition()))
return;
if (m_toggle)
m_on = !m_on;
@@ -1839,30 +1797,6 @@ public:
Refresh();
return changed;
}
// Soft-disable: the button is drawn faded and swallows clicks, but stays a live window, so hovering it
// still raises its tooltip - now with `reason` appended, saying what to do to make it usable. A window
// really disabled with Enable(false) gets no mouse events at all on GTK and MSW, which leaves the user
// guessing; this is the same trade-off the gizmo panel's icon_toggle() makes with its `unavailable`.
// An empty reason makes the button usable again.
void SetUnavailable(const wxString &reason)
{
if (reason == m_unavailable)
return;
m_unavailable = reason;
SetToolTip(reason.empty() || m_tip.empty() ? m_tip : m_tip + "\n\n" + reason);
if (!m_unavailable.empty())
m_pressed = false; // a reason appearing mid-press cancels the press
Refresh();
}
// Replaces the plain tooltip, keeping whatever reason is currently appended to it.
void SetTip(const wxString &tip)
{
if (tip == m_tip)
return;
m_tip = tip;
SetToolTip(m_unavailable.empty() || m_tip.empty() ? m_tip : m_tip + "\n\n" + m_unavailable);
}
bool usable() const { return IsEnabled() && m_unavailable.empty(); }
protected:
wxSize DoGetBestSize() const override
@@ -1881,7 +1815,7 @@ private:
const PaneColors c = PaneColors::current();
const wxRect r = GetClientRect();
const wxColour teal(0x00, 0x96, 0x88);
const bool enabled = usable();
const bool enabled = IsEnabled();
wxColour fill = c.bg, border = c.frame, text = c.ink;
if (m_accent) {
@@ -1941,8 +1875,6 @@ private:
bool m_toggle = false;
bool m_accent = false;
int m_size_dip = 26;
wxString m_tip; // the tooltip without any m_unavailable reason appended
wxString m_unavailable; // non-empty: faded and unclickable, and why (see SetUnavailable())
bool m_on = false;
bool m_badge = false;
bool m_hover = false;
@@ -1988,9 +1920,6 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
});
m_layer_name->SetMinSize(wxSize(FromDIP(30), -1));
m_tile = text(wxEmptyString, c.dim);
m_tile->SetToolTip(_L("The active layer's tile size: how much of the model one repeat of the texture covers. The "
"canvas is measured in tiles, so one grid cell is one repeat. Change it with Tiling in the "
"layer's settings."));
header->Add(m_thumb, 0, wxALIGN_CENTER_VERTICAL);
header->Add(m_layer_name, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, gap);
header->Add(m_tile, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, gap);
@@ -2044,19 +1973,9 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
strip->Add(r, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(11));
strip->AddSpacer(FromDIP(7));
};
m_select[0] = tool(ID_UV_SELECT_ISLAND, "texture_displacement_uv_select_island",
_L("Island - work on whole islands. Click one to select it, then drag to move it, right-drag to "
"rotate it, or press R to rotate and S to scale with the mouse (click or Enter to confirm, Esc "
"to cancel)."),
true);
m_select[1] = tool(ID_UV_SELECT_VERTEX, "texture_displacement_uv_select_vertex",
_L("Vertex - drag vertices to reshape an island by hand; Shift adds to the selection, Ctrl "
"toggles one in or out of it."),
true);
m_select[2] = tool(ID_UV_SELECT_EDGE, "texture_displacement_uv_select_edge",
_L("Edge - drag edges to reshape an island by hand; Shift adds to the selection, Ctrl toggles "
"one in or out of it."),
true);
m_select[0] = tool(ID_UV_SELECT_ISLAND, "texture_displacement_uv_select_island", _L("Island - move, rotate and scale whole islands"), true);
m_select[1] = tool(ID_UV_SELECT_VERTEX, "texture_displacement_uv_select_vertex", _L("Vertex - drag vertices to reshape; Shift/Ctrl to multi-select"), true);
m_select[2] = tool(ID_UV_SELECT_EDGE, "texture_displacement_uv_select_edge", _L("Edge - drag edges to reshape; Shift/Ctrl to multi-select"), true);
strip_rule();
m_mark_seams = tool(ID_UV_MARK_SEAMS, "texture_displacement_uv_seam",
_L("Mark seams - click edges on the model to cut the unwrap along them. The edge under the cursor is "
@@ -2077,11 +1996,7 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
m_clear_edits = tool(ID_UV_CLEAR_EDITS, "texture_displacement_uv_clear_edits",
_L("Clear UV edits - discard all manual vertex/edge moves and return the unwrap to its automatic shape"), false);
m_snap = tool(ID_UV_SNAP, "texture_displacement_uv_snap", _L("Snap - stick islands together when dragging one against another"), true);
m_frame = tool(ID_UV_FRAME, "texture_displacement_uv_frame",
_L("Frame all islands, fitting every one of them in view (Home or F).\n"
"Elsewhere on the canvas: scroll to zoom around the cursor, and middle-drag - or drag empty space - "
"to pan."),
false);
m_frame = tool(ID_UV_FRAME, "texture_displacement_uv_frame", _L("Frame all islands (Home)"), false);
strip->AddSpacer(FromDIP(4));
m_canvas = new UVEditorCanvas(this);
@@ -2092,7 +2007,6 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
// ---- status line: the current gesture on the left, the unwrap summary on the right ----
m_status = text(wxEmptyString, c.dim, wxST_ELLIPSIZE_END);
m_status->SetToolTip(_L("What is selected, and the exact figures of the move, rotation or scale while you drag one."));
m_status->SetMinSize(wxSize(FromDIP(40), -1));
m_stats = text(wxEmptyString, c.dim);
auto *status = new wxBoxSizer(wxHORIZONTAL);
@@ -2168,25 +2082,19 @@ void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
} else {
m_thumb->SetBitmap(wxNullBitmap);
}
// Every tool that cannot be used right now is faded with the reason appended to its tooltip, rather than
// being hard-disabled (which would hide the tooltip too - see UVToolButton::SetUnavailable()).
const wxString no_layer = s.has_layer ? wxString() :
_L("The pane follows the active texture layer, and that layer has to be mapped "
"with Unwrap (LSCM). Add a layer and set its Mapping to Unwrap.");
m_thumb->SetUnavailable(no_layer);
m_layer_name->SetToolTip(s.has_layer ? m_thumb->GetToolTipText() : no_layer);
m_thumb->Enable(s.has_layer);
for (int i = 0; i < 3; ++i) {
m_background[i]->SetValue(int(s.background) == i);
m_background[i]->SetUnavailable(no_layer);
m_background[i]->Enable(s.has_layer);
}
m_unwrap->SetUnavailable(no_layer);
m_unwrap->Enable(s.has_layer);
m_unwrap->SetBadge(s.unwrap_stale);
m_unwrap->SetTip(s.unwrap_stale ?
_L("Out of date - the paint, the seams or the seam angle changed since this unwrap was made. "
"Press to unwrap again.") :
_L("Flatten the painted area into UV islands. It is computed only when you press this, not on "
"every edit - so paint, change the seam angle or mark seams first, then press Unwrap."));
m_unwrap->SetToolTip(s.unwrap_stale ?
_L("Out of date - the paint, the seams or the seam angle changed since this unwrap was made. "
"Press to unwrap again.") :
_L("Flatten the painted area into UV islands. It is computed only when you press this, not on "
"every edit - so paint, change the seam angle or mark seams first, then press Unwrap."));
if (m_seam_angle->GetValue() != int(std::lround(s.seam_angle_deg)))
m_seam_angle->SetValue(int(std::lround(s.seam_angle_deg)));
@@ -2195,26 +2103,15 @@ void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
m_connect->Enable(s.has_layer);
m_mark_seams->SetValue(s.mark_seams);
m_mark_seams->SetUnavailable(no_layer);
m_mark_seams->Enable(s.has_layer);
m_seam_path->SetValue(s.seam_path);
m_seam_path->SetUnavailable(!no_layer.empty() ? no_layer :
s.mark_seams ? wxString() :
_L("Turn Mark seams on first - Path is a quicker way of marking them."));
m_clear_seams->SetUnavailable(!no_layer.empty() ? no_layer :
s.has_seams ? wxString() :
_L("No seams are marked on this layer."));
m_clear_edits->SetUnavailable(!no_layer.empty() ? no_layer :
s.has_uv_edits ? wxString() :
_L("No islands have been reshaped by hand, so there is nothing to "
"discard."));
m_seam_path->Enable(s.has_layer && s.mark_seams);
m_clear_seams->Enable(s.has_layer && s.has_seams);
m_clear_edits->Enable(s.has_layer && s.has_uv_edits);
m_stats->SetLabel(s.unwrapped ? wxString::Format(_L("%d islands, %s faces"), s.island_count,
wxString(std::to_string(s.face_count))) :
wxString());
m_stats->SetToolTip(s.unwrapped ? _L("How the painted area came out of the unwrap: the number of separate pieces it "
"was cut into (at the seams and at edges sharper than the seam angle), and how "
"many triangles they hold in total.") :
wxString());
refresh_selection_tools();
if (relayout)
Layout();
@@ -2224,24 +2121,19 @@ void UVEditorPanel::refresh_selection_tools()
{
const bool has_islands = m_canvas->has_islands();
const int mode = int(m_canvas->select_mode());
// Faded rather than hard-disabled, so the tooltip still says what is missing (see apply_state()).
const wxString not_unwrapped = has_islands ? wxString() : _L("Press Unwrap first - there are no islands to work on yet.");
for (int i = 0; i < 3; ++i) {
m_select[i]->SetValue(i == mode);
m_select[i]->SetUnavailable(not_unwrapped);
m_select[i]->Enable(has_islands);
}
const bool island_picked = has_islands && m_canvas->select_mode() == UVEditorCanvas::SelectMode::Island &&
m_canvas->selected_island() >= 0;
const wxString no_island = !not_unwrapped.empty() ? not_unwrapped :
island_picked ? wxString() :
_L("Click an island on the canvas first, in Island mode.");
m_avg_scale->SetUnavailable(not_unwrapped);
m_cut->SetUnavailable(no_island);
m_join->SetUnavailable(no_island);
m_unjoin->SetUnavailable(no_island);
m_snap->SetUnavailable(not_unwrapped);
m_avg_scale->Enable(has_islands);
m_cut->Enable(island_picked);
m_join->Enable(island_picked);
m_unjoin->Enable(island_picked);
m_snap->Enable(has_islands);
m_snap->SetValue(m_canvas->snap_enabled());
m_frame->SetUnavailable(not_unwrapped);
m_frame->Enable(has_islands);
}
void UVEditorPanel::on_tool(wxCommandEvent &evt)
-11
View File
@@ -200,7 +200,6 @@ private:
void on_paint(wxPaintEvent &evt);
void on_size(wxSizeEvent &evt);
void on_mouse(wxMouseEvent &evt);
void on_capture_lost(wxMouseCaptureLostEvent &evt);
void on_key(wxKeyEvent &evt);
void on_leave(wxMouseEvent &evt); // drops the +/- cursor hint when the pointer leaves the canvas
void on_erase_background(wxEraseEvent &evt) {} // required to avoid flicker on MSW, deliberately a no-op
@@ -245,16 +244,6 @@ private:
// vertex of some *other* island, in texture-UV space. Zero if nothing is within reach (#2).
Vec2f snap_correction(int island) const;
void end_gesture();
// Cancels the gesture instead of finishing it: nothing is committed, and a modal rotate/scale is
// put back the way Esc puts it back. Used when the capture is taken away from us.
void cancel_gesture();
// One capture at a time, released exactly once. Pressing a second button mid-drag would otherwise
// nest a second capture that the single release on button-up cannot undo, and macOS never sends
// wxEVT_MOUSE_CAPTURE_LOST to recover from that. A leaked capture is not a local problem there:
// while any wx window holds one, wxOSX routes every mouse event to it and the application stops
// seeing enter/leave and motion entirely, which also takes its tooltips down.
void grab_mouse();
void drop_mouse();
// Rebuilds the status line from the current gesture/selection and pushes it to m_on_status.
void update_status();
// Re-picks what a click at `pos` would grab in the current select mode, and repaints when that changed.
+19
View File
@@ -69,6 +69,10 @@ webkit_web_view_run_javascript_finish (WebKitWebView
GError **error);
WEBKIT_API void
webkit_javascript_result_unref (WebKitJavascriptResult *js_result);
WEBKIT_API unsigned int
webkit_get_major_version (void);
WEBKIT_API unsigned int
webkit_get_minor_version (void);
}
#endif
@@ -401,6 +405,21 @@ bool WebView::NeedsRecreateOnShow()
return false;
#endif
}
bool WebView::WebKitAtLeast(int major, int minor)
{
#if defined(__linux__)
const unsigned int running_major = webkit_get_major_version();
if (running_major != static_cast<unsigned int>(major))
return running_major > static_cast<unsigned int>(major);
return webkit_get_minor_version() >= static_cast<unsigned int>(minor);
#else
(void) major;
(void) minor;
return false;
#endif
}
#if wxUSE_WEBVIEW_EDGE
bool WebView::CheckWebViewRuntime()
{
+4
View File
@@ -30,6 +30,10 @@ public:
// ignoring every navigation. A panel that gets true here recreates its view on first Show().
static bool NeedsRecreateOnShow();
// Runtime version of the linked WebKitGTK, for gating the Linux DMA-BUF
// renderer workaround. Returns false on non-GTK builds.
static bool WebKitAtLeast(int major, int minor);
static void RecreateAll();
};
+73 -9
View File
@@ -116,6 +116,8 @@ static const std::unordered_map<std::string, AuditEventCategory> audit_event_cat
{"_winapi.CreateProcess", AuditEventCategory::ProcessCreate},
{"_posixsubprocess.fork_exec", AuditEventCategory::ProcessCreate},
// threading
{"_thread.start_new_thread", AuditEventCategory::Threading},
// processreplace: exec* replaces the current process image rather than spawning a child
{"os.exec", AuditEventCategory::ProcessReplace},
};
@@ -739,6 +741,12 @@ std::vector<std::string> audit_targets(const std::string& event_name, AuditEvent
}
return targets;
}
case AuditEventCategory::Threading:
// Thread creation exposes no user-supplied target. Use a fixed sentinel so the grant
// persists per plugin: the permission list matches targets by exact string, and the
// started function's repr embeds an address that changes every run.
targets.emplace_back("thread");
return targets;
default:
break;
}
@@ -765,6 +773,7 @@ std::vector<std::string>* permission_list_for(AuditEventCategory category, Plugi
case AuditEventCategory::Http: return &permissions.network_http;
case AuditEventCategory::Socket: return &permissions.network_socket;
case AuditEventCategory::ProcessCreate: return &permissions.process;
case AuditEventCategory::Threading: return &permissions.threading;
case AuditEventCategory::ProcessReplace: return &permissions.process;
default: return nullptr;
}
@@ -837,6 +846,8 @@ wxString audit_message(AuditEventCategory category, const wxString& plugin_name,
return wxString::Format(_L("Plugin \"%s\" is requesting to open a network connection to:\n%s"), plugin_name, target_list);
case AuditEventCategory::ProcessCreate:
return wxString::Format(_L("Plugin \"%s\" is requesting to run the following command(s):\n%s"), plugin_name, target_list);
case AuditEventCategory::Threading:
return wxString::Format(_L("Plugin \"%s\" is requesting permission to create a thread."), plugin_name);
case AuditEventCategory::ProcessReplace:
return wxString::Format(_L("Plugin \"%s\" is requesting to replace the running application with:\n%s"), plugin_name, target_list);
default:
@@ -844,6 +855,67 @@ wxString audit_message(AuditEventCategory category, const wxString& plugin_name,
}
}
// Builds and shows the modal permission prompt. Must run on the GUI thread.
bool prompt_for_targets(AuditEventCategory category, const std::string& plugin_name, const std::string& event_name,
const std::vector<std::string>& unresolved)
{
wxString target_list;
for (const auto& target : unresolved)
target_list += wxString::FromUTF8(target.c_str()) + "\n";
wxMessageDialog dialog(nullptr,
audit_message(category, wxString::FromUTF8(plugin_name.c_str()),
wxString::FromUTF8(event_name.c_str()), target_list),
_L("Plugin permission request"), wxYES_NO | wxICON_WARNING);
return dialog.ShowModal() == wxID_YES;
}
// Records a grant in the plugin's sidecar so it is not asked again. Reads the install state
// freshly because the async prompt outlives the caller's stack copy of it.
void persist_grant(const std::string& plugin_key, AuditEventCategory category, const std::vector<std::string>& targets)
{
PluginInstallState state;
if (!PluginManager::instance().get_install_state(plugin_key, state))
return;
std::vector<std::string>* permission_list = permission_list_for(category, state.permissions);
if (!permission_list)
return;
for (const auto& target : targets)
persist_permission(plugin_key, state, *permission_list, target);
}
// Requests permission for an audited event, returning true when it is already granted or the user
// approves an inline prompt.
//
// An audited event can fire on a thread the UI thread may itself be blocked waiting on: the
// SlicingPipeline hook runs on the slicing worker thread (see PluginHooks.cpp), and
// BackgroundSlicingProcess::stop()/stop_internal() park the UI thread until that worker stops.
// Blocking the worker on a marshaled modal -- which is safe for the plugin-load worker that
// request_filesystem_read_permissions runs on -- would therefore deadlock the application (the
// invariant PluginHostUi.cpp documents for slicing-hook UI calls). Off the main thread the prompt
// is therefore posted asynchronously and the current event denied (fail closed, like an unanswered
// prompt); the grant is persisted once the user accepts, so a later attempt succeeds without
// re-prompting.
bool request_permission(AuditEventCategory category, const std::string& plugin_key, const std::string& plugin_name,
const std::string& event_name, const std::vector<std::string>& unresolved)
{
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return false;
if (wxIsMainThread())
return prompt_for_targets(category, plugin_name, event_name, unresolved);
GUI::wxGetApp().CallAfter([category, plugin_key, plugin_name, event_name, unresolved]() {
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return;
if (prompt_for_targets(category, plugin_name, event_name, unresolved))
persist_grant(plugin_key, category, unresolved);
});
return false;
}
int decide_audited_event(PluginAuditManager& mgr,
PluginInstallState& state,
const std::string& plugin_key,
@@ -864,15 +936,7 @@ int decide_audited_event(PluginAuditManager& mgr,
return 0;
}
wxString target_list;
for (const auto& target : unresolved)
target_list += wxString::FromUTF8(target.c_str()) + "\n";
wxMessageDialog dialog(nullptr,
audit_message(category, wxString::FromUTF8(plugin_name.c_str()),
wxString::FromUTF8(event_name.c_str()), target_list),
_L("Plugin permission request"), wxYES_NO | wxICON_WARNING);
if (dialog.ShowModal() != wxID_YES)
if (!request_permission(category, plugin_key, plugin_name, event_name, unresolved))
return report_denied(mgr, event_name, {false, "audit permission required"});
if (permission_list)
+2
View File
@@ -807,6 +807,7 @@ bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstall
read_string_list("network_http", parsed.permissions.network_http);
read_string_list("network_socket", parsed.permissions.network_socket);
read_string_list("process", parsed.permissions.process);
read_string_list("threading", parsed.permissions.threading);
}
if (state.contains("enabled") && state["enabled"].is_boolean())
@@ -850,6 +851,7 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
{"network_http", state.permissions.network_http},
{"network_socket", state.permissions.network_socket},
{"process", state.permissions.process},
{"threading", state.permissions.threading},
};
nlohmann::json capabilities = nlohmann::json::array();
+1
View File
@@ -92,6 +92,7 @@ struct PluginPermissions
std::vector<std::string> network_http;
std::vector<std::string> network_socket;
std::vector<std::string> process;
std::vector<std::string> threading;
};
struct PluginInstallState {
+198
View File
@@ -1,6 +1,7 @@
#include <catch2/catch_all.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
@@ -1790,3 +1791,200 @@ TEST_CASE("Sparse plane-path anchors match the printed infill", "[Fill][Internal
// would hide anchors that no longer coincide with printed lines.
CHECK(unscale<double>(max_distance) <= config.opt_float("resolution"));
}
// Orca: Slices the meshes as the parts of one object, where they are.
static Print &slice_parts(Print &print, DynamicPrintConfig config, const std::vector<TriangleMesh> &parts)
{
config.set_deserialize_strict({{"layer_height", 0.2},
{"initial_layer_print_height", 0.2},
{"elefant_foot_compensation", 0},
{"top_shell_thickness", 0},
{"bottom_shell_thickness", 0}});
Model model;
Slic3r::Test::init_print({parts.front()}, print, model, config, nullptr, false);
for (size_t i = 1; i < parts.size(); ++ i)
model.objects.front()->add_volume(TriangleMesh(parts[i]), ModelVolumeType::MODEL_PART, false);
print.apply(model, config);
print.process();
return print;
}
// Orca: Two identical cubes in one mesh that never touch, so each is a body of its own.
static Print &slice_two_bodies(Print &print, const DynamicPrintConfig &config, double height)
{
TriangleMesh mesh = make_cube(20, 20, height);
TriangleMesh second = make_cube(20, 20, height);
second.translate(33, 7, 0);
mesh.merge(second);
return slice_parts(print, config, {mesh});
}
// Orca: Counts the points sampled along both sets that the other set does not repeat.
static void count_unmatched(const Polylines &a, const Polylines &b, size_t &sampled, size_t &unmatched)
{
const std::array<const Polylines *, 2> sets{&a, &b};
for (size_t i = 0; i < 2; ++ i) {
const Polylines &other = *sets[1 - i];
const AABBTreeLines::LinesDistancer<Line> distancer(to_lines(other));
for (const Polyline &path : *sets[i])
for (const Point &point : path.equally_spaced_points(scale_(0.2))) {
++ sampled;
unmatched += other.empty() || distancer.distance_from_lines<false>(point) > scale_(0.05);
}
}
}
static Polylines layer_paths(const Layer &layer, ExtrusionRole role)
{
Polylines polylines;
for (const LayerRegion *region : layer.regions())
for (const ExtrusionEntity *entity : region->fills.flatten().entities)
if (entity->role() == role)
entity->collect_polylines(polylines);
return polylines;
}
// Orca: Share of the paths of a role that the other body does not repeat around its own center.
static double unmatched_between_bodies(const Print &print, ExtrusionRole role)
{
size_t sampled = 0, unmatched = 0;
for (const Layer *layer : print.objects().front()->layers()) {
REQUIRE(layer->lslices.size() == 2);
const Polylines polylines = layer_paths(*layer, role);
std::array<Polylines, 2> paths;
for (size_t body = 0; body < 2; ++ body) {
// Orca: Exclude the links along the walls, which each body may chain differently.
paths[body] = intersection_pl(polylines, shrink(to_polygons(layer->lslices[body]), scale_(3.)));
for (Polyline &path : paths[body])
path.translate(-layer->lslices_bboxes[body].center());
}
count_unmatched(paths[0], paths[1], sampled, unmatched);
}
REQUIRE(sampled > 0);
return double(unmatched) / double(sampled);
}
// Orca: Share of the paths of a role inside a bed region that two slices of the same body do not share.
static double unmatched_between_prints(const Print &a, const Print &b, ExtrusionRole role, const Polygons &region)
{
const PrintObject &object_a = *a.objects().front(), &object_b = *b.objects().front();
REQUIRE(object_a.layer_count() == object_b.layer_count());
size_t sampled = 0, unmatched = 0;
for (size_t i = 0; i < object_a.layer_count(); ++ i) {
std::array<Polylines, 2> paths;
for (const PrintObject *object : {&object_a, &object_b}) {
Polylines &out = paths[object == &object_b];
out = layer_paths(*object->get_layer(int(i)), role);
for (Polyline &path : out)
path.translate(object->instances().front().shift);
out = intersection_pl(out, region);
}
count_unmatched(paths[0], paths[1], sampled, unmatched);
}
REQUIRE(sampled > 0);
return double(unmatched) / double(sampled);
}
TEST_CASE("Separated infill centers the sparse infill of each body on itself", "[Fill][Regression]")
{
const std::string pattern = GENERATE("line", "zigzag", "crosszag", "honeycomb", "3dhoneycomb", "crosshatch", "tpmsd", "tpmsfk", "gyroid");
const bool separated = GENERATE(false, true);
CAPTURE(pattern, separated);
auto config = DynamicPrintConfig::full_print_config();
// Orca: The Zig Zag patterns mirror each body about its own center.
config.set_deserialize_strict({{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "20%"},
{"symmetric_infill_y_axis", true},
{"top_shell_layers", 0},
{"bottom_shell_layers", 0},
{"separated_infills", separated}});
Print print;
const double unmatched = unmatched_between_bodies(slice_two_bodies(print, config, 2.), erInternalInfill);
// Orca: Without separation both bodies cut one object-wide pattern at different places.
if (separated)
CHECK(unmatched < 0.02);
else
CHECK(unmatched > 0.5);
}
TEST_CASE("Separated infill centers monotonic and rectilinear bridges on each body", "[Fill][InternalBridge][Regression]")
{
// Orca: Bridges use the Monotonic pattern below monotonic top surfaces and Rectilinear otherwise.
const std::string top_pattern = GENERATE("monotonicline", "rectilinear");
const bool separated = GENERATE(false, true);
CAPTURE(top_pattern, separated);
auto config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({{"sparse_infill_pattern", "rectilinear"},
{"sparse_infill_density", "15%"},
{"top_surface_pattern", top_pattern},
{"top_shell_layers", 4},
{"bottom_shell_layers", 0},
{"separated_infills", separated}});
Print print;
const double unmatched = unmatched_between_bodies(slice_two_bodies(print, config, 4.), erInternalBridgeInfill);
if (separated)
CHECK(unmatched < 0.02);
else
CHECK(unmatched > 0.5);
}
// Orca: Share of the infill of an off center pillar, and of the frame of four overlapping bars around it,
// that each body sliced alone does not repeat. The frame is one body of several parts that holds the pillar.
static std::pair<double, double> frame_and_pillar_unmatched(const DynamicPrintConfig &config)
{
auto box = [](double x, double y, double size_x, double size_y) {
TriangleMesh mesh = make_cube(size_x, size_y, 6);
mesh.translate(x, y, 0);
return mesh;
};
const std::vector<TriangleMesh> frame{box(0, 0, 60, 14), box(0, 46, 60, 14), box(0, 0, 14, 60), box(46, 0, 14, 60)};
const std::vector<TriangleMesh> pillar{box(18, 20, 16, 16)};
std::vector<TriangleMesh> both = frame;
both.push_back(pillar.front());
Print print_both, print_frame, print_pillar;
slice_parts(print_both, config, both);
slice_parts(print_frame, config, frame);
slice_parts(print_pillar, config, pillar);
// Orca: Bed regions 3 mm inside the walls, away from the links along them.
auto rect = [](double x0, double y0, double x1, double y1) {
return Polygon({Point::new_scale(x0, y0), Point::new_scale(x1, y0), Point::new_scale(x1, y1), Point::new_scale(x0, y1)});
};
return {unmatched_between_prints(print_both, print_pillar, erInternalInfill, {rect(21, 23, 31, 33)}),
unmatched_between_prints(print_both, print_frame, erInternalInfill, diff(Polygons{rect(3, 3, 57, 57)}, Polygons{rect(11, 11, 49, 49)}))};
}
TEST_CASE("Separated infill fills each body like the body sliced alone", "[Fill][Regression]")
{
// Orca: Hilbert Curve and the Zig Zag links follow the extent of the box, not only its center.
const std::string pattern = GENERATE("hilbertcurve", "zigzag", "crosszag", "gyroid");
CAPTURE(pattern);
auto config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "20%"},
{"symmetric_infill_y_axis", true},
{"top_shell_layers", 0},
{"bottom_shell_layers", 0},
{"separated_infills", true}});
const std::pair<double, double> unmatched = frame_and_pillar_unmatched(config);
CHECK(unmatched.first < 0.02);
CHECK(unmatched.second < 0.02);
}
TEST_CASE("Adaptive infill fills each body like the body sliced alone", "[Fill][Regression]")
{
const std::string pattern = GENERATE("adaptivecubic", "supportcubic");
// Orca: Octree infill centers each body whether or not separated infills are enabled.
const bool separated = GENERATE(false, true);
CAPTURE(pattern, separated);
auto config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "40%"},
{"top_shell_layers", 0},
{"bottom_shell_layers", 0},
{"separated_infills", separated}});
// Orca: The octree of the whole object is laid out from its center, which the off center pillar does not share.
const std::pair<double, double> unmatched = frame_and_pillar_unmatched(config);
CHECK(unmatched.first < 0.02);
CHECK(unmatched.second < 0.02);
}
+38
View File
@@ -910,6 +910,44 @@ TEST_CASE("Each filament sets the pressure advance of its extruder variant on a
}
}
// Without a dynamic nozzle map the hotend placeholders carry no hotend index (-1) on a BBL printer: Bambu
// firmware reads an explicit index as a request for the Filament Track Switch and rejects the job. Any other
// printer gets the extruder index of the filament.
TEST_CASE("Hotend placeholders resolve to -1 on a BBL printer and to the extruder index elsewhere", "[MultiFilament]")
{
// filament 1 prints the walls on extruder 2, filament 2 the infill on extruder 1
auto [is_bbl, start, changes] = GENERATE(table<bool, std::string, std::set<std::string>>({
{ true, "; hotend start filament 0: -1 -1 -1",
{ "; hotend change filament 0: -1 -1", "; hotend change filament 1: -1 -1" } },
// the first change loads filament 1 with no filament before it, so there is no outgoing extruder
{ false, "; hotend start filament 0: 1 1 1",
{ "; hotend change filament -1: -1 1", "; hotend change filament 0: 1 0", "; hotend change filament 1: 0 1" } },
}));
DynamicPrintConfig config = two_extruder_pressure_advance_config("2,1", "0,0,0,0", 1, 2);
config.set_key_value("machine_start_gcode", new ConfigOptionString(
"; hotend start filament [initial_no_support_extruder]: [initial_no_support_hotend] [current_hotend] {first_non_support_hotend[0]}"));
config.set_key_value("change_filament_gcode", new ConfigOptionString(
"; hotend change filament [current_filament_id]: [current_hotend] [next_hotend]"));
Print print;
print.is_BBL_printer() = is_bbl;
Model model;
init_print({ cube(20) }, print, model, config);
const std::string gcode = Slic3r::Test::gcode(print);
INFO("BBL printer: " << is_bbl);
std::vector<std::string> starts;
std::set<std::string> found;
std::istringstream stream(gcode);
for (std::string line; std::getline(stream, line);) {
if (line.rfind("; hotend start ", 0) == 0)
starts.push_back(line);
else if (line.rfind("; hotend change ", 0) == 0)
found.insert(line);
}
CHECK(starts == std::vector<std::string>{ start });
CHECK(found == changes);
}
// Filament 1 prints the walls on extruder 1 (variant index 0), filament 2 the infill on extruder 2 (variant index 3).
TEST_CASE("Adaptive pressure advance on one extruder leaves the other extruder's pressure advance alone", "[MultiFilament]")
{
+6 -4
View File
@@ -512,12 +512,13 @@ TEST_CASE("Separated infill keeps fragmented and nested bodies independent", "[P
print.process();
const PrintObject &object = *print.objects().front();
REQUIRE(object.layer_count() > 1);
CHECK(object.separated_body_bboxes().size() == grid_size * grid_size + 2);
for (const Layer *layer : object.layers()) {
REQUIRE(layer->lslices.size() == grid_size * grid_size + 2);
REQUIRE(layer->lslices_separated_component_bboxes.size() == layer->lslices.size());
REQUIRE(layer->lslices_separated_component_ids.size() == layer->lslices.size());
size_t holes = 0;
for (size_t i = 0; i < layer->lslices.size(); ++ i) {
const BoundingBox &body = layer->lslices_separated_component_bboxes[i];
const BoundingBox &body = object.separated_body_bboxes()[layer->lslices_separated_component_ids[i]];
const BoundingBox &island = layer->lslices_bboxes[i];
CHECK(body.min == island.min);
CHECK(body.max == island.max);
@@ -574,6 +575,7 @@ TEST_CASE("Body centering survives islands merging and splitting between layers"
REQUIRE(object.get_layer(1)->lslices.size() == 3);
REQUIRE(object.get_layer(2)->lslices.size() == 3);
REQUIRE(object.get_layer(4)->lslices.size() == 5);
CHECK(object.separated_body_bboxes().size() == 2);
BoundingBox isolated_bbox = object.get_layer(0)->lslices_bboxes.front();
for (const BoundingBox &bbox : object.get_layer(0)->lslices_bboxes)
@@ -585,10 +587,10 @@ TEST_CASE("Body centering survives islands merging and splitting between layers"
if (bbox.min.x() < isolated_bbox.min.x())
connected_bbox.merge(bbox);
for (const Layer *layer : object.layers()) {
REQUIRE(layer->lslices_separated_component_bboxes.size() == layer->lslices.size());
REQUIRE(layer->lslices_separated_component_ids.size() == layer->lslices.size());
for (size_t i = 0; i < layer->lslices.size(); ++ i) {
const BoundingBox &expected = layer->lslices_bboxes[i].min.x() < isolated_bbox.min.x() ? connected_bbox : isolated_bbox;
const BoundingBox &actual = layer->lslices_separated_component_bboxes[i];
const BoundingBox &actual = object.separated_body_bboxes()[layer->lslices_separated_component_ids[i]];
CHECK(actual.min == expected.min);
CHECK(actual.max == expected.max);
}
+27
View File
@@ -1,10 +1,15 @@
#include <catch2/catch_all.hpp>
#include <algorithm>
#include <utility>
#include <vector>
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Point.hpp"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "libslic3r/Model.hpp"
#include "libslic3r/Geometry.hpp"
@@ -44,3 +49,25 @@ TEST_CASE("A part's 2D convex hull is its footprint projected onto the bed", "[M
CHECK(bb.max.y() == scaled(45.));
}
}
TEST_CASE("An object's raw mesh keeps the triangles of each part on its own vertices", "[Model]")
{
Model model;
ModelObject *object = model.add_object();
object->add_volume(make_cube(10, 10, 10), ModelVolumeType::MODEL_PART, false);
TriangleMesh second = make_cube(10, 10, 10);
second.translate(30, 0, 0);
object->add_volume(std::move(second), ModelVolumeType::MODEL_PART, false);
// Two separate cubes stay two closed components, one around each cube.
const std::vector<indexed_triangle_set> parts = its_split(object->raw_indexed_triangle_set());
REQUIRE(parts.size() == 2);
std::vector<double> min_x;
for (const indexed_triangle_set &part : parts) {
CHECK(part.indices.size() == 12);
min_x.push_back(bounding_box(part).min.x());
}
std::sort(min_x.begin(), min_x.end());
CHECK_THAT(min_x.front(), Catch::Matchers::WithinAbs(0., 1e-4));
CHECK_THAT(min_x.back(), Catch::Matchers::WithinAbs(30., 1e-4));
}
@@ -122,6 +122,7 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins
state.permissions.network_http = {"https://api.example.com"};
state.permissions.network_socket = {"192.168.45.6:443"};
state.permissions.process = {"/usr/bin/curl"};
state.permissions.threading = {"thread"};
REQUIRE(write_install_state(plugin_dir, state));
// Permission data is persisted in the same sidecar as the installation metadata.
@@ -132,6 +133,7 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins
CHECK(persisted.permissions.network_http == state.permissions.network_http);
CHECK(persisted.permissions.network_socket == state.permissions.network_socket);
CHECK(persisted.permissions.process == state.permissions.process);
CHECK(persisted.permissions.threading == state.permissions.threading);
// Reading the sidecar back onto a freshly-scanned descriptor (whose header version is still
// 1.0.0) must surface the cloud-installed 1.2.0. This is what lets update_cloud_metadata compare