Compare commits

..
Author SHA1 Message Date
ExPikaPaka 24862cc3bf Include <mutex>, <ios> and boost/filesystem/operations.hpp where they are used 2026-10-09 09:12:35 +02:00
ExPikaPaka 48d7f23f9f Merge remote-tracking branch 'origin/main' into fix/gcode_preview_misaligned_after_post_script
# Conflicts:
#	src/slic3r/GUI/GCodeViewer.cpp
#	tests/fff_print/CMakeLists.txt
#	tests/fff_print/test_gcodeprocessor.cpp
2026-10-09 08:58:53 +02:00
ExPikaPaka 6d616c288a Texture displacement: speed up the connected-net layout 3x (#16149)
Make the connected-net layout affordable on a dense patch

Laying a patch out as a connected net cost ~175 ms on a 42k-triangle patch,
against ~21 ms for the unwrap it works from, and the gizmo asks for it on every
preview, overlay and bake. Measured on a real project the grid behind it ran
~19 million triangle-pair tests per net, nearly all of them misses: a cell
holds every triangle whose box touches it, and a candidate really meets a
couple of them.

Keep a bounding box with each stored triangle and answer those misses with four
comparisons instead of a full intersection. The net drops to ~53 ms with
identical output - the seam metrics on the test project did not move by one.

Two further attempts were measured and dropped, and are recorded in the comment
so they are not tried again: a free-space pre-check per chart came out slower,
because a folded chart lands against the net by construction and the cells
under it are occupied anyway, and splitting the boxes into their own array for
locality lost more to growing two vectors per bucket than it gained.

Also pick a pair's fold line from the longest boundary they share rather than
whichever edge came first, and grow the net strongest-adjacency-first rather
than breadth-first by area. Only the fold a chart is reached by comes out
matching, so a chart claimed across a short boundary leaves the long one it
shared with its true neighbour torn.

texture_unwrap_dump reports an unwrap from a saved project - charts, their
topology, folded triangles, where the texture is discontinuous and how long
those seams are. All of the above was found with it, and it is what keeps a
claim about this code honest; reading the 3D view and guessing had produced
three wrong diagnoses in a row.
2026-10-09 14:23:23 +08:00
ExPikaPakaandExPikaPaka bff19b07d2 Texture displacement: fix the Checker and Distortion views, add UV editor tooltips (#16147)
* Explain the texture displacement views that cannot be used, and show them

Checker and Distortion are views of a layer's unwrap, so they mean nothing on
any other mapping - but they were offered on all of them and simply drew
nothing when picked. Fade them out on anything but Unwrap (LSCM), with the
reason in the tooltip, and have a click bring the UV editor up on the view it
selected, since that pane is where the unwrap is actually worked on.

Neither view was visible even when it did apply. The overlay is built from the
base patch and drawn with a polygon offset, which biases depth values rather
than moving geometry, so it cannot win against the displaced preview standing
in front of it. Leave that preview out while a UV-check view is on and draw the
undisplaced surface instead, which is what the offset assumes and what the
mapping being inspected belongs to.

In the UV editor, a tool that cannot be used right now is faded rather than
disabled. A disabled window gets no mouse events on GTK or MSW, so every one of
those tools - Cut, Join, Unjoin, Clear seams, Clear UV edits, the select modes,
Snap, Frame - silently had no tooltip in the state where the user most needs to
know what is missing. Each now says what to do instead. The tile size, the
island statistics, the status line and the three select modes gained tooltips
of their own; the select modes now name the gestures they enable, which were
documented nowhere.

* UV editor: keep the mouse capture balanced

The canvas captured the mouse on every button press without checking whether
it already held one, released it in a single place, and handled no capture
loss at all. Two sequences leaked a capture: pressing a second button during a
drag nested a second one that the single release on button-up could not undo,
and a modal R/S skipped that release entirely while waiting for a confirming
click that may never come.

A leaked capture is not a local problem on macOS, where wxEVT_MOUSE_CAPTURE_LOST
is never sent and nothing recovers it. While any wx window holds a capture,
wxOSX routes every mouse event to that window and never calls through to
NSWindow, so the application stops seeing motion and enter/leave, and native
tooltips stop appearing anywhere in it.

Capture through grab_mouse()/drop_mouse() so there is at most one, give it back
on any button-up including a modal gesture (which tracks the pointer and needs
no capture), and cancel on wxEVT_MOUSE_CAPTURE_LOST: commit nothing, put back
what a modal rotate or scale already applied, and do not release a capture that
is already gone.

---------

Co-authored-by: ExPikaPaka <mrfsfyt@gmail.com>
2026-10-09 12:14:47 +08:00
yw4z 25ec403156 Unify wiki & video guide buttons (#16159)
* init

* fix build

* Update PrintOptionsDialog.cpp

* Update TroubleshootDialog.cpp
2026-10-09 12:13:00 +08:00
ExPikaPakaandRodrigo Faselli 018c4f49ee Fix a crash on loading a 3MF with empty project settings (#16016)
* Fix a crash on loading a 3MF with empty project settings

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

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

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

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

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

---------

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

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

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

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

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

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

* Update test_meshboolean.cpp

* missing headers

* Update test_meshboolean.cpp

* Refactor mcut difference test for source splits

* Apply suggestion from @raistlin7447
2026-10-08 16:00:28 -03:00
ExPikaPaka 6d6d8e8b46 Add tests for rebuilding the G-code line offsets 2026-07-29 08:20:28 +02:00
ExPikaPaka f3385c1e03 Clamp the G-code window reads to the mapped file size 2026-07-29 08:20:23 +02:00
ExPikaPaka 9553326e7d Rebuild the G-code line offsets after post-processing scripts run in place 2026-07-29 08:20:11 +02:00
72 changed files with 1875 additions and 496 deletions
@@ -61,10 +61,8 @@ 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. 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)
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)
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)
@@ -601,25 +599,6 @@ 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.**
+2 -2
View File
@@ -1084,10 +1084,10 @@ endif ()
find_path(SPNAV_INCLUDE_DIR spnav.h)
if (SPNAV_INCLUDE_DIR)
find_library(SPNAV_LIB NAMES libspnav.a) # Force linking libspnav statically
find_library(SPNAV_LIB NAMES libspnav.a spnav)
if (SPNAV_LIB)
add_definitions(-DHAVE_SPNAV)
message(STATUS "SPNAV library found")
message(STATUS "SPNAV library found: ${SPNAV_LIB}")
else()
message(STATUS "SPNAV library NOT found, Spacenavd not supported")
endif()
+2 -2
View File
@@ -104,8 +104,8 @@ fi
CMAKE_VERSION=$(cmake --version | head -1 | sed 's/[^0-9]*\([0-9]*\).*/\1/')
if [ "$CMAKE_VERSION" -ge 4 ] 2>/dev/null; then
export CMAKE_POLICY_VERSION_MINIMUM=3.5
export CMAKE_POLICY_COMPAT="-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
export CMAKE_POLICY_VERSION_MINIMUM=3.10
export CMAKE_POLICY_COMPAT="-DCMAKE_POLICY_VERSION_MINIMUM=3.10"
echo "Detected CMake 4.x, adding compatibility flag (env + cmake arg)"
else
export CMAKE_POLICY_COMPAT=""
+4 -4
View File
@@ -1,5 +1,5 @@
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "4.0")
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "" FORCE)
set(CMAKE_POLICY_VERSION_MINIMUM 3.10 CACHE STRING "" FORCE)
endif()
#
@@ -24,7 +24,7 @@ endif()
# therefore, unfortunately, the installation cannot be copied/moved elsewhere without re-installing wxWidgets.
#
cmake_minimum_required(VERSION 3.2)
cmake_minimum_required(VERSION 3.10)
if (APPLE)
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts)
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
@@ -224,7 +224,7 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
${_source_dir_arg}
${_gen}
CMAKE_ARGS
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
-DCMAKE_POLICY_VERSION_MINIMUM=3.10
-DCMAKE_INSTALL_PREFIX:STRING=${DESTDIR}
-DCMAKE_MODULE_PATH:STRING=${PROJECT_SOURCE_DIR}/../cmake/modules
-DCMAKE_PREFIX_PATH:STRING=${DESTDIR}
@@ -279,7 +279,7 @@ else()
${_source_dir_arg}
${_gen}
CMAKE_ARGS
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
-DCMAKE_POLICY_VERSION_MINIMUM=3.10
-DCMAKE_INSTALL_PREFIX:STRING=${DESTDIR}
-DCMAKE_PREFIX_PATH:STRING=${DESTDIR}
-DCMAKE_IGNORE_PREFIX_PATH:STRING=${CMAKE_IGNORE_PREFIX_PATH}
@@ -0,0 +1,28 @@
diff --git a/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx b/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
index 6f63781..9b1c08e 100644
--- a/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
+++ b/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
@@ -210,10 +210,10 @@ private:
// Define two pcurves of the seam-edge.
occ::handle<Geom2d_Curve> aPC1, aPC2;
- double af, al;
+ double af, al, af1, al1;
aE.Orientation(TopAbs_FORWARD);
- aPC1 = BRep_Tool::CurveOnSurface(aE, aF, af, al);
+ aPC1 = BRep_Tool::CurveOnSurface(aE, aF, af1, al1);
aE.Orientation(TopAbs_REVERSED);
aPC2 = BRep_Tool::CurveOnSurface(aE, aF, af, al);
@@ -224,7 +224,9 @@ private:
}
// Select the correct pcurve of the seam-edge.
- const gp_Pnt2d& aFPntOfPC1 = aPC1->Value(aPC1->FirstParameter());
+ // Use the edge's first parameter. A Geom2d_Line's FirstParameter() is -Precision::Infinite(),
+ // where a direction of (2e-16, -1) from rounding error gives an X far outside the U range.
+ const gp_Pnt2d aFPntOfPC1 = aPC1->Value(af1);
if (std::abs(aLPntOfIPC1.X() - aFPntOfPC1.X()) > Precision::Confusion())
{
+7
View File
@@ -25,9 +25,16 @@ endif()
# shipped bytes. Windows ships only the DLLs libslic3r links, so the tab adds the TKFillet,
# TKOffset and TKBool DLLs. See docs/HLSD/design-tab.md.
if (IN_GIT_REPO)
set(OCCT_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OCCT-prefix/src/dep_OCCT)
endif ()
orcaslicer_add_cmake_project(OCCT
URL https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/V8_0_1.zip
URL_HASH SHA256=7c033d917ee8f040c0512d289dcc5f02c148889d5bac17c3e25639accb44f0da
# Makes BRepMesh triangulate cone faces whose seam pcurve is slightly tilted
# (Open-Cascade-SAS/OCCT#572); remove the patch once an OCCT release includes the fix.
PATCH_COMMAND git apply ${OCCT_DIRECTORY_FLAG} --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-BRepMesh-seam-pcurve-at-edge-parameter.patch
#DEPENDS dep_Boost
DEPENDS ${FREETYPE_PKG}
CMAKE_ARGS
+7
View File
@@ -79,6 +79,13 @@ the same `infill_body()`. The octree of the whole object is built only for an
object of a single body, or when some body received no triangles, which then
uses it.
The line spacing of an octree comes from the density, line width and multiline
count of a region, so a modifier or a part with its own density needs octrees of
its own. `adaptive_fill_line_spacing()` gives the spacing of each region, and
`FillAdaptive::RegionOctrees` holds one set of octrees per distinct spacing,
shared by the regions that have it. A set is built only for the bodies its
regions fill. The fill takes the set of its region, then the octree of its body.
## Patterns left out
Lightning grows its trees over the whole object, so moving a reference point
+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25"><circle cx="9.5" cy="12.5" r="9.5" style="fill:#009688"/><path d="M6.5,10.5a3,3,0,0,1,6,0c0,3-3,2-3,4v1" style="fill:none;stroke:#e6e6e6;stroke-linecap:round;stroke-linejoin:round"/><rect x="9" y="17" width="1" height="1" style="fill:#e6e6e6"/></svg>
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><rect x="7" y="12" width="1" height="1" style="fill:#fefefe;"/><path d="M4.5,5.5c0-1.71,1.43-3.09,3.16-3,1.62.08,2.84,1.54,2.84,3.17h0c0,.61-.25,1.2-.69,1.62l-2.31,2.24v.97" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/></svg>

Before

Width:  |  Height:  |  Size: 333 B

After

Width:  |  Height:  |  Size: 366 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25"><circle cx="9.5" cy="12.5" r="9.5" style="fill:#33aba1"/><path d="M6.5,10.5a3,3,0,0,1,6,0c0,3-3,2-3,4v1" style="fill:none;stroke:#e6e6e6;stroke-linecap:round;stroke-linejoin:round"/><rect x="9" y="17" width="1" height="1" style="fill:#e6e6e6"/></svg>

Before

Width:  |  Height:  |  Size: 333 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M12.31,8l-8.14,4.16c-.31.16-.67-.07-.67-.41V3.27c0-.35.37-.57.68-.41l8.14,4.32c.33.17.33.65,0,.82Z" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/></svg>

After

Width:  |  Height:  |  Size: 301 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M7.5,3.5c2-2,5.1-1,6-1" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><path d="M7.5,12.5c2-2,5.1-1,6-1" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><path d="M7.5,3.5C5.5,1.5,2.4,2.5,1.5,2.5" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><path d="M7.5,12.5c-2-2-5.1-1-6-1" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><line x1="13.5" y1="11.5" x2="13.5" y2="2.5" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><line x1="7.5" y1="12.5" x2="7.5" y2="3.5" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><line x1="1.5" y1="11.5" x2="1.5" y2="2.5" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/></svg>

After

Width:  |  Height:  |  Size: 954 B

File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -46,7 +46,7 @@ RUN set -eux; \
tar -xzf /tmp/assimp.tar.gz -C /tmp/assimp-src --strip-components=1; \
DESTDIR=/OrcaSlicer/deps/build/destdir/usr/local; \
cmake -S /tmp/assimp-src -B /tmp/assimp-build -G Ninja \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_POLICY_VERSION_MINIMUM=3.10 \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$DESTDIR" \
-DCMAKE_PREFIX_PATH="$DESTDIR" \
+6 -37
View File
@@ -165,7 +165,6 @@ 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).
@@ -1156,12 +1155,6 @@ 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");
@@ -1225,34 +1218,6 @@ 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.
@@ -1855,6 +1820,8 @@ int CLI::run(int argc, char **argv)
old_printable_width = static_cast<int>(old_printable_bbox.size().x());
old_printable_depth = static_cast<int>(old_printable_bbox.size().y());
}
// A 3mf can carry an empty project_settings.config - the models in
// resources/handy_models do - and opt_float() dereferences without checking.
if (config.option<ConfigOptionFloat>("printable_height"))
old_printable_height = (int)(config.opt_float("printable_height"));
@@ -2540,7 +2507,8 @@ int CLI::run(int argc, char **argv)
orig_printable_width = static_cast<int>(orig_printable_bbox.size().x());
orig_printable_depth = static_cast<int>(orig_printable_bbox.size().y());
}
orig_printable_height = (int)(config.opt_float("printable_height"));
if (config.option<ConfigOptionFloat>("printable_height"))
orig_printable_height = (int)(config.opt_float("printable_height"));
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(":%1%, check printable size: old_printable_width=%2%, orig_printable_width=%3%, old_printable_depth=%4%, orig_printable_depth=%5%, old_printable_height=%6%, orig_printable_height=%7%")
%__LINE__ %old_printable_width %orig_printable_width %old_printable_depth %orig_printable_depth %old_printable_height %orig_printable_height;
if ((orig_printable_width > 0) && (orig_printable_depth > 0) && (orig_printable_height > 0))
@@ -4652,7 +4620,8 @@ int CLI::run(int argc, char **argv)
BoundingBoxf temp_printable_bbox(temp_printable_area);
printer_plate.printable_width = static_cast<int>(temp_printable_bbox.size().x());
printer_plate.printable_depth = static_cast<int>(temp_printable_bbox.size().y());
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
if (config.option<ConfigOptionFloat>("printable_height"))
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
}
if (temp_exclude_area.size() >= 4) {
printer_plate.exclude_width = (int)(temp_exclude_area[2].x() - temp_exclude_area[0].x());
+6
View File
@@ -28,6 +28,12 @@ if (ORCA_TOOLS)
target_link_libraries(generate_system_cache libslic3r boost_headeronly)
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})
# texture_unwrap_dump: reports the LSCM unwrap of a saved project's texture displacement layers,
# chart by chart, so a defect can be reproduced from the project file instead of from a screenshot.
add_executable(texture_unwrap_dump texture_unwrap_dump.cpp)
target_link_libraries(texture_unwrap_dump libslic3r boost_headeronly nanosvg)
target_compile_definitions(texture_unwrap_dump PRIVATE ${_DEV_DEFS})
# profile_include_dump: prints what included templates contribute to a vendor's presets,
# to diff against the same tool built in BambuStudio. Built only on request.
add_executable(profile_include_dump EXCLUDE_FROM_ALL profile_include_dump.cpp)
@@ -303,10 +303,11 @@ else
fi
has_host_runtime_library() {
local lib_name path
local lib_name="\$1"
local path
if command -v ldconfig >/dev/null 2>&1; then
if ldconfig -p 2>/dev/null | grep -Fq " \$lib_name"; then
if ldconfig -p 2>/dev/null | grep -Fq "\$lib_name ("; then
return 0
fi
fi
+292
View File
@@ -0,0 +1,292 @@
// Diagnostic for the LSCM unwrap of a texture displacement layer.
//
// It exists because the defect it hunts only shows up on a real painted patch: the paint mask is built
// by TriangleSelector splitting base triangles, so the patch topology cannot be written down by hand,
// and reasoning about it from a screenshot of the 3D view had already produced three wrong diagnoses.
// This loads a saved project, rebuilds exactly the patch the bake would act on, runs the same unwrap,
// and reports what came out - per chart, so a bad one can be pointed at rather than guessed at.
//
// texture_unwrap_dump <project.3mf>
// nanosvg is header-only and libslic3r's 3mf import references it without carrying the implementation,
// so every executable that links libslic3r has to supply it. Must precede any include that pulls the
// header in, or its include guard suppresses the implementation. Same pattern as the other dev tools.
#define NANOSVG_IMPLEMENTATION
#include "nanosvg/nanosvg.h"
#define NANOSVGRAST_IMPLEMENTATION
#include "nanosvg/nanosvgrast.h"
#include <chrono>
#include <cstdio>
#include <string>
#include <functional>
#include <unordered_map>
#include <vector>
#include "libslic3r/Model.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/filesystem.hpp>
using namespace Slic3r;
namespace {
uint64_t edge_key(int a, int b)
{
if (a > b)
std::swap(a, b);
return (uint64_t(uint32_t(a)) << 32) | uint32_t(b);
}
// Boundary loops and the Euler characteristic of a face set, which together say whether a chart is the
// topological disk LSCM needs (one loop, V - E + F == 1).
void chart_topology(const indexed_triangle_set &mesh, const std::vector<int> &faces, int &loops, int &euler)
{
std::unordered_map<uint64_t, int> edge_use;
std::unordered_map<int, int> local;
for (const int f : faces) {
const stl_triangle_vertex_indices &t = mesh.indices[size_t(f)];
for (int i = 0; i < 3; ++i) {
++edge_use[edge_key(t[i], t[(i + 1) % 3])];
local.emplace(t[i], int(local.size()));
}
}
euler = int(local.size()) - int(edge_use.size()) + int(faces.size());
std::unordered_map<int, int> parent;
const std::function<int(int)> find = [&](int x) {
while (parent[x] != x)
x = parent[x] = parent[parent[x]];
return x;
};
for (const auto &[key, uses] : edge_use)
if (uses == 1)
for (const int v : { int(key >> 32), int(uint32_t(key)) })
parent.emplace(v, v);
for (const auto &[key, uses] : edge_use)
if (uses == 1) {
const int a = find(int(key >> 32)), b = find(int(uint32_t(key)));
if (a != b)
parent[b] = a;
}
std::unordered_map<int, int> roots;
for (const auto &[v, p] : parent)
roots[find(v)] = 1;
loops = int(roots.size());
}
float signed_area_2d(const Vec2f &a, const Vec2f &b, const Vec2f &c)
{
return 0.5f * ((b.x() - a.x()) * (c.y() - a.y()) - (c.x() - a.x()) * (b.y() - a.y()));
}
} // namespace
int main(int argc, char **argv)
{
if (argc < 2) {
std::printf("usage: texture_unwrap_dump <project.3mf>\n");
return 2;
}
Model model;
DynamicPrintConfig config;
ConfigSubstitutionContext ctx(ForwardCompatibilitySubstitutionRule::Enable);
PlateDataPtrs plate_data;
std::vector<Preset *> project_presets;
bool is_bbl_3mf = false, is_orca_3mf = false;
Semver file_version;
// The importer writes a backup copy under the data dir and silently loses objects without one.
const boost::filesystem::path tmp = boost::filesystem::temp_directory_path() / "texture_unwrap_dump";
boost::filesystem::create_directories(tmp);
set_data_dir(tmp.string());
// LoadModel so the meshes come through; AddDefaultInstances because an object with no instance is
// dropped by the plate mapping, which is what "skip this object" in the log means.
if (!load_bbs_3mf(argv[1], &config, &ctx, &model, &plate_data, &project_presets, &is_bbl_3mf, &is_orca_3mf,
&file_version, nullptr,
LoadStrategy::LoadModel | LoadStrategy::LoadConfig | LoadStrategy::AddDefaultInstances |
LoadStrategy::Silence)) {
std::printf("failed to load %s\n", argv[1]);
return 1;
}
std::printf("loaded: %zu object(s)\n", model.objects.size());
for (const ModelObject *object : model.objects)
for (const ModelVolume *volume : object->volumes) {
if (volume->texture_displacement_layers.empty()) {
std::printf("volume \"%s\": no texture displacement layers; paint masks per slot:",
volume->name.c_str());
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
std::printf(" %zu", volume->texture_displacement_facet(i).get_data().triangles_to_split.size());
std::printf("\n");
continue;
}
std::printf("volume \"%s\": %zu base triangles, %zu layer(s)\n", volume->name.c_str(),
volume->mesh().its.indices.size(), volume->texture_displacement_layers.size());
for (const TextureDisplacementLayer &layer : volume->texture_displacement_layers) {
std::printf("\n layer %d \"%s\" mapping=%d seam_angle=%.1f connect=%d islands_stored=%zu\n",
layer.slot, layer.name.c_str(), int(layer.projection_method),
layer.lscm_seam_angle_deg, int(layer.auto_connect_islands), layer.islands.size());
if (layer.projection_method != TextureProjectionMethod::LSCM)
continue;
const indexed_triangle_set patch =
extract_painted_patch(volume->mesh().its, volume->texture_displacement_facet(layer.slot).get_data());
std::printf(" patch: %zu vertices, %zu triangles\n", patch.vertices.size(), patch.indices.size());
if (patch.indices.empty())
continue;
const auto t0 = std::chrono::steady_clock::now();
const PatchUnwrap unwrap = compute_patch_unwrap(patch, layer.lscm_seam_angle_deg, 0.f,
layer.lscm_seam_edges);
const auto t1 = std::chrono::steady_clock::now();
std::printf(" TIMING compute_patch_unwrap: %.0f ms\n",
std::chrono::duration<double, std::milli>(t1 - t0).count());
std::printf(" unwrap: %d charts, %zu unwrapped triangles\n", unwrap.chart_count,
unwrap.indices.size());
// Group the patch's faces by chart so each can be examined on its own.
std::vector<std::vector<int>> chart_faces(size_t(std::max(unwrap.chart_count, 0)));
for (size_t i = 0; i < unwrap.indices.size(); ++i) {
const int chart = unwrap.vertex_chart[size_t(unwrap.indices[i][0])];
if (chart >= 0 && size_t(chart) < chart_faces.size())
chart_faces[size_t(chart)].push_back(unwrap.source_face[i]);
}
int bad_charts = 0;
for (size_t c = 0; c < chart_faces.size(); ++c) {
int loops = 0, euler = 0;
chart_topology(patch, chart_faces[c], loops, euler);
// Flipped triangles: the unwrap folded over itself, which is what a planar fallback
// does to a chart that is not flat. Measured on the unwrap's own triangles.
int pos = 0, neg = 0;
for (size_t i = 0; i < unwrap.indices.size(); ++i) {
const stl_triangle_vertex_indices &t = unwrap.indices[i];
if (unwrap.vertex_chart[size_t(t[0])] != int(c))
continue;
const float a = signed_area_2d(unwrap.uvs[size_t(t[0])], unwrap.uvs[size_t(t[1])],
unwrap.uvs[size_t(t[2])]);
if (a > 0.f) ++pos; else if (a < 0.f) ++neg;
}
const int flipped = std::min(pos, neg);
const bool disk = loops == 1 && euler == 1;
if (!disk || flipped > 0) {
++bad_charts;
std::printf(" chart %2zu: %4zu faces loops=%d euler=%d%s flipped=%d/%d%s\n", c,
chart_faces[c].size(), loops, euler, disk ? "" : " NOT A DISK", flipped,
pos + neg, flipped ? " FOLDED" : "");
}
}
std::printf(" charts with a defect: %d / %d\n", bad_charts, unwrap.chart_count);
// What the eye actually sees. Every patch edge shared by two charts should carry the same
// UV on both sides once the islands are laid out as a connected net; where it does not,
// the texture jumps across that seam. Measured through compute_lscm_uvs(), i.e. the exact
// coordinates the bake and the checker overlay sample.
{
const auto n0 = std::chrono::steady_clock::now();
const std::vector<TextureIsland> net = compute_connected_net(unwrap);
const auto n1 = std::chrono::steady_clock::now();
std::printf(" TIMING compute_connected_net: %.0f ms (%zu islands)\n",
std::chrono::duration<double, std::milli>(n1 - n0).count(), net.size());
}
const auto t2 = std::chrono::steady_clock::now();
const std::vector<Vec2f> uv = compute_lscm_uvs(patch, layer);
const auto t3 = std::chrono::steady_clock::now();
std::printf(" TIMING compute_lscm_uvs: %.0f ms (called on every preview, overlay and bake)\n",
std::chrono::duration<double, std::milli>(t3 - t2).count());
if (uv.size() != patch.vertices.size()) {
std::printf(" compute_lscm_uvs returned %zu uvs for %zu vertices\n", uv.size(),
patch.vertices.size());
continue;
}
// Per-corner UVs carry each chart's own placement, so an edge shared by two charts shows
// the jump directly: the same mesh vertex lands at two different UVs. That is exactly what
// the eye reads as the texture breaking.
const auto t4 = std::chrono::steady_clock::now();
const std::vector<Vec2f> corner = compute_lscm_corner_uvs(patch, layer);
const auto t5 = std::chrono::steady_clock::now();
std::printf(" TIMING compute_lscm_corner_uvs: %.0f ms\n",
std::chrono::duration<double, std::milli>(t5 - t4).count());
// Keyed by edge, holding the UV each incident face gives to the edge's *lower-numbered*
// endpoint. Comparing that same vertex on both sides is the point: indexing by corner
// position instead compares opposite ends of the edge, because the two faces wind it in
// opposite directions.
std::unordered_map<uint64_t, std::vector<Vec2f>> edge_seen;
if (corner.size() == patch.indices.size() * 3)
for (size_t f = 0; f < patch.indices.size(); ++f) {
const stl_triangle_vertex_indices &t = patch.indices[f];
for (int k = 0; k < 3; ++k) {
const int a = t[k], b = t[(k + 1) % 3];
const int probe = std::min(a, b);
const int local = (a == probe) ? k : (k + 1) % 3;
edge_seen[edge_key(a, b)].push_back(corner[f * 3 + size_t(local)]);
}
}
// Which chart each patch face belongs to, so a broken edge can be attributed to a pair.
std::vector<int> chart_of_face(patch.indices.size(), -1);
for (size_t i = 0; i < unwrap.indices.size(); ++i)
chart_of_face[size_t(unwrap.source_face[i])] = unwrap.vertex_chart[size_t(unwrap.indices[i][0])];
std::unordered_map<uint64_t, std::vector<int>> edge_faces;
for (size_t f = 0; f < patch.indices.size(); ++f) {
const stl_triangle_vertex_indices &t = patch.indices[f];
for (int k = 0; k < 3; ++k)
edge_faces[edge_key(t[k], t[(k + 1) % 3])].push_back(int(f));
}
int adjacent = 0, broken = 0, broken_same_chart = 0;
float worst = 0.f;
std::map<std::pair<int, int>, std::pair<int, float>> by_pair;
for (const auto &[key, seen] : edge_seen) {
if (seen.size() != 2)
continue;
++adjacent;
const float d = (seen[0] - seen[1]).norm();
if (d <= 1e-4f)
continue;
++broken;
worst = std::max(worst, d);
const auto &faces_here = edge_faces[key];
int c1 = -1, c2 = -1;
if (faces_here.size() == 2) {
c1 = chart_of_face[size_t(faces_here[0])];
c2 = chart_of_face[size_t(faces_here[1])];
}
if (c1 == c2)
++broken_same_chart;
auto &slot = by_pair[{ std::min(c1, c2), std::max(c1, c2) }];
++slot.first;
slot.second = std::max(slot.second, d);
}
std::printf(" broken edges inside a single chart: %d\n", broken_same_chart);
std::printf(" broken by chart pair:");
for (const auto &[pk, v] : by_pair)
std::printf(" (%d,%d)x%d/%.1f", pk.first, pk.second, v.first, v.second);
std::printf("\n");
// Total length of the seams left broken, in mm: how much visibly torn edge the layout has,
// which is what the eye adds up. A count alone hides whether the breaks are hairlines or
// whole sides of an island.
float seam_mm = 0.f;
for (const auto &[key, seen] : edge_seen) {
if (seen.size() != 2 || (seen[0] - seen[1]).norm() <= 1e-4f)
continue;
seam_mm += (patch.vertices[size_t(key >> 32)] - patch.vertices[size_t(uint32_t(key))]).norm();
}
std::printf(" interior edges: %d, discontinuous: %d, total torn seam: %.2f mm (worst jump %.3f)\n",
adjacent, broken, seam_mm, worst);
std::printf(" stored islands %zu vs charts %d -> %s\n", layer.islands.size(),
unwrap.chart_count,
layer.islands.size() == size_t(unwrap.chart_count) ? "stored placements used"
: "net rebuilt");
}
}
return 0;
}
+4 -4
View File
@@ -1320,7 +1320,7 @@ void export_group_fills_to_svg(const char *path, const std::vector<SurfaceFill>
#endif
// friend to Layer
void Layer::make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator)
void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator)
{
for (LayerRegion *layerm : m_regions)
layerm->fills.clear();
@@ -1353,7 +1353,7 @@ void Layer::make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const
f->z = this->print_z;
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = surface_fill.params.pattern == ipSupportCubic ? support_fill_octrees : adaptive_fill_octrees;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
if (surface_fill.params.pattern == ipConcentricInternal) {
@@ -1516,7 +1516,7 @@ void Layer::make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const
* - For lightning/adaptive patterns, the respective generators are wired so their
* polylines match the final infill layout.
*/
Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator) const
Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator) const
{
LockRegionParam skin_inner_param;
std::vector<SurfaceFill> surface_fills = group_fills(*this, skin_inner_param);
@@ -1574,7 +1574,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
f->z = this->print_z;
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = surface_fill.params.pattern == ipSupportCubic ? support_fill_octrees : adaptive_fill_octrees;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
+14 -77
View File
@@ -301,88 +301,25 @@ void OctreeDeleter::operator()(Octree *p) {
delete p;
}
std::pair<double, double> adaptive_fill_line_spacing(const PrintObject &print_object)
std::vector<double> adaptive_fill_line_spacing(const PrintObject &print_object)
{
// Output, spacing for icAdaptiveCubic and icSupportCubic
double adaptive_line_spacing = 0.;
double support_line_spacing = 0.;
enum class Tristate {
Yes,
No,
Maybe
};
struct RegionFillData {
Tristate has_adaptive_infill;
Tristate has_support_infill;
double density;
double extrusion_width;
};
std::vector<RegionFillData> region_fill_data;
region_fill_data.reserve(print_object.num_printing_regions());
bool build_octree = false;
std::vector<double> line_spacing(print_object.num_printing_regions(), 0.);
const std::vector<double> &nozzle_diameters = print_object.print()->config().nozzle_diameter.values;
double max_nozzle_diameter = *std::max_element(nozzle_diameters.begin(), nozzle_diameters.end());
double default_infill_extrusion_width = Flow::auto_extrusion_width(FlowRole::frInfill, float(max_nozzle_diameter));
for (size_t region_id = 0; region_id < print_object.num_printing_regions(); ++ region_id) {
const PrintRegionConfig &config = print_object.printing_region(region_id).config();
bool nonempty = config.sparse_infill_density > 0;
bool has_adaptive_infill = nonempty && config.sparse_infill_pattern == ipAdaptiveCubic;
bool has_support_infill = nonempty && config.sparse_infill_pattern == ipSupportCubic;
double sparse_infill_line_width = config.sparse_infill_line_width.get_abs_value(max_nozzle_diameter);
region_fill_data.push_back(RegionFillData({
has_adaptive_infill ? Tristate::Maybe : Tristate::No,
has_support_infill ? Tristate::Maybe : Tristate::No,
config.sparse_infill_density,
sparse_infill_line_width != 0. ? sparse_infill_line_width : default_infill_extrusion_width
}));
build_octree |= has_adaptive_infill || has_support_infill;
for (size_t region_id = 0; region_id < line_spacing.size(); ++ region_id) {
const PrintRegionConfig &config = print_object.printing_region(region_id).config();
if (config.sparse_infill_density <= 0 || ! is_octree_infill_pattern(config.sparse_infill_pattern) ||
std::none_of(print_object.layers().begin(), print_object.layers().end(), [region_id](const Layer *layer) {
return region_id < layer->regions().size() && ! layer->regions()[region_id]->fill_surfaces.empty();
}))
continue;
double extrusion_width = config.sparse_infill_line_width.get_abs_value(max_nozzle_diameter);
if (extrusion_width == 0.)
extrusion_width = default_infill_extrusion_width;
line_spacing[region_id] = extrusion_width / ((config.sparse_infill_density / 100.0f) * 0.333333333f) * config.fill_multiline.value;
}
if (build_octree) {
// Compute the average of above parameters over all layers
for (const Layer *layer : print_object.layers())
for (size_t region_id = 0; region_id < layer->regions().size(); ++ region_id) {
RegionFillData &rd = region_fill_data[region_id];
if (rd.has_adaptive_infill == Tristate::Maybe && ! layer->regions()[region_id]->fill_surfaces.empty())
rd.has_adaptive_infill = Tristate::Yes;
if (rd.has_support_infill == Tristate::Maybe && ! layer->regions()[region_id]->fill_surfaces.empty())
rd.has_support_infill = Tristate::Yes;
}
double adaptive_fill_density = 0.;
double adaptive_infill_extrusion_width = 0.;
int adaptive_cnt = 0;
double support_fill_density = 0.;
double support_infill_extrusion_width = 0.;
int support_cnt = 0;
for (const RegionFillData &rd : region_fill_data) {
if (rd.has_adaptive_infill == Tristate::Yes) {
adaptive_fill_density += rd.density;
adaptive_infill_extrusion_width += rd.extrusion_width;
++ adaptive_cnt;
} else if (rd.has_support_infill == Tristate::Yes) {
support_fill_density += rd.density;
support_infill_extrusion_width += rd.extrusion_width;
++ support_cnt;
}
}
auto to_line_spacing = [](int cnt, double density, double extrusion_width) {
if (cnt) {
density /= double(cnt);
extrusion_width /= double(cnt);
return extrusion_width / ((density / 100.0f) * 0.333333333f);
} else
return 0.;
};
const int n_multiline = print_object.printing_region(0).config().fill_multiline.value;
adaptive_line_spacing = to_line_spacing(adaptive_cnt, adaptive_fill_density, adaptive_infill_extrusion_width) * n_multiline;
support_line_spacing = to_line_spacing(support_cnt, support_fill_density, support_infill_extrusion_width) * n_multiline;
}
return std::make_pair(adaptive_line_spacing, support_line_spacing);
return line_spacing;
}
// Context used by generate_infill_lines() when recursively traversing an octree in a DDA fashion
+16 -5
View File
@@ -59,11 +59,22 @@ struct Octrees
}
};
// Calculate line spacing for
// 1) adaptive cubic infill
// 2) adaptive internal support cubic infill
// Returns zero for a particular infill type if no such infill is to be generated.
std::pair<double, double> adaptive_fill_line_spacing(const PrintObject &print_object);
// Orca: The octrees of each line spacing the regions of an object fill with.
struct RegionOctrees
{
std::vector<Octrees> sets;
// Index into sets for each region, -1 for a region without adaptive or support cubic infill.
std::vector<int> region_set;
const Octrees *region(size_t region_id) const
{
return region_id < region_set.size() && region_set[region_id] >= 0 ? &sets[region_set[region_id]] : nullptr;
}
};
// Line spacing of the adaptive or support cubic infill of each region of the object,
// zero for a region that generates no such infill.
std::vector<double> adaptive_fill_line_spacing(const PrintObject &print_object);
// Rotation of the octree to stand on one of its corners.
Eigen::Quaterniond transform_to_world();
+38
View File
@@ -33,6 +33,7 @@
#include <boost/filesystem/path.hpp>
#include <cstddef>
#include <mutex>
#include <cmath>
#include <exception>
#include <deque>
@@ -2649,6 +2650,43 @@ void GCodeProcessorResult::reset() {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: this=%2% reset finished")%__LINE__%this;
}
bool GCodeProcessorResult::rebuild_lines_ends()
{
// lock_guard rather than the lock()/unlock() pair reset() uses: this reads a file and grows a
// vector, so a throw between the two would leave the mutex held forever.
std::lock_guard<std::mutex> lock(result_mutex);
// A partially rebuilt map would have the G-code window slicing lines at wrong offsets all over
// again, so every failure below leaves lines_ends empty, which just hides the window.
lines_ends.clear();
FilePtr in{ boost::nowide::fopen(filename.c_str(), "rb") };
if (in.f == nullptr) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": cannot open " << filename << " to rebuild the G-code line offsets.";
return false;
}
// Record the offset one past each '\n', matching what ExportLines emits during the export pass.
std::vector<char> buffer(65536, 0);
size_t file_pos = 0;
for (;;) {
const size_t cnt_read = ::fread(buffer.data(), 1, buffer.size(), in.f);
if (::ferror(in.f)) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": error while reading " << filename
<< " to rebuild the G-code line offsets.";
lines_ends.clear();
return false;
}
for (size_t i = 0; i < cnt_read; ++i) {
if (buffer[i] == '\n')
lines_ends.emplace_back(file_pos + i + 1);
}
file_pos += cnt_read;
if (cnt_read < buffer.size())
break;
}
return true;
}
const std::vector<std::pair<GCodeProcessor::EProducer, std::string>> GCodeProcessor::Producers = {
//BBS: OrcaSlicer is also "bambu". Otherwise the time estimation didn't work.
//FIXME: Workaround and should be handled when do removing-bambu
+5
View File
@@ -334,6 +334,11 @@ class Print;
BedType bed_type = BedType::btCount;
void reset();
// Re-scan this->filename and rebuild lines_ends from it. Needed whenever the exported G-code is
// modified in place after export (post-processing scripts), which shifts the byte offsets the
// G-code viewer uses to slice lines out of the memory-mapped file. Returns false and leaves
// lines_ends empty if the file cannot be read.
bool rebuild_lines_ends();
//BBS: add mutex for protection of gcode result
mutable std::mutex result_mutex;
+4 -5
View File
@@ -33,7 +33,7 @@ class PrintObject;
class Print;
namespace FillAdaptive {
struct Octrees;
struct RegionOctrees;
};
namespace FillLightning {
@@ -207,10 +207,9 @@ public:
static bool is_perimeter_compatible(const Print& print, const PrintRegion& a, const PrintRegion& b);
void make_perimeters();
// Phony version of make_fills() without parameters for Perl integration only.
void make_fills() { this->make_fills(nullptr, nullptr); }
void make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator = nullptr);
Polylines generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::Octrees *adaptive_fill_octrees,
const FillAdaptive::Octrees *support_fill_octrees,
void make_fills() { this->make_fills(nullptr); }
void make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator = nullptr);
Polylines generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::RegionOctrees *fill_octrees,
FillLightning::Generator* lightning_generator) const;
void make_ironing();
// Returns the filament id (1-based) the region is ironed with, or -1 when the
+22 -1
View File
@@ -1045,7 +1045,28 @@ void do_boolean(McutMesh& srcMesh, const McutMesh& cutMesh, const std::string& b
// But we can force it to work by spliting the src mesh into disconnected components,
// and do booleans seperately, then merge all the results.
indexed_triangle_set all_its;
if (boolean_opts == "UNION" || boolean_opts == "A_NOT_B") {
if (boolean_opts == "A_NOT_B") {
// Each cut can leave the source with several disconnected components, which mcut rejects
// in the next dispatch, so re-split after every cut part (e.g. each letter of a text).
std::vector<indexed_triangle_set> parts = std::move(src_parts);
for (size_t j = 0; j < cut_parts.size(); j++) {
auto cut_part = triangle_mesh_to_mcut(cut_parts[j]);
std::vector<indexed_triangle_set> next_parts;
for (indexed_triangle_set &part : parts) {
auto src_part = triangle_mesh_to_mcut(part);
if (do_boolean_single(*src_part, *cut_part, boolean_opts)) {
TriangleMesh tri_part = mcut_to_triangle_mesh(*src_part);
std::vector<indexed_triangle_set> pieces = its_split(tri_part.its);
std::move(pieces.begin(), pieces.end(), std::back_inserter(next_parts));
} else
next_parts.emplace_back(std::move(part));
}
parts = std::move(next_parts);
}
for (const indexed_triangle_set &part : parts)
its_merge(all_its, part);
}
else if (boolean_opts == "UNION") {
for (size_t i = 0; i < src_parts.size(); i++) {
auto src_part = triangle_mesh_to_mcut(src_parts[i]);
for (size_t j = 0; j < cut_parts.size(); j++) {
+2 -2
View File
@@ -583,7 +583,7 @@ private:
void discover_horizontal_shells();
void combine_infill();
void _generate_support_material();
std::pair<FillAdaptive::Octrees, FillAdaptive::Octrees> prepare_adaptive_infill_data(
FillAdaptive::RegionOctrees prepare_adaptive_infill_data(
const std::vector<std::pair<const Surface*, const Layer*>>& surfaces_w_layer) const;
FillLightning::GeneratorPtr prepare_lightning_infill_data();
@@ -616,7 +616,7 @@ private:
// so that next call to make_perimeters() performs a union() before computing loops
bool m_typed_slices = false;
std::pair<FillAdaptive::Octrees, FillAdaptive::Octrees> m_adaptive_fill_octrees;
FillAdaptive::RegionOctrees m_adaptive_fill_octrees;
std::vector<BoundingBox> m_separated_body_bboxes;
FillLightning::GeneratorPtr m_lightning_generator;
+53 -35
View File
@@ -848,7 +848,7 @@ void PrintObject::infill()
[this](const tbb::blocked_range<size_t>& range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
m_print->throw_if_canceled();
m_layers[layer_idx]->make_fills(&m_adaptive_fill_octrees.first, &m_adaptive_fill_octrees.second, this->m_lightning_generator.get());
m_layers[layer_idx]->make_fills(&m_adaptive_fill_octrees, this->m_lightning_generator.get());
}
}
);
@@ -1168,14 +1168,27 @@ static std::vector<indexed_triangle_set> split_mesh_by_body(const PrintObject &o
return bodies;
}
std::pair<FillAdaptive::Octrees, FillAdaptive::Octrees> PrintObject::prepare_adaptive_infill_data(
FillAdaptive::RegionOctrees PrintObject::prepare_adaptive_infill_data(
const std::vector<std::pair<const Surface *, const Layer *>> &surfaces_w_layer) const
{
using namespace FillAdaptive;
auto [adaptive_line_spacing, support_line_spacing] = adaptive_fill_line_spacing(*this);
if ((adaptive_line_spacing == 0. && support_line_spacing == 0.) || this->layers().empty())
// Orca: Each region fills with the octrees of its own line spacing, shared by the regions of equal spacing.
const std::vector<double> line_spacing = adaptive_fill_line_spacing(*this);
std::vector<std::pair<double, bool>> spacings; // Line spacing, support cubic.
RegionOctrees octrees;
octrees.region_set.assign(line_spacing.size(), -1);
for (size_t region_id = 0; region_id < line_spacing.size(); ++ region_id)
if (line_spacing[region_id] > 0.) {
const std::pair<double, bool> spacing(line_spacing[region_id], this->printing_region(region_id).config().sparse_infill_pattern == ipSupportCubic);
const auto it = std::find(spacings.begin(), spacings.end(), spacing);
octrees.region_set[region_id] = int(it - spacings.begin());
if (it == spacings.end())
spacings.push_back(spacing);
}
if (spacings.empty() || this->layers().empty())
return {};
octrees.sets.resize(spacings.size());
indexed_triangle_set mesh = this->model_object()->raw_indexed_triangle_set();
// Rotate mesh and build octree on it with axis-aligned (standart base) cubes.
@@ -1198,44 +1211,50 @@ std::pair<FillAdaptive::Octrees, FillAdaptive::Octrees> PrintObject::prepare_ada
}
});
// Orca: Each body gets the octree it has when sliced on its own, from its own triangles.
std::pair<Octrees, Octrees> octrees;
const size_t num_bodies = m_separated_body_bboxes.size();
bool need_object = num_bodies <= 1;
// Orca: Each body gets the octree it has when sliced on its own, from its own triangles, for each line spacing
// its regions fill with. Body num_bodies stands for the whole object, which serves an object of a single body
// and the surfaces of bodies that have no octree of their own.
const size_t num_bodies = m_separated_body_bboxes.size();
std::vector<std::pair<size_t, size_t>> to_build; // Set, body.
std::vector<indexed_triangle_set> body_meshes;
std::vector<std::vector<Vec3d>> body_overhangs(num_bodies);
if (num_bodies > 1) {
const std::vector<indexed_triangle_set> body_meshes = split_mesh_by_body(*this, mesh, num_bodies);
need_object = std::any_of(body_meshes.begin(), body_meshes.end(), [](const indexed_triangle_set &its) { return its.indices.empty(); });
std::vector<std::vector<Vec3d>> body_overhangs(num_bodies);
body_meshes = split_mesh_by_body(*this, mesh, num_bodies);
for (size_t i = 0; i < surfaces_w_layer.size(); ++ i)
if (const int body = separated_body_at(*surfaces_w_layer[i].second, surfaces_w_layer[i].first->expolygon.contour.points.front()); body >= 0)
append(body_overhangs[body], overhangs[i]);
if (adaptive_line_spacing)
octrees.first.bodies.resize(num_bodies);
if (support_line_spacing)
octrees.second.bodies.resize(num_bodies);
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_bodies), [&, adaptive_spacing = adaptive_line_spacing, support_spacing = support_line_spacing](
const tbb::blocked_range<size_t> &range) {
for (size_t body = range.begin(); body < range.end(); ++ body) {
m_print->throw_if_canceled();
if (body_meshes[body].indices.empty())
continue;
if (adaptive_spacing)
octrees.first.bodies[body] = build_octree(body_meshes[body], body_overhangs[body], adaptive_spacing, false);
if (support_spacing)
octrees.second.bodies[body] = build_octree(body_meshes[body], body_overhangs[body], support_spacing, true);
}
});
}
std::vector<std::vector<char>> fills(spacings.size(), std::vector<char>(num_bodies + 1, false));
for (const Layer *layer : m_layers)
for (size_t region_id = 0; region_id < layer->regions().size() && region_id < octrees.region_set.size(); ++ region_id)
if (const int set = octrees.region_set[region_id]; set >= 0)
for (const Surface &surface : layer->regions()[region_id]->fill_surfaces) {
const int body = separated_body_at(*layer, surface.expolygon.contour.points.front());
fills[set][body >= 0 && ! body_meshes[body].indices.empty() ? size_t(body) : num_bodies] = true;
}
for (size_t set = 0; set < spacings.size(); ++ set) {
octrees.sets[set].bodies.resize(num_bodies);
for (size_t body = 0; body <= num_bodies; ++ body)
if (fills[set][body])
to_build.emplace_back(set, body);
}
} else
for (size_t set = 0; set < spacings.size(); ++ set)
to_build.emplace_back(set, num_bodies);
// and gather them.
for (size_t i = 1; i < overhangs.size(); ++ i)
append(overhangs.front(), std::move(overhangs[i]));
// Orca: The object's octree only serves bodies that have none of their own.
if (need_object && adaptive_line_spacing)
octrees.first.object = build_octree(mesh, overhangs.front(), adaptive_line_spacing, false);
if (need_object && support_line_spacing)
octrees.second.object = build_octree(mesh, overhangs.front(), support_line_spacing, true);
tbb::parallel_for(tbb::blocked_range<size_t>(0, to_build.size()), [&](const tbb::blocked_range<size_t> &range) {
for (size_t i = range.begin(); i < range.end(); ++ i) {
m_print->throw_if_canceled();
const auto [set, body] = to_build[i];
const bool object = body == num_bodies;
(object ? octrees.sets[set].object : octrees.sets[set].bodies[body]) =
build_octree(object ? mesh : body_meshes[body], object ? overhangs.front() : body_overhangs[body], spacings[set].first,
spacings[set].second);
}
});
return octrees;
}
@@ -3077,8 +3096,7 @@ void PrintObject::bridge_over_infill()
for (size_t job_idx = r.begin(); job_idx < r.end(); job_idx++) {
size_t lidx = layers_to_generate_infill[job_idx];
infill_lines.at(
lidx) = po->get_layer(lidx)->generate_sparse_infill_polylines_for_anchoring(&po->m_adaptive_fill_octrees.first,
&po->m_adaptive_fill_octrees.second,
lidx) = po->get_layer(lidx)->generate_sparse_infill_polylines_for_anchoring(&po->m_adaptive_fill_octrees,
po->m_lightning_generator.get());
}
});
+127 -38
View File
@@ -1248,52 +1248,81 @@ bool triangles_overlap(const Tri2 &a, const Tri2 &b, float eps)
struct NetGrid
{
static constexpr int BIG_SPAN = 16;
float cell;
float eps;
std::unordered_map<uint64_t, std::vector<Tri2>> cells;
std::vector<Tri2> big;
// Each stored triangle keeps its own bounding box. Overlap testing is dominated by rejects - a cell
// holds every triangle whose box touches it, and a candidate meets only a couple of them for real -
// so paying six floats per entry to answer most of those rejects with four comparisons, instead of a
// full triangle intersection, is what makes the net affordable. Measured on a 42k-triangle patch the
// grid ran ~19 million candidate pairs per net, nearly all of them misses, and rejecting them this
// way took the net from ~175 ms to ~53 ms.
//
// The box rides inside the entry rather than in a parallel array: splitting them to scan boxes back
// to back was tried and came out slower, because each bucket then grows two vectors instead of one.
struct Entry
{
Tri2 tri;
Vec2f lo, hi;
};
float cell;
float eps;
std::unordered_map<uint64_t, std::vector<Entry>> cells;
std::vector<Entry> big;
static uint64_t key(int x, int y) { return (uint64_t(uint32_t(x)) << 32) | uint32_t(y); }
bool range(const Tri2 &t, int &x0, int &y0, int &x1, int &y1) const
static Entry entry(const Tri2 &t)
{
return Entry{ t, t[0].cwiseMin(t[1]).cwiseMin(t[2]), t[0].cwiseMax(t[1]).cwiseMax(t[2]) };
}
bool range(const Vec2f &lo, const Vec2f &hi, int &x0, int &y0, int &x1, int &y1) const
{
const Vec2f lo = t[0].cwiseMin(t[1]).cwiseMin(t[2]), hi = t[0].cwiseMax(t[1]).cwiseMax(t[2]);
x0 = int(std::floor(lo.x() / cell));
y0 = int(std::floor(lo.y() / cell));
x1 = int(std::floor(hi.x() / cell));
y1 = int(std::floor(hi.y() / cell));
return x1 - x0 <= BIG_SPAN && y1 - y0 <= BIG_SPAN;
}
// Boxes grown by eps on both sides, to match the tolerance triangles_overlap() itself works to: a
// reject here must never discard a pair that test would have called touching.
bool boxes_apart(const Entry &a, const Entry &b) const
{
return a.hi.x() + eps < b.lo.x() || b.hi.x() + eps < a.lo.x() || a.hi.y() + eps < b.lo.y() ||
b.hi.y() + eps < a.lo.y();
}
bool hits(const Entry &q, const std::vector<Entry> &bucket) const
{
for (const Entry &b : bucket)
if (!boxes_apart(q, b) && triangles_overlap(q.tri, b.tri, eps))
return true;
return false;
}
bool overlaps(const Tri2 &t) const
{
for (const Tri2 &b : big)
if (triangles_overlap(t, b, eps))
return true;
const Entry q = entry(t);
if (hits(q, big))
return true;
int x0, y0, x1, y1;
if (!range(t, x0, y0, x1, y1)) {
for (const auto &[k, tris] : cells)
for (const Tri2 &b : tris)
if (triangles_overlap(t, b, eps))
return true;
if (!range(q.lo, q.hi, x0, y0, x1, y1)) {
for (const auto &[k, bucket] : cells)
if (hits(q, bucket))
return true;
return false;
}
for (int x = x0; x <= x1; ++x)
for (int y = y0; y <= y1; ++y)
if (const auto it = cells.find(key(x, y)); it != cells.end())
for (const Tri2 &b : it->second)
if (triangles_overlap(t, b, eps))
return true;
if (const auto it = cells.find(key(x, y)); it != cells.end() && hits(q, it->second))
return true;
return false;
}
void insert(const Tri2 &t)
{
int x0, y0, x1, y1;
if (!range(t, x0, y0, x1, y1)) {
big.push_back(t);
const Entry e = entry(t);
int x0, y0, x1, y1;
if (!range(e.lo, e.hi, x0, y0, x1, y1)) {
big.push_back(e);
return;
}
for (int x = x0; x <= x1; ++x)
for (int y = y0; y <= y1; ++y)
cells[key(x, y)].push_back(t);
cells[key(x, y)].push_back(e);
}
};
} // namespace
@@ -1305,11 +1334,20 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
if (n <= 1)
return islands;
// Chart adjacency, with one representative shared edge per adjacent pair.
// Chart adjacency, with one representative shared edge per adjacent pair: the fold line the pair is
// unfolded about.
//
// Which edge that is matters, because two charts can touch along more than one run. A chart cut open
// to flatten it - a ring opened by segment_into_charts(), say - touches its other half along *both*
// sides of the cut. Folding is rigid, so only the run the fold line belongs to comes out matching;
// every other run is left mismatched, and a mismatched run is exactly where the texture visibly
// jumps. Taking whichever edge the map happened to yield first therefore left the long side broken
// about as often as the short one. The fold line is picked from the longest run instead, so what is
// left discontinuous is the shortest boundary the pair has.
const auto edges = build_shared_edges(unwrap);
struct PairEdge { ChartEdge a, b; };
std::map<std::pair<int, int>, PairEdge> pair_edge;
std::vector<std::vector<int>> adj(static_cast<size_t>(n));
struct SharedEdge { PairEdge fold; int base_lo = -1, base_hi = -1; float length = 0.f; };
std::map<std::pair<int, int>, std::vector<SharedEdge>> pair_shared;
for (const auto &[base_edge, list] : edges) {
for (size_t i = 0; i < list.size(); ++i)
for (size_t j = i + 1; j < list.size(); ++j) {
@@ -1317,14 +1355,50 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
if (c1 == c2 || c1 < 0 || c2 < 0 || c1 >= n || c2 >= n)
continue;
const std::pair<int, int> pk{ std::min(c1, c2), std::max(c1, c2) };
if (pair_edge.count(pk))
continue; // keep the first shared edge as the fold line for this pair
pair_edge[pk] = (c1 < c2) ? PairEdge{ list[i], list[j] } : PairEdge{ list[j], list[i] };
adj[size_t(pk.first)].push_back(pk.second);
adj[size_t(pk.second)].push_back(pk.first);
SharedEdge se;
se.fold = (c1 < c2) ? PairEdge{ list[i], list[j] } : PairEdge{ list[j], list[i] };
se.base_lo = base_edge.first;
se.base_hi = base_edge.second;
// The unwrap is scaled to true surface area, so a uv distance is a length in mm.
se.length = (unwrap.uvs[size_t(se.fold.a.uv_lo)] - unwrap.uvs[size_t(se.fold.a.uv_hi)]).norm();
pair_shared[pk].push_back(se);
}
}
std::map<std::pair<int, int>, PairEdge> pair_edge;
std::map<std::pair<int, int>, float> pair_weight; // length of the run each pair folds across
std::vector<std::vector<int>> adj(static_cast<size_t>(n));
for (const auto &[pk, shared] : pair_shared) {
// Group the pair's shared edges into runs - edges joined end to end through a base vertex - and
// total each run's length.
std::unordered_map<int, int> local;
for (const SharedEdge &se : shared)
for (const int v : { se.base_lo, se.base_hi })
local.emplace(v, int(local.size()));
UnionFind runs(local.size());
for (const SharedEdge &se : shared)
runs.unite(local[se.base_lo], local[se.base_hi]);
std::unordered_map<int, float> run_length;
std::unordered_map<int, size_t> run_first;
for (size_t i = 0; i < shared.size(); ++i) {
const int root = runs.find(local[shared[i].base_lo]);
run_length[root] += shared[i].length;
run_first.emplace(root, i);
}
int best_root = -1;
float best_len = -1.f;
for (const auto &[root, len] : run_length)
if (len > best_len) { best_len = len; best_root = root; }
if (best_root < 0)
continue;
pair_edge[pk] = shared[run_first[best_root]].fold;
pair_weight[pk] = best_len;
adj[size_t(pk.first)].push_back(pk.second);
adj[size_t(pk.second)].push_back(pk.first);
}
// Per chart: its vertices, its triangles and its flattened area.
std::vector<std::vector<int>> chart_verts(static_cast<size_t>(n)), chart_tris(static_cast<size_t>(n));
std::vector<float> chart_area(static_cast<size_t>(n), 0.f);
@@ -1378,15 +1452,29 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
for (const int t : chart_tris[size_t(root)])
grid.insert(placed(m, t));
}
std::queue<int> q;
q.push(root);
// Grown strongest-adjacency-first (Prim, not breadth-first): a chart is folded onto whichever
// neighbour it shares the longest boundary with, among everything reachable so far. Order matters
// because only the fold a chart is actually reached by comes out matching - every other boundary
// it has is left to chance. Taking neighbours in breadth-first order, biggest-area first, let a
// far-off branch claim a chart across a short boundary before its true neighbour was reached, and
// the long boundary they shared then stayed broken. That is the visible seam next to a hole: a
// ring is cut into two halves that share a long boundary, and whichever half was reached first
// took the other one along some unrelated edge.
using Candidate = std::pair<float, std::pair<int, int>>; // weight, (from, to)
std::priority_queue<Candidate> q;
const auto push_neighbours = [&](int p) {
for (const int c : adj[size_t(p)])
if (net_of[size_t(c)] < 0 && !chart_tris[size_t(c)].empty()) {
const auto w = pair_weight.find({ std::min(p, c), std::max(p, c) });
q.push({ w == pair_weight.end() ? 0.f : w->second, { p, c } });
}
};
push_neighbours(root);
while (!q.empty()) {
const int p = q.front();
const auto [weight, link] = q.top();
q.pop();
std::vector<int> neighbours = adj[size_t(p)];
std::stable_sort(neighbours.begin(), neighbours.end(),
[&chart_area](int a, int b) { return chart_area[size_t(a)] > chart_area[size_t(b)]; });
for (const int c : neighbours) {
const int p = link.first, c = link.second;
{
if (net_of[size_t(c)] >= 0 || chart_tris[size_t(c)].empty())
continue;
const auto it = pair_edge.find({ std::min(p, c), std::max(p, c) });
@@ -1417,7 +1505,7 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
for (const Tri2 &t : tris)
grid.insert(t);
net_of[size_t(c)] = net;
q.push(c);
push_neighbours(c);
}
}
}
@@ -5053,4 +5141,5 @@ indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const s
return out;
}
} // namespace Slic3r
+12 -7
View File
@@ -19,7 +19,6 @@
#include "slic3r/GUI/Widgets/Label.hpp"
#include <utility>
#include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/GUI/Widgets/HyperLink.hpp"
#include <cstdlib>
#include <cstdio>
#include "slic3r/GUI/DeviceCore/DevDefs.h"
@@ -48,6 +47,7 @@
#include <wx/panel.h>
#include <wx/textctrl.h>
#include <wx/string.h>
#include <wx/utils.h>
#include <wx/valtext.h>
#include <wx/sizer.h>
#include <wx/peninfobase.h>
@@ -103,6 +103,14 @@ void AMSMaterialsSetting::create()
m_sizer_button->Add(0, 0, 1, wxEXPAND, 0);
// Orca: link to the Orca Slicer pressure-advance wiki (region-agnostic).
m_wiki_ctrl = new Button(this, "", "toolbar_wiki", 0, 15);
auto wiki_url = "https://www.orcaslicer.com/wiki/pressure_advance_calib";
m_wiki_ctrl->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
m_wiki_ctrl->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
m_wiki_ctrl->SetCanFocus(false);
m_wiki_ctrl->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
m_button_confirm = new Button(this, _L("Confirm"));
m_button_confirm->SetStyle(ButtonStyle::Confirm, ButtonType::Choice);
m_button_confirm->Bind(wxEVT_BUTTON, &AMSMaterialsSetting::on_select_ok, this);
@@ -115,8 +123,9 @@ void AMSMaterialsSetting::create()
m_button_close->SetStyle(ButtonStyle::Regular, ButtonType::Choice);
m_button_close->Bind(wxEVT_BUTTON, &AMSMaterialsSetting::on_select_close, this);
m_sizer_button->Add(m_button_confirm, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(20));
m_sizer_button->Add(m_button_reset, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(20));
m_sizer_button->Add(m_wiki_ctrl, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(20));
m_sizer_button->Add(m_button_confirm, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(10));
m_sizer_button->Add(m_button_reset, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(10));
m_sizer_button->Add(m_button_close, 0, wxALIGN_CENTER, 0);
m_sizer_main->Add(m_panel_normal, 0, wxALL, FromDIP(2));
@@ -340,11 +349,7 @@ void AMSMaterialsSetting::create_panel_kn(wxWindow* parent)
m_ratio_text->SetForegroundColour(wxColour(50, 58, 61));
m_ratio_text->SetFont(Label::Head_14);
// Orca: link to the Orca Slicer pressure-advance wiki (region-agnostic).
wxString link_url = "https://www.orcaslicer.com/wiki/pressure_advance_calib";
m_wiki_ctrl = new HyperLink(parent, _L("Wiki Guide"), link_url);
cali_title_sizer->Add(m_ratio_text, 0, wxALIGN_CENTER_VERTICAL);
cali_title_sizer->Add(m_wiki_ctrl, 0, wxALIGN_CENTER_VERTICAL);
wxBoxSizer *m_sizer_cali_resutl = new wxBoxSizer(wxHORIZONTAL);
// pa profile
+1 -2
View File
@@ -14,7 +14,6 @@
#include "Widgets/CheckBox.hpp"
#include "Widgets/ComboBox.hpp"
#include "Widgets/TextInput.hpp"
#include "Widgets/HyperLink.hpp"
#include "slic3r/Utils/CalibUtils.hpp"
#include <wx/anybutton.h>
#include <wx/colour.h>
@@ -194,7 +193,7 @@ protected:
wxPanel * m_panel_kn;
wxStaticText* m_ratio_text;
HyperLink * m_wiki_ctrl;
Button * m_wiki_ctrl;
wxStaticText* m_k_param;
TextInput* m_input_k_val;
wxStaticText* m_n_param;
+7 -1
View File
@@ -287,7 +287,13 @@ void BackgroundSlicingProcess::process_fff()
// GCodeProcessorResult, so m_gcode_result->nozzle_group_result (consumed by the H2C print-dispatch
// nozzle mapping) survives post-processing. No preservation guard is needed on this path.
if (m_fff_print->is_BBL_printer()) {
run_post_process_scripts(m_temp_output_path, false, "File", m_temp_output_path, m_fff_print->full_print_config());
if (run_post_process_scripts(m_temp_output_path, false, "File", m_temp_output_path, m_fff_print->full_print_config()))
// The scripts/plugins rewrote the very file the G-code viewer memory-maps, so every byte
// offset in m_gcode_result->lines_ends (built while exporting the pre-processed G-code) is
// now stale and GCodeWindow would slice the file mid-line. Re-scan it. Note this only
// realigns the line framing: if a script inserts or removes lines, the moves' gcode_id
// still refers to the pre-processed numbering and the highlighted line stays shifted.
m_gcode_result->rebuild_lines_ends();
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": export gcode finished");
+4 -2
View File
@@ -21,7 +21,6 @@
#include "slic3r/GUI/PresetComboBoxes.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
#include <wx/anybutton.h>
#include "slic3r/GUI/Widgets/HyperLink.hpp"
#include <wx/utils.h>
#include <wx/arrstr.h>
#include "slic3r/GUI/Widgets/PopupWindow.hpp"
@@ -494,7 +493,10 @@ void CaliPageCaption::init_bitmaps() {
void CaliPageCaption::create_wiki(wxWindow* parent)
{
// ORCA standardized HyperLink
m_wiki_text = new HyperLink(parent, _L("Wiki Guide"));
m_wiki_text = new Button(parent, "", "toolbar_wiki", 0, 15);
m_wiki_text->SetToolTip(_L("Wiki Guide"));
m_wiki_text->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
m_wiki_text->SetCanFocus(false);
m_wiki_text->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) {
if (!m_wiki_url.empty())
wxLaunchDefaultBrowser(m_wiki_url);
+1 -2
View File
@@ -7,7 +7,6 @@
#include "Widgets/TextInput.hpp"
#include "Widgets/AMSControl.hpp"
#include "Widgets/ProgressBar.hpp"
#include "Widgets/HyperLink.hpp"
#include "wxExtensions.hpp"
#include "PresetComboBoxes.hpp"
@@ -155,7 +154,7 @@ private:
void init_bitmaps();
void create_wiki(wxWindow* parent);
HyperLink* m_wiki_text; // ORCA
Button* m_wiki_text; // ORCA
wxString m_wiki_url;
ScalableBitmap m_prev_bmp_normal;
ScalableBitmap m_prev_bmp_hover;
+18 -12
View File
@@ -4,7 +4,6 @@
#include "wx/dcgraph.h"
#include "I18N.hpp"
#include "PartPlate.hpp"
#include "Widgets/HyperLink.hpp"
#include <wx/colour.h>
#include "libslic3r/Config.hpp"
#include "libslic3r/PrintConfig.hpp"
@@ -27,6 +26,7 @@
#include "slic3r/GUI/Widgets/StaticBox.hpp"
#include <utility>
#include "slic3r/GUI/Widgets/SwitchButton.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include <wx/tglbtn.h>
namespace Slic3r { namespace GUI {
@@ -203,18 +203,24 @@ FilamentGroupPopup::FilamentGroupPopup(wxWindow *parent) : PopupWindow(parent, w
{
wxBoxSizer *button_sizer = new wxBoxSizer(wxHORIZONTAL);
// ORCA Unified hyperlinks
video_link = new HyperLink(this, _L("Video tutorial"));
video_link->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent& e)
{
play_dual_extruder_slice_video();
wxGetApp().app_config->set("play_slicing_video", "false");
});
button_sizer->Add(video_link, 0, wxLEFT, horizontal_margin + FromDIP(3));
button_sizer->AddStretchSpacer();
wiki_link = new HyperLink(this, _L("Wiki Guide"));
wiki_link->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent&) { open_filament_group_wiki(); });
button_sizer->Add(wiki_link, 0, wxLEFT, horizontal_margin);
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + "https://e.bambulab.com/t?c=mOkvsXkJ9pldGYp9");
wiki_btn->SetStyle(ButtonStyle::Confirm, ::ButtonType::Circle);
wiki_btn->SetCanFocus(false);
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([](auto& e) {open_filament_group_wiki();}));
auto video_btn = new Button(this, "", "toolbar_video_guide", 0, 15);
video_btn->SetToolTip(_L("Video Guide"));
video_btn->SetStyle(ButtonStyle::Confirm, ::ButtonType::Circle);
video_btn->SetCanFocus(false);
video_btn->Bind(wxEVT_LEFT_DOWN, ([](auto& e) {
play_dual_extruder_slice_video();
wxGetApp().app_config->set("play_slicing_video", "false");
}));
button_sizer->Add(wiki_btn , 0, wxLEFT, horizontal_margin + FromDIP(3));
button_sizer->Add(video_btn, 0, wxLEFT, FromDIP(10));
top_sizer->Add(button_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, horizontal_margin);
}
+1 -5
View File
@@ -29,6 +29,7 @@ public:
FilamentGroupPopup(wxWindow *parent);
void tryPopup(Plater* plater,PartPlate* plate, bool slice_all);
void tryClose();
void Dismiss() override;
FilamentMapMode GetSelectedMode() const { return m_mode; }
private:
@@ -40,7 +41,6 @@ private:
void OnLeaveWindow(wxMouseEvent &);
void OnEnterWindow(wxMouseEvent &);
void OnTimer(wxTimerEvent &event);
void Dismiss();
void CreateBmps();
@@ -80,10 +80,6 @@ private:
wxBitmap unchecked_hover_bmp;
wxBitmap global_tag_bmp;
wxStaticText *wiki_link;
wxStaticText *video_link;
StaticBox *m_smart_filament_panel{nullptr};
wxSizerItem *m_smart_filament_spacer{nullptr};
SwitchButton *m_smart_filament_switch{nullptr};
+14 -32
View File
@@ -32,6 +32,7 @@
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GLModel.hpp"
#include "slic3r/GUI/GLShader.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoUtils.hpp"
#include "libslic3r/Point.hpp"
#include <math.h>
#include "libvgcode/include/Viewer.hpp"
@@ -889,10 +890,16 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, f
auto update_lines = [this](uint64_t start_id, uint64_t end_id) {
std::vector<Line> ret;
ret.reserve(end_id - start_id + 1);
// Orca: m_lines_ends indexes into a memory mapping, so it must be clamped to the mapping. If the
// file was modified behind our back (an in-place post-processing script that shrank it), an
// unchecked read is an access violation, which the caller's try/catch cannot catch on Windows.
const size_t file_size = m_file.size();
for (uint64_t id = start_id; id <= end_id; ++id) {
// read line from file
const size_t start = id == 1 ? 0 : m_lines_ends[id - 2];
const size_t original_len = m_lines_ends[id - 1] - start;
// Keep one entry per id: render() indexes m_lines by (id - start_id).
const size_t start = id == 1 ? 0 : std::min(m_lines_ends[id - 2], file_size);
const size_t end = std::min(m_lines_ends[id - 1], file_size);
const size_t original_len = end > start ? end - start : 0;
// A character is four bytes at most, so 55 of them always fit in 220.
const size_t len = std::min(original_len, (size_t) 55 * 4);
std::string gline(m_file.data() + start, len);
@@ -3127,27 +3134,6 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
}
};
auto link_filament_group_wiki = [&](const std::string& label) {
ImVec2 wiki_part_size = ImGui::CalcTextSize(label.c_str());
ImColor HyperColor = ImColor(0, 150, 136, 255); // ORCA match color
ImGui::PushStyleColor(ImGuiCol_Text, HyperColor.Value);
imgui.text(label.c_str());
ImGui::PopStyleColor();
// ORCA use underline to match hyperlink style
ImVec2 lineEnd = ImGui::GetItemRectMax();
lineEnd.y -= 2.0f;
ImVec2 lineStart = lineEnd;
lineStart.x = ImGui::GetItemRectMin().x;
ImGui::GetWindowDrawList()->AddLine(lineStart, lineEnd, HyperColor);
// click behavior
if (ImGui::IsMouseHoveringRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), true)) {
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
open_filament_group_wiki();
}
}
};
auto draw_dash_line = [&](ImDrawList* draw_list, int dash_length = 5, int gap_length = 3) {
ImVec2 p1 = ImGui::GetCursorScreenPos();
ImVec2 p2 = ImVec2(p1.x + ImGui::GetContentRegionAvail().x, p1.y);
@@ -3221,7 +3207,7 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
else
tips_count = 5;
float AMS_container_height = ams_item_height + line_height * tips_count + line_height;
float AMS_container_height = ams_item_height + line_height * tips_count + line_height + window_padding * 2.f;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // this shold be 0 since its child of gcodeviewer
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(window_padding * 3, 0));
@@ -3236,10 +3222,6 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
ImGui::Dummy({window_padding, window_padding});
ImGui::PushStyleColor(ImGuiCol_Separator, ImVec4(1.0f,1.0f,1.0f,0.6f));
imgui.bold_text(_u8L("Filament Grouping"));
ImGui::SameLine();
std::string tip_str = _u8L("Why this grouping");
ImGui::SetCursorPosX(ImGui::GetWindowContentRegionWidth() - window_padding - ImGui::CalcTextSize(tip_str.c_str()).x);
link_filament_group_wiki(tip_str);
ImGui::Separator();
ImGui::PopStyleColor();
ImGui::Dummy({window_padding, window_padding});
@@ -3344,6 +3326,9 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
}
ImGui::Dummy({window_padding, window_padding});
GLGizmoUtils::render_wiki_guide_button(*wxGetApp().plater()->get_current_canvas3D(), m_scale,"https://e.bambulab.com/t?c=mOkvsXkJ9pldGYp9");
ImGui::SameLine();
if (!is_optimal_group) {
link_text_set_to_optional(_u8L("Set to Optimal"));
ImGui::SameLine();
@@ -3352,10 +3337,7 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
}
link_text(_u8L("Regroup filament"));
ImGui::SameLine();
std::string wiki_str = _u8L("Wiki Guide"); // ORCA
ImGui::SetCursorPosX(ImGui::GetWindowContentRegionWidth() - window_padding - ImGui::CalcTextSize(wiki_str.c_str()).x);
link_filament_group_wiki(wiki_str);
ImGui::Dummy({window_padding, window_padding});
ImGui::EndChild();
}
@@ -27,6 +27,7 @@
#include "slic3r/GUI/MsgDialog.hpp"
#include "slic3r/GUI/OpenGLManager.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/TextureLibrary.hpp"
#include "slic3r/GUI/TextureProjectorFrame.hpp"
#include "slic3r/GUI/UVEditorCanvas.hpp"
@@ -407,11 +408,48 @@ GLGizmoTextureDisplacement::GLGizmoTextureDisplacement(GLCanvas3D& parent, const
bool GLGizmoTextureDisplacement::on_init()
{
m_shortcut = Shortcut::GizmoDisplacement;
const wxString ctrl = GUI::shortkey_ctrl_prefix();
const wxString alt = GUI::shortkey_alt_prefix();
const wxString shift = GUI::shortkey_shift_prefix();
m_desc["cursor_size"] = _L("Brush size");
m_desc["circle"] = _L("Circle");
m_desc["sphere"] = _L("Sphere");
m_desc["remove_layer"] = _L("Remove");
m_desc["bake"] = _L_CONTEXT("Bake", "Texture Displacement");
m_desc["paint"] = _L("Paint");
m_desc["erase"] = _L("Erase");
m_desc["gap_area"] = _L("Gap area");
m_desc["smart_fill_angle"] = _L("Smart fill angle");
m_desc["toggle_wireframe"] = _L("Toggle Wireframe");
std::pair<wxString, wxString> paint_shortcut = {_L("Left mouse button"), m_desc["paint"]};
std::pair<wxString, wxString> erase_shortcut = {shift + _L("Left mouse button"), m_desc["erase"]};
std::pair<wxString, wxString> toggle_wireframe_shortcut = {alt + shift + _L_CONTEXT("Enter", "Keyboard Shortcut"), m_desc["toggle_wireframe"]};
m_shortcuts_brush = {
paint_shortcut,
erase_shortcut,
{ctrl + _L("Mouse wheel"), m_desc["cursor_size"]},
toggle_wireframe_shortcut
};
m_shortcuts_bucket_fill = {
paint_shortcut,
erase_shortcut,
{ctrl + _L("Mouse wheel"), m_desc["smart_fill_angle"]},
toggle_wireframe_shortcut
};
m_shortcuts_gap_fill = {
{ctrl + _L("Mouse wheel"), m_desc["gap_area"]},
toggle_wireframe_shortcut
};
return true;
}
@@ -523,7 +561,15 @@ 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();
const bool use_true_preview = !use_shaded && m_preview_glmodel.is_initialized();
// 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();
// 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
@@ -551,7 +597,13 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
render_triangles(selection);
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
}
} else if (show_paint_overlay) {
} 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.
render_triangles(selection);
}
@@ -5313,6 +5365,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
return;
ModelVolume *mv = texture_volume();
float scale = m_parent.get_scale();
#ifdef WIN32
int dpi = get_dpi_for_window(wxGetApp().GetTopWindow());
scale *= (float) dpi / (float) DPI_DEFAULT;
#endif // WIN32
const float approx_height = m_imgui->scaled(24.f);
y = std::min(y, bottom_limit - approx_height);
@@ -5742,14 +5800,20 @@ 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;
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())
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())
new_mode = 0;
const float x0 = ImGui::GetCursorPosX();
@@ -5768,12 +5832,20 @@ 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")))
new_mode = 2;
"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;
}
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"), distortion_na))
new_mode = 3;
_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;
}
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")))
@@ -5788,6 +5860,13 @@ 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) {
@@ -6891,18 +6970,34 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
}
const float button_h = std::round(frame_h * 1.25f);
const float third = std::floor((panel_w - style.ItemSpacing.x) / 3.f);
if (busy) {
if (ImGui::Button((_u8L("Stop") + "##stop").c_str(), ImVec2(third, button_h)))
wxGetApp().plater()->get_ui_job_worker().cancel_all();
hover_tip(_u8L("Stops the bake. Whatever it had already finished stays on the model, and can be undone."));
} else {
if (ImGui::Button((_u8L("Close") + "##close").c_str(), ImVec2(third, button_h)))
m_parent.reset_all_gizmos();
hover_tip(_u8L("Closes the tool without baking. Your paint, layers and settings stay with the model."));
}
const float button_w = std::max({
ImGui::CalcTextSize(_u8L("Close").c_str()).x,
ImGui::CalcTextSize(_u8L("Stop").c_str()).x,
ImGui::CalcTextSize(_u8L("Preparing...").c_str()).x,
ImGui::CalcTextSize(_u8L("Baking...").c_str()).x,
}) + m_imgui->scaled(0.5f);
const float row_y = ImGui::GetCursorPosY();
const float icon_h = 21.f * scale;
const float row_h = std::max(icon_h, button_h);
const std::vector<std::pair<wxString, wxString>> shortcut =
m_tool_type == ToolType::BUCKET_FILL ? m_shortcuts_bucket_fill
: m_tool_type == ToolType::SMART_FILL ? m_shortcuts_bucket_fill
: m_tool_type == ToolType::BRUSH ? m_shortcuts_brush
: m_tool_type == ToolType::GAP_FILL ? m_shortcuts_gap_fill
: std::vector<std::pair<wxString, wxString>>{};
ImGui::SetCursorPosY(row_y + (row_h - icon_h) * .5f); // center vertically
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(int(m_imgui->scaled(.5f)), style.ItemSpacing.y));
GLGizmoUtils::render_tooltip_button(m_imgui, m_parent, shortcut, x, y);
ImGui::SameLine();
GLGizmoUtils::render_wiki_guide_button(m_parent, scale, "https://www.orcaslicer.com/wiki/print_prepare/prepare_texture_displacement");
ImGui::SameLine();
GLGizmoUtils::render_video_guide_button(m_parent, scale, "https://www.youtube.com/watch?v=D7w3tG1kdvE");
ImGui::PopStyleVar(1);
ImGui::SameLine(x0 + panel_w - button_w * 2 - style.ItemSpacing.x);
const bool can_bake = !busy && mv != nullptr && mv->is_texture_displacement_painted();
const std::string bake_label = m_prepare_in_progress ? _u8L("Preparing...") :
m_bake_in_progress ? _u8L("Baking...") :
@@ -6910,7 +7005,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
GLGizmoUtils::push_orca_button_style();
m_imgui->push_bold_font();
m_imgui->disabled_begin(!can_bake);
if (ImGui::Button((bake_label + "##bake").c_str(), ImVec2(x0 + panel_w - ImGui::GetCursorPosX(), button_h))) {
ImGui::SetCursorPosY(row_y + (row_h - button_h) * .5f); // center vertically
if (ImGui::Button((bake_label + "##bake").c_str(), ImVec2(button_w, button_h))) {
// Standard mode's Bake is the whole pipeline (remesh -> refine -> displace); Pro's is only the
// displacement, because there the user has already prepared the mesh with the controls above.
if (pro_mode())
@@ -6935,6 +7031,17 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"land on - all in one step."),
wrap_w);
ImGui::SameLine();
if (busy) {
if (ImGui::Button((_u8L("Stop") + "##stop").c_str(), ImVec2(button_w, button_h)))
wxGetApp().plater()->get_ui_job_worker().cancel_all();
hover_tip(_u8L("Stops the bake. Whatever it had already finished stays on the model, and can be undone."));
} else {
if (ImGui::Button((_u8L("Close") + "##close").c_str(), ImVec2(button_w, button_h)))
m_parent.reset_all_gizmos();
hover_tip(_u8L("Closes the tool without baking. Your paint, layers and settings stay with the model."));
}
// What Bake will produce, and which layers it will skip.
if (mv != nullptr) {
const std::string base_count = base_k > 0 ? Slic3r::format(_u8L("%1% k"), base_k) :
@@ -943,6 +943,13 @@ private:
std::map<std::string, wxString> m_desc;
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
std::vector<std::pair<wxString, wxString>> m_shortcuts_brush;
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
std::vector<std::pair<wxString, wxString>> m_shortcuts_bucket_fill;
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
std::vector<std::pair<wxString, wxString>> m_shortcuts_gap_fill;
// Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
// the theme's normal (grey) foreground colour and an active one shows it in its original colours -
+57 -9
View File
@@ -3,6 +3,7 @@
#include "GLGizmosManager.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/I18N.hpp"
#include <vector>
#include <utility>
#include <imgui.h>
@@ -10,6 +11,7 @@
#include <string_view>
#include <cstddef>
#include <wx/app.h>
#include <wx/utils.h>
#include <boost/algorithm/string.hpp>
#include "slic3r/GUI/GUI_Utils.hpp"
@@ -55,7 +57,6 @@ namespace Slic3r::GUI::GLGizmoUtils {
auto& gizmos_manager = canvas.get_gizmos_manager();
ImTextureID normal_id = gizmos_manager.get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP);
ImTextureID hover_id = gizmos_manager.get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP_HOVER);
float scale = canvas.get_scale();
#ifdef WIN32
@@ -63,13 +64,7 @@ namespace Slic3r::GUI::GLGizmoUtils {
scale *= (float)dpi / (float)DPI_DEFAULT;
#endif
ImVec2 button_size = ImVec2(25 * scale, 25 * scale);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, { 0, 0 });
ImGui::ImageButton3(normal_id, hover_id, button_size);
toolbar_circular_button(normal_id, "##tooltip_btn", scale);
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip2(ImVec2(x, caption_y));
for (const auto& item : shortcuts) {
@@ -79,7 +74,60 @@ namespace Slic3r::GUI::GLGizmoUtils {
}
ImGui::EndTooltip();
}
ImGui::PopStyleVar(2);
}
bool render_wiki_guide_button(const GLCanvas3D& canvas, float scale, const char* url)
{
auto& gm = canvas.get_gizmos_manager();
auto icon = gm.get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_WIKI_GUIDE);
bool clicked = toolbar_circular_button(icon, "##wiki_guide_btn", scale);
if (url && *url && clicked)
wxLaunchDefaultBrowser(wxString::FromUTF8(url));
if (ImGui::IsItemHovered()){
if(url && *url)
ImGui::SetTooltip("%s\n%s", _u8L("Wiki Guide").c_str(), url);
else
ImGui::SetTooltip("%s", _u8L("Wiki Guide").c_str());
}
return clicked; // for dynamically generated links
}
bool render_video_guide_button(const GLCanvas3D& canvas, float scale, const char* url)
{
auto& gm = canvas.get_gizmos_manager();
auto icon = gm.get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_VIDEO_GUIDE);
bool clicked = toolbar_circular_button(icon, "##video_guide_btn", scale);
if (url && *url && clicked)
wxLaunchDefaultBrowser(wxString::FromUTF8(url));
if (ImGui::IsItemHovered()){
if(url && *url)
ImGui::SetTooltip("%s\n%s", _u8L("Video Guide").c_str(), url);
else
ImGui::SetTooltip("%s", _u8L("Video Guide").c_str());
}
return clicked; // for dynamically generated links
}
bool toolbar_circular_button(ImTextureID textureID, const char* id, float scale)
{
ImVec2 btn_sz = ImVec2(21 * scale, 21 * scale);
ImVec2 icon_sz = ImVec2(15 * scale, 15 * scale);
float btn_pad = (btn_sz.x - icon_sz.x) * .5f;
ImVec2 p = ImGui::GetCursorScreenPos();
bool clicked = ImGui::InvisibleButton(id, btn_sz);
ImDrawList* dl = ImGui::GetWindowDrawList();
bool is_dark = ImGuiWrapper::COL_WINDOW_BG.x != ImGui::GetStyleColorVec4(ImGuiCol_WindowBg).x;
ImVec4 col = is_dark ? ImGuiWrapper::COL_ORCA_DARK : ImGuiWrapper::COL_ORCA;
ImVec4 col_hvr = is_dark ? ImGuiWrapper::COL_ORCA_HOVER_DARK : ImGuiWrapper::COL_ORCA_HOVER;
dl->AddCircleFilled(
ImVec2(p.x + btn_sz.x * .5f, p.y + btn_sz.y * .5f),
btn_sz.x * .5f,
ImGui::ColorConvertFloat4ToU32((ImGui::IsItemActive()||ImGui::IsItemHovered()) ? col_hvr : col)
);
dl->AddImage(textureID, ImVec2(p.x + btn_pad, p.y + btn_pad), ImVec2(p.x + btn_sz.x - btn_pad, p.y + btn_sz.y - btn_pad));
return clicked;
}
void begin_right_aligned_buttons(const std::vector<wxString>& labels)
+7
View File
@@ -19,6 +19,11 @@ namespace GLGizmoUtils {
// Renders a tooltip button using the provided shortcuts
void render_tooltip_button(
ImGuiWrapper* imgui_wrapper, const GLCanvas3D& canvas, const std::vector<std::pair<wxString, wxString>>& shortcuts, float x, float y);
void render_wiki_buttons(
ImGuiWrapper* imgui_wrapper, const GLCanvas3D& canvas, const char* wiki_url, const char* video_url = "");
bool render_wiki_guide_button(const GLCanvas3D& canvas, float scale, const char* url = "");
bool render_video_guide_button(const GLCanvas3D& canvas, float scale, const char* url = "");
// Sets up ImGui to render buttons that are right-aligned within the current window, using the provided labels to calculate spacing.
void begin_right_aligned_buttons(const std::vector<wxString>& labels);
@@ -27,6 +32,8 @@ namespace GLGizmoUtils {
void pop_orca_button_style();
bool toolbar_circular_button(ImTextureID textureID, const char* id, float scale);
} // namespace GLGizmoUtils
} // namespace Slic3r::GUI
+8 -3
View File
@@ -308,13 +308,18 @@ bool GLGizmosManager::init_icon_textures()
else
return false;
if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/toolbar_tooltip.svg", 25, 25, texture_id)) // ORCA: Use same resolution with gizmos to prevent blur on icon
if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/toolbar_tooltip.svg", 15, 15, texture_id)) // ORCA: Use same resolution with gizmos to prevent blur on icon
icon_list.insert(std::make_pair((int)IC_TOOLBAR_TOOLTIP, texture_id));
else
return false;
if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/toolbar_tooltip_hover.svg", 25, 25, texture_id)) // ORCA: Use same resolution with gizmos to prevent blur on icon
icon_list.insert(std::make_pair((int)IC_TOOLBAR_TOOLTIP_HOVER, texture_id));
if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/toolbar_wiki.svg", 15, 15, texture_id))
icon_list.insert(std::make_pair((int)IC_TOOLBAR_WIKI_GUIDE, texture_id));
else
return false;
if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/toolbar_video_guide.svg", 15, 15, texture_id))
icon_list.insert(std::make_pair((int)IC_TOOLBAR_VIDEO_GUIDE, texture_id));
else
return false;
+2 -1
View File
@@ -182,7 +182,8 @@ public:
IC_TOOLBAR_RESET_ZERO,
IC_TOOLBAR_RESET_ZERO_HOVER,
IC_TOOLBAR_TOOLTIP,
IC_TOOLBAR_TOOLTIP_HOVER,
IC_TOOLBAR_WIKI_GUIDE,
IC_TOOLBAR_VIDEO_GUIDE,
IC_NAME_COUNT,
IC_CANVAS_MENU,
IC_CANVAS_MENU_HOVER,
+6
View File
@@ -2309,6 +2309,12 @@ wxBoxSizer* MainFrame::create_side_tools()
bool slice = true;
// The Slice-plate hover popup is a transient popup that keeps grabbing
// the mouse capture while shown. Left behind the modal grouping dialog it
// would starve that dialog of mouse events, so close it synchronously first.
if (m_filament_group_popup)
m_filament_group_popup->Dismiss();
auto curr_plate = m_plater->get_partplate_list().get_curr_plate();
#ifdef __linux__
PresetBundle* preset = wxGetApp().preset_bundle;
+3 -3
View File
@@ -3128,7 +3128,7 @@ void PartPlate::set_vase_mode_related_object_config(int obj_id) {
new_conf.set_key_value("detect_thin_wall", new ConfigOptionBool(false));
new_conf.set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
new_conf.set_key_value("overhang_reverse", new ConfigOptionBool(false));
auto applying_keys = global_config->diff(new_conf);
const auto applying_keys = global_config->diff(new_conf);
for (ModelObject* object : obj_ptrs) {
ModelConfigObject& config = object->config;
@@ -3137,8 +3137,8 @@ void PartPlate::set_vase_mode_related_object_config(int obj_id) {
config.set_key_value(opt_key, new_conf.option(opt_key)->clone());
}
applying_keys = config.get().diff(new_conf);
for (auto opt_key : applying_keys) {
const auto object_keys = config.get().diff(new_conf);
for (auto opt_key : object_keys) {
config.set_key_value(opt_key, new_conf.option(opt_key)->clone());
}
}
+33 -28
View File
@@ -8800,6 +8800,20 @@ void read_binary_stl(const std::string& filename, std::string& model_id, std::st
return;
}
// Logs what show_substitutions_info() would list, for loads that don't show the dialog.
static void log_substitutions(const ConfigSubstitutions& substitutions, const std::string& source)
{
for (const ConfigSubstitution& substitution : substitutions)
BOOST_LOG_TRIVIAL(warning) << "Loading " << source << ": " << substitution.opt_def->opt_key << " = \"" << substitution.old_value
<< "\" replaced with \"" << substitution.new_value->serialize() << "\"";
}
static void log_substitutions(const PresetsConfigSubstitutions& substitutions, const std::string& source)
{
for (const PresetConfigSubstitutions& preset : substitutions)
log_substitutions(preset.substitutions, source + " (preset " + preset.preset_name + ")");
}
// BBS: backup & restore
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files,
LoadStrategy strategy,
@@ -9101,7 +9115,6 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
Semver app_version = *(Semver::parse(SoftFever_VERSION));
const wxString load_3mf_title = _L("Load 3MF");
const wxString newer_3mf_title = _L("Newer 3MF version");
const wxString bambu_project_title = _L("BambuStudio Project");
const wxString msg_unsupported_geometry = _L("The 3MF is not supported by OrcaSlicer, loading geometry data only.");
const wxString msg_old_orca_geometry = _L("The 3MF file was generated by an old OrcaSlicer version, loading geometry data only.");
const wxString msg_older_geometry = _L("The 3MF file was generated by an older version, loading geometry data only.");
@@ -9112,6 +9125,8 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
<< boost::format("3MF import message [%1%]: %2% | file: %3%") % into_u8(title) % into_u8(text) % path.string();
show_info(q, text, title);
};
// Untagged files up to 2.3.2 may also come from OrcaSlicer, which only started tagging its 3MFs after it.
const bool is_bambu_studio_project = en_3mf_file_type == En3mfType::From_BBS && file_version > Semver(2, 3, 2);
if (en_3mf_file_type == En3mfType::From_Prusa) {
// do not reset the model config
load_config = false;
@@ -9171,8 +9186,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
}
else if (en_3mf_file_type == En3mfType::From_BBS) {
// No OrcaSlicer tag - check Bambu/Application version
Semver orca_tag_start_version(2, 3, 2);
if (file_version <= orca_tag_start_version) {
if (!is_bambu_studio_project) {
// Compatible old version (before OrcaSlicer tagging was introduced after 2.3.2).
// Any version prior or equal to 2.3.2 is older than the current one, no version warnings needed.
// Still apply migration fixes for known old versions.
@@ -9205,33 +9219,17 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
}
} else {
// BambuStudio project (version > 2.3.2 without OrcaSlicer tag)
// Report that a BambuStudio project is being imported and compare with SLIC3R_VERSION
Semver slic3r_version = *(Semver::parse(SLIC3R_VERSION));
if (load_config && config_loaded.empty()) {
load_config = false;
log_and_show_3mf_info(msg_bambu_geometry, load_3mf_title);
}
else if (load_config && (file_version > slic3r_version)) {
// BambuStudio file version is newer than our compatible SLIC3R_VERSION
if (config_substitutions.unrecogized_keys.size() > 0) {
wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Found unrecognized settings:"),
file_version.to_string(), slic3r_version.to_string());
text += "\n";
wxString context = text;
wxString append = _L("You should update your software.\n");
context += "\n\n";
context += append;
log_and_show_3mf_info(context, bambu_project_title);
} else {
wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Some settings may not be fully compatible."),
file_version.to_string(), slic3r_version.to_string());
text += "\n";
log_and_show_3mf_info(text, bambu_project_title);
}
} else if (load_config && !published_config.published) {
// BambuStudio version is older or same as our SLIC3R_VERSION
wxString text = _L("The 3MF was created by BambuStudio. Some settings may differ from OrcaSlicer.");
log_and_show_3mf_info(text, bambu_project_title);
else if (load_config) {
// Logged, not shown: it is the same for every BambuStudio project and needs no action.
std::string unrecognized;
for (const std::string& key : config_substitutions.unrecogized_keys)
unrecognized += (unrecognized.empty() ? "" : ", ") + key;
BOOST_LOG_TRIVIAL(info) << "BambuStudio " << file_version.to_string() << " project " << path.string()
<< ", unrecognized settings: " << (unrecognized.empty() ? "none" : unrecognized);
}
}
}
@@ -9320,7 +9318,10 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
PresetsConfigSubstitutions preset_substitutions;
PresetBundle & preset_bundle = *wxGetApp().preset_bundle;
preset_substitutions = preset_bundle.load_project_embedded_presets(project_presets, ForwardCompatibilitySubstitutionRule::Enable);
if (!preset_substitutions.empty()) show_substitutions_info(preset_substitutions);
if (is_bambu_studio_project)
log_substitutions(preset_substitutions, path.string());
else if (!preset_substitutions.empty())
show_substitutions_info(preset_substitutions);
}
if (project_presets.size() > 0) {
for (unsigned int i = 0; i < project_presets.size(); i++) { delete project_presets[i]; }
@@ -9358,7 +9359,11 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
notify_manager->bbl_show_3mf_warn_notification(error_message);
}
}
if (!config_substitutions.empty()) show_substitutions_info(config_substitutions.substitutions, filename.string());
// BambuStudio projects routinely carry values Orca replaces; log them rather than showing a dialog on every open.
if (is_bambu_studio_project)
log_substitutions(config_substitutions.substitutions, path.string());
else if (!config_substitutions.empty())
show_substitutions_info(config_substitutions.substitutions, filename.string());
// BBS
if (load_model && !load_config) {
+12 -9
View File
@@ -20,7 +20,6 @@
#include "slic3r/GUI/DeviceCore/DevConfigUtil.h"
#include "slic3r/GUI/DeviceManager.hpp"
#include <wx/sizer.h>
#include "slic3r/GUI/Widgets/HyperLink.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "libslic3r/CommonDefs.hpp"
#include "slic3r/GUI/DeviceCore/DevDefs.h"
@@ -1545,12 +1544,14 @@ PrinterPartsDialog::PrinterPartsDialog(wxWindow* parent)
change_nozzle_tips->SetFont(Label::Body_13);
change_nozzle_tips->SetForegroundColour(STATIC_TEXT_CAPTION_COL);
m_wiki_link = new HyperLink(single_panel, _L("Wiki Guide")); // ORCA
m_wiki_link->SetFont(Label::Body_13);
m_wiki_link = new Button(single_panel, "", "toolbar_wiki", 0, 15);
m_wiki_link->SetToolTip(_L("Wiki Guide"));
m_wiki_link->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
m_wiki_link->SetCanFocus(false);
m_wiki_link->Bind(wxEVT_LEFT_DOWN, &PrinterPartsDialog::OnWikiClicked, this);
h_tips_sizer->Add(change_nozzle_tips, 0, wxLEFT);
h_tips_sizer->Add(m_wiki_link, 0, wxLEFT, FromDIP(5));
h_tips_sizer->Add(m_wiki_link);
h_tips_sizer->Add(change_nozzle_tips, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(5));
wxSizer* single_update_nozzle_sizer = new wxBoxSizer(wxHORIZONTAL);
m_single_update_nozzle_button = new Button(single_panel, _L("Refresh"));
@@ -1656,13 +1657,15 @@ PrinterPartsDialog::PrinterPartsDialog(wxWindow* parent)
multiple_change_nozzle_tips->SetFont(Label::Body_13);
multiple_change_nozzle_tips->SetForegroundColour(STATIC_TEXT_CAPTION_COL);
multiple_wiki_link = new HyperLink(multiple_panel, _L("Wiki Guide")); // ORCA
multiple_wiki_link->SetFont(Label::Body_13);
multiple_wiki_link = new Button(multiple_panel, "", "toolbar_wiki", 0, 15);
multiple_wiki_link->SetToolTip(_L("Wiki Guide"));
multiple_wiki_link->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
multiple_wiki_link->SetCanFocus(false);
multiple_wiki_link->Bind(wxEVT_LEFT_DOWN, &PrinterPartsDialog::OnWikiClicked, this);
wxSizer* multiple_change_tips_sizer = new wxBoxSizer(wxHORIZONTAL);
multiple_change_tips_sizer->Add(multiple_change_nozzle_tips, 0, wxLEFT);
multiple_change_tips_sizer->Add(multiple_wiki_link, 0, wxLEFT, FromDIP(5));
multiple_change_tips_sizer->Add(multiple_wiki_link);
multiple_change_tips_sizer->Add(multiple_change_nozzle_tips, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(5));
wxSizer* multiple_update_nozzle_sizer = new wxBoxSizer(wxHORIZONTAL);
m_multiple_update_nozzle_button = new Button(multiple_panel, _L("Refresh"));
+3 -3
View File
@@ -22,9 +22,9 @@
#include "Widgets/CheckBox.hpp"
#include "Widgets/StaticLine.hpp"
#include "Widgets/ComboBox.hpp"
#include "Widgets/HyperLink.hpp"
// Previous definitions
class Button;
class SwitchBoard;
class MultiSwitchButton;
@@ -41,7 +41,7 @@ protected:
Label* nozzle_flow_type_label;
ComboBox* nozzle_flow_type_checkbox;
Label *change_nozzle_tips;
HyperLink* m_wiki_link;
Button* m_wiki_link;
Button* m_single_update_nozzle_button;
Button* m_multiple_update_nozzle_button;
@@ -54,7 +54,7 @@ protected:
ComboBox *multiple_right_nozzle_flow_checkbox;
Label *multiple_change_nozzle_tips;
HyperLink* multiple_wiki_link;
Button* multiple_wiki_link;
wxPanel *single_panel;
wxPanel *multiple_panel;
+20 -7
View File
@@ -8,8 +8,8 @@
#include "ConfigValueFormatter.hpp"
#include "FilamentBitmapUtils.hpp"
#include "Widgets/Label.hpp"
#include "Widgets/Button.hpp"
#include "Widgets/CheckBox.hpp"
#include "Widgets/HyperLink.hpp"
#include "Widgets/TextInput.hpp"
#include "Widgets/DialogButtons.hpp"
#include "Widgets/StaticLine.hpp"
@@ -756,11 +756,24 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent,
dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
// Guide links, bottom-left, sharing the footer row with the OK/Cancel buttons (pushed right).
wxBoxSizer* links_sizer = new wxBoxSizer(wxVERTICAL);
auto* wiki_link = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf.html");
auto* video_link = new HyperLink(this, _L("Video Guide"), "https://www.youtube.com/watch?v=-xt1N29UIOg");
links_sizer->Add(wiki_link , 0, wxALIGN_LEFT);
links_sizer->Add(video_link, 0, wxTOP | wxALIGN_LEFT, FromDIP(4));
wxBoxSizer* links_sizer = new wxBoxSizer(wxHORIZONTAL);
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
auto wiki_url = "https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf";
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
wiki_btn->SetCanFocus(false);
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
auto video_btn = new Button(this, "", "toolbar_video_guide", 0, 15);
auto video_url = "https://www.youtube.com/watch?v=-xt1N29UIOg";
video_btn->SetToolTip(_L("Video Guide") + "\n" + video_url);
video_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
video_btn->SetCanFocus(false);
video_btn->Bind(wxEVT_LEFT_DOWN, ([video_url](auto& e) {wxLaunchDefaultBrowser(video_url);}));
links_sizer->Add(wiki_btn , 0, wxLEFT, FromDIP(10));
links_sizer->Add(video_btn, 0, wxLEFT, FromDIP(10));
wxBoxSizer* footer = new wxBoxSizer(wxHORIZONTAL);
footer->Add(links_sizer, 0, wxALIGN_CENTER_VERTICAL);
@@ -771,7 +784,7 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent,
footer_line->SetMinSize(wxSize(-1, 1));
footer_line->SetMaxSize(wxSize(-1, 1));
w_sizer->Add(footer_line, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10));
w_sizer->Add(footer, 0, wxRIGHT | wxLEFT | wxTOP | wxBOTTOM | wxEXPAND, FromDIP(10));
w_sizer->Add(footer, 0, wxEXPAND, FromDIP(10));
SetSizerAndFit(w_sizer);
fit_to_content(); // initial size only; the dialog is resizable
+1
View File
@@ -113,6 +113,7 @@ constexpr std::array<ShortcutInfo, size_t(Shortcut::Count)> shortcut_table = {{
SHORTCUT(GizmoMeasure, "gizmo_measure", L("Gizmo measure"), PLATER, { 'U' }),
SHORTCUT(GizmoAssembly, "gizmo_assembly", L("Gizmo assemble"), PLATER, { 'Y' }),
SHORTCUT(GizmoBrimEars, "gizmo_brim_ears", L("Gizmo brim ears"), PLATER, { 'E' }),
SHORTCUT(GizmoDisplacement, "gizmo_displacement", L("Gizmo texture displacement painting"), PLATER, { 'D' }),
// Sliders
SHORTCUT(GoToLayer, "go_to_layer", L("Jump to layer"), PREVIEW, { 'G', SHIFT }),
+1 -1
View File
@@ -38,7 +38,7 @@ enum class Shortcut : uint8_t {
MoveSelectionLeft, MoveSelectionRight, MoveSelectionUp, MoveSelectionDown, RotateSelectionLeft, RotateSelectionRight,
// Gizmos
GizmoMove, GizmoRotate, GizmoScale, GizmoFlatten, GizmoCut, GizmoMeshBoolean, GizmoFdmSupports, GizmoSeam, GizmoFuzzySkin,
GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars,
GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars, GizmoDisplacement,
// Sliders
GoToLayer, LayerSliderUp, LayerSliderDown, MovesSliderLeft, MovesSliderRight, MovesSliderStart, MovesSliderEnd,
// Painting tools
+18 -11
View File
@@ -4,7 +4,6 @@
#include "slic3r/GUI/GUI_Utils.hpp"
#include "slic3r/GUI/Widgets/StateColor.hpp"
#include "slic3r/GUI/Widgets/Label.hpp"
#include "slic3r/GUI/Widgets/HyperLink.hpp"
#include "slic3r/GUI/MsgDialog.hpp"
#include "libslic3r/LocalesUtils.hpp"
#include "libslic3r/Thread.hpp"
@@ -26,6 +25,7 @@
#include <chrono>
#include <wx/string.h>
#include <wx/textctrl.h>
#include <wx/utils.h>
#include <wx/valtext.h>
using namespace Slic3r;
@@ -131,19 +131,14 @@ StepMeshDialog::StepMeshDialog(wxWindow* parent, Slic3r::Step& file, double line
auto tip_frame = new RoundedRectangle(this, StateColor::darkModeColorFor(wxColour("#F1F1F1")), wxDefaultPosition, wxSize(-1,-1), 6, 0);
wxBoxSizer* tips_sizer = new wxBoxSizer(wxVERTICAL);
wxBoxSizer* tips_sizer = new wxBoxSizer(wxHORIZONTAL);
wxStaticText* info = new wxStaticText(tip_frame, wxID_ANY, _L("Smaller linear and angular deflections result in higher-quality transformations but increase the processing time."));
info->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
info->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F1F1F1")));
info->SetFont(::Label::Body_14);
info->Wrap(FromDIP(450));
// ORCA standardized HyperLink
HyperLink *tips = new HyperLink(tip_frame, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/import_export#step");
tips->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F1F1F1")));
tips_sizer->Add(info, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(10));
tips_sizer->Add(tips, 0, wxALL, FromDIP(10));
tips_sizer->Add(info, 0, wxEXPAND |wxALL , FromDIP(10));
tip_frame->SetSizer(tips_sizer);
tip_frame->Layout();
@@ -289,12 +284,24 @@ StepMeshDialog::StepMeshDialog(wxWindow* parent, Slic3r::Step& file, double line
save_default_sizer->Add(m_save_default_checkbox, 0, wxALIGN_LEFT);
bSizer->Add(save_default_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, LEFT_RIGHT_PADING);
wxBoxSizer* bSizer_button = new wxBoxSizer(wxHORIZONTAL);
bSizer_button->SetMinSize(wxSize(FromDIP(100), -1));
bSizer->AddSpacer(FromDIP(10));
m_checkbox = new wxCheckBox(this, wxID_ANY, _L("Don't show again"), wxDefaultPosition, wxDefaultSize, 0);
m_checkbox->SetFont(::Label::Body_14);
m_checkbox->SetForegroundColour(StateColor::darkModeColorFor(FONT_COLOR));
bSizer_button->Add(m_checkbox, 0, wxALIGN_LEFT | wxLEFT | wxALIGN_CENTER_VERTICAL, LEFT_RIGHT_PADING);
bSizer->Add(m_checkbox, 0, wxLEFT, LEFT_RIGHT_PADING);
wxBoxSizer* bSizer_button = new wxBoxSizer(wxHORIZONTAL);
// ORCA standardized HyperLink
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
auto wiki_url = "https://www.orcaslicer.com/wiki/import_export#step";
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
wiki_btn->SetCanFocus(false);
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
bSizer_button->Add(wiki_btn, 0, wxALIGN_LEFT | wxLEFT | wxALIGN_CENTER_VERTICAL, LEFT_RIGHT_PADING);
bSizer_button->AddStretchSpacer(1);
auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"});
+18 -3
View File
@@ -236,7 +236,19 @@ TroubleshootDialog::TroubleshootDialog()
Fit();
});
auto link_wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/troubleshoot_center");
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
auto wiki_url = "https://www.orcaslicer.com/wiki/troubleshoot_center";
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
wiki_btn->SetCanFocus(false);
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
auto video_btn = new Button(this, "", "toolbar_video_guide", 0, 15);
auto video_url = "https://www.youtube.com/watch?v=CFzt8W7OCx0";
video_btn->SetToolTip(_L("Video Guide") + "\n" + video_url);
video_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
video_btn->SetCanFocus(false);
video_btn->Bind(wxEVT_LEFT_DOWN, ([video_url](auto& e) {wxLaunchDefaultBrowser(video_url);}));
// RIGHT SIZER //////////////////////
@@ -367,6 +379,10 @@ TroubleshootDialog::TroubleshootDialog()
sys_btn_sizer->AddStretchSpacer();
sys_btn_sizer->Add(sys_copy_btn, 0, wxLEFT | wxRIGHT, FromDIP(5));
wxBoxSizer *link_btn_sizer = new wxBoxSizer(wxHORIZONTAL);
link_btn_sizer->Add(wiki_btn);
link_btn_sizer->Add(video_btn, 0, wxLEFT, FromDIP(10));
left_sizer->Add(m_header_logo , 0, wxEXPAND | wxALIGN_CENTER);
left_sizer->Add(logo_line , 0, wxEXPAND | wxTOP, FromDIP(12));
left_sizer->Add(version , 0, wxEXPAND | wxTOP, FromDIP(6));
@@ -374,8 +390,7 @@ TroubleshootDialog::TroubleshootDialog()
left_sizer->Add(sys_panel , 0, wxEXPAND | wxTOP, FromDIP(15));
left_sizer->AddStretchSpacer();
left_sizer->Add(sys_btn_sizer , 0, wxEXPAND | wxTOP, FromDIP(15));
left_sizer->Add(link_wiki , 0, wxALIGN_CENTER | wxTOP, FromDIP(15));
left_sizer->AddSpacer(FromDIP(5));
left_sizer->Add(link_btn_sizer , 0, wxALIGN_CENTER | wxTOP, FromDIP(15));
wxBoxSizer *right_sizer = new wxBoxSizer(wxVERTICAL);
+138 -30
View File
@@ -199,6 +199,7 @@ 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);
@@ -750,10 +751,47 @@ 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();
@@ -932,7 +970,7 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
}
m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan;
}
CaptureMouse();
grab_mouse();
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);
@@ -942,12 +980,16 @@ 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());
CaptureMouse();
grab_mouse();
} else if (type == wxEVT_MIDDLE_DOWN) {
m_gesture = Gesture::Pan;
m_drag_last_px = pos;
CaptureMouse();
grab_mouse();
} 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();
@@ -1735,7 +1777,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_size_dip(size_dip), m_tip(tip)
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetToolTip(tip);
@@ -1748,7 +1790,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 (IsEnabled()) {
if (usable()) {
m_pressed = true;
Refresh();
}
@@ -1757,7 +1799,7 @@ public:
const bool was_pressed = m_pressed;
m_pressed = false;
Refresh();
if (!was_pressed || !IsEnabled() || !GetClientRect().Contains(e.GetPosition()))
if (!was_pressed || !usable() || !GetClientRect().Contains(e.GetPosition()))
return;
if (m_toggle)
m_on = !m_on;
@@ -1797,6 +1839,30 @@ 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
@@ -1815,7 +1881,7 @@ private:
const PaneColors c = PaneColors::current();
const wxRect r = GetClientRect();
const wxColour teal(0x00, 0x96, 0x88);
const bool enabled = IsEnabled();
const bool enabled = usable();
wxColour fill = c.bg, border = c.frame, text = c.ink;
if (m_accent) {
@@ -1875,6 +1941,8 @@ 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;
@@ -1920,6 +1988,9 @@ 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);
@@ -1973,9 +2044,19 @@ 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 - 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);
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);
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 "
@@ -1996,7 +2077,11 @@ 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 (Home)"), false);
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);
strip->AddSpacer(FromDIP(4));
m_canvas = new UVEditorCanvas(this);
@@ -2007,6 +2092,7 @@ 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);
@@ -2082,19 +2168,25 @@ void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
} else {
m_thumb->SetBitmap(wxNullBitmap);
}
m_thumb->Enable(s.has_layer);
// 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);
for (int i = 0; i < 3; ++i) {
m_background[i]->SetValue(int(s.background) == i);
m_background[i]->Enable(s.has_layer);
m_background[i]->SetUnavailable(no_layer);
}
m_unwrap->Enable(s.has_layer);
m_unwrap->SetUnavailable(no_layer);
m_unwrap->SetBadge(s.unwrap_stale);
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."));
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."));
if (m_seam_angle->GetValue() != int(std::lround(s.seam_angle_deg)))
m_seam_angle->SetValue(int(std::lround(s.seam_angle_deg)));
@@ -2103,15 +2195,26 @@ void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
m_connect->Enable(s.has_layer);
m_mark_seams->SetValue(s.mark_seams);
m_mark_seams->Enable(s.has_layer);
m_mark_seams->SetUnavailable(no_layer);
m_seam_path->SetValue(s.seam_path);
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_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_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();
@@ -2121,19 +2224,24 @@ 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]->Enable(has_islands);
m_select[i]->SetUnavailable(not_unwrapped);
}
const bool island_picked = has_islands && m_canvas->select_mode() == UVEditorCanvas::SelectMode::Island &&
m_canvas->selected_island() >= 0;
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);
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_snap->SetValue(m_canvas->snap_enabled());
m_frame->Enable(has_islands);
m_frame->SetUnavailable(not_unwrapped);
}
void UVEditorPanel::on_tool(wxCommandEvent &evt)
+11
View File
@@ -200,6 +200,7 @@ 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
@@ -244,6 +245,16 @@ 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.
+9 -3
View File
@@ -40,6 +40,7 @@
#include <wx/textctrl.h>
#include <wx/settings.h>
#include <wx/tokenzr.h>
#include <wx/utils.h>
#include <wx/variant.h>
#include <wx/window.h>
#include <wx/toplevel.h>
@@ -62,7 +63,6 @@
#include "PresetComboBoxes.hpp"
#include "Widgets/CheckBox.hpp"
#include "Widgets/DialogButtons.hpp"
#include "Widgets/HyperLink.hpp"
#ifdef __linux__
#define wxLinux true
@@ -1011,8 +1011,14 @@ void UnsavedChangesDialog::build(Preset::Type type, PresetCollection *dependent_
checkbox_sizer->Show(bool(m_buttons & REMEMBER_CHOISE));
if (dependent_presets != nullptr) {
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/transfer_discard_changes");
m_sizer_button->Add(wiki, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(22));
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
auto wiki_url = "https://www.orcaslicer.com/wiki/transfer_discard_changes";
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
wiki_btn->SetCanFocus(false);
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
m_sizer_button->Add(wiki_btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(22));
}
m_sizer_button->Add(0, 0, 1, 0, 0);
+6
View File
@@ -223,6 +223,12 @@ void Button::SetStyle(const ButtonStyle style, const ButtonType type)
this->SetMinSize(FromDIP(wxSize(26, 26)));
this->SetSize(FromDIP(wxSize(26, 26)));
this->SetCornerRadius(this->FromDIP(4));
} else if (type == ButtonType::Circle) {
this->SetPaddingSize(FromDIP(wxSize(6, 6)));
this->SetMinSize(FromDIP(wxSize(25, 25)));
this->SetMaxSize(FromDIP(wxSize(25, 25)));
this->SetSize(FromDIP(wxSize(25, 25)));
this->SetCornerRadius(this->FromDIP(12));
} else if (type == ButtonType::Expanded) {
this->SetMinSize(FromDIP(wxSize(-1, 32)));
this->SetPaddingSize(FromDIP(wxSize(12, 8)));
+1
View File
@@ -34,6 +34,7 @@ enum class ButtonType {
Parameter, // Font14 Semi-Rounded For buttons that near parameter boxes
Icon, // ------ Semi-Rounded For buttons that only has icons. icons should be 16x16 and iconSize has to be defined as 16 while
// creation of button
Circle, // ------ FullyRounded Same as icon
Expanded, // Font14 Semi-Rounded For full length buttons. ex. buttons in static box
};
-19
View File
@@ -69,10 +69,6 @@ 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
@@ -405,21 +401,6 @@ bool WebView::NeedsRecreateOnShow()
return false;
#endif
}
bool WebView::WebKitAtLeast(int major, int minor)
{
#if defined(__linux__)
const unsigned int running_major = webkit_get_major_version();
if (running_major != static_cast<unsigned int>(major))
return running_major > static_cast<unsigned int>(major);
return webkit_get_minor_version() >= static_cast<unsigned int>(minor);
#else
(void) major;
(void) minor;
return false;
#endif
}
#if wxUSE_WEBVIEW_EDGE
bool WebView::CheckWebViewRuntime()
{
-4
View File
@@ -30,10 +30,6 @@ 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();
};
+83 -19
View File
@@ -12,6 +12,7 @@
#include <cstddef>
#include "slic3r/GUI/GUI_Utils.hpp"
#include <wx/colour.h>
#include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/Widgets/Label.hpp"
#include "slic3r/GUI/Widgets/LabeledStaticBox.hpp"
#include "slic3r/GUI/Widgets/RadioGroup.hpp"
@@ -25,7 +26,6 @@
#include <wx/dcgraph.h>
#include "MainFrame.hpp"
#include "Widgets/DialogButtons.hpp"
#include "Widgets/HyperLink.hpp"
#include <string>
#include <vector>
#include <cmath>
@@ -35,6 +35,7 @@
#include <wx/dialog.h>
#include <wx/valtext.h>
#include <wx/tglbtn.h>
#include <wx/utils.h>
#include <wx/event.h>
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Flow.hpp"
@@ -237,8 +238,15 @@ PA_Calibration_Dlg::PA_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plater*
auto dlg_btns = new DialogButtons(this, {"OK"});
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/pressure_advance_calib");
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
auto wiki_url = "https://www.orcaslicer.com/wiki/pressure_advance_calib";
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
wiki_btn->SetCanFocus(false);
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
bottom_sizer->AddStretchSpacer();
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
@@ -459,8 +467,15 @@ Temp_Calibration_Dlg::Temp_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plat
auto dlg_btns = new DialogButtons(this, {"OK"});
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/temp_calib");
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
auto wiki_url = "https://www.orcaslicer.com/wiki/temp_calib";
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
wiki_btn->SetCanFocus(false);
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
bottom_sizer->AddStretchSpacer();
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
@@ -642,8 +657,15 @@ MaxVolumetricSpeed_Test_Dlg::MaxVolumetricSpeed_Test_Dlg(wxWindow* parent, wxWin
auto dlg_btns = new DialogButtons(this, {"OK"});
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/volumetric_speed_calib");
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
auto wiki_url = "https://www.orcaslicer.com/wiki/volumetric_speed_calib";
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
wiki_btn->SetCanFocus(false);
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
bottom_sizer->AddStretchSpacer();
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
@@ -766,8 +788,15 @@ VFA_Test_Dlg::VFA_Test_Dlg(wxWindow* parent, wxWindowID id, Plater* plater)
auto dlg_btns = new DialogButtons(this, {"OK"});
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/vfa_calib");
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
auto wiki_url = "https://www.orcaslicer.com/wiki/vfa_calib";
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
wiki_btn->SetCanFocus(false);
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
bottom_sizer->AddStretchSpacer();
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
@@ -1005,8 +1034,15 @@ Retraction_Test_Dlg::Retraction_Test_Dlg(wxWindow* parent, wxWindowID id, Plater
auto dlg_btns = new DialogButtons(this, {"OK"});
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/retraction_calib");
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
auto wiki_url = "https://www.orcaslicer.com/wiki/retraction_calib";
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
wiki_btn->SetCanFocus(false);
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
bottom_sizer->AddStretchSpacer();
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
@@ -1185,8 +1221,15 @@ Input_Shaping_Freq_Test_Dlg::Input_Shaping_Freq_Test_Dlg(wxWindow* parent, wxWin
auto dlg_btns = new DialogButtons(this, {"OK"});
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/input_shaping_calib");
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
auto wiki_url = "https://www.orcaslicer.com/wiki/input_shaping_calib";
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
wiki_btn->SetCanFocus(false);
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
bottom_sizer->AddStretchSpacer();
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
@@ -1384,8 +1427,15 @@ Input_Shaping_Damp_Test_Dlg::Input_Shaping_Damp_Test_Dlg(wxWindow* parent, wxWin
auto dlg_btns = new DialogButtons(this, {"OK"});
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/input_shaping_calib");
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
auto wiki_url = "https://www.orcaslicer.com/wiki/input_shaping_calib";
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
wiki_btn->SetCanFocus(false);
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
bottom_sizer->AddStretchSpacer();
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
@@ -1580,8 +1630,15 @@ Cornering_Test_Dlg::Cornering_Test_Dlg(wxWindow* parent, wxWindowID id, Plater*
auto dlg_btns = new DialogButtons(this, {"OK"});
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/cornering_calib");
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
auto wiki_url = "https://www.orcaslicer.com/wiki/cornering_calib";
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
wiki_btn->SetCanFocus(false);
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
bottom_sizer->AddStretchSpacer();
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
@@ -1719,8 +1776,15 @@ FlowRateCalibrationDialog::FlowRateCalibrationDialog(wxWindow* parent, wxWindowI
auto dlg_btns = new DialogButtons(this, {"OK"});
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/flow_ratio_calib");
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
auto wiki_url = "https://www.orcaslicer.com/wiki/flow_ratio_calib";
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
wiki_btn->SetCanFocus(false);
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
bottom_sizer->AddStretchSpacer();
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
+3 -4
View File
@@ -947,13 +947,12 @@ std::string Http::tls_global_init()
std::string Http::tls_system_cert_store()
{
std::string ret;
#ifdef OPENSSL_CERT_OVERRIDE
ret = ::getenv(X509_get_default_cert_file_env());
if (const char *cert_file = ::getenv(X509_get_default_cert_file_env()))
return cert_file;
#endif
return ret;
return {};
}
std::string Http::url_encode(const std::string &str)
+17 -6
View File
@@ -25,11 +25,19 @@ extern const char* const INSTALL_STATE_FILE;
// Plugin config and orca.host.ui payloads both cross the boundary as plain JSON-compatible
// values, so both go through these.
inline pybind11::object json_to_py(const nlohmann::json& j)
// Maximum nesting depth for JSON <-> Python conversion. A self-referential or pathologically
// deep value would otherwise recurse until the native C stack overflows, an uncatchable crash;
// past this bound we raise instead. 200 is far beyond any legitimate plugin config or UI payload.
inline constexpr int kMaxJsonConversionDepth = 200;
inline pybind11::object json_to_py(const nlohmann::json& j, int depth = 0)
{
namespace py = pybind11;
using json = nlohmann::json;
if (depth > kMaxJsonConversionDepth)
throw py::value_error("Plugin JSON value nested too deeply");
switch (j.type()) {
case json::value_t::null: return py::none();
case json::value_t::boolean: return py::bool_(j.get<bool>());
@@ -40,24 +48,27 @@ inline pybind11::object json_to_py(const nlohmann::json& j)
case json::value_t::array: {
py::list lst;
for (const auto& e : j)
lst.append(json_to_py(e));
lst.append(json_to_py(e, depth + 1));
return lst;
}
case json::value_t::object: {
py::dict d;
for (auto it = j.begin(); it != j.end(); ++it)
d[py::str(it.key())] = json_to_py(it.value());
d[py::str(it.key())] = json_to_py(it.value(), depth + 1);
return d;
}
default: return py::none();
}
}
inline nlohmann::json py_to_json(const pybind11::handle& o)
inline nlohmann::json py_to_json(const pybind11::handle& o, int depth = 0)
{
namespace py = pybind11;
using json = nlohmann::json;
if (depth > kMaxJsonConversionDepth)
throw py::value_error("Plugin value nested too deeply (possible cycle)");
if (o.is_none())
return json(nullptr);
if (py::isinstance<py::bool_>(o)) // bool before int (bool subclasses int in Python)
@@ -73,13 +84,13 @@ inline nlohmann::json py_to_json(const pybind11::handle& o)
if (py::isinstance<py::dict>(o)) {
json obj = json::object();
for (auto item : py::reinterpret_borrow<py::dict>(o))
obj[py::str(item.first).cast<std::string>()] = py_to_json(item.second);
obj[py::str(item.first).cast<std::string>()] = py_to_json(item.second, depth + 1);
return obj;
}
if (py::isinstance<py::list>(o) || py::isinstance<py::tuple>(o)) {
json arr = json::array();
for (auto e : o)
arr.push_back(py_to_json(e));
arr.push_back(py_to_json(e, depth + 1));
return arr;
}
return py::str(o).cast<std::string>(); // fallback: str()
+7
View File
@@ -11,6 +11,13 @@ endif ()
add_test(NAME cli_strict_mode
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_strict.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON})
add_test(NAME cli_empty_project_config
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_empty_project_config.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON} ${CMAKE_SOURCE_DIR})
set_tests_properties(cli_empty_project_config PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
TIMEOUT 300)
set_tests_properties(cli_strict_mode PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Regression check: a 3mf whose Metadata/project_settings.config carries no settings must load.
#
# The CLI reads printable_height out of the project config with opt_float(), which dereferences
# what option<>() returns. With create = false that is null when the key is absent, so a project
# saved without settings used to take the CLI down with a segfault. Both models in
# resources/handy_models are such files.
#
# usage: test_cli_empty_project_config.sh <orca-slicer binary> <python3> <source dir>
set -u
BIN="${1:-}"
PY="${2:-python3}"
SRC="${3:-}"
# 77 is the test's SKIP_RETURN_CODE.
[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; }
[ -f "$SRC/resources/handy_models/OrcaBadge.3mf" ] || { echo "SKIP: handy model not found"; exit 77; }
WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-emptycfg.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/datadir"
# Rewrite the project settings to an empty object, so the test holds no matter what the shipped
# models carry later on.
cp "$SRC/resources/handy_models/OrcaBadge.3mf" "$WORK/empty_config.3mf"
"$PY" - "$WORK/empty_config.3mf" <<'PYEOF'
import shutil, sys, zipfile
path = sys.argv[1]
entry = "Metadata/project_settings.config"
with zipfile.ZipFile(path) as src:
items = [(i, src.read(i.filename)) for i in src.infolist()]
with zipfile.ZipFile(path + ".new", "w", zipfile.ZIP_DEFLATED) as dst:
seen = False
for info, data in items:
if info.filename == entry:
data, seen = b"{\n}\n", True
dst.writestr(info, data)
if not seen:
dst.writestr(entry, b"{\n}\n")
shutil.move(path + ".new", path)
PYEOF
"$BIN" --datadir "$WORK/datadir" --info "$WORK/empty_config.3mf" > "$WORK/info.txt" 2>&1
rc=$?
if [ $rc -ne 0 ]; then
echo "FAIL: --info on a project with empty settings exited $rc"
tail -20 "$WORK/info.txt"
exit 1
fi
grep -q "size_x" "$WORK/info.txt" || { echo "FAIL: --info printed no geometry"; cat "$WORK/info.txt"; exit 1; }
echo "PASS: a project with empty settings loads"
+166
View File
@@ -0,0 +1,166 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('Open CASCADE Model'),'2;1');
FILE_NAME('Open CASCADE Shape Model','2026-10-08T09:57:33',('Author'),(
'Open CASCADE'),'Open CASCADE STEP processor 8.0','Open CASCADE 8.0'
,'Unknown');
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
ENDSEC;
DATA;
#1 = APPLICATION_PROTOCOL_DEFINITION('international standard',
'automotive_design',2000,#2);
#2 = APPLICATION_CONTEXT(
'core data for automotive mechanical design processes');
#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10);
#4 = PRODUCT_DEFINITION_SHAPE('','',#5);
#5 = PRODUCT_DEFINITION('design','',#6,#9);
#6 = PRODUCT_DEFINITION_FORMATION('','',#7);
#7 = PRODUCT('Open CASCADE STEP translator 8.0 1',
'Open CASCADE STEP translator 8.0 1','',(#8));
#8 = PRODUCT_CONTEXT('',#2,'mechanical');
#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#15),#121);
#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14);
#12 = CARTESIAN_POINT('',(0.,0.,0.));
#13 = DIRECTION('',(0.,0.,1.));
#14 = DIRECTION('',(1.,0.,-0.));
#15 = MANIFOLD_SOLID_BREP('',#16);
#16 = CLOSED_SHELL('',(#17,#113,#117));
#17 = ADVANCED_FACE('',(#18),#31,.T.);
#18 = FACE_BOUND('',#19,.T.);
#19 = EDGE_LOOP('',(#20,#58,#81,#112));
#20 = ORIENTED_EDGE('',*,*,#21,.F.);
#21 = EDGE_CURVE('',#22,#22,#24,.T.);
#22 = VERTEX_POINT('',#23);
#23 = CARTESIAN_POINT('',(-36.4,0.,25.));
#24 = SURFACE_CURVE('',#25,(#30,#42),.PCURVE_S1.);
#25 = CIRCLE('',#26,36.4);
#26 = AXIS2_PLACEMENT_3D('',#27,#28,#29);
#27 = CARTESIAN_POINT('',(0.,0.,25.));
#28 = DIRECTION('',(0.,0.,1.));
#29 = DIRECTION('',(1.,0.,-0.));
#30 = PCURVE('',#31,#36);
#31 = CONICAL_SURFACE('',#32,36.4,0.785398163397);
#32 = AXIS2_PLACEMENT_3D('',#33,#34,#35);
#33 = CARTESIAN_POINT('',(0.,0.,25.));
#34 = DIRECTION('',(0.,0.,1.));
#35 = DIRECTION('',(1.,0.,-0.));
#36 = DEFINITIONAL_REPRESENTATION('',(#37),#41);
#37 = LINE('',#38,#39);
#38 = CARTESIAN_POINT('',(-6.28318530718,3.552713678801E-15));
#39 = VECTOR('',#40,1.);
#40 = DIRECTION('',(1.,0.));
#41 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#42 = PCURVE('',#43,#48);
#43 = PLANE('',#44);
#44 = AXIS2_PLACEMENT_3D('',#45,#46,#47);
#45 = CARTESIAN_POINT('',(0.,0.,25.));
#46 = DIRECTION('',(-0.,-0.,-1.));
#47 = DIRECTION('',(-1.,0.,0.));
#48 = DEFINITIONAL_REPRESENTATION('',(#49),#57);
#49 = ( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#50,#51,#52,#53,#54,#55,#56),
.UNSPECIFIED.,.T.,.F.) B_SPLINE_CURVE_WITH_KNOTS((1,2,2,2,2,1),(
-2.094395102393,0.,2.094395102393,4.188790204786,6.28318530718,
8.377580409573),.UNSPECIFIED.) CURVE() GEOMETRIC_REPRESENTATION_ITEM()
RATIONAL_B_SPLINE_CURVE((1.,0.5,1.,0.5,1.,0.5,1.)) REPRESENTATION_ITEM(
'') );
#50 = CARTESIAN_POINT('',(-36.4,0.));
#51 = CARTESIAN_POINT('',(-36.4,63.046649395507));
#52 = CARTESIAN_POINT('',(18.2,31.523324697754));
#53 = CARTESIAN_POINT('',(72.8,8.915428697793E-15));
#54 = CARTESIAN_POINT('',(18.2,-31.52332469775));
#55 = CARTESIAN_POINT('',(-36.4,-63.0466493955));
#56 = CARTESIAN_POINT('',(-36.4,0.));
#57 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#58 = ORIENTED_EDGE('',*,*,#59,.T.);
#59 = EDGE_CURVE('',#22,#60,#62,.T.);
#60 = VERTEX_POINT('',#61);
#61 = CARTESIAN_POINT('',(-11.4,0.,3.552713678801E-15));
#62 = SEAM_CURVE('',#63,(#67,#74),.PCURVE_S1.);
#63 = LINE('',#64,#65);
#64 = CARTESIAN_POINT('',(-36.4,0.,25.));
#65 = VECTOR('',#66,1.);
#66 = DIRECTION('',(0.707106781187,8.659560562349E-17,-0.707106781187));
#67 = PCURVE('',#31,#68);
#68 = DEFINITIONAL_REPRESENTATION('',(#69),#73);
#69 = LINE('',#70,#71);
#70 = CARTESIAN_POINT('',(-3.14159265359,3.552713678801E-15));
#71 = VECTOR('',#72,1.);
#72 = DIRECTION('',(2.13162820728E-16,-1.));
#73 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#74 = PCURVE('',#31,#75);
#75 = DEFINITIONAL_REPRESENTATION('',(#76),#80);
#76 = LINE('',#77,#78);
#77 = CARTESIAN_POINT('',(3.14159265359,3.552713678801E-15));
#78 = VECTOR('',#79,1.);
#79 = DIRECTION('',(2.13162820728E-16,-1.));
#80 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#81 = ORIENTED_EDGE('',*,*,#82,.F.);
#82 = EDGE_CURVE('',#60,#60,#83,.T.);
#83 = SURFACE_CURVE('',#84,(#89,#96),.PCURVE_S1.);
#84 = CIRCLE('',#85,11.4);
#85 = AXIS2_PLACEMENT_3D('',#86,#87,#88);
#86 = CARTESIAN_POINT('',(0.,0.,3.552713678801E-15));
#87 = DIRECTION('',(0.,0.,-1.));
#88 = DIRECTION('',(1.,0.,0.));
#89 = PCURVE('',#31,#90);
#90 = DEFINITIONAL_REPRESENTATION('',(#91),#95);
#91 = LINE('',#92,#93);
#92 = CARTESIAN_POINT('',(6.28318530718,-25.));
#93 = VECTOR('',#94,1.);
#94 = DIRECTION('',(-1.,0.));
#95 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#96 = PCURVE('',#97,#102);
#97 = PLANE('',#98);
#98 = AXIS2_PLACEMENT_3D('',#99,#100,#101);
#99 = CARTESIAN_POINT('',(0.,0.,3.552713678801E-15));
#100 = DIRECTION('',(0.,0.,1.));
#101 = DIRECTION('',(1.,0.,-0.));
#102 = DEFINITIONAL_REPRESENTATION('',(#103),#111);
#103 = ( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#104,#105,#106,#107,#108,#109
,#110),.UNSPECIFIED.,.T.,.F.) B_SPLINE_CURVE_WITH_KNOTS((1,2,2,2,2,1),(
-2.094395102393,0.,2.094395102393,4.188790204786,6.28318530718,
8.377580409573),.UNSPECIFIED.) CURVE() GEOMETRIC_REPRESENTATION_ITEM()
RATIONAL_B_SPLINE_CURVE((1.,0.5,1.,0.5,1.,0.5,1.)) REPRESENTATION_ITEM(
'') );
#104 = CARTESIAN_POINT('',(11.4,0.));
#105 = CARTESIAN_POINT('',(11.4,-19.74537920628));
#106 = CARTESIAN_POINT('',(-5.7,-9.872689603143));
#107 = CARTESIAN_POINT('',(-22.8,-2.792194702056E-15));
#108 = CARTESIAN_POINT('',(-5.7,9.872689603143));
#109 = CARTESIAN_POINT('',(11.4,19.745379206285));
#110 = CARTESIAN_POINT('',(11.4,0.));
#111 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#112 = ORIENTED_EDGE('',*,*,#59,.F.);
#113 = ADVANCED_FACE('',(#114),#43,.F.);
#114 = FACE_BOUND('',#115,.F.);
#115 = EDGE_LOOP('',(#116));
#116 = ORIENTED_EDGE('',*,*,#21,.F.);
#117 = ADVANCED_FACE('',(#118),#97,.F.);
#118 = FACE_BOUND('',#119,.F.);
#119 = EDGE_LOOP('',(#120));
#120 = ORIENTED_EDGE('',*,*,#82,.F.);
#121 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#125)) GLOBAL_UNIT_ASSIGNED_CONTEXT
((#122,#123,#124)) REPRESENTATION_CONTEXT('Context #1',
'3D Context with UNIT and UNCERTAINTY') );
#122 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
#123 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
#124 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
#125 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#122,
'distance_accuracy_value','confusion accuracy');
#126 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7));
ENDSEC;
END-ISO-10303-21;
+36 -3
View File
@@ -1780,7 +1780,7 @@ TEST_CASE("Sparse plane-path anchors match the printed infill", "[Fill][Internal
const AABBTreeLines::LinesDistancer<Line> printed_tree(to_lines(printed));
// Orca: Exclude perimeter connections: anchoring and extrusion can trim those differently.
const Polylines anchors = intersection_pl(layer.generate_sparse_infill_polylines_for_anchoring(nullptr, nullptr, nullptr),
const Polylines anchors = intersection_pl(layer.generate_sparse_infill_polylines_for_anchoring(nullptr, nullptr),
shrink(to_polygons(layer.lslices), scale_(3.)));
REQUIRE_FALSE(anchors.empty());
double max_distance = 0.;
@@ -1792,8 +1792,9 @@ TEST_CASE("Sparse plane-path anchors match the printed infill", "[Fill][Internal
CHECK(unscale<double>(max_distance) <= config.opt_float("resolution"));
}
// Orca: Slices the meshes as the parts of one object, where they are.
static Print &slice_parts(Print &print, DynamicPrintConfig config, const std::vector<TriangleMesh> &parts)
// Orca: Slices the meshes as the parts of one object, where they are, with modifiers of their own config.
static Print &slice_parts(Print &print, DynamicPrintConfig config, const std::vector<TriangleMesh> &parts,
const std::vector<std::pair<TriangleMesh, DynamicPrintConfig>> &modifiers = {})
{
config.set_deserialize_strict({{"layer_height", 0.2},
{"initial_layer_print_height", 0.2},
@@ -1804,6 +1805,8 @@ static Print &slice_parts(Print &print, DynamicPrintConfig config, const std::ve
Slic3r::Test::init_print({parts.front()}, print, model, config, nullptr, false);
for (size_t i = 1; i < parts.size(); ++ i)
model.objects.front()->add_volume(TriangleMesh(parts[i]), ModelVolumeType::MODEL_PART, false);
for (const auto &[mesh, modifier_config] : modifiers)
model.objects.front()->add_volume(TriangleMesh(mesh), ModelVolumeType::PARAMETER_MODIFIER, false)->config.apply(modifier_config);
print.apply(model, config);
print.process();
return print;
@@ -1988,3 +1991,33 @@ TEST_CASE("Adaptive infill fills each body like the body sliced alone", "[Fill][
CHECK(unmatched.first < 0.02);
CHECK(unmatched.second < 0.02);
}
TEST_CASE("Adaptive infill of a modifier leaves the density of the other regions", "[Fill][Regression]")
{
const std::string pattern = GENERATE("adaptivecubic", "supportcubic");
CAPTURE(pattern);
auto config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "15%"},
{"top_shell_layers", 0},
{"bottom_shell_layers", 0}});
TriangleMesh bodies = make_cube(30, 30, 6), second = make_cube(30, 30, 6);
second.translate(40, 0, 0);
bodies.merge(second);
// Orca: A denser modifier over the right half of the second body.
TriangleMesh modifier = make_cube(20, 40, 10);
modifier.translate(55, -5, -2);
DynamicPrintConfig dense = config;
dense.set_deserialize_strict({{"sparse_infill_density", "60%"}});
Print print, print_sparse, print_dense;
slice_parts(print, config, {bodies}, {{modifier, dense}});
slice_parts(print_sparse, config, {bodies});
slice_parts(print_dense, dense, {bodies});
// Orca: Bed regions 3 mm inside the walls and the modifier, away from the links along them.
auto rect = [](double x0, double y0, double x1, double y1) {
return Polygon({Point::new_scale(x0, y0), Point::new_scale(x1, y0), Point::new_scale(x1, y1), Point::new_scale(x0, y1)});
};
CHECK(unmatched_between_prints(print, print_sparse, erInternalInfill, {rect(3, 3, 27, 27), rect(43, 3, 52, 27)}) < 0.02);
CHECK(unmatched_between_prints(print, print_dense, erInternalInfill, {rect(58, 3, 67, 27)}) < 0.02);
}
+96
View File
@@ -14,9 +14,14 @@
#include "test_helpers.hpp"
#include "test_utils.hpp"
#include <boost/filesystem/operations.hpp>
#include <boost/nowide/fstream.hpp>
#include <algorithm>
#include <cstddef>
#include <fstream>
#include <ios>
#include <iterator>
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Point.hpp"
#include <sstream>
@@ -221,3 +226,94 @@ TEST_CASE("Line ends of the exported G-code mark every newline in the file", "[G
INFO("first difference at line " << difference.first - result.lines_ends.begin() + 1);
CHECK(difference.first == result.lines_ends.end());
}
namespace {
// Writes the bytes verbatim: the G-code export is binary, so no newline translation must creep in
// here either, otherwise the CRLF cases below would silently test LF.
void write_gcode(const std::string &path, const std::string &content)
{
boost::nowide::ofstream f(path, std::ios::binary);
f << content;
}
// Cuts line `id` (1-based) out of the file the way GCodeViewer's G-code window does: the text it
// shows is bytes [lines_ends[id - 2], lines_ends[id - 1]). Reproducing that here is what makes these
// assertions about the rendered result rather than about the offsets themselves.
std::string line_at(const std::string &path, const std::vector<size_t> &lines_ends, size_t id)
{
boost::nowide::ifstream f(path, std::ios::binary);
const std::string bytes((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
const size_t start = id == 1 ? 0 : lines_ends[id - 2];
return bytes.substr(start, lines_ends[id - 1] - start);
}
} // namespace
TEST_CASE("rebuild_lines_ends realigns the line offsets after the G-code is rewritten in place", "[GCodeProcessor]")
{
ScopedTemporaryFile tmp(".gcode");
GCodeProcessorResult result;
result.filename = tmp.string();
write_gcode(tmp.string(), "G1 X1\nG1 X2\nG1 X3\n");
REQUIRE(result.rebuild_lines_ends());
REQUIRE(result.lines_ends.size() == 3);
CHECK(line_at(tmp.string(), result.lines_ends, 2) == "G1 X2\n");
// What a post-processing script that opens the file in Python text mode on Windows does: prepend a
// comment and translate every LF to CRLF. Every byte offset shifts, and cumulatively.
write_gcode(tmp.string(), ";EDITED\r\nG1 X1\r\nG1 X2\r\nG1 X3\r\n");
CHECK(line_at(tmp.string(), result.lines_ends, 2) != "G1 X1\r\n");
REQUIRE(result.rebuild_lines_ends());
REQUIRE(result.lines_ends.size() == 4);
CHECK(line_at(tmp.string(), result.lines_ends, 1) == ";EDITED\r\n");
CHECK(line_at(tmp.string(), result.lines_ends, 2) == "G1 X1\r\n");
CHECK(line_at(tmp.string(), result.lines_ends, 4) == "G1 X3\r\n");
}
TEST_CASE("rebuild_lines_ends maps a file longer than one read chunk", "[GCodeProcessor]")
{
ScopedTemporaryFile tmp(".gcode");
GCodeProcessorResult result;
result.filename = tmp.string();
const size_t line_count = 20000;
std::string content;
for (size_t i = 0; i < line_count; ++i)
content += "G1 X" + std::to_string(i) + "\n";
// The scan reads the file in chunks, so it has to span at least one chunk boundary to be meaningful.
REQUIRE(content.size() > 65536);
write_gcode(tmp.string(), content);
REQUIRE(result.rebuild_lines_ends());
REQUIRE(result.lines_ends.size() == line_count);
CHECK(result.lines_ends.back() == content.size());
CHECK(line_at(tmp.string(), result.lines_ends, line_count) == "G1 X" + std::to_string(line_count - 1) + "\n");
}
TEST_CASE("rebuild_lines_ends ignores a trailing line that has no newline", "[GCodeProcessor]")
{
ScopedTemporaryFile tmp(".gcode");
GCodeProcessorResult result;
result.filename = tmp.string();
// Matches the export pass, which only ever records the offset one past a '\n'.
write_gcode(tmp.string(), "G1 X1\nG1 X2");
REQUIRE(result.rebuild_lines_ends());
CHECK(result.lines_ends.size() == 1);
CHECK(line_at(tmp.string(), result.lines_ends, 1) == "G1 X1\n");
}
TEST_CASE("rebuild_lines_ends empties the map when the G-code file cannot be read", "[GCodeProcessor]")
{
// A partially rebuilt map would have the G-code window slicing lines at wrong offsets again; an
// empty one just hides the window.
GCodeProcessorResult result;
result.filename = (boost::filesystem::temp_directory_path() / "orca-nonexistent-gcode-file.gcode").string();
result.lines_ends = {1, 2, 3};
CHECK_FALSE(result.rebuild_lines_ends());
CHECK(result.lines_ends.empty());
}
+1
View File
@@ -36,6 +36,7 @@ add_executable(${_TEST_NAME}_tests
test_geometry.cpp
test_multimaterial_segmentation.cpp
test_placeholder_parser.cpp
test_png_read_write.cpp
test_polygon.cpp
test_mutable_polygon.cpp
test_mutable_priority_queue.cpp
+39 -1
View File
@@ -1,9 +1,11 @@
#include <catch2/catch_all.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/catch_approx.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <libslic3r/TriangleMesh.hpp>
#include <libslic3r/MeshBoolean.hpp>
#include <vector>
using namespace Slic3r;
@@ -24,3 +26,39 @@ TEST_CASE("CGAL and TriangleMesh conversions", "[MeshBoolean]") {
REQUIRE(! MeshBoolean::cgal::does_self_intersect(M));
}
TEST_CASE("mcut difference handles source splits between cuts", "[MeshBoolean]") {
TriangleMesh body = make_cube(30., 10., 10.);
TriangleMesh tool;
// First cut splits the source into two disconnected components.
TriangleMesh slab = make_cube(2., 12., 20.);
slab.translate(Vec3f(14.f, -1.f, -5.f));
its_merge(tool.its, slab.its);
// These cuts must still be applied after the source has been split.
TriangleMesh left_hole = make_cube(4., 4., 20.);
left_hole.translate(Vec3f(3.f, 3.f, -5.f));
its_merge(tool.its, left_hole.its);
TriangleMesh right_hole = make_cube(4., 4., 20.);
right_hole.translate(Vec3f(21.f, 3.f, -5.f));
its_merge(tool.its, right_hole.its);
std::vector<TriangleMesh> result;
MeshBoolean::mcut::make_boolean(body, tool, result, "A_NOT_B");
REQUIRE(result.size() == 1);
const std::vector<indexed_triangle_set> components =
its_split(result.front().its);
REQUIRE(components.size() == 2);
// 3000 - 200 - 160 - 160 = 2480.
REQUIRE_THAT(
result.front().volume(),
Catch::Matchers::WithinRel(2480., 1e-3)
);
}
+83
View File
@@ -0,0 +1,83 @@
#include <catch2/catch_all.hpp>
#include <cstdint>
#include <fstream>
#include <iterator>
#include <vector>
#include <boost/filesystem.hpp>
#include "libslic3r/PNGReadWrite.hpp"
using namespace Slic3r;
// libpng reports a corrupt or truncated file by longjmp()ing out of the decoder, so the decoders have
// to come back with false rather than crash or hand back a half filled image.
namespace {
// A real PNG, produced by the writer next door, so the bytes are a file libpng accepts.
std::vector<uint8_t> encoded_png(size_t w, size_t h)
{
std::vector<uint8_t> pixels(w * h);
for (size_t i = 0; i < pixels.size(); ++ i)
pixels[i] = uint8_t((i * 7) % 256);
const boost::filesystem::path path = boost::filesystem::temp_directory_path() /
boost::filesystem::unique_path("png_rw_%%%%%%%%.png");
REQUIRE(png::write_gray_to_file(path.string(), w, h, pixels));
std::vector<uint8_t> bytes;
{
std::ifstream ifs(path.string(), std::ios::binary);
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
boost::system::error_code ec;
boost::filesystem::remove(path, ec);
REQUIRE(bytes.size() > 64);
return bytes;
}
png::ReadBuf buf_of(const std::vector<uint8_t> &bytes, size_t size)
{
return png::ReadBuf{ bytes.data(), size };
}
} // namespace
TEST_CASE("A whole PNG decodes", "[PNG]") {
const std::vector<uint8_t> bytes = encoded_png(24, 16);
png::ImageGreyscale grey;
REQUIRE(png::decode_png(buf_of(bytes, bytes.size()), grey));
CHECK(grey.cols == 24);
CHECK(grey.rows == 16);
CHECK(grey.buf.size() == 24 * 16);
}
TEST_CASE("A truncated PNG is refused instead of crashing", "[PNG]") {
const std::vector<uint8_t> bytes = encoded_png(64, 64);
// Cut past the signature: inside the header, and inside the pixel data. Not in the trailing
// chunks - decode_png() does not read those, so a file missing only its IEND still decodes, and
// that is the pre-existing contract rather than anything this change touches.
const size_t size = GENERATE_COPY(size_t(16), size_t(40), bytes.size() / 2, bytes.size() * 3 / 4);
REQUIRE(size < bytes.size());
png::ImageGreyscale grey;
CHECK_FALSE(png::decode_png(buf_of(bytes, size), grey));
png::ImageColorscale colour;
CHECK_FALSE(png::decode_colored_png(buf_of(bytes, size), colour));
}
TEST_CASE("A PNG whose body is garbage is refused", "[PNG]") {
std::vector<uint8_t> bytes = encoded_png(32, 32);
// Keep the signature, scribble over everything after it.
for (size_t i = 8; i < bytes.size(); ++ i)
bytes[i] = uint8_t(0xA5);
png::ImageGreyscale grey;
CHECK_FALSE(png::decode_png(buf_of(bytes, bytes.size()), grey));
png::ImageColorscale colour;
CHECK_FALSE(png::decode_colored_png(buf_of(bytes, bytes.size()), colour));
}
+18
View File
@@ -8,6 +8,7 @@
#include <catch2/generators/catch_generators.hpp>
#include "libslic3r/Model.hpp"
#include "libslic3r/Format/STEP.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "test_utils.hpp"
using namespace Slic3r;
@@ -73,6 +74,23 @@ TEST_CASE("A security classification assignment does not crash import", "[Step]"
CHECK(model.objects.front()->volumes.front()->mesh().facets_count() == 4); // a tetrahedron
}
// The fixture is a truncated cone whose seam pcurves have the direction (2.1e-16, -1).
TEST_CASE("A cone with a slightly tilted seam imports as a closed mesh", "[Step]")
{
const std::string path = std::string(TEST_DATA_DIR) + PATH_SEPARATOR "cone_tilted_seam_pcurve.step";
Model model;
bool cancel = false;
Step step(path);
REQUIRE(step.load() == Step::Step_Status::LOAD_SUCCESS);
REQUIRE(step.mesh(&model, cancel, false) == Step::Step_Status::MESH_SUCCESS);
REQUIRE(model.objects.size() == 1);
REQUIRE(model.objects.front()->volumes.size() == 1);
CHECK(its_num_open_edges(model.objects.front()->volumes.front()->mesh().its) == 0);
}
TEST_CASE("isUtf8 recognises two, three and four byte sequences", "[Step]")
{
CHECK(StepPreProcessor::isUtf8("\xC3\xA9")); // U+00E9
+1
View File
@@ -34,6 +34,7 @@ add_executable(${_TEST_NAME}_tests
test_plugin_sort.cpp
test_plugin_cloud_metadata.cpp
test_plugin_audit.cpp
test_plugin_json_depth.cpp
test_shortcuts.cpp
test_file_url.cpp
test_user_manager.cpp
@@ -0,0 +1,73 @@
#include <catch2/catch_all.hpp>
#include <catch2/catch_test_macros.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <slic3r/plugin/PythonInterpreter.hpp>
#include "plugin_test_utils.hpp"
#include <cstdint>
#include <exception>
#include <utility>
#include <nlohmann/json.hpp>
#include <pybind11/embed.h>
#include <pybind11/gil.h>
#include <pybind11/pytypes.h>
using namespace Slic3r;
namespace {
// Brings the embedded interpreter up for one test and tears it down before boost::log does,
// mirroring the ScopedPluginManager idiom in the other plugin tests.
struct ScopedPluginManager
{
ScopedDataDir python_data_dir{"plugin-json-depth"};
bool initialized = PluginManager::instance().initialize();
~ScopedPluginManager()
{
PluginManager::instance().shutdown();
PythonInterpreter::instance().shutdown();
}
};
} // namespace
TEST_CASE("py_to_json raises instead of overflowing on pathologically deep input", "[PluginHost][Python]")
{
ScopedPluginManager manager;
REQUIRE(manager.initialized);
namespace py = pybind11;
py::gil_scoped_acquire gil;
// [[[ ... 0 ... ]]] nested 300 deep: past the 200 conversion-depth cap, but shallow enough
// that the pre-fix code returns without crashing, so a regression fails cleanly rather than
// taking the process down. Built in C++ so the test does not depend on Python builtins.
py::object deep = py::int_(0);
for (int i = 0; i < 300; ++i) {
py::list wrapper;
wrapper.append(deep);
deep = std::move(wrapper);
}
CHECK_THROWS_AS(py_to_json(deep), std::exception);
}
TEST_CASE("py_to_json still converts reasonably nested input", "[PluginHost][Python]")
{
ScopedPluginManager manager;
REQUIRE(manager.initialized);
namespace py = pybind11;
py::gil_scoped_acquire gil;
py::dict d;
d["a"] = py::int_(1);
py::list inner;
inner.append(py::str("x"));
inner.append(py::int_(2));
d["b"] = inner;
const nlohmann::json j = py_to_json(d);
CHECK(j.at("a").get<std::int64_t>() == 1);
CHECK(j.at("b").at(0).get<std::string>() == "x");
CHECK(j.at("b").at(1).get<std::int64_t>() == 2);
}