Compare commits

...
Author SHA1 Message Date
Ian Chua ae36951f99 fix: clang-tidy 2026-10-17 15:31:46 +08:00
Ian Chua b32856317a Merge branch 'main' into feat/printer-agent-impl 2026-10-17 15:30:43 +08:00
fd28f6c189 Fix Ctrl+A (select current plate) and Delete All skipping objects that no longer fit or while one of instances not on the bed (#15895)
* Fix Ctrl+A (select current plate) skipping objects that no longer fit the bed

Selection::add_curr_plate() used PartPlate::contain_instance_totally(),
which excludes any instance flagged in instance_outside_set -- an
instance too big for, or dragged outside, the bed. Since that is
exactly the kind of object a user reaches for Ctrl+A to move or
delete, it silently dropped out of "select all" (and out of the
selection entirely when it was the only object on the plate).

Selection::remove_curr_plate(), used for "delete all on this plate",
already does this correctly with the plain membership check,
PartPlate::contain_instance() (no outside-bed exclusion). Match it.

Likely reachable since #12236 changed Ctrl+A's target from "all
plates" to "current plate", switching it onto this pre-existing
function.

Fixes OrcaSlicer/OrcaSlicer#15888

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Trim comment verbosity per review feedback

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix behaviour for instances

* simplify code

* Update Selection.cpp

* Update Selection.cpp

* update

* update

---------

Co-authored-by: Fernando Marino <f.marino@rheagroup.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: yw4z <ywsyildiz@gmail.com>
2026-10-17 02:21:41 +03:00
Kris Austin 9b2fcaa53a fix: "bad allocation" when opening a 2.4 project with a custom filament (#16569) 2026-10-16 20:12:24 -03:00
Ian BassiandRodrigo Faselli 0acf1e6680 Fix wall order on narrow tube walls and under outer brims, and test wall order and direction (#16421)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-16 18:56:55 -03:00
Ján Mišenko c6804fbb68 fix: print statistics placeholders fail in custom G-code (#16348)
The G-code editor lists the Print Statistics placeholders for every
custom G-code field, but total_weight, used_filament, total_cost,
print_time, normal_print_time, silent_print_time,
total_wipe_tower_cost and total_wipe_tower_filament were only defined
for filename_format. Using one in custom G-code, including the File
header G-code, stopped slicing with "Not a variable name".

They are known only once the whole G-code is generated, so they use the
reserved-tag mechanism of print_time_sec and used_filament_length: the
placeholder parser gets a tag such as @TOTAL_WEIGHT@ and
GCodeProcessor::run_post_process() replaces it with the same value
filename_format gets, with numbers written to two decimals. The
per-tag replacement loops are folded into one helper. The running
extruded_* totals are unchanged.

Fixes #15320
Fixes #7870
2026-10-16 16:30:05 -05:00
Kiss Lorand 6d40ec93f6 Fix missing tree support paths and misplaced extra walls (#16549) 2026-10-16 18:04:57 -03:00
SoftFever 82aee1abdd Cache compiles in the macOS build script, including Xcode builds (#16553) 2026-10-16 19:36:50 +08:00
Ian Chua cefdd9b609 fix: max filepath length on windows (#16552)
# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->

Windows traditionally limits many file operations to `MAX_PATH` (260
characters). Deeply nested plugin packages can exceed this limit during
`.whl` extraction, leaving files missing and causing the plugin to fail
to load.

This change adds a shared extended_path helper that prepares paths for
Windows file operations using the extended-length prefixes `\\?\` or
`\\?\UNC\`. The helper is used for ZIP archive I/O and extraction
destinations, including plugin wheel extraction. This avoids relying on
the Windows long-path setting, which changes machine-wide settings.

Paths remain unprefixed outside file operations. Code that bypasses the
shared ZIP helpers may still require separate long-path handling.

See [Microsoft's documentation on path length
limitation](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry)

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->
Tested on Windows 11 by using a portable installation of OrcaSlicer and
placing it in some really long filepath length e.g.
`C:/somename/<240 characters long folder path>/OrcaSlicer` and then
running and trying to install any plugin locally or on OrcaCloud.

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-16 18:47:49 +08:00
SoftFever caade80624 Keep the texture picker from stalling on open (#16547)
# Description

Opening the texture picker decoded every texture in the expanded
sections on the GUI thread in a single frame, each one twice at full
size. This stalled the UI on large libraries, and the full-size decodes
stayed in memory afterwards.

- Picker thumbnails are loaded lazily: only tiles inside the visible
scroll area are considered, at most 2 uncached decodes per frame, and
the rest fill in over the following frames. Deferred tiles draw as a
placeholder.
- Those follow-up frames are requested with `request_extra_frame()`.
Marking the canvas dirty from inside the render did not schedule another
idle pass, so thumbnails waited for the next mouse event.
- The selected texture is still centred on open, even when it is off
screen. The scroll reveal runs before the visibility check so that tile
is loaded on the next frame.
- Library validation no longer decodes pixels. The new
`png::is_decodable_8bit_png()` reads only the PNG header and accepts
8-bit grey or RGB/RGBA, which is what `decode_png()` and
`decode_colored_png()` take.
- Picker thumbnails are decoded without entering the decode cache
(`decode_height_texture(layer, keep_decoded = false)`). Only the 128 px
thumbnail is kept per library entry; the full-size decode is made
transiently and freed.

Tradeoff: the header-only check does not read pixel data, so a PNG with
a valid header but corrupt or truncated pixel data now passes validation
and fails when the texture is loaded. The previous full decode caught
that case.

Each thumbnail decode still runs at full size on the GUI thread, one at
a time. Peak memory per decode is unchanged; a streaming downscale would
be needed to reduce it.

# Screenshots/Recordings/Graphs

Old:


https://github.com/user-attachments/assets/03038532-396f-48c2-898c-aa06ef83e5f5


New:


https://github.com/user-attachments/assets/49fdc31a-ce1b-470a-88a7-4c6319f03b81



## Tests

Not yet run. Suggested checks:
- Open the texture picker with a library of many textures, and do not
move the mouse: tiles should fill in over a few frames without a visible
stall.
- Open the picker with a layer whose texture is far down the list: it
should be centred and loaded.
- Import a valid 8-bit grey and an RGB PNG, plus an unsupported one
(16-bit, palette, or grey-alpha): only the supported ones should be
accepted as-is, and the rest converted on load.
- Watch memory while scrolling through a large colour library: it should
not grow by the full decoded size of every texture shown.

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-16 18:11:46 +08:00
Lam Wei Lun cbd8ce98d5 Keep the texture picker redrawing while thumbnails load
Marking the canvas dirty from inside the render does not schedule another
idle pass, so deferred thumbnails waited for the next mouse event. Asking
for an extra frame does.
2026-10-16 17:55:37 +08:00
Lam Wei Lun 99714afc4c Keep picker thumbnails from retaining full-size texture decodes
The thumbnail is downscaled on upload, but the full decode it was made
from stayed in the decode cache for as long as the library entry lived.
Thumbnail decodes now skip the cache, so only the 128 px thumbnail is
kept per library texture.
2026-10-16 17:49:35 +08:00
Lam Wei Lun 3d07663ab2 Merge branch 'main' into fix/texture-picker-lazy-png-header 2026-10-16 15:41:04 +08:00
HanifKoh 29b6bbb8c9 Keep Slice and Print Visible When Plugin Page Tabs Overflow (#16541) 2026-10-16 15:39:02 +08:00
Lam Wei Lun 8b6c074c69 Fix publish dialog tab clipping and extruder switch distortion (#16543)
* Fix TabCtrl clipping titles and indicator dots

Button measures a label plus icon without the icon gap that render draws,
so icon tabs were ellipsized and the indicator dot was cut off. Widen the
horizontal padding so the measured width covers the gap.

* Re-layout publish dialog when extruder switch labels change

Setting the indicator bullet on a switch option changed its width without
laying out the page, leaving the switch distorted until the next layout.
2026-10-16 15:37:17 +08:00
Error404JoyNotFound 109d09e0b8 Fix multi-tool standby heaters by turning off unused extruders (#15849)
With ooze prevention on, an extruder that has no layers left is set to 0 °C when it is parked, instead of its standby temperature. On IMEX printers where several filaments share one heater, the heater is only turned off once every filament on it is done.
2026-10-15 16:35:53 -05:00
Matias Alejandro Yocca b00237807d Fix crash when dragging objects in sequential printing mode (#16029)
The clearance preview shown while dragging sorted instances with a comparator that is not a strict weak ordering, which aborts with libc++ debug hardening. The preview now uses object-list order, with each object's copies in their last validated print order, which is the order print validation uses. The height check is now in SequentialPrintClearance.hpp, with unit tests.
2026-10-15 16:13:28 -05:00
Lam Wei Lun 4149401c53 Keep the texture picker from stalling on open
Opening the texture picker decoded every texture in the expanded sections
on the GUI thread in one frame, each one twice at full size. Only the
visible tiles now load, at most a couple per frame, and the rest fill in
over the following frames.

The library validation no longer decodes the pixels: a header-only check,
is_decodable_8bit_png(), accepts exactly what the two decoders take.
2026-10-16 02:41:12 +08:00
mosfet80 f6cf18fb96 Update libjpeg-turbo to 3.2.0 (#16098)
Also removes the Linux-only JPEG_VERSION block, so Linux builds libjpeg with the default 6.2 ABI like Windows and macOS.
2026-10-15 12:47:21 -05:00
SoftFever 4ee1b9c6e9 Add a ResourceTexture plugin capability for height-map texture packs (#16473)
# Description

Adds a fifth Python plugin capability type, `ResourceTexture`, which
contributes
height-map textures to the Texture Displacement picker. A plugin returns
a
directory of images; they appear as selectable textures in the picker,
grouped
under one collapsible section per pack. Disabling or uninstalling a pack
removes
them again, and projects that already use one of its textures keep
working,
because a layer holds its own image bytes.

**The plugin-facing contract** is one method:

```python
class WoodPack(orca.resource_texture.ResourceTexturePluginCapabilityBase):
    def get_name(self):
        return "Wood Pack"
    def get_texture_dir(self):
        return os.path.join(os.path.dirname(__file__), "textures")
```

The host scans one non-recursive level of that directory, accepting the
same
formats a hand-picked image already accepts (`.png/.jpg/.jpeg/.bmp`),
and decodes
them through the same `load_texture_image_data()` the picker uses for
the user's
own images, so a pack texture and a hand-picked one behave identically.

**Distribution is `.whl` only for v1.** `install_plugin` copies exactly
one
file, so a `.py` plugin cannot ship sibling images; a wheel is a single
file and
the loader already extracts it on demand.

**Files of interest**

- `src/slic3r/plugin/pluginTypes/resourceTexture/` — the capability, its
  trampoline and its pybind base, mirroring the Pages type.
- `src/slic3r/plugin/host/PluginResourceTextures.{hpp,cpp}` — host
registry,
  free of wx and GUI includes so it can sit beside `PluginManager`.
- `src/slic3r/GUI/TextureLibrary.{hpp,cpp}` — the third texture source,
the
  `pack_name` provenance on an entry, and a generation counter.
- `src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp` — the picker.
- `src/slic3r/plugin/PluginFsUtils.{hpp,cpp}` —
`collect_resource_texture_files()`.

**Behaviour change worth a reviewer's attention:** the picker popup is
now a
fixed size that scrolls rather than sizing itself to its content, and it
gains
one collapsible section per pack. The sizing applies to every user, not
only
those with packs installed. With no packs installed the picker otherwise
behaves
exactly as before.

**Not in scope for v1:** folder or `.zip` install for non-programmers,
and
preset-referenceable pack textures (which would need a plugin-backed
`ConfigOptionDef::plugin_type`).

# Screenshots/Recordings/Graphs


https://github.com/user-attachments/assets/b96787cf-b475-4698-826d-5db8b9f386f1

## Tests

New suite `tests/slic3rutils/test_plugin_resource_texture.cpp`, 19
cases:

- Validator: a file inside the root is listed; a parent-directory escape
and an
absolute directory outside the root are refused; a symlink pointing
outside is
skipped; files in a subdirectory are not listed; non-regular entries are
skipped; the extension whitelist is case-insensitive and rejects
near-misses;
  the listing stops at 64 files; a missing directory yields an error.
- Listing and lifecycle: a pack's images appear and clear on disable and
unload;
an empty pack contributes nothing; two distinctly named packs each stay
one
contiguous run; a second pack declaring a loaded capability name is
refused;
  reinstalling with different bytes at the same path changes the decoded
  content.
- Hostile overrides: `get_texture_dir()` returning `data_dir()` or
`resources_dir()` is refused and the pack loads with no textures; a
raising
  override still loads the pack.
- Decoding: PNG and BMP from a pack decode through the same path as the
same
  files picked by hand.
- Persistence: a project keeps a pack texture after the pack is
uninstalled,
  bytes unchanged across a re-save.
- Wheel metadata: a minimal resource-texture wheel is accepted.

Known gaps, for transparency: the wheel path is covered only at the
metadata
gate, not through a full extraction-and-import lifecycle; there is no
test for a
registry callback after interpreter shutdown, as there is no seam for it
without
reaching into `src/`; and `.jpg` decode parity is covered by the listing
rather
than by a real fixture, since no JPEG encoder is available to synthesise
one.

Manually exercised in-app on Linux with a locally built pack wheel (five
colour
PNGs): install, enable, textures listed under the pack's own section,
then
disable and uninstall remove them. The new suite itself has not been run
locally — CI is the first execution.
2026-10-16 00:35:30 +08:00
SoftFever bc8d0a7858 Wait for the cloud preset sync before offering to migrate filament presets (#16523) 2026-10-16 00:27:33 +08:00
SoftFever e0651ec31d Let ccache cache precompiled-header compiles in Linux builds (#16519) 2026-10-16 00:09:08 +08:00
Lam Wei LunandSoftFever 0caefcf4aa Linux: only disable WebKit compositing for WebKitGTK older than 2.42 (#16385)
* Linux: only disable WebKit compositing for WebKitGTK older than 2.42

The default Wayland path set WEBKIT_DISABLE_COMPOSITING_MODE=1 whenever the
session had both DISPLAY and WAYLAND_DISPLAY, i.e. any Wayland session with
XWayland. That forces WebKitGTK accelerated compositing off and also disables
threaded scrolling, so the Device tab web view (Mainsail/Fluidd) lagged unless
the app was launched with GDK_BACKEND=x11, the only path that skipped the
variable. Disabling compositing also turns off threaded scrolling.

The XWayland compositing bug the fallback worked around is fixed in WebKitGTK
>= 2.42, so apply it only for older WebKit. WebView::NeedsCompositingWorkaround()
reads the runtime WebKitGTK version; the X11 opt-in path is unchanged.

Behavior is unchanged on WebKit < 2.42, on pure X11, and on native Wayland
without XWayland. The wxWidgets skill reference is updated to match.

* Linux: use WebKit's shared-memory transport for the NVIDIA DMABUF renderer

The Device tab web view is slow on NVIDIA proprietary drivers even with
accelerated compositing left on: since the DMABUF renderer became the
default in WebKitGTK 2.42, the driver cannot back it, and web views fall
back to a slow path. Gating the old WEBKIT_DISABLE_COMPOSITING_MODE
fallback on WebKit < 2.42 is the wrong axis, because the failure is
version-independent from 2.42 on.

Force the shared-memory transport with WEBKIT_DMABUF_RENDERER_FORCE_SHM=1.
That keeps compositing and threaded scrolling and skips only the hardware
path. Also set WEBKIT_FORCE_DMABUF_RENDERER=1, which clears the isNVIDIA()
early return that Debian/Ubuntu/openSUSE patch into
rendererBufferTransportMode() and which otherwise returns before the
FORCE_SHM check is reached.

Both are set before the X11-vs-Wayland split so the workaround also covers
XWayland, and only when WebKitGTK is new enough to honour FORCE_SHM
(>= 2.44, WebView::SupportsForceSharedMemory()).

The AppImage launcher exported the superseded
WEBKIT_DISABLE_DMABUF_RENDERER=1, which is read first, leaves the transport
set empty and so disables compositing outright; replace it with the same
pair. The wxWidgets skill reference is updated to match.

* Linux: drop the AppImage WebKit DMABUF exports; correct the skill notes

The launcher cannot gate on the runtime WebKitGTK version, so exporting
WEBKIT_DMABUF_RENDERER_FORCE_SHM/WEBKIT_FORCE_DMABUF_RENDERER there would
clear a distro isNVIDIA() guard on hosts older than 2.44, where FORCE_SHM
is ignored, re-exposing the broken hardware path. The binary already sets
both with the correct gate, so remove them and say who owns the workaround.

Also correct two claims in the skill reference. An empty transport set only
means "no accelerated compositing" from 2.44 on; 2.42/2.43 still fall back
to the Wayland/X11 renderers, removed in the same release that added
FORCE_SHM. And threaded scrolling is off by default in 2.44/2.45 (on from
2.46), so FORCE_SHM keeps compositing rather than scrolling. Document the
module-level detection cost and the WEBKIT_DMABUF_RENDERER_FORCE_SHM=0
opt-out.

* Flatpak: force WebKit's shared-memory transport instead of disabling DMABUF

The entrypoint exported WEBKIT_DISABLE_DMABUF_RENDERER=1 for NVIDIA. WebKit
reads that variable first and leaves the transport set empty, which turns
accelerated compositing off, so the Device tab still lagged for NVIDIA
Flatpak users, and the binary could not override it because its setenv is
non-replacing.

The runtime's WebKitGTK is vanilla, so there is no distro isNVIDIA() guard
to clear and WEBKIT_DMABUF_RENDERER_FORCE_SHM=1 alone is the right switch:
compositing stays on and only the hardware DMABUF path is skipped.

* Flatpak: drop the WebKit NVIDIA export; the binary owns the workaround

/proc/driver/nvidia/version is readable inside the Flatpak sandbox, so the
binary's NVIDIA branch already sets WEBKIT_DMABUF_RENDERER_FORCE_SHM with
the correct runtime gate and the entrypoint's own export is redundant.

Verified with:
  flatpak run --command=cat com.orcaslicer.OrcaSlicer /proc/driver/nvidia/version
which prints the NVRM version (driver 615.71.09) from inside the sandbox.

* Linux: say what the XWayland compositing gate actually tests

DISPLAY and WAYLAND_DISPLAY are both set in a native Wayland session as
well, because GNOME and KDE run Xwayland as part of the session and export
DISPLAY for it whether or not this process is an X client. The pair means
XWayland is available in the session, not that GTK picked X11 for this
process, so "(an XWayland session)" reads as more than the condition
establishes.

Say what the pair means, and name the case the fallback actually covers:
the X11 backend inside a Wayland session, which is what the forced
GDK_BACKEND=x11 above produces in a build without EGL.

Comment only; behaviour is unchanged.

* Linux: apply the XWayland compositing fallback only where GTK picks X11

The default path set WEBKIT_DISABLE_COMPOSITING_MODE whenever DISPLAY and
WAYLAND_DISPLAY were both set, but a native Wayland app inherits both, so a
build with EGL disabled accelerated compositing — and with it threaded
scrolling — while running on Wayland, where the XWayland bug does not apply.

The only default-path case that really runs through XWayland is the forced
GDK_BACKEND=x11 used when wx lacks EGL, so set the variable there. The
backend cannot be re-checked later: that branch exports GDK_BACKEND itself,
so the force and a user's GDK_BACKEND=x11 opt-in, which deliberately keeps
compositing, would be indistinguishable.

Behaviour is unchanged for WebKitGTK >= 2.42, where the gate already skipped
the variable, and for builds without EGL. A build with EGL on WebKitGTK <
2.42 in a Wayland session no longer loses compositing and threaded scrolling.

The wxWidgets skill reference is updated to match.

---------

Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
2026-10-15 22:35:56 +08:00
SoftFever 86905a8a20 Use a keyboard icon for the shortcut button and fix unreadable guide tooltips (#16515)
* Fix unreadable text in wiki and video guide tooltips

* Use a keyboard icon for the gizmo shortcut button

* Fix blank tooltips on the Move and Scale gizmo combo boxes
2026-10-15 20:28:13 +08:00
Noisyfox d4fabbf7c8 Fix AMS gradient rendering, color presets and printer writeback (#16252)
* Fix external spool gradient and multicolor rendering

* Display AMS gradients and multicolors in color popup

* Preserve AMS gradient selection through color popup

* Match popup selections by full filament color data

* Align AMS Lite gradient geometry with BambuStudio

* Align AMS color palette with BambuStudio presets

* Match Bambu circular color rendering without background masks

* Preserve AMS gradient colors when applying filament settings

* Match AMS preview gradient swatches to the solid color shape
2026-10-15 19:43:07 +08:00
SoftFever 51efb24752 Complete traditional Chinese translations (#16513) 2026-10-15 19:32:54 +08:00
SoftFever cf9ec18df5 Complete the Simplified Chinese translations (#16512) 2026-10-15 19:32:30 +08:00
Lam Wei Lun 99de46b9d0 Rename the texture picker's pack section and OrcaCloud shortcut
Use "Texture libraries" for the plugin pack group and "Explore more
texture libraries" for the button that opens the OrcaCloud plugin hub.
2026-10-15 19:19:28 +08:00
Tobias Glothandyw4z 258f927a0f Fix: Crash by null mainframe in BackgroundSlicingProcess for CLI (#15367)
* add nullptr check

* use plater from app instead mainframe

---------

Co-authored-by: yw4z <ywsyildiz@gmail.com>
2026-10-15 14:14:26 +03:00
Lam Wei Lun b8d40ed61f Load the OrcaCloud plugin page directly when the pane is already open
The texture picker's Download from OrcaCloud shortcut asks for a specific page. Re-running the
session handoff to reach it flashed the sign-in interstitial every time; instead an already-open
pane is navigated straight to the page URL, keeping its session. The handoff is still used on the
first open, where a session has to be established.

set_cloud_mode() gains an optional page_url for this. The homepage nav passes none, so its ordinary
re-open keeps the page the pane already holds (guide §8/§10).
2026-10-15 18:46:30 +08:00
Lam Wei Lun 54e6df0235 Group texture packs under one picker section and add a Download from OrcaCloud shortcut
The texture picker lists every plugin pack as an indented, collapsible child of one
"Resource Texture" section, which now always renders so the new button is reachable even
before any pack is installed. A pack whose name carries a " by ..." suffix shows that part
on a smaller, dimmed second line, keeping the name line short.

The button switches to the Home tab and opens the OrcaCloud pane on the plugin hub filtered
to resource textures. A native open now also drives the homepage shell (nav highlight and the
cloud-mode layout), and the requested page is remembered so a sign-in re-seed keeps it.

Also fixes the PluginsDialog fuzzy-search script path, which pointed at a directory the file
is not in.
2026-10-15 18:10:33 +08:00
SoftFever 79ed246d26 Refresh the texture picker's look and scrolling 2026-10-15 15:34:05 +08:00
SoftFever 92b5301bf6 Fix trackpad scroll bounce in ImGui panels 2026-10-15 15:34:05 +08:00
SoftFever 20c157b0f4 Merge branch 'main' into feature/resource-texture-pack-plugins 2026-10-15 13:57:38 +08:00
Lam Wei Lun 4cf16febf6 Fix plugin unit test 2026-10-14 18:23:32 +08:00
Lam Wei Lun 70e52a105f Fix clang tidy 2026-10-14 17:38:52 +08:00
Lam Wei Lun b525cb79e1 Merge main 2026-10-14 17:31:35 +08:00
Lam Wei Lun 86ab598899 Add a ResourceTexture plugin capability for height-map texture packs
A fifth Python plugin capability type, ResourceTexture, contributes
height-map textures to the Texture Displacement picker. A pack returns a
directory of images; the host validates it once at load and lists the
images under a section of their own.

- New capability type ResourceTexture, type string "resource-texture",
  exposed to plugin authors as
  orca.resource_texture.ResourceTexturePluginCapabilityBase with one
  required method, get_texture_dir().
- The directory is resolved once on the load worker, under the GIL, and
  cached as a plain string: the picker runs inside the ImGui frame, where
  a trampoline call would block on the GIL behind a load/unload worker, or
  throw past EndPopup.
- Validation resolves the directory and requires containment in the plugin
  root, re-checked per file so a symlink inside the pack cannot resolve
  outside it. Containment is the only security control here and it guards
  exfiltration: layer bytes are embedded in every saved .3mf, so a pack
  that could return ~/.ssh/id_rsa would leak it through any shared project.
- New host registry PluginResourceTextures, filled from both the plugin
  and the capability lifecycle callbacks plus a startup sweep. It holds
  strings only and never a capability reference, so nothing outlives
  Py_Finalize.
- The picker lists pack textures read-only, one collapsible section per
  pack, in a fixed-size scrolling popup whose height follows the window. A
  generation counter invalidates the cached listing, and each gizmo's
  cached thumbnails, when the set of packs changes.
- Tests in tests/slic3rutils/test_plugin_resource_texture.cpp cover the
  validator (parent-directory and absolute escapes, an escaping symlink,
  the extension whitelist, the 64-file cap), the listing and its
  load/disable/re-enable/unload lifecycle, hostile get_texture_dir()
  overrides, decode parity with hand-picked files, and a .3mf round trip
  with the pack uninstalled.

No persistence work is needed: a layer carries its own image bytes and 3MF
writes them into the archive, so uninstalling a pack leaves existing
projects intact.

Behaviour change worth noting for review: the picker popup is now a fixed
size that scrolls, rather than sizing itself to its content, and it gains
one collapsible section per pack. That sizing applies to every user, not
only those with packs installed.
2026-10-14 17:15:39 +08:00
Ian Chua 476a7b1302 fix: handle missing API keys correctly 2026-10-09 22:12:32 +08:00
Ian Chua 6070436860 Merge branch 'main' into feat/printer-agent-impl 2026-10-08 19:56:02 +08:00
Ian Chua f075cdcb98 fix: harden printer-agent filament sync and teardown 2026-10-08 19:55:39 +08:00
Ian Chua e6db086729 Merge branch 'main' into feat/printer-agent-impl 2026-10-07 19:16:58 +08:00
Ian Chua f8a718b338 fix: remove dead code 2026-10-07 19:16:50 +08:00
Ian Chua a0f2ea0c29 fix: clang-tidy 2026-10-07 18:59:15 +08:00
Ian Chua 3225bfea9d fix: batch box mapping, de-dup fetches and reject unsupported starts 2026-10-07 18:58:02 +08:00
Ian Chua 5f78baafe2 fix: name Moonraker default ports and warn when a custom CA is ignored 2026-10-07 18:28:56 +08:00
Ian Chua 376ad61a5e fix: fetch webcam info in ws loop 2026-10-07 18:11:44 +08:00
Ian Chua 5b81536be2 fix: snapshot Moonraker connection state and make agent teardown responsive 2026-10-07 16:58:13 +08:00
Ian Chua 5e3a526b72 fix: pick the direction-correct lamp macro and report control-command failures 2026-10-07 16:42:50 +08:00
Ian Chua 5f0534ffd4 fix: verify TLS hostname on Moonraker wss and apply TLS config to Snapmaker fetch 2026-10-07 16:40:25 +08:00
Ian Chua 33894bb46b fix: harden printer-agent async callbacks and detached fetch threads 2026-10-07 16:08:17 +08:00
Ian Chua 17b0eda7f8 fix: clang-tidy 2026-10-06 21:06:31 +08:00
Ian Chua ec22321a4b fix: move connect_mutex to protected 2026-10-06 21:02:27 +08:00
Ian Chua 5e861b31f9 fix: use-after-free in MoonrakerPrinterAgent teardown 2026-10-06 20:54:47 +08:00
Ian Chua 334ba8f84d chore: reconcile with PR 15710 squash base 2026-10-06 20:05:39 +08:00
Ian Chua 418623e2c5 chore: remove orcasonar test changes
(cherry picked from commit c6d3e755ec)
2026-10-06 20:05:38 +08:00
Ian Chua cf476637d4 refactor: move orcasonar changes to dedicated branch
(cherry picked from commit 797c2c1b33)
2026-10-06 20:05:38 +08:00
Ian Chua ceadec4e2d chore: port add_platform_root_certificates from 16027
(cherry picked from commit 6194c4f8bb)
2026-10-06 20:05:37 +08:00
peachismomo 92b3068cf6 refactor: separate out networking infra
(cherry picked from commit 1cac04eae8)
2026-10-06 20:05:37 +08:00
peachismomo e45f5846b2 fix: printer agent and camera shutdown races
(cherry picked from commit a6cad8a6e5)
2026-10-06 20:05:36 +08:00
Ian Chua f0be15df9e fix: remove redundant earlier expires_never call
(cherry picked from commit 50c5ec2862)
2026-10-06 20:05:36 +08:00
Ian Chua 0ddb86bec7 fix: orca mqtt session timeout after connack
(cherry picked from commit b36a72e16a)
2026-10-06 20:05:35 +08:00
Ian Chua fe30bfe7c7 fix: serialize MQTT ws I/O and dewcouple keepalive
(cherry picked from commit 87eb8de001)
2026-10-06 20:05:35 +08:00
Ian Chua 44675778e2 fix: cancel inflight cloud signaling requests
(cherry picked from commit 220508534a)
2026-10-06 20:05:34 +08:00
Ian Chua b870835b01 fix: clarify OrcaCloud signaling endpoint contract
(cherry picked from commit 5a0810365b)
2026-10-06 20:05:34 +08:00
Ian Chua ea464401be feat: add ssl and ca_cert for OrcaPrinterAgent
(cherry picked from commit fa98ec5afe)
2026-10-06 20:05:34 +08:00
Ian Chua 4540a98c58 feat: add https compatibility for moonraker based printer agents
(cherry picked from commit 936b0bbc17)
2026-10-06 20:05:33 +08:00
Ian Chua 76cf64d6c9 fix: command_ams_refresh_rfid function signature in test_printer_agent.cpp
(cherry picked from commit 5222a37eff)
2026-10-06 20:05:33 +08:00
peachismomo 8d7df2684d fix: compilation after merge
(cherry picked from commit 6da938b660)
2026-10-06 20:05:32 +08:00
Ian Chua 62bd8c3743 fix: refresh token before configuring printer MQTT
(cherry picked from commit 2dcdfe2d32)
2026-10-06 20:05:32 +08:00
Ian Chua 936daee0e6 feat: add ssl compatibility with moonrakerprinteragent
(cherry picked from commit 6c353d9596)
2026-10-06 20:05:31 +08:00
Ian Chua 9a161692b5 fix: snapmakerprinteragent use moonraker's lifecycle-owned command worker
(cherry picked from commit ce0c759ded)
2026-10-06 20:05:31 +08:00
Ian Chua eb071f30f8 fix: re-implement missing commands on BBLPrinterAgent
(cherry picked from commit 9a7e925790)
2026-10-06 20:05:30 +08:00
Ian Chua 931b07364a fix: camera signaling API
(cherry picked from commit d9f1f8b207)
2026-10-06 20:05:30 +08:00
peachismomo 34c2c926d1 fix: load windows roots for cloud MQTT and camera signaling
(cherry picked from commit b91f48141d)
2026-10-06 20:05:29 +08:00
Ian Chua fc56ad002a fix: swtich start print command from http to mqtt command
(cherry picked from commit 5287fd0160)
2026-10-06 20:05:29 +08:00
Ian Chua 7b75fe02c8 fix: remove unimplemented pure virtual functions
(cherry picked from commit a7ab01b815)
2026-10-06 20:05:28 +08:00
Ian Chua 2f6de2ed86 fix: updated stale docs
(cherry picked from commit dcddb9283c)
2026-10-06 20:05:28 +08:00
Ian Chua 172f492b74 Revert "fix: latent ams in use bug"
This reverts commit 910dbcd2e6.

(cherry picked from commit ba9cf6e897)
2026-10-06 20:05:28 +08:00
Ian Chua b74fac92a5 feat: restore printer agent implementations
(cherry picked from commit d1f2ecb903)
2026-10-06 20:05:27 +08:00
107 changed files with 15900 additions and 6336 deletions

No files matched your search

@@ -569,12 +569,18 @@ on the **first** call. So:
#endif #endif
``` ```
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) branches on **Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) sets the NVIDIA
`GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On the default path it forces `GDK_BACKEND=x11` DMABUF workaround and then branches on `GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On both
when wx lacks EGL and `WAYLAND_DISPLAY` is set. It sets `WEBKIT_DISABLE_COMPOSITING_MODE=1` paths, when `/proc/driver/nvidia/version` exists and the linked WebKitGTK honours the switch (>= 2.44,
(non-replacing) only when both `DISPLAY` and `WAYLAND_DISPLAY` are set, and calls `XInitThreads()` `WebView::SupportsForceSharedMemory()`), it sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and
`WEBKIT_FORCE_DMABUF_RENDERER=1` (non-replacing): the proprietary driver cannot back WebKit's hardware
DMABUF renderer. On the default path it forces `GDK_BACKEND=x11` when wx lacks EGL and `WAYLAND_DISPLAY`
is set, and sets `WEBKIT_DISABLE_COMPOSITING_MODE=1` (non-replacing) inside that branch when the linked
WebKitGTK is older than 2.42: a forced X11 backend in a Wayland session is the only default-path case
that runs through XWayland, which `DISPLAY` and `WAYLAND_DISPLAY` alone do not tell you (a native
Wayland app inherits both). It calls `XInitThreads()`
only when `DISPLAY` is set. With neither variable set, the GUI refuses to start ("Neither DISPLAY nor only when `DISPLAY` is set. With neither variable set, the GUI refuses to start ("Neither DISPLAY nor
WAYLAND_DISPLAY set"). The details and the WebKit rule (c12912e0df) are in WAYLAND_DISPLAY set"). The details and the WebKit rules (c12912e0df) are in
`references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions. `references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions.
**wx's own detectors**, for reference: **wx's own detectors**, for reference:
@@ -61,7 +61,8 @@ available; GTK2/WebKit1 is an opt-out configuration the GUI does not support.
11. A web host that re-themes in place handles `EVT_WEBVIEW_RECREATED` without `Skip()`; one that 11. A web host that re-themes in place handles `EVT_WEBVIEW_RECREATED` without `Skip()`; one that
needs a reload lets it through. §[Orca wrapper](#orca-webview-wrapper-widgetswebview) needs a reload lets it through. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
12. Keep `WebViewWebKit`'s destructor removing the `"wx"` handler. §[Orca wrapper](#orca-webview-wrapper-widgetswebview) 12. Keep `WebViewWebKit`'s destructor removing the `"wx"` handler. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond Orca's default-path XWayland case, and never 13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond the no-EGL default path where Orca forces
`GDK_BACKEND=x11`, for WebKitGTK older than 2.42, and never
make a page's progress depend only on a C++→JS callback. §[WebKitGTK on Linux](#webkitgtk-on-linux-sessions) make a page's progress depend only on a C++→JS callback. §[WebKitGTK on Linux](#webkitgtk-on-linux-sessions)
14. GL attribute lists: legacy `int[]` lists end with `0` and spell out `WX_GL_RGBA` and 14. GL attribute lists: legacy `int[]` lists end with `0` and spell out `WX_GL_RGBA` and
`WX_GL_DOUBLEBUFFER`; `wxGLAttributes`/`wxGLContextAttrs` end with `EndList()`; MSAA is requested `WX_GL_DOUBLEBUFFER`; `wxGLAttributes`/`wxGLContextAttrs` end with `EndList()`; MSAA is requested
@@ -567,33 +568,69 @@ vue-resize workaround script and sets a persistent SQLite cookie store on the vi
## WebKitGTK on Linux sessions ## WebKitGTK on Linux sessions
**Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises) **Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises)
branches on `GDK_BACKEND`: sets the NVIDIA DMABUF workaround and then branches on `GDK_BACKEND`:
- **Both paths**, before the split: when `/proc/driver/nvidia/version` exists and the linked WebKitGTK
honours the switch (>= 2.44, `WebView::SupportsForceSharedMemory()`), it sets
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WEBKIT_FORCE_DMABUF_RENDERER=1` non-replacing. The
proprietary driver cannot back WebKit's hardware DMABUF renderer, so the shared-memory transport keeps
accelerated compositing instead of dropping it; `WEBKIT_FORCE_DMABUF_RENDERER` clears the
Debian/Ubuntu/openSUSE `isNVIDIA()` early return, which otherwise returns before the FORCE_SHM check.
- **Default path** (`GDK_BACKEND` unset or not starting with `x11`): if wx lacks EGL (`!wxHAS_EGL`) and - **Default path** (`GDK_BACKEND` unset or not starting with `x11`): if wx lacks EGL (`!wxHAS_EGL`) and
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`; it sets `WEBKIT_DISABLE_COMPOSITING_MODE=1` `WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`, and in that same branch it sets
non-replacing **only** when both `DISPLAY` and `WAYLAND_DISPLAY` are non-empty (a Wayland session with `WEBKIT_DISABLE_COMPOSITING_MODE=1` non-replacing when the linked WebKitGTK predates 2.42
XWayland available — the session, not the backend GTK will pick); `XInitThreads()` runs only when (`WebView::NeedsCompositingWorkaround()`). That forced X11 backend in a Wayland session is the only
`DISPLAY` is set. default-path case that really runs through XWayland; `DISPLAY` and `WAYLAND_DISPLAY` alone do not
identify it, because a native Wayland app inherits both. `XInitThreads()` runs only when `DISPLAY` is
set.
- **`GDK_BACKEND=x11…` opt-in path**: sets `DRI_PRIME=1` (and the NVIDIA PRIME variables when - **`GDK_BACKEND=x11…` opt-in path**: sets `DRI_PRIME=1` (and the NVIDIA PRIME variables when
`/proc/driver/nvidia/version` exists), calls `XInitThreads()`, and deliberately does **not** disable `/proc/driver/nvidia/version` exists), calls `XInitThreads()`, and deliberately does **not** disable
compositing (the comment: the WebKit2GTK XWayland bug appears fixed in WebKit2GTK ≥ 2.42, and leaving compositing, which keeps hardware acceleration for Device / Setup Wizard / login pages.
compositing on keeps hardware acceleration for Device / Setup Wizard / login pages).
Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_running_on_x11()` Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_running_on_x11()`
(see `references/platforms.md`), not these environment variables. (see `references/platforms.md`), not these environment variables.
**Pitfall.** **Pitfall.**
- **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path in Wayland sessions, - **Rule:** On NVIDIA reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
non-replacing (`setenv(..., false)`, so a value the user already exported wins), before GTK `WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver cannot back WebKit's hardware
initialises; never unconditionally. Keep JS-side fallbacks so a page never DMABUF renderer, leaving web views blank or slow; FORCE_SHM keeps accelerated compositing and only
skips the hardware path, while disabling compositing drops it. Threaded scrolling is a separate,
release-dependent default (off in 2.44/2.45, on from 2.46). `WEBKIT_DISABLE_DMABUF_RENDERER` is not a
substitute — it is read first, leaves the transport set empty (no compositing from 2.44 on, where the
X11/Wayland fallback renderers were removed) and has a reported crash path. On Debian/Ubuntu and
openSUSE the `isNVIDIA()` patch returns before the FORCE_SHM check, so `WEBKIT_FORCE_DMABUF_RENDERER=1`
must clear it first; that variable exists only in those patches. WebKit reads all of these once per
process, before the first WebView.
The test is `/proc/driver/nvidia/version`, so a hybrid laptop that renders on its iGPU also takes the
shared-memory path, which is a per-frame GPU readback rather than zero-copy; exporting
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=0` in the environment restores the hardware path.
```cpp
// Wrong: kills compositing and threaded scrolling (and can crash on modern WebKit)
::setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", false);
// Right: keep compositing, skip only the hardware DMABUF path
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /*replace*/ false);
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /*replace*/ false);
}
```
Cite: WebKit 2.52.6 `Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.cpp`
(`rendererBufferTransportMode()`), Debian `disable-nvidia-dmabuf.patch`, WebKit bug 262607.
- **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path, in the branch where wx
lacks EGL and Orca forces `GDK_BACKEND=x11`, non-replacing (`setenv(..., false)`, so a value the user
already exported wins), before GTK
initialises; never unconditionally, and only for the WebKitGTK releases that need it (older than
2.42, `WebView::NeedsCompositingWorkaround()`). Keep JS-side fallbacks so a page never
waits forever for a C++→JS call. waits forever for a C++→JS call.
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland, leaving web views **Why:** WebKit2GTK accelerated compositing could fail silently under XWayland before 2.42, leaving
blank (Setup Wizard stuck on "Loading..."); disabling it everywhere needlessly drops hardware web views blank (Setup Wizard stuck on "Loading..."); the variable also turns off threaded scrolling,
acceleration; without the JS timeout the user is stuck when the signal is lost. so applying it on a newer WebKit needlessly drops both GPU compositing and async scrolling (the
Device tab loses its smoothness); without the JS timeout the user is stuck when the signal is lost.
```cpp ```cpp
// Wrong: unconditional, kills HW acceleration everywhere // Wrong: unconditional, kills HW acceleration everywhere
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false); ::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false);
// Right (default path only): non-replacing, before GTK init, Wayland session with XWayland // Right (default path only): non-replacing, before GTK init, WebKitGTK < 2.42,
if (display && *display && wayland && *wayland) // and only in the branch that forces GDK_BACKEND=x11 (wx without EGL), the
// case that runs through XWayland
if (WebView::NeedsCompositingWorkaround() && wayland && *wayland)
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /*replace*/ false); ::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /*replace*/ false);
``` ```
Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit` Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit`
+13
View File
@@ -288,6 +288,19 @@ if (APPLE)
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts. # 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") set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO")
# The Xcode generator ignores CMAKE_<LANG>_COMPILER_LAUNCHER, so hand the launcher to
# Xcode's own Compiler Launcher setting, which wraps every clang compile (C, C++,
# Objective-C++ and the precompiled header) with one program: the C++ launcher wins when
# both are set. The setting takes a single program path, not a CMake launcher list.
# Xcode versions without it ignore it and build uncached.
if (CMAKE_GENERATOR STREQUAL "Xcode")
if (CMAKE_CXX_COMPILER_LAUNCHER)
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_CXX_COMPILER_LAUNCHER}")
elseif (CMAKE_C_COMPILER_LAUNCHER)
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_C_COMPILER_LAUNCHER}")
endif ()
endif ()
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}") message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(CMAKE_INSTALL_RPATH "$ORIGIN") set(CMAKE_INSTALL_RPATH "$ORIGIN")
+15 -1
View File
@@ -22,7 +22,7 @@ function usage() {
echo " -g: run the requested build steps inside a Docker/Podman Ubuntu 24.04 container similar to the GitHub Actions Linux runner" echo " -g: run the requested build steps inside a Docker/Podman Ubuntu 24.04 container similar to the GitHub Actions Linux runner"
echo " -h: prints this help text" echo " -h: prints this help text"
echo " -i: build the Orca Slicer AppImage (optional)" echo " -i: build the Orca Slicer AppImage (optional)"
echo " -p: boost ccache hit rate by disabling precompiled headers (default: ON)" echo " -p: disable precompiled headers (on by default): more ccache hits after header changes, slower misses"
echo " -r: skip RAM and disk checks (low RAM compiling)" echo " -r: skip RAM and disk checks (low RAM compiling)"
echo " -s: build the Orca Slicer (optional)" echo " -s: build the Orca Slicer (optional)"
echo " -t: build tests (optional), requires -s flag" echo " -t: build tests (optional), requires -s flag"
@@ -549,6 +549,20 @@ fi
if [ -n "${CMAKE_CCACHE}" ] ; then if [ -n "${CMAKE_CCACHE}" ] ; then
echo "${CMAKE_CCACHE} found, enabling compiler caching..." echo "${CMAKE_CCACHE} found, enabling compiler caching..."
export CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}") export CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
# The same ccache settings as the CI build. Without this sloppiness ccache refuses every
# compile that uses the precompiled header, which is most of OrcaSlicer, and skips headers
# a checkout has just written. It is added to the sloppiness already configured, and
# CCACHE_SLOPPINESS or CCACHE_(NO)DEPEND set in the environment win.
if [[ "$(basename "${CMAKE_CCACHE}")" == ccache ]] ; then
if [[ -z "${CCACHE_SLOPPINESS+x}" ]] ; then
CCACHE_SLOPPINESS=$("${CMAKE_CCACHE}" --get-config sloppiness 2>/dev/null || true)
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:+${CCACHE_SLOPPINESS},}pch_defines,time_macros,include_file_mtime,include_file_ctime"
fi
# Hash the includes the compiler reports instead of preprocessing every miss.
if [[ -z "${CCACHE_DEPEND+x}${CCACHE_NODEPEND+x}" ]] ; then
export CCACHE_DEPEND=1
fi
fi
else else
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds." echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
fi fi
+50
View File
@@ -111,6 +111,54 @@ else
export CMAKE_POLICY_COMPAT="" export CMAKE_POLICY_COMPAT=""
fi fi
# Compiler cache, as in build_linux.sh. The program comes from CMAKE_CCACHE, else from a
# launcher already set in the environment (the CI build sets ccache there), else sccache or
# ccache from PATH; one program serves both languages. Each run applies its own choice to the
# build directories, replacing a launcher an earlier run or a hand-run configure recorded.
CMAKE_CCACHE_ARGS=()
CCACHE_VAR=""
for var in CMAKE_CCACHE CMAKE_CXX_COMPILER_LAUNCHER CMAKE_C_COMPILER_LAUNCHER; do
if [ -n "${!var:-}" ]; then
CCACHE_VAR=$var
break
fi
done
if [ -n "$CCACHE_VAR" ]; then
CCACHE_PROGRAM=${!CCACHE_VAR}
echo "Checking ${CCACHE_VAR} environment variable for compiler cache program..."
CMAKE_CCACHE=$(command -v "${CCACHE_PROGRAM}") || {
echo "${CCACHE_VAR} environment variable is set to '${CCACHE_PROGRAM}' but it was not found in PATH."
CMAKE_CCACHE=""
}
elif command -v sccache >/dev/null 2>&1 ; then
CMAKE_CCACHE=$(command -v sccache)
elif command -v ccache >/dev/null 2>&1 ; then
CMAKE_CCACHE=$(command -v ccache)
fi
if [ -n "${CMAKE_CCACHE}" ] ; then
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
# The same ccache settings as the CI build. Without this sloppiness ccache refuses every
# compile that uses the precompiled header, which is most of OrcaSlicer, and skips headers
# a checkout has just written. It is added to the sloppiness already configured, and
# CCACHE_SLOPPINESS or CCACHE_(NO)DEPEND set in the environment win.
if [[ "$(basename "${CMAKE_CCACHE}")" == ccache ]] ; then
if [[ -z "${CCACHE_SLOPPINESS+x}" ]] ; then
CCACHE_SLOPPINESS=$("${CMAKE_CCACHE}" --get-config sloppiness 2>/dev/null || true)
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:+${CCACHE_SLOPPINESS},}pch_defines,time_macros,include_file_mtime,include_file_ctime"
fi
# Hash the includes the compiler reports instead of preprocessing every miss.
if [[ -z "${CCACHE_DEPEND+x}${CCACHE_NODEPEND+x}" ]] ; then
export CCACHE_DEPEND=1
fi
fi
else
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
# Clear a launcher recorded by an earlier configure. The entries must be set empty rather
# than removed: CMake fills a missing launcher entry from the environment.
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER:STRING= -DCMAKE_CXX_COMPILER_LAUNCHER:STRING=)
fi
echo "Build params:" echo "Build params:"
echo " - ARCH: $ARCH" echo " - ARCH: $ARCH"
echo " - BUILD_CONFIG: $BUILD_CONFIG" echo " - BUILD_CONFIG: $BUILD_CONFIG"
@@ -162,6 +210,7 @@ function build_deps() {
-DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \ -DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \ -DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \ -DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT} ${CMAKE_POLICY_COMPAT}
fi fi
cmake --build . --config "$BUILD_CONFIG" --target deps cmake --build . --config "$BUILD_CONFIG" --target deps
@@ -264,6 +313,7 @@ function build_slicer() {
-DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \ -DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \ -DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \ -DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT} ${CMAKE_POLICY_COMPAT}
fi fi
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET" cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
+2 -17
View File
@@ -1,22 +1,7 @@
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
if (JPEG_VERSION STREQUAL "6")
message("Using Jpeg Lib 62")
set(jpeg_flag "")
elseif (JPEG_VERSION STREQUAL "7")
message("Using Jpeg Lib 70")
set(jpeg_flag "-DWITH_JPEG7=ON")
else ()
message("Using Jpeg Lib 80")
set(jpeg_flag "-DWITH_JPEG8=ON")
endif ()
endif()
orcaslicer_add_cmake_project(JPEG orcaslicer_add_cmake_project(JPEG
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
URL_HASH SHA256=d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f URL_HASH SHA256=1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
DEPENDS ${ZLIB_PKG}
CMAKE_ARGS CMAKE_ARGS
-DENABLE_SHARED=OFF -DENABLE_SHARED=OFF
-DENABLE_STATIC=ON -DENABLE_STATIC=ON
${jpeg_flag}
) )
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><rect x="7" y="12" width="1" height="1" style="fill:#fefefe;"/><path d="M4.5,5.5c0-1.71,1.43-3.09,3.16-3,1.62.08,2.84,1.54,2.84,3.17h0c0,.61-.25,1.2-.69,1.62l-2.31,2.24v.97" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/></svg> <?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><rect x="1.5" y="3.5" width="12" height="8" rx="1" ry="1" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><path d="M3,5h1v1h-1Z M5,5h1v1h-1Z M7,5h1v1h-1Z M9,5h1v1h-1Z M11,5h1v1h-1Z M4,7h1v1h-1Z M6,7h1v1h-1Z M8,7h1v1h-1Z M10,7h1v1h-1Z M5,9h5v1h-5Z" style="fill:#fefefe;"/></svg>

Before

Width:  |  Height:  |  Size: 366 B

After

Width:  |  Height:  |  Size: 415 B

@@ -18,7 +18,7 @@
<script src="../js/plugin-config-ui.js"></script> <script src="../js/plugin-config-ui.js"></script>
<script src="./index.js"></script> <script src="./index.js"></script>
<script src="./plugin-sort.js"></script> <script src="./plugin-sort.js"></script>
<script src="../js/fuzzy-search.js"></script> <script src="../../js/fuzzy-search.js"></script>
<script src="./plugin-search.js"></script> <script src="./plugin-search.js"></script>
</head> </head>
<body onLoad="OnInit()"> <body onLoad="OnInit()">
+6 -2
View File
@@ -156,12 +156,16 @@ function SetCloudMode( bOn )
m_CloudMode=bOn; m_CloudMode=bOn;
$("body").toggleClass("cloud-mode", bOn); $("body").toggleClass("cloud-mode", bOn);
if( bOn )
{
$(".BtnItem").removeClass("BtnItemSelected");
$(".BtnShortcut").addClass("BtnItemSelected");
}
} }
function GotoCloud() function GotoCloud()
{ {
$(".BtnItem").removeClass("BtnItemSelected");
$(".BtnShortcut").addClass("BtnItemSelected");
SetCloudMode(true); SetCloudMode(true);
SendSimpleCommand("homepage_open_cloud"); SendSimpleCommand("homepage_open_cloud");
} }
@@ -336,10 +336,10 @@ modules:
sha256: 3d22d46c566b1761a0e15ea397589b3a5f36ac09b7c785382e6470156c04247f sha256: 3d22d46c566b1761a0e15ea397589b3a5f36ac09b7c785382e6470156c04247f
dest: external-packages/PNG dest: external-packages/PNG
# libjpeg-turbo 3.0.1 # libjpeg-turbo 3.2.0
- type: file - type: file
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
sha256: d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f sha256: 1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
dest: external-packages/JPEG dest: external-packages/JPEG
# Freetype 2.12.1 # Freetype 2.12.1
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# Work-around https://gitlab.gnome.org/GNOME/gnome-build-meta/-/issues/754 # WebKit's NVIDIA DMABUF workaround (gnome-build-meta#754) is applied by the
grep -q org.freedesktop.Platform.GL.nvidia /.flatpak-info && export WEBKIT_DISABLE_DMABUF_RENDERER=1 # binary itself, gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
# Work-around https://github.com/bambulab/BambuStudio/issues/3440 # Work-around https://github.com/bambulab/BambuStudio/issues/3440
# Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues # Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues
+20 -14
View File
@@ -163,6 +163,7 @@ using json = nlohmann::json;
#include <X11/Xlib.h> #include <X11/Xlib.h>
#endif #endif
#include <unistd.h> #include <unistd.h>
#include "slic3r/GUI/Widgets/WebView.hpp"
#endif #endif
#ifdef SLIC3R_GUI #ifdef SLIC3R_GUI
@@ -1156,8 +1157,10 @@ int CLI::run(int argc, char **argv)
// opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it // opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it
// worked around appears fixed in WebKit2GTK >= 2.42; leaving it // worked around appears fixed in WebKit2GTK >= 2.42; leaving it
// unset preserves WebKit hardware acceleration on Device / Setup // unset preserves WebKit hardware acceleration on Device / Setup
// Wizard / login / store. The default path still applies it on // Wizard / login / store. The default path applies it only where it
// XWayland sessions as a conservative fallback for older WebKit. // forces GDK_BACKEND=x11 below (a build without EGL, so the app really
// runs through XWayland), and only when the linked WebKitGTK predates
// the fix (< 2.42), as a conservative fallback for older WebKit.
// ------------------------------------------------------------------ // ------------------------------------------------------------------
{ {
const char* gdk_backend = ::getenv("GDK_BACKEND"); const char* gdk_backend = ::getenv("GDK_BACKEND");
@@ -1165,6 +1168,14 @@ int CLI::run(int argc, char **argv)
// honours the first backend in the comma-separated list). // honours the first backend in the comma-separated list).
const bool x11_opt_in = gdk_backend && boost::starts_with(gdk_backend, "x11"); const bool x11_opt_in = gdk_backend && boost::starts_with(gdk_backend, "x11");
// NVIDIA's proprietary driver cannot back WebKitGTK's DMABUF renderer.
// Force the shared-memory transport, which keeps compositing and threaded
// scrolling; the second variable clears Debian/Ubuntu/openSUSE's isNVIDIA().
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /* replace */ false);
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /* replace */ false);
}
if (x11_opt_in) { if (x11_opt_in) {
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) ===== // ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds."; BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
@@ -1204,22 +1215,17 @@ int CLI::run(int argc, char **argv)
if (wayland_env && *wayland_env) { if (wayland_env && *wayland_env) {
BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend."; BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend.";
::setenv("GDK_BACKEND", "x11", true); ::setenv("GDK_BACKEND", "x11", true);
}
}
#endif
// WebKit2GTK compositing can fail under XWayland on older // GTK will use X11 in a Wayland session here, which is the
// WebKit releases. Disable it only when both DISPLAY and // one default-path case that really runs through XWayland.
// WAYLAND_DISPLAY are set (i.e. an XWayland session is in // WebKit2GTK accelerated compositing could fail silently
// use). On pure X11 or native Wayland, compositing is left // there on releases before 2.42, so force it off. Disabling
// enabled so WebKit retains HW acceleration. // it also turns off threaded scrolling, hence the gate.
{ if (WebView::NeedsCompositingWorkaround())
const char* display_env_wk = ::getenv("DISPLAY");
const char* wayland_env_wk = ::getenv("WAYLAND_DISPLAY");
if (display_env_wk && *display_env_wk && wayland_env_wk && *wayland_env_wk) {
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false); ::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
} }
} }
#endif
// XInitThreads is needed before GStreamer may use Xlib. On // XInitThreads is needed before GStreamer may use Xlib. On
// native Wayland without DISPLAY, GStreamer uses waylandsink // native Wayland without DISPLAY, GStreamer uses waylandsink
@@ -429,7 +429,8 @@ if [ "\$XDG_SESSION_TYPE" = "wayland" ] && [ "\$ZINK_DISABLE_OVERRIDE" != "1" ];
export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json
export MESA_LOADER_DRIVER_OVERRIDE=zink export MESA_LOADER_DRIVER_OVERRIDE=zink
export GALLIUM_DRIVER=zink export GALLIUM_DRIVER=zink
export WEBKIT_DISABLE_DMABUF_RENDERER=1 # WebKit's NVIDIA DMABUF workaround is applied by the binary itself,
# gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
fi fi
fi fi
fi fi
+3 -7
View File
@@ -812,7 +812,8 @@ bool WallToolPaths::removeEmptyToolPaths(std::vector<VariableWidthLines> &toolpa
* Get the order constraints of the insets when printing walls per region / hole. * Get the order constraints of the insets when printing walls per region / hole.
* Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right. * Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right.
* *
* Odd walls should always go after their enclosing wall polygons. * Odd walls follow the same order as even ones, so that the centre line of a narrow wall is an inner wall
* like any other when printing inner to outer.
* *
* \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one. * \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one.
*/ */
@@ -886,12 +887,7 @@ WallToolPaths::ExtrusionLineSet WallToolPaths::getRegionOrder(const std::vector<
continue; // not directly adjacent continue; // not directly adjacent
if (!shorter_then(loc_here - lineloc_nearby.j.p, (lineloc_here.j.w + lineloc_nearby.j.w) / 2 * diagonal_extension)) if (!shorter_then(loc_here - lineloc_nearby.j.p, (lineloc_here.j.w + lineloc_nearby.j.w) / 2 * diagonal_extension))
continue; // points are too far away from each other continue; // points are too far away from each other
if (here->is_odd || nearby->is_odd) { if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
if (here->is_odd && !nearby->is_odd && nearby->inset_idx < here->inset_idx)
order_requirements.emplace(std::make_pair(nearby, here));
if (nearby->is_odd && !here->is_odd && here->inset_idx < nearby->inset_idx)
order_requirements.emplace(std::make_pair(here, nearby));
} else if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
order_requirements.emplace(std::make_pair(nearby, here)); order_requirements.emplace(std::make_pair(nearby, here));
} else { } else {
assert((nearby->inset_idx > here->inset_idx) == outer_to_inner); assert((nearby->inset_idx > here->inset_idx) == outer_to_inner);
+34 -15
View File
@@ -432,27 +432,36 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
std::string OozePrevention::pre_toolchange(GCode& gcodegen) std::string OozePrevention::pre_toolchange(GCode& gcodegen)
{ {
std::string gcode;
unsigned int extruder_id = gcodegen.writer().filament()->id(); unsigned int extruder_id = gcodegen.writer().filament()->id();
// Check if this tool will ever be used again in this print.
bool is_last_use = false;
if (gcodegen.m_print != nullptr && !gcodegen.m_print->tool_ordering().empty()) {
coordf_t current_z = gcodegen.layer() ? gcodegen.layer()->print_z : gcodegen.writer().get_position().z();
const auto& tool_ordering = gcodegen.m_print->tool_ordering();
const auto& cfg = gcodegen.config();
const int heater = imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(extruder_id));
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, extruder_id);
for (unsigned int f : tool_ordering.all_extruders())
if (is_last_use && imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(f)) == heater)
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, f);
}
int temp = 0;
if (!is_last_use) {
const auto& filament_idle_temp = gcodegen.config().idle_temperature; const auto& filament_idle_temp = gcodegen.config().idle_temperature;
if (filament_idle_temp.get_at(extruder_id) == 0) { if (filament_idle_temp.get_at(extruder_id) == 0) {
// There is no idle temperature defined in filament settings. if (gcodegen.config().standby_temperature_delta.value == 0)
// Use the delta value from print config. return std::string();
if (gcodegen.config().standby_temperature_delta.value != 0) { temp = this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value;
// we assume that heating is always slower than cooling, so no need to block
gcode += gcodegen.writer().set_temperature
(this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, extruder_id);
gcode.pop_back();
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
}
} else { } else {
// Use the value from filament settings. That one is absolute, not delta. temp = filament_idle_temp.get_at(extruder_id);
gcode += gcodegen.writer().set_temperature(filament_idle_temp.get_at(extruder_id), false, extruder_id); }
gcode.pop_back();
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
} }
std::string gcode = gcodegen.writer().set_temperature(temp, false, extruder_id);
gcode.pop_back();
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
return gcode; return gcode;
} }
@@ -3976,6 +3985,16 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder))); this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder))); this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder))); this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
// The final print statistics are known only after the whole G-code is generated, so they are filled in during post-processing too.
// The running extruded_* totals are left alone; profiles use them as numbers.
this->placeholder_parser().set("used_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
this->placeholder_parser().set("print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
this->placeholder_parser().set("normal_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
this->placeholder_parser().set("silent_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Silent_Print_Time_Placeholder)));
this->placeholder_parser().set("total_weight", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Weight_Placeholder)));
this->placeholder_parser().set("total_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Cost_Placeholder)));
this->placeholder_parser().set("total_wipe_tower_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Cost_Placeholder)));
this->placeholder_parser().set("total_wipe_tower_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Filament_Placeholder)));
// Sync variant-mapped params into placeholder_parser before processing start gcode // Sync variant-mapped params into placeholder_parser before processing start gcode
update_placeholder_parser_with_variant_params(); update_placeholder_parser_with_variant_params();
+1
View File
@@ -1004,6 +1004,7 @@ protected:
size_t num_objects, size_t num_objects,
size_t num_islands); size_t num_islands);
friend class OozePrevention;
friend class Wipe; friend class Wipe;
friend class WipeTowerIntegration; friend class WipeTowerIntegration;
friend class PressureEqualizer; friend class PressureEqualizer;
+45 -69
View File
@@ -120,7 +120,13 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags = {
"@PRINT_TIME_HOUR@", "@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@", "@PRINT_TIME_MINUTE@",
"@PRINT_TIME_SEC@", "@PRINT_TIME_SEC@",
"@USED_FILAMENT_LENGTH@" "@USED_FILAMENT_LENGTH@",
"@NORMAL_PRINT_TIME@",
"@SILENT_PRINT_TIME@",
"@TOTAL_WEIGHT@",
"@TOTAL_COST@",
"@TOTAL_WIPE_TOWER_COST@",
"@TOTAL_WIPE_TOWER_FILAMENT@"
}; };
const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = { const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
@@ -147,7 +153,13 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
"@PRINT_TIME_HOUR@", "@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@", "@PRINT_TIME_MINUTE@",
"@PRINT_TIME_SEC@", "@PRINT_TIME_SEC@",
"@USED_FILAMENT_LENGTH@" "@USED_FILAMENT_LENGTH@",
"@NORMAL_PRINT_TIME@",
"@SILENT_PRINT_TIME@",
"@TOTAL_WEIGHT@",
"@TOTAL_COST@",
"@TOTAL_WIPE_TOWER_COST@",
"@TOTAL_WIPE_TOWER_FILAMENT@"
}; };
@@ -1264,19 +1276,22 @@ void GCodeProcessor::run_post_process()
return ret; return ret;
}; };
// Process inline placeholders (print_time_total_sec, print_time_day, print_time_hour, print_time_minute, print_time_sec and used_filament_length) // Process inline placeholders (print_time_*, used_filament_length and the print statistics known only after export)
auto process_inline_placeholders = [&](std::string& gcode_line) { auto process_inline_placeholders = [&](std::string& gcode_line) {
bool processed = false; bool processed = false;
// Every inline placeholder contains '@', so a line without one has nothing to replace. // Every inline placeholder contains '@', so a line without one has nothing to replace.
if (gcode_line.find('@') == std::string::npos) if (gcode_line.find('@') == std::string::npos)
return processed; return processed;
const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder); auto replace_tag = [&gcode_line, &processed](ETags tag, const std::string& value) {
const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder); const std::string& placeholder = reserved_tag(tag);
const std::string& print_time_hour_placeholder = reserved_tag(ETags::Print_Time_Hour_Placeholder); size_t pos = gcode_line.find(placeholder);
const std::string& print_time_minute_placeholder = reserved_tag(ETags::Print_Time_Minute_Placeholder); while (pos != std::string::npos) {
const std::string& print_time_sec_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder); gcode_line.replace(pos, placeholder.length(), value);
const std::string& used_filament_placeholder = reserved_tag(ETags::Used_Filament_Length_Placeholder); processed = true;
pos = gcode_line.find(placeholder, pos + value.length());
}
};
double print_time_total_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time; double print_time_total_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time;
if (print_time_total_sec < 0.0) if (print_time_total_sec < 0.0)
@@ -1289,70 +1304,31 @@ void GCodeProcessor::run_post_process()
int print_time_minute = (day_remainder_seconds % 3600) / 60; int print_time_minute = (day_remainder_seconds % 3600) / 60;
int print_time_sec = day_remainder_seconds % 60; int print_time_sec = day_remainder_seconds % 60;
// Replace print_time_total_sec
size_t pos = gcode_line.find(print_time_total_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%.2f", print_time_total_sec);
gcode_line.replace(pos, print_time_total_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_total_placeholder, pos + strlen(buf));
}
// Replace print_time_day
pos = gcode_line.find(print_time_day_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_day);
gcode_line.replace(pos, print_time_day_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_day_placeholder, pos + strlen(buf));
}
// Replace print_time_hour
pos = gcode_line.find(print_time_hour_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_hour);
gcode_line.replace(pos, print_time_hour_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_hour_placeholder, pos + strlen(buf));
}
// Replace print_time_minute
pos = gcode_line.find(print_time_minute_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_minute);
gcode_line.replace(pos, print_time_minute_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_minute_placeholder, pos + strlen(buf));
}
// Replace print_time_sec
pos = gcode_line.find(print_time_sec_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_sec);
gcode_line.replace(pos, print_time_sec_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_sec_placeholder, pos + strlen(buf));
}
// Replace used_filament_length
pos = gcode_line.find(used_filament_placeholder);
while (pos != std::string::npos) {
double total_filament_mm = 0.0; double total_filament_mm = 0.0;
for (const auto& mm : filament_mm) { for (const auto& mm : filament_mm) {
total_filament_mm += mm; total_filament_mm += mm;
} }
double used_filament_length = total_filament_mm / 1000.0; // Convert mm to m
char buf[64]; replace_tag(ETags::Print_Time_Total_Sec_Placeholder, Slic3r::float_to_string_decimal_point(print_time_total_sec, 2));
sprintf(buf, "%.2f", used_filament_length); replace_tag(ETags::Print_Time_Day_Placeholder, std::to_string(print_time_day));
gcode_line.replace(pos, used_filament_placeholder.length(), buf); replace_tag(ETags::Print_Time_Hour_Placeholder, std::to_string(print_time_hour));
processed = true; replace_tag(ETags::Print_Time_Minute_Placeholder, std::to_string(print_time_minute));
pos = gcode_line.find(used_filament_placeholder, pos + strlen(buf)); replace_tag(ETags::Print_Time_Sec_Placeholder, std::to_string(print_time_sec));
} replace_tag(ETags::Used_Filament_Length_Placeholder,
Slic3r::float_to_string_decimal_point(total_filament_mm / 1000.0, 2)); // Convert mm to m
// Same values as the print statistics that filename_format gets (PrintStatistics::config(), filled in by
// update_print_estimated_stats() in GCode.cpp), which runs only after this post-processing.
replace_tag(ETags::Normal_Print_Time_Placeholder, get_time_dhm(PrintEstimatedStatistics::ETimeMode::Normal));
replace_tag(ETags::Silent_Print_Time_Placeholder, is_stealth_time_estimator_enabled() ?
get_time_dhm(PrintEstimatedStatistics::ETimeMode::Stealth) :
short_time("N/A"));
const double total_cost = filament_total_cost + m_print->config().time_cost.value * print_time_total_sec / 3600.0;
replace_tag(ETags::Total_Weight_Placeholder, Slic3r::float_to_string_decimal_point(filament_total_g, 2));
replace_tag(ETags::Total_Cost_Placeholder, Slic3r::float_to_string_decimal_point(total_cost, 2));
replace_tag(ETags::Total_Wipe_Tower_Cost_Placeholder,
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_cost, 2));
replace_tag(ETags::Total_Wipe_Tower_Filament_Placeholder,
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_filament, 2));
return processed; return processed;
}; };
+6
View File
@@ -601,6 +601,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
Print_Time_Minute_Placeholder, Print_Time_Minute_Placeholder,
Print_Time_Sec_Placeholder, Print_Time_Sec_Placeholder,
Used_Filament_Length_Placeholder, Used_Filament_Length_Placeholder,
Normal_Print_Time_Placeholder,
Silent_Print_Time_Placeholder,
Total_Weight_Placeholder,
Total_Cost_Placeholder,
Total_Wipe_Tower_Cost_Placeholder,
Total_Wipe_Tower_Filament_Placeholder,
}; };
static const std::string& reserved_tag(ETags tag) { return s_IsBBLPrinter ? Reserved_Tags[static_cast<unsigned char>(tag)] : Reserved_Tags_compatible[static_cast<unsigned char>(tag)]; } static const std::string& reserved_tag(ETags tag) { return s_IsBBLPrinter ? Reserved_Tags[static_cast<unsigned char>(tag)] : Reserved_Tags_compatible[static_cast<unsigned char>(tag)]; }
+14 -1
View File
@@ -1276,6 +1276,13 @@ void ToolOrdering::collect_extruder_statistics(bool prime_multi_material)
sort_remove_duplicates(m_all_printing_extruders); sort_remove_duplicates(m_all_printing_extruders);
} }
m_last_layer_per_extruder.clear();
for (const LayerTools& lt : m_layer_tools) {
for (unsigned int ext : lt.extruders) {
m_last_layer_per_extruder[ext] = lt.print_z;
}
}
if (prime_multi_material && ! m_all_printing_extruders.empty()) { if (prime_multi_material && ! m_all_printing_extruders.empty()) {
// Reorder m_all_printing_extruders in the sequence they will be primed, the last one will be m_first_printing_extruder. // Reorder m_all_printing_extruders in the sequence they will be primed, the last one will be m_first_printing_extruder.
// Then set m_first_printing_extruder to the 1st extruder primed. // Then set m_first_printing_extruder to the 1st extruder primed.
@@ -3664,5 +3671,11 @@ int WipingExtrusions::get_support_interface_extruder_overrides(const PrintObject
return -1; return -1;
} }
bool ToolOrdering::is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const
{
auto it = m_last_layer_per_extruder.find(extruder_id);
if (it == m_last_layer_per_extruder.end())
return true;
return print_z >= it->second - EPSILON;
}
} // namespace Slic3r } // namespace Slic3r
+3
View File
@@ -370,6 +370,7 @@ public:
std::vector<int> & initial_filaments); std::vector<int> & initial_filaments);
bool has_non_support_filament(const PrintConfig &config); bool has_non_support_filament(const PrintConfig &config);
bool is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const;
private: private:
void initialize_layers(std::vector<coordf_t> &zs); void initialize_layers(std::vector<coordf_t> &zs);
@@ -413,6 +414,8 @@ private:
// which keeps every legacy per-object code path bit-identical (loops over an empty map are // which keeps every legacy per-object code path bit-identical (loops over an empty map are
// no-ops; downstream emission falls through to the per-object branch). // no-ops; downstream emission falls through to the per-object branch).
std::map<unsigned int, std::map<LayerTools::MixedSubLayerGroup::VolumeKey, std::vector<size_t>>> m_gradient_volume_layers; std::map<unsigned int, std::map<LayerTools::MixedSubLayerGroup::VolumeKey, std::vector<size_t>>> m_gradient_volume_layers;
// Map: extruder_id -> print_z (z coordinate) of the last layer it is used on
std::map<unsigned int, coordf_t> m_last_layer_per_extruder;
const PrintObject* m_print_object_ptr = nullptr; const PrintObject* m_print_object_ptr = nullptr;
Print* m_print; Print* m_print;
bool m_sorted = false; bool m_sorted = false;
+29
View File
@@ -211,6 +211,35 @@ bool decode_colored_png(const ReadBuf &in_buf, ImageColorscale &out_img)
return decode_colored_png(stream, out_img); return decode_colored_png(stream, out_img);
} }
bool is_decodable_8bit_png(const ReadBuf &pngbuf)
{
static const constexpr int PNG_SIG_BYTES = 8;
struct ReadBufStream stream{pngbuf};
std::vector<png_byte> sig(PNG_SIG_BYTES, 0);
stream.read(sig.data(), PNG_SIG_BYTES);
if (!png_check_sig(sig.data(), PNG_SIG_BYTES))
return false;
PNGDescr dsc;
dsc.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!dsc.png)
return false;
dsc.info = png_create_info_struct(dsc.png);
if (!dsc.info)
return false;
if (!png_read_header_guarded(dsc.png, dsc.info, &stream, PNG_SIG_BYTES))
return false;
const size_t color_type = png_get_color_type(dsc.png, dsc.info);
const size_t bit_depth = png_get_bit_depth(dsc.png, dsc.info);
const bool grey = color_type == PNG_COLOR_TYPE_GRAY;
const bool colour = color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA;
return png_get_image_width(dsc.png, dsc.info) > 0 && png_get_image_height(dsc.png, dsc.info) > 0 &&
bit_depth == 8 && (grey || colour);
}
// Down to earth function to store a packed RGB image to file. Mostly useful for debugging purposes. // Down to earth function to store a packed RGB image to file. Mostly useful for debugging purposes.
// Based on https://www.lemoda.net/c/write-png/ // Based on https://www.lemoda.net/c/write-png/
+3
View File
@@ -50,6 +50,9 @@ struct ReadBuf { const void *buf = nullptr; const size_t sz = 0; };
bool is_png(const ReadBuf &pngbuf); bool is_png(const ReadBuf &pngbuf);
// True if decode_png() or decode_colored_png() would accept these bytes, judged from the header alone.
bool is_decodable_8bit_png(const ReadBuf &pngbuf);
struct ReadBufStream: public IStream { struct ReadBufStream: public IStream {
const ReadBuf &rbuf_ref; const ReadBuf &rbuf_ref;
size_t pos = 0; size_t pos = 0;
+65 -24
View File
@@ -62,7 +62,12 @@ public:
// Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole. // Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole.
unsigned short depth; unsigned short depth;
// ORCA: an external hole perimeter touching the next outer wall all around, with nothing between them. // ORCA: an external hole perimeter touching the next outer wall all around, with nothing between them.
// E.g. a 1mm tube wall with 0.5mm walls: 2 lines, the contour's outer wall and the hole's.
bool is_thin_wall_hole = false; bool is_thin_wall_hole = false;
// ORCA: an external hole perimeter with no room for an inner wall beside it, at most gap fill. Every thin wall
// hole is one, and so is e.g. a 1.5mm tube wall with 0.5mm walls: 3 lines, the two outer walls and gap fill
// between them. A 2mm tube wall has room for an inner wall a side and is not. Only set for Inner/Outer/Inner.
bool is_narrow_wall_hole = false;
// Children contour, may be both CCW and CW oriented (outer contours or holes). // Children contour, may be both CCW and CW oriented (outer contours or holes).
std::vector<PerimeterGeneratorLoop> children; std::vector<PerimeterGeneratorLoop> children;
@@ -572,7 +577,8 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
extrusion_coll.append(std::move(extrusion_loop)); extrusion_coll.append(std::move(extrusion_loop));
// Orca: Reverse the order of paths for thin wall holes. We define thin wall hole as a hole with only one perimeter. // Orca: Reverse the order of paths for thin wall holes. We define thin wall hole as a hole with only one perimeter.
const bool thin_wall_hole = !pg_extrusion.is_contour && pg_extrusions.size() == 2; const bool thin_wall_hole = !pg_extrusion.is_contour && pg_extrusions.size() == 2;
if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner) if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner &&
!perimeter_generator.outer_walls_first_for_brim())
std::reverse(extrusion_coll.entities.begin(), extrusion_coll.entities.end()); std::reverse(extrusion_coll.entities.begin(), extrusion_coll.entities.end());
} }
else { else {
@@ -1690,13 +1696,24 @@ void PerimeterGenerator::process_classic()
last = std::move(offsets); last = std::move(offsets);
// ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall. // ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall, a
// narrow wall hole none for an inner wall, the room the next loop is offset by below.
if (i == 0 && ! holes[0].empty()) { if (i == 0 && ! holes[0].empty()) {
const float room = float(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width)); auto reach = [&last](double room) {
const Polygons reach = to_polygons(offset2_ex(last, -room, room + float(SCALED_EPSILON))); return to_polygons(offset2_ex(last, -float(room), float(room) + float(SCALED_EPSILON)));
for (PerimeterGeneratorLoop &hole : holes[0]) };
hole.is_thin_wall_hole = intersection_pl(Polylines{ hole.polygon.split_at_first_point() }, auto out_of_reach = [](const Polygon &hole, const Polygons &reach) {
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole.polygon).inflated(SCALED_EPSILON))).empty(); return intersection_pl(Polylines{ hole.split_at_first_point() },
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole).inflated(SCALED_EPSILON))).empty();
};
// Only Inner/Outer/Inner orders narrow wall holes apart.
const bool narrow_holes_ordered = this->config->wall_sequence == WallSequence::InnerOuterInner;
const Polygons gap_fill_reach = reach(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
const Polygons inner_wall_reach = narrow_holes_ordered ? reach(ext_perimeter_spacing2 + min_spacing / 2.) : Polygons();
for (PerimeterGeneratorLoop &hole : holes[0]) {
hole.is_thin_wall_hole = out_of_reach(hole.polygon, gap_fill_reach);
hole.is_narrow_wall_hole = narrow_holes_ordered && out_of_reach(hole.polygon, inner_wall_reach);
}
} }
//BBS: refer to superslicer //BBS: refer to superslicer
@@ -1835,18 +1852,16 @@ void PerimeterGenerator::process_classic()
// if brim will be printed, reverse the order of perimeters so that // if brim will be printed, reverse the order of perimeters so that
// we continue inwards after having finished the brim // we continue inwards after having finished the brim
// TODO: add test for perimeter order
bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner; bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner;
if (is_outer_wall_first || // Orca: a contour with only a narrow wall hole beside it has nothing to sandwich, and prints the hole first,
//BBS: always print outer wall first when there indeed has brim. // then the contour, then the gap fill between them, as Arachne does.
// btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact). const PerimeterGeneratorLoops &island_loops = contours.front();
(this->layer_id == 0 && const bool narrow_wall_hole_island = island_loops.size() == 1 && island_loops.front().children.size() == 1 &&
(this->object_config->brim_type == BrimType::btOuterOnly || island_loops.front().children.front().is_narrow_wall_hole;
this->object_config->brim_type == BrimType::btLeadingEdgeOnly) && if (is_outer_wall_first || this->outer_walls_first_for_brim())
this->object_config->brim_width.value > 0))
entities.reverse(); entities.reverse();
// Orca: sandwich mode. Apply after 1st layer. // Orca: sandwich mode. Apply after 1st layer.
else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0){ else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0 && !narrow_wall_hole_island){
entities.reverse(); // reverse all entities - order them from external to internal entities.reverse(); // reverse all entities - order them from external to internal
if(entities.entities.size()>2){ // 3 walls minimum needed to do inner outer inner ordering if(entities.entities.size()>2){ // 3 walls minimum needed to do inner outer inner ordering
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island. int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
@@ -2412,11 +2427,25 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
// Function to reorder perimeters starting from a given reference index // Function to reorder perimeters starting from a given reference index
auto reorderFromReference = [&](int referenceIndex) { auto reorderFromReference = [&](int referenceIndex) {
std::unordered_set<int> firstLevelIndices; // Find first level touching perimeters. The odd centre line of a wall too narrow for two inner walls is shared
firstLevelIndices.insert(referenceIndex); // by the outer walls on both sides, so those join this island and are printed before it too, holes first.
std::vector<int> outerIndices{referenceIndex};
// Find first level touching perimeters std::vector<int> firstLevelTouchingIndices;
std::vector<int> firstLevelTouchingIndices = findAllTouchingPerimeters(entities, firstLevelIndices, threshold_external, threshold_internal, 1); for (size_t next = 0; next < outerIndices.size(); ++next)
for (int idx : findAllTouchingPerimeters(entities, {outerIndices[next]}, threshold_external, threshold_internal, 1)) {
if (std::find(firstLevelTouchingIndices.begin(), firstLevelTouchingIndices.end(), idx) != firstLevelTouchingIndices.end())
continue;
firstLevelTouchingIndices.push_back(idx);
if (!entities[idx].extrusion->is_odd)
continue;
for (size_t i = 0; i < entities.size(); ++i)
if (entities[i].extrusion->inset_idx == 0 && includedIndices.count(int(i)) == 0 &&
std::find(outerIndices.begin(), outerIndices.end(), int(i)) == outerIndices.end() &&
arachne_lines_touch(*entities[i].extrusion, *entities[idx].extrusion, double(threshold_external)))
outerIndices.push_back(int(i));
}
std::stable_partition(outerIndices.begin(), outerIndices.end(),
[&entities](int idx) { return entities[idx].extrusion->is_closed && !entities[idx].is_contour; });
// Bring the largest first level perimeter to the front // Bring the largest first level perimeter to the front
// The longest first neighbour is most likely the dominant proximate perimeter // The longest first neighbour is most likely the dominant proximate perimeter
// hence printing it immediately after the external perimeter should speed things up // hence printing it immediately after the external perimeter should speed things up
@@ -2427,8 +2456,10 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
std::iter_swap(maxIt, firstLevelTouchingIndices.end() - 1); std::iter_swap(maxIt, firstLevelTouchingIndices.end() - 1);
} }
// Insert first level perimeters into reordered list // Insert first level perimeters into reordered list
reordered.push_back(entities[referenceIndex]); for (int idx : outerIndices) {
includedIndices.insert(referenceIndex); reordered.push_back(entities[idx]);
includedIndices.insert(idx);
}
for (int idx : firstLevelTouchingIndices) { for (int idx : firstLevelTouchingIndices) {
if (includedIndices.count(idx) == 0) { if (includedIndices.count(idx) == 0) {
@@ -2757,7 +2788,7 @@ void PerimeterGenerator::process_arachne()
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
is_outer_wall_first = is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner; this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim();
} }
if (is_outer_wall_first) { if (is_outer_wall_first) {
start_perimeter = 0; start_perimeter = 0;
@@ -3042,6 +3073,16 @@ ExPolygons PerimeterGenerator::printable_slices(const ExPolygons &slices) const
return min_width > EPSILON ? opening_ex(slices, float(scale_(min_width / 2.))) : slices; return min_width > EPSILON ? opening_ex(slices, float(scale_(min_width / 2.))) : slices;
} }
bool PerimeterGenerator::outer_walls_first_for_brim() const
{
return layer_id == 0 && brim_puts_outer_walls_first(object_config->brim_type.value, object_config->brim_width.value);
}
bool brim_puts_outer_walls_first(BrimType brim_type, double brim_width)
{
return brim_width > 0 && (brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btLeadingEdgeOnly);
}
std::vector<Polygons> PerimeterGenerator::generate_lower_polygons_series(float width) std::vector<Polygons> PerimeterGenerator::generate_lower_polygons_series(float width)
{ {
float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1); float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1);
+6
View File
@@ -164,6 +164,8 @@ public:
Polygons lower_slices_polygons() const { return m_lower_slices_polygons; } Polygons lower_slices_polygons() const { return m_lower_slices_polygons; }
// ORCA: the slices less the slivers the wall generator prints nothing for, so they never count as support. // ORCA: the slices less the slivers the wall generator prints nothing for, so they never count as support.
ExPolygons printable_slices(const ExPolygons &slices) const; ExPolygons printable_slices(const ExPolygons &slices) const;
// ORCA: whether this layer prints its outer walls first for the brim, see brim_puts_outer_walls_first().
bool outer_walls_first_for_brim() const;
private: private:
std::vector<Polygons> generate_lower_polygons_series(float width); std::vector<Polygons> generate_lower_polygons_series(float width);
@@ -183,6 +185,10 @@ private:
Polygons m_lower_slices_polygons; Polygons m_lower_slices_polygons;
}; };
// ORCA: a brim against the outer walls makes the first layer print them first, whatever the wall sequence, so the
// brim runs straight on into the outer wall. Auto, ear and painted brims may leave the outer walls bare.
bool brim_puts_outer_walls_first(BrimType brim_type, double brim_width);
} }
#endif #endif
+1
View File
@@ -2293,6 +2293,7 @@ void PresetCollection::load_project_embedded_presets(std::vector<Preset*>& proje
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file; BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file;
continue; continue;
} }
extend_default_config_length(config, false, {});
preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
//preset->config.apply(std::move(config)); //preset->config.apply(std::move(config));
Preset::normalize(preset->config); Preset::normalize(preset->config);
+8 -9
View File
@@ -17,6 +17,7 @@
#include "I18N.hpp" #include "I18N.hpp"
#include "Layer.hpp" #include "Layer.hpp"
#include "MutablePolygon.hpp" #include "MutablePolygon.hpp"
#include "PerimeterGenerator.hpp"
#include "PrintBase.hpp" #include "PrintBase.hpp"
#include "PrintConfig.hpp" #include "PrintConfig.hpp"
#include "SLA/IndexedMesh.hpp" #include "SLA/IndexedMesh.hpp"
@@ -1464,15 +1465,13 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "top_surface_speed") { || opt_key == "top_surface_speed") {
// Brim is printed below supports, support invalidates brim and skirt. // Brim is printed below supports, support invalidates brim and skirt.
steps.emplace_back(posSupportMaterial); steps.emplace_back(posSupportMaterial);
if (opt_key == "brim_type") { if (opt_key == "brim_type" || opt_key == "brim_width") {
const auto* old_brim_type = old_config.option<ConfigOptionEnum<BrimType>>(opt_key); // Regenerate the walls when the brim starts or stops putting the first layer's outer walls first.
const auto* new_brim_type = new_config.option<ConfigOptionEnum<BrimType>>(opt_key); auto outer_walls_first = [](const ConfigOptionResolver &config) {
//BBS: When switch to manual brim, the object must have brim, then re-generate perimeter return brim_puts_outer_walls_first(config.option<ConfigOptionEnum<BrimType>>("brim_type")->value,
//to make the wall order of first layer to be outer-first config.option<ConfigOptionFloat>("brim_width")->value);
// btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it };
// takes part in the same first-layer wall order rule. if (outer_walls_first(old_config) != outer_walls_first(new_config))
if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly ||
old_brim_type->value == btLeadingEdgeOnly || new_brim_type->value == btLeadingEdgeOnly)
steps.emplace_back(posPerimeters); steps.emplace_back(posPerimeters);
} }
} else if ( } else if (
+3 -6
View File
@@ -707,10 +707,12 @@ void tree_supports_generate_paths(
false); false);
} }
if (eec) { if (eec) {
if (!eec->empty()) {
std::reverse(eec->entities.begin(), eec->entities.end()); std::reverse(eec->entities.begin(), eec->entities.end());
dst.emplace_back(eec.release()); dst.emplace_back(eec.release());
} }
} }
}
} }
void fill_expolygons_with_sheath_generate_paths( void fill_expolygons_with_sheath_generate_paths(
@@ -1825,12 +1827,7 @@ void generate_support_toolpaths(
no_sort = true; no_sort = true;
} else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic && } else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic &&
(config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) { (config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) {
// Orca: A special case for the hollow Organic supports tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params);
// Orca: If the tree supports are too tall, use a double wall to make it stronger
SupportParameters support_params2 = support_params;
if (support_layer.print_z > 100.0)
support_params2.tree_branch_diameter_double_wall_area_scaled = 0.1;
tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params2);
done = true; done = true;
} }
if (! done) if (! done)
+10 -22
View File
@@ -2149,7 +2149,9 @@ void TreeSupport::draw_circles()
const bool with_lightning_infill = m_support_params.base_fill_pattern == ipLightning; const bool with_lightning_infill = m_support_params.base_fill_pattern == ipLightning;
coordf_t support_extrusion_width = m_support_params.support_extrusion_width; coordf_t support_extrusion_width = m_support_params.support_extrusion_width;
const float tree_brim_width = config.tree_support_brim_width.value; const float tree_brim_width = config.tree_support_brim_width.value;
// Reinforce branches moving more than half a support-line width between layers.
// Square the threshold for comparison with the squared XY movement below.
const double max_move_sq = sqr(double(scale_(m_object_config->support_line_width / 2)));
// Belt floor: the first object layer is not on a flat bed — it rests on the // Belt floor: the first object layer is not on a flat bed — it rests on the
// tilted, moving belt. So the first-object-layer adhesion features (the tree // tilted, moving belt. So the first-object-layer adhesion features (the tree
// support brim, the hybrid first-layer base expansion) must be suppressed: // support brim, the hybrid first-layer base expansion) must be suppressed:
@@ -2201,7 +2203,7 @@ void TreeSupport::draw_circles()
size_t first_base_roof_area = 0; size_t first_base_roof_area = 0;
bool floor_interface_as_base = false; bool floor_interface_as_base = false;
bool has_circle_node = false; bool has_circle_node = false;
bool need_extra_wall = false; ExPolygons extra_wall_areas;
ExPolygons collision_sharp_tails; ExPolygons collision_sharp_tails;
ExPolygons collision_base; ExPolygons collision_base;
auto get_collision = [&](bool sharp_tail) -> ExPolygons &{ auto get_collision = [&](bool sharp_tail) -> ExPolygons &{
@@ -2279,7 +2281,6 @@ void TreeSupport::draw_circles()
// area = diff_clipped({ ExPolygon(circle) }, get_collision(node.is_sharp_tail && node.distance_to_top <= 0)); // area = diff_clipped({ ExPolygon(circle) }, get_collision(node.is_sharp_tail && node.distance_to_top <= 0));
if (!area.empty()) has_circle_node = true; if (!area.empty()) has_circle_node = true;
if (node.need_extra_wall) need_extra_wall = true;
// merge overhang to get a smoother interface surface // merge overhang to get a smoother interface surface
// Do not merge when buildplate_only is on, because some underneath nodes may have been deleted. // Do not merge when buildplate_only is on, because some underneath nodes may have been deleted.
@@ -2294,6 +2295,10 @@ void TreeSupport::draw_circles()
} }
} }
// Use the Pythagorean theorem to compare movement magnitude (dx² + dy²) without a square root.
if (node.is_processed && node.movement.cast<double>().squaredNorm() > max_move_sq)
append(extra_wall_areas, area);
if (obj_layer_nr>0 && node.distance_to_top < 0) if (obj_layer_nr>0 && node.distance_to_top < 0)
append(roof_gap_areas, area); append(roof_gap_areas, area);
else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 && else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 &&
@@ -2530,7 +2535,8 @@ void TreeSupport::draw_circles()
//if (area(expoly) < SQ(scale_(1))) continue; //if (area(expoly) < SQ(scale_(1))) continue;
area_groups.emplace_back(&expoly, SupportLayer::BaseType, max_layers_above_base); area_groups.emplace_back(&expoly, SupportLayer::BaseType, max_layers_above_base);
area_groups.back().need_infill = overlaps({ expoly }, area_poly); area_groups.back().need_infill = overlaps({ expoly }, area_poly);
area_groups.back().need_extra_wall = need_extra_wall && !area_groups.back().need_infill; area_groups.back().need_extra_wall = !area_groups.back().need_infill &&
!extra_wall_areas.empty() && overlaps({expoly}, extra_wall_areas);
} }
for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) { for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) {
auto &expoly = ts_layer->roof_areas[roof_idx]; auto &expoly = ts_layer->roof_areas[roof_idx];
@@ -3433,11 +3439,6 @@ void TreeSupport::smooth_nodes()
} }
} }
float max_move = scale_(m_object_config->support_line_width / 2);
// if the branch is very tall, the tip also needs extra wall
float thresh_tall_branch = 100;
float thresh_dist_to_top = 30;
for (int layer_nr = 0; layer_nr< contact_nodes.size(); layer_nr++) { for (int layer_nr = 0; layer_nr< contact_nodes.size(); layer_nr++) {
std::vector<SupportNode *> &curr_layer_nodes = contact_nodes[layer_nr]; std::vector<SupportNode *> &curr_layer_nodes = contact_nodes[layer_nr];
if (curr_layer_nodes.empty()) continue; if (curr_layer_nodes.empty()) continue;
@@ -3447,20 +3448,17 @@ void TreeSupport::smooth_nodes()
std::vector<double> radii; std::vector<double> radii;
std::vector<SupportNode *> branch; std::vector<SupportNode *> branch;
SupportNode * p_node = node; SupportNode * p_node = node;
float total_height = 0;
// add a fixed head if it's not a polygon node, see STUDIO-4403 // add a fixed head if it's not a polygon node, see STUDIO-4403
// Polygon node can't be added because the move distance might be huge, making the nodes in between jump and dangling // Polygon node can't be added because the move distance might be huge, making the nodes in between jump and dangling
if (node->child && node->child->type!=ePolygon) { if (node->child && node->child->type!=ePolygon) {
pts.push_back(p_node->child->position); pts.push_back(p_node->child->position);
radii.push_back(p_node->child->radius); radii.push_back(p_node->child->radius);
branch.push_back(p_node->child); branch.push_back(p_node->child);
total_height += p_node->child->height;
} }
do { do {
pts.push_back(p_node->position); pts.push_back(p_node->position);
radii.push_back(p_node->radius); radii.push_back(p_node->radius);
branch.push_back(p_node); branch.push_back(p_node);
total_height += p_node->height;
p_node = p_node->parent; p_node = p_node->parent;
} while (p_node && !p_node->is_processed); } while (p_node && !p_node->is_processed);
if (pts.size() < 3) continue; if (pts.size() < 3) continue;
@@ -3479,9 +3477,6 @@ void TreeSupport::smooth_nodes()
branch[i]->radius = radii1[i]; branch[i]->radius = radii1[i];
branch[i]->movement = (pts[i + 1] - pts[i - 1]) / 2; branch[i]->movement = (pts[i + 1] - pts[i - 1]) / 2;
branch[i]->is_processed = true; branch[i]->is_processed = true;
if (branch[i]->parents.size() > 1 || (branch[i]->movement.x() > max_move || branch[i]->movement.y() > max_move) ||
(total_height > thresh_tall_branch && branch[i]->dist_mm_to_top < thresh_dist_to_top))
branch[i]->need_extra_wall = true;
BOOST_LOG_TRIVIAL(info) << "smooth_nodes: layer_nr=" << layer_nr << ", i=" << i << ", pt=" << pt << ", movement=" << branch[i]->movement << ", radius=" << branch[i]->radius; BOOST_LOG_TRIVIAL(info) << "smooth_nodes: layer_nr=" << layer_nr << ", i=" << i << ", pt=" << pt << ", movement=" << branch[i]->movement << ", radius=" << branch[i]->radius;
} }
} }
@@ -3489,13 +3484,6 @@ void TreeSupport::smooth_nodes()
std::swap(pts, pts1); std::swap(pts, pts1);
std::swap(radii, radii1); std::swap(radii, radii1);
} }
else {
// interpolate need_extra_wall in the end
for (size_t i = 1; i < branch.size() - 1; i++) {
if (branch[i - 1]->need_extra_wall && branch[i + 1]->need_extra_wall)
branch[i]->need_extra_wall = true;
}
}
} }
} }
} }
-1
View File
@@ -135,7 +135,6 @@ struct SupportNode
TreeNodeType type = eCircle; TreeNodeType type = eCircle;
bool is_corner = false; bool is_corner = false;
bool is_processed = false; bool is_processed = false;
bool need_extra_wall = false;
bool is_sharp_tail = false; bool is_sharp_tail = false;
bool valid = true; bool valid = true;
ExPolygon overhang; // when type==ePolygon, set this value to get original overhang area ExPolygon overhang; // when type==ePolygon, set this value to get original overhang area
+3 -1
View File
@@ -283,7 +283,7 @@ bool height_texture_has_color(const TextureDisplacementLayer &layer)
return decode_height_texture(layer).has_color(); return decode_height_texture(layer).has_color();
} }
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer) DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded)
{ {
DecodedHeightTexture result; DecodedHeightTexture result;
if (layer.empty()) if (layer.empty())
@@ -351,6 +351,7 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
} }
} }
if (keep_decoded) {
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex); std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
// Opportunistically drop entries for image_data that no longer exists anywhere, so the // Opportunistically drop entries for image_data that no longer exists anywhere, so the
// cache doesn't grow without bound across many add/remove-texture cycles in a long session. // cache doesn't grow without bound across many add/remove-texture cycles in a long session.
@@ -359,6 +360,7 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
it = it->second.first.expired() ? entries.erase(it) : std::next(it); it = it->second.first.expired() ? entries.erase(it) : std::next(it);
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result }; entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
} }
}
// The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before, // The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before,
// because `preview_adjust` is an edit to the image while `smoothing` is a live low-pass over whatever // because `preview_adjust` is an edit to the image while `smoothing` is a live low-pass over whatever
+3 -2
View File
@@ -599,8 +599,9 @@ DecodedHeightTexture downscale_height_texture(const DecodedHeightTexture &src, i
// colour PNGs are accepted; for a colour source the height is its luminance, using the same // colour PNGs are accepted; for a colour source the height is its luminance, using the same
// coefficients wxImage::ConvertToGreyscale() uses, so a texture imported as colour displaces exactly // coefficients wxImage::ConvertToGreyscale() uses, so a texture imported as colour displaces exactly
// as it did when the importer flattened it to grey on the way in. Returns an empty // as it did when the importer flattened it to grey on the way in. Returns an empty
// DecodedHeightTexture if image_data is empty or is not a PNG at all. // DecodedHeightTexture if image_data is empty or is not a PNG at all. keep_decoded = false leaves the
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer); // result out of the decode cache, for one-off reads such as a picker thumbnail that must not hold full-size pixels.
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded = true);
// decode_height_texture(layer).has_color(), answered from the decode cache rather than from a copy of the // decode_height_texture(layer).has_color(), answered from the decode cache rather than from a copy of the
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read. // texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
+7
View File
@@ -842,6 +842,13 @@ void load_string_file(const boost::filesystem::path& p, std::string& str);
bool check_layer_id_pattern(const std::string& pattern, int layer_id); bool check_layer_id_pattern(const std::string& pattern, int layer_id);
// On Windows, return an equivalent path carrying the extended-length ("\\?\" / "\\?\UNC\")
// prefix so the Win32 APIs accept it past MAX_PATH even when LongPathsEnabled is off. The path
// is made absolute and lexically normalized first, because the prefix disables normalisation.
// A path that is already extended (or a device path) is returned unchanged; on other platforms,
// or when an absolute path cannot be formed, the input is returned unchanged.
boost::filesystem::path extended_path(const boost::filesystem::path& path);
} // namespace Slic3r } // namespace Slic3r
#if WIN32 #if WIN32
+53 -8
View File
@@ -17,6 +17,7 @@
#if defined(_MSC_VER) || defined(__MINGW64__) #if defined(_MSC_VER) || defined(__MINGW64__)
#include "boost/nowide/cstdio.hpp" #include "boost/nowide/cstdio.hpp"
#include "boost/nowide/convert.hpp"
#endif #endif
#include "I18N.hpp" #include "I18N.hpp"
@@ -62,7 +63,11 @@ bool open_zip(mz_zip_archive *zip, const char *fname, bool isread)
FILE *f = nullptr; FILE *f = nullptr;
#if defined(_MSC_VER) || defined(__MINGW64__) #if defined(_MSC_VER) || defined(__MINGW64__)
f = boost::nowide::fopen(fname, mode); // fname is UTF-8 (the nowide contract). Route it through the shared extended-path helper so an
// archive stored past MAX_PATH opens even with LongPathsEnabled off.
const std::wstring wide_path = boost::nowide::widen(fname);
const std::string extended = boost::nowide::narrow(Slic3r::extended_path(boost::filesystem::path(wide_path)).wstring());
f = boost::nowide::fopen(extended.c_str(), mode);
#elif defined(__GNUC__) && defined(_LARGEFILE64_SOURCE) #elif defined(__GNUC__) && defined(_LARGEFILE64_SOURCE)
f = fopen64(fname, mode); f = fopen64(fname, mode);
#else #else
@@ -101,6 +106,14 @@ bool close_zip(mz_zip_archive *zip, bool isread)
} }
return ret; return ret;
} }
#ifdef WIN32
// mz_file_write_func writing an extracted entry into an already-open FILE*.
size_t mz_write_to_cfile(void *opaque, mz_uint64 /*file_ofs*/, const void *buffer, size_t size)
{
return std::fwrite(buffer, 1, size, static_cast<FILE *>(opaque));
}
#endif
} }
bool open_zip_reader(mz_zip_archive *zip, const std::string &fname) bool open_zip_reader(mz_zip_archive *zip, const std::string &fname)
@@ -116,6 +129,36 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname)
bool close_zip_reader(mz_zip_archive *zip) { return close_zip(zip, true); } bool close_zip_reader(mz_zip_archive *zip) { return close_zip(zip, true); }
bool close_zip_writer(mz_zip_archive *zip) { return close_zip(zip, false); } bool close_zip_writer(mz_zip_archive *zip) { return close_zip(zip, false); }
bool extract_zip_entry_to_file(mz_zip_archive *zip, mz_uint file_index, const boost::filesystem::path &dest_path)
{
if (!zip)
return false;
#ifdef WIN32
// Open the destination ourselves with a Windows extended-length path: miniz's narrow
// extract_to_file cannot be relied on past MAX_PATH with LongPathsEnabled off, and its wide
// variant takes a plain (unprefixed) path and a text-mode stream. This keeps the write binary.
FILE *file = ::_wfopen(Slic3r::extended_path(dest_path).wstring().c_str(), L"wb");
if (!file) {
zip->m_last_error = MZ_ZIP_FILE_OPEN_FAILED;
return false;
}
const bool written = mz_zip_reader_extract_to_callback(zip, file_index, mz_write_to_cfile, file, 0) != 0;
const bool closed = std::fclose(file) == 0;
if (!written || !closed) {
if (written && !closed)
zip->m_last_error = MZ_ZIP_FILE_CLOSE_FAILED;
// Drop the partial entry rather than leave a truncated file behind.
boost::system::error_code ec;
boost::filesystem::remove(Slic3r::extended_path(dest_path), ec);
return false;
}
return true;
#else
return mz_zip_reader_extract_to_file(zip, file_index, dest_path.string().c_str(), 0) != 0;
#endif
}
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat) std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat)
{ {
if (stat.m_is_utf8) if (stat.m_is_utf8)
@@ -156,10 +199,13 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
} }
const std::string dest_file = dest_dir + "/" + stat.m_filename; const std::string dest_file = dest_dir + "/" + stat.m_filename;
try { try {
if (stat.m_is_directory) { // Use the extended-length form for every filesystem call so entries under a destination
// past MAX_PATH are created even with LongPathsEnabled off.
const boost::filesystem::path dest_path(dest_file); const boost::filesystem::path dest_path(dest_file);
if (!boost::filesystem::exists(dest_path)) const boost::filesystem::path io_dest = extended_path(dest_path);
boost::filesystem::create_directories(dest_path); if (stat.m_is_directory) {
if (!boost::filesystem::exists(io_dest))
boost::filesystem::create_directories(io_dest);
continue; continue;
} }
if (stat.m_uncomp_size == 0) { if (stat.m_uncomp_size == 0) {
@@ -167,10 +213,9 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
continue; continue;
} }
// Replace a symlink at the destination rather than writing through it. // Replace a symlink at the destination rather than writing through it.
const boost::filesystem::path dest_path(dest_file); if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(io_dest)))
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(dest_path))) boost::filesystem::remove(io_dest);
boost::filesystem::remove(dest_path); if (!extract_zip_entry_to_file(&archive, stat.m_file_index, dest_path)) {
if (!mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0)) {
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed"; BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
close_zip_reader(&archive); close_zip_reader(&archive);
return false; return false;
+5
View File
@@ -1,6 +1,7 @@
#ifndef MINIZ_EXTENSION_HPP #ifndef MINIZ_EXTENSION_HPP
#define MINIZ_EXTENSION_HPP #define MINIZ_EXTENSION_HPP
#include <boost/filesystem/path.hpp>
#include <string> #include <string>
#include <miniz.h> #include <miniz.h>
@@ -13,6 +14,10 @@ bool close_zip_writer(mz_zip_archive *zip);
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat); std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
// Extracts every entry of the archive under dest_dir. Nothing is written if any entry would resolve outside dest_dir. // Extracts every entry of the archive under dest_dir. Nothing is written if any entry would resolve outside dest_dir.
bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir); bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir);
// Extracts one entry into dest_path, binary-safe and overwriting. On Windows the destination is
// opened through an extended-length path so it works past MAX_PATH even with LongPathsEnabled off;
// a partial file is removed if the write fails. False for a directory/unsupported entry or on error.
bool extract_zip_entry_to_file(mz_zip_archive *zip, mz_uint file_index, const boost::filesystem::path &dest_path);
class MZ_Archive { class MZ_Archive {
public: public:
+40
View File
@@ -2254,5 +2254,45 @@ bool check_layer_id_pattern(const std::string& pattern, int layer_id){
return mod >= 0 && mod < count; return mod >= 0 && mod < count;
} }
boost::filesystem::path extended_path(const boost::filesystem::path& path) {
#ifdef WIN32
namespace fs = boost::filesystem;
// Already extended, or an explicit device path (which is not subject to MAX_PATH and must not
// be absolutized or normalized): leave it alone.
{
fs::path prefix_candidate = path;
const std::wstring native = prefix_candidate.make_preferred().wstring();
if (native.rfind(L"\\\\?\\", 0) == 0 || native.rfind(L"\\\\.\\", 0) == 0)
return fs::path(native);
}
fs::path absolute = path;
if (!absolute.is_absolute()) {
boost::system::error_code ec;
absolute = fs::absolute(absolute, ec);
if (ec)
return path; // can't form a valid prefixed path from a relative one
}
// A trailing separator is redundant to the Win32 APIs, but lexically_normal() would rewrite it
// as a "." element, which the "\\?\" prefix (normalisation off) takes literally. Drop it first.
while (absolute.has_relative_path() && absolute.filename() == ".")
absolute = absolute.parent_path();
// The "\\?\" prefix turns off Win32 normalisation, so do it here first.
absolute = absolute.lexically_normal();
// Extended paths only recognize backslash separators.
const std::wstring native = absolute.make_preferred().wstring();
if (native.empty())
return path;
if (native.rfind(L"\\\\", 0) == 0)
return fs::path(L"\\\\?\\UNC\\" + native.substr(2)); // \\server\share\... -> \\?\UNC\server\share\...
return fs::path(L"\\\\?\\" + native); // C:\dir\... -> \\?\C:\dir\...
#else
return path;
#endif
}
}; // namespace Slic3r }; // namespace Slic3r
+6
View File
@@ -222,6 +222,7 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmoUtils.hpp GUI/Gizmos/GLGizmoUtils.hpp
GUI/GLCanvas3D.cpp GUI/GLCanvas3D.cpp
GUI/GLCanvas3D.hpp GUI/GLCanvas3D.hpp
GUI/SequentialPrintClearance.hpp
GUI/GLModel.cpp GUI/GLModel.cpp
GUI/GLModel.hpp GUI/GLModel.hpp
GUI/GLSelectionRectangle.cpp GUI/GLSelectionRectangle.cpp
@@ -689,6 +690,8 @@ set(SLIC3R_GUI_SOURCES
plugin/host/PluginHostUi.hpp plugin/host/PluginHostUi.hpp
plugin/host/PluginPages.cpp plugin/host/PluginPages.cpp
plugin/host/PluginPages.hpp plugin/host/PluginPages.hpp
plugin/host/PluginResourceTextures.cpp
plugin/host/PluginResourceTextures.hpp
plugin/CloudPluginService.cpp plugin/CloudPluginService.cpp
plugin/CloudPluginService.hpp plugin/CloudPluginService.hpp
plugin/PluginFsUtils.cpp plugin/PluginFsUtils.cpp
@@ -712,6 +715,9 @@ set(SLIC3R_GUI_SOURCES
plugin/pluginTypes/pages/PagesPluginCapability.hpp plugin/pluginTypes/pages/PagesPluginCapability.hpp
plugin/pluginTypes/pages/PagesPluginCapability.cpp plugin/pluginTypes/pages/PagesPluginCapability.cpp
plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.cpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapabilityTrampoline.hpp
plugin/pluginTypes/script/ScriptPluginCapability.hpp plugin/pluginTypes/script/ScriptPluginCapability.hpp
plugin/pluginTypes/script/ScriptPluginCapability.cpp plugin/pluginTypes/script/ScriptPluginCapability.cpp
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp
+259 -71
View File
@@ -85,6 +85,96 @@ static std::string float_to_string_with_precision(float value, int precision = 3
return stream.str(); return stream.str();
} }
static std::string colour_to_ams_string(const wxColour& color)
{
char col_buf[10];
sprintf(col_buf, "%02X%02X%02X%02X", (int)color.Red(), (int)color.Green(), (int)color.Blue(), (int)color.Alpha());
return col_buf;
}
static wxColour mix_colour(const wxColour& left, const wxColour& right, double ratio)
{
ratio = std::max(0.0, std::min(1.0, ratio));
auto mix_channel = [ratio](unsigned char l, unsigned char r) {
return static_cast<unsigned char>(std::round(l + (r - l) * ratio));
};
return wxColour(mix_channel(left.Red(), right.Red()),
mix_channel(left.Green(), right.Green()),
mix_channel(left.Blue(), right.Blue()),
mix_channel(left.Alpha(), right.Alpha()));
}
static std::vector<ColorPickerPopup::ColorItem> collect_ams_color_items(DevFilaSystem* fila_system)
{
std::vector<ColorPickerPopup::ColorItem> items;
if (!fila_system) return items;
for (const auto& ams_pair : fila_system->GetAmsList()) {
DevAms* ams = ams_pair.second;
if (!ams) continue;
for (const auto& tray_pair : ams->GetTrays()) {
DevAmsTray* tray = tray_pair.second;
if (!tray || !tray->is_tray_info_ready()) continue;
ColorPickerPopup::ColorItem item;
for (const std::string& col : tray->cols) {
item.colors.emplace_back(DevAmsTray::decode_color(col));
}
if (item.colors.empty() && !tray->color.empty()) {
item.colors.emplace_back(DevAmsTray::decode_color(tray->color));
}
if (item.colors.empty()) continue;
item.ctype = item.colors.size() > 1 ? tray->ctype : 2;
items.emplace_back(std::move(item));
}
}
return items;
}
static std::vector<ColorPickerPopup::ColorItem> collect_preset_color_items(const std::string& filament_id)
{
std::vector<ColorPickerPopup::ColorItem> items;
if (filament_id.empty()) return items;
auto* clr_query = GUI::wxGetApp().get_filament_color_code_query();
if (!clr_query) return items;
auto* agent = wxGetApp().getAgent();
const std::string catalog_id = agent ? agent->from_orca_filament_id(filament_id) : filament_id;
FilamentColorCodes* color_codes = clr_query->GetFilaInfoMap(wxString::FromUTF8(catalog_id));
if (!color_codes || !color_codes->GetFilamentColor2CodeMap()) return items;
std::set<std::string> seen;
for (const auto& color_pair : *color_codes->GetFilamentColor2CodeMap()) {
const FilamentColor& fila_color = color_pair.first;
FilamentColorCode* color_code = color_pair.second;
if (!color_code || fila_color.GetColors().empty()) continue;
ColorPickerPopup::ColorItem item;
item.ctype = fila_color.ColorCount() < 2 ? 2 :
(fila_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR ? 0 : 1);
item.name = color_code->GetFilaColorName();
std::string key = std::to_string(item.ctype);
for (const wxColour& color : fila_color.GetColors()) {
item.colors.emplace_back(color);
char rgba[9];
std::snprintf(rgba, sizeof(rgba), "%02X%02X%02X%02X",
static_cast<unsigned>(color.Red()), static_cast<unsigned>(color.Green()),
static_cast<unsigned>(color.Blue()), static_cast<unsigned>(color.Alpha()));
key += "|" + std::string(rgba);
}
if (item.colors.empty() || !seen.insert(key).second) continue;
items.emplace_back(std::move(item));
}
return items;
}
AMSMaterialsSetting::AMSMaterialsSetting(wxWindow *parent, wxWindowID id) AMSMaterialsSetting::AMSMaterialsSetting(wxWindow *parent, wxWindowID id)
: DPIDialog(parent, id, _L("AMS Materials Setting"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX) : DPIDialog(parent, id, _L("AMS Materials Setting"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
, m_color_picker_popup(ColorPickerPopup(this)) , m_color_picker_popup(ColorPickerPopup(this))
@@ -740,8 +830,19 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
nozzle_temp_min.ToLong(&nozzle_temp_min_int); nozzle_temp_min.ToLong(&nozzle_temp_min_int);
nozzle_temp_max.ToLong(&nozzle_temp_max_int); nozzle_temp_max.ToLong(&nozzle_temp_max_int);
wxColour color = m_clr_picker->m_colour; wxColour color = m_clr_picker->m_colour;
char col_buf[10]; std::string tray_color = colour_to_ams_string(color);
sprintf(col_buf, "%02X%02X%02X%02X", (int)color.Red(), (int)color.Green(), (int)color.Blue(), (int)color.Alpha()); std::vector<std::string> tray_colors;
int tray_ctype = 2;
// Only firmware advertising fun2[23] receives the manual multi-color fields.
if (obj->is_support_filament_manual_multi_color) {
for (const wxColour& selected_color : m_clr_picker->m_cols) {
tray_colors.emplace_back(colour_to_ams_string(selected_color));
}
if (tray_colors.empty()) {
tray_colors.emplace_back(tray_color);
}
tray_ctype = tray_colors.size() > 1 ? m_clr_picker->ctype : 2;
}
if (ams_filament_id.empty() || nozzle_temp_min.empty() || nozzle_temp_max.empty() || m_filament_type.empty()) { if (ams_filament_id.empty() || nozzle_temp_min.empty() || nozzle_temp_max.empty() || m_filament_type.empty()) {
BOOST_LOG_TRIVIAL(trace) << "Invalid Setting id"; BOOST_LOG_TRIVIAL(trace) << "Invalid Setting id";
@@ -760,13 +861,33 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
<< ", tray_info_idx (filament_id) = " << ams_filament_id << ", tray_info_idx (filament_id) = " << ams_filament_id
<< ", setting_id = " << ams_setting_id << ", setting_id = " << ams_setting_id
<< ", tray_type = " << m_filament_type << ", tray_type = " << m_filament_type
<< ", tray_color = " << col_buf << ", tray_color = " << tray_color
<< ", nozzle_temp_min = " << nozzle_temp_min_int << ", nozzle_temp_min = " << nozzle_temp_min_int
<< ", nozzle_temp_max = " << nozzle_temp_max_int; << ", nozzle_temp_max = " << nozzle_temp_max_int;
// set filament // set filament
if (m_is_third) { if (m_is_third) {
obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, std::string(col_buf), m_filament_type, nozzle_temp_min_int, nozzle_temp_max_int); obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, tray_color, m_filament_type, nozzle_temp_min_int, nozzle_temp_max_int,
tray_colors, tray_ctype);
// Refresh the edited tray immediately while its MQTT acknowledgement is pending.
DevAmsTray* tray = nullptr;
if (is_virtual_tray()) {
for (auto& virtual_tray : obj->vt_slot) {
if (virtual_tray.id == std::to_string(ams_id)) {
tray = &virtual_tray;
break;
}
}
} else {
tray = obj->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id));
}
if (tray) {
tray->color = tray_color;
tray->cols = tray_colors;
tray->ctype = tray_ctype;
tray->set_hold_count();
}
} }
//reset param //reset param
@@ -923,8 +1044,15 @@ void AMSMaterialsSetting::set_ctype(int ctype)
void AMSMaterialsSetting::on_picker_color(wxCommandEvent& event) void AMSMaterialsSetting::on_picker_color(wxCommandEvent& event)
{ {
std::vector<wxColour> colors = m_color_picker_popup.get_selected_colours();
if (colors.empty()) {
unsigned int color_num = event.GetInt(); unsigned int color_num = event.GetInt();
set_color(wxColour(color_num>>24&0xFF, color_num>>16&0xFF, color_num>>8&0xFF, color_num&0xFF)); colors.emplace_back(color_num >> 24 & 0xFF, color_num >> 16 & 0xFF, color_num >> 8 & 0xFF, color_num & 0xFF);
}
set_ctype(m_color_picker_popup.get_selected_ctype());
set_color(colors.front());
set_colors(colors);
} }
void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event) void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
@@ -939,14 +1067,12 @@ void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
} }
} }
std::vector<wxColour> ams_colors; m_color_picker_popup.set_ams_colours(collect_ams_color_items(obj->GetFilaSystem().get()));
obj->GetFilaSystem()->CollectAmsColors(ams_colors); m_color_picker_popup.set_preset_colours(collect_preset_color_items(ams_filament_id));
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour, m_clr_picker->m_cols, m_clr_picker->ctype);
wxPoint img_pos = m_clr_picker->ClientToScreen(wxPoint(0, 0)); wxPoint img_pos = m_clr_picker->ClientToScreen(wxPoint(0, 0));
wxPoint popup_pos(img_pos.x - m_color_picker_popup.GetSize().x - FromDIP(95), img_pos.y - FromDIP(65)); wxPoint popup_pos(img_pos.x - m_color_picker_popup.GetSize().x - FromDIP(95), img_pos.y - FromDIP(65));
m_color_picker_popup.Position(popup_pos, wxSize(0, 0)); m_color_picker_popup.Position(popup_pos, wxSize(0, 0));
m_color_picker_popup.set_ams_colours(ams_colors);
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour);
m_color_picker_popup.Popup(); m_color_picker_popup.Popup();
} }
@@ -1735,7 +1861,46 @@ void ColorPicker::doRender(wxDC& dc)
auto radius = m_show_full ? size.x / 2 - FromDIP(1) : size.x / 2; auto radius = m_show_full ? size.x / 2 - FromDIP(1) : size.x / 2;
if (m_selected) radius -= FromDIP(1); if (m_selected) radius -= FromDIP(1);
if (alpha == 0) { if (m_show_full && m_cols.size() > 1) {
if (ctype == 0) {
const double center_x = size.x / 2.0;
const double center_y = size.y / 2.0;
const double draw_radius = radius;
dc.SetPen(*wxTRANSPARENT_PEN);
for (int x = 0; x < size.x; ++x) {
const double dx = x + 0.5 - center_x;
if (std::abs(dx) > draw_radius) continue;
const double half_height = std::sqrt(std::max(0.0, draw_radius * draw_radius - dx * dx));
const int top = std::max(0, static_cast<int>(std::ceil(center_y - half_height)));
const int bottom = std::min(size.y - 1, static_cast<int>(std::floor(center_y + half_height)));
const double pos = size.x > 1 ? static_cast<double>(x) / (size.x - 1) : 0.0;
const double scaled = pos * (m_cols.size() - 1);
const size_t idx = std::min(static_cast<size_t>(scaled), m_cols.size() - 2);
const double ratio = scaled - idx;
if (top <= bottom) {
dc.SetBrush(wxBrush(mix_colour(m_cols[idx], m_cols[idx + 1], ratio)));
dc.DrawRectangle(x, top, 1, bottom - top + 1);
}
}
}
else {
float ev_angle = 360.0 / m_cols.size();
const float overlap = 2.0f;
wxPoint center(size.x / 2, size.y / 2);
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(m_cols.front()));
dc.DrawCircle(center.x, center.y, radius);
for (int i = 0; i < m_cols.size(); i++) {
dc.SetBrush(m_cols[i]);
float startAngle = 270.0f + i * ev_angle;
float endAngle = startAngle + ev_angle;
dc.DrawEllipticArc(center.x - radius, center.y - radius, 2 * radius, 2 * radius, startAngle - overlap, endAngle + overlap);
}
}
}
else if (alpha == 0) {
wxSize bmp_size = m_bitmap_transparent_def.GetBmpSize(); wxSize bmp_size = m_bitmap_transparent_def.GetBmpSize();
int center_x = (size.x - bmp_size.x) / 2; int center_x = (size.x - bmp_size.x) / 2;
int center_y = (size.y - bmp_size.y) / 2; int center_y = (size.y - bmp_size.y) / 2;
@@ -1777,52 +1942,6 @@ void ColorPicker::doRender(wxDC& dc)
dc.SetPen(wxPen(wxColour("#6B6B6B"))); dc.SetPen(wxPen(wxColour("#6B6B6B")));
dc.SetBrush(*wxTRANSPARENT_BRUSH); dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawCircle(size.x / 2, size.y / 2, radius); dc.DrawCircle(size.x / 2, size.y / 2, radius);
if (m_cols.size() > 1) {
if (ctype == 0) {
int left = FromDIP(0);
float total_width = size.x;
int gwidth = std::round(total_width / (m_cols.size() - 1));
for (int i = 0; i < m_cols.size() - 1; i++) {
if ((left + gwidth) > (size.x)) {
gwidth = size.x - left;
}
auto rect = wxRect(left, 0, gwidth, size.y);
dc.GradientFillLinear(rect, m_cols[i], m_cols[i + 1], wxEAST);
left += gwidth;
}
if (wxGetApp().dark_mode()) {
dc.DrawBitmap(m_bitmap_border_dark, wxPoint(0, 0));
}
else {
dc.DrawBitmap(m_bitmap_border, wxPoint(0, 0));
}
}
else {
float ev_angle = 360.0 / m_cols.size();
float startAngle = 270.0;
float endAngle = 270.0;
dc.SetPen(*wxTRANSPARENT_PEN);
for (int i = 0; i < m_cols.size(); i++) {
dc.SetBrush(m_cols[i]);
endAngle += ev_angle;
endAngle = endAngle > 360.0 ? endAngle - 360.0 : endAngle;
wxPoint center(size.x / 2, size.y / 2);
dc.DrawEllipticArc(center.x - radius, center.y - radius, 2 * radius, 2 * radius, startAngle, endAngle);
startAngle += ev_angle;
startAngle = startAngle > 360.0 ? startAngle - 360.0 : startAngle;
}
if (wxGetApp().dark_mode()) {
dc.DrawBitmap(m_bitmap_border_dark, wxPoint(0, 0));
}
else {
dc.DrawBitmap(m_bitmap_border, wxPoint(0, 0));
}
}
}
} }
if (m_is_empty) { if (m_is_empty) {
@@ -1891,19 +2010,21 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
m_ams_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); m_ams_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
//other //other
wxFlexGridSizer* fg_sizer; m_other_fg_sizer = new wxFlexGridSizer(0, 8, 0, 0);
fg_sizer = new wxFlexGridSizer(0, 8, 0, 0); m_other_fg_sizer->SetFlexibleDirection(wxBOTH);
fg_sizer->SetFlexibleDirection(wxBOTH); m_other_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
for (wxColour col : m_def_colors) { for (wxColour col : m_def_colors) {
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize); auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
cp->set_color(col); cp->set_color(col);
cp->set_colors({col});
cp->ctype = 2;
cp->set_selected(false); cp->set_selected(false);
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238))); cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
m_color_pickers.push_back(cp); m_color_pickers.push_back(cp);
fg_sizer->Add(cp, 0, wxALL, FromDIP(3)); m_default_color_pickers.push_back(cp);
m_other_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) { cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
set_def_colour(cp->m_colour); set_def_colour(cp->m_colour);
@@ -1970,7 +2091,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
m_sizer_box->Add(m_sizer_ams, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10)); m_sizer_box->Add(m_sizer_ams, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_ams_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10)); m_sizer_box->Add(m_ams_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_sizer_other, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10)); m_sizer_box->Add(m_sizer_other, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10)); m_sizer_box->Add(m_other_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_sizer_custom, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10)); m_sizer_box->Add(m_sizer_custom, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_custom_cp, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(16)); m_sizer_box->Add(m_custom_cp, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(16));
m_sizer_box->Add(0, 0, 0, wxTOP, FromDIP(10)); m_sizer_box->Add(0, 0, 0, wxTOP, FromDIP(10));
@@ -1981,7 +2102,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
m_def_color_box->Fit(); m_def_color_box->Fit();
m_sizer_main->Add(m_def_color_box, 0, wxALL | wxEXPAND, 10); m_sizer_main->Add(m_def_color_box, 0, wxALL | wxEXPAND, 10);
SetSizer(m_sizer_main); SetSizerAndFit(m_sizer_main);
Layout(); Layout();
Fit(); Fit();
@@ -2019,13 +2140,15 @@ void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& /*event*/)
wxPostEvent(GetParent(), evt); wxPostEvent(GetParent(), evt);
} }
void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams) void ColorPickerPopup::set_ams_colours(const std::vector<ColorItem>& ams)
{ {
m_ams_fg_sizer->Clear(false);
if (m_ams_color_pickers.size() > 0) { if (m_ams_color_pickers.size() > 0) {
for (ColorPicker* col_pick:m_ams_color_pickers) { for (ColorPicker* col_pick:m_ams_color_pickers) {
std::vector<ColorPicker*>::iterator iter = find(m_color_pickers.begin(), m_color_pickers.end(), col_pick); std::vector<ColorPicker*>::iterator iter = find(m_color_pickers.begin(), m_color_pickers.end(), col_pick);
if (iter != m_color_pickers.end()) { if (iter != m_color_pickers.end()) {
col_pick->Hide();
col_pick->Destroy(); col_pick->Destroy();
m_color_pickers.erase(iter); m_color_pickers.erase(iter);
} }
@@ -2035,17 +2158,22 @@ void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
} }
m_ams_colors = ams; m_ams_color_items = ams;
for (wxColour col : m_ams_colors) { for (const ColorItem& item : m_ams_color_items) {
if (item.colors.empty()) continue;
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize); auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
cp->set_color(col); cp->set_color(item.colors.front());
cp->set_colors(item.colors);
cp->ctype = item.colors.size() > 1 ? item.ctype : 2;
cp->set_show_full(true);
cp->set_selected(false); cp->set_selected(false);
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238))); cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
m_color_pickers.push_back(cp); m_color_pickers.push_back(cp);
m_ams_color_pickers.push_back(cp); m_ams_color_pickers.push_back(cp);
m_ams_fg_sizer->Add(cp, 0, wxALL, FromDIP(3)); m_ams_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) { cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
set_def_colour(cp->m_colour); set_def_colour(cp->m_colour, cp->m_cols, cp->ctype);
wxCommandEvent evt(EVT_SELECTED_COLOR); wxCommandEvent evt(EVT_SELECTED_COLOR);
unsigned long g_col = ((cp->m_colour.Red() & 0xff) << 24) + ((cp->m_colour.Green() & 0xff) << 16) + ((cp->m_colour.Blue() & 0xff) << 8) + (cp->m_colour.Alpha() & 0xff); unsigned long g_col = ((cp->m_colour.Red() & 0xff) << 24) + ((cp->m_colour.Green() & 0xff) << 16) + ((cp->m_colour.Blue() & 0xff) << 8) + (cp->m_colour.Alpha() & 0xff);
@@ -2058,9 +2186,68 @@ void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
Fit(); Fit();
} }
void ColorPickerPopup::set_def_colour(wxColour col) void ColorPickerPopup::set_preset_colours(const std::vector<ColorItem>& preset_colors)
{
m_other_fg_sizer->Clear(false);
if (!m_preset_color_pickers.empty()) {
for (ColorPicker* col_pick : m_preset_color_pickers) {
auto iter = std::find(m_color_pickers.begin(), m_color_pickers.end(), col_pick);
if (iter != m_color_pickers.end()) {
col_pick->Hide();
col_pick->Destroy();
m_color_pickers.erase(iter);
}
}
m_preset_color_pickers.clear();
}
for (const ColorItem& item : preset_colors) {
if (item.colors.empty()) continue;
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
cp->set_color(item.colors.front());
cp->set_colors(item.colors);
cp->ctype = item.colors.size() > 1 ? item.ctype : 2;
cp->set_show_full(true);
cp->set_selected(false);
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
if (!item.name.empty()) {
cp->SetToolTip(item.name);
}
m_color_pickers.push_back(cp);
m_preset_color_pickers.push_back(cp);
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
set_def_colour(cp->m_colour, cp->m_cols, cp->ctype);
wxCommandEvent evt(EVT_SELECTED_COLOR);
unsigned long g_col = ((cp->m_colour.Red() & 0xff) << 24) + ((cp->m_colour.Green() & 0xff) << 16) + ((cp->m_colour.Blue() & 0xff) << 8) + (cp->m_colour.Alpha() & 0xff);
evt.SetInt(g_col);
wxPostEvent(GetParent(), evt);
});
}
const bool show_default_colours = m_preset_color_pickers.empty();
for (ColorPicker* cp : m_default_color_pickers) {
cp->Show(show_default_colours);
}
for (ColorPicker* cp : (show_default_colours ? m_default_color_pickers : m_preset_color_pickers)) {
m_other_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
}
m_other_fg_sizer->Layout();
Layout();
Fit();
}
void ColorPickerPopup::set_def_colour(wxColour col, std::vector<wxColour> cols, int ctype)
{ {
m_def_col = col; m_def_col = col;
if (cols.empty()) {
cols.push_back(col);
}
m_def_cols = cols;
m_def_ctype = m_def_cols.size() > 1 ? ctype : 2;
for (ColorPicker* cp : m_color_pickers) { for (ColorPicker* cp : m_color_pickers) {
if (cp->m_selected) { if (cp->m_selected) {
@@ -2069,7 +2256,8 @@ void ColorPickerPopup::set_def_colour(wxColour col)
} }
for (ColorPicker* cp : m_color_pickers) { for (ColorPicker* cp : m_color_pickers) {
if (cp->m_colour == m_def_col) { if (!cp->IsShown()) continue;
if (cp->ctype == m_def_ctype && cp->m_cols == m_def_cols) {
cp->set_selected(true); cp->set_selected(true);
break; break;
} }
+19 -4
View File
@@ -82,14 +82,22 @@ public:
class ColorPickerPopup : public PopupWindow class ColorPickerPopup : public PopupWindow
{ {
public: public:
struct ColorItem
{
std::vector<wxColour> colors;
int ctype = 2;
wxString name;
};
ScalableBitmap m_ts_bitmap_custom; ScalableBitmap m_ts_bitmap_custom;
wxStaticBitmap* m_ts_stbitmap_custom; wxStaticBitmap* m_ts_stbitmap_custom;
StaticBox* m_custom_cp; StaticBox* m_custom_cp;
StaticBox* m_def_color_box; StaticBox* m_def_color_box;
wxFlexGridSizer* m_ams_fg_sizer; wxFlexGridSizer* m_ams_fg_sizer;
wxFlexGridSizer* m_other_fg_sizer;
wxColour m_def_col; wxColour m_def_col;
std::vector<wxColour> m_def_colors; std::vector<wxColour> m_def_colors;
std::vector<wxColour> m_ams_colors; std::vector<ColorItem> m_ams_color_items;
std::vector<ColorPicker*> m_color_pickers; std::vector<ColorPicker*> m_color_pickers;
std::vector<ColorPicker*> m_ams_color_pickers; std::vector<ColorPicker*> m_ams_color_pickers;
@@ -97,13 +105,20 @@ public:
ColorPickerPopup(wxWindow* parent); ColorPickerPopup(wxWindow* parent);
~ColorPickerPopup() {}; ~ColorPickerPopup() {};
void on_custom_clr_picker(wxMouseEvent& event); void on_custom_clr_picker(wxMouseEvent& event);
void set_ams_colours(std::vector<wxColour> ams); void set_ams_colours(const std::vector<ColorItem>& ams);
void set_def_colour(wxColour col); void set_preset_colours(const std::vector<ColorItem>& preset_colors);
void set_def_colour(wxColour col, std::vector<wxColour> cols = {}, int ctype = 2);
const std::vector<wxColour>& get_selected_colours() const { return m_def_cols; }
int get_selected_ctype() const { return m_def_ctype; }
void paintEvent(wxPaintEvent& evt); void paintEvent(wxPaintEvent& evt);
virtual void OnDismiss() wxOVERRIDE; virtual void OnDismiss() wxOVERRIDE;
virtual bool ProcessLeftDown(wxMouseEvent& event) wxOVERRIDE; virtual bool ProcessLeftDown(wxMouseEvent& event) wxOVERRIDE;
public: private:
std::vector<ColorPicker*> m_default_color_pickers;
std::vector<ColorPicker*> m_preset_color_pickers;
std::vector<wxColour> m_def_cols;
int m_def_ctype = 2;
}; };
+3 -1
View File
@@ -747,7 +747,9 @@ Print::ApplyStatus BackgroundSlicingProcess::apply(const Model& model, const Dyn
// Orca: prevent resetting under gcode viewer mode // Orca: prevent resetting under gcode viewer mode
if (invalidated != PrintBase::APPLY_STATUS_UNCHANGED) { if (invalidated != PrintBase::APPLY_STATUS_UNCHANGED) {
const auto plater = GUI::wxGetApp().mainframe->m_plater; // Use the app’s plater accessor instead of going through mainframe, otherwise anything during startup that reaches apply()
// (loading presets, config or input files, e.g. from the CLI) therefore dereferences a null `mainframe` and crashes.
const auto plater = GUI::wxGetApp().plater();
if (plater && plater->only_gcode_mode()) { if (plater && plater->only_gcode_mode()) {
invalidated = PrintBase::APPLY_STATUS_UNCHANGED; invalidated = PrintBase::APPLY_STATUS_UNCHANGED;
} }
+5 -2
View File
@@ -15,6 +15,7 @@
#include <string> #include <string>
#include "slic3r/GUI/wxExtensions.hpp" #include "slic3r/GUI/wxExtensions.hpp"
#include "libslic3r/AppConfig.hpp" #include "libslic3r/AppConfig.hpp"
#include "IPrinterAgent.hpp"
#include "DeviceCore/DevManager.h" #include "DeviceCore/DevManager.h"
@@ -170,14 +171,16 @@ void ConnectPrinterDialog::on_input_enter(wxCommandEvent& evt)
void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event) void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event)
{ {
wxString code = m_textCtrl_code->GetTextCtrl()->GetValue(); wxString code = m_textCtrl_code->GetTextCtrl()->GetValue();
if (code.empty() && (!m_obj || m_obj->printer_agent_id != MOONRAKER_PRINTER_AGENT_ID)) // Validate only what the user typed; the no-key sentinel is applied afterwards (it contains
code = "88888888"; // non-alphanumeric characters, so it must not go through this check).
for (char c : code) { for (char c : code) {
if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) { if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) {
show_error(this, _L("Invalid input")); show_error(this, _L("Invalid input"));
return; return;
} }
} }
if (code.empty())
code = NO_API_KEY_SENTINEL;
if (m_obj) { if (m_obj) {
m_obj->set_access_code(code.ToStdString()); m_obj->set_access_code(code.ToStdString());
} }
+9 -2
View File
@@ -1798,7 +1798,8 @@ int MachineObject::command_ams_calibrate(int ams_id)
return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer())); return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
} }
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max) int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type,
int nozzle_temp_min, int nozzle_temp_max, const std::vector<std::string>& tray_colors, int tray_ctype)
{ {
int tag_tray_id = 0; int tag_tray_id = 0;
int tag_ams_id = ams_id; int tag_ams_id = ams_id;
@@ -1827,6 +1828,10 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
j["print"]["nozzle_temp_min"] = nozzle_temp_min; j["print"]["nozzle_temp_min"] = nozzle_temp_min;
j["print"]["nozzle_temp_max"] = nozzle_temp_max; j["print"]["nozzle_temp_max"] = nozzle_temp_max;
j["print"]["tray_type"] = tray_type; j["print"]["tray_type"] = tray_type;
if (!tray_colors.empty()) {
j["print"]["cols"] = tray_colors;
j["print"]["ctype"] = tray_ctype;
}
return this->publish_json(j); return this->publish_json(j);
} }
@@ -2680,7 +2685,8 @@ int MachineObject::connect()
{ {
if (m_access_revoked || get_dev_ip().empty()) return -1; if (m_access_revoked || get_dev_ip().empty()) return -1;
std::string username = m_agent ? m_agent->default_lan_username() : std::string(); std::string username = m_agent ? m_agent->default_lan_username() : std::string();
std::string password = get_access_code(); // The stored access code may be the "no API key" sentinel; agents expect an empty credential.
std::string password = normalize_access_code(get_access_code());
std::string port; std::string port;
std::string input = get_dev_ip(); std::string input = get_dev_ip();
@@ -5478,6 +5484,7 @@ void MachineObject::parse_new_info(json print)
is_support_pa_mode = (get_flag_bits_no_border(fun2, 3) == 1); is_support_pa_mode = (get_flag_bits_no_border(fun2, 3) == 1);
is_support_remote_dry = (get_flag_bits_no_border(fun2, 5) == 1); is_support_remote_dry = (get_flag_bits_no_border(fun2, 5) == 1);
is_support_check_track_switch_match_slice_printer = get_flag_bits_no_border(fun2, 19) == 1; is_support_check_track_switch_match_slice_printer = get_flag_bits_no_border(fun2, 19) == 1;
is_support_filament_manual_multi_color = (get_flag_bits_no_border(fun2, 23) == 1);
if (DevPrinterConfigUtil::support_print_check_firmware_for_tpu_left(printer_type)) { if (DevPrinterConfigUtil::support_print_check_firmware_for_tpu_left(printer_type)) {
m_firmware_support_print_tpu_left = get_flag_bits_no_border(fun2, 7) == 1; m_firmware_support_print_tpu_left = get_flag_bits_no_border(fun2, 7) == 1;
+2 -1
View File
@@ -674,6 +674,7 @@ public:
bool is_support_remote_dry = false; bool is_support_remote_dry = false;
bool is_support_check_track_switch_match_slice_printer{false}; bool is_support_check_track_switch_match_slice_printer{false};
bool is_support_pa_mode{false}; bool is_support_pa_mode{false};
bool is_support_filament_manual_multi_color{false};
std::optional<bool> m_firmware_support_print_tpu_left; std::optional<bool> m_firmware_support_print_tpu_left;
bool installed_upgrade_kit{false}; bool installed_upgrade_kit{false};
@@ -804,7 +805,7 @@ public:
int command_ams_switch_filament(bool switch_filament); int command_ams_switch_filament(bool switch_filament);
int command_ams_air_print_detect(bool air_print_detect); int command_ams_air_print_detect(bool air_print_detect);
int command_ams_calibrate(int ams_id); int command_ams_calibrate(int ams_id);
int command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max); int command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max, const std::vector<std::string>& tray_colors = {}, int tray_ctype = 2);
int command_ams_select_tray(std::string tray_id); int command_ams_select_tray(std::string tray_id);
int command_ams_refresh_rfid(int ams_id, int slot_id); int command_ams_refresh_rfid(int ams_id, int slot_id);
int command_ams_control(std::string action); int command_ams_control(std::string action);
+1 -1
View File
@@ -96,7 +96,7 @@ void FilamentColorCodeQuery::LoadFromLocal()
const auto& fila_color_strs = json_data_item["fila_color"].get<std::vector<wxString>>(); const auto& fila_color_strs = json_data_item["fila_color"].get<std::vector<wxString>>();
for (const auto& color_str : fila_color_strs) { for (const auto& color_str : fila_color_strs) {
if (color_str.size() > 3) /* Skip the value like "#0"*/{ if (color_str.size() > 3) /* Skip the value like "#0"*/{
fila_color.m_colors.emplace(wxColour(color_str)); fila_color.AddColor(wxColour(color_str));
} }
} }
} }
+11
View File
@@ -87,6 +87,17 @@ struct FilamentColor
public: public:
size_t ColorCount() const noexcept { return m_colors.size(); } size_t ColorCount() const noexcept { return m_colors.size(); }
// Preserve catalog endpoint order for display while retaining set-based lookup identity.
const std::vector<wxColour>& GetColors() const noexcept { return m_color_list; }
void AddColor(const wxColour& color)
{
if (m_colors.insert(color).second) m_color_list.push_back(color);
}
private:
std::vector<wxColour> m_color_list;
public:
void EndSet(int ctype) void EndSet(int ctype)
{ {
+15 -91
View File
@@ -65,6 +65,7 @@
#include <string_view> #include <string_view>
#include "slic3r/GUI/Selection.hpp" #include "slic3r/GUI/Selection.hpp"
#include "GLCanvas3D.hpp" #include "GLCanvas3D.hpp"
#include "SequentialPrintClearance.hpp"
#ifdef SLIC3R_CAD #ifdef SLIC3R_CAD
#include "slic3r/GUI/CAD/DesignSketchTool.hpp" // Design tab: interactive 2D sketch tool #include "slic3r/GUI/CAD/DesignSketchTool.hpp" // Design tab: interactive 2D sketch tool
#endif #endif
@@ -6397,17 +6398,16 @@ void GLCanvas3D::update_sequential_clearance()
//BBS: add the height logic //BBS: add the height logic
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
Polygons polygons; Polygons polygons;
std::vector<std::pair<Polygon, float>> height_polygons;
polygons.reserve(instances_count); polygons.reserve(instances_count);
height_polygons.reserve(instances_count); std::vector<SequentialClearanceInstance> convex_and_bounding_boxes;
std::vector<struct height_info> convex_and_bounding_boxes; convex_and_bounding_boxes.reserve(instances_count);
struct height_info std::map<ObjectID, int> print_order;
{ for (const PrintObject* print_object : fff_print()->objects())
double instance_height; for (const PrintInstance& instance : print_object->instances())
BoundingBox bounding_box; print_order.emplace(instance.model_instance->id(), instance.model_instance->arrange_order);
Polygon hull_polygon;
};
for (size_t i = 0; i < instance_transforms.size(); ++i) { for (size_t i = 0; i < instance_transforms.size(); ++i) {
const size_t first_instance = convex_and_bounding_boxes.size();
const auto& instances = instance_transforms[i]; const auto& instances = instance_transforms[i];
double rotation_z0 = instances.front()->get_rotation().z(); double rotation_z0 = instances.front()->get_rotation().z();
int index = 0; int index = 0;
@@ -6427,96 +6427,20 @@ void GLCanvas3D::update_sequential_clearance()
Polygon convex_hull(std::move(inst_pts)); Polygon convex_hull(std::move(inst_pts));
BoundingBox bouding_box = convex_hull.bounding_box(); BoundingBox bouding_box = convex_hull.bounding_box();
BoundingBox plate_bb = plate->get_bounding_box_crd(); BoundingBox plate_bb = plate->get_bounding_box_crd();
const ObjectID instance_id = m_model->objects[i]->instances[index]->id();
double instance_height = m_model->objects[i]->get_instance_max_z(index++); double instance_height = m_model->objects[i]->get_instance_max_z(index++);
//skip the object for not current plate //skip the object for not current plate
if (!plate_bb.overlap(bouding_box)) if (!plate_bb.overlap(bouding_box))
continue; continue;
convex_and_bounding_boxes.push_back({instance_height, bouding_box, convex_hull}); convex_and_bounding_boxes.push_back({instance_height, bouding_box, convex_hull, instance_id});
polygons.emplace_back(std::move(convex_hull)); polygons.emplace_back(std::move(convex_hull));
} }
sort_sequential_clearance_instances(convex_and_bounding_boxes.begin() + first_instance, convex_and_bounding_boxes.end(), print_order);
} }
//sort the print instance const auto& config = fff_print()->config();
std::sort(convex_and_bounding_boxes.begin(), convex_and_bounding_boxes.end(), const auto height_polygons = sequential_clearance_height_polygons(convex_and_bounding_boxes, config.printable_height,
[](auto &l, auto &r) { config.extruder_clearance_height_to_lid, config.extruder_clearance_height_to_rod);
auto ly1 = l.bounding_box.min.y();
auto ly2 = l.bounding_box.max.y();
auto ry1 = r.bounding_box.min.y();
auto ry2 = r.bounding_box.max.y();
auto inter_min = std::max(ly1, ry1);
auto inter_max = std::min(ly2, ry2);
auto lx = l.bounding_box.min.x();
auto rx = r.bounding_box.min.x();
if (inter_max - inter_min > 0)
return (lx < rx) || ((lx == rx)&&(ly1 < ry1));
else
return (ly1 < ry1);
});
/*bool has_interlaced_objects = false;
for (int k = 0; k < bounding_box_count; k++)
{
Polygon& convex = convex_and_bounding_boxes[k].hull_polygon;
BoundingBox& bbox = convex_and_bounding_boxes[k].bounding_box;
auto iy1 = bbox.min.y();
auto iy2 = bbox.max.y();
for (int i = k+1; i < bounding_box_count; i++)
{
Polygon& next_convex = convex_and_bounding_boxes[i].hull_polygon;
BoundingBox& next_bbox = convex_and_bounding_boxes[i].bounding_box;
auto py1 = next_bbox.min.y();
auto py2 = next_bbox.max.y();
auto inter_min = std::max(iy1, py1); // min y of intersection
auto inter_max = std::min(iy2, py2); // max y of intersection. length=max_y-min_y>0 means intersection exists
if (inter_max - inter_min > 0) {
has_interlaced_objects = true;
break;
}
}
if (has_interlaced_objects)
break;
}*/
int bounding_box_count = convex_and_bounding_boxes.size();
double printable_height = fff_print()->config().printable_height;
double hc1 = fff_print()->config().extruder_clearance_height_to_lid;
double hc2 = fff_print()->config().extruder_clearance_height_to_rod;
for (int k = 0; k < bounding_box_count; k++)
{
Polygon& convex = convex_and_bounding_boxes[k].hull_polygon;
BoundingBox& bbox = convex_and_bounding_boxes[k].bounding_box;
auto iy1 = bbox.min.y();
auto iy2 = bbox.max.y();
double height = (k == (bounding_box_count - 1))?printable_height:hc1;
/*if (has_interlaced_objects) {
if ((k < (bounding_box_count - 1)) && (convex_and_bounding_boxes[k].instance_height > hc2)) {
height_polygons.emplace_back(std::make_pair(convex, hc2));
}
}
else {
if ((k < (bounding_box_count - 1)) && (convex_and_bounding_boxes[k].instance_height > hc1)) {
height_polygons.emplace_back(std::make_pair(convex, hc1));
}
}*/
for (int i = k+1; i < bounding_box_count; i++)
{
Polygon& next_convex = convex_and_bounding_boxes[i].hull_polygon;
BoundingBox& next_bbox = convex_and_bounding_boxes[i].bounding_box;
auto py1 = next_bbox.min.y();
auto py2 = next_bbox.max.y();
auto inter_min = std::max(iy1, py1); // min y of intersection
auto inter_max = std::min(iy2, py2); // max y of intersection. length=max_y-min_y>0 means intersection exists
if (inter_max - inter_min > 0) {
height = hc2;
break;
}
}
if (height < convex_and_bounding_boxes[k].instance_height)
height_polygons.emplace_back(std::make_pair(convex, height));
}
// sends instances 2d hulls to be rendered // sends instances 2d hulls to be rendered
set_sequential_print_clearance_visible(true); set_sequential_print_clearance_visible(true);
+113 -15
View File
@@ -252,9 +252,11 @@
#include "slic3r/GUI/ParamsDialog.hpp" #include "slic3r/GUI/ParamsDialog.hpp"
#include "slic3r/GUI/ParamsPanel.hpp" #include "slic3r/GUI/ParamsPanel.hpp"
#include "slic3r/GUI/PartPlate.hpp" #include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/TextureLibrary.hpp"
#include "slic3r/GUI/Widgets/Button.hpp" #include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/wxExtensions.hpp" #include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/plugin/host/PluginPages.hpp" #include "slic3r/plugin/host/PluginPages.hpp"
#include "slic3r/plugin/host/PluginResourceTextures.hpp"
#include <wx/defs.h> #include <wx/defs.h>
#include "slic3r/GUI/Widgets/WebView.hpp" #include "slic3r/GUI/Widgets/WebView.hpp"
#include <cwchar> #include <cwchar>
@@ -333,6 +335,23 @@ static std::shared_ptr<OrcaCloudServiceAgent> get_orca_cloud_agent(NetworkAgent*
return agent ? std::dynamic_pointer_cast<OrcaCloudServiceAgent>(agent->get_cloud_agent()) : nullptr; return agent ? std::dynamic_pointer_cast<OrcaCloudServiceAgent>(agent->get_cloud_agent()) : nullptr;
} }
// Full {web} page URL for a same-site OrcaCloud path, or empty when it cannot be built. An already
// open cloud pane is navigated straight to it, without re-running the session handoff.
static wxString cloud_page_url(NetworkAgent* agent, const std::string& path)
{
if (path.empty())
return {};
auto orca = get_orca_cloud_agent(agent);
if (!orca)
return {};
std::string base = orca->get_cloud_base_url();
while (!base.empty() && base.back() == '/')
base.pop_back();
if (base.empty())
return {};
return from_u8(base + path);
}
void start_ping_test() void start_ping_test()
{ {
return; return;
@@ -3063,6 +3082,37 @@ void GUI_App::init_plugin_gui_wiring()
refresh_plugins_dialog(); refresh_plugins_dialog();
switch_printer_agent_after_unload(capability.plugin_key); switch_printer_agent_after_unload(capability.plugin_key);
}); });
plugin_mgr.subscribe_on_load_callback([](const std::string& plugin_key) {
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_plugin_register(plugin_key);
Slic3r::GUI::bump_texture_library_generation();
});
plugin_mgr.subscribe_on_unload_callback([](const std::string& plugin_key) {
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_plugin_deregister(plugin_key);
Slic3r::GUI::bump_texture_library_generation();
});
plugin_mgr.subscribe_on_capability_load_callback([](const PluginCapabilityId& capability) {
if (capability.type != PluginCapabilityType::ResourceTexture)
return;
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_cap_register(capability);
Slic3r::GUI::bump_texture_library_generation();
});
plugin_mgr.subscribe_on_capability_unload_callback([](const PluginCapabilityId& capability) {
if (capability.type != PluginCapabilityType::ResourceTexture)
return;
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_cap_deregister(capability);
Slic3r::GUI::bump_texture_library_generation();
});
// Packs already loaded before this wiring, or in tests where no callback fires.
PluginResourceTextures::instance().initial_sweep();
Slic3r::GUI::bump_texture_library_generation();
} }
bool GUI_App::on_init_inner() bool GUI_App::on_init_inner()
@@ -4304,6 +4354,11 @@ void GUI_App::select_machine(const std::string& agent_id)
machine.dev_ip = dev_id; machine.dev_ip = dev_id;
machine.dev_name = dev_id; machine.dev_name = dev_id;
machine.printer_type = preset.config.opt_string("printer_model"); machine.printer_type = preset.config.opt_string("printer_model");
auto access_code = preset.config.opt_string("printhost_apikey");
// Orca expect non empty access code
if (access_code.empty()) {
access_code = NO_API_KEY_SENTINEL;
}
existing = m_device_manager->insert_local_device( existing = m_device_manager->insert_local_device(
machine, "lan", "free", "", access_code); machine, "lan", "free", "", access_code);
@@ -5434,12 +5489,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
this->request_open_project({}); this->request_open_project({});
} }
else if (command_str.compare("homepage_open_cloud") == 0) { else if (command_str.compare("homepage_open_cloud") == 0) {
// Deferred: set_cloud_mode() may create a second native webview, which must this->open_orca_cloud();
// not run inside this webview's script-message callback.
CallAfter([this]() {
if (WebViewPanel* home = WebViewPanel::if_built())
home->set_cloud_mode(true, cloud_webview_handoff());
});
} }
else if (command_str.compare("homepage_close_cloud") == 0) { else if (command_str.compare("homepage_close_cloud") == 0) {
CallAfter([]() { CallAfter([]() {
@@ -5588,6 +5638,22 @@ std::string GUI_App::handle_web_request(std::string cmd)
return ""; return "";
} }
void GUI_App::open_orca_cloud(const std::string& next)
{
// Remembered so a login while the pane is open re-seeds the same page rather than the default.
m_cloud_open_path = next;
// Deferred: set_cloud_mode() may create the second native webview, so this must not run inside
// the caller's frame (a webview script-message callback or an ImGui frame).
CallAfter([this]() {
if (mainframe != nullptr)
mainframe->select_tab(TAB_ID_HOME);
// The Home page is built lazily, so the tab switch may not have created it yet: ensure() builds
// it if needed, matching what showing the tab does, rather than dropping the open request.
if (WebViewPanel* home = WebViewPanel::ensure())
home->set_cloud_mode(true, cloud_webview_handoff(m_cloud_open_path), cloud_page_url(m_agent, m_cloud_open_path));
});
}
void GUI_App::handle_script_message(std::string msg, const std::string& provider) void GUI_App::handle_script_message(std::string msg, const std::string& provider)
{ {
try { try {
@@ -5865,7 +5931,7 @@ void GUI_App::on_update_machine_list(wxCommandEvent &evt)
}*/ }*/
} }
wxString GUI_App::cloud_webview_handoff() wxString GUI_App::cloud_webview_handoff(const std::string& next)
{ {
// The cloud pane loads the Start URL, not a token: the page checks the webview's own session // The cloud pane loads the Start URL, not a token: the page checks the webview's own session
// and asks for a handoff itself (guide §4). Signed out, user_id is omitted and the page will // and asks for a handoff itself (guide §4). Signed out, user_id is omitted and the page will
@@ -5873,7 +5939,7 @@ wxString GUI_App::cloud_webview_handoff()
auto orca = get_orca_cloud_agent(m_agent); auto orca = get_orca_cloud_agent(m_agent);
if (!orca) if (!orca)
return {}; return {};
return from_u8(orca->build_web_session_start_url()); return from_u8(orca->build_web_session_start_url(next));
} }
void GUI_App::request_cloud_web_session(const std::string& state) void GUI_App::request_cloud_web_session(const std::string& state)
@@ -6016,7 +6082,7 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
m_cloud_handoff_state.clear(); m_cloud_handoff_state.clear();
if (WebViewPanel* home = WebViewPanel::if_built()) if (WebViewPanel* home = WebViewPanel::if_built())
if (home->cloud_mode_active()) if (home->cloud_mode_active())
home->set_cloud_mode(true, cloud_webview_handoff()); home->set_cloud_mode(true, cloud_webview_handoff(m_cloud_open_path));
} }
@@ -6863,7 +6929,7 @@ void GUI_App::push_notification(const MachineObject* obj, wxString msg, wxStrin
} }
} }
void GUI_App::reload_settings() bool GUI_App::reload_settings()
{ {
if (preset_bundle && m_agent) { if (preset_bundle && m_agent) {
// Load user's personal presets // Load user's personal presets
@@ -6871,7 +6937,7 @@ void GUI_App::reload_settings()
int result = m_agent->get_user_presets(&user_presets); int result = m_agent->get_user_presets(&user_presets);
if (result != 0) { if (result != 0) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": get_user_presets failed with code " << result << ", skipping sync"; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": get_user_presets failed with code " << result << ", skipping sync";
return; return false;
} }
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " cloud user preset number is: " << user_presets.size(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " cloud user preset number is: " << user_presets.size();
auto refresh_synced_ui = [this, user_presets = std::move(user_presets)]() mutable { auto refresh_synced_ui = [this, user_presets = std::move(user_presets)]() mutable {
@@ -6946,13 +7012,30 @@ void GUI_App::reload_settings()
refresh_synced_ui(); refresh_synced_ui();
else else
CallAfter(refresh_synced_ui); CallAfter(refresh_synced_ui);
return true;
} }
return false;
} }
void GUI_App::check_legacy_user_filament_presets(bool by_user) void GUI_App::check_legacy_user_filament_presets(bool by_user)
{ {
// One check at a time: the startup check can come due while the manual one is open, and on macOS the menu bar
// stays usable while a modal dialog is up.
if (m_legacy_filament_check_running)
return;
if (!by_user && app_config->get_bool("legacy_user_filament_presets_dont_remind")) if (!by_user && app_config->get_bool("legacy_user_filament_presets_dont_remind"))
return; return;
// The local copies may be older than the cloud ones another machine changed: migrating them would push changes
// made against stale copies and conflict with the cloud. The startup check waits for the presets to be pulled.
if (!by_user && m_user_preset_pull_pending) {
m_legacy_filament_check_due = true;
return;
}
// This check also answers a startup check that is still waiting.
m_legacy_filament_check_due = false;
m_legacy_filament_check_running = true;
ScopeGuard running_guard([this] { m_legacy_filament_check_running = false; });
const std::vector<LegacyUserFilamentPreset> legacy_presets = preset_bundle->legacy_user_filament_presets(); const std::vector<LegacyUserFilamentPreset> legacy_presets = preset_bundle->legacy_user_filament_presets();
if (legacy_presets.empty()) { if (legacy_presets.empty()) {
if (by_user) if (by_user)
@@ -6997,6 +7080,9 @@ void GUI_App::check_legacy_user_filament_presets(bool by_user)
show_error(mainframe, format_wxstr(_L("Your filament presets could not be migrated. A backup of your presets was saved to:\n%1%"), backup)); show_error(mainframe, format_wxstr(_L("Your filament presets could not be migrated. A backup of your presets was saved to:\n%1%"), backup));
return; return;
} }
// Ask the sync loop to push on its next pass instead of its next 60 s round, which shortens the time the migrated
// presets stay unpushed.
m_sync_user_presets_now.store(true, std::memory_order_release);
preset_bundle->update_compatible(PresetSelectCompatibleType::Never); preset_bundle->update_compatible(PresetSelectCompatibleType::Never);
mainframe->update_side_preset_ui(); mainframe->update_side_preset_ui();
for (auto tab : tabs_list) { for (auto tab : tabs_list) {
@@ -7763,6 +7849,7 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
BOOST_LOG_TRIVIAL(info) << "start_sync_service..."; BOOST_LOG_TRIVIAL(info) << "start_sync_service...";
// BBS // BBS
m_user_sync_token.reset(new int(0)); m_user_sync_token.reset(new int(0));
m_user_preset_pull_pending = true;
if (with_progress_dlg) { if (with_progress_dlg) {
// Mark a manual progress dialog as active so restart_sync_user_preset() ignores // Mark a manual progress dialog as active so restart_sync_user_preset() ignores
// repeat triggers while it is on screen (prevents stacking modal dialogs). // repeat triggers while it is on screen (prevents stacking modal dialogs).
@@ -7836,8 +7923,16 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
finishFn(ret == 0); finishFn(ret == 0);
if (ret == 0 && m_agent && !t.expired()) // Queued after the refresh reload_settings() queues, so it runs once the pulled presets are loaded.
reload_settings(); if (ret == 0 && m_agent && !t.expired() && reload_settings())
CallAfter([this, t] {
// A stopped or restarted sync: the next sync, if one starts, pulls the presets again.
if (t.expired() || is_closing())
return;
m_user_preset_pull_pending = false;
if (m_legacy_filament_check_due)
check_legacy_user_filament_presets(false);
});
// For orca specific syncing // For orca specific syncing
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent()); auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent());
@@ -7852,8 +7947,11 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
// Sync once immediately, then every 60 seconds. // Sync once immediately, then every 60 seconds.
while (!t.expired()) { while (!t.expired()) {
++tick_tock; ++tick_tock;
// Sync once immediately, then every 60s, or right away when a force-push asked for it. // Sync once immediately, then every 60s, or right away when a force-push or a preset migration asked
if (tick_tock % 120 == 0 || m_sync_user_presets_now.exchange(false, std::memory_order_acq_rel)) { // for it. The request is consumed on every tick, so one made before a scheduled pass is served by it
// instead of running a second pass straight after.
const bool push_now = m_sync_user_presets_now.exchange(false, std::memory_order_acq_rel);
if (tick_tock % 120 == 0 || push_now) {
tick_tock = 0; tick_tock = 0;
if (m_agent) { if (m_agent) {
if (!m_agent->is_user_login()) { if (!m_agent->is_user_login()) {
+13 -3
View File
@@ -355,6 +355,9 @@ private:
std::atomic<bool> m_restart_sync_pending {false}; std::atomic<bool> m_restart_sync_pending {false};
std::atomic<bool> m_sync_user_preset_dlg_active {false}; // a manual "Sync Presets" progress dialog is on screen (see restart_sync_user_preset) std::atomic<bool> m_sync_user_preset_dlg_active {false}; // a manual "Sync Presets" progress dialog is on screen (see restart_sync_user_preset)
std::atomic<bool> m_sync_user_presets_now {false}; // request the sync loop to push user presets on its next tick std::atomic<bool> m_sync_user_presets_now {false}; // request the sync loop to push user presets on its next tick
bool m_user_preset_pull_pending {false}; // set when a sync thread starts, cleared once its pull is applied; a failed pull or a stopped sync leaves it set
bool m_legacy_filament_check_due {false}; // the startup check waits for that pull
bool m_legacy_filament_check_running {false}; // a check is in progress, its dialog may be open
std::atomic<bool> m_migration_retry_pending {false}; std::atomic<bool> m_migration_retry_pending {false};
bool m_is_dark_mode{ false }; bool m_is_dark_mode{ false };
bool m_adding_script_handler { false }; bool m_adding_script_handler { false };
@@ -365,6 +368,8 @@ private:
// and a generation counter so a superseded mint/login result is dropped (guide §5, "only the // and a generation counter so a superseded mint/login result is dropped (guide §5, "only the
// newest request counts"). // newest request counts").
std::string m_cloud_handoff_state; std::string m_cloud_handoff_state;
// The OrcaCloud path the open requests target, remembered so a login re-seed keeps the same page.
std::string m_cloud_open_path;
uint64_t m_cloud_handoff_gen { 0 }; uint64_t m_cloud_handoff_gen { 0 };
bool m_show_error_msgdlg{false}; bool m_show_error_msgdlg{false};
wxString m_info_dialog_content; wxString m_info_dialog_content;
@@ -466,6 +471,9 @@ public:
#if wxUSE_WEBVIEW_EDGE #if wxUSE_WEBVIEW_EDGE
void init_webview_runtime(); void init_webview_runtime();
#endif #endif
// Switches to the Home tab and opens the OrcaCloud pane beside the homepage on `next`, a
// same-site OrcaCloud path (empty = the page's default). Work is deferred internally.
void open_orca_cloud(const std::string& next = std::string());
static unsigned get_colour_approx_luma(const wxColour& colour); static unsigned get_colour_approx_luma(const wxColour& colour);
static bool dark_mode(); static bool dark_mode();
const wxColour get_label_default_clr_system(); const wxColour get_label_default_clr_system();
@@ -572,7 +580,7 @@ public:
// called from the pane when the page asks for a session; it opens the desktop login dialog if // called from the pane when the page asks for a session; it opens the desktop login dialog if
// signed out, otherwise mints a one-time token on a worker thread and loads the Complete URL // signed out, otherwise mints a one-time token on a worker thread and loads the Complete URL
// in the same webview. // in the same webview.
wxString cloud_webview_handoff(); wxString cloud_webview_handoff(const std::string& next = std::string());
void request_cloud_web_session(const std::string& state); void request_cloud_web_session(const std::string& state);
void mint_cloud_web_session(const std::string& state, uint64_t gen); void mint_cloud_web_session(const std::string& state, uint64_t gen);
void on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash); void on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash);
@@ -589,7 +597,8 @@ public:
void check_new_version_sf(bool show_tips = false, int by_user = 0); void check_new_version_sf(bool show_tips = false, int by_user = 0);
// Offers to migrate user filament presets made against an older release that lost printers their parent // Offers to migrate user filament presets made against an older release that lost printers their parent
// listed then. At startup it asks on every start that finds such presets, unless the user chose not to be // listed then. At startup it asks on every start that finds such presets, unless the user chose not to be
// reminded; by_user also reports when there is nothing to migrate. // reminded; while the user presets sync with the cloud, it waits until they have been pulled. by_user also
// reports when there is nothing to migrate.
void check_legacy_user_filament_presets(bool by_user); void check_legacy_user_filament_presets(bool by_user);
bool process_network_msg(std::string dev_id, std::string msg); bool process_network_msg(std::string dev_id, std::string msg);
void request_new_version(int by_user); void request_new_version(int by_user);
@@ -600,7 +609,8 @@ public:
std::string format_IP(const std::string& ip); std::string format_IP(const std::string& ip);
void show_dialog(wxString msg); void show_dialog(wxString msg);
void push_notification(const MachineObject* obj, wxString msg, wxString title = wxEmptyString, UserNotificationStyle style = UserNotificationStyle::UNS_NORMAL); void push_notification(const MachineObject* obj, wxString msg, wxString title = wxEmptyString, UserNotificationStyle style = UserNotificationStyle::UNS_NORMAL);
void reload_settings(); // Returns false when the cloud presets could not be fetched, so nothing was reloaded.
bool reload_settings();
void remove_user_presets(); void remove_user_presets();
bool maybe_migrate_user_presets_on_login(); bool maybe_migrate_user_presets_on_login();
@@ -40,6 +40,7 @@
#include "slic3r/Utils/UndoRedo.hpp" #include "slic3r/Utils/UndoRedo.hpp"
#include "GLGizmoUtils.hpp" #include "GLGizmoUtils.hpp"
#include <cctype>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <functional> #include <functional>
@@ -72,6 +73,7 @@
#include <array> #include <array>
#include <cmath> #include <cmath>
#include <limits> #include <limits>
#include <map>
#include <numeric> #include <numeric>
#include <queue> #include <queue>
#include <set> #include <set>
@@ -97,6 +99,48 @@ namespace {
// bigger jump than the whole useful region. // bigger jump than the whole useful region.
constexpr float ImGuiLogSlider = float(ImGuiSliderFlags_Logarithmic); constexpr float ImGuiLogSlider = float(ImGuiSliderFlags_Logarithmic);
// The scrollbar of the panel's scrolling areas (the panel body and the texture library). ImGui's stock
// scrollbar is a wide, square-cornered slab in a tinted track - against the flat panel it reads as a raw
// widget bolted onto the edge - so it is slimmed to a rounded thumb over an invisible track. The child
// background goes transparent with it, so the area reads as part of the panel.
void push_slim_scrollbar_style(float width, bool dark)
{
const ImVec4 grab = dark ? ImVec4(1.f, 1.f, 1.f, 0.26f) : ImVec4(0.f, 0.f, 0.f, 0.26f);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, width);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.5f * width);
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(grab.x, grab.y, grab.z, 0.45f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(grab.x, grab.y, grab.z, 0.65f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.f, 0.f, 0.f, 0.f));
}
void pop_slim_scrollbar_style()
{
ImGui::PopStyleColor(5);
ImGui::PopStyleVar(2);
}
// Cuts a UTF-8 string down to `max_w` pixels, ending it in "..." when it had to be cut.
std::string ellipsize(std::string s, float max_w, float font_size = 0.f)
{
const auto text_width = [&](const std::string &text) {
return font_size > 0.f
? ImGui::GetFont()->CalcTextSizeA(font_size, std::numeric_limits<float>::max(), 0.f, text.c_str()).x
: ImGui::CalcTextSize(text.c_str()).x;
};
if (text_width(s) <= max_w)
return s;
while (!s.empty() && text_width(s + "...") > max_w) {
unsigned char c;
do { // a whole code point at a time
c = static_cast<unsigned char>(s.back());
s.pop_back();
} while (!s.empty() && (c & 0xC0) == 0x80);
}
return s + "...";
}
// Uploads decoded 8-bit grayscale height data as an RGBA GPU texture for display in the panel. // Uploads decoded 8-bit grayscale height data as an RGBA GPU texture for display in the panel.
// Shared by the per-layer thumbnail and the picker's library entries, which differ only in where // Shared by the per-layer thumbnail and the picker's library entries, which differ only in where
// their pixels came from. // their pixels came from.
@@ -130,6 +174,10 @@ constexpr int HEIGHT_TEX_MAX_PX = 2048;
// The full-size image is only ever adjusted once, on commit. // The full-size image is only ever adjusted once, on commit.
constexpr int ADJUST_PREVIEW_MAX_PX = 384; constexpr int ADJUST_PREVIEW_MAX_PX = 384;
// Library textures the texture picker decodes per frame. Each decode runs on the GUI thread, so the
// rest of the visible tiles fill in over the following frames instead of in one stall.
constexpr int PICKER_LOADS_PER_FRAME = 2;
// How many rings of vertex-adjacent triangles either side of the paint's edge join the border refine // How many rings of vertex-adjacent triangles either side of the paint's edge join the border refine
// band (see collect_paint_region()). Two is enough to grade the size change without the band's own // band (see collect_paint_region()). Two is enough to grade the size change without the band's own
// cost growing to matter: it is a ring around a perimeter, not an area. // cost growing to matter: it is a ring around a perimeter, not an area.
@@ -2921,6 +2969,7 @@ void GLGizmoTextureDisplacement::run_uv_command(int cmd, float value)
// The same texture library the layer card opens; the panel renders it this frame. // The same texture library the layer card opens; the panel renders it this frame.
m_picker_slot = m_active_layer_slot; m_picker_slot = m_active_layer_slot;
m_picker_open_request = true; m_picker_open_request = true;
m_picker_reveal_selected = true;
} else if (cmd == int(Command::Unwrap)) { } else if (cmd == int(Command::Unwrap)) {
// The solve runs only on this - painting, the seam angle and seam marking never trigger it. // The solve runs only on this - painting, the seam angle and seam marking never trigger it.
m_uv_unwrap_pending = true; m_uv_unwrap_pending = true;
@@ -4073,7 +4122,7 @@ const GLGizmoTextureDisplacement::LibraryTexture *GLGizmoTextureDisplacement::ge
if (entry.image_data) { if (entry.image_data) {
TextureDisplacementLayer probe; TextureDisplacementLayer probe;
probe.image_data = entry.image_data; probe.image_data = entry.image_data;
entry.thumbnail = upload_height_thumbnail(decode_height_texture(probe)); entry.thumbnail = upload_height_thumbnail(decode_height_texture(probe, /* keep_decoded */ false));
if (!entry.thumbnail) if (!entry.thumbnail)
entry.image_data.reset(); // decoded to nothing usable - treat it as a failed load entry.image_data.reset(); // decoded to nothing usable - treat it as a failed load
} else { } else {
@@ -4102,7 +4151,7 @@ void GLGizmoTextureDisplacement::set_layer_texture(TextureDisplacementLayer &lay
// the PNG the first time the layer is previewed or baked. // the PNG the first time the layer is previewed or baked.
layer.image_data = tex->image_data; layer.image_data = tex->image_data;
// The user's own textures are listed most recently used first. // The user's own textures are listed most recently used first.
if (entry.is_user) if (entry.source == TextureSource::User)
touch_user_texture(entry.path); touch_user_texture(entry.path);
rebuild_preview(); rebuild_preview();
@@ -4129,6 +4178,40 @@ void GLGizmoTextureDisplacement::import_custom_texture(TextureDisplacementLayer
set_layer_texture(layer, *entry); set_layer_texture(layer, *entry);
} }
void GLGizmoTextureDisplacement::ease_wheel_scroll(float &target, float &applied)
{
ImGuiIO &io = ImGui::GetIO();
const float scroll_now = ImGui::GetScrollY();
const float scroll_max = ImGui::GetScrollMaxY();
// Anything that moved the scroll without us - dragging the grab, a keyboard/gamepad nav step, the
// content shrinking under a clamped offset, a SetScroll*() call - has to re-seed the target, or the
// easing below would immediately drag the view back to where it last animated to.
if (applied < 0.f || std::abs(scroll_now - applied) > 0.5f)
target = scroll_now;
if (io.MouseWheel != 0.f && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows))
target -= io.MouseWheel * ImGui::GetFontSize() * 4.f;
// Whole pixels: ImGui floors whatever SetScrollY() is given, so a fractional target could never be
// reached and the "still gliding" test below would stay true forever, repainting the canvas for good.
target = std::floor(std::clamp(target, 0.f, scroll_max));
const float delta = target - scroll_now;
if (std::abs(delta) >= 1.f) {
// Exponential ease, formulated against the frame time so the glide takes the same wall time
// whether the canvas is running at 30 or 144 fps. The last sub-pixel step would be floored
// away, so land on the target outright once the remainder is that small.
const float t = 1.f - std::exp(-20.f * std::clamp(io.DeltaTime, 1.f / 240.f, 1.f / 15.f));
const float step = (std::abs(delta * t) < 1.f) ? delta : delta * t;
const float next = std::floor(scroll_now + step);
ImGui::SetScrollY(next);
applied = next;
m_parent.set_as_dirty(); // nothing else would redraw mid-glide once the mouse stops
} else {
applied = scroll_now;
}
}
void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max) void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max)
{ {
ModelVolume *mv = texture_volume(); ModelVolume *mv = texture_volume();
@@ -4149,13 +4232,51 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
const float spacing = std::round(m_imgui->scaled(0.4f)); const float spacing = std::round(m_imgui->scaled(0.4f));
const float grid_w = 4.f * tile + 3.f * spacing; const float grid_w = 4.f * tile + 3.f * spacing;
const float gap = m_imgui->scaled(0.5f); const float gap = m_imgui->scaled(0.5f);
const float scrollbar_w = m_imgui->scaled(0.5f);
const float ring_w = std::max(2.f, std::round(m_imgui->scaled(0.1f))); // the current texture's ring
const float frame_h = ImGui::GetFrameHeight();
const ImVec4 orca = ImGuiWrapper::COL_ORCA;
const ImGuiStyle &style = ImGui::GetStyle();
// A fixed size rather than a fit-to-content one. The width is the four-column grid, a gutter and the
// slim scrollbar beside it, plus the window padding, so the columns always fit and no horizontal
// scrollbar is needed; it derives from scaled values, so it tracks the DPI like the rest.
const float fixed_w = grid_w + spacing + scrollbar_w + 2.f * style.WindowPadding.x;
// BeginPopup() ORs in ImGuiWindowFlags_AlwaysAutoResize (deps_src/imgui/imgui.cpp:8371) and no flag
// turns it back off. A SetNextWindowSize() overrides it for each axis it sets (imgui.cpp:5983-5996),
// so an explicit size is the only way to fix the popup's size; the grid child below then scrolls.
//
// Top-aligned with the panel, which keeps it clear of the gizmo toolbar above a docked panel, and as
// tall as the panel or 70% of the window, whichever is taller, while keeping a gap inside the window.
// The height follows the window and the panel, never the content, so it does not jump as packs load or
// sections collapse.
const ImGuiViewport *viewport = ImGui::GetMainViewport();
const float vp_top = viewport->Pos.y + gap;
const float vp_bottom = viewport->Pos.y + viewport->Size.y - gap;
const float height = std::max(std::min(std::max(0.70f * viewport->Size.y, panel_max.y - panel_min.y), vp_bottom - vp_top),
2.f * frame_h);
const float popup_y = std::max(vp_top, std::min(panel_min.y, vp_bottom - height));
// Beside the panel rather than over it, so the layer being retextured stays in view: to its left when // Beside the panel rather than over it, so the layer being retextured stays in view: to its left when
// there is room (the docked panel sits against the right edge of the canvas), otherwise to its right. // there is room (the docked panel sits against the right edge of the canvas), otherwise to its right.
const float popup_w = grid_w + 2.f * ImGui::GetStyle().WindowPadding.x; const float width = viewport->Size.x > 0.f ? std::min(fixed_w, viewport->Size.x) : fixed_w;
const bool left = panel_min.x - gap - popup_w >= 0.f; const bool left = panel_min.x - gap - width >= 0.f;
ImGui::SetNextWindowPos(ImVec2(left ? panel_min.x - gap : panel_max.x + gap, panel_min.y + m_imgui->scaled(4.f)), const float popup_x = left ? panel_min.x - gap - width : panel_max.x + gap;
ImGuiCond_Appearing, ImVec2(left ? 1.f : 0.f, 0.f)); // Cond_Always, not Appearing: a viewport resize while the popup is open must move and resize it too.
if (!ImGui::BeginPopup(popup_id)) ImGui::SetNextWindowPos(ImVec2(popup_x, popup_y), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Always);
// Drawn as the panel's own sheet: its opaque background and rounding, no border. A popup is filled with
// PopupBg, which the panel's toolbar style leaves at ImGui's translucent near-black - a dark window
// beside a light panel, with whatever it covers showing through. Only Begin reads these, so they are
// popped at once and the tooltips shown from inside keep their own look.
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg));
ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, style.WindowRounding);
ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 0.f);
// The window itself never scrolls; the grid child below does.
const bool open = ImGui::BeginPopup(popup_id, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
if (!open)
return; return;
if (layer == nullptr) { if (layer == nullptr) {
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
@@ -4171,16 +4292,51 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
m_tooltip_anchor_max = ImVec2(m_tooltip_anchor_min.x + ImGui::GetWindowSize().x, m_tooltip_anchor_max = ImVec2(m_tooltip_anchor_min.x + ImGui::GetWindowSize().x,
m_tooltip_anchor_min.y + ImGui::GetWindowSize().y); m_tooltip_anchor_min.y + ImGui::GetWindowSize().y);
m_imgui->push_bold_font(); // The header stays put while the grid scrolls under it: the title says what the popup is for, and the
m_imgui->text(_L("Choose a texture")); // close button is there for anyone who does not know that a click outside closes it.
m_imgui->pop_bold_font(); bool close_clicked = false;
{
ImDrawList *hdl = ImGui::GetWindowDrawList();
const ImVec2 row = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos(ImVec2(row.x, row.y + 0.5f * (frame_h - ImGui::GetTextLineHeight())));
m_imgui->bold_text(_u8L("Choose a texture"));
const ImVec2 btn(ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x - frame_h, row.y);
ImGui::SetCursorScreenPos(btn);
close_clicked = ImGui::InvisibleButton("##close", ImVec2(frame_h, frame_h));
const bool hot = ImGui::IsItemHovered();
const ImVec2 c(btn.x + 0.5f * frame_h, btn.y + 0.5f * frame_h);
const float k = std::round(0.18f * frame_h);
const float line_w = std::max(1.f, std::round(m_imgui->scaled(0.08f)));
const ImU32 x_col = ImGui::GetColorU32(ImGuiCol_Text, hot ? 1.f : 0.6f);
if (hot)
hdl->AddCircleFilled(c, 0.5f * frame_h, ImGui::GetColorU32(ImGuiCol_Text, 0.08f));
hdl->AddLine(ImVec2(c.x - k, c.y - k), ImVec2(c.x + k, c.y + k), x_col, line_w);
hdl->AddLine(ImVec2(c.x - k, c.y + k), ImVec2(c.x + k, c.y - k), x_col, line_w);
ImGui::SetCursorScreenPos(ImVec2(row.x, row.y + frame_h + style.ItemSpacing.y));
ImGui::Separator();
}
const std::vector<TextureLibraryEntry> &library = texture_library(); const std::vector<TextureLibraryEntry> &library = texture_library();
ImDrawList *dl = ImGui::GetWindowDrawList(); // A reinstalled pack can drop new bytes at a path already cached on this gizmo, and get_library_texture()
const ImU32 selected_col = ImGui::GetColorU32(ImGuiWrapper::COL_ORCA); // caches a failure as well as a success, so a moved generation makes every cached entry suspect.
if (const uint64_t generation = texture_library_generation(); generation != m_library_generation) {
m_library_textures.clear();
m_library_generation = generation;
}
// The grid scrolls as the panel body does: the same slim scrollbar, and the wheel eased by
// ease_wheel_scroll() instead of ImGui's five-line jumps (hence NoScrollWithMouse).
push_slim_scrollbar_style(scrollbar_w, wxGetApp().dark_mode());
ImGui::BeginChild("##texture_grid", ImVec2(0.f, 0.f), false, ImGuiWindowFlags_NoScrollWithMouse);
ease_wheel_scroll(m_picker_scroll_target, m_picker_scroll_applied);
ImDrawList *dl = ImGui::GetWindowDrawList(); // the child's, so tiles clip to the grid
std::optional<TextureLibraryEntry> chosen; std::optional<TextureLibraryEntry> chosen;
std::optional<TextureLibraryEntry> to_remove; std::optional<TextureLibraryEntry> to_remove;
bool import_clicked = false; bool import_clicked = false;
bool download_clicked = false;
int loads_left = PICKER_LOADS_PER_FRAME;
bool loads_deferred = false;
// Tiles are placed on the four-column grid explicitly rather than flowed with SameLine(): a user tile // Tiles are placed on the four-column grid explicitly rather than flowed with SameLine(): a user tile
// overlays a remove button on its corner, and that button would otherwise be "the previous item" the // overlays a remove button on its corner, and that button would otherwise be "the previous item" the
@@ -4196,16 +4352,34 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
ImGui::Dummy(ImVec2(grid_w, float(rows) * tile + float(rows - 1) * spacing)); ImGui::Dummy(ImVec2(grid_w, float(rows) * tile + float(rows - 1) * spacing));
} }
}; };
// One group's tiles: shipped textures alphabetically, or the user's own most recently used first (the // One section's tiles, in the order the section holds them (shipped alphabetically, pack textures by
// order texture_library() keeps), those with a remove button on hover. Returns how many it drew. // name, the user's own most recently used first - the order texture_library() produced); a user tile
const auto grid = [&](const ImVec2 &origin, bool user) { // carries a remove button on hover. Returns how many tiles it laid out, drawn or not.
const auto grid = [&](const ImVec2 &origin, const std::vector<const TextureLibraryEntry *> &entries, bool removable) {
int n = 0; int n = 0;
for (const TextureLibraryEntry &entry : library) { for (const TextureLibraryEntry *entry : entries) {
if (entry.is_user != user) const ImVec2 rect_min = tile_pos(origin, n++);
const ImVec2 rect_max = ImVec2(rect_min.x + tile, rect_min.y + tile);
const bool current = entry->path == layer->path;
// The selected tile is centred even when it is off screen, so it is drawn and loaded next frame.
if (current && m_picker_reveal_selected) {
ImGui::SetScrollFromPosY(0.5f * (rect_min.y + rect_max.y) - ImGui::GetWindowPos().y, 0.5f);
m_parent.request_extra_frame();
}
if (!ImGui::IsRectVisible(rect_min, rect_max))
continue; continue;
ImGui::SetCursorScreenPos(tile_pos(origin, n++)); // Thumbnails decode on this thread, so only a few per frame; the rest fill in on later frames.
ImGui::PushID(entry.path.c_str()); const bool cached = m_library_textures.count(entry->path) != 0;
const LibraryTexture *tex = get_library_texture(entry.path); if (!cached && loads_left == 0) {
loads_deferred = true;
dl->AddRectFilled(rect_min, rect_max, ImGui::GetColorU32(ImGuiCol_FrameBg), style.FrameRounding);
continue;
}
if (!cached)
--loads_left;
ImGui::SetCursorScreenPos(rect_min);
ImGui::PushID(entry->path.c_str());
const LibraryTexture *tex = get_library_texture(entry->path);
GLTexture *thumb = tex != nullptr ? tex->thumbnail.get() : nullptr; GLTexture *thumb = tex != nullptr ? tex->thumbnail.get() : nullptr;
bool clicked = false; bool clicked = false;
if (thumb != nullptr) { if (thumb != nullptr) {
@@ -4223,15 +4397,23 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
} else { } else {
clicked = ImGui::Button("?", ImVec2(tile, tile)); clicked = ImGui::Button("?", ImVec2(tile, tile));
} }
if (user) if (removable)
ImGui::SetItemAllowOverlap(); // the remove button drawn over its corner takes its own clicks ImGui::SetItemAllowOverlap(); // the remove button drawn over its corner takes its own clicks
const ImVec2 a = ImGui::GetItemRectMin(), b = ImGui::GetItemRectMax(); const ImVec2 a = ImGui::GetItemRectMin(), b = ImGui::GetItemRectMax();
if (entry.path == layer->path) const bool hovered = ImGui::IsItemHovered();
dl->AddRect(ImVec2(a.x - 1.f, a.y - 1.f), ImVec2(b.x + 1.f, b.y + 1.f), selected_col, 0.f, 0, 2.f); // The layer's current texture carries an accent ring, and the tile under the pointer a thinner,
if (ImGui::IsItemHovered()) // fainter one. Both are drawn inside the tile, where the grid's clip rect never cuts them.
panel_tooltip(entry.name); if (current || hovered) {
const float w = current ? ring_w : std::round(0.5f * ring_w);
dl->AddRect(ImVec2(a.x + 0.5f * w, a.y + 0.5f * w), ImVec2(b.x - 0.5f * w, b.y - 0.5f * w),
ImGui::GetColorU32(current ? orca : ImVec4(orca.x, orca.y, orca.z, 0.6f)), 0.f, 0, w);
}
if (hovered)
panel_tooltip(entry->name);
if (user && ImGui::IsMouseHoveringRect(a, b)) { // Clipped by the grid's clip rect, so a tile scrolled out of the grid or under its scrollbar
// does not offer its remove button. IsMouseHoveringRect() clips by default.
if (removable && ImGui::IsMouseHoveringRect(a, b)) {
const float r = std::round(tile * 0.17f); const float r = std::round(tile * 0.17f);
const ImVec2 c(b.x - r - 2.f, a.y + r + 2.f); const ImVec2 c(b.x - r - 2.f, a.y + r + 2.f);
ImGui::SetCursorScreenPos(ImVec2(c.x - r, c.y - r)); ImGui::SetCursorScreenPos(ImVec2(c.x - r, c.y - r));
@@ -4244,27 +4426,162 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
if (hot) if (hot)
panel_tooltip(_u8L("Remove from My textures")); panel_tooltip(_u8L("Remove from My textures"));
if (remove_clicked) if (remove_clicked)
to_remove = entry; to_remove = *entry;
} }
ImGui::PopID(); ImGui::PopID();
if (clicked) if (clicked)
chosen = entry; chosen = *entry;
} }
return n; return n;
}; };
ImGui::TextDisabled("%s", _u8L("Built-in").c_str()); // Every section collapses; the state is remembered for the app run. Pack keys use pack_name, a
// capability name unique per type across loaded packages and stable across reinstalls. This function-local
// static survives popup closes, never touches app_config, and resets on restart.
static std::map<std::string, bool> s_section_collapsed; // true = collapsed; absent = expanded
const auto split_pack_title = [](const std::string &title) -> std::pair<std::string, std::string> {
size_t split = std::string::npos;
for (size_t i = 0; i + 3 < title.size(); ++i)
if (title[i] == ' ' && (title[i + 1] == 'b' || title[i + 1] == 'B') &&
(title[i + 2] == 'y' || title[i + 2] == 'Y') && title[i + 3] == ' ')
split = i;
if (split == std::string::npos)
return { title, {} };
std::string name = title.substr(0, split);
while (!name.empty() && std::isspace(static_cast<unsigned char>(name.back())))
name.pop_back();
std::string attribution = title.substr(split);
size_t first = 0;
while (first < attribution.size() && std::isspace(static_cast<unsigned char>(attribution[first])))
++first;
attribution.erase(0, first);
if (name.empty() || attribution.empty())
return { title, {} };
return { name, attribution };
};
// A section heading, one row as wide as the grid: a click anywhere on it collapses or expands the
// section. Child headings indent their contents while keeping their counts right-aligned. Attribution
// lines use a smaller disabled style; the grid follows closer than the next heading does.
// Returns true when the section is open, so the caller knows whether to draw its contents.
const auto section_header = [&](const std::string &key, const std::string &title, size_t count, float indent,
const std::string &attribution, const std::string &full_title) {
bool &collapsed = s_section_collapsed[key]; // default-inserts false -> expanded
const ImVec2 row = ImGui::GetCursorScreenPos();
const bool two_lines = !attribution.empty();
const float line_h = ImGui::GetTextLineHeight();
const float small_font_size = ImGui::GetFontSize() * 0.8f;
const float small_line_h = line_h * 0.8f;
const float row_h = two_lines ? std::max(frame_h, line_h + small_line_h + spacing) : frame_h;
const ImVec2 row_max(row.x + grid_w, row.y + row_h);
if (ImGui::InvisibleButton(key.c_str(), ImVec2(grid_w, row_h)))
collapsed = !collapsed;
const bool hovered = ImGui::IsItemHovered();
if (hovered)
dl->AddRectFilled(row, row_max, ImGui::GetColorU32(ImGuiCol_Text, 0.05f), style.FrameRounding);
const float inset = std::round(0.25f * frame_h);
const float arrow = std::round(ImGui::GetFontSize() * 0.55f);
const float cx = row.x + inset + indent + 0.5f * arrow;
const float cy = row.y + 0.5f * row_h;
const float hs = 0.3f * arrow;
const ImU32 accent = ImGui::GetColorU32(orca);
if (collapsed)
dl->AddTriangleFilled(ImVec2(cx - 0.5f * hs, cy - hs), ImVec2(cx + 0.5f * hs, cy), ImVec2(cx - 0.5f * hs, cy + hs), accent);
else
dl->AddTriangleFilled(ImVec2(cx - hs, cy - 0.5f * hs), ImVec2(cx + hs, cy - 0.5f * hs), ImVec2(cx, cy + 0.5f * hs), accent);
const std::string n = std::to_string(count);
const float n_w = ImGui::CalcTextSize(n.c_str()).x;
const float tx = row.x + inset + indent + arrow + spacing;
const float max_text_w = std::max(0.f, row_max.x - inset - n_w - spacing - tx);
const std::string shown = ellipsize(title, max_text_w);
const std::string shown_attribution = two_lines ? ellipsize(attribution, max_text_w, small_font_size) : std::string{};
const float title_y = two_lines ? row.y + 0.5f * (row_h - line_h - small_line_h)
: row.y + 0.5f * (frame_h - line_h);
const float count_y = row.y + 0.5f * (row_h - line_h);
dl->AddText(ImVec2(tx, title_y), ImGui::GetColorU32(ImGuiCol_Text), shown.c_str());
if (two_lines)
dl->AddText(ImGui::GetFont(), small_font_size, ImVec2(tx, title_y + line_h),
ImGui::GetColorU32(ImGuiCol_TextDisabled), shown_attribution.c_str());
dl->AddText(ImVec2(row_max.x - inset - n_w, count_y), ImGui::GetColorU32(ImGuiCol_TextDisabled), n.c_str());
if (hovered && (shown != title || shown_attribution != attribution))
panel_tooltip(full_title); // a long plugin-supplied name, cut by the fixed width
ImGui::SetCursorScreenPos(ImVec2(row.x, row_max.y + spacing));
return !collapsed;
};
// Built-in is first, plugin packs are grouped under Texture libraries, and My
// textures stays last so its import tile and help line describe that group. Pack names identify both
// their child section and state.
struct Section
{ {
std::string key;
std::string title;
std::vector<const TextureLibraryEntry *> entries;
};
std::vector<Section> sections;
std::map<std::string, size_t> pack_section; // pack_name -> index in `sections`
sections.push_back({ "builtin:", _u8L("Built-in"), {} });
for (const TextureLibraryEntry &entry : library) {
if (entry.source == TextureSource::Shipped) {
sections.front().entries.push_back(&entry); // the section exists even when the library is empty
continue;
}
if (entry.source == TextureSource::User)
continue; // added below, so My textures stays last
if (entry.source != TextureSource::Plugin)
continue;
const auto [it, inserted] = pack_section.emplace(entry.pack_name, sections.size());
if (inserted)
sections.push_back({ "pack:" + entry.pack_name, entry.pack_name, {} });
sections[it->second].entries.push_back(&entry);
}
sections.push_back({ "user:", _u8L("My textures"), {} });
for (const TextureLibraryEntry &entry : library)
if (entry.source == TextureSource::User)
sections.back().entries.push_back(&entry);
const float child_indent = std::round(m_imgui->scaled(0.8f));
const Section &built_in = sections.front();
if (section_header(built_in.key, built_in.title, built_in.entries.size(), 0.f, {}, built_in.title)) {
const ImVec2 origin = ImGui::GetCursorScreenPos(); const ImVec2 origin = ImGui::GetCursorScreenPos();
const int count = grid(origin, false); const int count = grid(origin, built_in.entries, false);
end_grid(origin, count); end_grid(origin, count);
if (count == 0) if (count == 0)
ImGui::TextDisabled("%s", _u8L("No textures found.").c_str()); ImGui::TextDisabled("%s", _u8L("No textures found.").c_str());
} }
ImGui::TextDisabled("%s", _u8L("My textures").c_str());
{ size_t subscribed_count = 0;
for (size_t i = 1; i + 1 < sections.size(); ++i)
subscribed_count += sections[i].entries.size();
const std::string subscribed_title = _u8L("Texture libraries");
if (section_header("subscribed:", subscribed_title, subscribed_count, 0.f, {}, subscribed_title)) {
for (size_t i = 1; i + 1 < sections.size(); ++i) {
const Section &pack = sections[i];
const auto [name, attribution] = split_pack_title(pack.title);
if (!section_header(pack.key, name, pack.entries.size(), child_indent, attribution, pack.title))
continue;
const ImVec2 origin = ImGui::GetCursorScreenPos(); const ImVec2 origin = ImGui::GetCursorScreenPos();
const int count = grid(origin, true); const int count = grid(origin, pack.entries, false);
end_grid(origin, count);
}
ImGui::Dummy(ImVec2(0.f, spacing));
const std::string download_label = _u8L("Explore more texture libraries");
if (ImGui::Button(download_label.c_str(), ImVec2(grid_w, frame_h)))
download_clicked = true;
if (ImGui::IsItemHovered())
panel_tooltip(_u8L("Opens OrcaCloud in the Home page to browse for plugins."));
}
const Section &user = sections.back();
if (section_header(user.key, user.title, user.entries.size(), 0.f, {}, user.title)) {
const ImVec2 origin = ImGui::GetCursorScreenPos();
const int count = grid(origin, user.entries, true);
ImGui::SetCursorScreenPos(tile_pos(origin, count)); ImGui::SetCursorScreenPos(tile_pos(origin, count));
if (ImGui::Button("+##import_texture", ImVec2(tile, tile))) if (ImGui::Button("+##import_texture", ImVec2(tile, tile)))
import_clicked = true; import_clicked = true;
@@ -4273,17 +4590,27 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
"bakes from and saved to your personal texture folder, kept separate from the " "bakes from and saved to your personal texture folder, kept separate from the "
"textures shipped with OrcaSlicer.")); "textures shipped with OrcaSlicer."));
end_grid(origin, count + 1); end_grid(origin, count + 1);
} // Belongs to this section: it describes the import tile and the remove-on-hover tiles, both of
// which live here, so it is hidden with them rather than left describing controls off-screen.
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + grid_w); ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + grid_w);
ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str()); ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str());
ImGui::PopTextWrapPos(); ImGui::PopTextWrapPos();
}
if (loads_deferred)
m_parent.request_extra_frame();
// Done whether or not the current texture was drawn: one in a collapsed section stays where it is.
m_picker_reveal_selected = false;
ImGui::EndChild();
pop_slim_scrollbar_style();
if (chosen || import_clicked) if (chosen || import_clicked || close_clicked || download_clicked)
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
m_tooltip_anchor_min = prev_anchor_min; m_tooltip_anchor_min = prev_anchor_min;
m_tooltip_anchor_max = prev_anchor_max; m_tooltip_anchor_max = prev_anchor_max;
ImGui::EndPopup(); ImGui::EndPopup();
// The OrcaCloud plugin hub filtered to resource textures; the pane's session handoff redirects here.
if (download_clicked)
wxGetApp().open_orca_cloud("/app/plugins/plugin-hub?types=resource-textures");
// Only once the loop over the library is done: importing, picking (which reorders the recently used) // Only once the loop over the library is done: importing, picking (which reorders the recently used)
// and removing all change the library the loop was iterating. // and removing all change the library the loop was iterating.
@@ -6586,19 +6913,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
m_imgui->text(text); m_imgui->text(text);
m_imgui->pop_bold_font(); m_imgui->pop_bold_font();
}; };
// Cuts a UTF-8 string down to `max_w` pixels, ending it in "..." when it had to be cut.
const auto ellipsize = [](std::string s, float max_w) {
if (ImGui::CalcTextSize(s.c_str()).x <= max_w)
return s;
while (!s.empty() && ImGui::CalcTextSize((s + "...").c_str()).x > max_w) {
unsigned char c;
do { // a whole code point at a time
c = static_cast<unsigned char>(s.back());
s.pop_back();
} while (!s.empty() && (c & 0xC0) == 0x80);
}
return s + "...";
};
// Label column + a slider filling the rest of the row, value printed inside the track. // Label column + a slider filling the rest of the row, value printed inside the track.
// `right_inset` keeps a layer card's inner padding. // `right_inset` keeps a layer card's inner padding.
const auto slider_label = [&](const wxString &label) { const auto slider_label = [&](const wxString &label) {
@@ -6791,52 +7105,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
style.WindowPadding.y - style.ItemSpacing.y); style.WindowPadding.y - style.ItemSpacing.y);
const float body_h = m_panel_body_h > 0.f ? std::min(m_panel_body_h + 1.f, max_body_h) : max_body_h; const float body_h = m_panel_body_h > 0.f ? std::min(m_panel_body_h + 1.f, max_body_h) : max_body_h;
// ImGui's stock scrollbar is a wide, square-cornered slab in a tinted track - against this flat panel push_slim_scrollbar_style(m_imgui->scaled(0.5f), dark);
// it reads as a raw widget bolted onto the edge. Slim it to a rounded thumb over an invisible track. // NoScrollWithMouse: ease_wheel_scroll() eases the scroll instead of teleporting five text lines per
const float scrollbar_w = m_imgui->scaled(0.5f); // notch, which on blocks this tall lost the reader's place.
const ImVec4 grab = dark ? ImVec4(1.f, 1.f, 1.f, 0.26f) : ImVec4(0.f, 0.f, 0.f, 0.26f);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_w);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.5f * scrollbar_w);
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(grab.x, grab.y, grab.z, 0.45f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(grab.x, grab.y, grab.z, 0.65f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.f, 0.f, 0.f, 0.f));
// NoScrollWithMouse: the wheel is handled below so the scroll can be eased instead of teleporting
// five text lines per notch, which on blocks this tall lost the reader's place.
ImGui::BeginChild("##td_body", ImVec2(panel_w, body_h), false, ImGuiWindowFlags_NoScrollWithMouse); ImGui::BeginChild("##td_body", ImVec2(panel_w, body_h), false, ImGuiWindowFlags_NoScrollWithMouse);
{ ease_wheel_scroll(m_panel_scroll_target, m_panel_scroll_applied);
ImGuiIO &io = ImGui::GetIO();
const float scroll_now = ImGui::GetScrollY();
const float scroll_max = ImGui::GetScrollMaxY();
// Anything that moved the scroll without us - dragging the grab, a keyboard/gamepad nav step, the
// content shrinking under a clamped offset - has to re-seed the target, or the easing below would
// immediately drag the view back to where it last animated to.
if (m_panel_scroll_applied < 0.f || std::abs(scroll_now - m_panel_scroll_applied) > 0.5f)
m_panel_scroll_target = scroll_now;
if (io.MouseWheel != 0.f && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows))
m_panel_scroll_target -= io.MouseWheel * ImGui::GetFontSize() * 4.f;
// Whole pixels: ImGui floors whatever SetScrollY() is given, so a fractional target could never be
// reached and the "still gliding" test below would stay true forever, repainting the canvas for good.
m_panel_scroll_target = std::floor(std::clamp(m_panel_scroll_target, 0.f, scroll_max));
const float delta = m_panel_scroll_target - scroll_now;
if (std::abs(delta) >= 1.f) {
// Exponential ease, formulated against the frame time so the glide takes the same wall time
// whether the canvas is running at 30 or 144 fps. The last sub-pixel step would be floored
// away, so land on the target outright once the remainder is that small.
const float t = 1.f - std::exp(-20.f * std::clamp(io.DeltaTime, 1.f / 240.f, 1.f / 15.f));
const float step = (std::abs(delta * t) < 1.f) ? delta : delta * t;
const float next = std::floor(scroll_now + step);
ImGui::SetScrollY(next);
m_panel_scroll_applied = next;
m_parent.set_as_dirty(); // nothing else would redraw mid-glide once the mouse stops
} else {
m_panel_scroll_applied = scroll_now;
}
}
// Everything that changes the layer list is deferred until the list is no longer being drawn: // Everything that changes the layer list is deferred until the list is no longer being drawn:
// removing or reordering shifts mv->texture_displacement_layers under the pointers the loop holds, // removing or reordering shifts mv->texture_displacement_layers under the pointers the loop holds,
@@ -7083,6 +7356,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
const auto open_picker = [&](int slot) { const auto open_picker = [&](int slot) {
m_picker_slot = slot; m_picker_slot = slot;
m_picker_open_request = true; m_picker_open_request = true;
m_picker_reveal_selected = true;
}; };
// The six-dot drag icon, drawn into `list` over [x, x + w] x [y, y + h]. // The six-dot drag icon, drawn into `list` over [x, x + w] x [y, y + h].
const auto draw_drag_icon = [&](ImDrawList *list, float x, float y, float w, float h, ImU32 tint) { const auto draw_drag_icon = [&](ImDrawList *list, float x, float y, float w, float h, ImU32 tint) {
@@ -8077,8 +8351,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// height of everything above, whether or not it is scrolled. // height of everything above, whether or not it is scrolled.
m_panel_body_h = ImGui::GetCursorPosY() - style.ItemSpacing.y; m_panel_body_h = ImGui::GetCursorPosY() - style.ItemSpacing.y;
ImGui::EndChild(); ImGui::EndChild();
ImGui::PopStyleColor(5); pop_slim_scrollbar_style();
ImGui::PopStyleVar(2);
// The layer-list changes deferred from the loop above. // The layer-list changes deferred from the loop above.
if (slot_to_remove >= 0) if (slot_to_remove >= 0)
@@ -428,6 +428,10 @@ private:
void render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max); void render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max);
int m_picker_slot = -1; int m_picker_slot = -1;
bool m_picker_open_request = false; bool m_picker_open_request = false;
// Set when the popup opens, so the next layout scrolls the layer's current texture into view.
bool m_picker_reveal_selected = false;
float m_picker_scroll_target = 0.f; // see ease_wheel_scroll()
float m_picker_scroll_applied = -1.f;
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry); void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
void import_custom_texture(TextureDisplacementLayer &layer); void import_custom_texture(TextureDisplacementLayer &layer);
@@ -1062,6 +1066,8 @@ private:
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture). // Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
std::map<std::string, LibraryTexture> m_library_textures; std::map<std::string, LibraryTexture> m_library_textures;
// Generation of the library the above were cached against; a change clears them.
uint64_t m_library_generation = 0;
// Everything the *unwrap* depends on. update_uv_editor() runs from rebuild_preview(), i.e. on // Everything the *unwrap* depends on. update_uv_editor() runs from rebuild_preview(), i.e. on
// every stroke end and every slider release - but depth/tiling/rotation/offset/blend change // every stroke end and every slider release - but depth/tiling/rotation/offset/blend change
@@ -1103,12 +1109,15 @@ private:
// The config key behind m_undocked. // The config key behind m_undocked.
static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked"; static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked";
// Smooth scrolling for the panel body (everything between the header and the pinned Bake footer). // Smooth scrolling for a child window, called inside it: the panel body (everything between the header
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard // and the pinned Bake footer) and the texture library's grid. ImGui jumps a fixed number of lines per
// jolt rather than a scroll. The wheel is intercepted (ImGuiWindowFlags_NoScrollWithMouse) and // wheel notch, which on tall layer cards reads as a hard jolt rather than a scroll. The child sets
// moves a *target* offset instead; the real scroll is eased toward it over the following frames. // ImGuiWindowFlags_NoScrollWithMouse, and the wheel moves `target` instead; the real scroll is eased
// toward it over the following frames. `applied` is what the easing wrote last frame, <0 until the
// first one.
void ease_wheel_scroll(float &target, float &applied);
float m_panel_scroll_target = 0.f; float m_panel_scroll_target = 0.f;
float m_panel_scroll_applied = -1.f; // what the easing wrote last frame; <0 until the first one float m_panel_scroll_applied = -1.f;
// Last frame's body content height and footer height. The body is a child window that has to be // Last frame's body content height and footer height. The body is a child window that has to be
// given its height before its content is laid out, so it is sized from the previous frame: as tall // given its height before its content is laid out, so it is sized from the previous frame: as tall
// as its content, capped so the footer still fits above the bottom of the canvas. // as its content, capped so the footer still fits above the bottom of the canvas.
+6 -4
View File
@@ -83,11 +83,13 @@ namespace Slic3r::GUI::GLGizmoUtils {
bool clicked = toolbar_circular_button(icon, "##wiki_guide_btn", scale); bool clicked = toolbar_circular_button(icon, "##wiki_guide_btn", scale);
if (url && *url && clicked) if (url && *url && clicked)
wxLaunchDefaultBrowser(wxString::FromUTF8(url)); wxLaunchDefaultBrowser(wxString::FromUTF8(url));
// ImGuiWrapper::tooltip keeps the text readable on the dark tooltip background whatever text colour the
// host window pushed; a negative wrap position keeps the URL on one line.
if (ImGui::IsItemHovered()){ if (ImGui::IsItemHovered()){
if(url && *url) if(url && *url)
ImGui::SetTooltip("%s\n%s", _u8L("Wiki Guide").c_str(), url); wxGetApp().imgui()->tooltip(_u8L("Wiki Guide") + "\n" + url, -1.f);
else else
ImGui::SetTooltip("%s", _u8L("Wiki Guide").c_str()); wxGetApp().imgui()->tooltip(_u8L("Wiki Guide"), -1.f);
} }
return clicked; // for dynamically generated links return clicked; // for dynamically generated links
} }
@@ -101,9 +103,9 @@ namespace Slic3r::GUI::GLGizmoUtils {
wxLaunchDefaultBrowser(wxString::FromUTF8(url)); wxLaunchDefaultBrowser(wxString::FromUTF8(url));
if (ImGui::IsItemHovered()){ if (ImGui::IsItemHovered()){
if(url && *url) if(url && *url)
ImGui::SetTooltip("%s\n%s", _u8L("Video Guide").c_str(), url); wxGetApp().imgui()->tooltip(_u8L("Video Guide") + "\n" + url, -1.f);
else else
ImGui::SetTooltip("%s", _u8L("Video Guide").c_str()); wxGetApp().imgui()->tooltip(_u8L("Video Guide"), -1.f);
} }
return clicked; // for dynamically generated links return clicked; // for dynamically generated links
} }
@@ -857,11 +857,12 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
if (render_combo(imgui_wrapper, "", modes, selection_idx, 0, coord_combo_width)) { if (render_combo(imgui_wrapper, "", modes, selection_idx, 0, coord_combo_width)) {
combox_changed = true; combox_changed = true;
} }
ImGuiWrapper::pop_combo_style();
// After pop_combo_style(): its light popup background would hide the tooltip's white text.
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
auto tooltip_str = _L("Coordinate system used for transform actions."); auto tooltip_str = _L("Coordinate system used for transform actions.");
imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size); imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size);
} }
ImGuiWrapper::pop_combo_style();
// ORCA use TextColored to match axes color // ORCA use TextColored to match axes color
float offset_to_center = (unit_size - ImGui::CalcTextSize("O").x) / 2; float offset_to_center = (unit_size - ImGui::CalcTextSize("O").x) / 2;
@@ -964,11 +965,12 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
if (render_combo(imgui_wrapper, "", align_modes, align_mode_idx, 0, coord_combo_width)) if (render_combo(imgui_wrapper, "", align_modes, align_mode_idx, 0, coord_combo_width))
m_align_to_parent = align_mode_idx + 1 == align_modes.size(); m_align_to_parent = align_mode_idx + 1 == align_modes.size();
ImGui::PopID(); ImGui::PopID();
ImGuiWrapper::pop_combo_style();
// After pop_combo_style(): its light popup background would hide the tooltip's white text.
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
wxString tooltip_str = _L("Reference for alignment"); wxString tooltip_str = _L("Reference for alignment");
imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size); imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size);
} }
ImGuiWrapper::pop_combo_style();
const bool dark_mode = wxGetApp().dark_mode(); const bool dark_mode = wxGetApp().dark_mode();
int current_axis = -1; int current_axis = -1;
@@ -1377,11 +1379,12 @@ void GizmoObjectManipulation::do_render_scale_input_window(ImGuiWrapper* imgui_w
if (render_combo(imgui_wrapper, "", modes, selection_idx, 0, coord_combo_width)) { if (render_combo(imgui_wrapper, "", modes, selection_idx, 0, coord_combo_width)) {
combox_changed = true; combox_changed = true;
} }
ImGuiWrapper::pop_combo_style();
// After pop_combo_style(): its light popup background would hide the tooltip's white text.
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
auto tooltip_str = _L("Coordinate system used for transform actions."); auto tooltip_str = _L("Coordinate system used for transform actions.");
imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size); imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size);
} }
ImGuiWrapper::pop_combo_style();
// ORCA use TextColored to match axes color // ORCA use TextColored to match axes color
float offset_to_center = (unit_size - ImGui::CalcTextSize("O").x) / 2; float offset_to_center = (unit_size - ImGui::CalcTextSize("O").x) / 2;
+5 -3
View File
@@ -505,10 +505,12 @@ bool ImGuiWrapper::update_mouse_data(wxMouseEvent& evt)
io.MouseDown[0] = evt.LeftIsDown(); io.MouseDown[0] = evt.LeftIsDown();
io.MouseDown[1] = evt.RightIsDown(); io.MouseDown[1] = evt.RightIsDown();
io.MouseDown[2] = evt.MiddleIsDown(); io.MouseDown[2] = evt.MiddleIsDown();
float wheel_delta = static_cast<float>(evt.GetWheelDelta()); // Only a vertical wheel event with a direction is a wheel step. A macOS trackpad reports sideways motion
if (wheel_delta != 0.0f) { // as separate horizontal events, and its wheel events often carry a rotation of 0: wx truncates the
// sub-pixel deltas to whole ones and passes on events with no delta at all. Read as vertical steps,
// either kind scrolls against the gesture, and scrolling up bounces back and forth.
if (evt.GetWheelAxis() == wxMOUSE_WHEEL_VERTICAL && evt.GetWheelRotation() != 0)
io.MouseWheel = evt.GetWheelRotation() > 0 ? 1.f : -1.f; io.MouseWheel = evt.GetWheelRotation() > 0 ? 1.f : -1.f;
}
unsigned buttons = (evt.LeftIsDown() ? 1 : 0) | (evt.RightIsDown() ? 2 : 0) | (evt.MiddleIsDown() ? 4 : 0); unsigned buttons = (evt.LeftIsDown() ? 1 : 0) | (evt.RightIsDown() ? 2 : 0) | (evt.MiddleIsDown() ? 4 : 0);
m_mouse_buttons = buttons; m_mouse_buttons = buttons;
+3
View File
@@ -1739,6 +1739,9 @@ void MainFrame::fit_tab_labels()
for (size_t i = count - 1; i >= 1; --i) { for (size_t i = count - 1; i >= 1; --i) {
int right = ScreenToClient(m_slice_option_btn->ClientToScreen({})).x; int right = ScreenToClient(m_slice_option_btn->ClientToScreen({})).x;
int left = sizer->GetSize().GetWidth(); int left = sizer->GetSize().GetWidth();
// The overflow button sits between the tab buttons and the side tools, so it is not free space.
if (const wxWindow* overflow = ctrl->GetOverflowButton())
left += overflow->GetSize().GetWidth();
if (right - left - FromDIP(15) > 0) return; if (right - left - FromDIP(15) > 0) return;
ctrl->SetCompact(i, true); ctrl->SetCompact(i, true);
m_tabpanel->Refresh(); m_tabpanel->Refresh();
+1
View File
@@ -57,6 +57,7 @@ public:
// ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g. // ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g.
// an overflow indicator. Pass nullptr to remove it; ownership stays with the caller. // an overflow indicator. Pass nullptr to remove it; ownership stays with the caller.
void SetOverflowButton(wxWindow* button); void SetOverflowButton(wxWindow* button);
wxWindow* GetOverflowButton() const { return m_overflow_button; }
private: private:
wxFlexGridSizer* m_buttons_sizer; wxFlexGridSizer* m_buttons_sizer;
+3 -1
View File
@@ -2225,8 +2225,10 @@ void PublishSettingsDialog::refresh_tab_indicators()
if (section.variant_switch != nullptr) { if (section.variant_switch != nullptr) {
const wxString& title = m_categories[section.categories[i]].title; const wxString& title = m_categories[section.categories[i]].title;
const wxString text = on ? title + wxString(" ") + wxString(wxUniChar(0x2022)) : title; const wxString text = on ? title + wxString(" ") + wxString(wxUniChar(0x2022)) : title;
if (section.variant_switch->GetOptionText(static_cast<unsigned int>(i)) != text) if (section.variant_switch->GetOptionText(static_cast<unsigned int>(i)) != text) {
section.variant_switch->SetOptionText(static_cast<unsigned int>(i), text); section.variant_switch->SetOptionText(static_cast<unsigned int>(i), text);
section.page->Layout();
}
} }
any = any || on; any = any || on;
} }
+3 -2
View File
@@ -2,6 +2,7 @@
#include "I18N.hpp" #include "I18N.hpp"
#include "bambu_networking.hpp" #include "bambu_networking.hpp"
#include "IPrinterAgent.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "GUI.hpp" #include "GUI.hpp"
#include "GUI_App.hpp" #include "GUI_App.hpp"
@@ -1903,7 +1904,7 @@ void InputIpAddressDialog::on_ok(wxMouseEvent& evt)
std::string str_ip = m_input_ip->GetTextCtrl()->GetValue().ToStdString(); std::string str_ip = m_input_ip->GetTextCtrl()->GetValue().ToStdString();
std::string str_access_code = m_input_access_code->GetTextCtrl()->GetValue().ToStdString(); std::string str_access_code = m_input_access_code->GetTextCtrl()->GetValue().ToStdString();
if (str_access_code.empty()) if (str_access_code.empty())
str_access_code = "88888888"; str_access_code = NO_API_KEY_SENTINEL;
std::string str_name = m_input_printer_name->GetTextCtrl()->GetValue().Strip(wxString::both).ToStdString(); std::string str_name = m_input_printer_name->GetTextCtrl()->GetValue().Strip(wxString::both).ToStdString();
// Serial number should not contain lower case letters, and bambu_network plugin crashes // Serial number should not contain lower case letters, and bambu_network plugin crashes
// if user entered the wrong serial number, so we call `Upper()` here. // if user entered the wrong serial number, so we call `Upper()` here.
@@ -1950,7 +1951,7 @@ void InputIpAddressDialog::on_send_retry()
wxString ip = m_input_ip->GetTextCtrl()->GetValue(); wxString ip = m_input_ip->GetTextCtrl()->GetValue();
wxString str_access_code = m_input_access_code->GetTextCtrl()->GetValue(); wxString str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
if (str_access_code.IsEmpty()) if (str_access_code.IsEmpty())
str_access_code = "88888888"; str_access_code = NO_API_KEY_SENTINEL;
// check support function // check support function
if (!m_obj) return; if (!m_obj) return;
+27 -15
View File
@@ -474,17 +474,14 @@ void Selection::add_curr_plate()
if (!m_valid) if (!m_valid)
return; return;
std::vector<unsigned int> volume_idxs = get_volume_idxs_from_plate(wxGetApp().plater()->get_partplate_list().get_curr_plate_index());
if (volume_idxs.empty())
return;
wxGetApp().plater()->take_snapshot(std::string("Selection-Add Curr Plate All!")); wxGetApp().plater()->take_snapshot(std::string("Selection-Add Curr Plate All!"));
m_mode = Instance; m_mode = Instance;
clear(); clear();
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) {
if (plate && plate->contain_instance_totally(obj_idx, 0)) {
std::vector<unsigned int> volume_idxs = get_volume_idxs_from_object(obj_idx);
do_add_volumes(volume_idxs); do_add_volumes(volume_idxs);
}
}
update_type(); update_type();
this->set_bounding_boxes_dirty(); this->set_bounding_boxes_dirty();
@@ -511,20 +508,14 @@ void Selection::remove_curr_plate()
if (!m_valid) if (!m_valid)
return; return;
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); std::vector<unsigned int> volume_idxs = get_volume_idxs_from_plate(wxGetApp().plater()->get_partplate_list().get_curr_plate_index());
if (plate->empty()) if (volume_idxs.empty())
return; return;
wxGetApp().plater()->take_snapshot(std::string("Selection-Delete Curr Plate All")); wxGetApp().plater()->take_snapshot(std::string("Selection-Delete Curr Plate All"));
m_mode = Instance; m_mode = Instance;
clear(); clear();
for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) {
if (plate && plate->contain_instance(obj_idx, 0)) {
std::vector<unsigned int> volume_idxs = get_volume_idxs_from_object(obj_idx);
do_add_volumes(volume_idxs); do_add_volumes(volume_idxs);
}
}
update_type(); update_type();
this->set_bounding_boxes_dirty(); this->set_bounding_boxes_dirty();
@@ -2270,6 +2261,27 @@ std::vector<unsigned int> Selection::get_volume_idxs_from_volume(unsigned int ob
return idxs; return idxs;
} }
std::vector<unsigned int> Selection::get_volume_idxs_from_plate(const int plate_idx) const
{
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_plate(plate_idx);
std::vector<unsigned int> idxs;
if (!plate)
return idxs;
for (int obj_idx = 0; obj_idx < (int)m_model->objects.size(); obj_idx++) {
const ModelObject* obj = m_model->objects[obj_idx];
for (int inst_idx = 0; inst_idx < (int)obj->instances.size(); inst_idx++) {
// on this plate or overlapping it in some way (fully inside, oversized or partly outside)
if (plate->contain_instance(obj_idx, inst_idx) || plate->intersect_instance(obj_idx, inst_idx)) {
std::vector<unsigned int> inst_idxs = get_volume_idxs_from_instance(obj_idx, inst_idx);
idxs.insert(idxs.end(), inst_idxs.begin(), inst_idxs.end());
}
}
}
return idxs;
}
std::vector<unsigned int> Selection::get_missing_volume_idxs_from(const std::vector<unsigned int>& volume_idxs) const std::vector<unsigned int> Selection::get_missing_volume_idxs_from(const std::vector<unsigned int>& volume_idxs) const
{ {
std::vector<unsigned int> idxs; std::vector<unsigned int> idxs;
+2
View File
@@ -409,6 +409,8 @@ public:
std::vector<unsigned int> get_volume_idxs_from_instance(unsigned int object_idx, unsigned int instance_idx) const; std::vector<unsigned int> get_volume_idxs_from_instance(unsigned int object_idx, unsigned int instance_idx) const;
// returns the idx of the volume corresponding to the volume with the given idxs // returns the idx of the volume corresponding to the volume with the given idxs
std::vector<unsigned int> get_volume_idxs_from_volume(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx) const; std::vector<unsigned int> get_volume_idxs_from_volume(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx) const;
// returns the list of idxs of the volumes of all instances on the given plate or overlapping it. (fully inside, oversized or partly outside)
std::vector<unsigned int> get_volume_idxs_from_plate(const int plate_idx) const;
// returns the list of idxs of the volumes contained in the selection but not in the given list // returns the list of idxs of the volumes contained in the selection but not in the given list
std::vector<unsigned int> get_missing_volume_idxs_from(const std::vector<unsigned int>& volume_idxs) const; std::vector<unsigned int> get_missing_volume_idxs_from(const std::vector<unsigned int>& volume_idxs) const;
// returns the list of idxs of the volumes contained in the given list but not in the selection // returns the list of idxs of the volumes contained in the given list but not in the selection
@@ -0,0 +1,63 @@
#pragma once
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/Polygon.hpp"
#include <algorithm>
#include <cstddef>
#include <map>
#include <utility>
#include <vector>
namespace Slic3r::GUI {
struct SequentialClearanceInstance
{
double instance_height;
BoundingBox bounding_box;
Polygon hull_polygon;
ObjectID instance_id;
};
// PrintApply may group rotated copies in a different order from ModelObject::instances.
// Reorder one object's instances only when validation has assigned all of them an order.
inline void sort_sequential_clearance_instances(
std::vector<SequentialClearanceInstance>::iterator first, std::vector<SequentialClearanceInstance>::iterator last,
const std::map<ObjectID, int>& print_order)
{
if (!std::all_of(first, last, [&print_order](const SequentialClearanceInstance& instance) {
const auto order = print_order.find(instance.instance_id);
return order != print_order.end() && order->second > 0;
}))
return;
std::stable_sort(first, last, [&print_order](const SequentialClearanceInstance& lhs, const SequentialClearanceInstance& rhs) {
return print_order.at(lhs.instance_id) < print_order.at(rhs.instance_id);
});
}
// Input follows object-list order with validated print order within each object.
inline std::vector<std::pair<Polygon, float>> sequential_clearance_height_polygons(
const std::vector<SequentialClearanceInstance>& instances, double printable_height, double height_to_lid, double height_to_rod)
{
std::vector<std::pair<Polygon, float>> height_polygons;
height_polygons.reserve(instances.size());
for (size_t k = 0; k < instances.size(); ++k) {
const auto& instance = instances[k];
double height = k + 1 == instances.size() ? printable_height : height_to_lid;
for (size_t i = k + 1; i < instances.size(); ++i) {
const auto& next_bbox = instances[i].bounding_box;
if (std::min(instance.bounding_box.max.y(), next_bbox.max.y()) >
std::max(instance.bounding_box.min.y(), next_bbox.min.y())) {
height = height_to_rod;
break;
}
}
if (height < instance.instance_height)
height_polygons.emplace_back(instance.hull_polygon, float(height));
}
return height_polygons;
}
} // namespace Slic3r::GUI
+74 -24
View File
@@ -1,6 +1,7 @@
#include "TextureLibrary.hpp" #include "TextureLibrary.hpp"
#include <algorithm> #include <algorithm>
#include <atomic>
#include <boost/filesystem/operations.hpp> #include <boost/filesystem/operations.hpp>
#include <cstddef> #include <cstddef>
#include <boost/filesystem/directory.hpp> #include <boost/filesystem/directory.hpp>
@@ -21,6 +22,7 @@
#include <boost/system/error_code.hpp> #include <boost/system/error_code.hpp>
#include <vector> #include <vector>
#include <wx/app.h>
#include <wx/gdicmn.h> #include <wx/gdicmn.h>
#include <utility> #include <utility>
#include <wx/image.h> #include <wx/image.h>
@@ -36,6 +38,9 @@
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/I18N.hpp"
#include "slic3r/plugin/PluginFsUtils.hpp"
#include "slic3r/plugin/host/PluginResourceTextures.hpp"
namespace Slic3r::GUI { namespace Slic3r::GUI {
namespace { namespace {
@@ -50,7 +55,7 @@ bool is_image_file(const boost::filesystem::path &path)
return ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp"; return ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp";
} }
void scan_dir(const boost::filesystem::path &dir, bool is_user, std::vector<TextureLibraryEntry> &out) void scan_dir(const boost::filesystem::path &dir, TextureSource source, std::vector<TextureLibraryEntry> &out)
{ {
boost::system::error_code ec; boost::system::error_code ec;
if (!boost::filesystem::is_directory(dir, ec)) if (!boost::filesystem::is_directory(dir, ec))
@@ -60,12 +65,43 @@ void scan_dir(const boost::filesystem::path &dir, bool is_user, std::vector<Text
for (boost::filesystem::directory_iterator it(dir, ec), end; it != end && !ec; it.increment(ec)) { for (boost::filesystem::directory_iterator it(dir, ec), end; it != end && !ec; it.increment(ec)) {
if (!boost::filesystem::is_regular_file(it->path(), ec) || !is_image_file(it->path())) if (!boost::filesystem::is_regular_file(it->path(), ec) || !is_image_file(it->path()))
continue; continue;
out.push_back({ it->path().stem().string(), it->path().string(), is_user }); out.push_back({ it->path().stem().string(), it->path().string(), source });
} }
std::sort(out.begin() + first, out.end(), std::sort(out.begin() + first, out.end(),
[](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return a.name < b.name; }); [](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return a.name < b.name; });
} }
// Appends every loaded pack's textures, ordered by the pack's display name and then by file name. Each
// entry carries the contributing pack's display name, which the picker uses to group a pack's textures
// into one section; a pack name is unique among loaded packs of this capability type, so the name both
// groups the section and keeps its entries contiguous. A pack whose directory fails validation is
// skipped, so one bad pack never hides the others.
void scan_plugin_textures(std::vector<TextureLibraryEntry> &out)
{
std::vector<TextureLibraryEntry> found;
for (const ResourceTextureEntry &pack : PluginResourceTextures::instance().entries()) {
std::string error;
const std::vector<std::string> files =
collect_resource_texture_files(pack.texture_dir, pack.plugin_root, error);
if (!error.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Texture pack '" << pack.display_name << "' was skipped: " << error;
continue;
}
for (const std::string &file : files) {
const boost::filesystem::path path(file);
found.push_back({ path.stem().string(), file, TextureSource::Plugin, pack.display_name });
}
}
std::stable_sort(found.begin(), found.end(), [](const TextureLibraryEntry &a, const TextureLibraryEntry &b) {
if (a.pack_name != b.pack_name)
return a.pack_name < b.pack_name;
return a.name < b.name;
});
for (TextureLibraryEntry &entry : found)
out.push_back(std::move(entry));
}
// True if the image carries any colour at all, i.e. some pixel's channels are not all equal. A // True if the image carries any colour at all, i.e. some pixel's channels are not all equal. A
// photo saved as RGB but actually grey should still be stored as a grayscale PNG - it is a third of // photo saved as RGB but actually grey should still be stored as a grayscale PNG - it is a third of
// the bytes, and the colour feature keys off has_color(), so storing a grey image as RGB would offer // the bytes, and the colour feature keys off has_color(), so storing a grey image as RGB would offer
@@ -187,27 +223,20 @@ std::vector<unsigned char> read_file_bytes(const std::string &path)
return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>()); return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
} }
// True if these bytes are a PNG libslic3r can decode, i.e. can be stored on a layer as-is. Mirrors // True if decode_height_texture() can read these bytes, so they can be stored on a layer as-is.
// exactly what decode_height_texture() accepts: 8-bit grayscale, or 8-bit colour (whose luminance is // Anything else is converted on load.
// the height and whose RGB is available to colour the model). Anything else is converted on load.
bool is_supported_height_map(const std::vector<unsigned char> &bytes) bool is_supported_height_map(const std::vector<unsigned char> &bytes)
{ {
if (bytes.empty()) return !bytes.empty() && png::is_decodable_8bit_png(png::ReadBuf{ bytes.data(), bytes.size() });
return false;
const png::ReadBuf rbuf{ bytes.data(), bytes.size() };
if (!png::is_png(rbuf))
return false;
png::ImageGreyscale gray;
if (png::decode_png(rbuf, gray) && gray.cols > 0 && gray.rows > 0)
return true;
png::ImageColorscale color;
return png::decode_colored_png(rbuf, color) && color.cols > 0 && color.rows > 0 &&
color.bytes_per_pixel >= 3 &&
color.buf.size() == color.cols * color.rows * size_t(color.bytes_per_pixel);
} }
std::vector<TextureLibraryEntry> g_library; std::vector<TextureLibraryEntry> g_library;
bool g_library_scanned = false; bool g_library_scanned = false;
// The generation g_library was last built from, so texture_library() can rescan after a pack change.
uint64_t g_library_scan_generation = 0;
// Bumped by plugin callbacks, read by the GUI frame. Atomic so a read never races a write.
std::atomic<uint64_t> g_library_generation{ 0 };
// Writes `bytes` into the user's own texture folder as `stem`.png, uniquified so an existing texture - // Writes `bytes` into the user's own texture folder as `stem`.png, uniquified so an existing texture -
// shipped-name collision, or a second edit of the same source - is never overwritten. Returns the new // shipped-name collision, or a second edit of the same source - is never overwritten. Returns the new
@@ -255,6 +284,8 @@ const size_t RECENT_MAX = 200;
std::vector<std::string> recent_texture_names() std::vector<std::string> recent_texture_names()
{ {
std::vector<std::string> names; std::vector<std::string> names;
if (wxTheApp == nullptr) // headless or shutting down: there is no app config to read
return names;
if (wxGetApp().app_config == nullptr) if (wxGetApp().app_config == nullptr)
return names; return names;
const std::string value = wxGetApp().app_config->get(RECENT_SECTION, RECENT_KEY); const std::string value = wxGetApp().app_config->get(RECENT_SECTION, RECENT_KEY);
@@ -269,6 +300,8 @@ std::vector<std::string> recent_texture_names()
void store_recent_texture_names(std::vector<std::string> names) void store_recent_texture_names(std::vector<std::string> names)
{ {
if (wxTheApp == nullptr) // headless or shutting down: there is no app config to write
return;
if (wxGetApp().app_config == nullptr) if (wxGetApp().app_config == nullptr)
return; return;
if (names.size() > RECENT_MAX) if (names.size() > RECENT_MAX)
@@ -279,8 +312,9 @@ void store_recent_texture_names(std::vector<std::string> names)
wxGetApp().app_config->set(RECENT_SECTION, RECENT_KEY, value); wxGetApp().app_config->set(RECENT_SECTION, RECENT_KEY, value);
} }
// Reorders the user's entries (the tail of `library`) by recent use. Stable, so textures never used keep // Reorders the user's entries by recent use. Stable, so textures never used keep the name order
// the name order scan_dir() gave them, after every used one. // scan_dir() gave them, after every used one. Only the user's own range is touched: shipped and pack
// entries must keep the order their scans produced.
void sort_user_textures(std::vector<TextureLibraryEntry> &library) void sort_user_textures(std::vector<TextureLibraryEntry> &library)
{ {
const std::vector<std::string> names = recent_texture_names(); const std::vector<std::string> names = recent_texture_names();
@@ -291,8 +325,11 @@ void sort_user_textures(std::vector<TextureLibraryEntry> &library)
const auto it = rank.find(boost::filesystem::path(e.path).filename().string()); const auto it = rank.find(boost::filesystem::path(e.path).filename().string());
return it == rank.end() ? std::numeric_limits<int>::max() : it->second; return it == rank.end() ? std::numeric_limits<int>::max() : it->second;
}; };
const auto first_user = std::find_if(library.begin(), library.end(), [](const TextureLibraryEntry &e) { return e.is_user; }); const auto first_user = std::find_if(library.begin(), library.end(),
std::stable_sort(first_user, library.end(), [](const TextureLibraryEntry &e) { return e.source == TextureSource::User; });
const auto user_end = std::find_if(first_user, library.end(),
[](const TextureLibraryEntry &e) { return e.source != TextureSource::User; });
std::stable_sort(first_user, user_end,
[&rank_of](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return rank_of(a) < rank_of(b); }); [&rank_of](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return rank_of(a) < rank_of(b); });
} }
@@ -311,6 +348,16 @@ bool in_user_texture_dir(const std::string &path)
} // namespace } // namespace
uint64_t texture_library_generation()
{
return g_library_generation.load();
}
void bump_texture_library_generation()
{
g_library_generation.fetch_add(1);
}
std::string user_texture_dir() std::string user_texture_dir()
{ {
const boost::filesystem::path dir = boost::filesystem::path(Slic3r::data_dir()) / "textures" / "displacement"; const boost::filesystem::path dir = boost::filesystem::path(Slic3r::data_dir()) / "textures" / "displacement";
@@ -325,16 +372,19 @@ std::string user_texture_dir()
const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan) const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan)
{ {
if (g_library_scanned && !force_rescan) const uint64_t generation = texture_library_generation();
if (g_library_scanned && !force_rescan && generation == g_library_scan_generation)
return g_library; return g_library;
g_library.clear(); g_library.clear();
scan_dir(boost::filesystem::path(Slic3r::resources_dir()) / "textures" / "displacement", false, g_library); scan_dir(boost::filesystem::path(Slic3r::resources_dir()) / "textures" / "displacement", TextureSource::Shipped, g_library);
const std::string user_dir = user_texture_dir(); const std::string user_dir = user_texture_dir();
if (!user_dir.empty()) if (!user_dir.empty())
scan_dir(boost::filesystem::path(user_dir), true, g_library); scan_dir(boost::filesystem::path(user_dir), TextureSource::User, g_library);
sort_user_textures(g_library); sort_user_textures(g_library);
scan_plugin_textures(g_library);
g_library_scan_generation = generation;
g_library_scanned = true; g_library_scanned = true;
return g_library; return g_library;
} }
+29 -9
View File
@@ -1,6 +1,7 @@
#ifndef slic3r_TextureLibrary_hpp_ #ifndef slic3r_TextureLibrary_hpp_
#define slic3r_TextureLibrary_hpp_ #define slic3r_TextureLibrary_hpp_
#include <cstdint>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <string> #include <string>
@@ -10,26 +11,45 @@
namespace Slic3r::GUI { namespace Slic3r::GUI {
// One selectable height-map texture in the texture-displacement gizmo's texture picker. // Where a library texture came from. The picker lists each source under its own heading.
enum class TextureSource { Shipped, User, Plugin };
// One selectable height-map texture in the texture-displacement gizmo's texture picker. A texture that
// came from a plugin pack also carries the provenance the picker groups its sections by.
struct TextureLibraryEntry struct TextureLibraryEntry
{ {
std::string name; // display name (the file's stem, e.g. "Wood Grain") std::string name; // display name (the file's stem, e.g. "Wood Grain")
std::string path; // absolute path on disk std::string path; // absolute path on disk
bool is_user; // imported by the user, as opposed to shipped with OrcaSlicer TextureSource source; // which of the three sources it came from
std::string pack_name; // contributing pack's display name; empty for Shipped and User
}; };
// Every available height-map texture: the ones shipped in resources/textures/displacement first, sorted // Every available height-map texture, in the order the picker lists them: the ones shipped in
// by name, then the user's own from <data_dir>/textures/displacement - most recently used first (see // resources/textures/displacement first, by name; then the user's own from <data_dir>/textures/displacement,
// touch_user_texture()), then any never used, by name. // most recently used first (see touch_user_texture()) and the rest by name; then the loaded texture
// packs, ordered by pack display name and then by file name.
// //
// The two live in separate directories deliberately: an app update replaces the resources tree // The shipped and user textures live in separate directories deliberately: an app update replaces the
// wholesale, so anything the user imported has to sit somewhere that update can never overwrite or // resources tree wholesale, so anything the user imported has to sit somewhere that update can never
// delete. `is_user` is what the picker uses to show them under separate headings. // overwrite or delete. Pack textures are read-only like the shipped ones. Every texture carries its
// `source`; a pack texture also carries `pack_name`, and the picker groups pack textures into one
// collapsible section per pack name. A pack's name is unique among loaded packs of this capability type -
// the loader refuses a collision with "Capability collision for type ... and name ...", see
// PluginManager::check_registry_locked (PluginManager.cpp:211-225) - so the name on its own identifies a
// section and keeps that pack's entries contiguous.
// //
// Scanned once and cached. Pass force_rescan after an import, or to pick up a file the user dropped // Scanned once and cached. Pass force_rescan after an import, or to pick up a file the user dropped
// into either folder by hand while the app was running. // into either folder by hand while the app was running. A rescan also happens on its own when the pack
// registry changes, which is what texture_library_generation() tracks.
const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan = false); const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan = false);
// Monotonic counter, bumped whenever the set of loaded texture packs changes. The GUI reads it to
// decide whether texture_library() must rescan, and plugin callbacks bump it. Safe to read from any
// thread at any time.
uint64_t texture_library_generation();
void bump_texture_library_generation();
// Moves one of the user's own textures to the front of the recently-used order texture_library() lists // Moves one of the user's own textures to the front of the recently-used order texture_library() lists
// them in, and stores that order in the app config so it survives a restart. No-op for anything that is // them in, and stores that order in the app config so it survives a restart. No-op for anything that is
// not in user_texture_dir(). // not in user_texture_dir().
+9 -1
View File
@@ -537,7 +537,7 @@ void WebViewPanel::cloud_signed_out(const wxString& signout_url)
} }
} }
void WebViewPanel::set_cloud_mode(bool on, const wxString& url) void WebViewPanel::set_cloud_mode(bool on, const wxString& url, const wxString& page_url)
{ {
if (on) { if (on) {
// load_cloud_url records the origin from whatever it loads, including the plain cloud // load_cloud_url records the origin from whatever it loads, including the plain cloud
@@ -546,6 +546,10 @@ void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
create_cloud_browser(); create_cloud_browser();
m_cloud_seed_url = url; m_cloud_seed_url = url;
schedule_cloud_load(url.empty() ? wxString(kCloudUrl) : url); schedule_cloud_load(url.empty() ? wxString(kCloudUrl) : url);
} else if (!page_url.empty()) {
// The pane already holds a session, so an explicit page loads straight in; the handoff is
// only needed to establish one, not to reach an ordinary page.
load_cloud_url(page_url);
} else if (!url.empty() && url != m_cloud_seed_url) { } else if (!url.empty() && url != m_cloud_seed_url) {
// Re-seed only when the Start URL actually changed — a login, an account switch or a // Re-seed only when the Start URL actually changed — a login, an account switch or a
// sign-out changes it; an ordinary re-open does not (guide §8). Reloading an unchanged // sign-out changes it; an ordinary re-open does not (guide §8). Reloading an unchanged
@@ -556,6 +560,10 @@ void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
load_cloud_after_signout(url); load_cloud_after_signout(url);
} }
m_cloud_browser->Show(); m_cloud_browser->Show();
// The homepage shell owns the nav highlight and the cloud-mode layout class. A native open
// (not the page's own GotoCloud) must ask it to switch; the page's SetCloudMode no-ops if it
// already did. OnDocumentLoaded re-applies this when the homepage was still loading.
RunScript("SetCloudMode(true)");
} else if (m_cloud_browser) { } else if (m_cloud_browser) {
// Closing keeps the pane alive with its session, so re-opening does not have to sign in // Closing keeps the pane alive with its session, so re-opening does not have to sign in
// again (guide §10). Nothing is cleared here. // again (guide §10). Nothing is cleared here.
+4 -1
View File
@@ -118,7 +118,10 @@ public:
// Shows/hides the native OrcaCloud pane beside this homepage nav column. `url` is the // Shows/hides the native OrcaCloud pane beside this homepage nav column. `url` is the
// cloud Start URL ({web}/auth/handoff?next=…&user_id=…), which the page uses to decide // cloud Start URL ({web}/auth/handoff?next=…&user_id=…), which the page uses to decide
// whether it already has a session; empty falls back to the plain cloud page. // whether it already has a session; empty falls back to the plain cloud page.
void set_cloud_mode(bool on, const wxString& url = wxString()); // `page_url` is a full {web} page URL: an already-open pane is navigated straight there,
// without re-running the handoff, which is what an explicit "open this page" wants. Empty
// keeps whatever page the pane holds.
void set_cloud_mode(bool on, const wxString& url = wxString(), const wxString& page_url = wxString());
// True while the OrcaCloud pane exists and is visible. // True while the OrcaCloud pane exists and is visible.
bool cloud_mode_active() const { return m_cloud_browser && m_cloud_browser->IsShown(); } bool cloud_mode_active() const { return m_cloud_browser && m_cloud_browser->IsShown(); }
+57 -32
View File
@@ -38,6 +38,8 @@
#include <cstddef> #include <cstddef>
#include <wx/simplebook.h> #include <wx/simplebook.h>
#include <wx/dcgraph.h> #include <wx/dcgraph.h>
#include <wx/graphics.h>
#include <memory>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <wx/timer.h> #include <wx/timer.h>
@@ -1409,22 +1411,17 @@ void AMSLib::render_lite_lib(wxDC& dc)
dc.SetPen(wxPen(*wxTRANSPARENT_PEN)); dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
if (m_info.material_cols.size() > 1) { if (m_info.material_cols.size() > 1) {
int left = FromDIP(10); wxRect color_rect(FromDIP(10), FromDIP(10), libsize.x - FromDIP(18), libsize.y - FromDIP(18));
int gwidth = std::round(libsize.x / (m_info.material_cols.size() - 1));
//gradient
if (m_info.ctype == 0) { if (m_info.ctype == 0) {
for (int i = 0; i < m_info.material_cols.size() - 1; i++) { fill_gradient_rect_east(dc, color_rect, m_info.material_cols.front(), m_info.material_cols.back());
auto rect = wxRect(left, FromDIP(10), libsize.x - FromDIP(18), libsize.y - FromDIP(18));
fill_gradient_rect_east(dc, rect, m_info.material_cols[i], m_info.material_cols[i + 1]);
left += gwidth;
}
} }
else { else {
int cols_size = m_info.material_cols.size(); int cols_size = m_info.material_cols.size();
for (int i = 0; i < cols_size; i++) { for (int i = 0; i < cols_size; i++) {
dc.SetBrush(wxBrush(m_info.material_cols[i])); dc.SetBrush(wxBrush(m_info.material_cols[i]));
float x = FromDIP(10) + ((float)libsize.x - FromDIP(18)) * i / cols_size; int x = color_rect.x + color_rect.width * i / cols_size;
dc.DrawRoundedRectangle(x, FromDIP(10), ((float)libsize.x - FromDIP(17)) / cols_size, libsize.y - FromDIP(20), 0); int next_x = color_rect.x + color_rect.width * (i + 1) / cols_size;
dc.DrawRectangle(x, color_rect.y, next_x - x, color_rect.height);
} }
dc.SetBrush(wxBrush(tmp_lib_colour)); dc.SetBrush(wxBrush(tmp_lib_colour));
} }
@@ -1516,7 +1513,27 @@ void AMSLib::render_generic_lib(wxDC &dc)
int top = height - curr_height; int top = height - curr_height;
if (m_ams_model == EXT_AMS){ if (m_ams_model == EXT_AMS){
dc.DrawRoundedRectangle(FromDIP(1), FromDIP(1), size.x - FromDIP(2), size.y - FromDIP(1), m_radius - 1); wxRect color_rect(FromDIP(1), FromDIP(1), size.x - FromDIP(2), size.y - FromDIP(1));
if ((m_info.ctype == 0 || m_info.ctype == 1) && m_info.material_cols.size() > 1 && alpha != 0) {
if (m_info.ctype == 0) {
fill_gradient_rect_east(dc, color_rect, m_info.material_cols.front(), m_info.material_cols.back());
}
else {
int cols_size = static_cast<int>(m_info.material_cols.size());
for (int i = 0; i < cols_size; i++) {
dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
dc.SetBrush(wxBrush(m_info.material_cols[i]));
int x = color_rect.x + color_rect.width * i / cols_size;
int next_x = color_rect.x + color_rect.width * (i + 1) / cols_size;
dc.DrawRectangle(x, color_rect.y, next_x - x, color_rect.height);
}
}
dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
dc.SetBrush(wxBrush(tmp_lib_colour));
}
else {
dc.DrawRoundedRectangle(color_rect.x, color_rect.y, color_rect.width, color_rect.height, m_radius - 1);
}
if (alpha == 0) { if (alpha == 0) {
dc.DrawBitmap(m_bitmap_transparent_def.bmp(), FromDIP(2), FromDIP(2)); dc.DrawBitmap(m_bitmap_transparent_def.bmp(), FromDIP(2), FromDIP(2));
} }
@@ -2844,30 +2861,38 @@ void AMSPreview::doRender(wxDC &dc)
//dc.DrawRoundedRectangle((size.x - rec_size.x) / 2, (size.y - rec_size.y) / 2, rec_size.x, rec_size.y, FromDIP(2)); //dc.DrawRoundedRectangle((size.x - rec_size.x) / 2, (size.y - rec_size.y) / 2, rec_size.x, rec_size.y, FromDIP(2));
if (iter.material_cols.size() > 1) if (iter.material_cols.size() > 1)
{ {
int fleft = (size.x - AMS_ITEM_CUBE_SIZE.x) / 2; const wxSize swatch_size = m_ams_item_type == AMSModel::N3S_AMS ? AMS_ITEM_CUBE_SIZE : AMS_ITEM_CUBE_SIZE2;
const wxRect rect((size.x - swatch_size.x) / 2, (size.y - swatch_size.y) / 2, swatch_size.x, swatch_size.y);
float total_width = AMS_ITEM_CUBE_SIZE.x; const int radius = m_ams_item_type == AMSModel::N3S_AMS ? 0 : FromDIP(3);
int gwidth = (total_width / (iter.material_cols.size())); std::unique_ptr<wxGraphicsContext> owned_gc;
wxGraphicsContext* gc = dc.GetGraphicsContext();
if (!gc) {
owned_gc.reset(wxGraphicsContext::CreateFromUnknownDC(dc));
gc = owned_gc.get();
}
if (gc) {
wxGraphicsGradientStops stops(iter.material_cols.front(), iter.material_cols.back());
const size_t count = iter.material_cols.size();
if (iter.ctype == 0) { if (iter.ctype == 0) {
for (int i = 0; i < iter.material_cols.size() - 1; i++) { for (size_t i = 1; i + 1 < count; ++i)
stops.Add(iter.material_cols[i], static_cast<float>(i) / (count - 1));
if ((fleft + gwidth) > (AMS_ITEM_CUBE_SIZE.x)) { } else {
gwidth = (fleft + AMS_ITEM_CUBE_SIZE.x) - fleft; // Two stops at each boundary keep multicolour bands distinct.
} for (size_t i = 1; i < count; ++i) {
const float position = static_cast<float>(i) / count;
auto rect = wxRect(fleft, (size.y - AMS_ITEM_CUBE_SIZE.y) / 2, gwidth, AMS_ITEM_CUBE_SIZE.y); stops.Add(iter.material_cols[i - 1], position);
fill_gradient_rect_east(dc, rect, iter.material_cols[i], iter.material_cols[i + 1]); stops.Add(iter.material_cols[i], position);
fleft += gwidth;
} }
} }
else { gc->PushState();
int cols_size = iter.material_cols.size(); gc->SetPen(*wxTRANSPARENT_PEN);
for (int i = 0; i < cols_size; i++) { gc->SetBrush(gc->CreateLinearGradientBrush(rect.x, rect.y, rect.x + rect.width, rect.y, stops));
dc.SetPen(wxPen(*wxTRANSPARENT_PEN)); gc->DrawRoundedRectangle(rect.x, rect.y, rect.width, rect.height, radius);
dc.SetBrush(wxBrush(iter.material_cols[i])); gc->PopState();
float x = (size.x - AMS_ITEM_CUBE_SIZE.x) / 2 + total_width * i / cols_size; } else {
dc.DrawRectangle(x, (size.y - AMS_ITEM_CUBE_SIZE.y) / 2, total_width / cols_size, AMS_ITEM_CUBE_SIZE.y); dc.SetPen(*wxTRANSPARENT_PEN);
} dc.SetBrush(iter.material_colour);
dc.DrawRoundedRectangle(rect, radius);
} }
} }
else { else {
+1 -1
View File
@@ -32,7 +32,7 @@ END_EVENT_TABLE()
*/ */
#define TAB_BUTTON_SPACE 2 #define TAB_BUTTON_SPACE 2
#define TAB_BUTTON_PADDING_X 2 #define TAB_BUTTON_PADDING_X 3
#define TAB_BUTTON_PADDING_Y 2 #define TAB_BUTTON_PADDING_Y 2
#define TAB_BUTTON_PADDING TAB_BUTTON_PADDING_X, TAB_BUTTON_PADDING_Y #define TAB_BUTTON_PADDING TAB_BUTTON_PADDING_X, TAB_BUTTON_PADDING_Y
+27
View File
@@ -74,6 +74,10 @@ webkit_web_view_run_javascript_finish (WebKitWebView
GError **error); GError **error);
WEBKIT_API void WEBKIT_API void
webkit_javascript_result_unref (WebKitJavascriptResult *js_result); webkit_javascript_result_unref (WebKitJavascriptResult *js_result);
WEBKIT_API guint
webkit_get_major_version (void);
WEBKIT_API guint
webkit_get_minor_version (void);
} }
#endif #endif
@@ -427,6 +431,29 @@ bool WebView::NeedsRecreateOnShow()
return false; return false;
#endif #endif
} }
bool WebView::NeedsCompositingWorkaround()
{
#ifdef __linux__
const guint major = webkit_get_major_version();
const guint minor = webkit_get_minor_version();
return major < 2 || (major == 2 && minor < 42);
#else
return false;
#endif
}
bool WebView::SupportsForceSharedMemory()
{
#ifdef __linux__
const guint major = webkit_get_major_version();
const guint minor = webkit_get_minor_version();
return major > 2 || (major == 2 && minor >= 44);
#else
return false;
#endif
}
#if wxUSE_WEBVIEW_EDGE #if wxUSE_WEBVIEW_EDGE
bool WebView::CheckWebViewRuntime() bool WebView::CheckWebViewRuntime()
{ {
+10
View File
@@ -34,6 +34,16 @@ public:
// ignoring every navigation. A panel that gets true here recreates its view on first Show(). // ignoring every navigation. A panel that gets true here recreates its view on first Show().
static bool NeedsRecreateOnShow(); static bool NeedsRecreateOnShow();
// True when the linked WebKitGTK predates 2.42, where accelerated compositing
// could fail silently on XWayland sessions, so it is forced off as a fallback.
// False on every other platform. Safe to call before GTK initialises.
static bool NeedsCompositingWorkaround();
// True when the linked WebKitGTK is new enough (>= 2.44) to honour
// WEBKIT_DMABUF_RENDERER_FORCE_SHM, the shared-memory transport switch.
// False on every other platform. Safe to call before GTK initialises.
static bool SupportsForceSharedMemory();
static void RecreateAll(); static void RecreateAll();
}; };
+22 -6
View File
@@ -204,8 +204,9 @@ int BBLPrinterAgent::command_axis_control(std::string dev_id, std::string axis,
int dir = input_val > 0 ? 1 : -1; int dir = input_val > 0 ? 1 : -1;
// i3-arch printers move the bed for Y/Z, so the on-screen direction is // i3-arch printers move the bed for Y/Z, so the on-screen direction is
// reversed -- same negation the g-code fallback below applies. // reversed -- same negation the g-code fallback below applies.
if (!is_core_xy && (axis == "Y" || axis == "Z")) if (!is_core_xy && (axis == "Y" || axis == "Z")) {
dir = -dir; dir = -dir;
}
j["print"]["command"] = "xyz_ctrl"; j["print"]["command"] = "xyz_ctrl";
j["print"]["axis"] = axis; j["print"]["axis"] = axis;
@@ -215,8 +216,9 @@ int BBLPrinterAgent::command_axis_control(std::string dev_id, std::string axis,
} }
double value = input_val; double value = input_val;
if (!is_core_xy && (axis == "Y" || axis == "Z")) if (!is_core_xy && (axis == "Y" || axis == "Z")) {
value = -input_val; value = -1.0 * input_val;
}
std::string value_str = (boost::format("%.1f") % (value * unit)).str(); std::string value_str = (boost::format("%.1f") % (value * unit)).str();
std::string gcode; std::string gcode;
@@ -237,10 +239,11 @@ int BBLPrinterAgent::command_axis_control(std::string dev_id, std::string axis,
int BBLPrinterAgent::publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode) int BBLPrinterAgent::publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode)
{ {
const int rtn = lan_mode ? send_message_to_printer(dev_id, j.dump(), 0, 0) : send_message(dev_id, j.dump(), 0, 0); const int rtn = lan_mode ? send_message_to_printer(dev_id, j.dump(), 0, 0) : send_message(dev_id, j.dump(), 0, 0);
if (rtn == 0) if (rtn == 0) {
BOOST_LOG_TRIVIAL(info) << "publish_json: " << j.dump() << " code: " << rtn; BOOST_LOG_TRIVIAL(info) << "publish_json: " << j.dump() << " code: " << rtn;
else } else {
BOOST_LOG_TRIVIAL(error) << "publish_json: " << j.dump() << " code: " << rtn; BOOST_LOG_TRIVIAL(error) << "publish_json: " << j.dump() << " code: " << rtn;
}
return rtn; return rtn;
} }
@@ -586,8 +589,21 @@ int BBLPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateS
int BBLPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) int BBLPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
{ {
return dispatch_start<func_start_send_gcode_to_sdcard_legacy, func_start_send_gcode_to_sdcard_0203>( // dispatch_start() moves out of `params`, so snapshot the diagnostic fields first;
// logging them after the call would print empty strings.
const bool try_emmc_print = params.try_emmc_print;
const std::string dev_ip = params.dev_ip;
const std::string dev_id = params.dev_id;
int result = dispatch_start<func_start_send_gcode_to_sdcard_legacy, func_start_send_gcode_to_sdcard_0203>(
BBLNetworkPlugin::instance().get_start_send_gcode_to_sdcard(), params, update_fn, cancel_fn, wait_fn); BBLNetworkPlugin::instance().get_start_send_gcode_to_sdcard(), params, update_fn, cancel_fn, wait_fn);
if (result != 0) {
BOOST_LOG_TRIVIAL(error) << "start_send_gcode_to_sdcard failed: result=" << result
<< ", try_emmc_print=" << try_emmc_print
<< ", legacy_mode=" << BBLNetworkPlugin::instance().use_legacy_network()
<< ", dev_ip=" << dev_ip << ", dev_id=" << dev_id;
}
return result;
} }
int BBLPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) int BBLPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
+1
View File
@@ -110,6 +110,7 @@ public:
static std::string from_orca_payload(std::string json_text); static std::string from_orca_payload(std::string json_text);
private: private:
// why: the lan/cloud DECISION stays machine-side; keep this mechanical branch in sync with publish_json.
int publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode); int publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode);
}; };
+14 -9
View File
@@ -288,22 +288,27 @@ bool CrealityPrintAgent::parse_cfs_response(const std::string& response,
return true; return true;
} }
bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/) bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
{ {
if (device_info.dev_ip.empty()) { if (sync_mode != get_filament_sync_mode())
return false;
const MoonrakerDeviceInfo info = snapshot_device_info();
if (info.dev_ip.empty()) {
BOOST_LOG_TRIVIAL(warning) BOOST_LOG_TRIVIAL(warning)
<< "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent"; << "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent";
return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id)); return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id), sync_mode);
} }
// Build a CrealityPrint helper so we can use its model detection + WS helpers // Build a CrealityPrint helper so we can use its model detection + WS helpers
// (added in upstream PR #13291). // (added in upstream PR #13291).
DynamicPrintConfig cfg; DynamicPrintConfig cfg;
cfg.set_key_value("print_host", new ConfigOptionString("http://" + device_info.dev_ip)); cfg.set_key_value("print_host", new ConfigOptionString("http://" + info.dev_ip));
cfg.set_key_value("print_host_webui", new ConfigOptionString("")); cfg.set_key_value("print_host_webui", new ConfigOptionString(""));
cfg.set_key_value("printhost_cafile", new ConfigOptionString("")); cfg.set_key_value("printhost_cafile", new ConfigOptionString(""));
cfg.set_key_value("printhost_port", new ConfigOptionString("")); cfg.set_key_value("printhost_port", new ConfigOptionString(""));
cfg.set_key_value("printhost_apikey", new ConfigOptionString(device_info.api_key)); cfg.set_key_value("printhost_apikey", new ConfigOptionString(info.api_key));
cfg.set_key_value("printhost_ssl_ignore_revoke", new ConfigOptionBool(false)); cfg.set_key_value("printhost_ssl_ignore_revoke", new ConfigOptionBool(false));
CrealityPrint host(&cfg); CrealityPrint host(&cfg);
@@ -313,7 +318,7 @@ bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMod
BOOST_LOG_TRIVIAL(info) BOOST_LOG_TRIVIAL(info)
<< "CrealityPrintAgent: " << host.model_name() << "CrealityPrintAgent: " << host.model_name()
<< " is not CFS-capable, deferring to base Moonraker agent"; << " is not CFS-capable, deferring to base Moonraker agent";
return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id)); return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id), sync_mode);
} }
BOOST_LOG_TRIVIAL(info) BOOST_LOG_TRIVIAL(info)
@@ -328,7 +333,7 @@ bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMod
BOOST_LOG_TRIVIAL(warning) BOOST_LOG_TRIVIAL(warning)
<< "CrealityPrintAgent: CFS query failed (" << parse_err << "), " << "CrealityPrintAgent: CFS query failed (" << parse_err << "), "
<< "falling back to base agent"; << "falling back to base agent";
return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id)); return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id), sync_mode);
} }
if (box_count == 0) { if (box_count == 0) {
@@ -337,7 +342,7 @@ bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMod
// Moonraker exposes. // Moonraker exposes.
BOOST_LOG_TRIVIAL(info) BOOST_LOG_TRIVIAL(info)
<< "CrealityPrintAgent: no active CFS boxes, deferring to base agent"; << "CrealityPrintAgent: no active CFS boxes, deferring to base agent";
return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id)); return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id), sync_mode);
} }
BOOST_LOG_TRIVIAL(info) BOOST_LOG_TRIVIAL(info)
@@ -382,7 +387,7 @@ bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMod
} }
} }
build_ams_payload(box_count, max_slots - 1, trays); build_ams_payload(box_count, max_slots - 1, trays, sync_mode == FilamentSyncMode::pull);
return true; return true;
} }
+2 -1
View File
@@ -37,12 +37,13 @@ public:
}; };
explicit CrealityPrintAgent(std::string log_dir); explicit CrealityPrintAgent(std::string log_dir);
~CrealityPrintAgent() override = default; ~CrealityPrintAgent() override { shutdown(); }
static AgentInfo get_agent_info_static(); static AgentInfo get_agent_info_static();
AgentInfo get_agent_info() override { return get_agent_info_static(); } AgentInfo get_agent_info() override { return get_agent_info_static(); }
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override; bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; }
// Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info() into // Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info() into
// a flat list of loaded slots, plus the count of CFS boxes the printer reports. // a flat list of loaded slots, plus the count of CFS boxes the printer reports.
+21 -3
View File
@@ -45,6 +45,17 @@ struct PrinterConnectionParams
std::string ca_file; std::string ca_file;
}; };
// Sentinel stored as a LAN device's access code when the user has not supplied one, so the device
// stays registered (has_access_right() is !access_code.empty()). It is not a real credential: every
// printer agent must translate it back to an empty string before using/sending it on the wire.
inline constexpr const char* NO_API_KEY_SENTINEL = "*_{NO_API_KEY}_*";
// Returns an empty string when `code` is the "no API key" sentinel, otherwise `code` unchanged.
inline std::string normalize_access_code(const std::string& code)
{
return code == NO_API_KEY_SENTINEL ? std::string() : code;
}
/** /**
* FilamentSyncMode - Modes for filament data synchronization. * FilamentSyncMode - Modes for filament data synchronization.
* *
@@ -455,9 +466,16 @@ public:
virtual CameraStreamMode get_camera_stream_mode() const { return CameraStreamMode::none; } virtual CameraStreamMode get_camera_stream_mode() const { return CameraStreamMode::none; }
/** /**
* Refresh filament info from the printer synchronously. * Refresh filament info from the printer.
* Should only be called when get_filament_sync_mode() returns FilamentSyncMode::pull. *
* Populates the MachineObject's DevFilaSystem with fetched filament data. * When called with FilamentSyncMode::pull (which requires get_filament_sync_mode() to
* return pull) this is a blocking, synchronous call: when it returns true the MachineObject's
* DevFilaSystem has already been populated and the caller may read it immediately.
*
* When called with FilamentSyncMode::subscription — by an implementation's own status loop —
* the refresh may be performed asynchronously: a true return then means the refresh was
* scheduled (or is already in flight), and DevFilaSystem is updated later on the main thread.
* Callers must not assume the data is ready on return.
*/ */
virtual bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) { return false; } virtual bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) { return false; }
File diff suppressed because it is too large. Load diff
+181 -27
View File
@@ -12,6 +12,9 @@
#include <set> #include <set>
#include <string> #include <string>
#include <thread> #include <thread>
#include <chrono>
#include <condition_variable>
#include <deque>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <vector> #include <vector>
@@ -20,6 +23,56 @@ namespace Slic3r { class ICloudServiceAgent; }
namespace Slic3r { namespace Slic3r {
class Http;
bool moonraker_is_light_name(const std::string& name);
// Direction encoded by a light name: +1 turns on, -1 turns off, 0 is an ambiguous toggle.
// e.g. LIGHT_ON -> +1, LIGHT_OFF -> -1, LIGHT -> 0.
int moonraker_light_name_direction(const std::string& name);
struct MoonrakerWebcamSelection
{
std::string url;
CameraStreamMode mode = CameraStreamMode::none;
std::string name;
std::string error; // set when no selectable webcam was found
};
// Selects a webcam from a parsed /server/webcams/list response (the top-level JSON or its
// "result" value). Returns true and fills url/mode/name on success; on failure returns
// false and sets error.
bool moonraker_parse_webcam_list(const nlohmann::json& response, const std::string& base_url,
MoonrakerWebcamSelection& out);
class MoonrakerWebsocket
{
public:
enum class ReadResult
{
message,
timeout,
closed,
error,
};
MoonrakerWebsocket(bool secure, std::string api_key, std::string ca_file);
~MoonrakerWebsocket();
void connect(const std::string& host, const std::string& port, std::chrono::seconds timeout);
void tls_handshake(const std::string& host);
void handshake(const std::string& host, const std::string& target);
void text(bool enabled);
void write(const std::string& body);
ReadResult read(std::string& payload, std::string& error_message);
void close();
void expires_after(std::chrono::seconds timeout);
void abort();
private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};
class MoonrakerPrinterAgent : public IPrinterAgent class MoonrakerPrinterAgent : public IPrinterAgent
{ {
public: public:
@@ -65,12 +118,24 @@ public:
int set_on_local_connect_fn(OnLocalConnectedFn fn) override; int set_on_local_connect_fn(OnLocalConnectedFn fn) override;
int set_on_local_message_fn(OnMessageFn fn) override; int set_on_local_message_fn(OnMessageFn fn) override;
int set_queue_on_main_fn(QueueOnMainFn fn) override; int set_queue_on_main_fn(QueueOnMainFn fn) override;
// Pull-mode agent (on-demand filament sync)
FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; }
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override; bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
CameraStreamMode get_camera_stream_mode() const override;
std::string get_camera_url() const override;
// Called by the fetch-thread RAII guard when a background filament fetch finishes.
// Serialized with the reservation so shutdown()'s wait cannot miss the transition to 0.
void release_fetch_slot() noexcept;
protected: protected:
struct ConnectionSettings
{
std::string dev_id;
std::string base_url;
std::string api_key;
bool use_ssl = false;
std::string ca_file;
};
struct MoonrakerDeviceInfo struct MoonrakerDeviceInfo
{ {
std::string dev_id; std::string dev_id;
@@ -82,7 +147,9 @@ protected:
std::string dev_name; std::string dev_name;
std::string version; std::string version;
std::string klippy_state; std::string klippy_state;
float nozzle_diameter = 0.0f;
bool use_ssl = false; bool use_ssl = false;
std::string ca_file;
} device_info; } device_info;
// Tray data for AMS payload building // Tray data for AMS payload building
@@ -96,16 +163,44 @@ protected:
int nozzle_temp = 0; // Optional int nozzle_temp = 0; // Optional
}; };
// Build ams JSON and call parser // Build ams JSON and call parser.
void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays); // apply_inline: when true the MachineObject mutation runs on the calling thread, which
// must therefore be the main thread; when false it is deferred through queue_on_main_fn.
// Pull-mode fetches run synchronously on the GUI thread and are read back immediately, so
// they pass true to avoid the deferred mutation landing after the caller's read.
//
// resolve_tray_ids: optional hook that runs as part of the commit, i.e. on the main thread.
// Agents that would otherwise resolve tray_info_idx against GUI-owned preset state on a
// background fetch thread must do it here to avoid racing the GUI. It must not capture
// `this` (the commit may outlive the agent).
using ResolveAmsTrayIdsFn = std::function<void(std::vector<AmsTrayData>&)>;
void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays, bool apply_inline = false,
ResolveAmsTrayIdsFn resolve_tray_ids = {});
// Methods that derived classes may need to override or access // Methods that derived classes may need to override or access
virtual bool init_device_info(const std::string& dev_id, const std::string& dev_ip, const std::string& username, const std::string& password, bool use_ssl, const std::string& port); virtual bool init_device_info(const PrinterConnectionParams& params);
virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; virtual bool fetch_device_info(const ConnectionSettings& connection, MoonrakerDeviceInfo& info, std::string& error) const;
ConnectionSettings get_connection_settings() const;
// Copy of the mutable connection state, taken under connect_mutex. Background threads
// must use this instead of reading device_info directly.
MoonrakerDeviceInfo snapshot_device_info() const;
void configure_http(Http& http, const ConnectionSettings& connection) const;
static float parse_nozzle_diameter(const nlohmann::json& response);
// State access for derived classes // State access for derived classes
mutable std::recursive_mutex state_mutex; mutable std::recursive_mutex state_mutex;
// Detached fetch threads hold a raw `this`; shutdown() waits for this to reach 0.
std::atomic<int> filament_fetch_in_flight{0};
// Idempotent teardown; must be called from the most-derived destructor.
void shutdown();
std::atomic<bool> shutting_down{false};
// Serializes the shutting_down check with the in-flight reservation.
std::mutex fetch_lifecycle_mutex;
std::condition_variable fetch_done_cv; // notified when filament_fetch_in_flight reaches 0
// Helpers // Helpers
bool is_numeric(const std::string& value); bool is_numeric(const std::string& value);
std::string normalize_base_url(bool use_ssl, const std::string& host, const std::string& port); std::string normalize_base_url(bool use_ssl, const std::string& host, const std::string& port);
@@ -118,13 +213,30 @@ protected:
// Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility // Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility
static std::string map_filament_type_to_generic_id(const std::string& filament_type); static std::string map_filament_type_to_generic_id(const std::string& filament_type);
// Send a G-code script via Moonraker (/printer/gcode/script)
bool send_gcode(const std::string& dev_id, const std::string& gcode) const;
bool send_gcode(const std::string& dev_id, const std::string& gcode,
const ConnectionSettings& connection) const;
bool post_print_action(const std::string& action) const;
bool post_print_action(const std::string& action,
const ConnectionSettings& connection) const;
bool send_ws_rpc(const std::string& method, const nlohmann::json& params);
virtual void on_status_loop_tick(const std::string& dev_id) {}
// Queue work that may use agent state. The command worker is joined during
// destruction, so queued commands cannot outlive the agent.
void enqueue_command(std::function<void()> fn);
mutable std::recursive_mutex connect_mutex;
private: private:
int handle_request(const std::string& dev_id, const std::string& json_str); int handle_request(const std::string& dev_id, const std::string& json_str);
int send_version_info(const std::string& dev_id); int send_version_info(const std::string& dev_id);
int send_access_code(const std::string& dev_id); int send_access_code(const std::string& dev_id);
bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set<std::string>& objects, std::string& error) const; bool fetch_object_list(const ConnectionSettings& connection, std::set<std::string>& objects, std::string& error) const;
bool query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const; bool query_printer_status(const ConnectionSettings& connection, nlohmann::json& status, std::string& error) const;
bool send_gcode_sync(const std::string& dev_id, const std::string& gcode) const; bool send_gcode_sync(const std::string& dev_id, const std::string& gcode) const;
void send_gcode_async(const std::string& dev_id, const std::string& gcode, void send_gcode_async(const std::string& dev_id, const std::string& gcode,
std::function<void(bool)> on_result = {}) const; std::function<void(bool)> on_result = {}) const;
@@ -132,37 +244,45 @@ private:
void announce_printhost_device(); void announce_printhost_device();
void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg); void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg);
void dispatch_printer_connected(const std::string& dev_id); void dispatch_printer_connected(const std::string& dev_id);
// Self-contained snapshot of the message callbacks. Async completions capture this
// by value so they never dereference `this` after the agent may have been destroyed.
struct MessageRouter
{
OnMessageFn local_fn;
OnMessageFn cloud_fn;
QueueOnMainFn queue_fn;
std::string dev_id;
void deliver(std::string payload) const;
};
MessageRouter make_message_router(const std::string& dev_id) const;
void dispatch_message(const std::string& dev_id, const std::string& payload); void dispatch_message(const std::string& dev_id, const std::string& payload);
void start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key); void start_status_stream(const std::string& dev_id, ConnectionSettings connection);
void stop_status_stream(); void stop_status_stream();
void run_status_stream(std::string dev_id, std::string base_url, std::string api_key); void run_status_stream(std::string dev_id, ConnectionSettings connection);
void handle_ws_message(const std::string& dev_id, const std::string& payload); void handle_ws_message(std::string dev_id, std::string payload, ConnectionSettings connection);
void refresh_thumbnail_url(const ConnectionSettings& connection);
void update_status_cache(const nlohmann::json& updates); void update_status_cache(const nlohmann::json& updates);
nlohmann::json build_print_payload_locked() const; nlohmann::json build_print_payload_locked() const;
// Print control helpers
int pause_print(const std::string& dev_id);
int resume_print(const std::string& dev_id);
int cancel_print(const std::string& dev_id);
// File upload // File upload
bool upload_gcode(const std::string& local_path, const std::string& filename, bool upload_gcode(const std::string& local_path, const std::string& filename,
const std::string& base_url, const std::string& api_key, const ConnectionSettings& connection,
OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn);
// JSON-RPC helper
bool send_jsonrpc_command(const std::string& base_url, const std::string& api_key,
const nlohmann::json& request, std::string& response) const;
// Connection thread management // Connection thread management
void perform_connection_async(const std::string& dev_id, void perform_connection_async(const std::string& dev_id,
const std::string& base_url, ConnectionSettings connection,
const std::string& api_key,
uint64_t generation); uint64_t generation);
void refresh_webcam_info() const;
bool fetch_webcam_info(const ConnectionSettings& connection, uint64_t generation) const;
// System-specific filament fetch methods // System-specific filament fetch methods
bool fetch_hh_filament_info(std::vector<AmsTrayData>& trays, int& max_lane_index); bool fetch_hh_filament_info(const ConnectionSettings& connection, std::vector<AmsTrayData>& trays, int& max_lane_index);
bool fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index); bool fetch_moonraker_filament_data(const ConnectionSettings& connection, std::vector<AmsTrayData>& trays, int& max_lane_index);
// JSON helper methods // JSON helper methods
static std::string safe_json_string(const nlohmann::json& obj, const char* key); static std::string safe_json_string(const nlohmann::json& obj, const char* key);
@@ -188,15 +308,43 @@ private:
mutable std::recursive_mutex payload_mutex; mutable std::recursive_mutex payload_mutex;
nlohmann::json status_cache; nlohmann::json status_cache;
// note: guarded by payload_mutex; filled by refresh_thumbnail_url(), empty url = looked up, none found
std::string thumbnail_filename;
std::string thumbnail_url;
mutable std::string webcam_stream_url;
mutable CameraStreamMode webcam_stream_mode = CameraStreamMode::none;
// Next time the status loop may look the webcam up again (steady_clock ms).
mutable std::atomic<uint64_t> webcam_info_next_attempt_ms{0};
unsigned thumbnail_lookup_attempts = 0;
static constexpr uint64_t WEBCAM_INFO_REFRESH_INTERVAL_MS = 30000;
static constexpr uint64_t WEBCAM_INFO_FAILURE_BACKOFF_MS = 60000;
std::atomic<int> next_jsonrpc_id{1}; std::atomic<int> next_jsonrpc_id{1};
std::set<std::string> available_objects; // Track for feature detection std::set<std::string> available_objects; // Track for feature detection
bool assumed_light_on = false;
std::atomic<bool> ws_stop{false}; std::atomic<bool> ws_stop{false};
std::atomic<bool> ws_reconnect_requested{false}; // Flag to trigger reconnection std::atomic<bool> ws_reconnect_requested{false}; // Flag to trigger reconnection
std::atomic<uint64_t> ws_last_emit_ms{0}; std::atomic<uint64_t> ws_last_emit_ms{0};
std::thread ws_thread; std::thread ws_thread;
// stop_status_stream() invokes ws_abort_io to wake a blocked synchronous
// ws.read()/ws.write()/handshake in run_status_stream(): ws_stop is only
// observed between reads, and Beast's expires_after() does not bound
// synchronous operations.
std::mutex ws_abort_mutex;
std::function<void()> ws_abort_io; // guarded by ws_abort_mutex
// Interrupts the exponential reconnect backoff in run_status_stream() when stopping.
std::mutex ws_wait_mutex;
std::condition_variable ws_wait_cv;
// AMS/filament refresh cadence, independent of telemetry dispatch so a steady
// stream of status updates can't starve it (ws_last_emit_ms is reset by those).
static constexpr uint64_t AMS_REFRESH_INTERVAL_MS = 10000;
std::atomic<uint64_t> ams_last_fetch_ms{0};
// Throttling configuration for WebSocket updates // Throttling configuration for WebSocket updates
// Critical changes (state transitions) dispatch immediately; telemetry is throttled // Critical changes (state transitions) dispatch immediately; telemetry is throttled
static constexpr uint64_t STATUS_UPDATE_INTERVAL_MS = 1000; // 1 update/sec for telemetry static constexpr uint64_t STATUS_UPDATE_INTERVAL_MS = 1000; // 1 update/sec for telemetry
@@ -206,7 +354,13 @@ private:
// Connection thread management // Connection thread management
std::atomic<uint64_t> connect_generation{0}; std::atomic<uint64_t> connect_generation{0};
std::thread connect_thread; std::thread connect_thread;
std::recursive_mutex connect_mutex;
void run_command_worker();
std::thread cmd_thread;
std::deque<std::function<void()>> cmd_queue;
std::mutex cmd_mutex;
std::condition_variable cmd_cv;
bool cmd_stop = false;
}; };
} // namespace Slic3r } // namespace Slic3r
+223 -45
View File
@@ -2,18 +2,25 @@
#include "Http.hpp" #include "Http.hpp"
#include "MoonrakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp"
#include "IPrinterAgent.hpp" #include "IPrinterAgent.hpp"
#include "bambu_networking.hpp"
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "nlohmann/json.hpp" #include "nlohmann/json.hpp"
#include <atomic>
#include <boost/algorithm/string.hpp> #include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/trim.hpp> #include <boost/algorithm/string/trim.hpp>
#include <boost/algorithm/string/case_conv.hpp> #include <boost/algorithm/string/case_conv.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <cctype> #include <cctype>
#include <cmath>
#include "libslic3r/Preset.hpp" #include "libslic3r/Preset.hpp"
#include <cstddef>
#include <exception>
#include <map> #include <map>
#include <mutex>
#include <sstream> #include <sstream>
#include <thread>
#include <string> #include <string>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -34,6 +41,40 @@ bool has_visible_base_preset(const PresetCollection& filaments, const std::strin
return false; return false;
} }
// RAII release of the in-flight fetch slot; movable so a failed thread start still releases it.
struct InFlightGuard
{
MoonrakerPrinterAgent* owner;
explicit InFlightGuard(MoonrakerPrinterAgent& o) noexcept : owner(&o) {}
InFlightGuard(InFlightGuard&& other) noexcept : owner(other.owner) { other.owner = nullptr; }
InFlightGuard(const InFlightGuard&) = delete;
InFlightGuard& operator=(const InFlightGuard&) = delete;
InFlightGuard& operator=(InFlightGuard&&) = delete;
~InFlightGuard() { if (owner) owner->release_fetch_slot(); }
};
// nlohmann::json::value() returns the default only when the key is absent; a present but
// null/wrong-typed value throws. Firmware JSON is untrusted, so read defensively.
int read_int_or(const nlohmann::json& obj, const std::string& key, int fallback)
{
auto it = obj.find(key);
if (it == obj.end()) {
return fallback;
}
if (it->is_number_integer()) {
return it->get<int>();
}
// Firmware may report an integral field as a JSON float (e.g. 2.0). Accept it only when it
// has no fractional part, so a genuinely fractional value falls back instead of truncating.
if (it->is_number_float()) {
const double value = it->get<double>();
if (value == std::floor(value)) {
return static_cast<int>(value);
}
}
return fallback;
}
} // anonymous namespace } // anonymous namespace
const std::string QidiPrinterAgent_VERSION = "0.0.1"; const std::string QidiPrinterAgent_VERSION = "0.0.1";
@@ -47,51 +88,179 @@ AgentInfo QidiPrinterAgent::get_agent_info_static()
return AgentInfo{"qidi", "Qidi", QidiPrinterAgent_VERSION, "Qidi printer agent"}; return AgentInfo{"qidi", "Qidi", QidiPrinterAgent_VERSION, "Qidi printer agent"};
} }
bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/) FilamentSyncMode QidiPrinterAgent::get_filament_sync_mode() const
{ {
if (GUI::wxGetApp().app_config->get_bool("use_printer_agents"))
return FilamentSyncMode::subscription;
return FilamentSyncMode::pull;
}
bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
{
if (sync_mode != get_filament_sync_mode())
return false;
// Snapshot what the fetch needs; a reconnect can rewrite device_info meanwhile.
ConnectionSettings connection = get_connection_settings();
std::string model_id;
std::string model_name;
{
std::lock_guard<std::recursive_mutex> lock(connect_mutex);
model_id = device_info.model_id;
model_name = device_info.model_name;
}
// One implementation for both modes; only where it runs differs. pull is the documented
// blocking contract (the GUI thread reads DevFilaSystem right after the call), so it runs
// on the caller with the payload applied inline. subscription runs on a background thread
// so the status loop is not stalled, and the payload is marshalled back to the main thread.
auto work = [this, connection = std::move(connection), model_id, model_name](bool apply_inline) -> bool {
try {
std::string error; std::string error;
// 1. Fetch device info and infer series_id // 1. Fetch device info and infer series_id
std::string series_id; std::string series_id;
{ {
MoonrakerDeviceInfo info; MoonrakerDeviceInfo info;
if (fetch_device_info(device_info.base_url, device_info.api_key, info, error)) { if (fetch_device_info(connection, info, error)) {
series_id = infer_series_id(info.model_id, info.dev_name); series_id = infer_series_id(info.model_id, info.dev_name);
} }
} }
if (series_id.empty()) { if (series_id.empty()) {
// Fall back to the configured Orca model if Moonraker doesn't expose a usable identifier. // Fall back to the configured Orca model if Moonraker doesn't expose a usable identifier.
series_id = infer_series_id(device_info.model_id, device_info.model_name); series_id = infer_series_id(model_id, model_name);
} }
// 2. Fetch filament dictionary // 2. Fetch filament dictionary
QidiFilamentDict dict; QidiFilamentDict dict;
if (!fetch_filament_dict(device_info.base_url, device_info.api_key, dict, error)) { if (!fetch_filament_dict(connection, dict, error)) {
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error; BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error;
} }
// 3. Fetch slot info and build AmsTrayData directly // 3. Fetch slot info and build AmsTrayData directly
std::vector<AmsTrayData> trays; std::vector<AmsTrayData> trays;
int box_count = 0; int box_count = 0;
if (!fetch_slot_info(device_info.base_url, device_info.api_key, dict, series_id, trays, box_count, error)) { if (!fetch_slot_info(connection, dict, series_id, trays, box_count, error)) {
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error; BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error;
return false; return false;
} }
// 4. Build the AMS payload // 4. Build the AMS payload, resolving preset-dependent ids on the main thread.
build_ams_payload(box_count, box_count * 4 - 1, trays); auto resolve_ids = [](std::vector<AmsTrayData>& resolved_trays) {
auto* bundle = GUI::wxGetApp().preset_bundle;
if (!bundle) {
return; // keep the Qidi-specific ids computed from device data
}
for (auto& tray : resolved_trays) {
if (!tray.has_filament) {
continue;
}
if (!tray.tray_info_idx.empty() && has_visible_base_preset(bundle->filaments, tray.tray_info_idx)) {
continue;
}
tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type);
}
};
build_ams_payload(box_count, box_count * 4 - 1, trays, apply_inline, resolve_ids);
return true;
} catch (const std::exception& e) {
// why: an exception escaping a detached thread is std::terminate, and firmware
// JSON is untrusted; mirror run_command_worker and swallow it here.
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: unhandled exception: " << e.what();
return false;
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: unhandled exception";
return false;
}
};
if (sync_mode == FilamentSyncMode::pull) {
return work(/*apply_inline=*/true);
}
// Subscription: fire-and-forget. A `true` return means the refresh was scheduled (or is
// already running), not that DevFilaSystem has been updated. Reserve under the same mutex
// shutdown() uses, so the flag and the count can't race.
{
std::lock_guard<std::mutex> lock(fetch_lifecycle_mutex);
if (shutting_down.load())
return false;
if (filament_fetch_in_flight.load() > 0)
return true; // a fetch is already running; don't pile on
filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed);
}
InFlightGuard guard{*this};
std::thread([work = std::move(work), guard = std::move(guard)]() mutable {
work(/*apply_inline=*/false);
}).detach();
return true; return true;
} }
bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, bool QidiPrinterAgent::apply_box_mapping(const PrintParams& params) const
const std::string& api_key, {
// enable_box mirrors task_use_ams: engage the multi-color box only when this
// job actually routes filament through it. (See qidi-ams-findings.md §2/§8.3 —
// if firmware treats enable_box as "a box exists" rather than "use it this job",
// switch this gate to HasAms()/box_count instead.)
const int enable = params.task_use_ams ? 1 : 0;
const std::string dev_id = get_connection_settings().dev_id;
// Build one gcode/script request instead of N blocking HTTP calls: apply_box_mapping
// runs on the caller's (GUI) thread before the print starts, so per-tool round trips
// would freeze the UI.
std::string script = "SAVE_VARIABLE VARIABLE=enable_box VALUE=" + std::to_string(enable);
// When the box isn't used this job, leave the existing value_t<tool> slot
// assignments untouched (enable_box=0 is enough to disengage it).
if (enable) {
if (params.ams_mapping.empty()) {
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::apply_box_mapping: enable_box set but ams_mapping is empty";
} else {
// ams_mapping (v0) is a JSON array indexed by filament/tool; each value is the
// physical box slot (-1 = unmapped). Mirror it onto value_t<tool>.
auto mapping = nlohmann::json::parse(params.ams_mapping, nullptr, /*allow_exceptions*/ false);
if (mapping.is_discarded() || !mapping.is_array()) {
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: invalid ams_mapping: " << params.ams_mapping;
return false;
}
for (size_t tool = 0; tool < mapping.size(); ++tool) {
if (!mapping[tool].is_number_integer())
continue;
const int slot = mapping[tool].get<int>();
if (slot < 0)
continue; // unmapped filament — skip
script += "\nSAVE_VARIABLE VARIABLE=value_t" + std::to_string(tool) +
" VALUE=\"'slot" + std::to_string(slot) + "'\"";
}
}
}
if (!send_gcode(dev_id, script)) {
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: failed to send box mapping";
return false;
}
return true;
}
int QidiPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
{
// Only the LAN print path is implemented by the Moonraker base; the box config is emitted
// here, before the print starts. The cloud/record/sdcard variants inherited from the base
// deliberately return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED without side effects.
if (!apply_box_mapping(params))
return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED;
return MoonrakerPrinterAgent::start_local_print(std::move(params), update_fn, cancel_fn);
}
bool QidiPrinterAgent::fetch_slot_info(const ConnectionSettings& connection,
const QidiFilamentDict& dict, const QidiFilamentDict& dict,
const std::string& series_id, const std::string& series_id,
std::vector<AmsTrayData>& trays, std::vector<AmsTrayData>& trays,
int& box_count, int& box_count,
std::string& error) std::string& error)
{ {
std::string url = join_url(base_url, "/printer/objects/query?save_variables=variables"); std::string url = join_url(connection.base_url, "/printer/objects/query?save_variables=variables");
for (int i = 0; i < 16; ++i) { for (int i = 0; i < 16; ++i) {
url += "&box_stepper%20slot" + std::to_string(i) + "=runout_button"; url += "&box_stepper%20slot" + std::to_string(i) + "=runout_button";
} }
@@ -101,8 +270,9 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
std::string http_error; std::string http_error;
auto http = Http::get(url); auto http = Http::get(url);
if (!api_key.empty()) { configure_http(http, connection);
http.header("X-Api-Key", api_key); if (!connection.api_key.empty()) {
http.header("X-Api-Key", connection.api_key);
} }
http.timeout_connect(5) http.timeout_connect(5)
.timeout_max(10) .timeout_max(10)
@@ -127,22 +297,12 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
return false; return false;
} }
auto json = nlohmann::json::parse(response_body, nullptr, false, true); nlohmann::json status;
if (json.is_discarded()) { nlohmann::json variables;
error = "Invalid JSON response"; if (!parse_slot_response(response_body, status, variables, error))
return false; return false;
}
if (!json.contains("result") || !json["result"].contains("status") || !json["result"]["status"].contains("save_variables") || box_count = read_int_or(variables, "box_count", 1);
!json["result"]["status"]["save_variables"].contains("variables")) {
error = "Unexpected JSON structure";
return false;
}
auto& variables = json["result"]["status"]["save_variables"]["variables"];
auto& status = json["result"]["status"];
box_count = variables.value("box_count", 1);
if (box_count < 0) { if (box_count < 0) {
box_count = 0; box_count = 0;
} }
@@ -165,9 +325,9 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
tray.slot_index = i; tray.slot_index = i;
// Read slot variables // Read slot variables
const int color_index = variables.value("color_slot" + std::to_string(i), 1); const int color_index = read_int_or(variables, "color_slot" + std::to_string(i), 1);
const int filament_type = variables.value("filament_slot" + std::to_string(i), 1); const int filament_type = read_int_or(variables, "filament_slot" + std::to_string(i), 1);
const int vendor_type = variables.value("vendor_slot" + std::to_string(i), 0); const int vendor_type = read_int_or(variables, "vendor_slot" + std::to_string(i), 0);
// Check filament presence via runout sensor // Check filament presence via runout sensor
std::string box_stepper_key = "box_stepper slot" + std::to_string(i); std::string box_stepper_key = "box_stepper slot" + std::to_string(i);
@@ -175,7 +335,7 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
if (status.contains(box_stepper_key)) { if (status.contains(box_stepper_key)) {
auto& box_stepper = status[box_stepper_key]; auto& box_stepper = status[box_stepper_key];
if (box_stepper.contains("runout_button") && !box_stepper["runout_button"].is_null()) { if (box_stepper.contains("runout_button") && !box_stepper["runout_button"].is_null()) {
int runout_button = box_stepper["runout_button"].template get<int>(); const int runout_button = read_int_or(box_stepper, "runout_button", 0);
tray.has_filament = (runout_button == 0); tray.has_filament = (runout_button == 0);
} }
} }
@@ -189,16 +349,9 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
} }
tray.tray_type = normalize_filament_type(filament_name); tray.tray_type = normalize_filament_type(filament_name);
// Try Qidi-specific setting ID first; fall back to visible preset by type // Qidi-specific setting id derived from device data only; the preset-dependent
std::string setting_id = build_setting_id(filament_type, vendor_type, tray.tray_type); // fallback runs on the main thread in the resolve hook passed to build_ams_payload.
auto* bundle = GUI::wxGetApp().preset_bundle; tray.tray_info_idx = build_setting_id(filament_type, vendor_type, tray.tray_type);
if (!bundle) {
tray.tray_info_idx = setting_id;
} else if (!setting_id.empty() && has_visible_base_preset(bundle->filaments, setting_id)) {
tray.tray_info_idx = setting_id;
} else {
tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type);
}
// Look up color from dictionary // Look up color from dictionary
auto color_it = dict.colors.find(color_index); auto color_it = dict.colors.find(color_index);
@@ -215,20 +368,45 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
return true; return true;
} }
bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url, bool QidiPrinterAgent::parse_slot_response(const std::string& response_body,
const std::string& api_key, nlohmann::json& status,
nlohmann::json& variables,
std::string& error)
{
auto json = nlohmann::json::parse(response_body, nullptr, false, true);
if (json.is_discarded()) {
error = "Invalid JSON response";
return false;
}
if (!json.is_object() || !json.contains("result") || !json["result"].is_object() || !json["result"].contains("status") ||
!json["result"]["status"].is_object() || !json["result"]["status"].contains("save_variables") ||
!json["result"]["status"]["save_variables"].is_object() || !json["result"]["status"]["save_variables"].contains("variables") ||
!json["result"]["status"]["save_variables"]["variables"].is_object()) {
// why: Qidi firmware may send null here, but json::value() throws for it.
error = "Unexpected JSON structure: save_variables.variables must be an object";
return false;
}
status = json["result"]["status"];
variables = status["save_variables"]["variables"];
return true;
}
bool QidiPrinterAgent::fetch_filament_dict(const ConnectionSettings& connection,
QidiFilamentDict& dict, QidiFilamentDict& dict,
std::string& error) const std::string& error) const
{ {
std::string url = join_url(base_url, "/server/files/config/officiall_filas_list.cfg"); std::string url = join_url(connection.base_url, "/server/files/config/officiall_filas_list.cfg");
std::string response_body; std::string response_body;
bool success = false; bool success = false;
std::string http_error; std::string http_error;
auto http = Http::get(url); auto http = Http::get(url);
if (!api_key.empty()) { configure_http(http, connection);
http.header("X-Api-Key", api_key); if (!connection.api_key.empty()) {
http.header("X-Api-Key", connection.api_key);
} }
http.timeout_connect(5) http.timeout_connect(5)
.timeout_max(10) .timeout_max(10)
+20 -4
View File
@@ -3,6 +3,8 @@
#include "IPrinterAgent.hpp" #include "IPrinterAgent.hpp"
#include "MoonrakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp"
#include "bambu_networking.hpp"
#include "nlohmann/json_fwd.hpp"
#include <map> #include <map>
#include <string> #include <string>
@@ -14,7 +16,7 @@ class QidiPrinterAgent final : public MoonrakerPrinterAgent
{ {
public: public:
explicit QidiPrinterAgent(std::string log_dir); explicit QidiPrinterAgent(std::string log_dir);
~QidiPrinterAgent() override = default; ~QidiPrinterAgent() override { shutdown(); }
static AgentInfo get_agent_info_static(); static AgentInfo get_agent_info_static();
AgentInfo get_agent_info() override { return get_agent_info_static(); } AgentInfo get_agent_info() override { return get_agent_info_static(); }
@@ -22,7 +24,22 @@ public:
// Override filament sync (Qidi-specific implementation) // Override filament sync (Qidi-specific implementation)
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override; bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
static bool parse_slot_response(const std::string& response_body,
nlohmann::json& status,
nlohmann::json& variables,
std::string& error);
// Print operations — emit QiDi multi-color box config, then delegate to base.
// Only the LAN print path is supported by the base; the cloud/record/sdcard variants
// are inherited and return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED.
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override;
FilamentSyncMode get_filament_sync_mode() const override;
private: private:
// Push enable_box + value_t<tool> SAVE_VARIABLEs before a print starts.
// Returns false if any command fails (caller should abort the print).
bool apply_box_mapping(const PrintParams& params) const;
struct QidiFilamentDict struct QidiFilamentDict
{ {
std::map<int, std::string> colors; std::map<int, std::string> colors;
@@ -30,14 +47,13 @@ private:
}; };
// Qidi-specific methods // Qidi-specific methods
bool fetch_slot_info(const std::string& base_url, bool fetch_slot_info(const ConnectionSettings& connection,
const std::string& api_key,
const QidiFilamentDict& dict, const QidiFilamentDict& dict,
const std::string& series_id, const std::string& series_id,
std::vector<AmsTrayData>& trays, std::vector<AmsTrayData>& trays,
int& box_count, int& box_count,
std::string& error); std::string& error);
bool fetch_filament_dict(const std::string& base_url, const std::string& api_key, QidiFilamentDict& dict, std::string& error) const; bool fetch_filament_dict(const ConnectionSettings& connection, QidiFilamentDict& dict, std::string& error) const;
std::string normalize_filament_type(const std::string& filament_type); std::string normalize_filament_type(const std::string& filament_type);
std::string infer_series_id(const std::string& model_id, const std::string& dev_name); std::string infer_series_id(const std::string& model_id, const std::string& dev_name);
std::string normalize_model_key(std::string value); std::string normalize_model_key(std::string value);
+226 -40
View File
@@ -2,11 +2,20 @@
#include "Http.hpp" #include "Http.hpp"
#include "MoonrakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp"
#include "IPrinterAgent.hpp" #include "IPrinterAgent.hpp"
#include "bambu_networking.hpp"
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "nlohmann/json.hpp" #include "nlohmann/json.hpp"
#include <atomic>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <exception>
#include <mutex>
#include <sstream>
#include <thread>
#include <vector> #include <vector>
#include <string> #include <string>
#include "libslic3r/Preset.hpp" #include "libslic3r/Preset.hpp"
@@ -18,6 +27,91 @@ namespace Slic3r {
namespace { namespace {
constexpr const char* SNAPMAKER_AGENT_VERSION = "0.0.1"; constexpr const char* SNAPMAKER_AGENT_VERSION = "0.0.1";
constexpr int64_t CAMERA_REFRESH_INTERVAL_MS = 300'000;
int64_t now_ms()
{
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
}
// RAII release of the in-flight fetch slot; movable so a failed thread start still releases it.
struct InFlightGuard
{
MoonrakerPrinterAgent* owner;
explicit InFlightGuard(MoonrakerPrinterAgent& o) noexcept : owner(&o) {}
InFlightGuard(InFlightGuard&& other) noexcept : owner(other.owner) { other.owner = nullptr; }
InFlightGuard(const InFlightGuard&) = delete;
InFlightGuard& operator=(const InFlightGuard&) = delete;
InFlightGuard& operator=(InFlightGuard&&) = delete;
~InFlightGuard() { if (owner) owner->release_fetch_slot(); }
};
// nlohmann::json::value() returns the default only when the key is absent; a present but
// null/wrong-typed value throws. Firmware JSON is untrusted, so read defensively.
int read_int_or(const nlohmann::json& obj, const char* key, int fallback)
{
auto it = obj.find(key);
if (it == obj.end()) {
return fallback;
}
if (it->is_number_integer()) {
return it->get<int>();
}
// Firmware may report an integral field as a JSON float (e.g. 2.0). Accept it only when it
// has no fractional part, so a genuinely fractional value falls back instead of truncating.
if (it->is_number_float()) {
const double value = it->get<double>();
if (value == std::floor(value)) {
return static_cast<int>(value);
}
}
return fallback;
}
std::vector<std::string> read_string_array_or(const nlohmann::json& obj, const char* key)
{
auto it = obj.find(key);
if (it == obj.end() || !it->is_array())
return {};
std::vector<std::string> out;
out.reserve(it->size());
for (const auto& v : *it)
out.push_back(v.is_string() ? v.get<std::string>() : std::string{});
return out;
}
std::vector<bool> read_bool_array_or(const nlohmann::json& obj, const char* key)
{
auto it = obj.find(key);
if (it == obj.end() || !it->is_array())
return {};
std::vector<bool> out;
out.reserve(it->size());
for (const auto& v : *it)
out.push_back(v.is_boolean() ? v.get<bool>() : false);
return out;
}
// Parse a hex colour, stopping at the first non-hex character. std::stoul throws on
// empty/non-hex input, which must not escape the detached fetch thread.
unsigned int parse_hex_color(const std::string& hex)
{
unsigned int value = 0;
for (char c : hex) {
int digit;
if (c >= '0' && c <= '9')
digit = c - '0';
else if (c >= 'a' && c <= 'f')
digit = c - 'a' + 10;
else if (c >= 'A' && c <= 'F')
digit = c - 'A' + 10;
else
break;
value = (value << 4) | static_cast<unsigned int>(digit);
}
return value;
}
// Safely access a parallel array by index, returning a fallback if out of bounds. // Safely access a parallel array by index, returning a fallback if out of bounds.
template<typename T> template<typename T>
@@ -40,14 +134,16 @@ std::string find_closest_color_preset_by_vendor_and_type(const PresetCollection&
filaments.get_preset_base(p) == &p && p.config.opt_string("filament_vendor", 0u) == vendor_name && filaments.get_preset_base(p) == &p && p.config.opt_string("filament_vendor", 0u) == vendor_name &&
p.config.opt_string("filament_type", 0u) == filament_type) { p.config.opt_string("filament_type", 0u) == filament_type) {
// The printer returns RGBA in the format RRGGBBAA, but profiles store color as #RRGGBB, // The printer returns RGBA in the format RRGGBBAA, but profiles store color as #RRGGBB,
// so we must remove # and ignore alpha channel for distance calculation // so we must remove # and ignore alpha channel for distance calculation. Firmware
unsigned int target_color_value = std::stoul(color_rgba.substr(0, color_rgba.length() - 2), nullptr, 16); // colours are untrusted; parse_hex_color tolerates empty/non-hex instead of throwing.
unsigned int target_color_value =
parse_hex_color(color_rgba.substr(0, color_rgba.size() >= 2 ? color_rgba.size() - 2 : 0));
std::string p_color = p.config.opt_string("default_filament_colour", 0u); std::string p_color = p.config.opt_string("default_filament_colour", 0u);
unsigned int p_color_value; unsigned int p_color_value = 0;
if (!p_color.empty()) { if (!p_color.empty()) {
size_t hash_pos = p_color.find("#"); size_t hash_pos = p_color.find("#");
p_color_value = std::stoul(p_color.substr(hash_pos != std::string::npos ? hash_pos + 1 : 0), nullptr, 16); p_color_value = parse_hex_color(p_color.substr(hash_pos != std::string::npos ? hash_pos + 1 : 0));
} else { } else {
// Default to black if no color specified in profile. Assume other profiles might be a closer color match. // Default to black if no color specified in profile. Assume other profiles might be a closer color match.
// Could be a problem if the target color is also black and there exist a specific profile for that type, vendor and color // Could be a problem if the target color is also black and there exist a specific profile for that type, vendor and color
@@ -74,6 +170,31 @@ std::string find_closest_color_preset_by_vendor_and_type(const PresetCollection&
SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {} SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {}
void SnapmakerPrinterAgent::start_camera_monitor()
{
enqueue_command([this] {
send_ws_rpc("camera.start_monitor",
{{"domain", "lan"}, {"interval", 0}, {"expect_pw", false}});
});
m_camera_last_fire_ms.store(now_ms());
}
void SnapmakerPrinterAgent::on_status_loop_tick(const std::string& dev_id)
{
(void) dev_id;
const int64_t last = m_camera_last_fire_ms.load();
if (last == 0 || now_ms() - last >= CAMERA_REFRESH_INTERVAL_MS) {
start_camera_monitor();
}
}
int SnapmakerPrinterAgent::command_start_camera(std::string dev_id)
{
(void) dev_id;
start_camera_monitor();
return BAMBU_NETWORK_SUCCESS;
}
AgentInfo SnapmakerPrinterAgent::get_agent_info_static() AgentInfo SnapmakerPrinterAgent::get_agent_info_static()
{ {
return AgentInfo{"snapmaker", "Snapmaker", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"}; return AgentInfo{"snapmaker", "Snapmaker", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"};
@@ -109,17 +230,32 @@ std::string SnapmakerPrinterAgent::combine_filament_type(const std::string& type
return base; return base;
} }
bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/) bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
{ {
std::string url = join_url(device_info.base_url, "/printer/objects/query?print_task_config&filament_detect"); (void) dev_id;
if (sync_mode != get_filament_sync_mode())
return false;
// Snapshot everything the fetch needs (URL, api key, TLS/CA): a reconnect can rewrite
// device_info meanwhile.
const ConnectionSettings connection = get_connection_settings();
// One implementation for both modes; only where it runs differs. pull is the documented
// blocking contract (the GUI thread reads DevFilaSystem right after the call), so it runs
// on the caller with the payload applied inline. subscription runs on a background thread
// so the status loop is not stalled, and the payload is marshalled back to the main thread.
auto work = [this, connection](bool apply_inline) -> bool {
try {
const std::string url = join_url(connection.base_url, "/printer/objects/query?print_task_config&filament_detect");
std::string response_body; std::string response_body;
bool success = false; bool success = false;
std::string http_error; std::string http_error;
auto http = Http::get(url); auto http = Http::get(url);
if (!device_info.api_key.empty()) { configure_http(http, connection);
http.header("X-Api-Key", device_info.api_key); if (!connection.api_key.empty()) {
http.header("X-Api-Key", connection.api_key);
} }
http.timeout_connect(5) http.timeout_connect(5)
.timeout_max(10) .timeout_max(10)
@@ -151,8 +287,7 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
} }
// Navigate to result.status.print_task_config // Navigate to result.status.print_task_config
if (!json.contains("result") || !json["result"].contains("status") || if (!json.contains("result") || !json["result"].contains("status") || !json["result"]["status"].contains("print_task_config")) {
!json["result"]["status"].contains("print_task_config")) {
BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Missing print_task_config in response"; BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Missing print_task_config in response";
return false; return false;
} }
@@ -160,11 +295,11 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
auto& ptc = json["result"]["status"]["print_task_config"]; auto& ptc = json["result"]["status"]["print_task_config"];
// Read parallel arrays from print_task_config // Read parallel arrays from print_task_config
auto filament_exist = ptc.value("filament_exist", std::vector<bool>{}); auto filament_exist = read_bool_array_or(ptc, "filament_exist");
auto filament_type = ptc.value("filament_type", std::vector<std::string>{}); auto filament_type = read_string_array_or(ptc, "filament_type");
auto filament_sub_type = ptc.value("filament_sub_type", std::vector<std::string>{}); auto filament_sub_type = read_string_array_or(ptc, "filament_sub_type");
auto filament_color = ptc.value("filament_color_rgba", std::vector<std::string>{}); auto filament_color = read_string_array_or(ptc, "filament_color_rgba");
auto filament_vendor = ptc.value("filament_vendor", std::vector<std::string>{}); auto filament_vendor = read_string_array_or(ptc, "filament_vendor");
const int slot_count = static_cast<int>(filament_exist.size()); const int slot_count = static_cast<int>(filament_exist.size());
if (slot_count == 0) { if (slot_count == 0) {
@@ -174,14 +309,16 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
// Read NFC filament_detect data for temperature info (optional) // Read NFC filament_detect data for temperature info (optional)
nlohmann::json nfc_info; nlohmann::json nfc_info;
if (json["result"]["status"].contains("filament_detect") && if (json["result"]["status"].contains("filament_detect") && json["result"]["status"]["filament_detect"].contains("info")) {
json["result"]["status"]["filament_detect"].contains("info")) {
nfc_info = json["result"]["status"]["filament_detect"]["info"]; nfc_info = json["result"]["status"]["filament_detect"]["info"];
} }
static const std::string empty_str; static const std::string empty_str;
static const std::string default_color = "FFFFFFFF"; static const std::string default_color = "FFFFFFFF";
// Per-tray vendor names, kept so the preset lookup can run on the main thread.
std::vector<std::string> vendors(slot_count);
std::vector<AmsTrayData> trays; std::vector<AmsTrayData> trays;
trays.reserve(slot_count); trays.reserve(slot_count);
@@ -191,36 +328,24 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
tray.has_filament = filament_exist[i]; tray.has_filament = filament_exist[i];
if (tray.has_filament) { if (tray.has_filament) {
tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), safe_at(filament_sub_type, i, empty_str));
safe_at(filament_sub_type, i, empty_str));
tray.tray_color = safe_at(filament_color, i, default_color); tray.tray_color = safe_at(filament_color, i, default_color);
vendors[i] = safe_at(filament_vendor, i, empty_str);
auto* bundle = GUI::wxGetApp().preset_bundle; // Generic id derived from device data only; the preset-dependent lookup runs
// Try to find a matching preset for this filament based on vendor, type and color. // on the main thread in the resolve hook passed to build_ams_payload.
// If not found, default to traditional search by type only or generic type mapping.
if (bundle) {
std::string vendor = safe_at(filament_vendor, i, empty_str);
std::string filament_id = find_closest_color_preset_by_vendor_and_type(bundle->filaments, vendor, tray.tray_type,
tray.tray_color);
if (!filament_id.empty()) {
tray.tray_info_idx = filament_id;
BOOST_LOG_TRIVIAL(warning) << "Filament sync: Found manufacturer-specific profile for slot " << i << ": "
<< filament_id;
} else {
tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type);
}
} else {
tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type);
}
// Extract NFC temperature data if available // Extract NFC temperature data if available
if (nfc_info.is_array() && i < static_cast<int>(nfc_info.size()) && nfc_info[i].is_object()) { if (nfc_info.is_array() && i < static_cast<int>(nfc_info.size()) && nfc_info[i].is_object()) {
auto& nfc_slot = nfc_info[i]; auto& nfc_slot = nfc_info[i];
std::string vendor = nfc_slot.value("VENDOR", "NONE"); std::string vendor = "NONE";
if (auto vendor_it = nfc_slot.find("VENDOR"); vendor_it != nfc_slot.end() && vendor_it->is_string()) {
vendor = vendor_it->get<std::string>();
}
if (vendor != "NONE" && !vendor.empty()) { if (vendor != "NONE" && !vendor.empty()) {
tray.bed_temp = nfc_slot.value("BED_TEMP", 0); tray.bed_temp = read_int_or(nfc_slot, "BED_TEMP", 0);
tray.nozzle_temp = nfc_slot.value("FIRST_LAYER_TEMP", 0); tray.nozzle_temp = read_int_or(nfc_slot, "FIRST_LAYER_TEMP", 0);
} }
} }
} }
@@ -228,8 +353,69 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
trays.emplace_back(std::move(tray)); trays.emplace_back(std::move(tray));
} }
build_ams_payload(1, slot_count - 1, trays); // Resolve against GUI-owned presets on the main thread (inside build_ams_payload).
auto resolve_ids = [vendors](std::vector<AmsTrayData>& resolved_trays) {
auto* bundle = GUI::wxGetApp().preset_bundle;
if (!bundle) {
return; // keep the generic ids computed from device data
}
for (size_t i = 0; i < resolved_trays.size() && i < vendors.size(); ++i) {
AmsTrayData& tray = resolved_trays[i];
if (!tray.has_filament) {
continue;
}
// Prefer a manufacturer/colour-specific preset, else fall back to type.
std::string filament_id = find_closest_color_preset_by_vendor_and_type(bundle->filaments, vendors[i], tray.tray_type,
tray.tray_color);
tray.tray_info_idx = filament_id.empty() ? bundle->filaments.filament_id_by_type(tray.tray_type) : filament_id;
}
};
build_ams_payload(1, slot_count - 1, trays, apply_inline, resolve_ids);
return true;
} catch (const std::exception& e) {
// why: an exception escaping a detached thread is std::terminate, and firmware
// JSON is untrusted; mirror run_command_worker and swallow it here.
BOOST_LOG_TRIVIAL(error) << "SnapmakerPrinterAgent::fetch_filament_info: unhandled exception: " << e.what();
return false;
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "SnapmakerPrinterAgent::fetch_filament_info: unhandled exception";
return false;
}
};
if (sync_mode == FilamentSyncMode::pull) {
return work(/*apply_inline=*/true);
}
// Subscription: fire-and-forget. A `true` return means the refresh was scheduled (or is
// already running), not that DevFilaSystem has been updated. Reserve under the same mutex
// shutdown() uses, so the flag and the count can't race.
{
std::lock_guard<std::mutex> lock(fetch_lifecycle_mutex);
if (shutting_down.load())
return false;
if (filament_fetch_in_flight.load() > 0)
return true; // a fetch is already running; don't pile on
filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed);
}
InFlightGuard guard{*this};
std::thread([work = std::move(work), guard = std::move(guard)]() mutable {
work(/*apply_inline=*/false);
}).detach();
return true; return true;
} }
std::string SnapmakerPrinterAgent::get_camera_url() const
{
return join_url(get_connection_settings().base_url, "/server/files/camera/monitor.jpg");
}
FilamentSyncMode SnapmakerPrinterAgent::get_filament_sync_mode() const
{
if (GUI::wxGetApp().app_config->get_bool("use_printer_agents"))
return FilamentSyncMode::subscription;
return FilamentSyncMode::pull;
}
} // namespace Slic3r } // namespace Slic3r
+12 -1
View File
@@ -3,6 +3,8 @@
#include "IPrinterAgent.hpp" #include "IPrinterAgent.hpp"
#include "MoonrakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp"
#include <atomic>
#include <cstdint>
#include <string> #include <string>
namespace Slic3r { namespace Slic3r {
@@ -11,16 +13,25 @@ class SnapmakerPrinterAgent final : public MoonrakerPrinterAgent
{ {
public: public:
explicit SnapmakerPrinterAgent(std::string log_dir); explicit SnapmakerPrinterAgent(std::string log_dir);
~SnapmakerPrinterAgent() override = default; ~SnapmakerPrinterAgent() override { shutdown(); }
static AgentInfo get_agent_info_static(); static AgentInfo get_agent_info_static();
AgentInfo get_agent_info() override { return get_agent_info_static(); } AgentInfo get_agent_info() override { return get_agent_info_static(); }
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override; bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
FilamentSyncMode get_filament_sync_mode() const override;
int command_start_camera(std::string dev_id) override;
CameraStreamMode get_camera_stream_mode() const override { return CameraStreamMode::http_snapshot; }
std::string get_camera_url() const override;
private: private:
// Combine filament_type + filament_sub_type into a unified type string // Combine filament_type + filament_sub_type into a unified type string
static std::string combine_filament_type(const std::string& type, const std::string& sub_type); static std::string combine_filament_type(const std::string& type, const std::string& sub_type);
void start_camera_monitor();
void on_status_loop_tick(const std::string& dev_id) override;
std::atomic<int64_t> m_camera_last_fire_ms{0};
}; };
} // namespace Slic3r } // namespace Slic3r
+1
View File
@@ -529,6 +529,7 @@ std::string PluginConfig::capability_type_label(PluginCapabilityType type)
case PluginCapabilityType::Visualization: return _u8L("Visualization"); case PluginCapabilityType::Visualization: return _u8L("Visualization");
case PluginCapabilityType::Script: return _u8L("Script"); case PluginCapabilityType::Script: return _u8L("Script");
case PluginCapabilityType::SlicingPipeline: return _u8L("Slicing Pipeline"); case PluginCapabilityType::SlicingPipeline: return _u8L("Slicing Pipeline");
case PluginCapabilityType::ResourceTexture: return _u8L("Resource Texture");
default: return _u8L("Unknown"); default: return _u8L("Unknown");
} }
} }
+79 -17
View File
@@ -9,6 +9,7 @@
#include <boost/filesystem/directory.hpp> #include <boost/filesystem/directory.hpp>
#include <boost/filesystem/file_status.hpp> #include <boost/filesystem/file_status.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/nowide/fstream.hpp> #include <boost/nowide/fstream.hpp>
#include "PluginAuditManager.hpp" #include "PluginAuditManager.hpp"
@@ -33,10 +34,6 @@
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
#ifdef WIN32
#include <boost/locale/encoding_utf.hpp>
#endif
namespace Slic3r { namespace Slic3r {
const char* const INSTALL_STATE_FILE = ".install_state.json"; const char* const INSTALL_STATE_FILE = ".install_state.json";
@@ -664,7 +661,7 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
namespace fs = boost::filesystem; namespace fs = boost::filesystem;
boost::system::error_code ec; boost::system::error_code ec;
fs::create_directories(destination, ec); fs::create_directories(extended_path(destination), ec);
if (ec) { if (ec) {
error = "Failed to create plugin staging directory: " + ec.message(); error = "Failed to create plugin staging directory: " + ec.message();
return false; return false;
@@ -708,7 +705,7 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
const fs::path output_path = destination / relative_path; const fs::path output_path = destination / relative_path;
if (stat.m_is_directory || mz_zip_reader_is_file_a_directory(&reader.archive, stat.m_file_index)) { if (stat.m_is_directory || mz_zip_reader_is_file_a_directory(&reader.archive, stat.m_file_index)) {
fs::create_directories(output_path, ec); fs::create_directories(extended_path(output_path), ec);
if (ec) { if (ec) {
error = "Failed to create plugin zip directory " + output_path.string() + ": " + ec.message(); error = "Failed to create plugin zip directory " + output_path.string() + ": " + ec.message();
return false; return false;
@@ -716,26 +713,19 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
continue; continue;
} }
fs::create_directories(output_path.parent_path(), ec); fs::create_directories(extended_path(output_path.parent_path()), ec);
if (ec) { if (ec) {
error = "Failed to create plugin zip parent directory " + output_path.parent_path().string() + ": " + ec.message(); error = "Failed to create plugin zip parent directory " + output_path.parent_path().string() + ": " + ec.message();
return false; return false;
} }
if (fs::exists(output_path, ec) && fs::is_directory(output_path, ec)) { if (fs::exists(extended_path(output_path), ec) && fs::is_directory(extended_path(output_path), ec)) {
error = "Plugin zip file conflicts with an existing directory: " + output_path.string(); error = "Plugin zip file conflicts with an existing directory: " + output_path.string();
return false; return false;
} }
const std::string encoded_output_path = encode_path(output_path.string().c_str()); // Shared with the rest of the archive code: long-path capable and binary-safe.
mz_bool extracted = mz_zip_reader_extract_to_file(&reader.archive, stat.m_file_index, encoded_output_path.c_str(), 0); if (!extract_zip_entry_to_file(&reader.archive, stat.m_file_index, output_path)) {
#ifdef WIN32
if (!extracted) {
const std::wstring wide_output_path = boost::locale::conv::utf_to_utf<wchar_t>(output_path.generic_string());
extracted = mz_zip_reader_extract_to_file_w(&reader.archive, stat.m_file_index, wide_output_path.c_str(), 0);
}
#endif
if (!extracted) {
error = "Failed to extract plugin zip entry " + relative_key + ": " + error = "Failed to extract plugin zip entry " + relative_key + ": " +
MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive)); MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
return false; return false;
@@ -1255,4 +1245,76 @@ boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::pat
return {}; return {};
} }
std::vector<std::string> collect_resource_texture_files(const std::string& dir,
const std::string& plugin_root,
std::string& error)
{
namespace fs = boost::filesystem;
// Picker sanity bound, not a security control: the popup draws every entry in its grid.
constexpr size_t kMaxResourceTextureFiles = 64;
std::vector<std::string> files;
error.clear();
if (dir.empty()) {
error = _u8L("Resource texture directory is not set");
return files;
}
if (plugin_root.empty()) {
error = _u8L("Plugin folder could not be determined.");
return files;
}
boost::system::error_code ec;
const fs::path resolved_dir = fs::weakly_canonical(fs::path(dir), ec);
if (ec) {
error = _u8L("Resource texture directory could not be resolved");
return files;
}
if (!fs::is_directory(resolved_dir, ec) || ec) {
error = _u8L("Resource texture path is not a directory");
return files;
}
const fs::path resolved_root = fs::weakly_canonical(fs::path(plugin_root), ec);
if (ec) {
error = _u8L("Plugin folder could not be resolved");
return files;
}
if (!is_inside_allowed_root(resolved_dir, resolved_root)) {
error = _u8L("Resource texture directory is outside the plugin folder");
return files;
}
for (fs::directory_iterator it(resolved_dir, ec), end; it != end && !ec; it.increment(ec)) {
const fs::path& path = it->path();
if (!fs::is_regular_file(path, ec) || ec)
continue;
std::string ext = path.extension().string();
boost::algorithm::to_lower(ext);
if (ext != ".png" && ext != ".jpg" && ext != ".jpeg" && ext != ".bmp")
continue;
// Re-check per file: a symlink inside the pack must not resolve outside it.
if (!is_inside_allowed_root(path, resolved_root))
continue;
files.push_back(path.string());
if (files.size() >= kMaxResourceTextureFiles)
break;
}
if (ec) {
error = _u8L("Resource texture directory could not be read");
return {};
}
return files;
}
} // namespace Slic3r } // namespace Slic3r
+6
View File
@@ -176,6 +176,12 @@ bool read_wheel_plugin_metadata(const boost::filesystem::path& whl_path, PluginD
// Returns the path to the entry file, or an empty path with error set if zero or multiple candidates. // Returns the path to the entry file, or an empty path with error set if zero or multiple candidates.
boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::path& plugin_dir, std::string& error); boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::path& plugin_dir, std::string& error);
// Validated non-recursive .png/.jpg/.jpeg/.bmp files directly inside `dir`, every one contained in
// `plugin_root`. Empty with `error` set on failure.
std::vector<std::string> collect_resource_texture_files(const std::string& dir,
const std::string& plugin_root,
std::string& error);
// Canonical writer: emits the .install_state.json schema for the given state. // Canonical writer: emits the .install_state.json schema for the given state.
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginInstallState& state); bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginInstallState& state);
// Builds a PluginInstallState from the descriptor and delegates to the canonical writer. // Builds a PluginInstallState from the descriptor and delegates to the canonical writer.
+32
View File
@@ -9,6 +9,7 @@
#include "PluginFsUtils.hpp" #include "PluginFsUtils.hpp"
#include "PythonInterpreter.hpp" #include "PythonInterpreter.hpp"
#include "PythonPluginBridge.hpp" #include "PythonPluginBridge.hpp"
#include "pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/format.hpp" #include "slic3r/GUI/format.hpp"
@@ -387,6 +388,37 @@ bool load(const PluginDescriptor& descriptor,
instance->set_config_ui_available(false); instance->set_config_ui_available(false);
} }
// ResourceTexture capabilities resolve their texture directory here, under the
// GIL this block already holds, and cache the validated result. A resolution
// fault only costs this capability its textures, so it is caught locally like
// has_config_ui() above rather than failing the load.
if (auto* resource_texture = dynamic_cast<ResourceTexturePluginCapability*>(instance.get())) {
try {
const std::string texture_dir = resource_texture->get_texture_dir();
if (texture_dir.empty()) {
resource_texture->set_texture_dir({});
} else {
std::string texture_error;
collect_resource_texture_files(texture_dir, descriptor.plugin_root, texture_error);
if (texture_error.empty()) {
resource_texture->set_texture_dir(texture_dir);
} else {
BOOST_LOG_TRIVIAL(warning)
<< "Plugin capability '" << found.name << "' of plugin '" << descriptor.plugin_key
<< "': texture directory rejected (" << texture_error
<< "); the capability will provide no textures";
resource_texture->set_texture_dir({});
}
}
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning)
<< "Plugin capability '" << found.name << "' of plugin '" << descriptor.plugin_key
<< "': get_texture_dir() failed (" << ex.what()
<< "); the capability will provide no textures";
resource_texture->set_texture_dir({});
}
}
capabilities.push_back(std::move(instance)); capabilities.push_back(std::move(instance));
} }
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
+3
View File
@@ -25,6 +25,7 @@
#include "PyPluginTrampoline.hpp" #include "PyPluginTrampoline.hpp"
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp" #include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
#include "pluginTypes/pages/PagesPluginCapability.hpp" #include "pluginTypes/pages/PagesPluginCapability.hpp"
#include "pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
#include "pluginTypes/script/ScriptPluginCapability.hpp" #include "pluginTypes/script/ScriptPluginCapability.hpp"
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp" #include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
#include <pybind11/pytypes.h> #include <pybind11/pytypes.h>
@@ -374,6 +375,7 @@ void bind_python_api(pybind11::module_& m)
.value("Visualization", PluginCapabilityType::Visualization) .value("Visualization", PluginCapabilityType::Visualization)
.value("Script", PluginCapabilityType::Script) .value("Script", PluginCapabilityType::Script)
.value("SlicingPipeline", PluginCapabilityType::SlicingPipeline) .value("SlicingPipeline", PluginCapabilityType::SlicingPipeline)
.value("ResourceTexture", PluginCapabilityType::ResourceTexture)
.value("Unknown", PluginCapabilityType::Unknown) .value("Unknown", PluginCapabilityType::Unknown)
.export_values(); .export_values();
@@ -527,6 +529,7 @@ void bind_python_api(pybind11::module_& m)
PagesPluginCapability::RegisterBindings(m); PagesPluginCapability::RegisterBindings(m);
ScriptPluginCapability::RegisterBindings(m); ScriptPluginCapability::RegisterBindings(m);
SlicingPipelinePluginCapability::RegisterBindings(m); SlicingPipelinePluginCapability::RegisterBindings(m);
ResourceTexturePluginCapability::RegisterBindings(m);
PluginHost::RegisterBindings(m); PluginHost::RegisterBindings(m);
BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings"; BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings";
+5 -1
View File
@@ -14,7 +14,7 @@
namespace Slic3r { namespace Slic3r {
enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown }; enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, ResourceTexture, Unknown };
struct PluginCapabilityId struct PluginCapabilityId
{ {
@@ -48,6 +48,7 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
case PluginCapabilityType::Visualization: return "visualization"; case PluginCapabilityType::Visualization: return "visualization";
case PluginCapabilityType::Script: return "script"; case PluginCapabilityType::Script: return "script";
case PluginCapabilityType::SlicingPipeline: return "slicing-pipeline"; case PluginCapabilityType::SlicingPipeline: return "slicing-pipeline";
case PluginCapabilityType::ResourceTexture: return "resource-texture";
default: return "unknown"; default: return "unknown";
} }
} }
@@ -63,6 +64,7 @@ inline std::string plugin_capability_type_display_name(PluginCapabilityType type
case PluginCapabilityType::Visualization: return "Visualization"; case PluginCapabilityType::Visualization: return "Visualization";
case PluginCapabilityType::Script: return "Script"; case PluginCapabilityType::Script: return "Script";
case PluginCapabilityType::SlicingPipeline: return "Slicing Pipeline"; case PluginCapabilityType::SlicingPipeline: return "Slicing Pipeline";
case PluginCapabilityType::ResourceTexture: return "Resource Texture";
default: return "Unknown"; default: return "Unknown";
} }
} }
@@ -92,6 +94,8 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view
return PluginCapabilityType::Script; return PluginCapabilityType::Script;
if (lowered == "slicing-pipeline") if (lowered == "slicing-pipeline")
return PluginCapabilityType::SlicingPipeline; return PluginCapabilityType::SlicingPipeline;
if (lowered == "resource-texture")
return PluginCapabilityType::ResourceTexture;
return PluginCapabilityType::Unknown; return PluginCapabilityType::Unknown;
} }
+7
View File
@@ -4,6 +4,7 @@
#include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/Notebook.hpp" #include "slic3r/GUI/Notebook.hpp"
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/MainFrame.hpp"
#include "slic3r/GUI/Widgets/Button.hpp" #include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/wxExtensions.hpp" #include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/plugin/PluginManager.hpp" #include "slic3r/plugin/PluginManager.hpp"
@@ -382,6 +383,7 @@ void PluginPages::relayout()
m_parent->SelectPageByName(id_to_reselect); m_parent->SelectPageByName(id_to_reselect);
} }
const bool overflow_changed = need_overflow != (m_overflow_button != nullptr);
if (need_overflow) { if (need_overflow) {
if (m_overflow_button == nullptr) { if (m_overflow_button == nullptr) {
auto* btn = new Button(m_parent->GetBtnsListCtrl(), wxString(L"\u25BE"), wxString(), wxNO_BORDER); auto* btn = new Button(m_parent->GetBtnsListCtrl(), wxString(L"\u25BE"), wxString(), wxNO_BORDER);
@@ -398,6 +400,11 @@ void PluginPages::relayout()
m_overflow_button->Destroy(); m_overflow_button->Destroy();
m_overflow_button = nullptr; m_overflow_button = nullptr;
} }
// The tab strip changed width; compact the labels again so the side tools stay visible.
if ((!up_to_date || overflow_changed) && !GUI::wxGetApp().is_closing())
if (GUI::MainFrame* mainframe = GUI::wxGetApp().mainframe)
mainframe->fit_tab_labels();
} }
void PluginPages::select_page(const PluginCapabilityId& id) void PluginPages::select_page(const PluginCapabilityId& id)
@@ -0,0 +1,115 @@
#include "PluginResourceTextures.hpp"
#include "slic3r/plugin/PluginDescriptor.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <string>
#include <utility>
#include <vector>
namespace Slic3r {
PluginResourceTextures &PluginResourceTextures::instance()
{
static PluginResourceTextures registry;
return registry;
}
std::optional<ResourceTextureEntry> PluginResourceTextures::build_entry(const PluginCapabilityId &id) const
{
// Query by id and re-check state; the manager copies the shared_ptr and releases its lock.
auto capability = PluginManager::instance().get_plugin_capability(id, /*only_enabled=*/false);
if (!capability || !capability->is_enabled() || capability->type() != PluginCapabilityType::ResourceTexture)
return std::nullopt;
auto texture_capability = std::dynamic_pointer_cast<ResourceTexturePluginCapability>(capability);
if (!texture_capability)
return std::nullopt;
// An empty directory means resolution failed at load, so the pack contributes nothing.
const std::string &texture_dir = texture_capability->texture_dir();
if (texture_dir.empty())
return std::nullopt;
PluginDescriptor descriptor;
if (!PluginManager::instance().try_get_plugin_descriptor_for_capability(capability->name(),
PluginCapabilityType::ResourceTexture,
descriptor))
return std::nullopt;
ResourceTextureEntry entry;
entry.id = capability->identity();
entry.plugin_key = entry.id.plugin_key;
entry.display_name = capability->name();
entry.texture_dir = texture_dir;
entry.plugin_root = descriptor.plugin_root;
return entry;
}
void PluginResourceTextures::on_plugin_register(const std::string &plugin_key)
{
for (const auto &capability : PluginManager::instance().get_plugin_capabilities(plugin_key, PluginCapabilityType::ResourceTexture)) {
if (capability)
on_cap_register(capability->identity());
}
}
void PluginResourceTextures::on_plugin_deregister(const std::string &plugin_key)
{
std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_entries.begin(); it != m_entries.end();) {
if (it->first.plugin_key == plugin_key)
it = m_entries.erase(it);
else
++it;
}
}
void PluginResourceTextures::on_cap_register(const PluginCapabilityId &id)
{
std::optional<ResourceTextureEntry> entry = build_entry(id);
std::lock_guard<std::mutex> lock(m_mutex);
if (entry)
m_entries[entry->id] = std::move(*entry);
}
void PluginResourceTextures::on_cap_deregister(const PluginCapabilityId &id)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_entries.erase(id);
}
void PluginResourceTextures::initial_sweep()
{
std::map<PluginCapabilityId, ResourceTextureEntry> entries;
for (const auto &capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::ResourceTexture)) {
if (!capability)
continue;
std::optional<ResourceTextureEntry> entry = build_entry(capability->identity());
if (entry)
entries[entry->id] = std::move(*entry);
}
std::lock_guard<std::mutex> lock(m_mutex);
m_entries = std::move(entries);
}
std::vector<ResourceTextureEntry> PluginResourceTextures::entries() const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<ResourceTextureEntry> result;
result.reserve(m_entries.size());
for (const auto &entry : m_entries)
result.push_back(entry.second);
return result;
}
} // namespace Slic3r
@@ -0,0 +1,50 @@
#pragma once
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <map>
#include <mutex>
#include <optional>
#include <string>
#include <vector>
namespace Slic3r {
// One ResourceTexture capability contributing a texture directory to the texture picker.
struct ResourceTextureEntry
{
PluginCapabilityId id;
std::string plugin_key;
std::string display_name;
std::string texture_dir;
std::string plugin_root; // so a scan can re-check containment
};
// Tracks every loaded and enabled ResourceTexture capability by id. Holds strings only: a
// shared_ptr here would keep a Python object alive past Py_Finalize.
class PluginResourceTextures
{
public:
static PluginResourceTextures &instance();
PluginResourceTextures(const PluginResourceTextures &) = delete;
PluginResourceTextures &operator=(const PluginResourceTextures &) = delete;
void on_plugin_register(const std::string &plugin_key);
void on_plugin_deregister(const std::string &plugin_key);
void on_cap_register(const PluginCapabilityId &id);
void on_cap_deregister(const PluginCapabilityId &id);
void initial_sweep();
std::vector<ResourceTextureEntry> entries() const;
private:
PluginResourceTextures() = default;
std::optional<ResourceTextureEntry> build_entry(const PluginCapabilityId &id) const;
mutable std::mutex m_mutex;
std::map<PluginCapabilityId, ResourceTextureEntry> m_entries;
};
} // namespace Slic3r
@@ -0,0 +1,32 @@
#include "ResourceTexturePluginCapability.hpp"
#include "ResourceTexturePluginCapabilityTrampoline.hpp"
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>
#include <boost/log/trivial.hpp>
#include <memory>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <string>
namespace py = pybind11;
namespace Slic3r {
void ResourceTexturePluginCapability::RegisterBindings(pybind11::module_& module)
{
BOOST_LOG_TRIVIAL(debug) << "Registering orca.resource_texture bindings";
auto resource_texture = module.def_submodule("resource_texture", "Plugin resource texture API");
py::class_<ResourceTexturePluginCapability, PluginCapabilityInterface,
PyResourceTexturePluginCapabilityTrampoline,
std::shared_ptr<ResourceTexturePluginCapability>>(resource_texture,
"ResourceTexturePluginCapabilityBase")
.def(py::init<>())
.def("get_type", &ResourceTexturePluginCapability::get_type)
.def("get_texture_dir", &ResourceTexturePluginCapability::get_texture_dir);
}
} // namespace Slic3r
@@ -0,0 +1,28 @@
#pragma once
#include "../../PythonPluginInterface.hpp"
#include "pybind11/pybind11.h"
#include <string>
#include <utility>
namespace Slic3r {
class ResourceTexturePluginCapability : public PluginCapabilityInterface
{
public:
static void RegisterBindings(pybind11::module_& module);
PluginCapabilityType get_type() const override { return PluginCapabilityType::ResourceTexture; }
// Python contract: the directory the host scans for textures. Resolved once at materialization
// under the GIL; never called from the GUI. Overridden in Python through the trampoline.
virtual std::string get_texture_dir() const = 0;
// C++-only read of the validated directory cached by the loader; empty if resolution failed.
const std::string& texture_dir() const { return m_texture_dir; }
void set_texture_dir(std::string dir) { m_texture_dir = std::move(dir); }
private:
std::string m_texture_dir;
};
} // namespace Slic3r
@@ -0,0 +1,30 @@
#pragma once
#include "ResourceTexturePluginCapability.hpp"
#include "../../PyPluginTrampoline.hpp"
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>
#include <string>
#include "slic3r/plugin/PythonPluginInterface.hpp"
namespace Slic3r {
class PyResourceTexturePluginCapabilityTrampoline :
public PyPluginCommonTrampoline<ResourceTexturePluginCapability>
{
public:
using PyPluginCommonTrampoline<ResourceTexturePluginCapability>::PyPluginCommonTrampoline;
std::string get_texture_dir() const override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {},
PYBIND11_OVERRIDE_PURE,
std::string,
ResourceTexturePluginCapability,
get_texture_dir);
}
};
} // namespace Slic3r
@@ -163,7 +163,7 @@ M109 S240 T0 ; set nozzle temperature and wait for it to be reached
; CP TOOLCHANGE END ; CP TOOLCHANGE END
M104 S240 T1 ; preheat T1 time: 30s lead 30.0s M104 S240 T1 ; preheat T1 time: 30s lead 30.0s
; CP TOOLCHANGE START ; CP TOOLCHANGE START
M104 S200 T0 ; set nozzle temperature ;cooldown M104 S0 T0 ; set nozzle temperature ;cooldown
T1 ; change extruder T1 ; change extruder
M109 S240 T1 ; set nozzle temperature and wait for it to be reached M109 S240 T1 ; set nozzle temperature and wait for it to be reached
; CP TOOLCHANGE END ; CP TOOLCHANGE END
+109
View File
@@ -1742,3 +1742,112 @@ TEST_CASE("Each filament cools with the fan speeds of its extruder variant", "[M
CHECK(gcode.find("; start range high " + std::to_string(range_high) + "\n") != std::string::npos); CHECK(gcode.find("; start range high " + std::to_string(range_high) + "\n") != std::string::npos);
} }
} }
TEST_CASE("Shared heater on IMEX printer stays hot until all filaments on it are done", "[MultiFilament]")
{
DynamicPrintConfig cfg = multifilament_config(7);
cfg.set_deserialize_strict({
{"ooze_prevention", 1},
{"preheat_time", 0},
{"single_extruder_multi_material", 0},
});
imex_7x4_printer(cfg);
const std::string gcode = slice_with_object_overrides({make_cube(20., 20., 10.), make_cube(20., 20., 4.), make_cube(20., 20., 10.)},
cfg,
{{{"extruder", 1}, {"layer_height", 0.30}},
{{"extruder", 2}, {"layer_height", 0.15}},
{{"extruder", 5}}});
bool premature_s0_t0 = false;
double current_z = 0.0;
GCodeReader reader;
reader.parse_buffer(gcode, [&](GCodeReader& /*self*/, const GCodeReader::GCodeLine& line) {
if (line.has_z())
current_z = line.z();
const std::string raw = line.raw();
if (raw.find("M104 S0 T0") != std::string::npos && raw.find(";cooldown") != std::string::npos) {
if (current_z < 9.0)
premature_s0_t0 = true;
}
});
// Filaments 1 and 2 share head 0, and filament 1 prints up to 10mm.
// T0 must not shut down at ~4mm when filament 2 finishes.
CHECK_FALSE(premature_s0_t0);
}
TEST_CASE("Unused extruder turns off after its final layer on multi-object print", "[MultiFilament]")
{
const std::string gcode = slice_with_object_overrides(
{ make_cube(20., 20., 10.), make_cube(20., 20., 4.) },
multifilament_config(2, {
{ "single_extruder_multi_material", 0 },
{ "ooze_prevention", 1 },
{ "standby_temperature_delta", -40 },
{ "nozzle_temperature", "240,240" },
}),
{
{ { "extruder", 1 }, { "layer_height", 0.20 } },
{ { "extruder", 2 }, { "layer_height", 0.15 } }
});
int t1_s0_cooldown_count = 0;
int t0_s0_cooldown_count = 0;
std::istringstream stream(gcode);
for (std::string line; std::getline(stream, line);) {
if (line.find(";cooldown") != std::string::npos) {
if (line.find("M104 S0 T1") != std::string::npos)
++t1_s0_cooldown_count;
if (line.find("M104 S0 T0") != std::string::npos)
++t0_s0_cooldown_count;
}
}
// T1 (extruder 2, 4mm cube) gets exactly one S0 after its last layer.
// T0 (extruder 1, 10mm cube) never gets S0 mid-print.
CHECK(t1_s0_cooldown_count == 1);
CHECK(t0_s0_cooldown_count == 0);
}
TEST_CASE("Unused extruder turns off on non-IMEX three-tool print", "[MultiFilament]")
{
const std::string gcode = slice_with_object_overrides(
{ make_cube(20., 20., 4.), make_cube(20., 20., 10.), make_cube(20., 20., 10.) },
multifilament_config(3, {
{ "single_extruder_multi_material", 0 },
{ "ooze_prevention", 1 },
{ "standby_temperature_delta", -40 },
{ "nozzle_temperature", "240,240,240" },
}),
{
{ { "extruder", 1 } },
{ { "extruder", 2 } },
{ { "extruder", 3 } }
});
bool premature_s0_t1 = false;
bool premature_s0_t2 = false;
double current_z = 0.0;
GCodeReader reader;
reader.parse_buffer(gcode, [&](GCodeReader& /*self*/, const GCodeReader::GCodeLine& line) {
if (line.has_z())
current_z = line.z();
const std::string raw = line.raw();
if (raw.find(";cooldown") != std::string::npos && current_z < 9.0) {
if (raw.find("M104 S0 T1") != std::string::npos)
premature_s0_t1 = true;
if (raw.find("M104 S0 T2") != std::string::npos)
premature_s0_t2 = true;
}
});
// T1 and T2 print up to 10mm — neither should get S0 before that.
CHECK_FALSE(premature_s0_t1);
CHECK_FALSE(premature_s0_t2);
}
+382
View File
@@ -22,6 +22,7 @@
#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp" #include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Point.hpp" #include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Polyline.hpp" #include "libslic3r/Polyline.hpp"
#include <limits> #include <limits>
#include <string> #include <string>
@@ -994,3 +995,384 @@ TEST_CASE("A hole whose outer wall touches the contour's runs with it", "[Perime
CHECK(layers[i].holes_ccw.front() == ccw); CHECK(layers[i].holes_ccw.front() == ccw);
} }
} }
namespace {
// The outer radius of round_tube().
const double tube_radius = 11.5;
// Every setting the tube walls below are measured against. With 0.5mm walls a 1mm tube wall holds only the two
// outer walls, touching, and every further 0.5mm makes room for one more inner wall.
DynamicPrintConfig tube_config(const char *wall_generator, int wall_loops)
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "wall_generator", wall_generator },
{ "wall_loops", wall_loops },
{ "wall_sequence", "inner wall/outer wall" },
{ "wall_direction", "ccw" },
{ "layer_height", 0.2 },
{ "initial_layer_print_height", 0.2 },
{ "nozzle_diameter", "0.4" },
{ "outer_wall_line_width", 0.5 },
{ "inner_wall_line_width", 0.5 },
{ "precise_outer_wall", true },
{ "detect_thin_wall", false },
{ "only_one_wall_top", false },
{ "only_one_wall_first_layer", false },
{ "brim_type", "no_brim" },
{ "detect_overhang_wall", true },
{ "overhang_reverse", false },
{ "overhang_reverse_threshold", "0%" },
});
return config;
}
// A 2mm tall round tube whose wall is `wall` mm thick.
Print &round_tube(Print &print, Model &model, const DynamicPrintConfig &config, double wall)
{
ModelObject *object = model.add_object();
object->name = "round_tube.stl";
object->add_volume(make_cylinder(tube_radius, 2.), ModelVolumeType::MODEL_PART, false);
object->add_volume(make_cylinder(tube_radius - wall, 2.), ModelVolumeType::NEGATIVE_VOLUME, false);
object->add_instance();
object->ensure_on_bed();
print.auto_assign_extruders(object);
print.apply(model, config);
print.validate();
print.set_status_silent();
print.process();
return print;
}
// The side of the tube wall a wall loop lies on. The centre is the middle of the tube wall, where Arachne lays the
// inner wall both sides share when the tube wall is too narrow for a pair of them.
enum class TubeSide { Contour, Hole, Centre };
struct TubeLoop {
TubeSide side;
int inset;
bool ccw;
};
// The wall loops of every layer of round_tube() in extrusion order.
std::vector<std::vector<TubeLoop>> tube_loops(const Print &print, double wall)
{
const double centre = tube_radius - 0.5 * wall;
std::vector<std::vector<TubeLoop>> out;
for (const Layer *layer : print.objects().front()->layers()) {
const Vec2d axis = get_extents(layer->lslices).center().cast<double>();
std::vector<TubeLoop> &loops = out.emplace_back();
for (const LayerRegion *region : layer->regions())
for (const ExtrusionEntity *entity : region->perimeters.flatten().entities) {
if (! entity->is_loop())
continue;
const ExtrusionLoop *loop = static_cast<const ExtrusionLoop*>(entity);
const Polygon polygon = loop->polygon();
double radius = 0.;
for (const Point &p : polygon.points)
radius += unscaled<double>((p.cast<double>() - axis).norm());
radius /= double(polygon.points.size());
const TubeSide side = radius > centre + 0.1 ? TubeSide::Contour : radius < centre - 0.1 ? TubeSide::Hole : TubeSide::Centre;
loops.push_back({ side, loop->inset_idx, polygon.is_counter_clockwise() });
}
}
return out;
}
// The insets one side of a wall is printed in, given its deepest inset: inside out for Inner/Outer, outside in for
// Outer/Inner, and for Inner/Outer/Inner the third wall onwards inside out, then the outer wall, then the first inner
// wall. Inner/Outer/Inner prints its first layer Inner/Outer, and outside in when there is no third wall.
std::vector<int> wall_sequence_insets(const std::string &wall_sequence, bool first_layer, int deepest)
{
std::vector<int> insets;
if (wall_sequence == "inner wall/outer wall" || (wall_sequence == "inner-outer-inner wall" && first_layer)) {
for (int inset = deepest; inset >= 0; -- inset)
insets.push_back(inset);
} else if (wall_sequence == "outer wall/inner wall" || deepest < 2) {
for (int inset = 0; inset <= deepest; ++ inset)
insets.push_back(inset);
} else {
for (int inset = deepest; inset >= 2; -- inset)
insets.push_back(inset);
insets.push_back(0);
insets.push_back(1);
}
return insets;
}
// Whether a wall of round_tube() runs with the contour: all but the walls of the hole, and those too when the hole
// is a thin wall hole, its outer wall touching the contour's with nothing between them.
bool runs_with_contour(const TubeLoop &loop, bool thin_wall_hole)
{
return loop.side != TubeSide::Hole || thin_wall_hole;
}
// The insets of the walls on one side of a round_tube() layer, the shared centre line included, in extrusion order.
std::vector<int> side_insets(const std::vector<TubeLoop> &loops, TubeSide side)
{
std::vector<int> insets;
for (const TubeLoop &loop : loops)
if (loop.side == side || loop.side == TubeSide::Centre)
insets.push_back(loop.inset);
return insets;
}
// The walls of a round_tube() layer in extrusion order, as side and inset: "c0 c1 h0 h1" is the contour's outer wall,
// then its first inner wall, then the same for the hole, and "m" marks the wall in the middle both sides share.
std::string wall_order(const std::vector<TubeLoop> &loops)
{
std::string out;
for (const TubeLoop &loop : loops)
out += std::string(out.empty() ? "" : " ") + (loop.side == TubeSide::Contour ? "c" : loop.side == TubeSide::Hole ? "h" : "m") +
std::to_string(loop.inset);
return out;
}
} // namespace
// Each side of a tube wall prints its walls in the order the wall sequence asks for, from two walls a side up to
// four. The inner wall Arachne lays in the middle of a wall too narrow for a pair of them is an inner wall of both
// sides; Classic fills that middle with gap fill instead, so it has one wall less a side there.
TEST_CASE("Each side of a tube wall prints its walls in the wall sequence order", "[Perimeters]")
{
struct TubeWall {
double wall;
int wall_loops;
// Walls a side, a shared inner wall included.
int arachne_walls;
int classic_walls;
};
const TubeWall tube = GENERATE(values<TubeWall>({
{ 1.5, 3, 2, 1 }, { 2.0, 2, 2, 2 }, { 2.5, 3, 3, 2 }, { 3.0, 3, 3, 3 }, { 4.0, 4, 4, 4 } }));
const char *wall_generator = GENERATE("classic", "arachne");
const std::string wall_sequence = GENERATE("inner wall/outer wall", "outer wall/inner wall", "inner-outer-inner wall");
CAPTURE(tube.wall, tube.wall_loops, wall_generator, wall_sequence);
DynamicPrintConfig config = tube_config(wall_generator, tube.wall_loops);
config.set_deserialize_strict("wall_sequence", wall_sequence);
Print print;
Model model;
round_tube(print, model, config, tube.wall);
const std::vector<std::vector<TubeLoop>> layers = tube_loops(print, tube.wall);
// 2mm at 0.2mm layers.
REQUIRE(layers.size() == 10);
const int walls = std::string(wall_generator) == "arachne" ? tube.arachne_walls : tube.classic_walls;
for (size_t i = 0; i < layers.size(); ++ i) {
for (const TubeSide side : { TubeSide::Contour, TubeSide::Hole }) {
const bool contour_side = side == TubeSide::Contour;
CAPTURE(i, contour_side);
const std::vector<int> insets = side_insets(layers[i], side);
REQUIRE(int(insets.size()) == walls);
CHECK(insets == wall_sequence_insets(wall_sequence, i == 0, walls - 1));
}
}
}
// The outer wall of a thin wall hole touches the contour's with nothing between them. The hole is printed first, so
// the contour leans on it, unless the wall sequence puts the outer walls first.
TEST_CASE("A thin wall hole is printed before the contour unless the outer walls go first", "[Perimeters]")
{
const char *wall_generator = GENERATE("classic", "arachne");
const std::string wall_sequence = GENERATE("inner wall/outer wall", "outer wall/inner wall", "inner-outer-inner wall");
CAPTURE(wall_generator, wall_sequence);
DynamicPrintConfig config = tube_config(wall_generator, 3);
config.set_deserialize_strict("wall_sequence", wall_sequence);
Print print;
Model model;
round_tube(print, model, config, 1.);
const std::vector<std::vector<TubeLoop>> layers = tube_loops(print, 1.);
REQUIRE(layers.size() == 10);
const TubeSide first = wall_sequence == "outer wall/inner wall" ? TubeSide::Contour : TubeSide::Hole;
for (size_t i = 0; i < layers.size(); ++ i) {
CAPTURE(i);
REQUIRE(layers[i].size() == 2);
REQUIRE(layers[i][0].side != layers[i][1].side);
CHECK(layers[i].front().side == first);
}
}
// A 1.5mm tube wall leaves Inner/Outer/Inner no third wall to sandwich, only the inner wall Arachne lays in the middle
// for both sides, or Classic's gap fill. Past the first layer it prints the hole first, then the contour, then the
// middle.
TEST_CASE("Inner/Outer/Inner prints the hole of a narrow tube wall first and the middle last", "[Perimeters]")
{
const char *wall_generator = GENERATE("classic", "arachne");
CAPTURE(wall_generator);
DynamicPrintConfig config = tube_config(wall_generator, 3);
config.set_deserialize_strict("wall_sequence", "inner-outer-inner wall");
Print print;
Model model;
round_tube(print, model, config, 1.5);
const std::vector<std::vector<TubeLoop>> layers = tube_loops(print, 1.5);
REQUIRE(layers.size() == 10);
// Classic's gap fill is not a wall loop, and goes after the walls.
const std::string expected = std::string(wall_generator) == "arachne" ? "h0 c0 m1" : "h0 c0";
for (size_t i = 1; i < layers.size(); ++ i) {
CAPTURE(i);
CHECK(wall_order(layers[i]) == expected);
}
}
// The contour's walls run in the wall direction and the hole's against it, except that the walls of a thin wall
// hole run with the contour, as does the inner wall Arachne shares between both sides of a 1.5mm tube wall.
TEST_CASE("Tube walls run in the wall direction and hole walls against it", "[Perimeters]")
{
const double wall = GENERATE(1.0, 1.5, 2.0, 3.0);
const char *wall_generator = GENERATE("classic", "arachne");
const std::string wall_direction = GENERATE("ccw", "cw");
CAPTURE(wall, wall_generator, wall_direction);
DynamicPrintConfig config = tube_config(wall_generator, 3);
config.set_deserialize_strict("wall_direction", wall_direction);
Print print;
Model model;
round_tube(print, model, config, wall);
const std::vector<std::vector<TubeLoop>> layers = tube_loops(print, wall);
REQUIRE(layers.size() == 10);
const bool thin_wall_hole = wall == 1.;
for (size_t i = 0; i < layers.size(); ++ i) {
CAPTURE(i);
REQUIRE(std::any_of(layers[i].begin(), layers[i].end(), [](const TubeLoop &loop) { return loop.side == TubeSide::Contour; }));
REQUIRE(std::any_of(layers[i].begin(), layers[i].end(), [](const TubeLoop &loop) { return loop.side == TubeSide::Hole; }));
for (const TubeLoop &loop : layers[i]) {
CAPTURE(int(loop.side), loop.inset);
CHECK(loop.ccw == (runs_with_contour(loop, thin_wall_hole) == (wall_direction == "ccw")));
}
}
}
// Reverse on even with no threshold reverses every wall of every other layer, or only the inner walls when it is
// restricted to them, and leaves the order of the walls alone.
TEST_CASE("Reverse on even reverses the tube walls of every other layer", "[Perimeters]")
{
const double wall = GENERATE(1.0, 1.5, 3.0);
const char *wall_generator = GENERATE("classic", "arachne");
const bool reverse_internal_only = GENERATE(false, true);
CAPTURE(wall, wall_generator, reverse_internal_only);
auto slice = [&](bool overhang_reverse) {
DynamicPrintConfig config = tube_config(wall_generator, 3);
config.set_deserialize_strict({
{ "overhang_reverse", overhang_reverse },
{ "overhang_reverse_internal_only", reverse_internal_only },
});
Print print;
Model model;
round_tube(print, model, config, wall);
return tube_loops(print, wall);
};
const std::vector<std::vector<TubeLoop>> plain = slice(false);
const std::vector<std::vector<TubeLoop>> reversed = slice(true);
REQUIRE(plain.size() == 10);
REQUIRE(reversed.size() == plain.size());
for (size_t i = 0; i < plain.size(); ++ i) {
CAPTURE(i);
REQUIRE(reversed[i].size() == plain[i].size());
for (size_t j = 0; j < plain[i].size(); ++ j) {
CAPTURE(j);
CHECK(reversed[i][j].side == plain[i][j].side);
CHECK(reversed[i][j].inset == plain[i][j].inset);
const bool flips = i % 2 == 1 && (! reverse_internal_only || plain[i][j].inset > 0);
CHECK(reversed[i][j].ccw == (plain[i][j].ccw != flips));
}
}
}
namespace {
DynamicPrintConfig brim_tube_config(const char *wall_generator, const std::string &wall_sequence, const char *brim_type, double brim_width)
{
DynamicPrintConfig config = tube_config(wall_generator, 3);
config.set_deserialize_strict({
{ "wall_sequence", wall_sequence },
{ "brim_type", brim_type },
{ "brim_width", brim_width },
});
return config;
}
} // namespace
// A brim against the outer walls makes the first layer print them first, whatever the wall sequence, so the brim runs
// straight on into the outer wall: each side of the tube wall outside in, and the contour before a thin wall hole.
// Brims that may leave the outer walls bare keep the wall sequence, and so do the layers above.
TEST_CASE("A brim against the outer walls makes the first layer print them first", "[Perimeters]")
{
struct Brim {
const char *type;
double width;
bool outer_walls_first;
};
const Brim brim = GENERATE(values<Brim>({
{ "no_brim", 5., false }, { "inner_only", 5., false }, { "auto_brim", 5., false }, { "outer_only", 0., false },
{ "outer_only", 5., true }, { "outer_and_inner", 5., true }, { "leading_edge_only", 5., true } }));
const double wall = GENERATE(1.0, 3.0);
const char *wall_generator = GENERATE("classic", "arachne");
const std::string wall_sequence = GENERATE("inner wall/outer wall", "inner-outer-inner wall");
CAPTURE(brim.type, brim.width, wall, wall_generator, wall_sequence);
Print print;
Model model;
round_tube(print, model, brim_tube_config(wall_generator, wall_sequence, brim.type, brim.width), wall);
const std::vector<std::vector<TubeLoop>> layers = tube_loops(print, wall);
REQUIRE(layers.size() == 10);
// The thin wall hole has its outer wall a side, the 3mm tube wall three walls a side.
const size_t walls = wall == 1. ? 1 : 3;
for (const size_t i : { size_t(0), size_t(1) }) {
const bool outer_walls_first = brim.outer_walls_first && i == 0;
for (const TubeSide side : { TubeSide::Contour, TubeSide::Hole }) {
const bool contour_side = side == TubeSide::Contour;
CAPTURE(i, contour_side);
const std::vector<int> insets = side_insets(layers[i], side);
REQUIRE(insets.size() == walls);
CHECK(insets == wall_sequence_insets(outer_walls_first ? "outer wall/inner wall" : wall_sequence, i == 0, int(walls) - 1));
}
if (wall == 1.)
CHECK(layers[i].front().side == (outer_walls_first ? TubeSide::Contour : TubeSide::Hole));
}
}
// Changing the brim of a sliced print regenerates the walls when the first layer's wall order changes with it, so
// the print gets the walls a slice from scratch with the new brim gets.
TEST_CASE("Changing the brim updates the wall order of the first layer", "[Perimeters]")
{
struct BrimChange {
const char *from_type;
double from_width;
const char *to_type;
double to_width;
};
const BrimChange change = GENERATE(values<BrimChange>({
{ "outer_only", 0., "outer_only", 5. }, { "outer_only", 5., "outer_only", 0. },
{ "no_brim", 5., "outer_and_inner", 5. }, { "outer_and_inner", 5., "inner_only", 5. } }));
const char *wall_generator = GENERATE("classic", "arachne");
CAPTURE(change.from_type, change.from_width, change.to_type, change.to_width, wall_generator);
const std::string wall_sequence = "inner wall/outer wall";
Print print;
Model model;
round_tube(print, model, brim_tube_config(wall_generator, wall_sequence, change.from_type, change.from_width), 3.);
const std::string before = wall_order(tube_loops(print, 3.).front());
print.apply(model, brim_tube_config(wall_generator, wall_sequence, change.to_type, change.to_width));
print.process();
const std::string after = wall_order(tube_loops(print, 3.).front());
Print fresh_print;
Model fresh_model;
round_tube(fresh_print, fresh_model, brim_tube_config(wall_generator, wall_sequence, change.to_type, change.to_width), 3.);
const std::string fresh = wall_order(tube_loops(fresh_print, 3.).front());
// The change does turn the first layer around.
REQUIRE(before != fresh);
CHECK(after == fresh);
}
+59
View File
@@ -735,6 +735,65 @@ TEST_CASE("Custom G-code placeholders are substituted", "[Print]")
CHECK(per_layer.find(";Layer:399 ") != std::string::npos); CHECK(per_layer.find(";Layer:399 ") != std::string::npos);
} }
// The final print statistics are known only after the whole G-code is generated, yet every custom G-code,
// the file header included, gets the same values as filename_format.
TEST_CASE("Print statistics in custom G-code match the final print statistics", "[Print][Regression]")
{
const std::string gcode_key = GENERATE(as<std::string>{}, "file_start_gcode", "machine_start_gcode", "machine_end_gcode");
// Bambu printers use another tag table and G-code layout.
const bool is_bbl = GENERATE(false, true);
CAPTURE(gcode_key, is_bbl);
const std::vector<std::string> number_keys = { "total_weight", "used_filament", "total_cost", "total_wipe_tower_cost",
"total_wipe_tower_filament" };
const std::vector<std::string> time_keys = { "print_time", "normal_print_time", "silent_print_time" };
std::string templ;
for (const std::vector<std::string> *keys : { &number_keys, &time_keys })
for (const std::string &key : *keys)
templ += "; stat " + key + "={" + key + "}\n";
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ gcode_key, templ },
{ "filament_density", "1.25" },
{ "filament_cost", "20" },
{ "time_cost", "2" },
});
Print print;
Model model;
init_print({ cube(20) }, print, model, config);
print.is_BBL_printer() = is_bbl;
std::string gcode;
REQUIRE_NOTHROW(gcode = Slic3r::Test::gcode(print));
// Match whole lines only: the config block repeats the template on a single line, and Bambu printers
// write that block before the start G-code.
const std::string lines = "\n" + gcode;
const auto value_of = [&lines](const std::string &key) {
const std::string prefix = "\n; stat " + key + "=";
const size_t start = lines.find(prefix);
REQUIRE(start != std::string::npos);
const size_t begin = start + prefix.size();
return lines.substr(begin, lines.find('\n', begin) - begin);
};
// The same statistics filename_format is expanded with.
DynamicConfig stats = print.print_statistics().config();
// A 20mm cube with filament_density 1.25 weighs something, unlike the running extruded_weight_total in the header.
CHECK(stats.opt_float("total_weight") > 0.);
for (const std::string &key : number_keys) {
CAPTURE(key);
const std::string value = value_of(key);
REQUIRE(value.find('@') == std::string::npos);
// Written with two decimals.
CHECK_THAT(std::stod(value), Catch::Matchers::WithinAbs(stats.opt_float(key), 0.006));
}
for (const std::string &key : time_keys) {
CAPTURE(key);
CHECK(value_of(key) == stats.opt_string(key));
}
}
TEST_CASE("export_gcode writes G-code without a result pointer", "[Print][export_gcode]") TEST_CASE("export_gcode writes G-code without a result pointer", "[Print][export_gcode]")
{ {
Print print; Print print;
+50
View File
@@ -18,6 +18,7 @@
#include <fstream> #include <fstream>
#include <ios> #include <ios>
#include <iterator> #include <iterator>
#include <libslic3r/Utils.hpp>
#include <miniz.h> #include <miniz.h>
#include <string> #include <string>
#include <utility> #include <utility>
@@ -176,6 +177,55 @@ TEST_CASE("Confined extraction writes nothing outside the target for Windows-spe
CHECK(list_dir(tmp.path()) == std::vector<std::string>{"bundle.zip", "cache"}); CHECK(list_dir(tmp.path()) == std::vector<std::string>{"bundle.zip", "cache"});
} }
TEST_CASE("Confined extraction writes files under a target path longer than MAX_PATH", "[MinizExtension]")
{
ScopedTemporaryDir tmp;
const fs::path zip_file = tmp.path() / "bundle.zip";
// Directory entries are explicit: extract_archive_confined recreates directories only for them.
write_zip(zip_file, {{"vendor/", ""}, {"vendor/machine/", ""}, {"vendor.json", "{\"a\":1}"},
{"vendor/machine/printer.json", "{\"b\":2}"}});
// One 240-char component keeps every name below the NTFS 255 limit while pushing the full
// target path past the 260-char MAX_PATH the Win32 APIs enforce when LongPathsEnabled is off.
const fs::path target = tmp.path() / std::string(240, 'd');
REQUIRE(target.string().size() > 260);
fs::create_directories(extended_path(target));
REQUIRE(extract_archive_confined(zip_file.string(), target.string()));
// Verify through extended-length paths, otherwise the checks themselves are bounded by MAX_PATH.
CHECK(fs::is_directory(extended_path(target / "vendor")));
CHECK(read_file(extended_path(target / "vendor.json")) == "{\"a\":1}");
CHECK(read_file(extended_path(target / "vendor" / "machine" / "printer.json")) == "{\"b\":2}");
// The temporary dir guard cannot remove a deep tree on Windows, so drop it here.
boost::system::error_code ec;
fs::remove_all(extended_path(target), ec);
CHECK_FALSE(ec);
}
TEST_CASE("A zip archive under a path longer than MAX_PATH can be written and read", "[MinizExtension]")
{
ScopedTemporaryDir tmp;
// Create the long directory with the extended-length path, then exercise the archive
// reader/writer against an archive whose own path is past MAX_PATH.
const fs::path long_dir = tmp.path() / std::string(240, 'z');
fs::create_directories(extended_path(long_dir));
const fs::path zip_file = long_dir / "bundle.zip";
REQUIRE(zip_file.string().size() > 260);
write_zip(zip_file, {{"vendor.json", "{\"a\":1}"}});
const fs::path target = tmp.path() / "out";
fs::create_directories(target);
REQUIRE(extract_archive_confined(zip_file.string(), target.string()));
CHECK(read_file(target / "vendor.json") == "{\"a\":1}");
boost::system::error_code ec;
fs::remove_all(extended_path(long_dir), ec);
CHECK_FALSE(ec);
}
#ifndef _WIN32 #ifndef _WIN32
TEST_CASE("Confined extraction replaces a symlink at the destination instead of writing through it", "[MinizExtension]") TEST_CASE("Confined extraction replaces a symlink at the destination instead of writing through it", "[MinizExtension]")
{ {
@@ -985,6 +985,37 @@ TEST_CASE("A user filament synced to the cloud and back keeps its values and uns
} }
} }
TEST_CASE("A project filament storing one value for a per-variant key applies it to every variant", "[Preset][Bundle][Regression]")
{
PresetCollection filaments(Preset::TYPE_FILAMENT, Preset::filament_options(),
static_cast<const PrintRegionConfig &>(FullPrintConfig::defaults()));
Preset &parent = add_inmemory_preset(filaments, "Parent Filament");
parent.config.set_key_value("filament_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"}));
parent.config.set_key_value("adaptive_pressure_advance_model", new ConfigOptionStrings({"parent standard", "parent high flow"}));
parent.config.set_deserialize_strict("pressure_advance", "0.02,0.03");
parent.is_system = true;
// A filament saved before these keys became per-variant lists both variants but stores each key once.
const std::string model = "0.025,15.6,5000";
DynamicPrintConfig config;
config.set_key_value(BBL_JSON_KEY_INHERITS, new ConfigOptionString("Parent Filament"));
config.set_key_value("filament_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"}));
config.set_key_value("adaptive_pressure_advance_model", new ConfigOptionStrings({model}));
config.set_deserialize_strict("pressure_advance", "0.016");
auto embedded = std::make_unique<Preset>(Preset::TYPE_FILAMENT, "Project Filament", false);
embedded->config = std::move(config);
embedded->is_project_embedded = true;
std::vector<Preset *> project_presets{embedded.get()};
PresetsConfigSubstitutions substitutions;
filaments.load_project_embedded_presets(project_presets, PRESET_FILAMENT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable);
const Preset *loaded = filaments.find_preset("Project Filament");
REQUIRE(loaded != nullptr);
CHECK(loaded->config.option<ConfigOptionStrings>("adaptive_pressure_advance_model")->values == std::vector<std::string>{model, model});
check_double_vector(loaded->config.option<ConfigOptionFloats>("pressure_advance")->values, {0.016, 0.016});
}
TEST_CASE("Repeated loads of a user preset directory produce the same presets", "[Preset][Bundle]") TEST_CASE("Repeated loads of a user preset directory produce the same presets", "[Preset][Bundle]")
{ {
ScopedTemporaryDir temp_dir; ScopedTemporaryDir temp_dir;
+46
View File
@@ -521,3 +521,49 @@ TEST_CASE("ascii_iequals compares ASCII letters regardless of case", "[Utils]")
CHECK_FALSE(ascii_iequals("[", "{")); CHECK_FALSE(ascii_iequals("[", "{"));
CHECK_FALSE(ascii_iequals("@", "`")); CHECK_FALSE(ascii_iequals("@", "`"));
} }
TEST_CASE("extended_path turns a Windows path into its long-path form", "[Utils]") {
#ifdef _WIN32
namespace fs = boost::filesystem;
SECTION("a normal absolute drive path is prefixed") {
CHECK(extended_path(fs::path(R"(C:\models\model.3mf)")).wstring() == LR"(\\?\C:\models\model.3mf)");
}
SECTION("separators are made native") {
CHECK(extended_path(fs::path("C:/models/model.3mf")).wstring() == LR"(\\?\C:\models\model.3mf)");
}
SECTION("dot segments are collapsed before prefixing") {
// The "\\?\" prefix disables Win32 normalisation, so "." and ".." must be gone beforehand.
CHECK(extended_path(fs::path(R"(C:\models\..\model.3mf)")).wstring() == LR"(\\?\C:\model.3mf)");
}
SECTION("a trailing separator is dropped, not turned into a dot element") {
CHECK(extended_path(fs::path(R"(C:\models\)")).wstring() == LR"(\\?\C:\models)");
}
SECTION("a UNC path becomes an extended UNC path") {
CHECK(extended_path(fs::path(R"(\\server\share\model.3mf)")).wstring() == LR"(\\?\UNC\server\share\model.3mf)");
}
SECTION("an already extended path is returned unchanged") {
const fs::path extended(R"(\\?\C:\models\model.3mf)");
CHECK(extended_path(extended) == extended);
}
SECTION("a device path is returned unchanged") {
const fs::path device(R"(\\.\C:\models\model.3mf)");
CHECK(extended_path(device) == device);
}
SECTION("a bare device path without a root directory is returned unchanged") {
// Not "absolute" by Boost's rules, so it must be recognized before absolutizing.
const fs::path device(R"(\\.\PhysicalDrive0)");
CHECK(extended_path(device) == device);
}
SECTION("a relative path is completed against the working directory") {
const fs::path result = extended_path(fs::path("model.3mf"));
CHECK(result.wstring().rfind(L"\\\\?\\", 0) == 0);
CHECK(result.filename() == fs::path("model.3mf"));
}
#else
SECTION("non-Windows returns the path unchanged") {
const boost::filesystem::path p = boost::filesystem::temp_directory_path() / "model.3mf";
CHECK(extended_path(p) == p);
}
#endif
}
+4
View File
@@ -30,8 +30,10 @@ add_executable(${_TEST_NAME}_tests
test_plugin_capabilities_in_use.cpp test_plugin_capabilities_in_use.cpp
test_plugin_status.cpp test_plugin_status.cpp
test_printer_agent.cpp test_printer_agent.cpp
test_qidi_printer_agent.cpp
test_plugin_install.cpp test_plugin_install.cpp
test_plugin_lifecycle.cpp test_plugin_lifecycle.cpp
test_plugin_resource_texture.cpp
test_plugin_printer_agent.cpp test_plugin_printer_agent.cpp
test_printhost.cpp test_printhost.cpp
test_slicing_pipeline_bindings.cpp test_slicing_pipeline_bindings.cpp
@@ -44,7 +46,9 @@ add_executable(${_TEST_NAME}_tests
test_texture_color_palette.cpp test_texture_color_palette.cpp
test_texture_island_view.cpp test_texture_island_view.cpp
test_file_url.cpp test_file_url.cpp
test_imgui_wrapper.cpp
test_user_manager.cpp test_user_manager.cpp
test_sequential_clearance.cpp
../fff_print/test_helpers.cpp ../fff_print/test_helpers.cpp
) )
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff. Show more