mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-09 00:31:19 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bf0901a95 | ||
|
|
9d32c1c545 | ||
|
|
67a16dabca | ||
|
|
59fc97fb28 | ||
|
|
7d44b60ae4 | ||
|
|
776ca6a3e4 | ||
|
|
ee26e94170 |
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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 ¶ms = 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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -128,6 +128,9 @@ Polylines Fill::fill_surface(const Surface *surface, const FillParams ¶ms)
|
||||
{
|
||||
// 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 ¶ms)
|
||||
_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 ¶ms)
|
||||
|
||||
@@ -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 ¶ms);
|
||||
virtual ThickPolylines fill_surface_arachne(const Surface* surface, const FillParams& params);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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); };
|
||||
|
||||
@@ -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 ¶ms, 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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -31,6 +31,7 @@ public:
|
||||
Polylines& polylines_out) override;
|
||||
|
||||
bool is_self_crossing() override { return false; }
|
||||
bool aligned_to_origin() const override { return true; }
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -2229,7 +2229,7 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
for (const PrintRegion ®ion : 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ®ion)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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]")
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user