diff --git a/.github/workflows/ofl-ota-cronjob.yml b/.github/workflows/ofl-ota-cronjob.yml index cfbbc89081..3192cc4f05 100644 --- a/.github/workflows/ofl-ota-cronjob.yml +++ b/.github/workflows/ofl-ota-cronjob.yml @@ -45,7 +45,7 @@ jobs: [ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; } [ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; } - timestamp="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" + timestamp="$(date -u +%s)" echo "timestamp=$timestamp" >> "$GITHUB_OUTPUT" resp_file="$RUNNER_TEMP/ota-pending-clear-response.json" diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f8e994758..3a7b8adf5f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,9 +56,9 @@ You can do this in Environment Variables settings. endif () if (APPLE) - # if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 11.3 + # if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts) if (NOT CMAKE_OSX_DEPLOYMENT_TARGET) - set(CMAKE_OSX_DEPLOYMENT_TARGET "11.3" CACHE STRING "Minimum OS X deployment version" FORCE) + set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum OS X deployment version" FORCE) endif () message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}") endif () @@ -279,6 +279,11 @@ if (APPLE) endif() SET(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.orcaslicer.OrcaSlicer") + # The macOS CI jobs build with Ninja (build_release_macos.sh -x), so the Xcode generator + # is not covered. Xcode adds -Wshorten-64-to-32 by default ("Implicit Conversion to 32 Bit + # Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts. + set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO") + message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}") elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") set(CMAKE_INSTALL_RPATH "$ORIGIN") diff --git a/build_release_macos.sh b/build_release_macos.sh index b1de5df2e4..db03cb4ecd 100755 --- a/build_release_macos.sh +++ b/build_release_macos.sh @@ -53,7 +53,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do echo " -s: Build slicer only" echo " -u: Build universal app only (requires existing arm64 and x86_64 app bundles)" echo " -n: Nightly build" - echo " -t: Specify minimum version of the target platform, default is 11.3" + echo " -t: Specify minimum version of the target platform, default is 12.0" echo " -x: Use Ninja Multi-Config CMake generator, default is Xcode" echo " -b: Build without reconfiguring CMake" echo " -c: Set CMake build configuration, default is Release" @@ -95,7 +95,7 @@ if [ -z "$DEPS_CMAKE_GENERATOR" ]; then fi if [ -z "$OSX_DEPLOYMENT_TARGET" ]; then - export OSX_DEPLOYMENT_TARGET="11.3" + export OSX_DEPLOYMENT_TARGET="12.0" fi if [ -z "$CMAKE_IGNORE_PREFIX_PATH" ]; then diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 0e7141c12c..502e5df071 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -26,9 +26,9 @@ endif() cmake_minimum_required(VERSION 3.2) if (APPLE) - # if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 11.3 + # if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts) if (NOT CMAKE_OSX_DEPLOYMENT_TARGET) - set(CMAKE_OSX_DEPLOYMENT_TARGET "11.3" CACHE STRING "Minimum OS X deployment version" FORCE) + set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum OS X deployment version" FORCE) endif () message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}") diff --git a/docs/HLSD/deferred-page-construction.md b/docs/HLSD/deferred-page-construction.md index 748ca65d52..6a25d16fc3 100644 --- a/docs/HLSD/deferred-page-construction.md +++ b/docs/HLSD/deferred-page-construction.md @@ -2,219 +2,136 @@ ## Why it exists -The main window is a notebook of tabs. When the first frame appears, one tab is on -screen and the others are not; some of them are opened later in the session, some -never, and some only exist for certain printers. Building a tab before the first frame -adds its cost to every startup, whether or not the tab is used. +The main window is a notebook of tabs, and only one of them is on screen when the first +frame appears. Others are opened later in the session, some never, and some only exist +for certain printers. Building every tab before the first frame makes each startup pay for +tabs the user may never open. -This subsystem builds a tab's panel the first time the tab is shown. It also builds the -remaining tabs while the user is idle after startup, in units of tens of milliseconds, -so a click that lands in the middle of one waits for that unit and no longer. Startup -pays only for what the first frame shows, and the other tabs are usually built before -anyone opens them. +This subsystem builds a tab the first time it is shown, and builds the rest in small units +while the user is idle after startup. Startup pays only for what the first frame shows, +the other tabs are usually ready before anyone opens them, and a click that lands in the +middle of the idle build waits for one unit at most. Work that is not a tab, such as a +dialog or the 3D view's GL resources, uses the same machinery. ## The parts -`src/slic3r/GUI/Lazy.hpp`, `LazyPage.hpp`, `StagedBuild.hpp` and `IdleScheduler.hpp/.cpp` -(with its wx-free `PrebuildQueue.hpp`) are independent. A holder can hold anything a -factory makes, a page is a holder with a placeholder widget, a staged object can live -outside a holder, and the scheduler knows none of them; it runs tasks, which `MainFrame` -makes from the holders. +The parts are independent. A holder can hold anything a factory makes, a placeholder page +is a holder with a widget, a staged object can live outside a holder, and the scheduler +knows none of them; it runs tasks, which the main window makes from the holders. -### Lazy: the holder +### The holder: `Lazy` -Holds an object that a factory makes on first use, under a name for the log and a place -in the idle queue, both given by the owner. The header has no wx dependency; the busy -cursor for an on-demand build lives in `Lazy.cpp` and is skipped when there is no app, -so the holder is unit-tested. +A holder keeps one object and the factory that makes it. The rest of the app reads the +object if it exists, makes sure it exists now when about to show or navigate to it, or runs +something once it exists. A type with one instance in the app gets these as statics +through `LazyInstance`, so callers need no reference to the main window, and all of them +are harmless while no holder exists. -- `get()` is null until the object is completely built; `built()` says the same. -- `ensure()` builds whatever is left now, under a busy cursor, and returns the object, or - null in the two cases below. A click on an unbuilt tab or a first open of a dialog goes - through this, and it logs the units and time it took. -- `build_step()` runs one unit of construction and returns true while more remain. The - first unit is the factory call, and each later unit is one `StagedBuild` step if the - type has them. -- `when_built(fn)` runs `fn(object)` now if it exists, otherwise once it is built. -- `pending()` says whether the idle prebuild has work here: not built, and the factory - has not returned null. A null factory result is logged and the holder stays unbuilt. -- A unit that pumps the event loop cannot re-enter the holder; a nested `build_step()` - does nothing and a nested `ensure()` returns null. -- After a unit throws, the holder and the scheduler still run the next one. +The holder guarantees that callers see the object only once it is completely built. A +build cannot re-enter itself, so a nested request finds nothing yet, and a factory that +returns null or a unit that throws leaves the holder and the scheduler able to carry on. +The holder does not own the object; its wx parent does, as for any window. The holder has +no wx dependency and is unit-tested. -The holder does not own the object; its wx parent does, as for any window. A type with -one instance in the app derives from `LazyInstance`, which points at that instance's -holder (the holder registers itself, and a recreated `MainFrame`'s holder replaces the -old frame's) and gives the type the static entry points the rest of the app uses, -`T::if_built()`, `T::ensure()` and `T::when_built(fn)`. They return null, or do -nothing, while no holder exists, so a caller needs no `mainframe` check. +### The placeholder page: `LazyPage` -`built()` is an atomic flag, since a job worker reads it through the statics -(`MainFrame::get_calibration_curr_tab()`); it is set after the object is complete. +The notebook needs a page object for a tab to exist and for tabs to be inserted and +removed by pointer, and the placeholder is that object. It builds the real panel inside +itself the first time it is shown and forwards showing and hiding afterwards, so a panel's +own show handling stays its activation hook. Nothing builds while the main window is +hidden; the window's first show builds the start page. A page that is out of the book is +not prebuilt. A panel built while its page is hidden stays hidden, and gets the theming +the window applied before the panel existed. -`LazyBase` is the holder's type-free interface (`name()`, `built()`, `pending()`, -`build_step()`, `prebuild_order()`) and is what the scheduler side sees. +### Staged construction: `StagedBuild` -### LazyPage: the placeholder +A constructor too big to be one unit builds a skeleton and queues the rest as steps, which +run one per unit. A child panel's steps can be forwarded to its parent, and the parent is +complete only once the child is. Nothing may use what a step builds before the last step +has run, so staged panels follow these constraints: -A `wxPanel` placed in the parent in place of the real panel, and a `Lazy` whose -factory makes the panel inside it (by default `new Panel(parent)`). The notebook needs a -page object for the tab to exist and for `show_device()` to insert and remove tabs by -pointer, and the placeholder is that object. `MainFrame` creates every page once, named -after its `TAB_ID_*`, and keeps it for the frame's life; `show_device()` only moves -pages in and out of the book. - -- `Show()` is forwarded to the panel, so a panel's own `Show()` override stays its - activation hook (refresh timers, machine sync) and `SelectPageByName()` works - unchanged. A show builds the panel unless the frame itself is still hidden, because - the book selects its first page as it is inserted; `MainFrame::Show()` shows the - current page again when the frame becomes visible, which builds it. -- `in_book()` says whether the parent notebook currently lists the page, and - `pending()` is that and not built, so a tab `show_device()` has taken out of the book - is not prebuilt. -- A panel built while its page is hidden stays hidden, and a completed panel gets the - dark-mode pass the frame ran before it existed. - -The Compare presets dialog is a holder without a page. `MainFrame` keeps a -`Lazy` whose factory constructs the dialog and binds its events, the -dialog derives from `LazyInstance`, and its callers use -`DiffPresetDialog::ensure()->show()` and `DiffPresetDialog::if_built()` like a tab's -callers do. Saving a preset refreshes the dialog only while it is shown, since `show()` -reloads the presets. - -### StagedBuild: construction in units - -A mixin for a panel whose constructor is too big to be one unit. The constructor builds -the skeleton (sizers, and the parts other code may touch) and queues the rest with -`add_build_step()`. `build_step()` runs one step, and `add_build_steps_of(child)` -forwards a child's steps so a nested panel is spread the same way; the parent is built -only once the child is, including steps the child queues later. Steps run in order, on -the main thread. A `Lazy` recognises a staged type at compile time and runs its steps -one per unit. - -Nothing may touch what a step builds before the last step has run. In practice: - -- nothing paints the panel before it is complete, since a panel built at idle is hidden - with its page and one built on demand finishes inside `ensure()` before the event loop - runs again; -- members created in steps are initialised to null in the header, so a partially built - panel can be destroyed; -- timers and event handlers that use step content check `built()` first - (`MonitorPanel::update_all()`, `CalibrationPanel::update_all()`), and a child's steps - are queued before anything in the constructor can fire such a handler; -- a destructor that disconnects from step content checks `built()` first; -- a constructor or step does not take focus while the panel is off screen - (`IsShownOnScreen()` before `SetFocus()`), since it may run while the user is typing +- members created in steps start out null, so a partly built panel can be destroyed; +- timers, event handlers and destructors that touch step content check that the panel is + complete first; +- nothing takes focus while off screen, since a unit may run while the user is typing elsewhere; -- where a step's widgets must keep their place in a sizer that later steps also fill, the - constructor adds an empty slot sizer in that position and the step fills the slot - (`StatusBasePanel`). +- a widget added by a step keeps its place in the sizer through an empty slot the skeleton + creates. -### IdleScheduler: when to build +### The scheduler: `IdleScheduler` and `PrebuildQueue` -A task is any `LazyBase`: a name for the log, `pending()`, `build_step()` and -`prebuild_order()`. `PrebuildQueue` holds the tasks by order (equal order in the order -added) and runs a slice, the units of the first pending task until it completes, the -budget is spent on the clock it is given, or the interrupt predicate says input arrived. -It has no wx dependency and is unit-tested with a fake clock. A task whose work is gone -is passed over and stays in the queue, so a tab that `show_device()` removes and later -re-inserts is pending again. +The queue holds tasks in order, and a slice runs units of the first pending task until +the task finishes, the time budget is spent, or input arrives. It has no wx dependency and +is unit-tested with a fake clock. A task whose work goes away, such as a tab removed from +the book, is skipped, and becomes pending again if the work comes back. -`IdleScheduler` drives the queue with the real clock, the app's input timestamp and the -Windows queue check, and logs each unit and slice. Each slice is a timer message: a -period of 250 ms while waiting for the user to go quiet, and a one-shot of zero after a -slice that left work, so the event loop dispatches whatever it has queued (paint, -timers, input) before the next slice runs. Chaining slices with `CallAfter` would not do -this: wx drains pending events fully before the next native message, on every platform. -On GTK the one-shot is 5 ms, because a due GLib timeout runs ahead of the redraw and -idle sources that paint and deliver posted events. -A slice runs only once the user has been idle for the quiet time, and the timer stops -itself once no task is pending. The tick period, quiet time and slice length are -constants in `IdleScheduler.cpp`. A unit cannot be interrupted once started, so the -largest unit bounds click latency on Windows; on the other platforms a click also waits -for the rest of the slice. A unit that pumps the event loop lets the timer fire inside -its own slice, and that tick does nothing. +A slice runs only once the user has been idle for a short quiet time, and each slice is its +own timer message, so paint, timers and input queued in between are handled before the +next slice. Posting slices as pending events would not do that, because wx drains every +pending event before the next native message. On GTK a timer that is always due starves +the lower-priority sources that repaint and deliver posted events, so slices are a few +milliseconds apart. On Windows a slice also waits while the native queue holds input, +not counting mouse moves, which Windows synthesizes whenever a window appears under the +cursor. A slice never runs inside a `wxYield()`, where it would build pages in the middle of +the code that yielded. A unit cannot be interrupted, so the largest unit bounds how long a +click can wait. +When nothing is pending the timer stops and the subsystem costs nothing. -The tasks are made by their owners. `MainFrame::prebuild_pages_when_idle()` registers -every `LazyPage` the frame created, in or out of the book (`pending()` is false for a -page out of the book), the Compare presets holder, and the Prepare sidebar's settings -page from `ParamsPanel::settings_page_prebuild()`, whose first unit selects the default -tab if none is selected yet and whose later units build one option group each. -`show_device()` only restarts the timer. `MainFrame` owns the scheduler because it owns -everything the tasks build, and clearing the queue with the frame is what keeps a task -from outliving its object. +The main window owns the scheduler because it owns what the tasks build, and clearing the +queue with the window keeps a task from outliving its object. Each owner provides its own +tasks, such as a tab, a dialog, the Prepare tab's settings page one option group at a +time, the Prepare page's layout at the size the book gives its pages, or the 3D view's GL +resources. -Idle time comes from `GUI_App::FilterEvent`, which timestamps mouse and keyboard events -(`wxEVT_CATEGORY_USER_INPUT` minus command events, which all claim that category), and -`GUI_App::input_idle_ms()` reports it. On Windows a slice additionally refuses to start -when the message queue holds keyboard, button, touch or pen input. Mouse moves are -excluded because Windows synthesises one whenever a window appears under the cursor, -which every unit does. Other platforms use the timestamp alone. +### The 3D view's GL resources -Once every registered page is built the timer is stopped and the subsystem costs -nothing. +OpenGL is loaded on the Prepare tab's canvas. When the start page is not Prepare, loading +it is an idle task that makes the context current on the hidden canvas, so the start page +paints first and Prepare never appears. Loading it on a shown canvas under `Freeze()` holds +back the start page's paint, and on GTK `Freeze()` cannot hide the canvas, which is a +native child window or a Wayland subsurface drawn outside GTK. A hidden Windows child +window keeps its device context, macOS attaches the context to a hidden view, and GTK +creates the canvas's surface when the widget is realized, so on GTK the task realizes the +canvas first. If the context cannot be made current, the canvas's first render loads the +resources. ## Rules -**What builds before the first frame.** The Prepare tab's plater, because `post_init()` -needs its GL canvas on screen to initialise OpenGL in every startup state, and the start -page the user configured. Home is built by the first `MainFrame::Show()`, Prepare's -settings page by selecting the tab. `post_init()` passes through the Prepare tab for GL -init under `Freeze()` with `MainFrame::select_prepare_for_gl_init()`, which changes the -selection without the page-changed event, so nothing else is built for that pass. +**Before the first frame.** Only the start page and the Prepare tab's plater are built +before the first frame. Everything else goes through a holder. -**Reaching a lazy object.** A caller uses the type's own statics. `T::if_built()` may -return null and is for telling the object something it can live without (a rescale, a -colour change, a status update). `T::ensure()` builds the object and is for navigating -to it or for a caller that is about to show it. A caller that tells the object something -it would not fetch for itself on construction uses `T::when_built()`, which keeps the -message until the object exists. A panel that pulls its state when constructed (the Home -page requests the recent list on load, the Device tab reads the device manager on show) -is reached with `if_built()`; one that cannot pull gets `when_built()`. +**Reaching a lazy object.** Callers use the type's statics. Reading it if built is for +things the object can live without, such as a rescale, a color change or a status refresh. +Making sure it exists is for navigating to it or showing it. Running something once it is +built is for state it would not fetch for itself on construction. A panel that pulls its +state when constructed only ever needs to be read if built. -**Unit size.** A unit cannot be interrupted, so it should stay within the slice length on -a fast machine. A constructor above that is staged. A single widget above it is the -floor unless the widget itself is split. +**Unit size.** A unit should fit in one slice on a fast machine. A constructor over that is +staged, and a single widget over it is accepted unless the widget itself can be split. -**Order.** Cheapest and most likely to be opened first, given where `MainFrame` creates -each holder. The settings page is 0 (the Prepare tab's own content), Home 10, Device 20 -(a Bambu user's usual second stop), Calibration 30, Multi-device 40, the web Device -view 50, Project 60 (a second WebView2 instance) and the Compare presets dialog 100; -steps of ten so a new tab takes a value between its neighbours without renumbering -them. `MainFrame::prebuild_pages_when_idle()` registers the tasks once, from -`post_init()`. - -**Never prebuilt.** A holder with a negative order, for a tab that few sessions open -and that costs more to build unasked than it saves (the Design tab), and plugin-provided -tabs, which are Python-side and not lazy pages. +**Order.** Tasks run cheapest and most likely to be opened first. Each holder's order is +given where it is created, with gaps so a new task fits between its neighbors. A negative +order is never prebuilt, for something few sessions open that costs more to build unasked +than it saves. ## Adopting it -A lazy tab needs: +A lazy tab needs a panel type deriving from `LazyInstance`, a placeholder page the main +window creates once with its order and registers for the idle build, and every use of the +panel outside the main window going through the statics. Its constructor has to cope with +the main window already existing and the user being busy elsewhere, so it takes no focus +while off screen, and it does all of its own setup, since the main window does nothing to a +panel after creating it. -1. The panel derived from `LazyInstance`, since a tab's panel has one instance. -2. A `LazyPage*` member in `MainFrame`, created once in `init_tabpanel()` with - its `TAB_ID_*` as the name, its place by the order rule (and a factory if - `new Panel(parent)` is not enough), added to `m_lazy_pages`, and used wherever the - tab is added to or looked up in the notebook. -3. Every use of the panel outside `MainFrame` going through one of the panel's statics, - chosen by the rule above. When converting an existing member, `grep` for it; the - compiler finds the rest. -4. A constructor that copes with the frame already existing and the user being busy - elsewhere, since `wxGetApp().mainframe` is set, the frame may be shown, and the user - may be typing when a lazy panel is built: no `SetFocus()` while off screen, and - whatever the constructor did through a `MainFrame` accessor before (a mode update, a - deferred URL) done in the constructor itself. +To stage a heavy constructor, keep the skeleton in the constructor, move the rest into +steps in its original order, and follow the staged-construction constraints. Measure the +units; a step that is still one big widget is split inside the widget or accepted. -To stage a heavy constructor, inherit `StagedBuild`, keep the skeleton in the constructor, -move the rest into `add_build_step()` lambdas in the original order, and follow the -staged-panel rules above. Measure the units. A step that is still one big widget has to -be split inside that widget or accepted as the floor. +## Verifying -Verification is by log. `MainFrame::prebuild_pages_when_idle` lists the queue it -registered, `IdleScheduler::tick` reports each completed task by name at info level and -each slice and unit at debug level, and `Lazy::ensure` reports an object a user built -on demand with the units and time it took. A run from the configured start -page shows every registered page complete, in order, with no unit longer than intended. -A click on a tab mid-prebuild shows the finished panel with an `ensure` line for what was -left, and the slices resume for the remaining tasks once the user is idle again. +The log lists the queue when it is registered, reports each completed task at info level +and each slice and unit at debug level, and reports every on-demand build with its units +and time. A task's completion line counts only the slice it finished in. A run from the +configured start page should show every registered task complete in order, with no unit +longer than intended. A click on a tab during the idle build should show an on-demand +build for what was left, with the slices resuming once the user is idle again. diff --git a/docs/HLSD/multiline-infill.md b/docs/HLSD/multiline-infill.md new file mode 100644 index 0000000000..d644b197eb --- /dev/null +++ b/docs/HLSD/multiline-infill.md @@ -0,0 +1,123 @@ +# Multiline infill — High Level Design + +## Purpose and scope + +`fill_multiline` prints every sparse infill wall as N adjacent lines instead of +one, so a wall is `d1 = N * spacing` thick. Only internal sparse infill uses it. +Each pattern first builds its single-line centerlines at N times the usual line +spacing (so the density holds), and `multiline_fill()` then replaces each +centerline by the lines of that wall: the centerline itself when N is odd, and +closed outlines around it at every `spacing` out to `d1 / 2`. The outlines are +clipped to the fill region contracted by half a line width, then connected like +any other infill. + +Outlines of centerlines that cross each other overlap at every crossing, which +over-extrudes the wall intersections. The line-crossing patterns Grid, +Triangles, Tri-hexagon and Cubic therefore build centerlines that never cross +(`FillRectilinear::fill_surface_trapezoidal()`), and so do Adaptive Cubic and +Support Cubic (`FillAdaptive`); the other patterns outline their usual +centerlines. + +## Non-crossing centerlines + +The crossing lines are resolved into x-monotone paths, the levels of the line +arrangement: walking along x, the k-th path is always the k-th line from the +bottom. At every crossing, the two paths bounce off each other instead of +passing through. Adjacent paths meet only at crossings, so their outlines touch +there and nowhere overlap. + +Where two paths meet, each is cut short by a line perpendicular to the bisector +of its bend, `d1 / 2` from the crossing. The two cut segments are parallel and +`d1` apart, so the outermost lines of the two walls sit exactly `spacing` apart, +like the lines inside a wall. Where three lines meet at one point, the middle +path runs straight through and the outer two are cut `d1` from it. + +Each pattern builds its rows along x in a rotated frame. Grid lines run at ±45° +there, and its rows are trapezoid waves that transpose on alternate layers. The three families of Triangles, Tri-hexagon and +Cubic run at 0°, 60° and 120°. Those rows rotate by 120° every layer about a +3-fold center of the arrangement, so each family takes every role in turn. +The pattern is phased on fixed positions, so it lines up across layers and +across the regions of one layer. Rounding the corners with +`sparse_infill_smooth_factor` happens before `multiline_fill()`. + +## Cubic + +Single-line Cubic draws the three families at the same spacing `h` and shifts +them with z: by `+dx`, `-dx` and `+dx`, `dx = z / sqrt(2)`. The multiline paths +follow the same lines. In the frame where one family is horizontal, the other two +cross in rows `h` apart, alternating by half a period, at height +`tau = -3 * dx (mod h)` above the horizontal line below them. The crossings split +every band between horizontal lines into up-pointing triangles of height `tau`, +down-pointing triangles of height `h - tau`, and hexagons. At `tau = 0` (and `h`) +all three families meet at common points, as in Triangles. At `tau = h / 2` the +triangles are equal, as in Tri-hexagon. The origin of that frame is always a +3-fold center, whatever z is, so the per-layer rotation keeps the lines in place. + +Each band holds two paths that touch at its crossings: the upper one takes the +V below the crossing and runs along the top horizontal line, and the lower one +takes the inverted V above it and runs along the bottom line. Both are the same function +of `tau`, the lower one mirrored with `h - tau`. `cubic_upper_level()` builds one +period of the upper path as the lower envelope of five lines, clipped from below: + +- the two slanted lines through the crossings, +- the horizontal line, lowered when the triangle above it is less than `1.5 * d1` high, +- the two chamfers where the path turns onto and off the horizontal line, `d1 / 2` + from those crossings, +- the flat cut into the V at the crossing. + +The cut height `clamp(tau - d1 / 2, 0, h - d1) + d1` is what makes the pattern +continuous in z. While both triangles are at least `1.5 * d1` high, every +crossing is a pair of bends `d1 / 2` from it, as in Tri-hexagon. When a triangle +is thinner, its three paths stack like a triple crossing. The path through it +flattens toward its base line and lies on it once the triangle is under `d1 / 2` +high, and the paths beside it are pushed `d1` away. The layout thus reaches the +Triangles one where the families meet. Adjacent paths stay at least `d1` apart +at every `tau` and at every density up to 100%. + +## Adaptive Cubic + +Adaptive Cubic and Support Cubic take their lines from an octree of cubes +standing on a corner. On each layer every cube cuts its three mid-planes into +segments of the same three 60° families as Cubic, but the pattern is not +periodic. Smaller cubes near the surface add finer lines, and a finer line ends +where it meets the wall of its coarser cube, so the lines form crossings and +T-junctions. `FillAdaptive::multiline_paths()` builds the paths from these +segments directly, for each fill region and within `4 * d1` of it. + +At a crossing the two paths bounce as in Cubic. At a T-junction the through line +runs straight on and the path of the ending line stops there. Every path still +runs left to right in the frame where one family is horizontal, and that family +rotates with the layer. + +Every line of every cube size lies on one fine lattice, so crossings closer than +a few `d1` are the corners of one small triangle of that lattice, as in Cubic. +The cuts follow the Cubic rules without a closed formula: + +- The two bends of a crossing are cut `d1` apart, `d1 / 2` each, perpendicular + to their bisector, so their walls touch. A cut goes no further than the path + end, and the other bend takes the rest of `d1`. +- At the tip of a small triangle, between the two slanted families, a cut also + goes no further than the neighbouring bend turning the other way, and the + path beyond that bend is kept a wall away from it. The bends onto the + horizontal family are not limited this way: pushing their paths apart would + open gaps between walls that should touch. +- A cut moves the path only where the cut line lies beyond it, near its bend. + The sharp bends between the two slanted families are cut after the bends onto + the horizontal family, so the tip of a small triangle wins, as in Cubic. +- A path stopping at a T-junction is trimmed until it is `d1` less half a line + spacing from every other path, so that its end overlaps the wall it stops on + by half a line and bonds to it. The paths are trimmed one at a time against + the others as already trimmed, so two ends facing each other meet instead of + both backing off. A path stopping on the line of another is trimmed before + that one, so it gives way and the other still reaches the line it stops on. A + second round trims every path again from its full length, so an end grows + back where the ends it gave way to were trimmed later, and a last round only + shortens them, keeping them that far apart. Paths shorter than `d1` are left + out. +- A line that ends on another less than `2 * d1` past a crossing stops at that + crossing instead, the shorter one where both do. The path along such a stub + would be trimmed away, leaving a hole between the walls that were cut to + touch it. + +Short paths enclosed by coarser lines still print as closed outlines, but most +paths run on across several cells. diff --git a/docs/HLSD/prime-tower-sparse-layers.md b/docs/HLSD/prime-tower-sparse-layers.md new file mode 100644 index 0000000000..19802c9169 --- /dev/null +++ b/docs/HLSD/prime-tower-sparse-layers.md @@ -0,0 +1,197 @@ +# Prime tower sparse layers — High Level Design + +## Purpose and scope + +A prime tower exists to absorb filament changes, but it is planned on every +object layer below the topmost change, not only on the layers that purge. The +layers in between carry no filament change and print nothing but a block of the +tower's own footprint to keep its top level. They are called sparse layers, and +on a print with few changes they are most of the tower: they cost time, filament +and a travel to the tower on every layer. + +Two settings trade that cost against something else. `wipe_tower_no_sparse_layers` +drops them, which sinks the tower below the model. `wipe_tower_sparse_layers_combination` +merges runs of them into fewer, thicker layers, which keeps the tower level with +the model. Both are off by default, and with both off the tower prints one layer +per object layer as it always has. + +The decisions belong to tower planning and G-code emission. They do not change +sliced object geometry, but they do change the emitted G-code, the filament and +time estimates, and — for the compacted case — whether a plate is printable at +all. Changing either setting invalidates the tower step. + +## What a sparse layer is + +`ToolOrdering::fill_wipe_tower_partitions` counts the filament changes per layer +and propagates that count downwards, so every layer below the topmost change is +marked as carrying a tower. It then fills any gap between two tower layers, so +the tower is continuous from the bed to its last purge. `wipe_tower_layer_height` +is the distance from the previous tower layer, which is the object's layer height +whenever the tower prints on every layer. + +`Print::_make_wipe_tower` plans one tower layer per such object layer. A layer +whose only call keeps the current filament leaves no toolchange in the plan, and +the layer it generates is a single result whose initial and new tool are equal. +That is what `wipe_tower_layer_is_sparse` recognises, and it is the unit both +settings work on. + +The plan stays one entry per tower layer in every case. The G-code emitter walks +`WipeTowerData::tool_changes` by layer index, advancing once per object layer +that carries a tower, so a planner that removed entries would silently shift +every later layer onto the wrong tower geometry. Layers that print nothing are +therefore still planned and still generated; they are marked, and the emitter +drops them. + +## Shared rules + +Tower planning, G-code emission and the plate validation all have to agree about +which layers print and where. They ask one set of free functions, declared beside +the tower classes, rather than each re-deriving the answer from the raw options: + +- `wipe_tower_sparse_layers_skipped` — whether sparse layers are really dropped. + Smooth timelapse and clumping detection park the nozzle on the tower every + layer, so with either of them on no layer is ever dropped and the option reads + as off everywhere. +- `wipe_tower_sparse_layers_combined` — whether runs are really merged. The same + two rule it out, and so does `wipe_tower_no_sparse_layers`: dropping the layers + outright is the stronger answer to the same problem, so the two settings are + exclusive and the GUI greys out the second while the first is on. +- `wipe_tower_layer_is_sparse`, `wipe_tower_layer_is_combined_away` — per-layer + questions the emitter asks about generated results. +- `compute_compacted_wipe_tower_z` — the tower's print z per planned layer when + it is compacted. +- `combine_sparse_wipe_tower_layers` and its `combine_sparse_wipe_tower_plan` + wrapper — the merge rule, applied to either generator's plan. + +Both tower generators are driven through these. `WipeTower` (Type 1, the block +tower) and `WipeTower2` (Type 2, the default) keep separate plans with the same +per-layer shape — print z, layer height, toolchanges, and a `combined_away` flag +— so one template covers both. + +## Dropping sparse layers + +With `wipe_tower_no_sparse_layers`, the tower only grows on layers that carry a +real change. It therefore falls one layer height behind the object for every +sparse layer, and by the top of a tall print it can sit far below the model. The +nozzle has to reach down to it at each purge. + +`compute_compacted_wipe_tower_z` derives that z once, from the generated results, +so the emitter and the validator cannot disagree. Emission descends to it, but +only once the nozzle is parked over the tower: descending while still over the +model would drive the nozzle into the print, so a descent that would do that is +deferred until after the travel to the tower. Extrusions emitted without an +explicit z — the nozzle-change wipe in particular — are pulled down to the +compacted z for the same reason. + +Reaching down is only safe if nothing tall stands near the tower. `Print.hpp` +carries the clearance rule: a keep-out zone grown from the tower's footprint by +the spiral z-hop envelope, and a per-object limit on how high an object may rise +near it, tiered by the nozzle cone, the head body, the rod and the lid. The same +rule serves the precise check on real extrusions, the pre-slice estimate that +feeds the plater, and the outlines the plater draws while an object is dragged, +so that the ring the user sees touches the object's outline exactly when the +check trips. + +## Merging sparse layers + +With `wipe_tower_sparse_layers_combination`, no layer is dropped and nothing is +compacted: the tower keeps following the object, and the nozzle never descends. +Instead a run of consecutive sparse layers prints once, on the run's last layer, +at the accumulated height of everything it covers — the same way infill +combination merges sparse infill. The layers below it in the run print nothing. + +`combine_sparse_wipe_tower_plan` runs before the tower's depths are planned, +because the heights it rewrites feed the extrusion flow of every later pass. It +raises `height` in place on the layer that prints a run and sets `combined_away` +on the rest; generation then proceeds unchanged, and the flag is copied onto the +results so the emitter can drop them. + +Four constraints shape the rule: + +- **Whole layers only.** A tower layer is entered at the object's z, so a merged + layer has to end on an object layer boundary. The merged height is therefore a + sum of whole layer heights, never a clamped value. +- **The nozzle's maximum layer height.** A run stops growing as soon as one more + layer would pass `max_layer_height` for the nozzle printing it — three quarters + of the nozzle diameter when that is left at 0, as elsewhere in slicing. The cap + is read through the filament-to-nozzle map, since `max_layer_height` is per + nozzle while the tower indexes filaments. This is what makes the setting inert + at common layer heights: two 0.2 mm layers are 0.4 mm and do not fit under a + 0.3 mm maximum, so nothing merges until the layer height is 0.15 mm or below, + or the maximum is raised. +- **A filament change purges at its own z.** A layer with a real change can + neither be merged away nor absorb the run below it, so a run always ends on its + own last sparse layer and the change above it is untouched. +- **The first layer stays on the bed.** It carries the brim and is never merged. + +A run holds one filament throughout — that is what makes it sparse — so the cap +is uniform across it, and the tower reserves depth only for the purges above a +layer, so a run has one footprint and the merged layer covers exactly the area +the layers it replaces would have. + +## Emission and accounting + +`WipeTowerIntegration` drops a layer whose results are marked, for both settings, +through the same `ignore_sparse` path in `tool_change` and +`is_empty_wipe_tower_gcode`. A dropped layer emits no travel to the tower and no +extrusion. + +Filament used is accumulated by the generators while they write, so a layer that +will be dropped must not be charged. Type 1 asks `layer_is_printed` at each of +its accumulation points; Type 2 guards the equivalent block in `finish_layer`, +which also stops a merged-away layer from adding height of its own — the layer +that prints the run carries all of it. + +A merged layer is the only case where the tower's layer height differs from the +object layer it sits on, and therefore the only case where the height the +exporter already emitted for that layer is wrong for the tower. Both generators +do declare a height, but each hardcodes a tag dialect — the block tower forces +the BBL tag, the other writes the compatible one — while the G-code processor +reads only the tag its printer uses. On a non-BBL printer with a Type 1 tower the +declaration is dropped, and the merged layer is drawn and costed as a thin one. +`WipeTowerIntegration::tower_height_tag` therefore declares it at export time, +where the printer is known, and only when the tower's own G-code does not already +carry the tag that will be read. The object's height returns on the next object +path, because emission forces the processor role to the tower on any layer that +carries one. + +## Constraints + +A layer that prints nothing prints nothing at all, including any interface work +the tower planner scheduled there. The Type 1 block planner marks a layer as a +contact layer when a filament category stops or starts being used relative to the +layer below, and a sparse layer immediately above a change qualifies. Merging a +run, like dropping its layers, replaces that interface with the run's single +layer. Both settings are off by default for this among other reasons. + +Neither setting changes what the tower is for. A plate that needs a tower on +every layer — smooth timelapse, clumping detection — gets one, and the settings +read as off rather than compacting or merging in one place and not another. + +## Implementation and verification + +- [WipeTower.hpp](../../src/libslic3r/GCode/WipeTower.hpp) declares the shared + rules and the plan-merging template; + [WipeTower.cpp](../../src/libslic3r/GCode/WipeTower.cpp) implements them and + the Type 1 tower, [WipeTower2.cpp](../../src/libslic3r/GCode/WipeTower2.cpp) + the Type 2 tower. +- [ToolOrdering.cpp](../../src/libslic3r/GCode/ToolOrdering.cpp) decides which + layers carry a tower at all, and + [Print.cpp](../../src/libslic3r/Print.cpp) plans it and runs the clearance + check whose rule lives in [Print.hpp](../../src/libslic3r/Print.hpp). +- [GCode.cpp](../../src/libslic3r/GCode.cpp) emits the tower, drops the layers + that print nothing, and declares a merged layer's height; + [PrintConfig.cpp](../../src/libslic3r/PrintConfig.cpp) defines the settings and + [ConfigManipulation.cpp](../../src/slic3r/GUI/ConfigManipulation.cpp) their + mutual exclusion. +- [GLCanvas3D.cpp](../../src/slic3r/GUI/GLCanvas3D.cpp) and + [PartPlate.cpp](../../src/slic3r/GUI/PartPlate.cpp) draw the compacted tower's + keep-out outlines live while the user drags. +- [Rule tests](../../tests/libslic3r/test_wipe_tower.cpp) cover the gating of + both settings, the per-layer predicates, the compacted z, the merge rule's run + flushing, height conservation, the nozzle cap and the first-layer exemption, + and the clearance geometry the plater draws. +- [Slicing tests](../../tests/fff_print/test_wipe_tower.cpp) slice a real print + and check that a run folds, that the tower still covers the object exactly + once, that a run too thin for the cap is left alone, and that a merged layer + declares its height in the tag the printer's processor reads. diff --git a/resources/web/model/index.html b/resources/web/model/index.html index 72feda0f5f..86776b6577 100644 --- a/resources/web/model/index.html +++ b/resources/web/model/index.html @@ -7,7 +7,6 @@ - @@ -18,6 +17,7 @@ + diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 6d23ec3770..52b16f9458 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -697,7 +697,8 @@ public: } } else { // Resize by duplicating the last value. - this->values.resize(n, this->values./*back*/front()); + T v = this->values./*back*/front(); + this->values.resize(n, v); } } } @@ -772,8 +773,10 @@ public: if (this->values.empty()) this->values.resize(rhs_vec->size()); - else - this->values.resize(rhs_vec->size(), this->values.front()); + else { + T v = this->values.front(); + this->values.resize(rhs_vec->size(), v); + } assert(default_index.size() == rhs_vec->size()); diff --git a/src/libslic3r/Emboss.cpp b/src/libslic3r/Emboss.cpp index 34d9a93590..38c035806c 100644 --- a/src/libslic3r/Emboss.cpp +++ b/src/libslic3r/Emboss.cpp @@ -990,6 +990,7 @@ EmbossStyles Emboss::get_font_list_by_enumeration() { std::vector font_names; EnumFontFamilies(hDC, (LPCTSTR) NULL, EnumFamCallBack, (LPARAM) &font_names); + ReleaseDC(NULL, hDC); EmbossStyles font_list; for (const std::wstring &font_name : font_names) { diff --git a/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp b/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp index e5fbeb5cdb..be86e92245 100644 --- a/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp +++ b/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp @@ -464,6 +464,16 @@ void group_region_by_fuzzify(PerimeterGenerator& g) } } + g.fuzzy_supported_area.reset(); + if ((g.has_fuzzy_skin || g.has_fuzzy_hole) && g.lower_slices != nullptr) { + coord_t max_thickness = 0; + for (const auto& region : regions) + if (should_fuzzify(region.config, g.layer_id, 0, true) || should_fuzzify(region.config, g.layer_id, 0, false)) + max_thickness = std::max(max_thickness, region.config.thickness); + // Walls farther than a line width plus the noise amplitude from the layer below are bridging; keep them smooth. + g.fuzzy_supported_area = offset_ex(*g.lower_slices, float(g.ext_perimeter_flow.scaled_width() + max_thickness)); + } + if (regions.size() == 1) { // optimization g.regions_by_fuzzify.push_back({regions.front().config, {}}); return; @@ -560,13 +570,23 @@ static std::vector collect_merged_fuzzy_regions(const std::ve return merged_regions; } +// Afterwards an empty region means nothing to fuzzify, no longer full coverage. +static void restrict_to_supported(std::vector& merged_regions, const std::optional& supported) +{ + if (!supported) + return; + for (auto& merged_region : merged_regions) + merged_region.expolygons = merged_region.expolygons.empty() ? *supported : intersection_ex(merged_region.expolygons, *supported); +} + Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perimeter_generator, const size_t loop_idx, const bool is_contour) { Polygon fuzzified; const auto slice_z = perimeter_generator.slice_z; const auto& regions = perimeter_generator.regions_by_fuzzify; - if (regions.size() == 1) { // optimization + const auto& supported = perimeter_generator.fuzzy_supported_area; + if (regions.size() == 1 && !supported) { // optimization const auto& config = regions.begin()->first; const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, loop_idx, is_contour); if (!fuzzify) { @@ -590,7 +610,7 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim // Fast path: single merged region — apply directly without splitting if (merged_regions.size() == 1) { const auto& mr = merged_regions.front(); - if (mr.expolygons.empty()) { + if (mr.expolygons.empty() && !supported) { fuzzified = polygon; fuzzy_polyline(fuzzified.points, true, slice_z, *mr.config); return fuzzified; @@ -626,6 +646,8 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim if (!merged_regions[i].expolygons.empty() && !merged_regions[j].expolygons.empty()) merged_regions[i].expolygons = diff_ex(merged_regions[i].expolygons, merged_regions[j].expolygons); + restrict_to_supported(merged_regions, supported); + // Split the loops into lines with different config, and fuzzy them separately fuzzified = polygon; for (const auto& r : merged_regions) { @@ -689,7 +711,8 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato const auto slice_z = perimeter_generator.slice_z; const auto layer_height = perimeter_generator.layer_height; const auto& regions = perimeter_generator.regions_by_fuzzify; - if (regions.size() == 1) { // optimization + const auto& supported = perimeter_generator.fuzzy_supported_area; + if (regions.size() == 1 && !supported) { // optimization const auto& config = regions.begin()->first; const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour); if (fuzzify) @@ -703,7 +726,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato if (!merged_regions.empty()) { // Fast path: single merged region — apply directly without splitting - if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty()) { + if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty() && !supported) { fuzzy_extrusion_line(extrusion->junctions, slice_z, perimeter_generator.layer_height, *merged_regions.front().config, closed); return; } @@ -753,6 +776,8 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato if (!merged_regions[i].expolygons.empty() && !merged_regions[j].expolygons.empty()) merged_regions[i].expolygons = diff_ex(merged_regions[i].expolygons, merged_regions[j].expolygons); + restrict_to_supported(merged_regions, supported); + // Split the loops into lines with different config, and fuzzy them separately for (const auto& r : merged_regions) { const auto splitted = Algorithm::split_line(*extrusion, r.expolygons, false); diff --git a/src/libslic3r/Fill/FillAdaptive.cpp b/src/libslic3r/Fill/FillAdaptive.cpp index dbaa1f2ac9..809eddb218 100644 --- a/src/libslic3r/Fill/FillAdaptive.cpp +++ b/src/libslic3r/Fill/FillAdaptive.cpp @@ -1,3 +1,4 @@ +#include "../AABBTreeLines.hpp" #include "../ClipperUtils.hpp" #include "../ExPolygon.hpp" #include "../Surface.hpp" @@ -14,7 +15,9 @@ #include #include #include +#include #include +#include // Boost pool: Don't use mutexes to synchronize memory allocation. #define BOOST_POOL_NO_MT @@ -1318,6 +1321,564 @@ bool has_no_collinear_lines(const Polylines &polylines) } #endif +// Non-crossing centerlines for multiline adaptive cubic, see docs/HLSD/multiline-infill.md. +namespace noncrossing { + +// y = x() * x + y() in the frame where the sweep family is horizontal. +using Lin = Vec2d; + +static const Vec2d family_dir[3] { Vec2d(1., 0.), Vec2d(0.5, 0.5 * sqrt(3.)), Vec2d(0.5, -0.5 * sqrt(3.)) }; + +struct SweepLine +{ + Vec2d a, b; // a.x() < b.x() + int family; + Lin lin; + std::vector> junctions; // (x, junction) +}; + +struct Junction +{ + Vec2d p; + std::vector lines; + std::vector> pairs; // (left, right) line of each path through, bottom-up + std::vector> bends; // (path, bend) of each pair, -1 where it runs straight +}; + +struct LevelPath +{ + std::vector verts; // start, bends, end + std::vector lines; // line of each piece + std::vector junctions; // junction of each bend + std::vector turn; // 1 turning up, -1 turning down + std::vector> cuts; + std::vector> pushes; // (line, bend) keeping a wall away from a neighbour's cut + int start_term { -1 }; // junction where the path stops on another line, or -1 + int end_term { -1 }; +}; + +// Moves f onto line c (side 1: from below) wherever c lies beyond it, over the stretches overlapping [w0, w1]. +static void clip_profile(std::vector &f, const Lin &c, int side, double w0, double w1, double lim0, double lim1, bool cut_at_window, bool drop_past_limits) +{ + const double r0 = std::max(lim0, f.front().x()), r1 = std::min(lim1, f.back().x()); + if (r1 <= r0) + return; + const size_t ia = std::upper_bound(f.begin(), f.end(), r0, [](double x, const Vec2d &p) { return x < p.x(); }) - f.begin(); + const size_t ib = std::lower_bound(f.begin() + ia, f.end(), r1, [](const Vec2d &p, double x) { return p.x() < x; }) - f.begin(); + auto interpolate = [](const Vec2d &a, const Vec2d &b, double x) { return b.x() > a.x() ? a.y() + (x - a.x()) / (b.x() - a.x()) * (b.y() - a.y()) : b.y(); }; + std::vector local{ Vec2d(r0, interpolate(f[ia - 1], f[ia], r0)) }; + local.insert(local.end(), f.begin() + ia, f.begin() + ib); + local.emplace_back(r1, interpolate(f[ib - 1], f[ib], r1)); + + const double tol = 1.; + auto beyond = [&c, side, tol](const Vec2d &p) { return side * (c.x() * p.x() + c.y() - p.y()) - tol; }; + std::vector> stretches; + auto add = [&stretches](double x0, double x1) { + if (!stretches.empty() && stretches.back().second >= x0) + stretches.back().second = x1; + else + stretches.emplace_back(x0, x1); + }; + for (size_t i = 1; i < local.size(); ++i) { + const Vec2d &p = local[i - 1], &q = local[i]; + if (q.x() <= p.x()) + continue; + const double bp = beyond(p), bq = beyond(q); + if (bp > 0. && bq > 0.) + add(p.x(), q.x()); + else if (bp > 0. || bq > 0.) { + const double x = p.x() + bp / (bp - bq) * (q.x() - p.x()); + if (bp > 0.) + add(p.x(), x); + else + add(x, q.x()); + } + } + std::vector> keep; + for (auto [x0, x1] : stretches) { + if (x1 < w0 || x0 > w1) + continue; + if (drop_past_limits && ((x0 <= r0 && r0 == lim0) || (x1 >= r1 && r1 == lim1))) + continue; + keep.emplace_back(cut_at_window ? std::max(x0, w0) : x0, cut_at_window ? std::min(x1, w1) : x1); + } + if (keep.empty()) + return; + + auto y_local = [&](double x) { + size_t i = 1; + while (i + 1 < local.size() && local[i].x() < x) + ++i; + return interpolate(local[i - 1], local[i], x); + }; + std::vector out(f.begin(), f.begin() + ia); + auto push = [&out, tol](double x, double y) { + if (out.empty() || x > out.back().x() || std::abs(y - out.back().y()) > 2. * tol) + out.emplace_back(x, y); + }; + size_t k = 0; + for (const Vec2d &p : local) { + for (; k < keep.size() && keep[k].second < p.x(); ++k) { + const auto [x0, x1] = keep[k]; + push(x0, y_local(x0)); + push(x0, c.x() * x0 + c.y()); + push(x1, c.x() * x1 + c.y()); + push(x1, y_local(x1)); + } + if (k < keep.size() && keep[k].first <= p.x() && p.x() <= keep[k].second) + continue; + push(p.x(), p.y()); + } + for (; k < keep.size(); ++k) { + const auto [x0, x1] = keep[k]; + push(x0, y_local(x0)); + push(x0, c.x() * x0 + c.y()); + push(x1, c.x() * x1 + c.y()); + push(x1, y_local(x1)); + } + for (size_t i = ib; i < f.size(); ++i) + push(f[i].x(), f[i].y()); + f = std::move(out); +} + +static std::vector path_points(const LevelPath &path, const std::vector &lines, double reach) +{ + std::vector f = path.verts; + const int nb = int(path.junctions.size()); + auto x_of = [&path](int b) { return path.verts[b + 1].x(); }; + auto sharp = [&](int b) { return lines[path.lines[b]].family != 0 && lines[path.lines[b + 1]].family != 0; }; + // The run of bends turning the same way as bend b, up to the neighbouring bends turning the other way. + auto window = [&](int b) { + int l = b - 1, r = b + 1; + while (l >= 0 && path.turn[l] == path.turn[b]) + --l; + while (r < nb && path.turn[r] == path.turn[b]) + ++r; + return std::make_pair(l >= 0 ? x_of(l) : f.front().x(), r < nb ? x_of(r) : f.back().x()); + }; + // Sharp bends between the slanted lines go last, so they win at the tip of a small triangle. + for (int b = 0; b < nb; ++b) + if (!sharp(b)) { + const auto [w0, w1] = window(b); + for (const Lin &c : path.cuts[b]) + clip_profile(f, c, path.turn[b], w0, w1, x_of(b) - reach, x_of(b) + reach, true, false); + } + for (int b = 0; b < nb; ++b) + if (sharp(b)) + for (const Lin &c : path.cuts[b]) + clip_profile(f, c, path.turn[b], x_of(b), x_of(b), x_of(b) - reach, x_of(b) + reach, false, true); + for (const auto &[c, b] : path.pushes) { + const auto [w0, w1] = window(b); + clip_profile(f, c, path.turn[b], w0, w1, x_of(b) - reach, x_of(b) + reach, true, true); + } + std::vector pts; + for (const Vec2d &p : f) { + while (pts.size() >= 2 && std::abs(cross2(Vec2d(pts.back() - pts[pts.size() - 2]), Vec2d(p - pts.back()))) <= + 1e-9 * (pts.back() - pts[pts.size() - 2]).norm() * (p - pts.back()).norm()) + pts.pop_back(); + pts.push_back(p); + } + return pts; +} + +static double polyline_length(const std::vector &pts) +{ + double len = 0.; + for (size_t i = 1; i < pts.size(); ++i) + len += (pts[i] - pts[i - 1]).norm(); + return len; +} + +// Point at the given distance along pts, and the index of the segment it lies on. +static std::pair point_along(const std::vector &pts, double t) +{ + for (size_t i = 1; i < pts.size(); ++i) { + const double len = (pts[i] - pts[i - 1]).norm(); + if (t <= len) + return { pts[i - 1] + (len > 0. ? t / len : 0.) * (pts[i] - pts[i - 1]), i }; + t -= len; + } + return { pts.back(), pts.size() - 1 }; +} + +} // namespace noncrossing + +Polylines multiline_paths(const Lines &lines_in, double d1, double end_overlap, int sweep, const BoundingBox &cover) +{ + using namespace noncrossing; + const double eps = scale_(0.002); + const Eigen::Rotation2Dd to_sweep(-sweep * M_PI / 3.); + const BoundingBoxf box(cover.min.cast(), cover.max.cast()); + + // Lines in the sweep frame, collinear pieces merged. + struct Piece { double c, s0, s1; }; + std::array, 3> pieces; + for (const Line &line : lines_in) { + Vec2d a = line.a.cast(), b = line.b.cast(); + if (!Geometry::liang_barsky_line_clipping(a, b, box) || (b - a).norm() < 10. * eps) + continue; + a = to_sweep * a; + b = to_sweep * b; + const double angle = std::atan2(b.y() - a.y(), b.x() - a.x()) / (M_PI / 3.); + if (std::abs(angle - std::round(angle)) > 0.01) + // Not one of the three families. + return {}; + const int f = (int(std::round(angle)) % 3 + 3) % 3; + const Vec2d &d = family_dir[f]; + const Vec2d n(-d.y(), d.x()); + pieces[f].push_back({ n.dot(a), std::min(d.dot(a), d.dot(b)), std::max(d.dot(a), d.dot(b)) }); + } + std::vector lines; + for (int f = 0; f < 3; ++f) { + std::vector &ps = pieces[f]; + const Vec2d &d = family_dir[f]; + const Vec2d n(-d.y(), d.x()); + const double slope = d.y() / d.x(); + std::sort(ps.begin(), ps.end(), [](const Piece &l, const Piece &r) { return l.c < r.c; }); + for (size_t i = 0; i < ps.size();) { + size_t j = i + 1; + while (j < ps.size() && ps[j].c - ps[i].c < eps) + ++j; + std::sort(ps.begin() + i, ps.begin() + j, [](const Piece &l, const Piece &r) { return l.s0 < r.s0; }); + double c = 0.; + for (size_t k = i; k < j; ++k) + c += ps[k].c / double(j - i); + double s0 = ps[i].s0, s1 = ps[i].s1; + for (size_t k = i + 1; k <= j; ++k) { + if (k < j && ps[k].s0 <= s1 + eps) { + s1 = std::max(s1, ps[k].s1); + continue; + } + const Vec2d a = s0 * d + c * n; + lines.push_back({ a, s1 * d + c * n, f, Lin(slope, a.y() - slope * a.x()), {} }); + if (k < j) { + s0 = ps[k].s0; + s1 = ps[k].s1; + } + } + i = j; + } + } + + auto along = [](const SweepLine &l, const Vec2d &p) { return family_dir[l.family].dot(p - l.a); }; + auto length = [](const SweepLine &l) { return (l.b - l.a).norm(); }; + + // Crossings, including the ends of lines stopping on another line. + std::vector junctions; + auto detect_junctions = [&]() { + struct Hit { Vec2d p; int i, j; }; + std::vector hits; + std::vector order(lines.size()); + std::iota(order.begin(), order.end(), 0); + std::sort(order.begin(), order.end(), [&lines](int l, int r) { return lines[l].a.x() < lines[r].a.x(); }); + for (size_t oi = 0; oi < order.size(); ++oi) { + const SweepLine &li = lines[order[oi]]; + for (size_t oj = oi + 1; oj < order.size() && lines[order[oj]].a.x() <= li.b.x() + eps; ++oj) { + const SweepLine &lj = lines[order[oj]]; + if (li.family == lj.family) + continue; + const double x = (lj.lin.y() - li.lin.y()) / (li.lin.x() - lj.lin.x()); + const Vec2d p(x, li.lin.x() * x + li.lin.y()); + const double ti = along(li, p), tj = along(lj, p); + if (ti > -eps && ti < length(li) + eps && tj > -eps && tj < length(lj) + eps) + hits.push_back({ p, order[oi], order[oj] }); + } + } + std::sort(hits.begin(), hits.end(), [](const Hit &l, const Hit &r) { return l.p.x() < r.p.x(); }); + junctions.clear(); + for (const Hit &hit : hits) { + int found = -1; + for (int k = int(junctions.size()) - 1; k >= 0 && junctions[k].p.x() > hit.p.x() - eps; --k) + if (std::abs(junctions[k].p.y() - hit.p.y()) < eps) { + found = k; + break; + } + if (found < 0) { + found = int(junctions.size()); + junctions.push_back({ hit.p, {}, {}, {} }); + } + std::vector &jl = junctions[found].lines; + for (int li : { hit.i, hit.j }) + if (std::find(jl.begin(), jl.end(), li) == jl.end()) + jl.push_back(li); + } + for (SweepLine &l : lines) + l.junctions.clear(); + for (int ji = 0; ji < int(junctions.size()); ++ji) + for (int li : junctions[ji].lines) + lines[li].junctions.emplace_back(junctions[ji].p.x(), ji); + for (SweepLine &l : lines) + std::sort(l.junctions.begin(), l.junctions.end()); + }; + detect_junctions(); + auto has_arm = [&](int ji, int li, bool right) { + const double t = along(lines[li], junctions[ji].p); + return right ? t < length(lines[li]) - eps : t > eps; + }; + + // A line ending on another just past a crossing stops at the crossing, where its stub would leave a hole. + auto crosses = [&](int ji, int li) { return has_arm(ji, li, false) && has_arm(ji, li, true); }; + for (int pass = 0; pass < 3; ++pass) { + std::vector touched(lines.size(), false); + bool changed = false; + for (int ji = 0; ji < int(junctions.size()); ++ji) { + const Junction &J = junctions[ji]; + if (J.lines.size() != 2 || touched[J.lines[0]] || touched[J.lines[1]] || !crosses(ji, J.lines[0]) || !crosses(ji, J.lines[1])) + continue; + // Shortest arm of each line from J to the junction where it ends on another line. + struct DeadArm { double length; bool at_b; int end; }; + std::array dead; + dead.fill({ std::numeric_limits::max(), false, -1 }); + for (int k = 0; k < 2; ++k) { + const SweepLine &l = lines[J.lines[k]]; + const size_t at = std::find_if(l.junctions.begin(), l.junctions.end(), [ji](const std::pair &j) { return j.second == ji; }) - l.junctions.begin(); + const double t = along(l, J.p); + if (at + 1 < l.junctions.size() && length(l) - along(l, junctions[l.junctions[at + 1].second].p) < eps) + dead[k] = { length(l) - t, true, l.junctions[at + 1].second }; + if (at > 0 && along(l, junctions[l.junctions[at - 1].second].p) < eps && t < dead[k].length) + dead[k] = { t, false, l.junctions[at - 1].second }; + } + const int k = dead[0].length <= dead[1].length ? 0 : 1; + if (dead[k].length >= 2. * d1) + continue; + SweepLine &l = lines[J.lines[k]]; + (dead[k].at_b ? l.b : l.a) = J.p; + // Only the shortened line and those it ended on have stale junctions until the next pass. + for (int li : junctions[dead[k].end].lines) + touched[li] = true; + changed = true; + } + if (!changed) + break; + detect_junctions(); + } + + // At every crossing the lines bounce off each other, so that every path keeps running left to right. + for (int ji = 0; ji < int(junctions.size()); ++ji) { + Junction &J = junctions[ji]; + std::vector left, right; + for (int li : J.lines) + if (has_arm(ji, li, false) && has_arm(ji, li, true)) + left.push_back(li); + right = left; + std::sort(left.begin(), left.end(), [&lines](int l, int r) { return lines[l].lin.x() > lines[r].lin.x(); }); + std::sort(right.begin(), right.end(), [&lines](int l, int r) { return lines[l].lin.x() < lines[r].lin.x(); }); + for (size_t k = 0; k < left.size(); ++k) + J.pairs.emplace_back(left[k], right[k]); + J.bends.assign(J.pairs.size(), { -1, -1 }); + } + + std::vector paths; + for (int li = 0; li < int(lines.size()); ++li) { + const SweepLine &l = lines[li]; + const int start = !l.junctions.empty() && !has_arm(l.junctions.front().second, li, false) ? l.junctions.front().second : -1; + LevelPath path; + path.start_term = start; + path.verts.push_back(start >= 0 ? junctions[start].p : l.a); + path.lines.push_back(li); + int cur = li; + double x = path.verts.front().x(); + for (;;) { + const auto &js = lines[cur].junctions; + const auto it = std::find_if(js.begin(), js.end(), [x, eps](const std::pair &j) { return j.first > x + 0.25 * eps; }); + if (it == js.end()) { + path.verts.push_back(lines[cur].b); + break; + } + Junction &J = junctions[it->second]; + const size_t k = std::find_if(J.pairs.begin(), J.pairs.end(), [cur](const std::pair &p) { return p.first == cur; }) - J.pairs.begin(); + if (k == J.pairs.size()) { + path.verts.push_back(J.p); + path.end_term = it->second; + break; + } + if (const int next = J.pairs[k].second; next != cur) { + J.bends[k] = { int(paths.size()), int(path.junctions.size()) }; + path.verts.push_back(J.p); + path.lines.push_back(next); + path.junctions.push_back(it->second); + path.turn.push_back(lines[next].lin.x() > lines[cur].lin.x() ? 1 : -1); + cur = next; + } + x = it->first; + } + path.cuts.resize(path.junctions.size()); + paths.push_back(std::move(path)); + } + + // Cut the two bends of a crossing d1 apart, no further than the neighbouring bends turning the other way. + for (const Junction &J : junctions) { + if (J.pairs.size() < 2 || J.bends.front().first < 0 || J.bends.back().first < 0) + continue; + const Vec2d n = (family_dir[lines[J.pairs.back().second].family] - family_dir[lines[J.pairs.back().first].family]).normalized(); + auto cut = [&J, &n](double offset) { + const Vec2d q = J.p + offset * n; + const double s = -n.x() / n.y(); + return Lin(s, q.y() - s * q.x()); + }; + LevelPath &lo = paths[J.bends.front().first], &hi = paths[J.bends.back().first]; + const int lb = J.bends.front().second, hb = J.bends.back().second; + if (J.pairs.size() == 3) { + lo.cuts[lb].push_back(cut(-d1)); + hi.cuts[hb].push_back(cut(d1)); + continue; + } + // Only the tip of a small triangle, between the two slanted families, stops at its neighbouring bends. + const bool sharp = lines[J.pairs.front().first].family != 0 && lines[J.pairs.front().second].family != 0; + auto room = [&](const LevelPath &P, int b, int away, double sign) { + double c = std::numeric_limits::max(); + for (int nb : { b - 1, b + 1 }) + if (sharp && nb >= 0 && nb < int(P.junctions.size()) && P.turn[nb] == away) + c = std::min(c, sign * n.dot(junctions[P.junctions[nb]].p - J.p)); + if (b == 0 && P.start_term >= 0) + c = std::min(c, sign * n.dot(P.verts.front() - J.p)); + if (b + 1 == int(P.junctions.size()) && P.end_term >= 0) + c = std::min(c, sign * n.dot(P.verts.back() - J.p)); + return std::max(c, 0.); + }; + const double c_lo = room(lo, lb, 1, -1.), c_hi = room(hi, hb, -1, 1.); + double d_lo = 0.5 * d1; + if (d1 - c_hi <= c_lo) + d_lo = std::clamp(d_lo, d1 - c_hi, c_lo); + else if (c_lo + c_hi > 0.) + d_lo = d1 * c_lo / (c_lo + c_hi); + const double d_hi = d1 - d_lo; + lo.cuts[lb].push_back(cut(-d_lo)); + hi.cuts[hb].push_back(cut(d_hi)); + auto propagate = [&](const LevelPath &P, int b, int away, int step, double offset) { + for (int nb : { b - 1, b + 1 }) + if (nb >= 0 && nb < int(P.junctions.size()) && P.turn[nb] == away) { + const Junction &W = junctions[P.junctions[nb]]; + const int k = int(std::find_if(W.pairs.begin(), W.pairs.end(), [&](const std::pair &p) { return p.first == P.lines[nb]; }) - W.pairs.begin()) + step; + if (k >= 0 && k < int(W.bends.size()) && W.bends[k].first >= 0) + paths[W.bends[k].first].pushes.emplace_back(cut(offset), W.bends[k].second); + } + }; + if (sharp) { + propagate(lo, lb, 1, -1, -d_lo - d1); + propagate(hi, hb, -1, 1, d_hi + d1); + } + } + + std::vector> geometry; + Linesf segments; + std::vector> segment_start; // path, distance along it + std::vector> kept; // stretch of each path left by the trimming + for (const LevelPath &path : paths) { + geometry.push_back(path.junctions.empty() ? std::vector{ path.verts.front(), path.verts.back() } : path_points(path, lines, 4. * d1)); + double along_path = 0.; + for (size_t i = 1; i < geometry.back().size(); ++i) { + segments.emplace_back(geometry.back()[i - 1], geometry.back()[i]); + segment_start.emplace_back(int(geometry.size()) - 1, along_path); + along_path += (geometry.back()[i] - geometry.back()[i - 1]).norm(); + } + kept.emplace_back(0., along_path); + } + + // A path ending on another line stops end_overlap inside the walls of the others, as trimmed so far. + const double end_clearance = d1 - end_overlap; + AABBTreeLines::LinesDistancer tree(segments); + auto clearance = [&](int pi, const Vec2d &q) { + double dist = std::numeric_limits::max(); + for (size_t s : tree.all_lines_in_radius(q, d1)) { + const auto [pj, start] = segment_start[s]; + const Vec2d a = segments[s].a, d = segments[s].b - a; + const double len = d.norm(), t0 = std::max(0., kept[pj].first - start), t1 = std::min(len, kept[pj].second - start); + if (pj != pi && len > 0. && t0 <= t1) + dist = std::min(dist, line_alg::distance_to(Linef(a + t0 / len * d, a + t1 / len * d), q)); + } + return dist; + }; + // Returns the length trimmed off. + auto trim_front = [&](int pi, std::vector &pts) { + const double total = polyline_length(pts), step = d1 / 32.; + double t = 0.; + while (t <= total && clearance(pi, point_along(pts, t).first) < end_clearance) + t += step; + if (t > total) { + pts.clear(); + return total; + } + if (t == 0.) + return 0.; + for (double lo = std::max(0., t - step); t - lo > step / 256.;) + if (const double mid = 0.5 * (lo + t); clearance(pi, point_along(pts, mid).first) < end_clearance) + lo = mid; + else + t = mid; + const auto [q, seg] = point_along(pts, t); + pts.erase(pts.begin(), pts.begin() + (seg - 1)); + pts.front() = q; + return t; + }; + + // A path stopping on the line of another path is trimmed first, so that it gives way to that path. + std::vector>> carried(lines.size()); // x range and path of each piece + for (int pi = 0; pi < int(paths.size()); ++pi) + for (size_t i = 0; i < paths[pi].lines.size(); ++i) + carried[paths[pi].lines[i]].emplace_back(paths[pi].verts[i].x(), paths[pi].verts[i + 1].x(), pi); + std::vector> stopping_on(paths.size()); + for (int pi = 0; pi < int(paths.size()); ++pi) + for (const auto &[ji, own] : { std::make_pair(paths[pi].start_term, paths[pi].lines.front()), std::make_pair(paths[pi].end_term, paths[pi].lines.back()) }) + if (ji >= 0) + for (int li : junctions[ji].lines) + if (li != own) + for (const auto &[x0, x1, pj] : carried[li]) + if (pj != pi && x0 - eps <= junctions[ji].p.x() && junctions[ji].p.x() <= x1 + eps) + stopping_on[pj].push_back(pi); + std::vector order; + std::vector visited(paths.size(), false); + std::function visit = [&](int pi) { + if (visited[pi]) + return; + visited[pi] = true; + for (int child : stopping_on[pi]) + visit(child); + order.push_back(pi); + }; + for (int pi = 0; pi < int(paths.size()); ++pi) + visit(pi); + std::vector> trimmed(paths.size()); + auto trim = [&](int pi) { + std::vector &pts = trimmed[pi]; + if (paths[pi].start_term >= 0) + kept[pi].first += trim_front(pi, pts); + if (paths[pi].end_term >= 0 && !pts.empty()) { + std::reverse(pts.begin(), pts.end()); + kept[pi].second -= trim_front(pi, pts); + std::reverse(pts.begin(), pts.end()); + } + if (pts.size() < 2 || polyline_length(pts) < d1) { + pts.clear(); + kept[pi] = { 0., -1. }; + } + }; + // Ends grow back where the ends they gave way to were trimmed later; the last pass only shortens them. + for (int pass = 0; pass < 3; ++pass) + for (int pi : order) { + if (pass < 2) { + trimmed[pi] = geometry[pi]; + kept[pi] = { 0., polyline_length(geometry[pi]) }; + } else if (trimmed[pi].empty()) + continue; + trim(pi); + } + + Polylines out; + const Eigen::Rotation2Dd to_world = to_sweep.inverse(); + for (const std::vector &pts : trimmed) { + if (pts.empty()) + continue; + Polyline pl; + for (const Vec2d &p : pts) { + const Vec2d w = to_world * p; + pl.points.emplace_back(coord_t(std::round(w.x())), coord_t(std::round(w.y()))); + } + out.emplace_back(std::move(pl)); + } + return out; +} + void Filler::_fill_surface_single( const FillParams ¶ms, unsigned int thickness_layers, @@ -1371,6 +1932,17 @@ void Filler::_fill_surface_single( all_polylines.reserve(lines.size()); std::transform(lines.begin(), lines.end(), std::back_inserter(all_polylines), [](const Line& l) { return Polyline{ l.a, l.b }; }); + if (params.multiline > 1) { + const double d1 = scale_(this->spacing) * params.multiline; + BoundingBox cover = get_extents(expolygon); + cover.offset(coord_t(4. * d1)); + // Rotate the family the paths run along with the layer, like the other multiline patterns. + const int sweep = int((this->layer_id / std::max(thickness_layers, 1u)) % 3); + // Line ends overlap the walls they stop on by half a line, so that they bond. + if (Polylines paths = multiline_paths(lines, d1, 0.5 * scale_(this->spacing), sweep, cover); !paths.empty()) + all_polylines = std::move(paths); + } + // Apply multiline offset if needed multiline_fill(all_polylines, params, spacing); diff --git a/src/libslic3r/Fill/FillAdaptive.hpp b/src/libslic3r/Fill/FillAdaptive.hpp index 0132ffaa6c..de0768b26e 100644 --- a/src/libslic3r/Fill/FillAdaptive.hpp +++ b/src/libslic3r/Fill/FillAdaptive.hpp @@ -48,6 +48,9 @@ FillAdaptive::OctreePtr build_octree( // If true, octree is densified below internal overhangs only. bool support_overhangs_only); +// Multiline infill: lines of the three families to non-crossing paths d1 apart, ends reaching end_overlap into walls. +Polylines multiline_paths(const Lines &lines, double d1, double end_overlap, int sweep, const BoundingBox &cover); + // // Some of the algorithms used by class FillAdaptive were inspired by // Cura Engine's class SubDivCube diff --git a/src/libslic3r/Fill/FillRectilinear.cpp b/src/libslic3r/Fill/FillRectilinear.cpp index 7edf350081..55ae50f9bb 100644 --- a/src/libslic3r/Fill/FillRectilinear.cpp +++ b/src/libslic3r/Fill/FillRectilinear.cpp @@ -3047,12 +3047,56 @@ bool FillRectilinear::fill_surface_by_multilines(const Surface *surface, FillPar return true; } +// Upper level of a cubic band [0, h] over one period, from the crossing at (0, tau) to the one at (period, tau). +// See docs/HLSD/multiline-infill.md. +static std::vector cubic_upper_level(double tau, double h, double period, double d1) +{ + const double s3 = std::sqrt(3.); + const double y_cut = std::clamp(tau - 0.5 * d1, 0., h - d1) + d1; + const double y_flat = std::min(h, h + y_cut - 2. * d1); + const double x2 = (h - tau) / s3 + d1; + const double x3 = (h + tau) / s3 - d1; + // (slope, intercept) of the rising line, its chamfer, the horizontal line, the falling chamfer and line. + const std::array lines{ Vec2d(s3, tau), Vec2d(1. / s3, h - x2 / s3), Vec2d(0., y_flat), + Vec2d(-1. / s3, h + x3 / s3), Vec2d(-s3, tau + s3 * period) }; + auto y_at = [&lines, y_cut](double x) { + double y = std::numeric_limits::max(); + for (const Vec2d &l : lines) + y = std::min(y, l.x() * x + l.y()); + return std::max(y, y_cut); + }; + + std::vector xs; + for (size_t i = 0; i < lines.size(); ++i) { + if (lines[i].x() != 0.) + xs.emplace_back((y_cut - lines[i].y()) / lines[i].x()); + for (size_t j = i + 1; j < lines.size(); ++j) + xs.emplace_back((lines[j].y() - lines[i].y()) / (lines[i].x() - lines[j].x())); + } + xs.erase(std::remove_if(xs.begin(), xs.end(), [period](double x) { return x <= 1. || x >= period - 1.; }), xs.end()); + xs.insert(xs.end(), { 0., period }); + std::sort(xs.begin(), xs.end()); + xs.erase(std::unique(xs.begin(), xs.end(), [](double a, double b) { return b - a < 1.; }), xs.end()); + + std::vector pts; + for (double x : xs) { + const Vec2d p(x, y_at(x)); + if (pts.size() >= 2) { + const Vec2d &a = pts[pts.size() - 2], &b = pts.back(); + if (std::abs((b.y() - a.y()) / (b.x() - a.x()) - (p.y() - b.y()) / (p.x() - b.x())) < EPSILON) + pts.pop_back(); + } + pts.emplace_back(p); + } + return pts; +} + bool FillRectilinear::fill_surface_trapezoidal( const Surface* surface, FillParams params, const std::initializer_list& sweep_params, Polylines& polylines_out, - int Pattern_type) // 0=grid, 1=triangular, 2=stars + int Pattern_type) // 0=grid, 1=triangular, 2=stars, 3=cubic { assert(params.multiline > 1); @@ -3350,6 +3394,55 @@ bool FillRectilinear::fill_surface_trapezoidal( break; } + case 3: // Cubic + { + // Same z shifted lines as the single-line cubic; the slanted ones cross tau above the horizontal ones. + auto pos_mod = [](double a, double m) { const double r = std::fmod(a, m); return r < 0. ? r + m : r; }; + const double h = 0.5 * std::sqrt(3.0) * period; + const double shift = scale_(std::sqrt(0.5) * this->z); + const double tau = pos_mod(-3. * shift, h); + const double y0 = pos_mod(-2. * shift, 2. * h); + + std::array, 2> levels{ cubic_upper_level(h - tau, h, period, d1), cubic_upper_level(tau, h, period, d1) }; + for (Vec2d &p : levels.front()) + p.y() = h - p.y(); + + const size_t layer_mod = infill_layer_id % 3; + const double angle = layer_mod * 2.0 * M_PI / 3.0; + + // Only cover the surface, seen in the frame the pattern is built in. + ExPolygon local = expolygon; + local.translate(-rotate_vector.second.x(), -rotate_vector.second.y()); + if (layer_mod) + local.rotate(-angle); + BoundingBox cover = get_extents(local); + cover.offset(period); + + const int64_t n_min = int64_t(std::floor((cover.min.y() - y0) / h)) - 1; + const int64_t n_max = int64_t(std::ceil((cover.max.y() - y0) / h)) + 1; + for (int64_t n = n_min; n <= n_max; ++n) { + const double x_off = (n & 1) ? 0.5 * period : 0.; + const double base = y0 + double(n) * h - tau; + const int64_t j_min = int64_t(std::floor((cover.min.x() - x_off) / period)) - 1; + const int64_t j_max = int64_t(std::ceil((cover.max.x() - x_off) / period)); + for (const std::vector &level : levels) { + Polyline row; + row.points.reserve(size_t(j_max - j_min + 1) * level.size()); + for (int64_t j = j_min; j <= j_max; ++j) + for (size_t i = (j == j_min) ? 0 : 1; i < level.size(); ++i) + row.points.emplace_back(coord_t(std::round(x_off + double(j * period) + level[i].x())), + coord_t(std::round(base + level[i].y()))); + polylines.emplace_back(std::move(row)); + } + } + + if (layer_mod) + for (Polyline &pl : polylines) + pl.rotate(angle, Point(0, 0)); + + break; + } + default: // Handle unknown pattern type break; @@ -3532,6 +3625,11 @@ Polylines FillStars::fill_surface(const Surface *surface, const FillParams ¶ Polylines FillCubic::fill_surface(const Surface *surface, const FillParams ¶ms) { Polylines polylines_out; + if (params.multiline > 1) { + if (!this->fill_surface_trapezoidal(surface, params, {}, polylines_out, 3)) + BOOST_LOG_TRIVIAL(error) << "FillCubic::fill_surface_trapezoidal() failed."; + return polylines_out; + } coordf_t dx = sqrt(0.5) * z; if (! this->fill_surface_by_multilines( surface, params, diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 40743dd5ca..1f4120a0bb 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -970,6 +970,27 @@ static std::vector get_path_of_change_filament(const Print& print) return gcode; } + // A folded tower layer is thicker than the object layer it sits on, so the height process_layer + // emitted is not the tower's. Both writers declare one, but each hardcodes a tag dialect - Type 1 + // forces s_IsBBLPrinter and writes "; LAYER_HEIGHT:", Type 2 writes ";HEIGHT:" - and the processor + // reads only its printer's, so a Type 1 tower on a non-BBL printer loses it and the merged layer + // is drawn and costed as a thin one. Declare it here, where the printer is known, unless the tower + // already wrote the right tag. _extrude puts the object's height back on the next object path, + // since process_layer forces the role to erWipeTower on any layer with a tower. + std::string WipeTowerIntegration::tower_height_tag(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, + const std::string &tcr_gcode) const + { + const std::string tag = ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height); + if (! m_sparse_layers_combined || std::abs(gcodegen.m_last_height - tcr.layer_height) <= EPSILON || + tcr_gcode.find(tag) != std::string::npos) + return {}; + // Keep m_last_height what the G-code last declared, so a second visit does not repeat it. + gcodegen.m_last_height = tcr.layer_height; + char buf[64]; + sprintf(buf, "%s%g\n", tag.c_str(), tcr.layer_height); + return buf; + } + std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_filament_id, double z) const { if (new_filament_id != -1 && new_filament_id != tcr.new_tool) @@ -1467,6 +1488,7 @@ static std::vector get_path_of_change_filament(const Print& print) config.set_key_value("filament_start_gcode", new ConfigOptionString(start_filament_gcode_str)); std::string tcr_gcode, tcr_escaped_gcode = gcodegen.placeholder_parser_process("tcr_rotated_gcode", tcr_rotated_gcode, new_filament_id, &config); unescape_string_cstyle(tcr_escaped_gcode, tcr_gcode); + gcode += tower_height_tag(gcodegen, tcr, tcr_gcode); gcode += tcr_gcode; // Count the toolchange only when the emitted block really changed the tool — // tower visits without a filament change must not advance the ordinal. @@ -1799,6 +1821,7 @@ static std::vector get_path_of_change_filament(const Print& print) std::string tcr_gcode, tcr_escaped_gcode = gcodegen.placeholder_parser_process("tcr_rotated_gcode", tcr_rotated_gcode, new_extruder_id, &config); unescape_string_cstyle(tcr_escaped_gcode, tcr_gcode); + gcode += tower_height_tag(gcodegen, tcr, tcr_gcode); gcode += tcr_gcode; check_add_eol(toolchange_gcode_str); @@ -1946,7 +1969,8 @@ static std::vector get_path_of_change_filament(const Print& print) // Calculate where the wipe tower layer will be printed. -1 means that print z will not change, // resulting in a wipe tower with sparse layers. double wipe_tower_z = -1; - bool ignore_sparse = false; + // Folded into a later, thicker layer that prints at its own z: nothing to emit. + bool ignore_sparse = wipe_tower_layer_is_combined_away(m_tool_changes[m_layer_idx]); if (m_sparse_layers_skipped) { wipe_tower_z = m_last_wipe_tower_print_z; ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]) && m_layer_idx != 0; @@ -1964,7 +1988,8 @@ static std::vector get_path_of_change_filament(const Print& print) // Calculate where the wipe tower layer will be printed. -1 means that print z will not change, // resulting in a wipe tower with sparse layers. double wipe_tower_z = -1; - bool ignore_sparse = false; + // Folded into a later, thicker layer that prints at its own z: nothing to emit. + bool ignore_sparse = wipe_tower_layer_is_combined_away(m_tool_changes[m_layer_idx]); if (m_sparse_layers_skipped) { ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]); wipe_tower_z = m_compacted_tower_z[m_layer_idx]; @@ -1994,7 +2019,7 @@ static std::vector get_path_of_change_filament(const Print& print) if (m_layer_idx >= (int) m_tool_changes.size()) return true; - bool ignore_sparse = false; + bool ignore_sparse = wipe_tower_layer_is_combined_away(m_tool_changes[m_layer_idx]); if (m_sparse_layers_skipped) ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]); @@ -3070,162 +3095,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato if (m_config.small_area_infill_flow_compensation.value && !m_config.small_area_infill_flow_compensation_model.empty()) m_small_area_infill_flow_compensator = make_unique(print.config()); - // Process file_start_gcode - written at the very top of the file, before any header - { - std::string top_gcode_template = print.config().file_start_gcode.value; - if (!top_gcode_template.empty()) { - DynamicConfig top_config; - // file_start_gcode runs before the parser copy that normally restores these, so set them here. - PlaceholderParser::update_timestamp(top_config); - PlaceholderParser::update_user_name(top_config); - top_config.set_key_value("print_time_total_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Total_Sec_Placeholder))); - top_config.set_key_value("print_time_day", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Day_Placeholder))); - top_config.set_key_value("print_time_hour", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Hour_Placeholder))); - top_config.set_key_value("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder))); - top_config.set_key_value("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder))); - top_config.set_key_value("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder))); - std::string top_gcode = print.placeholder_parser().process(top_gcode_template, 0, &top_config); - if (!top_gcode.empty()) - file.writeln(top_gcode); - } - } - - // Orca: Don't output Header block if BTT thumbnail is identified in the list - // Get the thumbnails value as a string - std::string thumbnails_value = print.config().option("thumbnails")->value; - // search string for the BTT_TFT label - bool has_BTT_thumbnail = (thumbnails_value.find("BTT_TFT") != std::string::npos); - - if(!has_BTT_thumbnail){ - file.write_format("; HEADER_BLOCK_START\n"); - // Write information on the generator. - file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); - if (is_bbl_printers) - file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); - //BBS: total layer number - file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Layer_Number_Placeholder).c_str()); - //Orca: extra check for bbl printer - if (is_bbl_printers) { - if (print.calib_params().mode == CalibMode::Calib_None) { // Don't support skipping in cali mode - // list all label_object_id with sorted order here - m_enable_exclude_object = true; - m_label_objects_ids.clear(); - m_label_objects_ids.reserve(print.num_object_instances()); - for (const PrintObject *print_object : print.objects()) - for (const PrintInstance &print_instance : print_object->instances()) - m_label_objects_ids.push_back(print_instance.model_instance->get_labeled_id()); - - std::sort(m_label_objects_ids.begin(), m_label_objects_ids.end()); - - std::string objects_id_list = "; model label id: "; - for (auto it = m_label_objects_ids.begin(); it != m_label_objects_ids.end(); it++) - objects_id_list += (std::to_string(*it) + (it != m_label_objects_ids.end() - 1 ? "," : "\n")); - file.writeln(objects_id_list); - } else { - m_enable_exclude_object = false; - m_label_objects_ids.clear(); - } - } - - { - std::string filament_density_list = "; filament_density: "; - (filament_density_list+=m_config.filament_density.serialize()) +='\n'; - file.writeln(filament_density_list); - - std::string filament_diameter_list = "; filament_diameter: "; - (filament_diameter_list += m_config.filament_diameter.serialize()) += '\n'; - file.writeln(filament_diameter_list); - - coordf_t max_height_z = -1; - for (const auto& object : print.objects()) - max_height_z = std::max(object->layers().back()->print_z, max_height_z); - - std::ostringstream max_height_z_tip; - max_height_z_tip<<"; max_z_height: " << std::fixed << std::setprecision(2) << max_height_z << '\n'; - file.writeln(max_height_z_tip.str()); - } - - { - auto used_filaments = print.get_slice_used_filaments(false); - std::ostringstream out; - out << "; filament: "; - for (size_t idx = 0; idx < used_filaments.size(); ++idx) { - if (idx != 0) - out << ','; - out << used_filaments[idx] + 1; - } - file.writeln(out.str()); - } - - file.write_format("; HEADER_BLOCK_END\n\n"); - } - - // BBS: write global config at the beginning of gcode file because printer - // need these config information - // Append full config, delimited by two 'phony' configuration keys - // CONFIG_BLOCK_START and CONFIG_BLOCK_END. The delimiters are structured - // as configuration key / value pairs to be parsable by older versions of - // PrusaSlicer G-code viewer. - { - if (is_bbl_printers && !skip_config_block) { - file.write("; CONFIG_BLOCK_START\n"); - std::string full_config; - append_full_config(print, full_config); - if (!full_config.empty()) - file.write(full_config); - - // SoftFever: write compatiple image - int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type); - file.write_format("; first_layer_bed_temperature = %d\n", - first_layer_bed_temperature); - file.write_format( - "; first_layer_temperature = %d\n", - print.config().nozzle_temperature_initial_layer.get_at(0)); - file.write("; CONFIG_BLOCK_END\n\n"); - } else if (thumbnail_cb != nullptr) { - // generate the thumbnails - auto [thumbnails, errors] = GCodeThumbnails::make_and_check_thumbnail_list(print.full_print_config()); - - if (errors != enum_bitmask()) { - std::string error_str = format("Invalid thumbnails value:"); - error_str += GCodeThumbnails::get_error_string(errors); - throw Slic3r::ExportError(error_str); - } - - if (!thumbnails.empty()) - GCodeThumbnails::export_thumbnails_to_file( - thumbnail_cb, print.get_plate_index(), thumbnails, [&file](const char* sz) { file.write(sz); }, [&print]() { print.throw_if_canceled(); }); - } - } - - - // Write some terse information on the slicing parameters. - const PrintObject *first_object = print.objects().front(); - const double layer_height = first_object->config().layer_height.value; - const double initial_layer_print_height = print.config().initial_layer_print_height.value; - for (size_t region_id = 0; region_id < print.num_print_regions(); ++ region_id) { - const PrintRegion ®ion = print.get_print_region(region_id); - file.write_format("; external perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frExternalPerimeter, layer_height).width()); - file.write_format("; perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, layer_height).width()); - file.write_format("; infill extrusion width = %.2fmm\n", region.flow(*first_object, frInfill, layer_height).width()); - file.write_format("; solid infill extrusion width = %.2fmm\n", region.flow(*first_object, frSolidInfill, layer_height).width()); - file.write_format("; top infill extrusion width = %.2fmm\n", region.flow(*first_object, frTopSolidInfill, layer_height).width()); - if (print.has_support_material()) - file.write_format("; support material extrusion width = %.2fmm\n", support_material_flow(first_object).width()); - if (print.config().initial_layer_line_width.value > 0) - file.write_format("; first layer extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, initial_layer_print_height, true).width()); - file.write_format("\n"); - } - - file.write_format("; EXECUTABLE_BLOCK_START\n"); - - // SoftFever - if( m_enable_exclude_object) - file.write(set_object_info(&print)); - - // adds tags for time estimators - file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::First_Line_M73_Placeholder).c_str()); - // Prepare the helper object for replacing placeholders in custom G-code and output filename. m_placeholder_parser_integration.parser = print.placeholder_parser(); m_placeholder_parser_integration.parser.update_timestamp(); @@ -3360,16 +3229,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Orca: Initialise AdaptivePA processor filter m_pa_processor = std::make_unique(*this, tool_ordering.all_extruders()); - // Emit machine envelope limits for the Marlin firmware. - this->print_machine_envelope(file, print); - - // Disable fan. - if (m_config.auxiliary_fan.value && print.config().close_fan_the_first_x_layers.get_at(initial_extruder_id)) { - file.write(m_writer.set_fan(0)); - //BBS: disable additional fan - file.write(m_writer.set_additional_fan(0)); - } - // Update output variables after the extruders were initialized. m_placeholder_parser_integration.init(m_writer); @@ -3721,6 +3580,157 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Sync variant-mapped params into placeholder_parser before processing start gcode update_placeholder_parser_with_variant_params(); + // Expand the file header only after the start-up placeholders and writer state are ready. + // Use the regular custom G-code path so invalid placeholders report the template name. + if (!print.config().file_start_gcode.value.empty()) + file.writeln(this->placeholder_parser_process("file_start_gcode", print.config().file_start_gcode.value, initial_extruder_id)); + + // Orca: Don't output Header block if BTT thumbnail is identified in the list + // Get the thumbnails value as a string + std::string thumbnails_value = print.config().option("thumbnails")->value; + // search string for the BTT_TFT label + bool has_BTT_thumbnail = (thumbnails_value.find("BTT_TFT") != std::string::npos); + + if(!has_BTT_thumbnail){ + file.write_format("; HEADER_BLOCK_START\n"); + // Write information on the generator. + file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); + if (is_bbl_printers) + file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); + //BBS: total layer number + file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Layer_Number_Placeholder).c_str()); + //Orca: extra check for bbl printer + if (is_bbl_printers) { + if (print.calib_params().mode == CalibMode::Calib_None) { // Don't support skipping in cali mode + // list all label_object_id with sorted order here + m_enable_exclude_object = true; + m_label_objects_ids.clear(); + m_label_objects_ids.reserve(print.num_object_instances()); + for (const PrintObject *print_object : print.objects()) + for (const PrintInstance &print_instance : print_object->instances()) + m_label_objects_ids.push_back(print_instance.model_instance->get_labeled_id()); + + std::sort(m_label_objects_ids.begin(), m_label_objects_ids.end()); + + std::string objects_id_list = "; model label id: "; + for (auto it = m_label_objects_ids.begin(); it != m_label_objects_ids.end(); it++) + objects_id_list += (std::to_string(*it) + (it != m_label_objects_ids.end() - 1 ? "," : "\n")); + file.writeln(objects_id_list); + } else { + m_enable_exclude_object = false; + m_label_objects_ids.clear(); + } + } + + { + std::string filament_density_list = "; filament_density: "; + (filament_density_list+=m_config.filament_density.serialize()) +='\n'; + file.writeln(filament_density_list); + + std::string filament_diameter_list = "; filament_diameter: "; + (filament_diameter_list += m_config.filament_diameter.serialize()) += '\n'; + file.writeln(filament_diameter_list); + + coordf_t max_height_z = -1; + for (const auto& object : print.objects()) + max_height_z = std::max(object->layers().back()->print_z, max_height_z); + + std::ostringstream max_height_z_tip; + max_height_z_tip<<"; max_z_height: " << std::fixed << std::setprecision(2) << max_height_z << '\n'; + file.writeln(max_height_z_tip.str()); + } + + { + auto used_filaments = print.get_slice_used_filaments(false); + std::ostringstream out; + out << "; filament: "; + for (size_t idx = 0; idx < used_filaments.size(); ++idx) { + if (idx != 0) + out << ','; + out << used_filaments[idx] + 1; + } + file.writeln(out.str()); + } + + file.write_format("; HEADER_BLOCK_END\n\n"); + } + + // BBS: write global config at the beginning of gcode file because printer + // need these config information + // Append full config, delimited by two 'phony' configuration keys + // CONFIG_BLOCK_START and CONFIG_BLOCK_END. The delimiters are structured + // as configuration key / value pairs to be parsable by older versions of + // PrusaSlicer G-code viewer. + { + if (is_bbl_printers && !skip_config_block) { + file.write("; CONFIG_BLOCK_START\n"); + std::string full_config; + append_full_config(print, full_config); + if (!full_config.empty()) + file.write(full_config); + + // SoftFever: write compatiple image + int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type); + file.write_format("; first_layer_bed_temperature = %d\n", + first_layer_bed_temperature); + file.write_format( + "; first_layer_temperature = %d\n", + print.config().nozzle_temperature_initial_layer.get_at(0)); + file.write("; CONFIG_BLOCK_END\n\n"); + } else if (thumbnail_cb != nullptr) { + // generate the thumbnails + auto [thumbnails, errors] = GCodeThumbnails::make_and_check_thumbnail_list(print.full_print_config()); + + if (errors != enum_bitmask()) { + std::string error_str = format("Invalid thumbnails value:"); + error_str += GCodeThumbnails::get_error_string(errors); + throw Slic3r::ExportError(error_str); + } + + if (!thumbnails.empty()) + GCodeThumbnails::export_thumbnails_to_file( + thumbnail_cb, print.get_plate_index(), thumbnails, [&file](const char* sz) { file.write(sz); }, [&print]() { print.throw_if_canceled(); }); + } + } + + + // Write some terse information on the slicing parameters. + const PrintObject *first_object = print.objects().front(); + const double layer_height = first_object->config().layer_height.value; + const double initial_layer_print_height = print.config().initial_layer_print_height.value; + for (size_t region_id = 0; region_id < print.num_print_regions(); ++ region_id) { + const PrintRegion ®ion = print.get_print_region(region_id); + file.write_format("; external perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frExternalPerimeter, layer_height).width()); + file.write_format("; perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, layer_height).width()); + file.write_format("; infill extrusion width = %.2fmm\n", region.flow(*first_object, frInfill, layer_height).width()); + file.write_format("; solid infill extrusion width = %.2fmm\n", region.flow(*first_object, frSolidInfill, layer_height).width()); + file.write_format("; top infill extrusion width = %.2fmm\n", region.flow(*first_object, frTopSolidInfill, layer_height).width()); + if (print.has_support_material()) + file.write_format("; support material extrusion width = %.2fmm\n", support_material_flow(first_object).width()); + if (print.config().initial_layer_line_width.value > 0) + file.write_format("; first layer extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, initial_layer_print_height, true).width()); + file.write_format("\n"); + } + + file.write_format("; EXECUTABLE_BLOCK_START\n"); + + // SoftFever + if( m_enable_exclude_object) + file.write(set_object_info(&print)); + + // adds tags for time estimators + file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::First_Line_M73_Placeholder).c_str()); + + // Emit machine envelope limits for the Marlin firmware. + this->print_machine_envelope(file, print); + + // Disable fan. + if (m_config.auxiliary_fan.value && print.config().close_fan_the_first_x_layers.get_at(initial_extruder_id)) { + file.write(m_writer.set_fan(0)); + //BBS: disable additional fan + file.write(m_writer.set_additional_fan(0)); + } + std::string machine_start_gcode = this->placeholder_parser_process("machine_start_gcode", print.config().machine_start_gcode.value, initial_extruder_id); if (print.config().gcode_flavor != gcfKlipper) { // Set bed temperature if the start G-code does not contain any bed temp control G-codes. diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 5208c4a5f3..41986cc1d6 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -107,7 +107,8 @@ public: m_is_first_print(true), m_print_config(&print_config), m_last_wipe_tower_print_z(print_config.z_offset.value), - m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(print_config)) + m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(print_config)), + m_sparse_layers_combined(wipe_tower_sparse_layers_combined(print_config)) { // Precomputed rather than accumulated while emitting, so that the clearance validator and // the emitter cannot disagree about where the compacted tower sits on any given layer. @@ -138,6 +139,7 @@ public: private: WipeTowerIntegration& operator=(const WipeTowerIntegration&); std::string append_tcr(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const; + std::string tower_height_tag(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, const std::string &tcr_gcode) const; Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const Polygons &bed_polygons) const; std::string append_tcr2(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const; std::string travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const; @@ -175,6 +177,9 @@ private: // wipe_tower_no_sparse_layers, as answered by the shared compaction rule rather than by the raw // option: smooth timelapse and wrapping detection keep a tower on every layer regardless. const bool m_sparse_layers_skipped; + // Combined tower layers are thicker than the object layer they sit on, the only case where the + // tower's height is not the one process_layer already declared. + const bool m_sparse_layers_combined; // Print z of the compacted tower per planned layer. Empty when the tower is not compacted. std::vector m_compacted_tower_z; }; diff --git a/src/libslic3r/GCode/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp index eaf8a2eaf3..a700ada09f 100644 --- a/src/libslic3r/GCode/WipeTower.cpp +++ b/src/libslic3r/GCode/WipeTower.cpp @@ -49,6 +49,48 @@ std::vector compute_compacted_wipe_tower_z(const std::vector &layer_tool_changes) +{ + return ! layer_tool_changes.empty() && layer_tool_changes.front().combined_away; +} + +std::vector combine_sparse_wipe_tower_layers(std::vector &layer_height, + const std::vector &layer_is_sparse, + const std::vector &max_layer_height, + size_t first_layer_idx) +{ + assert(layer_is_sparse.size() == layer_height.size() && max_layer_height.size() == layer_height.size()); + std::vector combined_away(layer_height.size(), 0); + float pending_height = 0.f; // what the layers folded away so far add up to + for (size_t i = 0; i < layer_height.size(); ++i) { + // A toolchange has to purge at its own z, so it neither folds away nor takes over the run + // below it - and a run always flushes on its own last layer, so nothing is ever pending here. + if (! layer_is_sparse[i] || i <= first_layer_idx) { + pending_height = 0.f; + continue; + } + const float merged = pending_height + layer_height[i]; + // Hand the run on only if the next layer can swallow the whole thing; a layer already past + // the cap is left alone rather than shrunk. + const bool next_takes_it = i + 1 < layer_height.size() && layer_is_sparse[i + 1] && + merged + layer_height[i + 1] <= max_layer_height[i + 1] + float(EPSILON); + if (next_takes_it) { + combined_away[i] = 1; + pending_height = merged; + } else { + layer_height[i] = merged; + pending_height = 0.f; + } + } + return combined_away; +} + inline float align_round(float value, float base) { return std::round(value / base) * base; @@ -1904,6 +1946,7 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi //m_bridging(float(config.wipe_tower_bridging)), m_bridging(10.f), m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)), + m_sparse_layers_combined(wipe_tower_sparse_layers_combined(config)), m_gcode_flavor(config.gcode_flavor), m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))), m_current_tool(initial_tool), @@ -2028,6 +2071,16 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config) float nozzle_diameter = float(config.nozzle_diameter.get_at(idx)); m_filpar[idx].nozzle_diameter = nozzle_diameter; // to be used in future with (non-single) multiextruder MM + // Orca: max_layer_height is per nozzle, so read it through the filament->nozzle map rather than + // by filament id. Zero means three quarters of the nozzle diameter, as in Slicing.cpp. + { + const std::vector &filament_map = config.filament_map.values; // 1 based nozzle indices + const size_t nozzle_idx = idx < filament_map.size() && filament_map[idx] > 0 ? size_t(filament_map[idx] - 1) : 0; + const float max_layer_height = float(config.max_layer_height.get_at(nozzle_idx)); + m_filpar[idx].max_layer_height = max_layer_height > 0.f ? max_layer_height + : 0.75f * float(config.nozzle_diameter.get_at(nozzle_idx)); + } + float max_vol_speed = float(config.filament_max_volumetric_speed.get_at(idx)); if (max_vol_speed!= 0.f) m_filpar[idx].max_e_speed = (max_vol_speed / filament_area()); @@ -3001,7 +3054,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer(bool extrude_perimeter, bool // Ask our writer about how much material was consumed. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled. - if (! m_sparse_layers_skipped || toolchanges_on_layer) + if (layer_is_printed(toolchanges_on_layer)) if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); @@ -3898,7 +3951,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer_new(bool extrude_perimeter, // Ask our writer about how much material was consumed. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled. - if (!m_sparse_layers_skipped || toolchanges_on_layer) + if (layer_is_printed(toolchanges_on_layer)) if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); @@ -4008,7 +4061,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block(const WipeTowerBlock &block, // Ask our writer about how much material was consumed. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled. - if (!m_sparse_layers_skipped || toolchanges_on_layer) + if (layer_is_printed(toolchanges_on_layer)) if (filament_id < m_used_filament_length.size()) m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length(); @@ -4125,7 +4178,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block_solid(const WipeTowerBlock & // Ask our writer about how much material was consumed. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled. - if (!m_sparse_layers_skipped || toolchanges_on_layer) + if (layer_is_printed(toolchanges_on_layer)) if (filament_id < m_used_filament_length.size()) m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length(); @@ -4668,6 +4721,15 @@ void WipeTower::calc_block_infill_gap() m_extra_spacing = 1.f; } +// A folded layer is generated like any other but thrown away by the emitter, so its extrusions must +// not be charged to the filament used. +bool WipeTower::layer_is_printed(bool toolchanges_on_layer) const +{ + if (m_layer_info != m_plan.end() && m_layer_info->combined_away) + return false; + return ! m_sparse_layers_skipped || toolchanges_on_layer; +} + void WipeTower::plan_tower_new() { if (m_wipe_tower_brim_width < 0) m_wipe_tower_brim_width = get_auto_brim_by_height(m_wipe_tower_height); @@ -4820,6 +4882,9 @@ void WipeTower::generate_new(std::vector tool_changes; @@ -700,6 +711,62 @@ std::vector compute_compacted_wipe_tower_z(const std::vector &layer_tool_changes); + +// Folds runs of sparse layers into one. layer_height is raised in place on the layer that prints a +// run - always its last, so the merged extrusion lands on top of what it covers - and the returned +// mask marks the layers that now print nothing. A run stops growing once one more layer would pass +// max_layer_height of the nozzle that prints it. first_layer_idx and below never combine: the +// tower's first layer carries the brim. +std::vector combine_sparse_wipe_tower_layers(std::vector &layer_height, + const std::vector &layer_is_sparse, + const std::vector &max_layer_height, + size_t first_layer_idx); + +// Applies the rule above to a planned tower. Either generator's plan fits: both carry height, +// tool_changes and combined_away per layer, and index their filament parameters by tool. +template +void combine_sparse_wipe_tower_plan(PlanLayers &plan, const FilamentParams &filpar, size_t first_layer_idx, size_t initial_tool) +{ + const size_t n = plan.size(); + std::vector heights(n); + std::vector sparse(n); + std::vector caps(n); + + // A layer with no toolchange prints with the filament the layer below left loaded. + size_t tool = initial_tool; + for (const auto &layer : plan) + if (! layer.tool_changes.empty()) { + tool = layer.tool_changes.front().old_tool; + break; + } + for (size_t i = 0; i < n; ++i) { + heights[i] = plan[i].height; + sparse[i] = plan[i].tool_changes.empty() ? 1 : 0; + caps[i] = tool < filpar.size() ? filpar[tool].max_layer_height : 0.f; + if (! plan[i].tool_changes.empty()) + tool = plan[i].tool_changes.back().new_tool; + } + + const std::vector combined_away = combine_sparse_wipe_tower_layers(heights, sparse, caps, first_layer_idx); + for (size_t i = 0; i < n; ++i) { + plan[i].height = heights[i]; + plan[i].combined_away = combined_away[i] != 0; + } +} + + } // namespace Slic3r #endif // WipeTowerPrusaMM_hpp_ diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index 0080094270..a576172a39 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -1033,6 +1033,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau m_z_pos(0.f), m_bridging(float(config.wipe_tower_bridging)), m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)), + m_sparse_layers_combined(wipe_tower_sparse_layers_combined(config)), m_gcode_flavor(config.gcode_flavor), m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))), m_infill_speed(default_region_config.sparse_infill_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))), @@ -1150,6 +1151,16 @@ void WipeTower2::set_extruder(size_t idx, const PrintConfig& config) float nozzle_diameter = float(config.nozzle_diameter.get_at(idx)); m_filpar[idx].nozzle_diameter = nozzle_diameter; // to be used in future with (non-single) multiextruder MM + // Orca: max_layer_height is per nozzle, so read it through the filament->nozzle map rather than + // by filament id. Zero means three quarters of the nozzle diameter, as in Slicing.cpp. + { + const std::vector &filament_map = config.filament_map.values; // 1 based nozzle indices + const size_t nozzle_idx = idx < filament_map.size() && filament_map[idx] > 0 ? size_t(filament_map[idx] - 1) : 0; + const float max_layer_height = float(config.max_layer_height.get_at(nozzle_idx)); + m_filpar[idx].max_layer_height = max_layer_height > 0.f ? max_layer_height + : 0.75f * float(config.nozzle_diameter.get_at(nozzle_idx)); + } + float max_vol_speed = float(config.filament_max_volumetric_speed.get_at(idx)); if (max_vol_speed!= 0.f) m_filpar[idx].max_e_speed = (max_vol_speed / filament_area()); @@ -2103,7 +2114,9 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer() // Ask our writer about how much material was consumed. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled. - if (! m_sparse_layers_skipped || toolchanges_on_layer || first_layer) { + // A folded layer prints nothing, so it consumes nothing and adds no height of its own. + const bool combined_away = m_layer_info != m_plan.end() && m_layer_info->combined_away; + if ((! m_sparse_layers_skipped || toolchanges_on_layer || first_layer) && ! combined_away) { if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); m_current_height += m_layer_info->height; @@ -2435,6 +2448,10 @@ void WipeTower2::generate(std::vector> if (m_plan.empty()) return; + // Before planning: the layer heights this rewrites feed the extrusion flow of every later pass. + if (m_sparse_layers_combined) + combine_sparse_wipe_tower_plan(m_plan, m_filpar, m_first_layer_idx, m_current_tool); + plan_tower(); #if 1 for (int i=0;i<5;++i) { @@ -2533,6 +2550,10 @@ void WipeTower2::generate(std::vector> layer_result[idx] = merge_tcr(layer_result[idx], finish_layer_tcr); } + if (layer.combined_away) + for (WipeTower::ToolChangeResult &tcr : layer_result) + tcr.combined_away = true; + result.emplace_back(std::move(layer_result)); if (m_used_filament_length_until_layer.empty() || m_used_filament_length_until_layer.back().first != layer.z) diff --git a/src/libslic3r/GCode/WipeTower2.hpp b/src/libslic3r/GCode/WipeTower2.hpp index eb3a2562fb..02ce15672b 100644 --- a/src/libslic3r/GCode/WipeTower2.hpp +++ b/src/libslic3r/GCode/WipeTower2.hpp @@ -200,6 +200,8 @@ public: float tower_interface_pre_extrusion_length = 0.f; float tower_ironing_area = 4.f; float tower_interface_purge_length = 0.f; + // Tallest layer this filament's nozzle can lay down; caps the sparse layer combination. + float max_layer_height = 0.f; }; private: @@ -268,6 +270,7 @@ private: float m_extra_loading_move = 0.f; float m_bridging = 0.f; bool m_sparse_layers_skipped = false; + bool m_sparse_layers_combined = false; bool m_set_extruder_trimpot = false; bool m_adhesion = true; GCodeFlavor m_gcode_flavor; @@ -368,6 +371,8 @@ private: float z; // z position of the layer float height; // layer height float depth; // depth of the layer based on all layers above + // Folded into a later, thicker layer, so this one prints nothing at all. + bool combined_away{false}; float toolchanges_depth() const { float sum = 0.f; for (const auto &a : tool_changes) sum += a.required_depth; return sum; } std::vector tool_changes; diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 0a138834a7..1f1f1289e5 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -2256,13 +2256,45 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim // ORCA: // Inner Outer Inner wall ordering mode perimeter order optimisation functions + +// Whether two Arachne lines touch: somewhere the gap between their centrelines is no more than the +// touching distance there. Each junction of one line is measured against the segments of the other, +// both ways, and the search stops at the first spot that touches. +// Arachne varies line width to fill the region (e.g. the odd centre line of a narrow wall is wider +// than nominal), so the touching distance is half the combined width at the closest points, not the +// nominal spacing. Widths are taken locally so a line widened in one place (a wedge tip, a wall +// transition) does not count as touching where it passes close to other perimeters. min_threshold keeps +// the nominal spacing threshold as the lower bound. +static bool arachne_lines_touch(const Arachne::ExtrusionLine &a, const Arachne::ExtrusionLine &b, double min_threshold) +{ + auto one_way = [min_threshold](const Arachne::ExtrusionLine &from, const Arachne::ExtrusionLine &to) { + for (const Arachne::ExtrusionJunction &j : from.junctions) { + const Vec2d p = j.p.cast(); + for (size_t k = 0; k + 1 < to.junctions.size(); ++k) { + const Arachne::ExtrusionJunction &j0 = to.junctions[k]; + const Arachne::ExtrusionJunction &j1 = to.junctions[k + 1]; + const Vec2d s0 = j0.p.cast(); + const Vec2d seg = j1.p.cast() - s0; + const double l2 = seg.squaredNorm(); + const double t = l2 > 0. ? std::clamp((p - s0).dot(seg) / l2, 0., 1.) : 0.; + const double w = double(j0.w) + t * double(j1.w - j0.w); // width of `to` at the closest point + const double touch_distance = std::max(min_threshold, 0.5 * (double(j.w) + w)); + if ((s0 + t * seg - p).norm() <= touch_distance) + return true; + } + } + return false; + }; + return one_way(a, b) || one_way(b, a); +} + /** * @brief Finds all perimeters touching a given set of reference lines, given as indexes. * * @param entities The list of PerimeterGeneratorArachneExtrusion entities. * @param referenceIndices A set of indices representing the reference points. - * @param threshold_external The distance threshold to consider for proximity for a reference perimeter with inset index 0 - * @param threshold_internal The distance threshold to consider for proximity for a reference perimeter with inset index 1+ + * @param threshold_external The minimum touching distance for a reference perimeter with inset index 0 + * @param threshold_internal The minimum touching distance for a reference perimeter with inset index 1+ * @param considered_inset_idx What perimeter inset index are we searching for (eg. if we are searching for first internal perimeters proximate to the current reference perimeter, this value should be set to 1 etc). * @return std::vector A vector of indices representing the touching perimeters. */ @@ -2271,7 +2303,6 @@ std::vector findAllTouchingPerimeters(const std::vector 0) continue; @@ -2282,15 +2313,9 @@ std::vector findAllTouchingPerimeters(const std::vectorinset_idx == 0) - threshold = threshold_external; - else - threshold = threshold_internal; - if (distance <= threshold) { + // Add to touchingIndices if the lines touch. + const double threshold = double(referenceEntity.extrusion->inset_idx == 0 ? threshold_external : threshold_internal); + if (arachne_lines_touch(*referenceEntity.extrusion, *entity.extrusion, threshold)) { touchingIndices.insert(i); } } diff --git a/src/libslic3r/PerimeterGenerator.hpp b/src/libslic3r/PerimeterGenerator.hpp index 4b8f84ca5f..cd49b4acdd 100644 --- a/src/libslic3r/PerimeterGenerator.hpp +++ b/src/libslic3r/PerimeterGenerator.hpp @@ -2,6 +2,7 @@ #define slic3r_PerimeterGenerator_hpp_ #include "libslic3r.h" +#include #include #include "Layer.hpp" #include "Flow.hpp" @@ -105,6 +106,8 @@ public: bool has_fuzzy_hole = false; // Preserve construction order so overlap precedence remains deterministic. std::vector> regions_by_fuzzify; + // Area resting on the layer below, where fuzzy skin is allowed. Unset means no restriction. + std::optional fuzzy_supported_area; PerimeterGenerator( // Input: diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 214ef229df..9af8969490 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1199,6 +1199,7 @@ static std::vector s_Preset_print_options{ "enable_tower_interface_features", "enable_tower_interface_cooldown_during_tower", "wipe_tower_no_sparse_layers", + "wipe_tower_sparse_layers_combination", "compatible_printers", "compatible_printers_condition", "inherits", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 477c2c74ca..00398f9ebb 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -378,6 +378,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "wipe_tower_bridging" || opt_key == "wipe_tower_extra_flow" || opt_key == "wipe_tower_no_sparse_layers" + || opt_key == "wipe_tower_sparse_layers_combination" || opt_key == "flush_volumes_matrix" || opt_key == "prime_volume" || opt_key == "flush_into_infill" diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 9f317d21af..1d994d7874 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6705,6 +6705,20 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(false)); + def = this->add("wipe_tower_sparse_layers_combination", coBool); + def->label = L("Combine sparse layers"); + def->tooltip = L("If enabled, consecutive layers on which the prime tower has no filament change are printed as a single " + "thicker tower layer instead of one thin layer each, the same way infill combination merges sparse infill. " + "The merged layer is printed at the top of the run, at the height of everything it covers.\n\n" + "Only whole layers are merged, and never past the maximum layer height of the nozzle printing the tower " + "(three quarters of the nozzle diameter when that is left at 0). Two or more layers therefore have to fit " + "under that limit before anything changes at all: at a 0.2 mm layer height under a 0.3 mm maximum nothing " + "is merged, while at 0.1 mm three layers become one.\n\n" + "Unlike \"No sparse layers\" the tower keeps following the model, so the toolhead never has to reach down to it. " + "Has no effect with \"No sparse layers\", smooth timelapse or clumping detection, which need a tower on every layer."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("single_extruder_multi_material_priming", coBool); def->label = L("Prime all printing extruders"); def->tooltip = L("If enabled, all printing extruders will be primed at the front edge of the print bed at the start of the print."); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 9a485ff271..679811d699 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -163,6 +163,7 @@ inline bool is_smoothable_infill_pattern(InfillPattern pattern, int multiline = case ipGrid: case ipTriangles: case ipStars: + case ipCubic: return multiline > 1; default: return false; @@ -1631,6 +1632,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionString, toolchange_cyclic_order)) ((ConfigOptionBool, toolchange_cyclic_first_layer)) ((ConfigOptionBool, wipe_tower_no_sparse_layers)) + ((ConfigOptionBool, wipe_tower_sparse_layers_combination)) ((ConfigOptionString, change_filament_gcode)) ((ConfigOptionString, change_extrusion_role_gcode)) ((ConfigOptionString, process_change_extrusion_role_gcode)) diff --git a/src/slic3r/GUI/AmsMappingPopup.cpp b/src/slic3r/GUI/AmsMappingPopup.cpp index 22ffaef034..32a580845d 100644 --- a/src/slic3r/GUI/AmsMappingPopup.cpp +++ b/src/slic3r/GUI/AmsMappingPopup.cpp @@ -819,7 +819,7 @@ AmsMapingPopup::AmsMapingPopup(wxWindow *parent, bool use_in_sync_dialog) : m_scrolled_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxHSCROLL); m_scrolled_window->SetBackgroundColour(*wxWHITE); - m_scrolled_window->SetScrollRate(0, FromDIP(10)); + m_scrolled_window->SetScrollRate(0, FromDIP(20)); wxBoxSizer *title_sizer_h= new wxBoxSizer(wxHORIZONTAL); wxBoxSizer *title_sizer_v = new wxBoxSizer(wxVERTICAL); @@ -1000,13 +1000,13 @@ AmsMapingPopup::AmsMapingPopup(wxWindow *parent, bool use_in_sync_dialog) : show_pos.x = screen_size.GetLeft(); m_scrolled_window->SetMaxSize(wxSize(screen_size.GetWidth(), popup_height)); m_scrolled_window->SetMinSize(wxSize(screen_size.GetWidth(), popup_height)); - m_scrolled_window->SetScrollRate(FromDIP(10), FromDIP(10)); + m_scrolled_window->SetScrollRate(FromDIP(10), FromDIP(20)); } else { m_scrolled_window->SetMaxSize(wxSize(popup_width, popup_height)); m_scrolled_window->SetMinSize(wxSize(popup_width, popup_height)); - m_scrolled_window->SetScrollRate(0, FromDIP(10)); + m_scrolled_window->SetScrollRate(0, FromDIP(20)); } //Vertical Direction Processing @@ -2117,7 +2117,7 @@ void AmsReplaceMaterialDialog::create() identical_filament->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#009688"))); // Orca: accent teal (not brand green) m_scrollview_groups = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL); - m_scrollview_groups->SetScrollRate(5, 5); + m_scrollview_groups->SetScrollRate(5, FromDIP(20)); //m_scrollview_groups->SetMinSize(wxSize(400, 400)); //m_scrollview_groups->SetMaxSize(wxSize(400, 400)); m_scrollview_sizer = new wxBoxSizer(wxVERTICAL); diff --git a/src/slic3r/GUI/Auxiliary.cpp b/src/slic3r/GUI/Auxiliary.cpp index 14b8d238a2..444daa4f91 100644 --- a/src/slic3r/GUI/Auxiliary.cpp +++ b/src/slic3r/GUI/Auxiliary.cpp @@ -606,7 +606,7 @@ AuFolderPanel::AuFolderPanel(wxWindow *parent, AuxiliaryFolderType type, wxWindo wxBoxSizer *sizer_main = new wxBoxSizer(wxVERTICAL); m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL); - m_scrolledWindow->SetScrollRate(5, 5); + m_scrolledWindow->SetScrollRate(5, FromDIP(20)); wxBoxSizer *sizer_body = new wxBoxSizer(wxVERTICAL); wxBoxSizer *sizer_top = new wxBoxSizer(wxHORIZONTAL); diff --git a/src/slic3r/GUI/BindDialog.cpp b/src/slic3r/GUI/BindDialog.cpp index 4c6d3c249d..883765751b 100644 --- a/src/slic3r/GUI/BindDialog.cpp +++ b/src/slic3r/GUI/BindDialog.cpp @@ -558,7 +558,7 @@ PingCodeBindDialog::~PingCodeBindDialog() { //show bind failed info m_sw_bind_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(450), FromDIP(300)), wxVSCROLL); m_sw_bind_failed_info->SetBackgroundColour(*wxWHITE); - m_sw_bind_failed_info->SetScrollRate(5, 5); + m_sw_bind_failed_info->SetScrollRate(5, FromDIP(20)); m_sw_bind_failed_info->SetMinSize(wxSize(FromDIP(450), FromDIP(90))); m_sw_bind_failed_info->SetMaxSize(wxSize(FromDIP(450), FromDIP(90))); diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index d6e0329ef8..0b4f86a618 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -3521,7 +3521,7 @@ DesignPanel::DesignPanel(wxWindow* parent) cards->Show(m_box_mate, false, true); m_form->FitInside(); - m_form->SetScrollRate(0, 10); // vertical only, like Prepare's sidebar: never scroll labels out + m_form->SetScrollRate(0, FromDIP(20)); // vertical only, like Prepare's sidebar: never scroll labels out m_form->SetMinSize(wxSize(264, -1)); // Right column: a small view toolbar over the live 3D viewport that mirrors diff --git a/src/slic3r/GUI/CaliHistoryDialog.cpp b/src/slic3r/GUI/CaliHistoryDialog.cpp index 3ce8c1bb81..7055f441ac 100644 --- a/src/slic3r/GUI/CaliHistoryDialog.cpp +++ b/src/slic3r/GUI/CaliHistoryDialog.cpp @@ -115,7 +115,7 @@ HistoryWindow::HistoryWindow(wxWindow* parent, const std::vector& auto main_sizer = new wxBoxSizer(wxVERTICAL); auto scroll_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL); - scroll_window->SetScrollRate(5, 5); + scroll_window->SetScrollRate(5, FromDIP(20)); scroll_window->SetBackgroundColour(*wxWHITE); scroll_window->SetMinSize(HISTORY_WINDOW_SIZE); scroll_window->SetSize(HISTORY_WINDOW_SIZE); diff --git a/src/slic3r/GUI/CalibrationPanel.cpp b/src/slic3r/GUI/CalibrationPanel.cpp index fca9fa2be9..d906a8211c 100644 --- a/src/slic3r/GUI/CalibrationPanel.cpp +++ b/src/slic3r/GUI/CalibrationPanel.cpp @@ -207,7 +207,7 @@ SelectMObjectPopup::SelectMObjectPopup(wxWindow* parent) m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, SELECT_MACHINE_LIST_SIZE, wxHSCROLL | wxVSCROLL); m_scrolledWindow->SetBackgroundColour(*wxWHITE); m_scrolledWindow->SetMinSize(SELECT_MACHINE_LIST_SIZE); - m_scrolledWindow->SetScrollRate(0, 5); + m_scrolledWindow->SetScrollRate(0, SELECT_MACHINE_ITEM_SIZE.y); auto m_sizxer_scrolledWindow = new wxBoxSizer(wxVERTICAL); m_scrolledWindow->SetSizer(m_sizxer_scrolledWindow); m_scrolledWindow->Layout(); diff --git a/src/slic3r/GUI/CalibrationWizard.cpp b/src/slic3r/GUI/CalibrationWizard.cpp index fa42143f06..4d6e694380 100644 --- a/src/slic3r/GUI/CalibrationWizard.cpp +++ b/src/slic3r/GUI/CalibrationWizard.cpp @@ -81,7 +81,7 @@ CalibrationWizard::CalibrationWizard(wxWindow* parent, CalibMode mode, wxWindowI wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL); - m_scrolledWindow->SetScrollRate(5, 5); + m_scrolledWindow->SetScrollRate(5, FromDIP(20)); m_scrolledWindow->SetBackgroundColour(*wxWHITE); wxBoxSizer* padding_sizer = new wxBoxSizer(wxHORIZONTAL); diff --git a/src/slic3r/GUI/CalibrationWizardPage.cpp b/src/slic3r/GUI/CalibrationWizardPage.cpp index c117ec8857..ff02feab68 100644 --- a/src/slic3r/GUI/CalibrationWizardPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardPage.cpp @@ -834,7 +834,7 @@ void CaliPageSendingPanel::create(wxWindow* parent) m_sw_print_failed_info = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(380), FromDIP(125)), wxVSCROLL); m_sw_print_failed_info->SetBackgroundColour(*wxWHITE); - m_sw_print_failed_info->SetScrollRate(0, 5); + m_sw_print_failed_info->SetScrollRate(0, FromDIP(20)); m_sw_print_failed_info->SetMinSize(wxSize(FromDIP(380), FromDIP(125))); m_sw_print_failed_info->SetMaxSize(wxSize(FromDIP(380), FromDIP(125))); diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index f40c71ec4a..5a58ec7b6c 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -1046,6 +1046,8 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in // Orca: both tower generators skip sparse layers, so this is not a wipe tower 2 exclusive. toggle_line("wipe_tower_no_sparse_layers", have_prime_tower); + // Dropping the sparse layers outright leaves nothing to combine, so the two are exclusive. + toggle_line("wipe_tower_sparse_layers_combination", have_prime_tower && !config->opt_bool("wipe_tower_no_sparse_layers")); WipeTowerWallType wipe_tower_wall_type = config->opt_enum("wipe_tower_wall_type"); bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower; diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 412df19091..7aaa0e7865 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -2710,7 +2710,7 @@ ConfigWizard::ConfigWizard(wxWindow *parent) SetSizerAndFit(vsizer); // We can now enable scrolling on hscroll - p->hscroll->SetScrollRate(30, 30); + p->hscroll->SetScrollRate(30, FromDIP(20)); on_window_geometry(this, [this]() { p->init_dialog_size(); diff --git a/src/slic3r/GUI/CreatePresetsDialog.cpp b/src/slic3r/GUI/CreatePresetsDialog.cpp index e6fb6e0782..16b9a5765d 100644 --- a/src/slic3r/GUI/CreatePresetsDialog.cpp +++ b/src/slic3r/GUI/CreatePresetsDialog.cpp @@ -699,7 +699,7 @@ CreateFilamentPresetDialog::CreateFilamentPresetDialog(wxWindow *parent) m_scrolled_preset_panel = new wxScrolledWindow(this, wxID_ANY); m_scrolled_preset_panel->SetMaxSize(wxSize(-1, FromDIP(350))); m_scrolled_preset_panel->SetBackgroundColour(*wxWHITE); - m_scrolled_preset_panel->SetScrollRate(5, 5); + m_scrolled_preset_panel->SetScrollRate(5, FromDIP(20)); m_scrolled_sizer = new wxBoxSizer(wxVERTICAL); m_scrolled_sizer->Add(create_item(FilamentOptionType::PRESET_FOR_PRINTER), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5)); m_scrolled_sizer->Add(0, 0, 0, wxTOP, FromDIP(5)); @@ -1593,7 +1593,7 @@ CreatePrinterPresetDialog::CreatePrinterPresetDialog(wxWindow *parent) m_page1 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_page1->SetBackgroundColour(*wxWHITE); - m_page1->SetScrollRate(5, 5); + m_page1->SetScrollRate(5, FromDIP(20)); m_page2 = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_page2->SetBackgroundColour(*wxWHITE); create_printer_page1(m_page1); @@ -2652,7 +2652,7 @@ wxBoxSizer *CreatePrinterPresetDialog::create_presets_template_item(wxWindow *pa wxBoxSizer *vertical_sizer = new wxBoxSizer(wxVERTICAL); m_scrolled_preset_window = new wxScrolledWindow(parent); - m_scrolled_preset_window->SetScrollRate(5, 5); + m_scrolled_preset_window->SetScrollRate(5, FromDIP(20)); m_scrolled_preset_window->SetBackgroundColour(*wxWHITE); //m_scrolled_preset_window->SetMinSize(wxSize(FromDIP(1500), FromDIP(-1))); m_scrolled_preset_window->SetMaxSize(wxSize(FromDIP(1500), FromDIP(-1))); @@ -4297,7 +4297,7 @@ wxBoxSizer *ExportConfigsDialog::create_select_printer(wxWindow *parent) optionSizer->SetMinSize(OPTION_SIZE); horizontal_sizer->Add(optionSizer, 0, wxEXPAND | wxALL, FromDIP(10)); m_scrolled_preset_window = new wxScrolledWindow(parent); - m_scrolled_preset_window->SetScrollRate(5, 5); + m_scrolled_preset_window->SetScrollRate(5, FromDIP(20)); m_scrolled_preset_window->SetBackgroundColour(*wxWHITE); m_scrolled_preset_window->SetMaxSize(wxSize(FromDIP(660), FromDIP(400))); m_scrolled_preset_window->SetSize(wxSize(FromDIP(660), FromDIP(400))); @@ -4736,7 +4736,7 @@ wxBoxSizer *EditFilamentPresetDialog::create_preset_tree_sizer() { wxBoxSizer *filament_preset_tree_sizer = new wxBoxSizer(wxHORIZONTAL); m_preset_tree_window = new wxScrolledWindow(this); - m_preset_tree_window->SetScrollRate(5, 5); + m_preset_tree_window->SetScrollRate(5, FromDIP(20)); m_preset_tree_window->SetBackgroundColour(PRINTER_LIST_COLOUR); m_preset_tree_window->SetMinSize(wxSize(-1, FromDIP(400))); m_preset_tree_window->SetMaxSize(wxSize(-1, FromDIP(300))); diff --git a/src/slic3r/GUI/DeviceErrorDialog.cpp b/src/slic3r/GUI/DeviceErrorDialog.cpp index 9790db67bd..4e81135a75 100644 --- a/src/slic3r/GUI/DeviceErrorDialog.cpp +++ b/src/slic3r/GUI/DeviceErrorDialog.cpp @@ -39,7 +39,7 @@ DeviceErrorDialog::DeviceErrorDialog(MachineObject* obj, wxWindow* parent, wxWin m_line_top->SetBackgroundColour(wxColour(166, 169, 170)); m_scroll_area = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - m_scroll_area->SetScrollRate(0, 5); + m_scroll_area->SetScrollRate(0, FromDIP(20)); m_scroll_area->SetBackgroundColour(*wxWHITE); m_scroll_area->SetMinSize(wxSize(FromDIP(320), FromDIP(250))); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 71caeb9ee3..ffa067308c 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -7281,6 +7281,16 @@ void GLCanvas3D::_resize(unsigned int w, unsigned int h) m_last_w = w; m_last_h = h; + set_imgui_scaling(); + + this->request_extra_frame(); + + // ensures that this canvas is current + _set_current(); +} + +void GLCanvas3D::set_imgui_scaling() +{ float font_size = wxGetApp().em_unit(); #ifdef _WIN32 @@ -7293,15 +7303,10 @@ void GLCanvas3D::_resize(unsigned int w, unsigned int h) #endif #if ENABLE_RETINA_GL - imgui->set_scaling(font_size, 1.0f, m_retina_helper->get_scale_factor()); + wxGetApp().imgui()->set_scaling(font_size, 1.0f, m_retina_helper->get_scale_factor()); #else - imgui->set_scaling(font_size, m_canvas->GetContentScaleFactor(), 1.0f); + wxGetApp().imgui()->set_scaling(font_size, m_canvas->GetContentScaleFactor(), 1.0f); #endif - - this->request_extra_frame(); - - // ensures that this canvas is current - _set_current(); } BoundingBoxf3 GLCanvas3D::_max_bounding_box(bool include_gizmos, bool include_bed_model, bool include_plates) const diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 57912ee7cd..36a6c7b944 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -1299,6 +1299,8 @@ public: Vec3d _mouse_to_3d(const Point& mouse_pos, float* z = nullptr); bool make_current_for_postinit(); + // Sizes ImGui's fonts and style for this canvas; the fonts are rebuilt when the size changes. + void set_imgui_scaling(); private: bool _is_shown_on_screen() const; diff --git a/src/slic3r/GUI/GLModel.cpp b/src/slic3r/GUI/GLModel.cpp index 3dd15c3fdd..92296030e1 100644 --- a/src/slic3r/GUI/GLModel.cpp +++ b/src/slic3r/GUI/GLModel.cpp @@ -456,8 +456,9 @@ void GLModel::init_from(const indexed_triangle_set& its) data.reserve_vertices(3 * its.indices.size()); data.reserve_indices(3 * its.indices.size()); - // Read user preference: smooth normals enabled - const bool smooth_normals_enabled = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SMOOTH_NORMALS); + // CLI thumbnail generation can initialize models without a wx application. + const bool smooth_normals_enabled = wxApp::GetInstance() != nullptr && wxGetApp().app_config != nullptr && + wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SMOOTH_NORMALS); if (smooth_normals_enabled) { // Use per-corner smooth normals (via IGL) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 1a153ed4fb..a41001f9e0 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -857,7 +857,12 @@ void GUI_App::post_init() slow_bootup = true; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", slow bootup, won't render gl here."; } - if (!switch_to_3d) { + // Starting on Home, the GL resources load at idle so Home paints first and Prepare is never + // shown. + const bool gl_at_idle = !starts_on_prepare() && is_editor(); + if (!switch_to_3d && gl_at_idle) { + plater_->select_view_3D("3D"); + } else if (!switch_to_3d) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", begin load_gl_resources"; #ifndef __linux__ mainframe->Freeze(); @@ -865,9 +870,6 @@ void GUI_App::post_init() plater_->canvas3D()->enable_render(false); mainframe->select_prepare_for_gl_init(); plater_->select_view_3D("3D"); - // The first render happens before the queued new_project() sets the same view. - plater_->get_camera().select_view("topfront"); - plater_->get_camera().requires_zoom_to_bed = true; //BBS init the opengl resource here if (!plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen() || !plater_->canvas3D()->make_current_for_postinit()) { @@ -905,8 +907,6 @@ void GUI_App::post_init() } if (starts_on_prepare()) mainframe->select_tab(TAB_ID_PREPARE); - else if (is_editor()) - mainframe->select_tab(TAB_ID_HOME); #ifndef __linux__ mainframe->Thaw(); #endif @@ -3423,6 +3423,10 @@ bool GUI_App::on_init_inner() } BOOST_LOG_TRIVIAL(info) << "create the main window"; mainframe = new MainFrame(); + // The first render can happen as soon as the frame is shown, before the queued + // new_project() sets the same view. + plater_->get_camera().select_view("topfront"); + plater_->get_camera().requires_zoom_to_bed = true; if (is_editor()) { if (starts_on_prepare()) { mainframe->select_tab(TAB_ID_PREPARE); @@ -8201,10 +8205,12 @@ int GUI_App::input_idle_ms() const return int(std::chrono::duration_cast(std::chrono::steady_clock::now() - m_last_input).count()); } -// Every wxCommandEvent claims the user-input category, so only real mouse and key events count. +// Every wxCommandEvent claims the user-input category, so only real mouse and key events count, +// plus main window resizes, since a border drag produces no mouse events. int GUI_App::FilterEvent(wxEvent& event) { - if (!event.IsCommandEvent() && (event.GetEventCategory() & wxEVT_CATEGORY_USER_INPUT)) + if ((!event.IsCommandEvent() && (event.GetEventCategory() & wxEVT_CATEGORY_USER_INPUT)) || + (event.GetEventType() == wxEVT_SIZE && event.GetEventObject() == mainframe)) m_last_input = std::chrono::steady_clock::now(); return Event_Skip; } diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index e888a52b06..a8fa8432b1 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -389,7 +389,7 @@ public: bool is_editor() const { return m_app_mode == EAppMode::Editor; } bool is_gcode_viewer() const { return m_app_mode == EAppMode::GCodeViewer; } bool is_recreating_gui() const { return m_is_recreating_gui; } - // Milliseconds since the last mouse or keyboard event the app processed. + // Milliseconds since the last mouse or keyboard event the app processed, or main window resize. int input_idle_ms() const; int FilterEvent(wxEvent& event) override; // The Preferences "Default page" choice, stored as its index: 0 Home, 1 Prepare. diff --git a/src/slic3r/GUI/GUI_ObjectTable.cpp b/src/slic3r/GUI/GUI_ObjectTable.cpp index a496eca6d3..c3c3295800 100644 --- a/src/slic3r/GUI/GUI_ObjectTable.cpp +++ b/src/slic3r/GUI/GUI_ObjectTable.cpp @@ -2764,12 +2764,12 @@ ObjectTablePanel::ObjectTablePanel( wxWindow* parent, wxWindowID id, const wxPoi //m_object_grid->AssignTable(m_object_grid_table); m_side_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(440),FromDIP(480)), wxVSCROLL); - m_side_window->SetScrollRate( 0, 5 ); + m_side_window->SetScrollRate(0, FromDIP(20)); m_page_sizer = new wxBoxSizer(wxVERTICAL); //m_page_top_sizer = new wxBoxSizer(wxHORIZONTAL); m_side_window->SetBackgroundColour(wxColour(0xff, 0xff, 0xff)); m_side_window->SetSizer(m_page_sizer); - m_side_window->SetScrollbars(1, 20, 1, 2); + m_side_window->SetScrollbars(1, FromDIP(20), 1, 2); //m_side_window->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_NEVER); //m_side_window->EnableScrolling(false, true); diff --git a/src/slic3r/GUI/GUI_Utils.cpp b/src/slic3r/GUI/GUI_Utils.cpp index 10dd29c9c1..5c55e341c9 100644 --- a/src/slic3r/GUI/GUI_Utils.cpp +++ b/src/slic3r/GUI/GUI_Utils.cpp @@ -247,7 +247,9 @@ int get_dpi_for_window(const wxWindow *window) const HDC hdc = GetDC(hwnd); if (hdc == NULL) { return DPI_DEFAULT; } - return GetDeviceCaps(hdc, LOGPIXELSX); + const int dpi = GetDeviceCaps(hdc, LOGPIXELSX); + ReleaseDC(hwnd, hdc); // GetDC's handle must be released, unlike GetWindowDC's + return dpi; } #elif defined __linux__ // TODO diff --git a/src/slic3r/GUI/HMSPanel.cpp b/src/slic3r/GUI/HMSPanel.cpp index c2a8314bd5..1268cb8f7b 100644 --- a/src/slic3r/GUI/HMSPanel.cpp +++ b/src/slic3r/GUI/HMSPanel.cpp @@ -171,7 +171,7 @@ HMSPanel::HMSPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wx m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_scrolledWindow->SetBackgroundColour(*wxWHITE); - m_scrolledWindow->SetScrollRate(5, 5); + m_scrolledWindow->SetScrollRate(5, FromDIP(20)); m_top_sizer = new wxBoxSizer(wxVERTICAL); diff --git a/src/slic3r/GUI/IdleScheduler.cpp b/src/slic3r/GUI/IdleScheduler.cpp index 2b71a9f8e6..18498fce0d 100644 --- a/src/slic3r/GUI/IdleScheduler.cpp +++ b/src/slic3r/GUI/IdleScheduler.cpp @@ -3,6 +3,7 @@ #include #include +#include #include "libslic3r/Utils.hpp" @@ -23,12 +24,9 @@ constexpr int quiet_ms = 500; // queued meanwhile are handled first; a click waits at most a slice plus the unit that // overran it. constexpr int slice_ms = 40; -// On GTK a due timer runs ahead of repaints and posted events, so the next slice waits a few ms. -#ifdef __WXGTK__ +// Delay before the next slice; on GTK a due timer runs ahead of repaints and posted events, +// and wxOSX rejects a 0 ms timer. constexpr int next_slice_ms = 5; -#else -constexpr int next_slice_ms = 0; -#endif // True when unhandled keyboard, button, touch or pen input is queued; only Windows can ask. bool input_pending() @@ -42,6 +40,13 @@ bool input_pending() #endif } +// True inside a wxYield(), where a slice would build pages in the middle of the code that yielded. +bool yielding() +{ + const wxEventLoopBase* loop = wxEventLoopBase::GetActive(); + return loop != nullptr && loop->IsYielding(); +} + } // namespace IdleScheduler::IdleScheduler(std::function input_idle_ms) : m_input_idle_ms(std::move(input_idle_ms)) @@ -69,7 +74,7 @@ void IdleScheduler::tick() stop(); return; } - if (m_input_idle_ms() < quiet_ms || input_pending()) { + if (m_input_idle_ms() < quiet_ms || input_pending() || yielding()) { start(); return; } diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index c9fdfe4ab4..63580387a7 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -211,7 +211,7 @@ wxPanel* KBShortcutsDialog::create_page(wxWindow* parent, const Page& page) wxGetApp().UpdateDarkUI(scrollable_panel); const wxColour page_colour = StateColor::darkModeColorFor(*wxWHITE); scrollable_panel->SetBackgroundColour(page_colour); - scrollable_panel->SetScrollRate(0, 20); + scrollable_panel->SetScrollRate(0, FromDIP(20)); const int page_width = FromDIP(PAGE_WIDTH); scrollable_panel->SetInitialSize(wxSize(page_width, FromDIP(450))); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index ec4428a32a..a57dbf0b97 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -81,6 +81,7 @@ #ifdef __WXGTK__ #include +#include #endif // __WXGTK__ #include @@ -510,6 +511,9 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ wxQueueEvent(wxGetApp().plater(), new SimpleEvent(EVT_NOTICE_CHILDE_SIZE_CHANGED)); fit_tab_labels(); // ORCA on resize + // Restarts the idle build so a hidden Prepare page is laid out at the new size. + if (m_prebuild_started) + m_idle.start(); }); //BBS @@ -4008,13 +4012,81 @@ bool MainFrame::Show(bool show) return changed; } +bool MainFrame::GLResourcesPrebuild::built() const +{ + return m_frame.m_plater != nullptr && m_frame.m_plater->canvas3D()->is_initialized() && + m_frame.m_plater->get_partplate_list().icon_textures_loaded(); +} + +bool MainFrame::GLResourcesPrebuild::build_step() +{ + GLCanvas3D* canvas = m_frame.m_plater->canvas3D(); +#ifdef __WXGTK__ + // wx creates a GTK canvas's GL surface when the widget is realized, so the context can be + // made current on it while hidden. + gtk_widget_realize(canvas->get_wxglcanvas()->GetHandle()); +#endif + if (!canvas->make_current_for_postinit()) { + // The first render of the canvas loads everything instead. + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": cannot make the GL context current on the hidden canvas"; + m_failed = true; + return false; + } + switch (m_step) { + case 0: + m_failed = !wxGetApp().init_opengl(); + break; + case 1: { + const Size size = canvas->get_canvas_size(); + wxGetApp().imgui()->set_display_size(float(std::max(1, size.get_width())), float(std::max(1, size.get_height()))); + canvas->set_imgui_scaling(); + // Builds the font atlas without leaving a frame open at the hidden canvas's size. + wxGetApp().imgui()->new_frame(); + wxGetApp().imgui()->end_frame(); + break; + } + case 2: + // One texture per unit until none remain. + if (m_frame.m_plater->get_partplate_list().load_next_plate_texture()) + return true; + break; + case 3: + m_failed = !canvas->init(); + break; + default: + // Runs after init(), which sets the color mode the icons are drawn for. + m_frame.m_plater->get_partplate_list().load_icon_textures(); + return false; + } + ++m_step; + return !m_failed; +} + +bool MainFrame::PrepareLayoutPrebuild::built() const +{ + // The book lays out the page it shows. + const wxWindow* page = m_frame.m_tabpanel != nullptr ? m_frame.m_tabpanel->GetCurrentPage() : nullptr; + return page == nullptr || page == m_frame.m_plater || page->GetSize() == m_laid_out_size; +} + +bool MainFrame::PrepareLayoutPrebuild::build_step() +{ + // Sized as the book sizes the page it selects, so the selection finds nothing to lay out. + const wxWindow* page = m_frame.m_tabpanel->GetCurrentPage(); + m_laid_out_size = page->GetSize(); + m_frame.m_plater->SetSize(page->GetRect()); + return false; +} + // A page out of the book stays registered and is passed over; a negative order is never // registered. void MainFrame::prebuild_pages_when_idle() { m_idle.clear(); + m_idle.add(m_gl_prebuild); if (m_param_panel) m_idle.add(m_param_panel->settings_page_prebuild()); + m_idle.add(m_prepare_layout_prebuild); for (LazyBase* page : m_lazy_pages) if (page->prebuild_order() >= 0) m_idle.add(*page); diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index f580de57e0..dce12c3fc1 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -140,6 +140,38 @@ class MainFrame : public DPIFrame wxTimer* m_reset_title_text_colour_timer{ nullptr }; IdleScheduler m_idle; bool m_prebuild_started{ false }; + // Loads the Prepare canvas's GL resources while its page is hidden. + class GLResourcesPrebuild : public LazyBase + { + public: + explicit GLResourcesPrebuild(MainFrame& frame) : m_frame(frame) {} + const std::string& name() const override { return m_name; } + bool built() const override; + bool pending() const override { return !m_failed && !built(); } + bool build_step() override; + int prebuild_order() const override { return 0; } + + private: + MainFrame& m_frame; + std::string m_name{ "gl_resources" }; + int m_step{ 0 }; + bool m_failed{ false }; + } m_gl_prebuild{ *this }; + // Lays out the hidden Prepare page at the size the book gives its pages. + class PrepareLayoutPrebuild : public LazyBase + { + public: + explicit PrepareLayoutPrebuild(MainFrame& frame) : m_frame(frame) {} + const std::string& name() const override { return m_name; } + bool built() const override; + bool build_step() override; + int prebuild_order() const override { return 0; } + + private: + MainFrame& m_frame; + std::string m_name{ "prepare_layout" }; + wxSize m_laid_out_size; + } m_prepare_layout_prebuild{ *this }; // Every LazyPage, in and out of the book; prebuild_pages_when_idle() registers them. std::vector m_lazy_pages; // The latest EVT_LOAD_PRINTER_URL, applied when the web Device view is built. diff --git a/src/slic3r/GUI/MixedFilamentDialog.cpp b/src/slic3r/GUI/MixedFilamentDialog.cpp index 8176a76202..246c6548c6 100644 --- a/src/slic3r/GUI/MixedFilamentDialog.cpp +++ b/src/slic3r/GUI/MixedFilamentDialog.cpp @@ -1125,7 +1125,7 @@ wxBoxSizer* MixedFilamentDialog::create_recommendation_grid() outer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); m_recommendation_scroll = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(116))); - m_recommendation_scroll->SetScrollRate(0, 5); + m_recommendation_scroll->SetScrollRate(0, FromDIP(20)); m_recommendation_scroll->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8"))); m_recommendation_grid = new wxWrapSizer(wxHORIZONTAL, wxREMOVE_LEADING_SPACES); diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp index 7f1effd162..0f1d3f62d5 100644 --- a/src/slic3r/GUI/MsgDialog.cpp +++ b/src/slic3r/GUI/MsgDialog.cpp @@ -398,7 +398,7 @@ static void add_msg_content(wxWindow *parent, } wxScrolledWindow *scrolledWindow = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); scrolledWindow->SetBackgroundColour(*wxWHITE); - scrolledWindow->SetScrollRate(0, 20); + scrolledWindow->SetScrollRate(0, scrolledWindow->FromDIP(20)); scrolledWindow->EnableScrolling(false, true); wxBoxSizer *sizer_scrolled = new wxBoxSizer(wxHORIZONTAL); Label *wrapped_text = new Label(scrolledWindow, font, msg, LB_AUTO_WRAP, wxSize(info_width, -1)); diff --git a/src/slic3r/GUI/MultiMachineManagerPage.cpp b/src/slic3r/GUI/MultiMachineManagerPage.cpp index d5f9cbce8a..f68188a9f1 100644 --- a/src/slic3r/GUI/MultiMachineManagerPage.cpp +++ b/src/slic3r/GUI/MultiMachineManagerPage.cpp @@ -398,7 +398,7 @@ MultiMachineManagerPage::MultiMachineManagerPage(wxWindow* parent) m_machine_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_machine_list->SetBackgroundColour(*wxWHITE); - m_machine_list->SetScrollRate(0, 5); + m_machine_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT)); m_machine_list->SetMinSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT))); m_machine_list->SetMaxSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT))); diff --git a/src/slic3r/GUI/MultiMachinePage.cpp b/src/slic3r/GUI/MultiMachinePage.cpp index e48e402405..2b6fe75f34 100644 --- a/src/slic3r/GUI/MultiMachinePage.cpp +++ b/src/slic3r/GUI/MultiMachinePage.cpp @@ -323,7 +323,7 @@ MultiMachinePickPage::MultiMachinePickPage(Plater* plater /*= nullptr*/) scroll_macine_list->SetMinSize(wxSize(FromDIP(400), FromDIP(10 * 30))); scroll_macine_list->SetMaxSize(wxSize(FromDIP(400), FromDIP(10 * 30))); scroll_macine_list->SetBackgroundColour(*wxWHITE); - scroll_macine_list->SetScrollRate(0, 5); + scroll_macine_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT)); sizer_machine_list = new wxBoxSizer(wxVERTICAL); scroll_macine_list->SetSizer(sizer_machine_list); diff --git a/src/slic3r/GUI/MultiTaskManagerPage.cpp b/src/slic3r/GUI/MultiTaskManagerPage.cpp index 48a7fbe43e..d063b34743 100644 --- a/src/slic3r/GUI/MultiTaskManagerPage.cpp +++ b/src/slic3r/GUI/MultiTaskManagerPage.cpp @@ -675,7 +675,7 @@ LocalTaskManagerPage::LocalTaskManagerPage(wxWindow* parent) m_task_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_task_list->SetBackgroundColour(*wxWHITE); - m_task_list->SetScrollRate(0, 5); + m_task_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT)); m_task_list->SetMinSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), FromDIP(DEVICE_ITEM_MAX_HEIGHT))); m_task_list->SetMaxSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT))); @@ -1056,7 +1056,7 @@ CloudTaskManagerPage::CloudTaskManagerPage(wxWindow* parent) m_task_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_task_list->SetBackgroundColour(*wxWHITE); - m_task_list->SetScrollRate(0, 5); + m_task_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT)); m_task_list->SetMinSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), FromDIP(DEVICE_ITEM_MAX_HEIGHT))); m_task_list->SetMaxSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT))); diff --git a/src/slic3r/GUI/ObjColorDialog.cpp b/src/slic3r/GUI/ObjColorDialog.cpp index ef5d4ba9d2..4388449bb5 100644 --- a/src/slic3r/GUI/ObjColorDialog.cpp +++ b/src/slic3r/GUI/ObjColorDialog.cpp @@ -355,7 +355,7 @@ ObjColorPanel::ObjColorPanel(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, c //new color table m_scrolledWindow = new wxScrolledWindow(m_page_simple, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_scrolledWindow->SetBackgroundColour(*wxWHITE); - m_scrolledWindow->SetScrollRate(0, 20); + m_scrolledWindow->SetScrollRate(0, FromDIP(20)); m_scrolledWindow->EnableScrolling(false, true); m_scrolledWindow->ShowScrollbars(wxScrollbarVisibility::wxSHOW_SB_NEVER, wxScrollbarVisibility::wxSHOW_SB_DEFAULT); draw_new_table(); diff --git a/src/slic3r/GUI/OptionsGroup.hpp b/src/slic3r/GUI/OptionsGroup.hpp index 921ac62f15..8dde661bdd 100644 --- a/src/slic3r/GUI/OptionsGroup.hpp +++ b/src/slic3r/GUI/OptionsGroup.hpp @@ -112,7 +112,7 @@ public: bool split_multi_line{false}; bool option_label_at_right{false}; // BBS: new layout - wxWindow * stb; + wxWindow * stb{ nullptr }; const wxString icon; const wxString title; bool m_labels_hidden{false}; diff --git a/src/slic3r/GUI/ParamsPanel.cpp b/src/slic3r/GUI/ParamsPanel.cpp index b4fd81706a..9e16fcf8df 100644 --- a/src/slic3r/GUI/ParamsPanel.cpp +++ b/src/slic3r/GUI/ParamsPanel.cpp @@ -391,7 +391,7 @@ ParamsPanel::ParamsPanel( wxWindow* parent, wxWindowID id, const wxPoint& pos, c m_page_sizer = new wxBoxSizer(wxVERTICAL); m_page_view->SetSizer(m_page_sizer); - m_page_view->SetScrollbars(1, 20, 1, 2); + m_page_view->SetScrollbars(1, FromDIP(20), 1, 2); //m_page_view->SetScrollRate( 5, 5 ); if (m_mode_region) diff --git a/src/slic3r/GUI/ParamsPanel.hpp b/src/slic3r/GUI/ParamsPanel.hpp index 9f9264b480..f4e151456d 100644 --- a/src/slic3r/GUI/ParamsPanel.hpp +++ b/src/slic3r/GUI/ParamsPanel.hpp @@ -122,8 +122,8 @@ class ParamsPanel : public wxPanel wxPanel* m_current_tab { nullptr }; - // Builds the selected page's option groups at idle; while no tab is selected yet, - // its first unit selects the default one. + // Builds the selected page's option groups at idle and then shows them for the mode; + // while no tab is selected yet, its first unit selects the default one. class SettingsPagePrebuild : public LazyBase { public: diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index e90b21e0ff..8e2d6ba37b 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -799,67 +799,8 @@ void PartPlate::render_logo(bool bottom, bool render_cali) { if (!m_partplate_list->render_bedtype_logo) { // render third-party printer texture logo - if (m_partplate_list->m_logo_texture_filename.empty()) { - m_partplate_list->m_logo_texture.reset(); + if (!m_partplate_list->load_logo_texture()) return; - } - - //GLTexture* temp_texture = const_cast(&m_temp_texture); - - if (m_partplate_list->m_logo_texture.get_id() == 0 || m_partplate_list->m_logo_texture.get_source() != m_partplate_list->m_logo_texture_filename) { - m_partplate_list->m_logo_texture.reset(); - - if (boost::algorithm::iends_with(m_partplate_list->m_logo_texture_filename, ".svg")) { - /*// use higher resolution images if graphic card and opengl version allow - GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size(); - if (temp_texture->get_id() == 0 || temp_texture->get_source() != m_texture_filename) { - // generate a temporary lower resolution texture to show while no main texture levels have been compressed - if (!temp_texture->load_from_svg_file(m_texture_filename, false, false, false, max_tex_size / 8)) { - render_default(bottom, false); - return; - } - canvas.request_extra_frame(); - }*/ - - // starts generating the main texture, compression will run asynchronously - GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size(); - GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048; - if (!m_partplate_list->m_logo_texture.load_from_svg_file(m_partplate_list->m_logo_texture_filename, true, true, true, logo_tex_size)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % m_partplate_list->m_logo_texture_filename; - return; - } - } - else if (boost::algorithm::iends_with(m_partplate_list->m_logo_texture_filename, ".png")) { - // generate a temporary lower resolution texture to show while no main texture levels have been compressed - /* if (temp_texture->get_id() == 0 || temp_texture->get_source() != m_logo_texture_filename) { - if (!temp_texture->load_from_file(m_logo_texture_filename, false, GLTexture::None, false)) { - render_default(bottom, false); - return; - } - canvas.request_extra_frame(); - }*/ - - // starts generating the main texture, compression will run asynchronously - if (!m_partplate_list->m_logo_texture.load_from_file(m_partplate_list->m_logo_texture_filename, true, GLTexture::MultiThreaded, true)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % m_partplate_list->m_logo_texture_filename; - return; - } - } - else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not load logo texture from %1%, unsupported format") % m_partplate_list->m_logo_texture_filename; - return; - } - } - else if (m_partplate_list->m_logo_texture.unsent_compressed_data_available()) { - // sends to gpu the already available compressed levels of the main texture - m_partplate_list->m_logo_texture.send_compressed_data_to_gpu(); - - // the temporary texture is not needed anymore, reset it - //if (temp_texture->get_id() != 0) - // temp_texture->reset(); - - //canvas.request_extra_frame(); - } if (m_logo_triangles.is_initialized()) render_logo_texture(m_partplate_list->m_logo_texture, m_logo_triangles, bottom); @@ -4232,6 +4173,7 @@ Vec2d PartPlateList::compute_shape_position(int index, int cols) //generate icon textures void PartPlateList::generate_icon_textures() { + m_icon_textures_dark = m_is_dark; // use higher resolution images if graphic card and opengl version allow GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size(), icon_size = max_tex_size / 8; std::string path = resources_dir() + "/images/"; @@ -4447,6 +4389,9 @@ void PartPlateList::release_icon_textures() PartPlateList::is_load_bedtype_textures = false; PartPlateList::is_load_extruder_only_area_textures = false; PartPlateList::is_load_cali_texture = false; + m_next_bedtype_texture = 0; + m_next_extruder_only_area_texture = 0; + m_next_cali_texture = 0; for (int i = 0; i < btCount; i++) { for (auto& part: bed_texture_info[i].parts) { if (part.texture) { @@ -6002,12 +5947,7 @@ void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& pr plate_hover_action = hover_id % PartPlate::GRABBER_COUNT; } - static bool last_dark_mode_status = m_is_dark; - if (m_is_dark != last_dark_mode_status) { - last_dark_mode_status = m_is_dark; - generate_icon_textures(); - } else if(m_del_texture.get_id() == 0) - generate_icon_textures(); + load_icon_textures(); for (it = m_plate_list.begin(); it != m_plate_list.end(); it++) { int current_index = (*it)->get_index(); if (only_current && (current_index != m_current_plate)) @@ -6149,6 +6089,8 @@ bool PartPlateList::set_shapes(const Pointfs &shape, } is_load_bedtype_textures = false; //reload textures is_load_extruder_only_area_textures = false; // reload textures + m_next_bedtype_texture = 0; + m_next_extruder_only_area_texture = 0; calc_bounding_boxes(); update_logo_texture_filename(texture_filename); @@ -7089,53 +7031,120 @@ bool PartPlateList::init_extruder_only_area_info() return true; } -void PartPlateList::load_bedtype_textures() +static GLint logo_texture_size() { - if (PartPlateList::is_load_bedtype_textures) return; - - init_bed_type_info(); - GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size(); - GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048; - for (int i = 0; i < (unsigned int)btCount; ++i) { - for (int j = 0; j < bed_texture_info[i].parts.size(); j++) { - std::string filename = resources_dir() + "/images/" + bed_texture_info[i].parts[j].filename; - if (boost::filesystem::exists(filename)) { - PartPlateList::bed_texture_info[i].parts[j].texture = new GLTexture(); - if (!PartPlateList::bed_texture_info[i].parts[j].texture->load_from_svg_file(filename, true, true, true, logo_tex_size)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % filename; - } - } else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % filename; - } - } - } - PartPlateList::is_load_bedtype_textures = true; + return std::min(OpenGLManager::get_gl_info().get_max_tex_size(), 2048); } -void PartPlateList::load_extruder_only_area_textures() { - if (PartPlateList::is_load_extruder_only_area_textures) return; +// Loads the texture of the next untried part across the parts of `infos`, in order, advancing +// `next`; false once every part has been tried. +static bool load_next_part_texture(PartPlateList::BedTextureInfo* infos, size_t count, size_t& next, bool compress_and_filter) +{ + size_t k = next; + for (size_t i = 0; i < count; ++i) { + if (k >= infos[i].parts.size()) { + k -= infos[i].parts.size(); + continue; + } + ++next; + PartPlateList::BedTextureInfo::TexturePart& part = infos[i].parts[k]; + const std::string filename = resources_dir() + "/images/" + part.filename; + if (boost::filesystem::exists(filename)) { + part.texture = new GLTexture(); + if (!part.texture->load_from_svg_file(filename, true, compress_and_filter, compress_and_filter, logo_texture_size())) + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load texture from %1% failed!") % filename; + } else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load texture from %1% failed!") % filename; + } + return true; + } + return false; +} - auto ok = init_extruder_only_area_info(); - if (!ok) { +void PartPlateList::load_bedtype_textures() +{ + while (load_next_bedtype_texture()) {} +} + +bool PartPlateList::load_next_bedtype_texture() +{ + if (PartPlateList::is_load_bedtype_textures) + return false; + if (m_next_bedtype_texture == 0) + init_bed_type_info(); + if (load_next_part_texture(bed_texture_info, btCount, m_next_bedtype_texture, true)) + return true; + PartPlateList::is_load_bedtype_textures = true; + return false; +} + +bool PartPlateList::load_logo_texture() +{ + if (m_logo_texture_filename.empty()) { + m_logo_texture.reset(); + return false; + } + + if (m_logo_texture.get_id() != 0 && m_logo_texture.get_source() == m_logo_texture_filename) { + if (m_logo_texture.unsent_compressed_data_available()) + // sends to gpu the already available compressed levels of the main texture + m_logo_texture.send_compressed_data_to_gpu(); + return true; + } + + m_logo_texture.reset(); + // starts generating the main texture, compression will run asynchronously + if (boost::algorithm::iends_with(m_logo_texture_filename, ".svg")) { + if (!m_logo_texture.load_from_svg_file(m_logo_texture_filename, true, true, true, logo_texture_size())) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % m_logo_texture_filename; + return false; + } + } + else if (boost::algorithm::iends_with(m_logo_texture_filename, ".png")) { + if (!m_logo_texture.load_from_file(m_logo_texture_filename, true, GLTexture::MultiThreaded, true)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % m_logo_texture_filename; + return false; + } + } + else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not load logo texture from %1%, unsupported format") % m_logo_texture_filename; + return false; + } + return true; +} + +void PartPlateList::load_icon_textures() +{ + if (!icon_textures_loaded()) + generate_icon_textures(); +} + +bool PartPlateList::load_next_plate_texture() +{ + if (!render_bedtype_logo) { + load_logo_texture(); + return false; + } + return load_next_bedtype_texture() || load_next_cali_texture() || load_next_extruder_only_area_texture(); +} + +void PartPlateList::load_extruder_only_area_textures() +{ + while (load_next_extruder_only_area_texture()) {} +} + +bool PartPlateList::load_next_extruder_only_area_texture() +{ + if (PartPlateList::is_load_extruder_only_area_textures) + return false; + if (m_next_extruder_only_area_texture == 0 && !init_extruder_only_area_info()) { PartPlateList::is_load_extruder_only_area_textures = true; - return; - } - GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size(); - GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048; - for (int i = 0; i < (unsigned int) ExtruderOnlyAreaType::btAreaCount; ++i) { - for (int j = 0; j < extruder_only_area_info[i].parts.size(); j++) { - std::string filename = resources_dir() + "/images/" + extruder_only_area_info[i].parts[j].filename; - if (boost::filesystem::exists(filename)) { - PartPlateList::extruder_only_area_info[i].parts[j].texture = new GLTexture(); - if (!PartPlateList::extruder_only_area_info[i].parts[j].texture->load_from_svg_file(filename, true, false, false, logo_tex_size)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % filename; - } - } else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % filename; - } - } + return false; } + if (load_next_part_texture(extruder_only_area_info, (size_t) ExtruderOnlyAreaType::btAreaCount, m_next_extruder_only_area_texture, false)) + return true; PartPlateList::is_load_extruder_only_area_textures = true; + return false; } void PartPlateList::init_cali_texture_info() @@ -7150,26 +7159,19 @@ void PartPlateList::init_cali_texture_info() void PartPlateList::load_cali_textures() { - if (PartPlateList::is_load_cali_texture) return; + while (load_next_cali_texture()) {} +} - init_cali_texture_info(); - GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size(); - GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048; - for (int i = 0; i < (unsigned int)btCount; ++i) { - for (int j = 0; j < cali_texture_info.parts.size(); j++) { - std::string filename = resources_dir() + "/images/" + cali_texture_info.parts[j].filename; - if (boost::filesystem::exists(filename)) { - PartPlateList::cali_texture_info.parts[j].texture = new GLTexture(); - if (!PartPlateList::cali_texture_info.parts[j].texture->load_from_svg_file(filename, true, true, true, logo_tex_size)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load cali texture from %1% failed!") % filename; - } - } - else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load cali texture from %1% failed!") % filename; - } - } - } +bool PartPlateList::load_next_cali_texture() +{ + if (PartPlateList::is_load_cali_texture) + return false; + if (m_next_cali_texture == 0) + init_cali_texture_info(); + if (load_next_part_texture(&cali_texture_info, 1, m_next_cali_texture, true)) + return true; PartPlateList::is_load_cali_texture = true; + return false; } void PartPlateList::on_extruder_count_changed(int extruder_count) diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 9bee917243..52a277d0e3 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -644,6 +644,7 @@ class PartPlateList : public ObjectBase std::string m_hover_tooltip; bool m_is_dark = false; + bool m_icon_textures_dark = false; int m_filament_count = 1; @@ -943,12 +944,25 @@ public: bool calc_extruder_only_area(Rect &left_only_rect, Rect &right_only_rect); void init_bed_type_info(); bool init_extruder_only_area_info(); + // Each load_*_textures() loads whatever of its set is not loaded yet; each load_next_*() + // loads one texture and returns false once none remain. void load_bedtype_textures(); + bool load_next_bedtype_texture(); void load_extruder_only_area_textures(); + bool load_next_extruder_only_area_texture(); + // Starts loading the printer's logo texture, or sends the levels compressed since; false when + // there is no logo to draw. + bool load_logo_texture(); void show_cali_texture(bool show = true); void init_cali_texture_info(); void load_cali_textures(); + bool load_next_cali_texture(); + bool icon_textures_loaded() const { return m_del_texture.get_id() != 0 && m_icon_textures_dark == m_is_dark; } + void load_icon_textures(); + // Loads the next bed-type, calibration or extruder-area texture, or the logo, which rendering + // otherwise loads on first use; false once none remain. + bool load_next_plate_texture(); void on_extruder_count_changed(int extruder_count); @@ -960,6 +974,13 @@ public: BedTextureInfo bed_texture_info[btCount]; BedTextureInfo cali_texture_info; BedTextureInfo extruder_only_area_info[(unsigned char) Slic3r::ExtruderOnlyAreaType::btAreaCount]; + +private: + // The next part to load in each texture set, counted across the set's parts in order; reset + // with the set's is_load_* flag. + size_t m_next_bedtype_texture{ 0 }; + size_t m_next_cali_texture{ 0 }; + size_t m_next_extruder_only_area_texture{ 0 }; }; } // namespace GUI diff --git a/src/slic3r/GUI/PartSkipDialog.cpp b/src/slic3r/GUI/PartSkipDialog.cpp index 9bd6687757..860779ead4 100644 --- a/src/slic3r/GUI/PartSkipDialog.cpp +++ b/src/slic3r/GUI/PartSkipDialog.cpp @@ -148,7 +148,7 @@ PartSkipDialog::PartSkipDialog(wxWindow *parent) : DPIDialog(parent, wxID_ANY, _ m_line->SetBackgroundColour(wxColor(238, 238, 238)); m_list_view = new wxScrolledWindow(m_book_third_panel, wxID_ANY, wxDefaultPosition, wxSize(267, -1), wxHSCROLL | wxVSCROLL); - m_list_view->SetScrollRate(5, 5); + m_list_view->SetScrollRate(5, FromDIP(30)); m_list_view->SetMinSize(wxSize(FromDIP(267), FromDIP(378))); m_list_view->SetMaxSize(wxSize(FromDIP(267), FromDIP(378))); m_list_view->SetBackgroundColour(*wxWHITE); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 05cdffadef..6d4154ee65 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3051,8 +3051,7 @@ Sidebar::Sidebar(Plater *parent) // add filament content p->m_panel_filament_content = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); - p->m_panel_filament_content->SetScrollbars(0, 100, 1, 2); - p->m_panel_filament_content->SetScrollRate(0, 5); + p->m_panel_filament_content->SetScrollRate(0, FromDIP(20)); //p->m_panel_filament_content->SetMaxSize(wxSize{-1, FromDIP(174)}); p->m_panel_filament_content->SetBackgroundColour(wxColour(255, 255, 255)); @@ -3144,8 +3143,7 @@ Sidebar::Sidebar(Plater *parent) // 3) Mixed filament rows, in their own scroll area so a long mixed list does not // push the physical filament list off screen. p->m_mixed_scroll_area = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - p->m_mixed_scroll_area->SetScrollbars(0, 100, 1, 2); - p->m_mixed_scroll_area->SetScrollRate(0, 5); + p->m_mixed_scroll_area->SetScrollRate(0, FromDIP(20)); p->m_mixed_scroll_area->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); { auto* mix_scroll_sizer = new wxBoxSizer(wxVERTICAL); @@ -3706,10 +3704,10 @@ void Sidebar::update_presets(Preset::Type preset_type) int select = -1; // ORCA get the actual nozzle diameter from printer config auto nozzle_dia = get_diameter_string(nozzle_diameter->values[extruder_index]); - // Named variants such as "0.4 High Flow" share a physical diameter. + // Named variants such as "0.4HS" and "0.4 High Flow" share a physical diameter. // Retain the variant selection unless the diameter was customized. const auto selected_variant = - (diameter == nozzle_dia || boost::algorithm::starts_with(diameter, nozzle_dia + " ")) && + diameter.substr(0, diameter.find_first_not_of("0123456789.")) == nozzle_dia && std::find(diameters.begin(), diameters.end(), diameter) != diameters.end() ? diameter : nozzle_dia; // ORCA try to add nozzle diameter from config if list is empty. fixes blank nozzle combo box when preset has no alias if(diameters[0].empty() && !nozzle_dia.empty()){ diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index fd5012b5ef..055449c35d 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1475,7 +1475,7 @@ void PreferencesDialog::create() app_config = get_app_config(); m_parent = new MyscrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - m_parent->SetScrollRate(5, 5); + m_parent->SetScrollRate(0, FromDIP(20)); m_parent->SetBackgroundColour(*wxWHITE); m_sizer_body = new wxBoxSizer(wxVERTICAL); diff --git a/src/slic3r/GUI/PrintOptionsDialog.cpp b/src/slic3r/GUI/PrintOptionsDialog.cpp index 349e75fa06..d68bd24ccc 100644 --- a/src/slic3r/GUI/PrintOptionsDialog.cpp +++ b/src/slic3r/GUI/PrintOptionsDialog.cpp @@ -32,7 +32,7 @@ PrintOptionsDialog::PrintOptionsDialog(wxWindow* parent) m_scrollwindow = new wxScrolledWindow(this, wxID_ANY); - m_scrollwindow->SetScrollRate(0, FromDIP(10)); + m_scrollwindow->SetScrollRate(0, FromDIP(20)); m_scrollwindow->SetBackgroundColour(*wxWHITE); m_scrollwindow->SetMinSize(wxSize(FromDIP(480), wxDefaultCoord)); m_scrollwindow->SetMaxSize(wxSize(FromDIP(480), wxDefaultCoord)); diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index bd5c045053..d231857f2c 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -1225,7 +1225,7 @@ size_t PublishSettingsDialog::category_index_for( } category.scroll = new wxScrolledWindow(category.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - category.scroll->SetScrollRate(0, 10); + category.scroll->SetScrollRate(0, FromDIP(20)); category.scroll->SetBackgroundColour(GetBackgroundColour()); category.list_sizer = new wxBoxSizer(wxVERTICAL); category.scroll->SetSizer(category.list_sizer); diff --git a/src/slic3r/GUI/ReleaseNote.cpp b/src/slic3r/GUI/ReleaseNote.cpp index 029310faf9..c8338dc79a 100644 --- a/src/slic3r/GUI/ReleaseNote.cpp +++ b/src/slic3r/GUI/ReleaseNote.cpp @@ -76,7 +76,7 @@ ReleaseNoteDialog::ReleaseNoteDialog(Plater *plater /*= nullptr*/) m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15)); m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)), wxVSCROLL); - m_vebview_release_note->SetScrollRate(5, 5); + m_vebview_release_note->SetScrollRate(5, FromDIP(20)); m_vebview_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8)); m_vebview_release_note->SetMaxSize(wxSize(FromDIP(560), FromDIP(430))); @@ -142,7 +142,7 @@ UpdatePluginDialog::UpdatePluginDialog(wxWindow* parent /*= nullptr*/) operation_tips->SetMaxSize(wxSize(FromDIP(260), -1)); m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - m_vebview_release_note->SetScrollRate(5, 5); + m_vebview_release_note->SetScrollRate(5, FromDIP(20)); m_vebview_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8)); m_vebview_release_note->SetMinSize(wxSize(FromDIP(260), FromDIP(150))); m_vebview_release_note->SetMaxSize(wxSize(FromDIP(260), FromDIP(150))); @@ -277,7 +277,7 @@ UpdateVersionDialog::UpdateVersionDialog(wxWindow *parent) m_simplebook_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8)); m_scrollwindows_release_note = new wxScrolledWindow(m_simplebook_release_note, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)), wxVSCROLL); - m_scrollwindows_release_note->SetScrollRate(5, 5); + m_scrollwindows_release_note->SetScrollRate(5, FromDIP(20)); m_scrollwindows_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8)); //webview @@ -538,7 +538,7 @@ SecondaryCheckDialog::SecondaryCheckDialog(wxWindow* parent, wxWindowID id, cons m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15)); m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - m_vebview_release_note->SetScrollRate(0, 5); + m_vebview_release_note->SetScrollRate(0, FromDIP(20)); m_vebview_release_note->SetBackgroundColour(*wxWHITE); m_vebview_release_note->SetMinSize(wxSize(FromDIP(400), FromDIP(380))); m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15)); @@ -823,7 +823,7 @@ PrintErrorDialog::PrintErrorDialog(wxWindow* parent, wxWindowID id, const wxStri m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(5)); m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - m_vebview_release_note->SetScrollRate(0, 5); + m_vebview_release_note->SetScrollRate(0, FromDIP(20)); m_vebview_release_note->SetBackgroundColour(*wxWHITE); m_vebview_release_note->SetMinSize(wxSize(FromDIP(320), FromDIP(250))); m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15)); @@ -1122,7 +1122,7 @@ ConfirmBeforeSendDialog::ConfirmBeforeSendDialog(wxWindow* parent, wxWindowID id m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15)); m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - m_vebview_release_note->SetScrollRate(0, 5); + m_vebview_release_note->SetScrollRate(0, FromDIP(20)); m_vebview_release_note->SetBackgroundColour(*wxWHITE); m_vebview_release_note->SetMinSize(wxSize(FromDIP(400), FromDIP(380))); m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15)); diff --git a/src/slic3r/GUI/SafetyOptionsDialog.cpp b/src/slic3r/GUI/SafetyOptionsDialog.cpp index 6536666bb1..c633d2778e 100644 --- a/src/slic3r/GUI/SafetyOptionsDialog.cpp +++ b/src/slic3r/GUI/SafetyOptionsDialog.cpp @@ -31,7 +31,7 @@ SafetyOptionsDialog::SafetyOptionsDialog(wxWindow* parent) SetSize(FromDIP(480),FromDIP(320)); m_scrollwindow = new wxScrolledWindow(this, wxID_ANY); - m_scrollwindow->SetScrollRate(0, FromDIP(10)); + m_scrollwindow->SetScrollRate(0, FromDIP(20)); m_scrollwindow->SetBackgroundColour(*wxWHITE); m_scrollwindow->SetMinSize(wxSize(FromDIP(480), wxDefaultCoord)); m_scrollwindow->SetMaxSize(wxSize(FromDIP(480), wxDefaultCoord)); @@ -272,4 +272,4 @@ void SafetyOptionsDialog::show_idel_heating_toast(const wxString &text) m_idel_heating_toast_timer.StartOnce(3000); } -}} // namespace Slic3r::GUI \ No newline at end of file +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index a9767d6c72..a3b83bd643 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -148,7 +148,7 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater) wxBoxSizer* m_scroll_sizer = new wxBoxSizer(wxVERTICAL); m_scroll_area = new wxScrolledWindow(this); - m_scroll_area->SetScrollRate(0, 20); + m_scroll_area->SetScrollRate(0, FromDIP(20)); m_scroll_area->SetBackgroundColour(m_colour_def_color); m_scroll_area->SetMinSize(wxSize(FromDIP(700), FromDIP(600))); m_scroll_area->SetMaxSize(wxSize(FromDIP(700), FromDIP(600))); @@ -707,7 +707,7 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater) //show bind failed info m_sw_print_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125)), wxVSCROLL); m_sw_print_failed_info->SetBackgroundColour(*wxWHITE); - m_sw_print_failed_info->SetScrollRate(0, 5); + m_sw_print_failed_info->SetScrollRate(0, FromDIP(20)); m_sw_print_failed_info->SetMinSize(wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125))); m_sw_print_failed_info->SetMaxSize(wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125))); diff --git a/src/slic3r/GUI/SelectMachinePop.cpp b/src/slic3r/GUI/SelectMachinePop.cpp index 9b34e74f23..0046fd5f7f 100644 --- a/src/slic3r/GUI/SelectMachinePop.cpp +++ b/src/slic3r/GUI/SelectMachinePop.cpp @@ -327,7 +327,7 @@ SelectMachinePopup::SelectMachinePopup(wxWindow *parent) m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, SELECT_MACHINE_LIST_SIZE, wxHSCROLL | wxVSCROLL); m_scrolledWindow->SetBackgroundColour(*wxWHITE); m_scrolledWindow->SetMinSize(SELECT_MACHINE_LIST_SIZE); - m_scrolledWindow->SetScrollRate(0, 5); + m_scrolledWindow->SetScrollRate(0, SELECT_MACHINE_ITEM_SIZE.y); auto m_sizxer_scrolledWindow = new wxBoxSizer(wxVERTICAL); m_scrolledWindow->SetSizer(m_sizxer_scrolledWindow); m_scrolledWindow->Layout(); diff --git a/src/slic3r/GUI/SendMultiMachinePage.cpp b/src/slic3r/GUI/SendMultiMachinePage.cpp index cafbb360e6..b386d1f451 100644 --- a/src/slic3r/GUI/SendMultiMachinePage.cpp +++ b/src/slic3r/GUI/SendMultiMachinePage.cpp @@ -278,7 +278,7 @@ SendMultiMachinePage::SendMultiMachinePage(Plater* plater) m_main_scroll = new ScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_main_scroll->SetBackgroundColour(*wxWHITE); - m_main_scroll->SetScrollRate(5, 5); + m_main_scroll->SetScrollRate(5, FromDIP(20)); m_sizer_body = new wxBoxSizer(wxVERTICAL); m_main_page = create_page(); @@ -1353,7 +1353,7 @@ wxPanel* SendMultiMachinePage::create_page() scroll_macine_list = new wxScrolledWindow(main_page, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(800), FromDIP(300)), wxHSCROLL | wxVSCROLL); scroll_macine_list->SetBackgroundColour(*wxWHITE); - scroll_macine_list->SetScrollRate(5, 5); + scroll_macine_list->SetScrollRate(5, FromDIP(SEND_ITEM_MAX_HEIGHT)); scroll_macine_list->SetMinSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(SEND_ITEM_MAX_HEIGHT))); scroll_macine_list->SetMaxSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(SEND_ITEM_MAX_HEIGHT))); diff --git a/src/slic3r/GUI/SendToPrinter.cpp b/src/slic3r/GUI/SendToPrinter.cpp index c4db0423ab..9071c0affd 100644 --- a/src/slic3r/GUI/SendToPrinter.cpp +++ b/src/slic3r/GUI/SendToPrinter.cpp @@ -384,7 +384,7 @@ SendToPrinterDialog::SendToPrinterDialog(Plater *plater) //show bind failed info m_sw_print_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(380), FromDIP(125)), wxVSCROLL); m_sw_print_failed_info->SetBackgroundColour(*wxWHITE); - m_sw_print_failed_info->SetScrollRate(0, 5); + m_sw_print_failed_info->SetScrollRate(0, FromDIP(20)); m_sw_print_failed_info->SetMinSize(wxSize(FromDIP(380), FromDIP(125))); m_sw_print_failed_info->SetMaxSize(wxSize(FromDIP(380), FromDIP(125))); diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp index 218371f0b5..0b6e4b6577 100644 --- a/src/slic3r/GUI/StatusPanel.cpp +++ b/src/slic3r/GUI/StatusPanel.cpp @@ -1302,7 +1302,7 @@ void PrintingTaskPanel::set_star_count(int star_count) StatusBasePanel::StatusBasePanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style, const wxString &name) : wxScrolledWindow(parent, id, pos, size, wxHSCROLL | wxVSCROLL) { - this->SetScrollRate(25, 25); + SetScrollRate(25, FromDIP(20)); Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (!dev) return; obj = dev->get_selected_machine(); diff --git a/src/slic3r/GUI/SyncAmsInfoDialog.cpp b/src/slic3r/GUI/SyncAmsInfoDialog.cpp index 7f075dbbfa..19b1ddc568 100644 --- a/src/slic3r/GUI/SyncAmsInfoDialog.cpp +++ b/src/slic3r/GUI/SyncAmsInfoDialog.cpp @@ -700,7 +700,7 @@ SyncAmsInfoDialog::SyncAmsInfoDialog(wxWindow *parent, SyncInfo &info) : //wxBoxSizer *m_scroll_sizer = new wxBoxSizer(wxVERTICAL); m_scrolledWindow = new wxScrolledWindow(m_show_page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_scrolledWindow->SetBackgroundColour(*wxWHITE); - m_scrolledWindow->SetScrollRate(0, 20); + m_scrolledWindow->SetScrollRate(0, FromDIP(20)); m_scrolledWindow->SetMinSize(wxSize(-1, SyncAmsInfoDialogHeightMAX)); m_scrolledWindow->SetMaxSize(wxSize(-1, SyncAmsInfoDialogHeightMAX)); m_scrolledWindow->EnableScrolling(false,true); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index ea0ba3fec4..bf348f856f 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3069,6 +3069,7 @@ void TabPrint::build() optgroup->append_single_option_line("wipe_tower_rib_width", "multimaterial_settings_prime_tower#rib-width"); optgroup->append_single_option_line("wipe_tower_fillet_wall", "multimaterial_settings_prime_tower#fillet-wall"); optgroup->append_single_option_line("wipe_tower_no_sparse_layers", "multimaterial_settings_prime_tower#no-sparse-layers"); + optgroup->append_single_option_line("wipe_tower_sparse_layers_combination", "multimaterial_settings_prime_tower#combine-sparse-layers"); optgroup->append_single_option_line("single_extruder_multi_material_priming", "multimaterial_settings_prime_tower"); optgroup = page->new_optgroup(L("Filament for Features"), L"param_filament_for_features"); @@ -7165,12 +7166,18 @@ void Tab::restore_last_select_item() bool Tab::page_build_pending() const { - return m_active_page != nullptr && m_active_page->build_pending(); + return m_active_page != nullptr && (m_active_page->build_pending() || m_active_page->visibility_pending()); } bool Tab::page_build_step() { - return m_active_page != nullptr && m_active_page->build_step(m_mode); + if (m_active_page == nullptr) + return false; + if (m_active_page->build_pending()) + m_active_page->build_step(m_mode); + else + m_active_page->update_visibility(m_mode, true); + return page_build_pending(); } void Tab::update_description_lines() @@ -8601,6 +8608,8 @@ void Page::update_visibility(ConfigOptionMode mode, bool update_contolls_visibil } m_show = ret_val; + if (update_contolls_visibility) + m_visibility_applied = true; #ifdef __WXMSW__ if (!m_show) return; // BBS: fix field control position @@ -8654,6 +8663,7 @@ bool Page::activate_group(size_t i, ConfigOptionMode mode, std::function auto& group = m_optgroups[i]; if (!group->activate(throw_if_canceled)) return false; + m_visibility_applied = false; m_vsizer->Add(group->sizer, 0, wxEXPAND | (group->is_legend_line() ? (wxLEFT|wxTOP) : wxALL), m_parent->FromDIP(5)); // ORCA use less margin on parameters section group->update_visibility(mode); #if HIDE_FIRST_SPLIT_LINE @@ -8688,6 +8698,7 @@ void Page::clear() for (auto group : m_optgroups) group->clear(); m_page_title = NULL; + m_visibility_applied = false; } void Page::msw_rescale() diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index c5879c6e94..fa8a7ae68b 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -71,6 +71,7 @@ class Page: public std::enable_shared_from_this// : public wxScrolledWindo // BBS: new layout wxStaticText* m_page_title; bool m_show = true; + bool m_visibility_applied = false; public: //BBS: GUI refactor Page(wxWindow* parent, const wxString& title, int iconID, wxPanel* tab_owner); @@ -98,6 +99,8 @@ public: bool build_pending() const; // Builds the next option group that has no controls yet; true while some remain. bool build_step(ConfigOptionMode mode); + // Whether the controls have not been shown or hidden for a mode since they were built. + bool visibility_pending() const { return !m_visibility_applied; } void clear(); void msw_rescale(); void sys_color_changed(); @@ -443,8 +446,8 @@ public: // BBS: new layout void set_expanded(bool value); void restore_last_select_item(); - // page_build_pending() says whether the selected page has groups without controls, and - // page_build_step() builds one. + // page_build_pending() says whether the selected page has groups without controls or controls + // not yet shown for the mode, and page_build_step() does the next of those. bool page_build_pending() const; bool page_build_step(); diff --git a/src/slic3r/GUI/TextureImportDialog.cpp b/src/slic3r/GUI/TextureImportDialog.cpp index 2e5c1145e1..7296a1ebeb 100644 --- a/src/slic3r/GUI/TextureImportDialog.cpp +++ b/src/slic3r/GUI/TextureImportDialog.cpp @@ -534,7 +534,7 @@ public: m_content = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_content->SetBackgroundColour(pop_bg); - m_content->SetScrollRate(0, FromDIP(5)); + m_content->SetScrollRate(0, FromDIP(20)); auto* outer = new wxBoxSizer(wxVERTICAL); const int pop_w = std::max(FromDIP(213), popup_width); @@ -2274,7 +2274,7 @@ void TextureImportDialog::build_mapping_panel(wxWindow* parent, wxSizer* sizer) m_mapping_scroll = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(300))); - m_mapping_scroll->SetScrollRate(0, FromDIP(10)); + m_mapping_scroll->SetScrollRate(0, FromDIP(20)); m_mapping_scroll->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); m_mapping_scroll->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this); @@ -4335,7 +4335,7 @@ void TextureImportDialog::on_dpi_changed(const wxRect&) if (m_mapping_scroll) { m_mapping_scroll->SetMinSize(wxSize(-1, FromDIP(300))); - m_mapping_scroll->SetScrollRate(0, FromDIP(10)); + m_mapping_scroll->SetScrollRate(0, FromDIP(20)); } if (m_btn_skip) { diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp index db64bdcc99..df809396f5 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.cpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp @@ -942,7 +942,7 @@ void UnsavedChangesDialog::build(Preset::Type type, PresetCollection *dependent_ m_sizer_tab->Add(m_table_top, 1, 0, 0); m_scrolledWindow = new wxScrolledWindow(m_panel_tab, wxID_ANY, wxDefaultPosition, UNSAVE_CHANGE_DIALOG_SCROLL_WINDOW_SIZE, wxNO_BORDER|wxVSCROLL); - m_scrolledWindow->SetScrollRate(0, 5); + m_scrolledWindow->SetScrollRate(0, FromDIP(20)); m_scrolledWindow->SetBackgroundColour(GREY200); m_sizer_bottom = new wxBoxSizer(wxVERTICAL); m_sizer_bottom->Add(m_scrolledWindow, 1, wxEXPAND, 0); diff --git a/src/slic3r/GUI/UpdateDialogs.cpp b/src/slic3r/GUI/UpdateDialogs.cpp index 0710e054f7..42383cd177 100644 --- a/src/slic3r/GUI/UpdateDialogs.cpp +++ b/src/slic3r/GUI/UpdateDialogs.cpp @@ -130,7 +130,7 @@ MsgUpdateConfig::MsgUpdateConfig(const std::vector &updates, bool force_ m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15)); auto m_scrollwindw_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)),wxVSCROLL); - m_scrollwindw_release_note->SetScrollRate(0, 5); + m_scrollwindw_release_note->SetScrollRate(0, FromDIP(20)); m_scrollwindw_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8)); m_scrollwindw_release_note->SetMaxSize(wxSize(FromDIP(560), FromDIP(430))); m_scrollwindw_release_note->SetWindowStyle(wxVSCROLL); diff --git a/src/slic3r/GUI/UpgradePanel.cpp b/src/slic3r/GUI/UpgradePanel.cpp index d31fde8613..5f168025ed 100644 --- a/src/slic3r/GUI/UpgradePanel.cpp +++ b/src/slic3r/GUI/UpgradePanel.cpp @@ -1629,7 +1629,7 @@ UpgradePanel::UpgradePanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, auto m_main_sizer = new wxBoxSizer(wxVERTICAL); m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - m_scrolledWindow->SetScrollRate(5, 25); + m_scrolledWindow->SetScrollRate(5, FromDIP(20)); m_machine_list_sizer = new wxBoxSizer(wxVERTICAL); diff --git a/src/slic3r/GUI/Widgets/CheckList.cpp b/src/slic3r/GUI/Widgets/CheckList.cpp index c0101441ff..ed404c62fe 100644 --- a/src/slic3r/GUI/Widgets/CheckList.cpp +++ b/src/slic3r/GUI/Widgets/CheckList.cpp @@ -62,7 +62,7 @@ CheckList::CheckList( s_sizer = new wxBoxSizer(wxVERTICAL); m_scroll_area = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, scroll_style); - m_scroll_area->SetScrollRate(0, 10); + m_scroll_area->SetScrollRate(0, FromDIP(20)); m_scroll_area->SetSizer(s_sizer); m_scroll_area->SetBackgroundColour(parent->GetBackgroundColour()); m_scroll_area->Bind(wxEVT_RIGHT_DOWN, &CheckList::ShowMenu, this); @@ -223,4 +223,4 @@ void CheckList::ShowMenu(wxMouseEvent &evt) wxPoint screen_pos = src->ClientToScreen(evt.GetPosition()); wxPoint local_pos = ScreenToClient(screen_pos); PopupMenu(&m, local_pos); -} \ No newline at end of file +} diff --git a/src/slic3r/GUI/Widgets/ProgressDialog.cpp b/src/slic3r/GUI/Widgets/ProgressDialog.cpp index c16f3bcf1f..6d42df1007 100644 --- a/src/slic3r/GUI/Widgets/ProgressDialog.cpp +++ b/src/slic3r/GUI/Widgets/ProgressDialog.cpp @@ -199,7 +199,7 @@ bool ProgressDialog::Create(const wxString &title, const wxString &message, int m_sizer_main->Add(m_simplebook, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(28)); } else { m_msg_scrolledWindow = new wxScrolledWindow( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL ); - m_msg_scrolledWindow->SetScrollRate(0,5); + m_msg_scrolledWindow->SetScrollRate(0, FromDIP(20)); wxBoxSizer* m_msg_sizer= new wxBoxSizer(wxVERTICAL); m_msg = new wxStaticText(m_msg_scrolledWindow, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(PROGRESSDIALOG_SIMPLEBOOK_SIZE.x, -1), 0); diff --git a/src/slic3r/GUI/Widgets/WebView.cpp b/src/slic3r/GUI/Widgets/WebView.cpp index a29a3cb725..a313e80d66 100644 --- a/src/slic3r/GUI/Widgets/WebView.cpp +++ b/src/slic3r/GUI/Widgets/WebView.cpp @@ -10,6 +10,7 @@ #include #include +#include #if wxUSE_WEBVIEW_EDGE #include #elif defined(__WXMAC__) @@ -235,7 +236,9 @@ class FakeWebView : public wxWebView wxDEFINE_EVENT(EVT_WEBVIEW_RECREATED, wxCommandEvent); static std::vector g_webviews; -static std::vector g_delay_webviews; +// Webviews waiting for their script handler while another one is added; adding it yields, so a +// view can be destroyed while it waits. +static std::vector> g_delay_webviews; class WebViewRef : public wxObjectRefData { @@ -340,8 +343,9 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url) addScriptMessageHandler(webView); while (!g_delay_webviews.empty()) { auto views = std::move(g_delay_webviews); - for (auto wv : views) - addScriptMessageHandler(wv); + for (const wxWeakRef& wv : views) + if (wv) + addScriptMessageHandler(wv.get()); } } #ifndef __WIN32__ diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index 88e2df31c0..0857612ac3 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -1222,7 +1222,7 @@ ImageTransientPopup::ImageTransientPopup( wxWindow *parent, bool scrolled, wxBit m_panel->SetSize(300, 300); // And also actually enable them. - m_panel->SetScrollRate(10, 10); + m_panel->SetScrollRate(10, FromDIP(20)); } else { diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index a3696c47ad..0ff7e65e5d 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -11,8 +12,10 @@ #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/AABBTreeLines.hpp" #include "libslic3r/Fill/Fill.hpp" +#include "libslic3r/Fill/FillAdaptive.hpp" #include "libslic3r/Flow.hpp" #include "libslic3r/Geometry.hpp" +#include "libslic3r/IntersectionPoints.hpp" #include "libslic3r/Layer.hpp" #include "libslic3r/Print.hpp" #include "libslic3r/PrintConfig.hpp" @@ -1196,6 +1199,231 @@ TEST_CASE("Trapezoidal grid infill rounds its corners only with more than one li REQUIRE(single_smooth.length == single_sharp.length); } +TEST_CASE("Multiline cubic infill follows the cubic lines without crossing itself", "[Fill]") +{ + const int multiline = GENERATE(2, 3); + const double spacing = 0.45; + const double density = 0.3; + const double wall = multiline * spacing; + CAPTURE(multiline); + + const ExPolygon region{ Slic3r::Points{ Point::new_scale(0., 0.), Point::new_scale(40., 0.), + Point::new_scale(40., 40.), Point::new_scale(0., 40.) } }; + auto fill = [®ion, spacing](int lines, double density, size_t layer_id, double z) { + std::unique_ptr filler(Slic3r::Fill::new_from_type("cubic")); + filler->spacing = spacing; + filler->angle = float(M_PI / 7.); + filler->layer_id = layer_id; + filler->z = z; + + FillParams params; + params.density = float(density); + params.multiline = lines; + params.dont_adjust = true; + params.anchor_length_max = 0.f; // The bare pattern, without connections along the boundary. + Slic3r::Surface surface(stInternal, region); + return filler->fill_surface(&surface, params); + }; + // Away from the boundary, where a line is clipped earlier than the side of its wall. + const Polygons inner = shrink(to_polygons(region), scale_(3.)); + auto farthest = [&inner](const Polylines &from, const Polylines &to) { + const AABBTreeLines::LinesDistancer tree(to_lines(to)); + double distance = 0.; + for (const Polyline &path : intersection_pl(from, inner)) + for (const Point &point : path.equally_spaced_points(scale_(0.2))) + distance = std::max(distance, tree.distance_from_lines(point)); + return unscale(distance); + }; + + // One z period of the pattern: sqrt(2) / 3 of the 3 * wall / density line spacing. + const double z_period = std::sqrt(2.) * wall / density; + const size_t layers = 30; + for (size_t layer_id = 0; layer_id < layers; ++layer_id) { + const double z = z_period * (layer_id + 0.5) / layers; + CAPTURE(layer_id, z); + const Polylines walls = fill(multiline, density, layer_id, z); + REQUIRE_FALSE(walls.empty()); + CHECK(get_intersections(to_lines(walls)).empty()); + // Long paths running out to the boundary, not loops around the cells. + CHECK(std::none_of(walls.begin(), walls.end(), [](const Polyline &path) { return path.first_point() == path.last_point(); })); + + // Single lines at the same spacing: the walls are drawn along them. + const Polylines lines = fill(1, density / multiline, layer_id, z); + REQUIRE_FALSE(lines.empty()); + CHECK(farthest(lines, walls) < 0.5 * wall); + CHECK(farthest(walls, lines) < 1.5 * wall); + } +} + +TEST_CASE("Multiline adaptive cubic infill keeps its lines apart without closing them around the cells", "[Fill]") +{ + const std::string pattern = GENERATE("adaptivecubic", "supportcubic"); + const int multiline = GENERATE(2, 3); + CAPTURE(pattern, multiline); + + // A sphere refines the octree all around, so the finer lines end on the coarser ones at every layer. + TriangleMesh sphere = Slic3r::Test::mesh(Slic3r::Test::TestMesh::sphere_50mm); + sphere.scale(0.3f); + Print print; + Slic3r::Test::init_and_process_print({sphere}, print, + {{"sparse_infill_pattern", pattern}, + {"sparse_infill_density", "40%"}, + {"fill_multiline", multiline}, + {"infill_anchor", 0}, + {"infill_anchor_max", 0}, + {"layer_height", 0.3}}); + + size_t paths = 0, loops = 0; + for (const Layer *layer : print.objects().front()->layers()) { + Polylines printed; + Polygons sparse; + double spacing = 0.; + for (const LayerRegion *region : layer->regions()) { + for (const ExtrusionEntity *entity : region->fills.flatten().entities) + if (entity->role() == erInternalInfill) + entity->collect_polylines(printed); + for (const Surface &surface : region->fill_surfaces.surfaces) + if (surface.surface_type == stInternal) + append(sparse, shrink(to_polygons(surface.expolygon), scale_(1.))); + spacing = region->flow(frInfill).spacing(); + } + if (printed.empty()) + continue; + CAPTURE(layer->print_z); + paths += printed.size(); + loops += std::count_if(printed.begin(), printed.end(), [](const Polyline &pl) { return pl.first_point() == pl.last_point(); }); + CHECK(get_intersections(to_lines(printed)).empty()); + + // Neighbouring lines stay a line spacing apart, less the overlap of a line end with the wall it stops on. + // Pieces of one line that meet end to end are one line. + std::vector line_of(printed.size()); + std::iota(line_of.begin(), line_of.end(), 0); + std::function find = [&](size_t i) { return line_of[i] == i ? i : line_of[i] = find(line_of[i]); }; + for (size_t i = 0; i < printed.size(); ++i) + for (size_t j = i + 1; j < printed.size(); ++j) + for (const Point &a : { printed[i].first_point(), printed[i].last_point() }) + for (const Point &b : { printed[j].first_point(), printed[j].last_point() }) + if ((a - b).cast().norm() < SCALED_EPSILON) + line_of[find(i)] = find(j); + Lines lines; + std::vector owner; + for (size_t i = 0; i < printed.size(); ++i) + for (const Line &line : printed[i].lines()) { + lines.push_back(line); + owner.push_back(find(i)); + } + AABBTreeLines::LinesDistancer tree(lines); + double closest = spacing; + for (size_t i = 0; i < printed.size(); ++i) + for (const Point &p : printed[i].equally_spaced_points(scale_(0.1))) + if (contains(sparse, p)) + for (size_t k : tree.all_lines_in_radius(p, scale_(spacing))) + if (owner[k] != find(i)) + closest = std::min(closest, unscale(lines[k].distance_to(p))); + CHECK(closest > 0.45 * spacing); + } + REQUIRE(paths > 0); + // The lines run on through the cells instead of each cell getting its own loops. + CHECK(loops < paths / 4); +} + +TEST_CASE("Multiline adaptive cubic paths touch where they bounce off each other", "[Fill]") +{ + const int sweep = GENERATE(0, 1, 2); + // Offset of the third family in walls, so the three meet in points or in small triangles. + const double shift = GENERATE(0., 0.1, 0.5, 1., 2.5, -0.5, -1.); + // Like finer octree lines ending on coarser ones, the 60 degree lines may start on the horizontal line through 0. + const bool starting = GENERATE(false, true); + CAPTURE(sweep, shift, starting); + + const double d1 = scale_(0.8), pitch = scale_(8.), inner = scale_(12.); + Lines lines; + for (int k = 0; k < 3; ++k) { + const Vec2d dir(std::cos(k * M_PI / 3.), std::sin(k * M_PI / 3.)), normal(-dir.y(), dir.x()); + for (int i = -6; i <= 6; ++i) { + const Vec2d mid = (i * pitch + (k == 2 ? shift * d1 : 0.)) * normal; + const double start = k == 1 && starting ? -mid.y() / dir.y() : -10. * pitch; + lines.emplace_back((mid + start * dir).cast(), (mid + 10. * pitch * dir).cast()); + } + } + const Polylines paths = FillAdaptive::multiline_paths(lines, d1, 0., sweep, BoundingBox(Point::new_scale(-20., -20.), Point::new_scale(20., 20.))); + REQUIRE_FALSE(paths.empty()); + CHECK(get_intersections(to_lines(paths)).empty()); + + Lines pieces; + std::vector owner; + for (size_t i = 0; i < paths.size(); ++i) + for (const Line &line : paths[i].lines()) { + pieces.push_back(line); + owner.push_back(i); + } + AABBTreeLines::LinesDistancer tree(pieces); + auto clearance = [&](size_t i) { + const Line &a = pieces[i]; + double distance = std::numeric_limits::max(); + for (size_t j : tree.all_lines_in_radius(a.midpoint(), 0.5 * a.length() + 2. * d1)) + if (owner[j] != owner[i]) { + const Line &b = pieces[j]; + distance = std::min({ distance, a.distance_to(b.a), a.distance_to(b.b), b.distance_to(a.a), b.distance_to(a.b) }); + } + return distance; + }; + auto inside = [inner](const Point &p) { return std::abs(p.x()) < inner && std::abs(p.y()) < inner; }; + + double closest = std::numeric_limits::max(); + for (size_t i = 0; i < pieces.size(); ++i) + if (inside(pieces[i].midpoint())) + closest = std::min(closest, clearance(i)); + CHECK(closest > 0.99 * d1); + + // Each path at a crossing touches another one there, none stops short of it. + double widest = 0.; + for (size_t i = 0; i < lines.size(); ++i) + for (size_t j = i + 1; j < lines.size(); ++j) + if (Point crossing; line_alg::intersection(lines[i], lines[j], &crossing) && inside(crossing)) { + std::map at; + for (size_t k : tree.all_lines_in_radius(crossing, 1.2 * d1)) + at.emplace(owner[k], std::numeric_limits::max()); + for (size_t k : tree.all_lines_in_radius(crossing, 2. * d1)) + if (auto it = at.find(owner[k]); it != at.end()) + it->second = std::min(it->second, clearance(k)); + for (const auto &path : at) + widest = std::max(widest, path.second); + } + CHECK(widest < 1.02 * d1); +} + +TEST_CASE("Multiline adaptive cubic paths reach the line they end on when another path ends on them", "[Fill]") +{ + const int sweep = GENERATE(0, 1, 2); + // Where the 120 degree line starts on the horizontal one, in walls from the 60 degree line. + const double start = GENERATE(0.3, 0.6, 1., 2.); + CAPTURE(sweep, start); + + const double d1 = scale_(0.8), overlap = 0.1 * d1, length = scale_(30.); + const Vec2d diagonal(0.5, 0.5 * std::sqrt(3.)), horizontal(1., 0.), steep(-0.5, 0.5 * std::sqrt(3.)); + const Vec2d on_horizontal = start * d1 * horizontal; + const Lines lines{ Line((-length * diagonal).cast(), (length * diagonal).cast()), + Line(Point(0, 0), (length * horizontal).cast()), + Line(on_horizontal.cast(), (on_horizontal - length * steep).cast()) }; + const Polylines paths = FillAdaptive::multiline_paths(lines, d1, overlap, sweep, BoundingBox(Point::new_scale(-40., -40.), Point::new_scale(40., 40.))); + + // The end of the path along each line nearest to where that line starts. + auto end_along = [&paths](const Line &line) { + for (const Polyline &path : paths) + if (line.distance_to(path.first_point()) < SCALED_EPSILON && line.distance_to(path.last_point()) < SCALED_EPSILON) + return (path.first_point() - line.a).cast().norm() < (path.last_point() - line.a).cast().norm() ? path.first_point() : path.last_point(); + return Point(std::numeric_limits::max(), 0); + }; + const Point horizontal_end = end_along(lines[1]), steep_end = end_along(lines[2]); + REQUIRE(horizontal_end.x() != std::numeric_limits::max()); + REQUIRE(steep_end.x() != std::numeric_limits::max()); + // Both reach the overlap into the wall of the path they stop at, none stops short of it. + CHECK_THAT(line_alg::distance_to_infinite(lines[0], horizontal_end) / d1, Catch::Matchers::WithinAbs(0.9, 0.01)); + CHECK(lines[1].distance_to(steep_end) / d1 < 0.91); + CHECK(get_intersections(to_lines(paths)).empty()); +} + TEST_CASE("3D honeycomb infill rounds its octahedral waves with the smooth factor", "[Fill]") { auto shape_for = [](const std::string &smooth_factor) { diff --git a/tests/fff_print/test_perimeters.cpp b/tests/fff_print/test_perimeters.cpp index 6d9442e5d7..459ba0a178 100644 --- a/tests/fff_print/test_perimeters.cpp +++ b/tests/fff_print/test_perimeters.cpp @@ -630,3 +630,97 @@ TEST_CASE("A lower layer sliver too thin to print does not support the wall abov // A rib that does get printed takes the 20mm outer wall running along it out of the overhangs. CHECK(printable < no_rib - scale_(15.)); } + +namespace { + +// Every setting the fuzzy skin assertions below depend on. +DynamicPrintConfig fuzzy_skin_config(const char *wall_generator) +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "wall_generator", wall_generator }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + // One wall, so every wall point along the long sides belongs to the fuzzed outer wall. + { "wall_loops", 1 }, + { "fuzzy_skin", "external" }, + { "fuzzy_skin_noise_type", "classic" }, + { "fuzzy_skin_thickness", 0.3 }, + { "fuzzy_skin_point_distance", 0.8 }, + }); + return config; +} + +// How far the wall points over the middle 60% of the layer's length stray across its width, worst side. +// A negative result means there is no layer at `print_z`. +double mid_span_wall_spread(const Print &print, double print_z) +{ + for (const Layer *layer : print.objects().front()->layers()) { + if (std::abs(layer->print_z - print_z) > 1e-4) + continue; + const BoundingBox bbox = get_extents(layer->lslices); + const coord_t x_min = bbox.min.x() + bbox.size().x() / 5; + const coord_t x_max = bbox.max.x() - bbox.size().x() / 5; + Points points; + for (const LayerRegion *region : layer->regions()) + region->perimeters.collect_points(points); + coord_t spread = 0; + for (const bool south : { true, false }) { + coord_t lo = bbox.max.y(), hi = bbox.min.y(); + for (const Point &p : points) + if (p.x() > x_min && p.x() < x_max && (p.y() < bbox.center().y()) == south) { + lo = std::min(lo, p.y()); + hi = std::max(hi, p.y()); + } + spread = std::max(spread, hi - lo); + } + return unscale(spread); + } + return -1.; +} + +} // namespace + +// TestMesh::bridge is a 50x10mm deck from z=5 to z=8 on two 5mm-wide pillars, leaving a 40mm span. The deck's +// first layer (print_z 5.2) crosses the span unsupported; the layers above it rest on the deck. +TEST_CASE("Fuzzy skin leaves the walls of a bridge smooth", "[Perimeters]") +{ + const char *wall_generator = GENERATE("classic", "arachne"); + CAPTURE(wall_generator); + + Print print; + init_and_process_print({ TestMesh::bridge }, print, fuzzy_skin_config(wall_generator)); + REQUIRE_FALSE(print.objects().empty()); + + // Control: one deck layer up the same walls rest on the deck, so they are fuzzed. + CHECK(mid_span_wall_spread(print, 5.6) > 0.1); + // Over the unsupported span the walls stay straight. + const double bridged = mid_span_wall_spread(print, 5.2); + CHECK(bridged >= 0.); + CHECK(bridged < 0.001); +} + +// One object: a 20x20x3mm block on the bed and a second one floating above it from z=5 to z=8. The layers in +// the gap are empty, so the floating block's first layer (print_z 5.2) has a layer below it with nothing +// printed on it; the layers above rest on the floating block. +TEST_CASE("Fuzzy skin leaves the walls over an empty layer smooth", "[Perimeters]") +{ + const char *wall_generator = GENERATE("classic", "arachne"); + CAPTURE(wall_generator); + + TriangleMesh mesh = make_cube(20., 20., 3.); + TriangleMesh floating = make_cube(20., 20., 3.); + floating.translate(0.f, 0.f, 5.f); + mesh.merge(floating); + + Print print; + init_and_process_print({ mesh }, print, fuzzy_skin_config(wall_generator)); + REQUIRE_FALSE(print.objects().empty()); + + // Control: one layer up the walls rest on the floating block, so they are fuzzed. + CHECK(mid_span_wall_spread(print, 5.6) > 0.1); + // Nothing is printed under the first floating layer, so its walls stay straight. + const double floating_first_layer = mid_span_wall_spread(print, 5.2); + CHECK(floating_first_layer >= 0.); + CHECK(floating_first_layer < 0.001); +} diff --git a/tests/fff_print/test_wipe_tower.cpp b/tests/fff_print/test_wipe_tower.cpp index 24d50d6e66..014eab6f8b 100644 --- a/tests/fff_print/test_wipe_tower.cpp +++ b/tests/fff_print/test_wipe_tower.cpp @@ -308,6 +308,119 @@ TEST_CASE("A single-filament plate reserves a tower only when one is actually pr } } +// Filament 2 on the top surface only, so every layer below it is a toolchange-free tower layer: the +// run "Combine sparse layers" folds. The two heights decide whether anything folds, so they are the +// caller's business. +static DynamicPrintConfig sparse_run_config(double layer_height, const char *max_layer_height, bool combine) +{ + DynamicPrintConfig config = multifilament_config(2, { + { "top_surface_filament_id", 2 }, + { "enable_prime_tower", true }, + { "wipe_tower_x", 50 }, // inside the 200x200 test bed + { "wipe_tower_y", 50 }, + { "prime_tower_width", 35 }, + { "min_layer_height", "0.08"}, + { "single_extruder_multi_material", true }, + { "timelapse_type", "0" }, + { "enable_wrapping_detection", false }, + { "raft_layers", "0" } }); + // A taller first layer would top the plan and hide what the run does, so slice at one height. + config.set_deserialize_strict({ { "layer_height", std::to_string(layer_height) }, + { "initial_layer_print_height", std::to_string(layer_height) }, + { "max_layer_height", max_layer_height }, + { "wipe_tower_sparse_layers_combination", combine ? "1" : "0" } }); + return config; +} + +// What a sliced tower did with its sparse run. +struct SparseRunResult { size_t planned, sparse, folded; float tallest_printed, printed_height; std::string gcode; }; + +static SparseRunResult slice_sparse_run(const DynamicPrintConfig &config) +{ + Print print; + Model model; + init_print({ cube(10) }, print, model, config); + print.apply(model, config); + print.process(); + REQUIRE(print.is_step_done(psWipeTower)); + + SparseRunResult r{}; + for (const std::vector &layer : print.wipe_tower_data().tool_changes) { + if (layer.empty()) + continue; + ++r.planned; + if (wipe_tower_layer_is_sparse(layer)) + ++r.sparse; + if (wipe_tower_layer_is_combined_away(layer)) { + ++r.folded; + } else { + r.tallest_printed = std::max(r.tallest_printed, layer.front().layer_height); + r.printed_height += layer.front().layer_height; + } + } + r.gcode = Slic3r::Test::gcode(print); + return r; +} + +// How often the G-code declares `height` in the tag this printer's processor reads. The dialect is a +// global the exporter sets from the printer, so this is only correct after a slice - the point below. +static size_t count_height_tags(const std::string &gcode, const char *height) +{ + const std::string tag = ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height) + height + "\n"; + size_t n = 0; + for (size_t p = gcode.find(tag); p != std::string::npos; p = gcode.find(tag, p + 1)) + ++n; + return n; +} + +TEST_CASE("Combining sparse layers folds a run into whole layers the nozzle can lay down", "[WipeTower]") +{ + // 0.1 mm layers under a 0.32 mm cap: three fit (0.3), a fourth does not, so a run prints once + // every three layers at 0.3 mm. + const SparseRunResult plain = slice_sparse_run(sparse_run_config(0.1, "0.32", false)); + const SparseRunResult combined = slice_sparse_run(sparse_run_config(0.1, "0.32", true)); + + REQUIRE(plain.planned == combined.planned); // the plan still has one layer per object layer + REQUIRE(plain.sparse > 10); + CHECK(plain.folded == 0); + CHECK_THAT(plain.tallest_printed, Catch::Matchers::WithinAbs(0.1f, 1e-4f)); + + CHECK(combined.folded > 0); + CHECK_THAT(combined.tallest_printed, Catch::Matchers::WithinAbs(0.3f, 1e-4f)); + // Two of every three sparse layers fold away, leaving the toolchange layers untouched. + CHECK(combined.folded <= plain.sparse); + CHECK(combined.folded >= plain.sparse / 2); + // What folds away comes back as height on the layer that prints the run: no gap, nothing twice. + CHECK_THAT(combined.printed_height, Catch::Matchers::WithinAbs(plain.printed_height, 1e-3f)); +} + +TEST_CASE("A run too thin to reach the nozzle's layer height is left alone", "[WipeTower]") +{ + // Only whole layers merge, so two 0.2 mm layers (0.4) do not fit a 0.32 mm maximum and the tower + // prints as if the option were off. This is the common 0.4 nozzle case; the tooltip says so. + const SparseRunResult plain = slice_sparse_run(sparse_run_config(0.2, "0.32", false)); + const SparseRunResult combined = slice_sparse_run(sparse_run_config(0.2, "0.32", true)); + + REQUIRE(plain.sparse > 10); + CHECK(combined.folded == 0); + CHECK(combined.planned == plain.planned); + CHECK_THAT(combined.tallest_printed, Catch::Matchers::WithinAbs(0.2f, 1e-4f)); +} + +TEST_CASE("A merged tower layer declares its own height to the G-code processor", "[WipeTower]") +{ + // Each writer declares a height in a hardcoded tag dialect while the processor reads only its + // printer's, so one of them is always dropped. A merged layer is the first time that shows, as a + // thick layer drawn and costed as a thin one. 0.2 mm layers under a 0.42 mm maximum merge in pairs. + const SparseRunResult plain = slice_sparse_run(sparse_run_config(0.2, "0.42", false)); + const SparseRunResult combined = slice_sparse_run(sparse_run_config(0.2, "0.42", true)); + + REQUIRE(combined.folded > 0); + CHECK_THAT(combined.tallest_printed, Catch::Matchers::WithinAbs(0.4f, 1e-4f)); + // Every layer that prints a merged run has to say so, and nothing may say so without the option. + CHECK(count_height_tags(combined.gcode, "0.4") - count_height_tags(plain.gcode, "0.4") == combined.folded); +} + TEST_CASE("A tower printed without a tool change is still validated against the bed", "[WipeTower]") { // Wrapping detection prints a tower on a plate that purges one filament. Neither the old diff --git a/tests/libslic3r/test_wipe_tower.cpp b/tests/libslic3r/test_wipe_tower.cpp index 277e11eceb..6f6daa74b8 100644 --- a/tests/libslic3r/test_wipe_tower.cpp +++ b/tests/libslic3r/test_wipe_tower.cpp @@ -278,6 +278,98 @@ TEST_CASE("Only the keep-out ring an object is measured against is drawn", "[Wip CHECK_THAT(unscaled(get_extents(zone.grown_body).max.x()), WithinAbs(10. + 0.5 * (40. - 0.2), 0.02)); } +// --------------------------------------------------------------------------------------------- +// "Combine sparse layers": folding a run of toolchange-free layers into one thicker tower layer. +// --------------------------------------------------------------------------------------------- + +TEST_CASE("Sparse layers are combined only when every layer is still the tower's to place", "[WipeTower][CombineSparseLayers]") { + PrintConfig cfg; + cfg.timelapse_type.value = TimelapseType::tlTraditional; + cfg.enable_wrapping_detection.value = false; + cfg.wipe_tower_no_sparse_layers.value = false; + + cfg.wipe_tower_sparse_layers_combination.value = false; + CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg)); + cfg.wipe_tower_sparse_layers_combination.value = true; + CHECK(wipe_tower_sparse_layers_combined(cfg)); + + // Dropping the sparse layers outright leaves nothing to combine. + cfg.wipe_tower_no_sparse_layers.value = true; + CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg)); + CHECK(wipe_tower_sparse_layers_skipped(cfg)); + cfg.wipe_tower_no_sparse_layers.value = false; + + // Both of these park the nozzle on the tower every layer, so no layer may be folded away. + cfg.timelapse_type.value = TimelapseType::tlSmooth; + CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg)); + cfg.timelapse_type.value = TimelapseType::tlTraditional; + cfg.enable_wrapping_detection.value = true; + CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg)); +} + +TEST_CASE("A layer folded into a later one is marked on the results the emitter reads", "[WipeTower][CombineSparseLayers]") { + WipeTower::ToolChangeResult folded = make_tcr(1, 1, 0.2f); + folded.combined_away = true; + CHECK(wipe_tower_layer_is_combined_away({folded})); + CHECK_FALSE(wipe_tower_layer_is_combined_away({make_tcr(1, 1, 0.2f)})); + CHECK_FALSE(wipe_tower_layer_is_combined_away({})); +} + +TEST_CASE("A run of sparse layers prints once, on its last layer, at the height it covers", "[WipeTower][CombineSparseLayers]") { + // Eight 0.1 mm layers on a 0.3 mm cap: a toolchange on the first and the last, sparse between. + std::vector heights(8, 0.1f); + const std::vector sparse{0, 1, 1, 1, 1, 1, 1, 0}; + const std::vector caps(8, 0.3f); + + const std::vector combined = combine_sparse_wipe_tower_layers(heights, sparse, caps, 0); + REQUIRE(combined.size() == heights.size()); + // Three layers fill the cap exactly: the run flushes on layers 3 and 6, the two below each go. + CHECK(combined == std::vector{0, 1, 1, 0, 1, 1, 0, 0}); + CHECK_THAT(heights[3], WithinAbs(0.3f, 1e-5f)); + CHECK_THAT(heights[6], WithinAbs(0.3f, 1e-5f)); + // Layers that print keep the object covered: nothing is lost and nothing is printed twice. + float printed = 0.f; + for (size_t i = 0; i < heights.size(); ++i) + if (! combined[i]) + printed += heights[i]; + CHECK_THAT(printed, WithinAbs(0.8f, 1e-5f)); + // A toolchange has to purge at its own z, so those layers are left exactly as planned. + CHECK_THAT(heights[0], WithinAbs(0.1f, 1e-5f)); + CHECK_THAT(heights[7], WithinAbs(0.1f, 1e-5f)); +} + +TEST_CASE("The maximum layer height of the nozzle that prints the run caps the merge", "[WipeTower][CombineSparseLayers]") { + // The cap that counts belongs to the layer that prints the run; one that prints nothing lays + // nothing down, so its own cap cannot constrain it. Five 0.1 mm layers, sparse above the first, + // layer 3's nozzle taking only 0.15. (A real run holds one filament, so this only tests the + // look-ahead.) + std::vector heights(5, 0.1f); + std::vector caps(5, 0.3f); + caps[3] = 0.15f; + const std::vector combined = combine_sparse_wipe_tower_layers(heights, {0, 1, 1, 1, 1}, caps, 0); + // Layer 2 cannot hand its 0.2 mm on to layer 3, so it prints there and a fresh run starts above. + CHECK(combined == std::vector{0, 1, 0, 1, 0}); + CHECK_THAT(heights[2], WithinAbs(0.2f, 1e-5f)); + CHECK_THAT(heights[4], WithinAbs(0.2f, 1e-5f)); + + // A single layer already past the cap is printed as planned rather than shrunk. + std::vector tall{0.2f, 0.4f, 0.4f}; + const std::vector tall_combined = combine_sparse_wipe_tower_layers(tall, {0, 1, 1}, {0.3f, 0.3f, 0.3f}, 0); + CHECK(tall_combined == std::vector{0, 0, 0}); + CHECK_THAT(tall[1], WithinAbs(0.4f, 1e-5f)); +} + +TEST_CASE("The tower's first layer is never folded away", "[WipeTower][CombineSparseLayers]") { + // It carries the brim and has to sit on the bed, however little it purges. + std::vector heights(4, 0.1f); + const std::vector combined = combine_sparse_wipe_tower_layers(heights, {1, 1, 1, 1}, std::vector(4, 0.5f), 0); + CHECK(combined.front() == 0); + CHECK_THAT(heights.front(), WithinAbs(0.1f, 1e-5f)); + // Everything above it merges into the top layer, which the cap still fits. + CHECK(combined == std::vector{0, 1, 1, 0}); + CHECK_THAT(heights.back(), WithinAbs(0.3f, 1e-5f)); +} + TEST_CASE("Footprint padding covers the brim and the extrusion half width on each side", "[WipeTower][NoSparseLayers]") { // A nominal outline hulls extrusion centre lines and is re-centred once the real wall is known, // so a line width per side on top of the brim is what keeps an estimate enclosing the real tower.