Compare commits

..
Author SHA1 Message Date
Hanif Koh 86941859fa Keep a Scalar Config Option's Value When Deserializing Fails
A failed stream extraction writes 0 into its target, so Float, Int, Percent and FloatOrPercent options were overwritten before deserialize reported the error, and Point/Point3 kept a partially scanned value. Parse into a temporary and assign only on success; FloatOrPercent sets its percent flag only then too.
2026-10-19 16:41:04 +08:00
HanifKoh d3699dd9f6 Keep Python Bytecode out of the Bundled Python Home (#16550)
* Keep Python Bytecode out of the Bundled Python Home

The bundled stdlib ships .pyc files that are stale against their sources:
they are compiled when the Python deps are installed, and the copy into the
app does not keep the sources' mtimes. CPython then recompiles every module
it imports and writes the result next to the source. A drag-installed macOS
app rewrites files inside its own bundle on first launch, so its signature
no longer verifies, and read-only installs (AppImage, Flatpak, Program Files)
recompile the same modules on every launch.

Point pycache_prefix at <data_dir>/python/pycache so the interpreter reads
and writes bytecode there. The bundle is never written, the cache survives
between launches, and the data directory is already a plugin read/write root.

* Drop Cached Bytecode Through the Interpreter in the Module Release Test
2026-10-19 13:48:53 +08:00
HanifKoh a315a96c5c Show "No Available Printers" Instead of an Empty Printer Dropdown (#16611)
* Show "No Available Printers" Instead of an Empty Printer Dropdown

A ComboBox with no items opened a one-row-high popup that DropDown never
paints, leaving a blank strip with stale pixels under the box. Don't open
the list when it is empty, and label the printer box in the print and
send-to-printer dialogs when no printer is available.

* Add "No Available Printers" to the Translation Template
2026-10-19 13:30:36 +08:00
HanifKoh 24cf1c6bcc Fix Crash Selecting a User Printer Whose Parent Preset Is Not Loaded (#16579)
A user preset can inherit a system preset from a vendor that is not
installed. get_preset_base() returns null for it, and
get_selected_preset_base() dereferenced that unconditionally, so picking
the printer crashed while building the shared profiles notification URL.
Fall back to the selected preset itself. The create printer and create
filament dialogs had the same null dereference on such presets.
2026-10-19 13:29:26 +08:00
Kris Austin d16ca97380 Snapmaker U1: set the top cover purifier mode and match Snapmaker's start G-code (#16590)
Port the U1 top cover purifier mode and the related start G-code
changes from Snapmaker's Orca (Snapmaker/OrcaSlicer#504 and
Snapmaker/OrcaSlicer#1005, as of 8931178229).

The start G-code sends SET_PURIFIER_MODE for strong cooling, weak
cooling or keep warm, picked from the lowest softening temperature of
the filaments used through min_vitrification_temperature, with
Snapmaker's thresholds and parameters. It also pre-extrudes the first
tool, and turns the part fan off for probing and back on for nozzle
cleaning.

U1 filament presets use Snapmaker's softening temperatures. U1 presets
that have no U1 version in Snapmaker's Orca use the value Snapmaker
gives that material on the U1.
2026-10-18 21:35:32 -05:00
Kris Austin da4d766d46 Fix Moonraker status stream hanging app exit and missing dead printers (#16604)
The status stream read the websocket synchronously. Beast's
expires_after() only applies to async operations, so when a printer
stopped sending, ws.read() blocked forever, and stop_status_stream()
joined that thread when the app closed. On Windows the stuck process
also froze the next launch through the shared WebView2 browser.

Run the whole stream on one io_context owned by the stream thread, and
have stop_status_stream() post an abort to it. A stop on an open
websocket sends a close frame and waits up to 1 s for the answer.
Keep-alive pings detect a dead printer, and the stream reconnects until
stopped, with the backoff capped at 32 s. When a quiet printer still
answers pings, its last status is sent again. The stream starts under
connect_mutex after a staleness check, and a stop from the stream's own
callback no longer joins its own thread.

The Moonraker tests move into test_moonraker_printer_agent.cpp.
2026-10-18 19:33:58 -05:00
Kris Austin 06665d0558 Post a readable diff of changed custom G-code on profile PRs (#16605)
A profile stores each custom G-code setting as one JSON string, so GitHub
shows a G-code edit as one long changed line and reviewers cannot see
what changed.

Add a gcode-diff command to orca_profile_tool.py. For each changed
profile file it decodes, on both sides, every *_gcode and multiline
string option PrintConfig.cpp defines, the options Tab.cpp edits as
code, and any string value that spans several lines. It prints one
collapsed unified diff per setting as markdown. Files with the same diff
for a setting share one section. Trailing spaces are removed, file names
cannot mention GitHub users, and the output is cut to whole sections to
fit a GitHub comment.

Check profiles runs it between the PR's merge commit and its first
parent and adds the result to its artifact, with a line naming the PR
commit. Post profile check comment posts it under its own marker. It
keeps the comment while the diff is unchanged, edits it in place when
only the commit changed, replaces it when the diff changes, and deletes
it when the PR no longer changes any G-code.

The posting job still runs no PR code. It now posts only when the run
checked the head of the PR the artifact names, which covers the
validation comment too.
2026-10-18 13:33:53 -05:00
Kris Austin 0f59cd5d05 Let Global shortcuts run while typing, except keys that edit text (#16558)
#16505 skipped every Global shortcut while ImGui had the keyboard, so
Ctrl+S, Ctrl+R and the others did nothing while the cursor was in the
text tool's box. Skip only the chords that edit text, and only while a
text field (a wx text entry, a spin control, a web view, or an active
ImGui InputText) has the focus. Skip every Global shortcut while an
ImGui modal is open on the focused canvas.

KeyChord::edits_text() defines the text-editing chords. The Global page
of the shortcuts dialog no longer says these shortcuts apply while
typing.
2026-10-18 13:32:41 -05:00
yw4z c1fb801ff3 Linux: Show display backend of app on Troubleshoot dialog (#16615)
* init

* Update TroubleshootDialog.cpp

* fix build
2026-10-18 22:45:30 +08:00
Lam Wei Lun 1ee0b730a1 Linux: use WebKit's shared-memory transport in hypervisor guests
Web views in VirtualBox (and other virtual GPUs) fail on WebKitGTK >= 2.44
because the guest GL stack cannot back the hardware DMABUF renderer. Setting
WEBKIT_DMABUF_RENDERER_FORCE_SHM=1 fixes it and keeps accelerated compositing,
confirmed in VirtualBox on WebKitGTK 2.54.

Detect a hypervisor guest from the cpuinfo hypervisor flag before GTK init and
set the variable non-replacing, so a user-exported value still wins. Bare metal
is unchanged. The skill references are updated to match.
2026-10-18 04:43:45 +08:00
Kiss Lorand 72ba7a2b35 Fix incorrect layer number in G-code conflict warning (#16588)
Fix layer number in G-code conflict warning

Correct the comparison used to look up a layer by Z height.

The reversed comparison could return the total layer count instead of
the layer corresponding to the reported conflict height.
2026-10-17 23:04:50 +03:00
Downtime 8160f64cae Skip the LAN reconnect after a print to the selected printer (#16271)
After a print, `Plater::print_job_finished` selects the target printer again. When that printer was already the selected LAN printer, `set_selected_machine` disconnected, reset and reconnected it for no reason. Skip the call in that case. Selecting the printer by hand still reconnects it, and cloud printers are unchanged.
2026-10-17 13:50:38 -05: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
149 changed files with 15674 additions and 7703 deletions

No files matched your search

@@ -569,12 +569,19 @@ on the **first** call. So:
#endif
```
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) branches on
`GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On the default path it forces `GDK_BACKEND=x11`
when wx lacks EGL and `WAYLAND_DISPLAY` is set. It sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
(non-replacing) only when both `DISPLAY` and `WAYLAND_DISPLAY` are set, and calls `XInitThreads()`
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) sets the NVIDIA
DMABUF workaround and then branches on `GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On both
paths, 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. On a hypervisor guest (`/proc/cpuinfo` `hypervisor` flag) and WebKitGTK >= 2.44 it sets
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` alone, since virtual GPUs cannot back the hardware path either. 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
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.
**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
needs a reload lets it through. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
12. Keep `WebViewWebKit`'s destructor removing the `"wx"` handler. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond Orca's default-path XWayland case, and never
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)
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
@@ -567,33 +568,74 @@ vue-resize workaround script and sets a persistent SQLite cookie store on the vi
## WebKitGTK on Linux sessions
**Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises)
branches on `GDK_BACKEND`:
sets the DMABUF workarounds 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.
- **Hypervisor guests**, same position: when `is_hypervisor_guest()` (the `hypervisor` flag in
`/proc/cpuinfo`) and WebKitGTK >= 2.44, it sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` non-replacing.
Virtual GPUs (VirtualBox VMSVGA, VMware, QEMU) cannot back the hardware DMABUF path either, and
FORCE_SHM alone was confirmed to fix web views in VirtualBox; the `WEBKIT_FORCE_DMABUF_RENDERER` pair
is not needed without the Debian `isNVIDIA()` patch. The flag is x86-only, so ARM guests are not covered.
- **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`
non-replacing **only** when both `DISPLAY` and `WAYLAND_DISPLAY` are non-empty (a Wayland session with
XWayland available — the session, not the backend GTK will pick); `XInitThreads()` runs only when
`DISPLAY` is set.
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`, and in that same branch it sets
`WEBKIT_DISABLE_COMPOSITING_MODE=1` non-replacing when the linked WebKitGTK predates 2.42
(`WebView::NeedsCompositingWorkaround()`). That forced X11 backend in a Wayland session is the only
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
`/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 on keeps hardware acceleration for Device / Setup Wizard / login pages).
compositing, which keeps hardware acceleration for Device / Setup Wizard / login pages.
Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_running_on_x11()`
(see `references/platforms.md`), not these environment variables.
**Pitfall.**
- **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path in Wayland sessions,
non-replacing (`setenv(..., false)`, so a value the user already exported wins), before GTK
initialises; never unconditionally. Keep JS-side fallbacks so a page never
- **Rule:** On NVIDIA and hypervisor guests reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver and virtual GPUs cannot back WebKit's
hardware 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.
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland, leaving web views
blank (Setup Wizard stuck on "Loading..."); disabling it everywhere needlessly drops hardware
acceleration; without the JS timeout the user is stuck when the signal is lost.
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland before 2.42, leaving
web views blank (Setup Wizard stuck on "Loading..."); the variable also turns off threaded scrolling,
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
// Wrong: unconditional, kills HW acceleration everywhere
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false);
// Right (default path only): non-replacing, before GTK init, Wayland session with XWayland
if (display && *display && wayland && *wayland)
// Right (default path only): non-replacing, before GTK init, WebKitGTK < 2.42,
// 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);
```
Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit`
+25
View File
@@ -232,6 +232,31 @@ jobs:
mkdir -p ${{ runner.temp }}/profile-check-results
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt
# Builds a readable diff of the custom G-code the PR changes, which GitHub shows as
# one long line per setting, for check_profiles_comment.yml to post.
- name: Diff custom G-code
if: ${{ always() && github.event_name == 'pull_request' }}
continue-on-error: true
env:
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
git fetch --no-tags --depth=2 origin "$(git rev-parse HEAD)"
results=${{ runner.temp }}/profile-check-results
diff=${{ runner.temp }}/gcode_diff.md
# HEAD is the PR's merge commit, so HEAD^1 is the base it merges onto.
python3 ./scripts/orca_profile_tool.py gcode-diff --base HEAD^1 --head HEAD > "$diff"
if [ -s "$diff" ]; then
{
echo "<!-- profile-gcode-diff-comment -->"
cat "$diff"
echo ""
# The comment workflow finds this line as the only one starting with <sub>.
echo "<sub>For commit ${PR_HEAD_SHA} of this PR.</sub>"
} > "$results/gcode_diff.md"
fi
# Tells the comment workflow that a missing gcode_diff.md means no G-code changed.
touch "$results/gcode_diff_done.txt"
- name: Prepare comment artifact
if: ${{ always() && github.event_name == 'pull_request' && (steps.profile_tool.outcome == 'failure' || steps.legacy_compat.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
run: |
+58 -3
View File
@@ -38,6 +38,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
if [ ! -f pr_number.txt ]; then
echo "No pr_number.txt in artifact, skipping."
@@ -49,18 +50,72 @@ jobs:
echo "Invalid PR number: $PR_NUMBER"
exit 1
fi
# PR code wrote the artifact, so only a run for the named PR's current head may post.
if [ -z "$HEAD_SHA" ]; then
echo "The triggering run names no head commit."
exit 1
fi
if ! PR_HEAD=$(gh api "repos/${GH_REPO}/pulls/${PR_NUMBER}" --jq .head.sha); then
echo "Cannot look up PR #${PR_NUMBER}."
exit 1
fi
if [ "$HEAD_SHA" != "$PR_HEAD" ]; then
echo "${HEAD_SHA} is not the head of PR #${PR_NUMBER}; skipping."
exit 0
fi
# Lists every bot comment on the PR, one JSON object per line.
COMMENTS=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" \
--jq '.[] | select(.user.login == "github-actions[bot]") | {id, body} | @json')
# Delete prior comments matching the marker (from check_profiles.yml) or the legacy heading.
OLD_IDS=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" \
--jq '.[] | select(.user.login == "github-actions[bot]") | select((.body | startswith("<!-- profile-validation-comment -->")) or (.body | startswith("## :x: Profile Validation Errors"))) | .id')
OLD_IDS=$(printf '%s\n' "$COMMENTS" | jq -r 'select((.body | startswith("<!-- profile-validation-comment -->")) or (.body | startswith("## :x: Profile Validation Errors"))) | .id')
for comment_id in $OLD_IDS; do
echo "Deleting outdated profile-validation comment ${comment_id}"
gh api -X DELETE "repos/${GH_REPO}/issues/comments/${comment_id}" || true
done
# Post a new comment only when validation failed (pr_comment.md present).
# A failed post fails the job only after the G-code diff is handled.
failed=0
if [ -f pr_comment.md ]; then
gh pr comment "$PR_NUMBER" --body-file pr_comment.md
gh pr comment "$PR_NUMBER" --body-file pr_comment.md || failed=1
else
echo "Validation succeeded; cleaned up prior comments without posting."
fi
# Makes the G-code diff comment match gcode_diff.md, or deletes it if there is none.
update_gcode_diff_comment() {
if [ ! -f gcode_diff_done.txt ]; then
echo "The G-code diff step did not finish; leaving any G-code diff comment as it is."
return 0
fi
GCODE_IDS=$(printf '%s\n' "$COMMENTS" | jq -r 'select(.body | startswith("<!-- profile-gcode-diff-comment -->")) | .id')
if [ -f gcode_diff.md ] && [ "$(wc -c < gcode_diff.md)" -gt 65536 ]; then
echo "gcode_diff.md is over GitHub's comment limit; not posting it."
rm gcode_diff.md
fi
if [ -f gcode_diff.md ] && [ "$(echo "$GCODE_IDS" | wc -w)" -eq 1 ]; then
OLD_BODY=$(printf '%s\n' "$COMMENTS" | jq -r --argjson id "$GCODE_IDS" 'select(.id == $id) | .body' | tr -d '\r')
NEW_BODY=$(tr -d '\r' < gcode_diff.md)
if [ "$OLD_BODY" = "$NEW_BODY" ]; then
echo "G-code diff unchanged; keeping comment ${GCODE_IDS}."
return 0
fi
# Edits in place when only the <sub> line differs, since edits notify nobody.
if [ "$(grep -v '^<sub>' <<< "$OLD_BODY")" = "$(grep -v '^<sub>' <<< "$NEW_BODY")" ]; then
echo "G-code diff unchanged at a new commit; updating comment ${GCODE_IDS}."
gh api -X PATCH "repos/${GH_REPO}/issues/comments/${GCODE_IDS}" -F body=@gcode_diff.md > /dev/null
return 0
fi
fi
for comment_id in $GCODE_IDS; do
echo "Deleting outdated G-code diff comment ${comment_id}"
gh api -X DELETE "repos/${GH_REPO}/issues/comments/${comment_id}" || true
done
if [ -f gcode_diff.md ]; then
gh pr comment "$PR_NUMBER" --body-file gcode_diff.md
fi
}
update_gcode_diff_comment
exit "$failed"
+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.
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}")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
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 " -h: prints this help text"
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 " -s: build the Orca Slicer (optional)"
echo " -t: build tests (optional), requires -s flag"
@@ -549,6 +549,20 @@ fi
if [ -n "${CMAKE_CCACHE}" ] ; then
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
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
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
fi
+50
View File
@@ -111,6 +111,54 @@ else
export CMAKE_POLICY_COMPAT=""
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 " - ARCH: $ARCH"
echo " - BUILD_CONFIG: $BUILD_CONFIG"
@@ -162,6 +210,7 @@ function build_deps() {
-DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT}
fi
cmake --build . --config "$BUILD_CONFIG" --target deps
@@ -264,6 +313,7 @@ function build_slicer() {
-DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT}
fi
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
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
URL_HASH SHA256=d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
DEPENDS ${ZLIB_PKG}
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
URL_HASH SHA256=1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
CMAKE_ARGS
-DENABLE_SHARED=OFF
-DENABLE_STATIC=ON
${jpeg_flag}
)
+7
View File
@@ -53,6 +53,13 @@ Space counts as typing. The speed dial's default is the one bare Space, and
`MainFrame` leaves it to a focused control that uses Space itself (text fields, buttons,
combo boxes), so it opens the dial from the canvases and the tab strip only.
`MainFrame::handle_global_shortcut` skips a chord that `KeyChord::edits_text()` reports
while a text field has the focus (a wx text entry, spin control, web view or ImGui
InputText), and skips every chord while an ImGui modal is open, as a wx modal dialog
would. `ImGuiWrapper::owns_key()` reads the active InputText and the popup stack
directly, since `WantCaptureKeyboard` lags a frame, and is called only when the focus is on
a visible canvas from `OpenGLManager::create_wxglcanvas()`.
## Which event a chord matches
Letters, digits and special keys match on `wxEVT_KEY_DOWN`. Its key codes do not depend
+3
View File
@@ -10657,6 +10657,9 @@ msgstr ""
msgid "Send Failed"
msgstr ""
msgid "No available printers"
msgstr ""
msgid "The name length exceeds the limit."
msgstr ""
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

+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

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Snapmaker",
"version": "02.04.00.17",
"version": "02.04.00.18",
"force_update": "0",
"description": "Snapmaker configurations",
"machine_model_list": [
@@ -49,5 +49,8 @@
],
"pressure_advance": [
"0.02"
],
"temperature_vitrification": [
"100"
]
}
@@ -46,5 +46,8 @@
],
"fan_min_speed": [
"0"
],
"temperature_vitrification": [
"100"
]
}
@@ -45,7 +45,7 @@
"250"
],
"temperature_vitrification": [
"178"
"70"
],
"fan_max_speed": [
"20"
@@ -4,5 +4,8 @@
"inherits": "fdm_filament_petg",
"from": "system",
"filament_id": "OFW8lqnb",
"instantiation": "false"
"instantiation": "false",
"temperature_vitrification": [
"70"
]
}
@@ -60,7 +60,7 @@
"250"
],
"temperature_vitrification": [
"178"
"70"
],
"fan_max_speed": [
"20"
@@ -34,5 +34,8 @@
],
"nozzle_temperature": [
"220"
],
"temperature_vitrification": [
"45"
]
}
@@ -34,5 +34,8 @@
],
"filament_cooling_final_speed": [
"60"
],
"temperature_vitrification": [
"45"
]
}
@@ -34,5 +34,8 @@
],
"nozzle_temperature": [
"220"
],
"temperature_vitrification": [
"45"
]
}
@@ -40,5 +40,8 @@
],
"nozzle_temperature": [
"220"
],
"temperature_vitrification": [
"45"
]
}
@@ -31,5 +31,8 @@
],
"slow_down_layer_time": [
"8"
],
"temperature_vitrification": [
"45"
]
}
@@ -50,5 +50,8 @@
"additional_cooling_fan_speed": [
"70",
"70"
],
"temperature_vitrification": [
"30"
]
}
@@ -137,7 +137,7 @@
"240"
],
"temperature_vitrification": [
"60"
"30"
],
"nozzle_temperature_range_low": [
"200"
@@ -302,7 +302,7 @@
"-1"
],
"temperature_vitrification": [
"60"
"30"
],
"textured_cool_plate_temp": [
"40"
@@ -29,5 +29,8 @@
"additional_cooling_fan_speed": [
"70",
"70"
],
"temperature_vitrification": [
"30"
]
}
@@ -49,5 +49,8 @@
"additional_cooling_fan_speed": [
"70",
"70"
],
"temperature_vitrification": [
"30"
]
}
@@ -97,7 +97,7 @@
"20",
"12"
],
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0",
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\n;===== top cover purifier =====\n{if min_vitrification_temperature <= 50}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=48 FAN_SPEED=0.6 DELAY_OFF=0 ; strong cooling\n{elsif min_vitrification_temperature <= 70}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=45 ALARM_TEMP=51 FAN_SPEED=0.6 DELAY_OFF=0 ; weak cooling\n{else}\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600 ; keep warm\n{endif}\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\n\n;==== 预挤出耗材 =====\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nT{initial_extruder}\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nSM_PRINT_PREEXTRUDE_FILAMENT INDEX={initial_extruder}\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nM107\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nM107\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nM106 S255\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\nM107\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\nM107\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; AlwLine truncated
"machine_tool_change_time": "5",
"max_layer_height": [
"0.14",
@@ -97,7 +97,7 @@
"20",
"12"
],
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0",
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\n;===== top cover purifier =====\n{if min_vitrification_temperature <= 50}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=48 FAN_SPEED=0.6 DELAY_OFF=0 ; strong cooling\n{elsif min_vitrification_temperature <= 70}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=45 ALARM_TEMP=51 FAN_SPEED=0.6 DELAY_OFF=0 ; weak cooling\n{else}\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600 ; keep warm\n{endif}\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\n\n;==== 预挤出耗材 =====\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nT{initial_extruder}\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nSM_PRINT_PREEXTRUDE_FILAMENT INDEX={initial_extruder}\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nM107\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nM107\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nM106 S255\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\nM107\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\nM107\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; AlwLine truncated
"machine_tool_change_time": "5",
"max_layer_height": [
"0.32",
@@ -97,7 +97,7 @@
"20",
"12"
],
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0",
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\n;===== top cover purifier =====\n{if min_vitrification_temperature <= 50}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=48 FAN_SPEED=0.6 DELAY_OFF=0 ; strong cooling\n{elsif min_vitrification_temperature <= 70}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=45 ALARM_TEMP=51 FAN_SPEED=0.6 DELAY_OFF=0 ; weak cooling\n{else}\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600 ; keep warm\n{endif}\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\n\n;==== 预挤出耗材 =====\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nT{initial_extruder}\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nSM_PRINT_PREEXTRUDE_FILAMENT INDEX={initial_extruder}\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nM107\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nM107\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nM106 S255\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\nM107\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\nM107\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; AlwLine truncated
"machine_tool_change_time": "5",
"max_layer_height": [
"0.42",
@@ -97,7 +97,7 @@
"20",
"12"
],
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0",
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\n;===== top cover purifier =====\n{if min_vitrification_temperature <= 50}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=48 FAN_SPEED=0.6 DELAY_OFF=0 ; strong cooling\n{elsif min_vitrification_temperature <= 70}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=45 ALARM_TEMP=51 FAN_SPEED=0.6 DELAY_OFF=0 ; weak cooling\n{else}\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600 ; keep warm\n{endif}\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\n\n;==== 预挤出耗材 =====\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nT{initial_extruder}\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nSM_PRINT_PREEXTRUDE_FILAMENT INDEX={initial_extruder}\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nM107\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nM107\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nM106 S255\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\nM107\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\nM107\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; AlwLine truncated
"machine_tool_change_time": "5",
"max_layer_height": [
"0.56",
@@ -36,11 +36,6 @@ uniform int palette_b[64];
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
// False means the texture is a single placement, not a repeat. The sampler's wrap mode can express
// that for the *height* - a zero border reads as no displacement - but not for colour, because a
// zero border is indistinguishable from the image genuinely being black there. So the uv is tested
// instead, which is what sample_layer_color() does on the CPU side.
uniform bool tile_enabled;
uniform bool volume_mirrored;
uniform mat4 view_model_matrix;
@@ -50,14 +45,6 @@ uniform sampler2D height_tex;
uniform vec2 height_tex_texel;
uniform float depth_mm;
uniform float tiling_scale;
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - the square tile
// every layer had before the two axes could differ - which is also what layer_tile_mm() does on the
// CPU side, so an unset uniform behaves exactly as this shader always did.
uniform float tiling_scale_v;
// Mirror the texture along one axis of the tile. Both default to false, which GLSL guarantees for an
// uninitialised bool uniform, so a shader whose caller has not set them behaves exactly as before.
uniform bool mirror_u;
uniform bool mirror_v;
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
uniform float tex_aspect;
@@ -67,12 +54,6 @@ uniform bool invert;
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
// The placement's rotation and its inverse, so this shader turns a world position into the texture
// frame exactly as texture_displacement_bake_frame() does. Both are passed rather than one
// transposed here, because GLSL 110 has no transpose(). Identity leaves the old world-aligned
// behaviour, which is what an unset uniform gives.
uniform mat3 tex_rot; // world -> texture frame
uniform mat3 tex_rot_inv; // texture frame -> world
uniform bool use_vertex_uv;
// 2x3 affine (lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the dragged island's uv; see the
// 140 variant. Identity when nothing is dragged.
@@ -181,29 +162,13 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
}
}
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
vec2 tile_mm()
{
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
}
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
vec2 mirror_sign()
{
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
}
vec2 project_uv(vec3 p, vec3 n)
{
vec2 planar = projection_raw(p, n);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
// the same division, in the tile's own frame.
r *= mirror_sign() / tile_mm();
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
r.y *= tex_aspect;
return r + uv_offset;
@@ -272,12 +237,9 @@ void main()
// World millimetres throughout, like the bake - see the 140 variant.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); // the frame the texture is projected in, as the bake does
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
if (volume_mirrored)
triangle_normal = -triangle_normal;
// The same normal in that frame. triangle_normal itself stays in world, because the lighting at
// the end of this shader needs it there.
vec3 tex_normal = tex_rot * triangle_normal;
// Where the colour is read from. Both branches below already compute the uv this fragment's
// *height* came from - including the parallax-marched one on the triplanar path - and the colour
@@ -308,14 +270,14 @@ void main()
} else if (weight > 0.0) {
vec3 t, b;
vec2 units_per_mm;
projection_axes(tex_pos, tex_normal, t, b, units_per_mm);
projection_axes(tex_pos, triangle_normal, t, b, units_per_mm);
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
// first point where it drops below the displaced surface (see header).
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
vec3 view_dir = normalize(tex_rot * eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, tex_normal);
vec2 uv = project_uv(tex_pos, tex_normal);
vec3 view_dir = normalize(eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, triangle_normal);
vec2 uv = project_uv(tex_pos, triangle_normal);
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
@@ -327,18 +289,18 @@ void main()
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
// a texel there is no parallax to find and the march would be pure cost - which is the common
// case of looking straight down at a surface.
float sweep = length(view_dir - tex_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
if (v_dot_n > 0.05 && sweep > 0.5 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) {
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
// by construction, and step inward; the crossing is what this pixel actually sees.
float s = h_hi / v_dot_n;
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_normal);
vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
for (int i = 0; i < PARALLAX_STEPS; ++i) {
s -= ds;
vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_normal);
vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
if (gap <= 0.0) {
// Crossed between the last two samples - interpolating the hit is what stops it
@@ -360,23 +322,18 @@ void main()
float hU = texture2D(height_tex, uv + vec2(0.0, height_tex_texel.y)).r;
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
vec2 tile = tile_mm();
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
// One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
// the uv-space gradient carries its own factor before being rotated back into t/b.
// Each component carries its own extent and its own mirror sign, both of which project_uv()
// applied on the way in; without the sign a mirrored layer would light as though it were not.
vec2 ms = mirror_sign();
vec2 g = vec2(dh_duv.x * ms.x / tile.x, dh_duv.y * tex_aspect * ms.y / tile.y);
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
// component of the gradient carries the extra factor before being rotated back into t/b.
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
// ...and back out of raw-projection units into millimetres along t / b; see the 140 variant.
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm;
// t and b came out of the texture frame, so the gradient is in it too and has to come back
// to world before it perturbs the normal the lighting uses.
vec3 gradient = tex_rot_inv * (slope.x * t + slope.y * b);
vec3 gradient = slope.x * t + slope.y * b;
gradient -= triangle_normal * dot(triangle_normal, gradient);
triangle_normal = normalize(triangle_normal - gradient);
}
@@ -396,9 +353,7 @@ void main()
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
bool uv_covered = tile_enabled ||
(all(greaterThanEqual(color_uv, vec2(0.0))) && all(lessThan(color_uv, vec2(1.0))));
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0 && uv_covered)
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
albedo = palette_rgb[nearest_palette_entry(texture2D(color_tex, color_uv).rgb)];
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
@@ -16,16 +16,7 @@ uniform bool volume_mirrored;
uniform int mode;
uniform float checker_freq;
uniform float tiling_scale;
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - see
// layer_tile_mm() on the CPU side, so an unset uniform behaves exactly as this shader always did.
uniform float tiling_scale_v;
// Mirror along one tile axis; false by default, so an unset uniform is the old behaviour.
uniform bool mirror_u;
uniform bool mirror_v;
uniform vec3 tex_anchor; // the volume's origin in world space
// The placement's rotation, so this overlay projects the same frame the bake does. Identity, which
// is what an unset uniform gives, leaves the old world-aligned behaviour.
uniform mat3 tex_rot; // world -> texture frame
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool use_vertex_uv;
@@ -88,30 +79,13 @@ vec2 projection_raw(vec3 p, vec3 n)
return vec2(box_face_sign(n.z) * p.x, -p.y);
}
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
vec2 tile_mm()
{
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
}
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
vec2 mirror_sign()
{
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
}
vec2 project_uv(vec3 p, vec3 n)
{
vec2 planar = projection_raw(p, n);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
// the same division, in the tile's own frame.
r *= mirror_sign() / tile_mm();
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
return r + uv_offset;
}
@@ -131,7 +105,7 @@ void main()
// World space anchored at the volume's origin, like the bake and the shaded preview.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor);
vec3 tex_pos = world_pos.xyz - tex_anchor;
if (volume_mirrored)
triangle_normal = -triangle_normal;
@@ -139,7 +113,7 @@ void main()
if (mode == 1) {
base = heatmap(distortion);
} else {
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * triangle_normal);
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal);
vec2 c = floor(uv * checker_freq);
float check = mod(c.x + c.y, 2.0);
base = (check < 0.5) ? vec3(0.22, 0.23, 0.26) : vec3(0.82, 0.83, 0.86);
@@ -95,11 +95,6 @@ uniform int palette_b[64];
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
// False means the texture is a single placement, not a repeat. The sampler's wrap mode can express
// that for the *height* - a zero border reads as no displacement - but not for colour, because a
// zero border is indistinguishable from the image genuinely being black there. So the uv is tested
// instead, which is what sample_layer_color() does on the CPU side.
uniform bool tile_enabled;
uniform bool volume_mirrored;
uniform mat4 view_model_matrix;
@@ -109,14 +104,6 @@ uniform sampler2D height_tex;
uniform vec2 height_tex_texel; // (1/width, 1/height) of height_tex
uniform float depth_mm;
uniform float tiling_scale;
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - the square tile
// every layer had before the two axes could differ - which is also what layer_tile_mm() does on the
// CPU side, so an unset uniform behaves exactly as this shader always did.
uniform float tiling_scale_v;
// Mirror the texture along one axis of the tile. Both default to false, which GLSL guarantees for an
// uninitialised bool uniform, so a shader whose caller has not set them behaves exactly as before.
uniform bool mirror_u;
uniform bool mirror_v;
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
uniform float tex_aspect;
@@ -126,12 +113,6 @@ uniform bool invert;
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
// The placement's rotation and its inverse, so this shader turns a world position into the texture
// frame exactly as texture_displacement_bake_frame() does. Both are passed rather than one
// transposed here, because GLSL 110 has no transpose(). Identity leaves the old world-aligned
// behaviour, which is what an unset uniform gives.
uniform mat3 tex_rot; // world -> texture frame
uniform mat3 tex_rot_inv; // texture frame -> world
uniform bool use_vertex_uv; // true: sample at vertex_uv with a derived tangent frame (LSCM)
// A 2x3 affine (columns packed as lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the uv of
// the island currently being dragged in the UV editor (island_active > 0.5). Identity when nothing is
@@ -247,29 +228,13 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
}
}
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
vec2 tile_mm()
{
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
}
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
vec2 mirror_sign()
{
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
}
vec2 project_uv(vec3 p, vec3 n)
{
vec2 planar = projection_raw(p, n);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
// the same division, in the tile's own frame.
r *= mirror_sign() / tile_mm();
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
r.y *= tex_aspect;
return r + uv_offset;
@@ -340,12 +305,9 @@ void main()
// printed part whatever the instance's scale or rotation, so the preview has to project from the
// world position and perturb the world normal.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); // the frame the texture is projected in, as the bake does
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
if (volume_mirrored)
triangle_normal = -triangle_normal;
// The same normal in that frame. triangle_normal itself stays in world, because the lighting at
// the end of this shader needs it there.
vec3 tex_normal = tex_rot * triangle_normal;
// Where the colour is read from. Both branches below already compute the uv this fragment's
// *height* came from - including the parallax-marched one on the triplanar path - and the colour
@@ -389,14 +351,14 @@ void main()
// closed-form uv here, unlike the LSCM case.
vec3 t, b;
vec2 units_per_mm;
projection_axes(tex_pos, tex_normal, t, b, units_per_mm);
projection_axes(tex_pos, triangle_normal, t, b, units_per_mm);
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
// first point where it drops below the displaced surface (see header).
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
vec3 view_dir = normalize(tex_rot * eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, tex_normal);
vec2 uv = project_uv(tex_pos, tex_normal);
vec3 view_dir = normalize(eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, triangle_normal);
vec2 uv = project_uv(tex_pos, triangle_normal);
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
@@ -408,18 +370,18 @@ void main()
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
// a texel there is no parallax to find and the march would be pure cost - which is the common
// case of looking straight down at a surface.
float sweep = length(view_dir - tex_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
if (v_dot_n > 0.05 && sweep > 0.5 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) {
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
// by construction, and step inward; the crossing is what this pixel actually sees.
float s = h_hi / v_dot_n;
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_normal);
vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
for (int i = 0; i < PARALLAX_STEPS; ++i) {
s -= ds;
vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_normal);
vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
if (gap <= 0.0) {
// Crossed between the last two samples - interpolating the hit is what stops it
@@ -443,28 +405,23 @@ void main()
// Central difference, per uv unit (not per texel).
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
// uv -> mm is the reciprocal of the tile extents, applied per component below, which is what
// turns the uv-space gradient into a real surface slope.
vec2 tile = tile_mm();
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
// uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space
// gradient into a real surface slope.
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
// uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the
// gradient back into the axes' frame.
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
// One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
// the uv-space gradient carries its own factor before being rotated back into t/b.
// Each component carries its own extent and its own mirror sign, both of which project_uv()
// applied on the way in; without the sign a mirrored layer would light as though it were not.
vec2 ms = mirror_sign();
vec2 g = vec2(dh_duv.x * ms.x / tile.x, dh_duv.y * tex_aspect * ms.y / tile.y);
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
// component of the gradient carries the extra factor before being rotated back into t/b.
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
// ...and back out of raw-projection units into millimetres of travel along t / b, which is a
// no-op except for the spherical longitude (see projection_axes()). After the inverse rotation,
// because units_per_mm is expressed in the t/b frame rather than in uv.
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm;
// t and b came out of the texture frame, so the gradient is in it too and has to come back
// to world before it perturbs the normal the lighting uses.
vec3 gradient = tex_rot_inv * (slope.x * t + slope.y * b);
vec3 gradient = slope.x * t + slope.y * b;
gradient -= triangle_normal * dot(triangle_normal, gradient);
triangle_normal = normalize(triangle_normal - gradient);
}
@@ -484,9 +441,7 @@ void main()
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
bool uv_covered = tile_enabled ||
(all(greaterThanEqual(color_uv, vec2(0.0))) && all(lessThan(color_uv, vec2(1.0))));
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0 && uv_covered)
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
albedo = palette_rgb[nearest_palette_entry(texture(color_tex, color_uv).rgb)];
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
@@ -25,16 +25,7 @@ uniform bool volume_mirrored;
uniform int mode; // 0 checker, 1 distortion
uniform float checker_freq; // checker squares per uv unit (one uv unit == one texture tile)
uniform float tiling_scale;
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - see
// layer_tile_mm() on the CPU side, so an unset uniform behaves exactly as this shader always did.
uniform float tiling_scale_v;
// Mirror along one tile axis; false by default, so an unset uniform is the old behaviour.
uniform bool mirror_u;
uniform bool mirror_v;
uniform vec3 tex_anchor; // the volume's origin in world space
// The placement's rotation, so this overlay projects the same frame the bake does. Identity, which
// is what an unset uniform gives, leaves the old world-aligned behaviour.
uniform mat3 tex_rot; // world -> texture frame
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool use_vertex_uv;
@@ -99,30 +90,13 @@ vec2 projection_raw(vec3 p, vec3 n)
return vec2(box_face_sign(n.z) * p.x, -p.y);
}
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
vec2 tile_mm()
{
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
}
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
vec2 mirror_sign()
{
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
}
vec2 project_uv(vec3 p, vec3 n)
{
vec2 planar = projection_raw(p, n);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
// the same division, in the tile's own frame.
r *= mirror_sign() / tile_mm();
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
return r + uv_offset;
}
@@ -143,7 +117,7 @@ void main()
// World space anchored at the volume's origin, like the bake and the shaded preview.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor);
vec3 tex_pos = world_pos.xyz - tex_anchor;
if (volume_mirrored)
triangle_normal = -triangle_normal;
@@ -151,7 +125,7 @@ void main()
if (mode == 1) {
base = heatmap(distortion);
} else {
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * triangle_normal);
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal);
vec2 c = floor(uv * checker_freq);
float check = mod(c.x + c.y, 2.0);
// Two distinct greys, plus a faint tint on one set so orientation is readable at a glance.
@@ -18,7 +18,7 @@
<script src="../js/plugin-config-ui.js"></script>
<script src="./index.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>
</head>
<body onLoad="OnInit()">
+6 -2
View File
@@ -156,12 +156,16 @@ function SetCloudMode( bOn )
m_CloudMode=bOn;
$("body").toggleClass("cloud-mode", bOn);
if( bOn )
{
$(".BtnItem").removeClass("BtnItemSelected");
$(".BtnShortcut").addClass("BtnItemSelected");
}
}
function GotoCloud()
{
$(".BtnItem").removeClass("BtnItemSelected");
$(".BtnShortcut").addClass("BtnItemSelected");
SetCloudMode(true);
SendSimpleCommand("homepage_open_cloud");
}
@@ -336,10 +336,10 @@ modules:
sha256: 3d22d46c566b1761a0e15ea397589b3a5f36ac09b7c785382e6470156c04247f
dest: external-packages/PNG
# libjpeg-turbo 3.0.1
# libjpeg-turbo 3.2.0
- type: file
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
sha256: d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
sha256: 1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
dest: external-packages/JPEG
# Freetype 2.12.1
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env sh
# Work-around https://gitlab.gnome.org/GNOME/gnome-build-meta/-/issues/754
grep -q org.freedesktop.Platform.GL.nvidia /.flatpak-info && export WEBKIT_DISABLE_DMABUF_RENDERER=1
# WebKit's NVIDIA DMABUF workaround (gnome-build-meta#754) is applied by the
# binary itself, gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
# Work-around https://github.com/bambulab/BambuStudio/issues/3440
# Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues
+251 -4
View File
@@ -12,6 +12,7 @@ commands:
trim delete profile files no <vendor>.json list references
update-index regenerate the *_list sections of <vendor>.json
legacy-compat write the printers user presets can be migrated back to
gcode-diff print a readable markdown diff of changed custom G-code
options shared by several commands:
--vendor VENDOR act on one vendor bundle only; repeatable, empty means all
@@ -94,11 +95,14 @@ parent in the OFL map. filament_vendor / filament_type resolve the same way.
"""
import argparse
import difflib
import functools
import html
import json
import os
import posixpath
import re
import subprocess
import sys
import uuid
from collections import Counter, defaultdict
@@ -192,6 +196,7 @@ VECTOR_KEYS = {
PRINT_CONFIG_CPP = os.path.normpath(
os.path.join(SCRIPTS_DIR, "..", "src", "libslic3r", "PrintConfig.cpp"))
TAB_CPP = os.path.normpath(os.path.join(SCRIPTS_DIR, "..", "src", "slic3r", "GUI", "Tab.cpp"))
@functools.lru_cache(maxsize=None)
@@ -313,11 +318,11 @@ REMEDY_HINTS = {
}
def print_error(msg):
print(f"\033[91m[ERROR]\033[0m {msg}") # Red
def print_error(msg, file=None):
print(f"\033[91m[ERROR]\033[0m {msg}", file=file) # Red
def print_warning(msg):
print(f"\033[93m[WARNING]\033[0m {msg}") # Yellow
def print_warning(msg, file=None):
print(f"\033[93m[WARNING]\033[0m {msg}", file=file) # Yellow
def print_info(msg):
print(f"\033[94m[INFO]\033[0m {msg}") # Blue
@@ -3209,6 +3214,223 @@ def run_legacy_compat(base_dir, profiles_dir, output, check):
return 0
# ---------------------------------------------------------------------------
# gcode-diff
# ---------------------------------------------------------------------------
# UTF-8 bytes, kept well under GitHub's 65,536-character comment limit.
GCODE_DIFF_BUDGET = 60000
# The file names a section's summary shows before "and N more".
GCODE_DIFF_SUMMARY_FILES = 3
GCODE_DIFF_HEADER = (
"## Custom G-code changes\n"
"\n"
"GitHub shows custom G-code in a profile as one long line, so below is a line by line "
"diff of each changed G-code or other text setting.\n")
@functools.lru_cache(maxsize=None)
def _text_option_keys():
"""The string options PrintConfig.cpp defines as G-code or other multi-line text
(every *_gcode option, and every one it marks multiline, such as notes), plus the
ones Tab.cpp edits as code, such as filename_format."""
with open(PRINT_CONFIG_CPP, encoding="utf-8") as f:
source = f.read()
keys = set()
for body in re.split(r"\bdef\s*=\s*this->add(?:_nullable)?\(", source)[1:]:
match = re.match(r'"(\w+)",\s*coStrings?\b', body)
if match and (match.group(1).endswith("_gcode")
or re.search(r"def->multiline\s*=\s*true", body)):
keys.add(match.group(1))
if "machine_start_gcode" not in keys:
raise RuntimeError(f"{PRINT_CONFIG_CPP} no longer defines machine_start_gcode")
# Tab.cpp only adds keys, so a missing file is not an error.
try:
with open(TAB_CPP, encoding="utf-8") as f:
gui = f.read()
except OSError:
gui = ""
keys.update(re.findall(r'get_option\("(\w+)"\);(?:(?!get_option\().)*?\bis_code\s*=\s*true',
gui, re.DOTALL))
return frozenset(keys)
def _git(cwd, *args, stdin=None):
return subprocess.run(["git", "-C", cwd, *args], input=stdin, check=True,
capture_output=True).stdout
def _blobs_at(profiles_dir, specs):
"""{(ref, path): bytes, or None if not a file there} for (ref, path relative to
profiles_dir) pairs, all read by one git cat-file process."""
prefix = _git(profiles_dir, "rev-parse", "--show-prefix").decode("utf-8").strip()
out = _git(profiles_dir, "cat-file", "--batch",
stdin="".join(f"{ref}:{prefix}{rel}\n" for ref, rel in specs).encode("utf-8"))
blobs, pos = {}, 0
for spec in specs:
end = out.index(b"\n", pos)
header = out[pos:end]
pos = end + 1
# "<name> missing" and "<name> ambiguous" have no content; "<oid> <type> <size>" does.
if header.endswith((b" missing", b" ambiguous")):
blobs[spec] = None
continue
_oid, kind, size = header.split()
blobs[spec] = out[pos:pos + int(size)] if kind == b"blob" else None
pos += int(size) + 1
return blobs
def _parse_profile(raw, rel, where):
"""The JSON in raw, or None with a warning when it is absent or not JSON."""
if raw is None:
print_warning(f"{rel}: skipped, not found at {where}", file=sys.stderr)
return None
try:
return json.loads(raw.decode("utf-8-sig"))
except ValueError as e:
print_warning(f"{rel}: skipped, cannot read it at {where}: {e}", file=sys.stderr)
return None
def _changed_profiles(profiles_dir, base, head):
"""(old path, new path) of each JSON file under profiles_dir that differs between
base and head (the working tree, untracked files included, when None); the absent
side of an added or deleted file is None, and so is a side that is not JSON. Renames
are paired."""
out = _git(profiles_dir, "diff", "--name-status", "-z", "-M", "--relative", base,
*([head] if head else []), "--", ".")
fields = out.decode("utf-8").split("\0")
pairs, i = [], 0
while i + 1 < len(fields):
status = fields[i]
old = new = fields[i + 1]
if status[0] in "RC":
new = fields[i + 2]
i += 3
else:
i += 2
old = None if status[0] == "A" or not old.endswith(".json") else old
new = None if status[0] == "D" or not new.endswith(".json") else new
if old or new:
pairs.append((old, new))
if head is None:
out = _git(profiles_dir, "ls-files", "-z", "--others", "--exclude-standard", "--", ".")
pairs += [(None, rel) for rel in out.decode("utf-8").split("\0")
if rel.endswith(".json") and rel.split("/")[0] != USER_DIR]
return pairs
def _text_lines(value):
return [line.rstrip() for line in value.split("\n")] if value else []
def _text_changes(old, new):
"""(setting, unified diff) for every G-code or other text value that differs
between two profiles; a vector option with several values gives one per value."""
changes = []
for key in list(new) + [k for k in old if k not in new]:
a, b = _as_list(old.get(key, [])), _as_list(new.get(key, []))
if a == b or not all(isinstance(v, str) for v in a + b):
continue
if key not in _text_option_keys() and not any("\n" in v for v in a + b):
continue
width = max(len(a), len(b))
for i in range(width):
diff = list(difflib.unified_diff(
_text_lines(a[i] if i < len(a) else ""), _text_lines(b[i] if i < len(b) else ""),
lineterm=""))[2:] # drops the ---/+++ file header
if diff:
changes.append((key if width == 1 else f"{key}[{i}]", "\n".join(diff)))
return changes
def _gcode_diff_section(files, setting, diff):
# A fence longer than any backtick run in the G-code cannot be closed by it.
fence = "`" * max([3] + [len(run) + 1 for run in re.findall(r"`+", diff)])
# In <code>, the "@BBL" of a filament file name does not mention a GitHub user.
def code(text):
return f"<code>{html.escape(text, quote=False)}</code>"
names, listing = ", ".join(map(code, files)), ""
if len(files) > GCODE_DIFF_SUMMARY_FILES + 1: # never "and 1 more"
listing = f"<p>{names}</p>\n\n" # an HTML block, so markdown in names stays literal
names = (", ".join(map(code, files[:GCODE_DIFF_SUMMARY_FILES]))
+ f" and {len(files) - GCODE_DIFF_SUMMARY_FILES} more")
return (f"\n<details>\n<summary><b>{names} - {code(setting)} (click to expand)</b>"
f"</summary>\n\n{listing}{fence}diff\n{diff}\n{fence}\n\n</details>\n")
def gcode_diff(profiles_dir, base, head=None, vendors=None):
"""Markdown with one collapsed diff per changed G-code setting between base and
head, or "" when none changed. Files whose diff of a setting is identical share
one section. Cut to whole sections under GCODE_DIFF_BUDGET."""
pairs = [(old, new) for old, new in _changed_profiles(profiles_dir, base, head)
if not vendors or any(p and p.split("/")[0] in vendors for p in (old, new))]
specs = ([(base, old) for old, _ in pairs if old]
+ [(head, new) for _, new in pairs if new and head])
blobs = _blobs_at(profiles_dir, specs) if specs else {}
def load(ref, rel):
if rel is None:
return {}
if ref:
return _parse_profile(blobs[(ref, rel)], rel, ref)
try:
return _parse_profile(Path(profiles_dir, rel).read_bytes(), rel, "the working tree")
except OSError as e:
print_warning(f"{rel}: skipped, cannot read it: {e}", file=sys.stderr)
return None
groups = {} # (setting, diff) -> files, in the order git lists them
for old_rel, new_rel in pairs:
old, new = load(base, old_rel), load(head, new_rel)
if not isinstance(old, dict) or not isinstance(new, dict):
continue
for setting, diff in _text_changes(old, new):
groups.setdefault((setting, diff), []).append(new_rel or old_rel)
if not groups:
return ""
sections = [_gcode_diff_section(files, setting, diff)
for (setting, diff), files in groups.items()]
budget = GCODE_DIFF_BUDGET - len(GCODE_DIFF_HEADER.encode("utf-8"))
shown = 0
for section in sections:
budget -= len(section.encode("utf-8"))
if budget < 0:
break
shown += 1
note, hidden = "", len(sections) - shown
if hidden:
note = (f"\n{hidden} of {len(sections)} sections {'is' if hidden == 1 else 'are'} not "
"shown because the comment would be too long for GitHub. See the Files "
"changed tab for the rest.\n")
return GCODE_DIFF_HEADER + "".join(sections[:shown]) + note
def _default_gcode_diff_base(profiles_dir, head):
for main in ("upstream/main", "origin/main"):
try:
return _git(profiles_dir, "merge-base", head or "HEAD", main).decode().strip()
except subprocess.CalledProcessError:
continue
raise RuntimeError("no --base given, and neither upstream/main nor origin/main exists")
def run_gcode_diff(profiles_dir, vendors, base, head):
try:
markdown = gcode_diff(profiles_dir, base or _default_gcode_diff_base(profiles_dir, head),
head, vendors)
# UnicodeDecodeError means git listed a file name that is not UTF-8.
except (OSError, RuntimeError, UnicodeDecodeError, subprocess.CalledProcessError) as e:
stderr = getattr(e, "stderr", None)
print_error(stderr.decode(errors="replace").strip() if stderr else str(e), file=sys.stderr)
return 1
sys.stdout.write(markdown)
return 0
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
@@ -3233,6 +3455,9 @@ examples:
orca_profile_tool.py legacy-compat --base /tmp/v2.4.2/resources/profiles
rewrite resources/info/legacy_compatible_printers.json against 2.4.2's
profiles (git archive v2.4.2 resources/profiles | tar -x -C /tmp/v2.4.2)
orca_profile_tool.py gcode-diff > gcode.md
the changed custom G-code of the working tree since it left upstream/main,
as markdown with one collapsed diff per setting
after adding, renaming or deleting profile files, run in this order:
normalize -> update-index -> generate-id -> check
@@ -3387,6 +3612,25 @@ def build_parser():
legacy_cmd.add_argument("--check", action="store_true",
help="fail if the map differs from what --base implies; write nothing")
gcode_cmd = add(
"gcode-diff", [vendor_opt, profiles_opt],
"print a readable markdown diff of changed custom G-code",
"Print, as markdown, a unified diff of every G-code or other multi-line\n"
"setting that differs between --base and --head in each changed profile\n"
"file, one collapsed section per setting. Files whose diff of a setting is\n"
"identical share a section. Trailing spaces are removed from every line, and\n"
"the output is cut to whole sections to fit a GitHub comment. Prints nothing\n"
"when no such setting changed. CI posts it on profile PRs.\n"
"\n"
"The settings are the *_gcode and multiline string options PrintConfig.cpp\n"
"defines, the options Tab.cpp edits as code (such as filename_format), and any\n"
"string value that spans several lines.")
gcode_cmd.add_argument("--base", metavar="REF",
help="compare against this git ref (default: the merge base "
"with upstream/main, else origin/main)")
gcode_cmd.add_argument("--head", metavar="REF",
help="compare this git ref (default: the working tree)")
return parser
@@ -3423,6 +3667,9 @@ def main(argv=None):
if args.command == "legacy-compat":
return run_legacy_compat(args.base, profiles_dir, args.output, args.check)
if args.command == "gcode-diff":
return run_gcode_diff(profiles_dir, vendors, args.base, args.head)
if args.command == "fix-variant":
_changed, errors = fix_variant_arrays(profiles_dir, vendors, dry_run=args.dry_run,
strict=args.strict)
+309 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Tests for the tree-maintenance half of scripts/orca_profile_tool.py: the
normalize, trim, update-index, fix-variant and check commands, and the subcommand
dispatch that reaches them (stdlib unittest, no external deps).
normalize, trim, update-index, fix-variant, check and gcode-diff commands, and the
subcommand dispatch that reaches them (stdlib unittest, no external deps).
The id halves are covered by test_filament_id.py and test_setting_id.py.
@@ -14,9 +14,12 @@ import json
import os
import re
import shutil
import stat
import subprocess
import sys
import tempfile
import unittest
from unittest import mock
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
@@ -41,7 +44,24 @@ class Tree:
os.makedirs(self.profiles)
def cleanup(self):
shutil.rmtree(self.dir, ignore_errors=True)
root = os.path.abspath(self.dir)
def writable_retry(func, path, _exc):
# Windows refuses to delete read-only files such as git's objects, and POSIX
# refuses to delete the entries of a read-only directory.
if not os.path.lexists(path): # already removed, as on a second cleanup
return
parent = os.path.dirname(os.path.abspath(path))
try:
if os.path.commonpath([parent, root]) == root: # never the temp dir itself
os.chmod(parent, stat.S_IRWXU)
if not os.path.islink(path): # chmod would follow it out of the tree
os.chmod(path, stat.S_IRWXU)
func(path)
except (OSError, ValueError): # commonpath raises ValueError across drives
pass
hook = "onexc" if sys.version_info >= (3, 12) else "onerror"
shutil.rmtree(self.dir, **{hook: writable_retry})
def index_path(self, vendor):
return os.path.join(self.profiles, vendor + ".json")
@@ -1409,6 +1429,292 @@ class TestDispatch(TreeCase):
self.assertEqual(self.t.read("V", "filament/A.json")["type"], "filament")
# ---------------------------------------------------------------------------
# gcode-diff
# ---------------------------------------------------------------------------
@unittest.skipUnless(shutil.which("git"), "git not installed")
class TestGcodeDiff(TreeCase):
START = "G28\nM104 S200\nG1 Z5"
def git(self, *args):
subprocess.run(["git", "-C", self.t.dir, "-c", "user.name=t", "-c", "user.email=t@t",
"-c", "commit.gpgsign=false", *args], check=True, capture_output=True)
def printer(self, name, **extra):
self.t.write("V", f"machine/{name}.json", {"type": "machine", "name": name, **extra})
def commit(self):
self.git("add", "-A")
self.git("commit", "-q", "--allow-empty", "-m", "x")
@classmethod
def setUpClass(cls):
# git's list of the variables that tie a process to a repository, asked with no
# GIT_* set since a stray GIT_DIR fails the query.
clean = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
cls.repo_env_vars = set(subprocess.run(
["git", "rev-parse", "--local-env-vars"], env=clean, check=True,
capture_output=True, text=True).stdout.split())
def setUp(self):
super().setUp()
# A git hook's GIT_DIR or GIT_INDEX_FILE would point every git call here, the
# tool's included, at the real repository instead of the temp tree.
env = {k: v for k, v in os.environ.items() if k not in self.repo_env_vars}
patcher = mock.patch.dict(os.environ, env, clear=True)
patcher.start()
self.addCleanup(patcher.stop)
self.git("init", "-q")
for name in ("A", "B", "C"):
self.printer(name, machine_start_gcode=self.START, nozzle_diameter=["0.4"])
self.printer("Gone", machine_end_gcode="M84")
self.commit()
def diff(self):
rc, out = self.run_command("gcode-diff", "--base", "HEAD")
self.assertEqual(rc, 0, out)
return out
def test_identical_edits_share_one_section(self):
for name in ("A", "B"):
self.printer(name, machine_start_gcode="G28\nM104 S210 \nG1 Z5")
self.printer("C", machine_start_gcode="G28\nM104 S220\nG1 Z5")
out = self.diff()
self.assertEqual(out.count("<details>"), 2)
self.assertIn("<code>V/machine/A.json</code>, <code>V/machine/B.json</code> - "
"<code>machine_start_gcode</code>", out)
self.assertIn("<code>V/machine/C.json</code> - <code>machine_start_gcode</code>", out)
# Trailing spaces are removed.
self.assertIn("\n-M104 S200\n+M104 S210\n", out)
self.assertIn("\n\n```diff\n@@ ", out)
self.assertIn("\n```\n\n</details>", out)
def test_nothing_is_printed_without_a_text_change(self):
self.printer("A", machine_start_gcode=self.START, nozzle_diameter=["0.6"])
self.assertEqual(self.diff(), "")
def test_added_and_deleted_files_show_the_whole_value(self):
# An untracked file counts as added; the validator's user dir is not a profile.
self.printer("New", machine_start_gcode="G28\nG29")
self.t.write("user", "default/machine/U.json", {"machine_start_gcode": "G28"})
os.remove(self.t.path("V", "machine/Gone.json"))
out = self.diff()
self.assertIn("@@ -0,0 +1,2 @@\n+G28\n+G29\n", out)
self.assertIn("<code>V/machine/Gone.json</code> - <code>machine_end_gcode</code>", out)
self.assertIn("@@ -1 +0,0 @@\n-M84\n", out)
self.assertNotIn("U.json", out)
def test_a_multi_line_value_counts_whatever_its_key(self):
self.printer("A", machine_start_gcode=self.START, custom_note="one\ntwo")
self.assertIn("<code>V/machine/A.json</code> - <code>custom_note</code>", self.diff())
def test_a_file_name_cannot_mention_a_user(self):
self.t.write("V", "filament/PLA @BBL A1.json", {"filament_start_gcode": ["M106"]})
self.assertIn("<code>V/filament/PLA @BBL A1.json</code>", self.diff())
def test_head_compares_two_commits(self):
self.printer("A", machine_start_gcode="G28")
self.commit()
rc, out = self.run_command("gcode-diff", "--base", "HEAD~1", "--head", "HEAD")
self.assertEqual(rc, 0, out)
self.assertIn("<code>V/machine/A.json</code> - <code>machine_start_gcode</code>", out)
def test_a_renamed_file_diffs_against_its_old_self(self):
os.rename(self.t.path("V", "machine/A.json"), self.t.path("V", "machine/A2.json"))
self.printer("A2", machine_start_gcode="G28\nM104 S210\nG1 Z5", nozzle_diameter=["0.4"])
self.commit()
rc, out = self.run_command("gcode-diff", "--base", "HEAD~1", "--head", "HEAD")
self.assertEqual(rc, 0, out)
self.assertIn("<code>V/machine/A2.json</code> - <code>machine_start_gcode</code>", out)
self.assertIn("\n G28\n-M104 S200\n+M104 S210\n G1 Z5\n", out)
def test_a_file_renamed_off_json_loses_its_values(self):
os.rename(self.t.path("V", "machine/Gone.json"), self.t.path("V", "machine/Gone.json.bak"))
self.commit()
rc, out = self.run_command("gcode-diff", "--base", "HEAD~1", "--head", "HEAD")
self.assertEqual(rc, 0, out)
self.assertIn("<code>V/machine/Gone.json</code> - <code>machine_end_gcode</code>", out)
self.assertIn("@@ -1 +0,0 @@\n-M84\n", out)
def test_a_file_renamed_onto_json_is_new(self):
with open(self.t.path("V", "machine/Draft.txt"), "w", encoding="utf-8") as f:
json.dump({"type": "machine", "machine_start_gcode": self.START}, f)
self.commit()
os.rename(self.t.path("V", "machine/Draft.txt"), self.t.path("V", "machine/Draft.json"))
self.commit()
rc, out = self.run_command("gcode-diff", "--base", "HEAD~1", "--head", "HEAD")
self.assertEqual(rc, 0, out)
self.assertIn("@@ -0,0 +1,3 @@\n+G28\n+M104 S200\n+G1 Z5\n", out)
def test_a_batch_read_survives_paths_that_are_not_files(self):
blobs = apt._blobs_at(self.t.profiles, [("HEAD", "V/machine"), ("HEAD", "V/no such.json"),
("HEAD", "V/machine/Gone.json")])
self.assertIsNone(blobs[("HEAD", "V/machine")])
self.assertIsNone(blobs[("HEAD", "V/no such.json")])
self.assertEqual(json.loads(blobs[("HEAD", "V/machine/Gone.json")])["machine_end_gcode"],
"M84")
def test_each_value_of_a_vector_option_is_its_own_setting(self):
self.t.write("V", "filament/F.json", {"filament_start_gcode": ["M106", "M107"]})
self.commit()
self.t.write("V", "filament/F.json", {"filament_start_gcode": ["M106", "M106 S0"]})
out = self.diff()
self.assertEqual(out.count("<details>"), 1)
self.assertIn("<code>filament_start_gcode[1]</code>", out)
def test_vendor_limits_the_files(self):
self.printer("A", machine_start_gcode="G28")
self.t.write("W", "machine/X.json", {"machine_start_gcode": "G28"})
rc, out = self.run_command("gcode-diff", "--base", "HEAD", "--vendor", "V")
self.assertEqual(rc, 0, out)
self.assertIn("V/machine/A.json", out)
self.assertNotIn("W/machine/X.json", out)
def test_vendor_includes_files_moved_out_of_it(self):
self.t.add_vendor("W")
os.rename(self.t.path("V", "machine/A.json"), self.t.path("W", "machine/A.json"))
data = self.t.read("W", "machine/A.json")
self.t.write("W", "machine/A.json", {**data, "machine_start_gcode": "G28\nM104 S210\nG1 Z5"})
self.commit()
rc, out = self.run_command("gcode-diff", "--base", "HEAD~1", "--head", "HEAD",
"--vendor", "V")
self.assertEqual(rc, 0, out)
self.assertIn("<code>W/machine/A.json</code> - <code>machine_start_gcode</code>", out)
def same_edit(self, names):
"""The diff after one start G-code edit to each of names; returns (summary, output)."""
for name in names:
self.printer(name, machine_start_gcode=self.START, nozzle_diameter=["0.4"])
self.commit()
for name in names:
self.printer(name, machine_start_gcode="G28")
out = self.diff()
return re.search(r"<summary>(.*)</summary>", out).group(1), out
def test_a_long_file_list_moves_into_the_section(self):
summary, out = self.same_edit("ABCDE")
self.assertIn("<code>V/machine/C.json</code> and 2 more - ", summary)
self.assertNotIn("D.json", summary)
self.assertIn("<code>V/machine/D.json</code>, <code>V/machine/E.json</code></p>\n\n```diff",
out)
def test_four_files_are_all_named_in_the_summary(self):
summary, _out = self.same_edit("ABCD")
self.assertIn("<code>V/machine/D.json</code> - ", summary)
self.assertNotIn("more", summary)
def test_an_error_goes_to_stderr_only(self):
err = io.StringIO()
with contextlib.redirect_stderr(err):
rc, out = self.run_command("gcode-diff", "--base", "no-such-ref")
self.assertEqual(rc, 1)
self.assertEqual(out, "")
self.assertIn("no-such-ref", err.getvalue())
def test_a_file_name_that_is_not_utf8_is_an_error(self):
bad = UnicodeDecodeError("utf-8", b"\xe9.json", 0, 1, "invalid continuation byte")
err = io.StringIO()
with mock.patch.object(apt, "_changed_profiles", side_effect=bad), \
contextlib.redirect_stderr(err):
rc, out = self.run_command("gcode-diff", "--base", "HEAD")
self.assertEqual((rc, out), (1, ""))
self.assertIn("invalid continuation byte", err.getvalue())
def test_output_is_cut_to_whole_sections(self):
for name in ("A", "B", "C"):
self.printer(name, machine_start_gcode=name * 300)
size = len(apt.gcode_diff(self.t.profiles, "HEAD").encode("utf-8"))
with mock.patch.object(apt, "GCODE_DIFF_BUDGET", size - 1):
out = self.diff()
self.assertEqual(out.count("<details>"), 2)
self.assertEqual(out.count("</details>"), 2)
self.assertIn("\n1 of 3 sections is not shown", out)
with mock.patch.object(apt, "GCODE_DIFF_BUDGET", 1):
self.assertIn("\n3 of 3 sections are not shown", self.diff())
def test_cleanup_removes_the_repo(self):
# setUp's commit leaves read-only git objects, and a read-only directory blocks
# deletion on POSIX.
os.chmod(self.t.path("V", "machine"), stat.S_IRUSR | stat.S_IXUSR)
self.t.cleanup()
self.assertFalse(os.path.exists(self.t.dir))
with mock.patch.object(os, "chmod") as chmod:
self.t.cleanup() # a second cleanup, as addCleanup runs, must not touch the temp dir
chmod.assert_not_called()
def test_only_repository_variables_are_stripped(self):
self.assertTrue({"GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"} <= self.repo_env_vars)
self.assertFalse({"GIT_SSH", "GIT_EXEC_PATH", "GIT_EDITOR"} & self.repo_env_vars)
def test_cleanup_leaves_symlink_targets_alone(self):
outside = tempfile.mkdtemp(prefix="profile_tool_outside_")
self.addCleanup(shutil.rmtree, outside, True)
target = os.path.join(outside, "f")
open(target, "w").close()
os.chmod(target, stat.S_IRUSR)
self.addCleanup(os.chmod, target, stat.S_IRWXU) # runs before the rmtree above
machine = self.t.path("V", "machine")
try:
os.symlink(target, os.path.join(machine, "link"))
except (OSError, NotImplementedError):
self.skipTest("cannot create symlinks here")
os.chmod(machine, stat.S_IRUSR | stat.S_IXUSR) # so removing the link fails at first
self.t.cleanup()
self.assertFalse(os.path.exists(self.t.dir))
self.assertEqual(stat.S_IMODE(os.stat(target).st_mode) & stat.S_IWUSR, 0)
def test_a_git_hook_environment_is_ignored(self):
scripts = os.path.join(REPO_ROOT, "scripts")
result = subprocess.run(
[sys.executable, "-m", "unittest",
"tests.test_profile_tool.TestGcodeDiff.test_head_compares_two_commits"],
cwd=scripts, capture_output=True, text=True,
env={**os.environ, "GIT_DIR": os.path.join(self.t.dir, "nowhere")})
self.assertEqual(result.returncode, 0, result.stderr)
def test_the_options_are_read_from_the_engine(self):
keys = apt._text_option_keys()
self.assertTrue({"machine_start_gcode", "filament_start_gcode", "printing_by_object_gcode",
"notes", "post_process"} <= keys)
self.assertNotIn("emit_machine_limits_to_gcode", keys) # a coBool
START_DEF = 'def = this->add("machine_start_gcode", coString);\n'
def keys_from(self, print_config, tab=None):
"""_text_option_keys read from these sources; no tab means no Tab.cpp."""
paths = [os.path.join(self.t.dir, "PrintConfig.cpp"), os.path.join(self.t.dir, "Tab.cpp")]
for path, text in zip(paths, (print_config, tab)):
if text is not None:
with open(path, "w", encoding="utf-8") as f:
f.write(text)
apt._text_option_keys.cache_clear()
self.addCleanup(apt._text_option_keys.cache_clear)
with mock.patch.object(apt, "PRINT_CONFIG_CPP", paths[0]), \
mock.patch.object(apt, "TAB_CPP", paths[1]):
return apt._text_option_keys()
def test_multiline_is_read_whatever_its_spacing(self):
keys = self.keys_from(self.START_DEF + 'def = this->add("printer_notes", coString);\n'
' def->multiline = true;\n')
self.assertIn("printer_notes", keys)
def test_the_code_options_of_tab_cpp_are_read(self):
keys = self.keys_from(self.START_DEF, 'option = optgroup->get_option("filename_format");\n'
'option.opt.is_code = true;\n'
'option = optgroup->get_option("layer_height");\n')
self.assertIn("filename_format", keys)
self.assertNotIn("layer_height", keys)
def test_a_missing_tab_cpp_is_not_an_error(self):
self.assertIn("machine_start_gcode", self.keys_from(self.START_DEF))
def test_markdown_in_a_file_name_stays_literal(self):
section = apt._gcode_diff_section(["V/a *b*.json", "V/c.json", "V/d.json", "V/e.json",
"V/f.json"], "machine_start_gcode", "@@ -1 +1 @@")
self.assertIn("\n<p><code>V/a *b*.json</code>, ", section)
# ---------------------------------------------------------------------------
# the real tree
# ---------------------------------------------------------------------------
+37 -15
View File
@@ -163,6 +163,7 @@ using json = nlohmann::json;
#include <X11/Xlib.h>
#endif
#include <unistd.h>
#include "slic3r/GUI/Widgets/WebView.hpp"
#endif
#ifdef SLIC3R_GUI
@@ -1125,6 +1126,19 @@ static void load_downward_settings_list_from_config(std::string config_file, std
}
}
#ifdef __WXGTK__
// Virtual GPUs (VirtualBox, VMware, QEMU) cannot back WebKit's hardware DMABUF renderer.
static bool is_hypervisor_guest()
{
std::ifstream cpuinfo("/proc/cpuinfo");
std::string line;
while (std::getline(cpuinfo, line))
if (boost::starts_with(line, "flags"))
return line.find(" hypervisor") != std::string::npos;
return false;
}
#endif
int CLI::run(int argc, char **argv)
{
// Mark the main thread for the debugger and for runtime checks.
@@ -1156,8 +1170,10 @@ int CLI::run(int argc, char **argv)
// opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it
// worked around appears fixed in WebKit2GTK >= 2.42; leaving it
// unset preserves WebKit hardware acceleration on Device / Setup
// Wizard / login / store. The default path still applies it on
// XWayland sessions as a conservative fallback for older WebKit.
// Wizard / login / store. The default path applies it only where it
// 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");
@@ -1165,6 +1181,17 @@ int CLI::run(int argc, char **argv)
// honours the first backend in the comma-separated list).
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 (is_hypervisor_guest() && WebView::SupportsForceSharedMemory())
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /* replace */ false);
if (x11_opt_in) {
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
@@ -1204,23 +1231,18 @@ int CLI::run(int argc, char **argv)
if (wayland_env && *wayland_env) {
BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend.";
::setenv("GDK_BACKEND", "x11", true);
// GTK will use X11 in a Wayland session here, which is the
// one default-path case that really runs through XWayland.
// WebKit2GTK accelerated compositing could fail silently
// there on releases before 2.42, so force it off. Disabling
// it also turns off threaded scrolling, hence the gate.
if (WebView::NeedsCompositingWorkaround())
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
}
}
#endif
// WebKit2GTK compositing can fail under XWayland on older
// WebKit releases. Disable it only when both DISPLAY and
// WAYLAND_DISPLAY are set (i.e. an XWayland session is in
// use). On pure X11 or native Wayland, compositing is left
// enabled so WebKit retains HW acceleration.
{
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);
}
}
// XInitThreads is needed before GStreamer may use Xlib. On
// native Wayland without DISPLAY, GStreamer uses waylandsink
// (no Xlib involved), so the call is skipped.
@@ -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 MESA_LOADER_DRIVER_OVERRIDE=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
+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.
* 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.
*/
@@ -886,12 +887,7 @@ WallToolPaths::ExtrusionLineSet WallToolPaths::getRegionOrder(const std::vector<
continue; // not directly adjacent
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
if (here->is_odd || nearby->is_odd) {
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) {
if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
order_requirements.emplace(std::make_pair(nearby, here));
} else {
assert((nearby->inset_idx > here->inset_idx) == outer_to_inner);
+37 -13
View File
@@ -841,8 +841,12 @@ public:
{
UNUSED(append);
std::istringstream iss(str);
iss >> this->value;
return !iss.fail();
double value;
iss >> value;
if (iss.fail())
return false;
this->value = value;
return true;
}
bool operator==(const ConfigOption &rhs) const override
@@ -1033,8 +1037,12 @@ public:
{
UNUSED(append);
std::istringstream iss(str);
iss >> this->value;
return !iss.fail();
int value;
iss >> value;
if (iss.fail())
return false;
this->value = value;
return true;
}
ConfigOptionInt& operator=(const ConfigOption *opt)
@@ -1252,8 +1260,12 @@ public:
UNUSED(append);
// don't try to parse the trailing % since it's optional
std::istringstream iss(str);
iss >> this->value;
return !iss.fail();
double value;
iss >> value;
if (iss.fail())
return false;
this->value = value;
return true;
}
private:
@@ -1365,10 +1377,14 @@ public:
bool deserialize(const std::string &str, bool append = false) override
{
UNUSED(append);
this->percent = str.find_first_of("%") != std::string::npos;
std::istringstream iss(str);
iss >> this->value;
return !iss.fail();
double value;
iss >> value;
if (iss.fail())
return false;
this->value = value;
this->percent = str.find_first_of("%") != std::string::npos;
return true;
}
private:
@@ -1538,8 +1554,12 @@ public:
{
UNUSED(append);
char dummy;
return sscanf(str.data(), " %lf , %lf %c", &this->value(0), &this->value(1), &dummy) == 2 ||
sscanf(str.data(), " %lf x %lf %c", &this->value(0), &this->value(1), &dummy) == 2;
Vec2d value;
if (sscanf(str.data(), " %lf , %lf %c", &value(0), &value(1), &dummy) != 2 &&
sscanf(str.data(), " %lf x %lf %c", &value(0), &value(1), &dummy) != 2)
return false;
this->value = value;
return true;
}
private:
@@ -1658,8 +1678,12 @@ public:
{
UNUSED(append);
char dummy;
return sscanf(str.data(), " %lf , %lf , %lf %c", &this->value(0), &this->value(1), &this->value(2), &dummy) == 3 ||
sscanf(str.data(), " %lf x %lf x %lf %c", &this->value(0), &this->value(1), &this->value(2), &dummy) == 3;
Vec3d value;
if (sscanf(str.data(), " %lf , %lf , %lf %c", &value(0), &value(1), &value(2), &dummy) != 3 &&
sscanf(str.data(), " %lf x %lf x %lf %c", &value(0), &value(1), &value(2), &dummy) != 3)
return false;
this->value = value;
return true;
}
private:
+37 -18
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 gcode;
unsigned int extruder_id = gcodegen.writer().filament()->id();
const auto& filament_idle_temp = gcodegen.config().idle_temperature;
if (filament_idle_temp.get_at(extruder_id) == 0) {
// There is no idle temperature defined in filament settings.
// Use the delta value from print config.
if (gcodegen.config().standby_temperature_delta.value != 0) {
// 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 {
// Use the value from filament settings. That one is absolute, not delta.
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
// 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;
if (filament_idle_temp.get_at(extruder_id) == 0) {
if (gcodegen.config().standby_temperature_delta.value == 0)
return std::string();
temp = this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value;
} else {
temp = filament_idle_temp.get_at(extruder_id);
}
}
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;
}
@@ -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_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)));
// 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
update_placeholder_parser_with_variant_params();
+1
View File
@@ -1004,6 +1004,7 @@ protected:
size_t num_objects,
size_t num_islands);
friend class OozePrevention;
friend class Wipe;
friend class WipeTowerIntegration;
friend class PressureEqualizer;
+47 -71
View File
@@ -120,7 +120,13 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags = {
"@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@",
"@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 = {
@@ -147,7 +153,13 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
"@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@",
"@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;
};
// 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) {
bool processed = false;
// Every inline placeholder contains '@', so a line without one has nothing to replace.
if (gcode_line.find('@') == std::string::npos)
return processed;
const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder);
const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder);
const std::string& print_time_hour_placeholder = reserved_tag(ETags::Print_Time_Hour_Placeholder);
const std::string& print_time_minute_placeholder = reserved_tag(ETags::Print_Time_Minute_Placeholder);
const std::string& print_time_sec_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder);
const std::string& used_filament_placeholder = reserved_tag(ETags::Used_Filament_Length_Placeholder);
auto replace_tag = [&gcode_line, &processed](ETags tag, const std::string& value) {
const std::string& placeholder = reserved_tag(tag);
size_t pos = gcode_line.find(placeholder);
while (pos != std::string::npos) {
gcode_line.replace(pos, placeholder.length(), value);
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;
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_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));
double total_filament_mm = 0.0;
for (const auto& mm : filament_mm) {
total_filament_mm += mm;
}
// 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;
for (const auto& mm : filament_mm) {
total_filament_mm += mm;
}
double used_filament_length = total_filament_mm / 1000.0; // Convert mm to m
char buf[64];
sprintf(buf, "%.2f", used_filament_length);
gcode_line.replace(pos, used_filament_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(used_filament_placeholder, pos + strlen(buf));
}
replace_tag(ETags::Print_Time_Total_Sec_Placeholder, Slic3r::float_to_string_decimal_point(print_time_total_sec, 2));
replace_tag(ETags::Print_Time_Day_Placeholder, std::to_string(print_time_day));
replace_tag(ETags::Print_Time_Hour_Placeholder, std::to_string(print_time_hour));
replace_tag(ETags::Print_Time_Minute_Placeholder, std::to_string(print_time_minute));
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;
};
+6
View File
@@ -601,6 +601,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
Print_Time_Minute_Placeholder,
Print_Time_Sec_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)]; }
+14 -1
View File
@@ -1276,6 +1276,13 @@ void ToolOrdering::collect_extruder_statistics(bool prime_multi_material)
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()) {
// 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.
@@ -3664,5 +3671,11 @@ int WipingExtrusions::get_support_interface_extruder_overrides(const PrintObject
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
+3
View File
@@ -370,6 +370,7 @@ public:
std::vector<int> & initial_filaments);
bool has_non_support_filament(const PrintConfig &config);
bool is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const;
private:
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
// 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;
// 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;
Print* m_print;
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);
}
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.
// 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);
// 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 {
const ReadBuf &rbuf_ref;
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.
unsigned short depth;
// 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;
// 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).
std::vector<PerimeterGeneratorLoop> children;
@@ -572,7 +577,8 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
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.
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());
}
else {
@@ -1690,13 +1696,24 @@ void PerimeterGenerator::process_classic()
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()) {
const float room = float(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
const Polygons reach = to_polygons(offset2_ex(last, -room, room + float(SCALED_EPSILON)));
for (PerimeterGeneratorLoop &hole : holes[0])
hole.is_thin_wall_hole = intersection_pl(Polylines{ hole.polygon.split_at_first_point() },
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole.polygon).inflated(SCALED_EPSILON))).empty();
auto reach = [&last](double room) {
return to_polygons(offset2_ex(last, -float(room), float(room) + float(SCALED_EPSILON)));
};
auto out_of_reach = [](const Polygon &hole, const Polygons &reach) {
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
@@ -1835,18 +1852,16 @@ void PerimeterGenerator::process_classic()
// if brim will be printed, reverse the order of perimeters so that
// 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;
if (is_outer_wall_first ||
//BBS: always print outer wall first when there indeed has brim.
// btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact).
(this->layer_id == 0 &&
(this->object_config->brim_type == BrimType::btOuterOnly ||
this->object_config->brim_type == BrimType::btLeadingEdgeOnly) &&
this->object_config->brim_width.value > 0))
// Orca: a contour with only a narrow wall hole beside it has nothing to sandwich, and prints the hole first,
// then the contour, then the gap fill between them, as Arachne does.
const PerimeterGeneratorLoops &island_loops = contours.front();
const bool narrow_wall_hole_island = island_loops.size() == 1 && island_loops.front().children.size() == 1 &&
island_loops.front().children.front().is_narrow_wall_hole;
if (is_outer_wall_first || this->outer_walls_first_for_brim())
entities.reverse();
// 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
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.
@@ -2412,11 +2427,25 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
// Function to reorder perimeters starting from a given reference index
auto reorderFromReference = [&](int referenceIndex) {
std::unordered_set<int> firstLevelIndices;
firstLevelIndices.insert(referenceIndex);
// Find first level touching perimeters
std::vector<int> firstLevelTouchingIndices = findAllTouchingPerimeters(entities, firstLevelIndices, threshold_external, threshold_internal, 1);
// Find first level touching perimeters. The odd centre line of a wall too narrow for two inner walls is shared
// 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};
std::vector<int> firstLevelTouchingIndices;
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
// The longest first neighbour is most likely the dominant proximate perimeter
// 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);
}
// Insert first level perimeters into reordered list
reordered.push_back(entities[referenceIndex]);
includedIndices.insert(referenceIndex);
for (int idx : outerIndices) {
reordered.push_back(entities[idx]);
includedIndices.insert(idx);
}
for (int idx : firstLevelTouchingIndices) {
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
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) {
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;
}
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)
{
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; }
// 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;
// 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:
std::vector<Polygons> generate_lower_polygons_series(float width);
@@ -183,6 +185,10 @@ private:
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
+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;
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.apply(std::move(config));
Preset::normalize(preset->config);
+8 -1
View File
@@ -756,7 +756,14 @@ public:
Preset& get_edited_preset() { return m_edited_preset; }
const Preset& get_edited_preset() const { return m_edited_preset; }
const Preset& get_selected_preset_base() const { return *get_preset_base(m_presets[m_idx_selected]); }
// The root of the selected preset's inherits chain, or the selected preset itself when
// the chain names a preset that is not loaded (e.g. a vendor that is not installed).
const Preset& get_selected_preset_base() const
{
const Preset &selected = m_presets[m_idx_selected];
const Preset *base = get_preset_base(selected);
return base ? *base : selected;
}
// Return the last saved preset.
// const Preset& get_saved_preset() const { return m_saved_preset; }
+8 -9
View File
@@ -17,6 +17,7 @@
#include "I18N.hpp"
#include "Layer.hpp"
#include "MutablePolygon.hpp"
#include "PerimeterGenerator.hpp"
#include "PrintBase.hpp"
#include "PrintConfig.hpp"
#include "SLA/IndexedMesh.hpp"
@@ -1464,15 +1465,13 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "top_surface_speed") {
// Brim is printed below supports, support invalidates brim and skirt.
steps.emplace_back(posSupportMaterial);
if (opt_key == "brim_type") {
const auto* old_brim_type = old_config.option<ConfigOptionEnum<BrimType>>(opt_key);
const auto* new_brim_type = new_config.option<ConfigOptionEnum<BrimType>>(opt_key);
//BBS: When switch to manual brim, the object must have brim, then re-generate perimeter
//to make the wall order of first layer to be outer-first
// btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it
// takes part in the same first-layer wall order rule.
if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly ||
old_brim_type->value == btLeadingEdgeOnly || new_brim_type->value == btLeadingEdgeOnly)
if (opt_key == "brim_type" || opt_key == "brim_width") {
// Regenerate the walls when the brim starts or stops putting the first layer's outer walls first.
auto outer_walls_first = [](const ConfigOptionResolver &config) {
return brim_puts_outer_walls_first(config.option<ConfigOptionEnum<BrimType>>("brim_type")->value,
config.option<ConfigOptionFloat>("brim_width")->value);
};
if (outer_walls_first(old_config) != outer_walls_first(new_config))
steps.emplace_back(posPerimeters);
}
} else if (
+5 -8
View File
@@ -707,8 +707,10 @@ void tree_supports_generate_paths(
false);
}
if (eec) {
std::reverse(eec->entities.begin(), eec->entities.end());
dst.emplace_back(eec.release());
if (!eec->empty()) {
std::reverse(eec->entities.begin(), eec->entities.end());
dst.emplace_back(eec.release());
}
}
}
}
@@ -1825,12 +1827,7 @@ void generate_support_toolpaths(
no_sort = true;
} else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic &&
(config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) {
// Orca: A special case for the hollow Organic supports
// 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);
tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params);
done = true;
}
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;
coordf_t support_extrusion_width = m_support_params.support_extrusion_width;
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
// tilted, moving belt. So the first-object-layer adhesion features (the tree
// 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;
bool floor_interface_as_base = false;
bool has_circle_node = false;
bool need_extra_wall = false;
ExPolygons extra_wall_areas;
ExPolygons collision_sharp_tails;
ExPolygons collision_base;
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));
if (!area.empty()) has_circle_node = true;
if (node.need_extra_wall) need_extra_wall = true;
// merge overhang to get a smoother interface surface
// 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)
append(roof_gap_areas, area);
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;
area_groups.emplace_back(&expoly, SupportLayer::BaseType, max_layers_above_base);
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) {
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++) {
std::vector<SupportNode *> &curr_layer_nodes = contact_nodes[layer_nr];
if (curr_layer_nodes.empty()) continue;
@@ -3447,20 +3448,17 @@ void TreeSupport::smooth_nodes()
std::vector<double> radii;
std::vector<SupportNode *> branch;
SupportNode * p_node = node;
float total_height = 0;
// 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
if (node->child && node->child->type!=ePolygon) {
pts.push_back(p_node->child->position);
radii.push_back(p_node->child->radius);
branch.push_back(p_node->child);
total_height += p_node->child->height;
}
do {
pts.push_back(p_node->position);
radii.push_back(p_node->radius);
branch.push_back(p_node);
total_height += p_node->height;
p_node = p_node->parent;
} while (p_node && !p_node->is_processed);
if (pts.size() < 3) continue;
@@ -3479,9 +3477,6 @@ void TreeSupport::smooth_nodes()
branch[i]->radius = radii1[i];
branch[i]->movement = (pts[i + 1] - pts[i - 1]) / 2;
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;
}
}
@@ -3489,13 +3484,6 @@ void TreeSupport::smooth_nodes()
std::swap(pts, pts1);
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;
bool is_corner = false;
bool is_processed = false;
bool need_extra_wall = false;
bool is_sharp_tail = false;
bool valid = true;
ExPolygon overhang; // when type==ePolygon, set this value to get original overhang area
+36 -103
View File
@@ -1,7 +1,6 @@
#include "TextureDisplacement.hpp"
#include <Eigen/Core>
#include <Eigen/SVD>
#include <Eigen/Geometry>
#include <algorithm>
#include <array>
@@ -284,7 +283,7 @@ bool height_texture_has_color(const TextureDisplacementLayer &layer)
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;
if (layer.empty())
@@ -352,13 +351,15 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
}
}
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
// 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.
auto &entries = g_decoded_texture_cache.entries;
for (auto it = entries.begin(); it != entries.end();)
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 };
if (keep_decoded) {
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
// 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.
auto &entries = g_decoded_texture_cache.entries;
for (auto it = entries.begin(); it != entries.end();)
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 };
}
}
// The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before,
@@ -723,10 +724,7 @@ V2Resolution recommend_v2_resolution(const indexed_triangle_set
const DecodedHeightTexture &tex = decode_height_texture(layer);
if (tex.width <= 0)
continue;
// Texels are square in both axes (the aspect correction is what keeps them so), so the finer
// of the two tile extents sets the finest texel the refinement has to resolve.
const Vec2f tile = layer_tile_mm(layer);
const float texel = std::min(tile.x(), tile.y()) / float(tex.width);
const float texel = layer.tiling_scale / float(tex.width);
if (out.texel_mm <= 0.f || texel < out.texel_mm) {
out.texel_mm = texel;
out.pixels_per_edge = analyze_texture_detail(layer).pixels_per_edge;
@@ -2168,7 +2166,7 @@ namespace {
// of them was most of the per-sample arithmetic.
struct UVTransform
{
float inv_u, inv_v, cs, sn, aspect;
float scale, cs, sn, aspect;
Vec2f offset;
// `normalized_uv` drops the two steps that only mean something for a coordinate measured on the
@@ -2176,18 +2174,7 @@ struct UVTransform
// rotation and offset. See apply_uv_transform(); TextureProjectionMethod::ModelUV is what needs it.
UVTransform(const TextureDisplacementLayer &layer, float aspect_, bool normalized_uv = false)
{
// The mirror folds into the reciprocal: it is a negation of the same coordinate, so there is
// nothing to do per sample beyond carrying the sign here. A normalized uv has no millimetre
// tiling to divide by, but a mirror is still a mirror, so the sign applies either way.
const Vec2f sign = layer_mirror_sign(layer);
if (normalized_uv) {
inv_u = sign.x();
inv_v = sign.y();
} else {
const Vec2f tile = layer_tile_mm(layer);
inv_u = sign.x() / tile.x();
inv_v = sign.y() / tile.y();
}
scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
cs = std::cos(rad);
sn = std::sin(rad);
@@ -2196,11 +2183,9 @@ struct UVTransform
}
Vec2f operator()(const Vec2f &planar) const
{
// See apply_uv_transform() for why both the tile extents and the aspect correction follow
// the rotation rather than preceding it.
Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
rotated.x() *= inv_u;
rotated.y() *= inv_v;
const Vec2f scaled = planar * scale;
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
// See apply_uv_transform() for why the aspect correction follows the rotation.
if (aspect > 0.f && aspect != 1.f)
rotated.y() *= aspect;
return rotated + offset;
@@ -2220,43 +2205,26 @@ inline Vec3f triplanar_weights(const Vec3f &normal)
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect, bool normalized_uv)
{
// A uv that is already normalized has no millimetre scale to divide by and no aspect to correct -
// see the header. UVTransform does the same, and the two must stay in step.
const float scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
const Vec2f scaled = planar * scale;
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
const float cs = std::cos(rad);
const float sn = std::sin(rad);
Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
// The tile extents. Dividing here rather than before the rotation is what makes a non-square
// tile a rotated *rectangle* on the surface: scaling the two axes of an already-rotated
// coordinate by different amounts would be a shear, so the rotation has to come first and the
// extents divide in the tile's own frame. With a square tile the two orders agree exactly,
// R being linear - which is why this is not a change in behaviour for any existing layer.
//
// The mirror rides along as a sign on the same division. It is in the tile's own frame too, so
// it mirrors along the tile's axes rather than the world's, and because the offset is what
// positions the tile, mirroring about the tile's middle is a matter of the offset the gizmo
// writes alongside it, not of anything here.
// A uv that is already normalized has no millimetre scale to divide by - see the header - but it
// can still be mirrored, so the sign is applied in both cases and only the extents are skipped.
// UVTransform does the same, and the two must stay in step.
const Vec2f sign = layer_mirror_sign(layer);
if (normalized_uv) {
rotated.x() *= sign.x();
rotated.y() *= sign.y();
} else {
const Vec2f tile = layer_tile_mm(layer);
rotated.x() *= sign.x() / tile.x();
rotated.y() *= sign.y() / tile.y();
}
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
// Non-square textures. Without this the [0,1] square of uv covers the whole image whatever its
// proportions, so a 2:1 image is squeezed into one tile and every feature in it comes out half
// as wide as it should be. The tile is `layer_tile_mm().y() * height / width` mm along v, which
// is exactly what keeps texels square - so dividing v by that extent is the same as multiplying
// it by width / height. A square texture has aspect 1 and is untouched, which is why this
// changes nothing for the shipped library.
// proportions, so a 2:1 image is squeezed into a square tile and every feature in it comes out
// half as wide as it should be. `tiling_scale` is the tile's size along u; the tile is
// `tiling_scale * height / width` mm along v, which is exactly what keeps texels square - so
// dividing v by that extent is the same as multiplying it by width / height. A square texture has
// aspect 1 and is untouched, which is why this changes nothing for the shipped library.
//
// Applied after the rotation, like the extents, and for the same reason: scaling one axis of an
// already-rotated coordinate before turning it would be a shear.
// Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a
// shear, and doing it the other way round would make "Rotation" skew the pattern instead of
// turning it.
if (!normalized_uv && aspect > 0.f && aspect != 1.f)
rotated.y() *= aspect;
@@ -3600,36 +3568,13 @@ void texture_displacement_patch_frame(const indexed_triangle_set &patch, const s
(an.y() <= an.x() && an.y() <= an.z()) ? Vec3f::UnitY() : Vec3f::UnitZ();
}
Matrix3d texture_displacement_frame_rotation(const Transform3d &volume_to_world)
{
// Separated out by polar decomposition rather than by assuming the linear part is a clean
// rotation times a scale: a sheared or mirrored placement is legal here. A reflection is left
// behind with the scale on purpose, so a mirrored instance still makes the bake frame's
// determinant negative and the bake still knows to flip the normals it derives.
const Matrix3d linear = volume_to_world.linear();
Eigen::JacobiSVD<Matrix3d> svd(linear, Eigen::ComputeFullU | Eigen::ComputeFullV);
Matrix3d rotation = svd.matrixU() * svd.matrixV().transpose();
if (rotation.determinant() < 0.0) {
Matrix3d u = svd.matrixU();
u.col(2) *= -1.0;
rotation = u * svd.matrixV().transpose();
}
return rotation.transpose(); // world -> texture frame
}
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world)
{
// The model's own orientation, world scale, and the origin moved to where the volume's origin
// sits. So the texture rides with the model however it is moved *and* however it is turned,
// while a tile is still `tiling_scale` printed millimetres whatever the instance's scale, and a
// single untiled stamp starts on the model rather than at the plate's corner.
//
// Orientation used to come from the world too, which meant turning a part on the plate slid the
// decoration across its surface. Turning a part is a print-time decision - clearance, supports,
// warping - and it should not be a design one.
Transform3d unrotate = Transform3d::Identity();
unrotate.linear() = texture_displacement_frame_rotation(volume_to_world);
return unrotate * Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world;
// World orientation and scale, but the origin moved to where the volume's own origin sits: the
// texture then rides with the model when it is moved about the plate (and a single, untiled stamp
// starts on the model rather than at the plate's corner), while a tile is still `tiling_scale`
// printed millimetres whatever the instance's scale.
return Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world;
}
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume)
@@ -5840,13 +5785,7 @@ std::string texture_displacement_layers_to_json(const std::vector<TextureDisplac
j["depth_mm"] = l.depth_mm;
j["tiling_scale"] = l.tiling_scale;
// The v extent and the mirroring. Written unconditionally, including the <= 0 "same as u"
// sentinel, so a project says plainly that the tile was square rather than leaving a reader
// to infer it from a missing key.
j["tiling_scale_v"] = l.tiling_scale_v;
j["mirror_u"] = l.mirror_u;
j["mirror_v"] = l.mirror_v;
j["rotation_deg"] = l.rotation_deg;
j["rotation_deg"] = l.rotation_deg;
j["offset"] = vec2_to_json(l.offset);
j["invert"] = l.invert;
j["midlevel"] = l.midlevel;
@@ -5954,12 +5893,6 @@ bool texture_displacement_layers_from_json(const std::string
read(j, "depth_mm", l.depth_mm);
read(j, "tiling_scale", l.tiling_scale);
// Absent in a project written before the tile could be rectangular or mirrored, and
// then each keeps its default: -1 for the v extent, which layer_tile_mm() reads as
// "square", and false for the mirrors. So an older project loads as what it was.
read(j, "tiling_scale_v", l.tiling_scale_v);
read(j, "mirror_u", l.mirror_u);
read(j, "mirror_v", l.mirror_v);
read(j, "rotation_deg", l.rotation_deg);
if (const auto o = j.find("offset"); o != j.end())
l.offset = vec2_from_json(*o, l.offset);
+11 -49
View File
@@ -225,17 +225,7 @@ struct TextureDisplacementLayer
std::shared_ptr<std::vector<unsigned char>> image_data;
float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm
float tiling_scale = 12.5f; // size of one texture tile along u, in mm
// Size of one tile along v, in mm. Anything <= 0 means "whatever tiling_scale is", which is both
// the default and what a layer created before the tile could be non-square meant - so an older
// undo snapshot still comes back as the square tile it was.
float tiling_scale_v = -1.f;
// Mirror the texture along one axis of the tile. Set by dragging a width or height handle back
// through the tile's middle, which is how a side handle behaves in an image editor: the extent
// cannot go negative, so the sign lives here instead. Both default to off, which is what every
// layer written before this meant.
bool mirror_u = false;
bool mirror_v = false;
float tiling_scale = 12.5f; // size of one texture tile, in mm
float rotation_deg = 0.f;
Vec2f offset = Vec2f::Zero();
bool invert = false;
@@ -372,8 +362,7 @@ struct TextureDisplacementLayer
static_cast<int>(tile_method), static_cast<int>(projection_method), lscm_seam_angle_deg, islands,
static_cast<int>(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right,
view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups,
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled, tiling_scale_v,
mirror_u, mirror_v);
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled);
}
template<class Archive> void load(Archive &ar)
{
@@ -385,7 +374,7 @@ struct TextureDisplacementLayer
tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel,
island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing,
edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
view_project_matrix, color_enabled, tiling_scale_v, mirror_u, mirror_v);
view_project_matrix, color_enabled);
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
tile_method = static_cast<TextureTileMethod>(tile_method_int);
projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
@@ -394,25 +383,6 @@ struct TextureDisplacementLayer
};
// The layer's tile extents in mm, u first. The v extent falls back to the u one whenever
// tiling_scale_v is unset, so "unset means square" is written down once and every consumer - the
// bake, the preview shaders, the UV pane and the placement gizmo - agrees on what a layer with no
// v extent means.
inline Vec2f layer_tile_mm(const TextureDisplacementLayer &layer)
{
const float su = (layer.tiling_scale > 1e-6f) ? layer.tiling_scale : 1.f;
return Vec2f(su, (layer.tiling_scale_v > 1e-6f) ? layer.tiling_scale_v : su);
}
// +1 or -1 per axis, for the layer's mirroring. A separate factor from the extents rather than a
// signed extent: the extents are a size the panel shows and the refinement measures texels against,
// and letting one of them go negative would mean every one of those places had to remember to take
// the magnitude.
inline Vec2f layer_mirror_sign(const TextureDisplacementLayer &layer)
{
return Vec2f(layer.mirror_u ? -1.f : 1.f, layer.mirror_v ? -1.f : 1.f);
}
// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next
// to texture_displacement_layers and consumed by build_texture_displacement().
struct TextureDisplacementOptions
@@ -629,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
// 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
// DecodedHeightTexture if image_data is empty or is not a PNG at all.
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
// DecodedHeightTexture if image_data is empty or is not a PNG at all. keep_decoded = false leaves the
// 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
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
@@ -698,19 +669,14 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
// projection method, without going through project_texture_displacement_uv()'s own dispatch
// (which only knows how to compute the *analytic* methods from a single vertex + normal).
// `aspect` is the height map's width / height. It scales the v axis so a non-square image is not
// squeezed into a square tile: the tile is `layer_tile_mm().y() * height / width` mm along v, which
// keeps texels square. 1 (the default) is the square case and leaves the coordinate exactly as it
// always was.
//
// The tile extents divide *after* the rotation, not before. With one extent that made no difference
// (R is linear, so R(p / s) == R(p) / s), but with two it is what keeps a non-square tile a rotated
// rectangle on the surface instead of a sheared parallelogram. The layer's mirroring is a sign on
// that same division.
// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is
// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the
// square case and leaves the coordinate exactly as it always was.
//
// `normalized_uv` says the coordinate is already a uv rather than a millimetre measurement on the
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tile extents (mm
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tiling scale (mm
// per tile is meaningless for it) nor the aspect correction (the artist's layout already accounts for
// the image's proportions) applies, and only the rotation, the mirroring and the offset are left.
// the image's proportions) applies, and only the rotation and offset are left.
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f,
bool normalized_uv = false);
@@ -1082,10 +1048,6 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume);
// The frame the bake and the previews project the texture in: `volume_to_world` with its translation
// removed, i.e. world orientation and scale about the volume's own origin. See build_texture_displacement().
// The rotation that carries a world direction into the texture's frame, i.e. the inverse of the
// placement's own rotation. Exposed because the preview shaders rebuild the frame themselves from a
// world position and have to turn it the same way the bake does, or the two disagree.
Matrix3d texture_displacement_frame_rotation(const Transform3d &volume_to_world);
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world);
// Area-weighted vertex normals of `its` - the directions the bake both projects and displaces along.
+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);
// 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
#if WIN32
+53 -8
View File
@@ -17,6 +17,7 @@
#if defined(_MSC_VER) || defined(__MINGW64__)
#include "boost/nowide/cstdio.hpp"
#include "boost/nowide/convert.hpp"
#endif
#include "I18N.hpp"
@@ -62,7 +63,11 @@ bool open_zip(mz_zip_archive *zip, const char *fname, bool isread)
FILE *f = nullptr;
#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)
f = fopen64(fname, mode);
#else
@@ -101,6 +106,14 @@ bool close_zip(mz_zip_archive *zip, bool isread)
}
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)
@@ -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_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)
{
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;
try {
// 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 io_dest = extended_path(dest_path);
if (stat.m_is_directory) {
const boost::filesystem::path dest_path(dest_file);
if (!boost::filesystem::exists(dest_path))
boost::filesystem::create_directories(dest_path);
if (!boost::filesystem::exists(io_dest))
boost::filesystem::create_directories(io_dest);
continue;
}
if (stat.m_uncomp_size == 0) {
@@ -167,10 +213,9 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
continue;
}
// 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(dest_path)))
boost::filesystem::remove(dest_path);
if (!mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0)) {
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(io_dest)))
boost::filesystem::remove(io_dest);
if (!extract_zip_entry_to_file(&archive, stat.m_file_index, dest_path)) {
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
close_zip_reader(&archive);
return false;
+5
View File
@@ -1,6 +1,7 @@
#ifndef MINIZ_EXTENSION_HPP
#define MINIZ_EXTENSION_HPP
#include <boost/filesystem/path.hpp>
#include <string>
#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);
// 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);
// 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 {
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;
}
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
+1 -1
View File
@@ -75,7 +75,7 @@ std::vector<float> Layers::get_zs() const
size_t Layers::get_layer_id_at(float z) const
{
auto iter = std::upper_bound(m_items.begin(), m_items.end(), z, [](float z, const Item& item) { return item.z < z; });
auto iter = std::upper_bound(m_items.begin(), m_items.end(), z, [](float z, const Item& item) { return item.z > z; });
return std::distance(m_items.begin(), iter);
}
+6
View File
@@ -222,6 +222,7 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmoUtils.hpp
GUI/GLCanvas3D.cpp
GUI/GLCanvas3D.hpp
GUI/SequentialPrintClearance.hpp
GUI/GLModel.cpp
GUI/GLModel.hpp
GUI/GLSelectionRectangle.cpp
@@ -689,6 +690,8 @@ set(SLIC3R_GUI_SOURCES
plugin/host/PluginHostUi.hpp
plugin/host/PluginPages.cpp
plugin/host/PluginPages.hpp
plugin/host/PluginResourceTextures.cpp
plugin/host/PluginResourceTextures.hpp
plugin/CloudPluginService.cpp
plugin/CloudPluginService.hpp
plugin/PluginFsUtils.cpp
@@ -712,6 +715,9 @@ set(SLIC3R_GUI_SOURCES
plugin/pluginTypes/pages/PagesPluginCapability.hpp
plugin/pluginTypes/pages/PagesPluginCapability.cpp
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.cpp
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp
+260 -72
View File
@@ -85,6 +85,96 @@ static std::string float_to_string_with_precision(float value, int precision = 3
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)
: DPIDialog(parent, id, _L("AMS Materials Setting"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
, 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_max.ToLong(&nozzle_temp_max_int);
wxColour color = m_clr_picker->m_colour;
char col_buf[10];
sprintf(col_buf, "%02X%02X%02X%02X", (int)color.Red(), (int)color.Green(), (int)color.Blue(), (int)color.Alpha());
std::string tray_color = colour_to_ams_string(color);
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()) {
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
<< ", setting_id = " << ams_setting_id
<< ", tray_type = " << m_filament_type
<< ", tray_color = " << col_buf
<< ", tray_color = " << tray_color
<< ", nozzle_temp_min = " << nozzle_temp_min_int
<< ", nozzle_temp_max = " << nozzle_temp_max_int;
// set filament
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
@@ -923,8 +1044,15 @@ void AMSMaterialsSetting::set_ctype(int ctype)
void AMSMaterialsSetting::on_picker_color(wxCommandEvent& event)
{
unsigned int color_num = event.GetInt();
set_color(wxColour(color_num>>24&0xFF, color_num>>16&0xFF, color_num>>8&0xFF, color_num&0xFF));
std::vector<wxColour> colors = m_color_picker_popup.get_selected_colours();
if (colors.empty()) {
unsigned int color_num = event.GetInt();
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)
@@ -939,14 +1067,12 @@ void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
}
}
std::vector<wxColour> ams_colors;
obj->GetFilaSystem()->CollectAmsColors(ams_colors);
m_color_picker_popup.set_ams_colours(collect_ams_color_items(obj->GetFilaSystem().get()));
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 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.set_ams_colours(ams_colors);
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour);
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;
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();
int center_x = (size.x - bmp_size.x) / 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.SetBrush(*wxTRANSPARENT_BRUSH);
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) {
@@ -1891,19 +2010,21 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
m_ams_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
//other
wxFlexGridSizer* fg_sizer;
fg_sizer = new wxFlexGridSizer(0, 8, 0, 0);
fg_sizer->SetFlexibleDirection(wxBOTH);
fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
m_other_fg_sizer = new wxFlexGridSizer(0, 8, 0, 0);
m_other_fg_sizer->SetFlexibleDirection(wxBOTH);
m_other_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
for (wxColour col : m_def_colors) {
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
cp->set_color(col);
cp->set_colors({col});
cp->ctype = 2;
cp->set_selected(false);
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
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) {
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_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(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_custom_cp, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(16));
m_sizer_box->Add(0, 0, 0, wxTOP, FromDIP(10));
@@ -1981,7 +2102,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
m_def_color_box->Fit();
m_sizer_main->Add(m_def_color_box, 0, wxALL | wxEXPAND, 10);
SetSizer(m_sizer_main);
SetSizerAndFit(m_sizer_main);
Layout();
Fit();
@@ -2019,13 +2140,15 @@ void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& /*event*/)
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) {
for (ColorPicker* col_pick:m_ams_color_pickers) {
std::vector<ColorPicker*>::iterator iter = 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);
}
@@ -2035,17 +2158,22 @@ void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
}
m_ams_colors = ams;
for (wxColour col : m_ams_colors) {
m_ams_color_items = ams;
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);
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->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
m_color_pickers.push_back(cp);
m_ams_color_pickers.push_back(cp);
m_ams_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
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);
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();
}
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;
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) {
if (cp->m_selected) {
@@ -2069,7 +2256,8 @@ void ColorPickerPopup::set_def_colour(wxColour col)
}
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);
break;
}
+19 -4
View File
@@ -82,14 +82,22 @@ public:
class ColorPickerPopup : public PopupWindow
{
public:
struct ColorItem
{
std::vector<wxColour> colors;
int ctype = 2;
wxString name;
};
ScalableBitmap m_ts_bitmap_custom;
wxStaticBitmap* m_ts_stbitmap_custom;
StaticBox* m_custom_cp;
StaticBox* m_def_color_box;
wxFlexGridSizer* m_ams_fg_sizer;
wxFlexGridSizer* m_other_fg_sizer;
wxColour m_def_col;
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_ams_color_pickers;
@@ -97,13 +105,20 @@ public:
ColorPickerPopup(wxWindow* parent);
~ColorPickerPopup() {};
void on_custom_clr_picker(wxMouseEvent& event);
void set_ams_colours(std::vector<wxColour> ams);
void set_def_colour(wxColour col);
void set_ams_colours(const std::vector<ColorItem>& ams);
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);
virtual void OnDismiss() 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
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()) {
invalidated = PrintBase::APPLY_STATUS_UNCHANGED;
}
+2 -2
View File
@@ -3151,7 +3151,7 @@ void CreatePrinterPresetDialog::set_current_visible_printer()
m_printer_name_to_preset.clear();
for (const Preset &printer_preset : printer_presets) {
if (!printer_preset.is_visible) continue;
if (preset_bundle->printers.get_preset_base(printer_preset)->name != printer_preset.name) continue;
if (preset_bundle->printers.get_preset_base(printer_preset) != &printer_preset) continue;
if (auto printer_model = dynamic_cast<ConfigOptionString*>(const_cast<Preset&>(printer_preset).config.option("printer_model", false))) {
if (m_printer_name_to_preset.find(printer_model->value) == m_printer_name_to_preset.end()) {
printer_choice.push_back(from_u8(printer_model->value));
@@ -4962,7 +4962,7 @@ void CreatePresetForPrinterDialog::get_visible_printer_and_compatible_filament_p
if (filament_preset.is_default || !filament_preset.is_compatible || filament_preset.is_project_embedded) continue;
ConfigOptionStrings *filament_types;
const Preset * filament_preset_base = m_preset_bundle->filaments.get_preset_base(filament_preset);
if (filament_preset_base == &filament_preset) {
if (filament_preset_base == &filament_preset || filament_preset_base == nullptr) {
filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(&filament_preset)->config.option("filament_type"));
} else {
filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(filament_preset_base)->config.option("filament_type"));
+7 -1
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()));
}
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_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_max"] = nozzle_temp_max;
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);
}
@@ -5478,6 +5483,7 @@ void MachineObject::parse_new_info(json print)
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_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)) {
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_check_track_switch_match_slice_printer{false};
bool is_support_pa_mode{false};
bool is_support_filament_manual_multi_color{false};
std::optional<bool> m_firmware_support_print_tpu_left;
bool installed_upgrade_kit{false};
@@ -804,7 +805,7 @@ public:
int command_ams_switch_filament(bool switch_filament);
int command_ams_air_print_detect(bool air_print_detect);
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_refresh_rfid(int ams_id, int slot_id);
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>>();
for (const auto& color_str : fila_color_strs) {
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:
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)
{
+15 -91
View File
@@ -65,6 +65,7 @@
#include <string_view>
#include "slic3r/GUI/Selection.hpp"
#include "GLCanvas3D.hpp"
#include "SequentialPrintClearance.hpp"
#ifdef SLIC3R_CAD
#include "slic3r/GUI/CAD/DesignSketchTool.hpp" // Design tab: interactive 2D sketch tool
#endif
@@ -6397,17 +6398,16 @@ void GLCanvas3D::update_sequential_clearance()
//BBS: add the height logic
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
Polygons polygons;
std::vector<std::pair<Polygon, float>> height_polygons;
polygons.reserve(instances_count);
height_polygons.reserve(instances_count);
std::vector<struct height_info> convex_and_bounding_boxes;
struct height_info
{
double instance_height;
BoundingBox bounding_box;
Polygon hull_polygon;
};
std::vector<SequentialClearanceInstance> convex_and_bounding_boxes;
convex_and_bounding_boxes.reserve(instances_count);
std::map<ObjectID, int> print_order;
for (const PrintObject* print_object : fff_print()->objects())
for (const PrintInstance& instance : print_object->instances())
print_order.emplace(instance.model_instance->id(), instance.model_instance->arrange_order);
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];
double rotation_z0 = instances.front()->get_rotation().z();
int index = 0;
@@ -6427,96 +6427,20 @@ void GLCanvas3D::update_sequential_clearance()
Polygon convex_hull(std::move(inst_pts));
BoundingBox bouding_box = convex_hull.bounding_box();
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++);
//skip the object for not current plate
if (!plate_bb.overlap(bouding_box))
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));
}
sort_sequential_clearance_instances(convex_and_bounding_boxes.begin() + first_instance, convex_and_bounding_boxes.end(), print_order);
}
//sort the print instance
std::sort(convex_and_bounding_boxes.begin(), convex_and_bounding_boxes.end(),
[](auto &l, auto &r) {
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));
}
const auto& config = fff_print()->config();
const auto height_polygons = sequential_clearance_height_polygons(convex_and_bounding_boxes, config.printable_height,
config.extruder_clearance_height_to_lid, config.extruder_clearance_height_to_rod);
// sends instances 2d hulls to be rendered
set_sequential_print_clearance_visible(true);
+108 -15
View File
@@ -252,9 +252,11 @@
#include "slic3r/GUI/ParamsDialog.hpp"
#include "slic3r/GUI/ParamsPanel.hpp"
#include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/TextureLibrary.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/plugin/host/PluginPages.hpp"
#include "slic3r/plugin/host/PluginResourceTextures.hpp"
#include <wx/defs.h>
#include "slic3r/GUI/Widgets/WebView.hpp"
#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;
}
// 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()
{
return;
@@ -3063,6 +3082,37 @@ void GUI_App::init_plugin_gui_wiring()
refresh_plugins_dialog();
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()
@@ -5434,12 +5484,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
this->request_open_project({});
}
else if (command_str.compare("homepage_open_cloud") == 0) {
// Deferred: set_cloud_mode() may create a second native webview, which must
// 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());
});
this->open_orca_cloud();
}
else if (command_str.compare("homepage_close_cloud") == 0) {
CallAfter([]() {
@@ -5588,6 +5633,22 @@ std::string GUI_App::handle_web_request(std::string cmd)
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)
{
try {
@@ -5865,7 +5926,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
// and asks for a handoff itself (guide §4). Signed out, user_id is omitted and the page will
@@ -5873,7 +5934,7 @@ wxString GUI_App::cloud_webview_handoff()
auto orca = get_orca_cloud_agent(m_agent);
if (!orca)
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)
@@ -6016,7 +6077,7 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
m_cloud_handoff_state.clear();
if (WebViewPanel* home = WebViewPanel::if_built())
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 +6924,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) {
// Load user's personal presets
@@ -6871,7 +6932,7 @@ void GUI_App::reload_settings()
int result = m_agent->get_user_presets(&user_presets);
if (result != 0) {
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();
auto refresh_synced_ui = [this, user_presets = std::move(user_presets)]() mutable {
@@ -6946,13 +7007,30 @@ void GUI_App::reload_settings()
refresh_synced_ui();
else
CallAfter(refresh_synced_ui);
return true;
}
return false;
}
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"))
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();
if (legacy_presets.empty()) {
if (by_user)
@@ -6997,6 +7075,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));
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);
mainframe->update_side_preset_ui();
for (auto tab : tabs_list) {
@@ -7763,6 +7844,7 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
BOOST_LOG_TRIVIAL(info) << "start_sync_service...";
// BBS
m_user_sync_token.reset(new int(0));
m_user_preset_pull_pending = true;
if (with_progress_dlg) {
// Mark a manual progress dialog as active so restart_sync_user_preset() ignores
// repeat triggers while it is on screen (prevents stacking modal dialogs).
@@ -7836,8 +7918,16 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
finishFn(ret == 0);
if (ret == 0 && m_agent && !t.expired())
reload_settings();
// Queued after the refresh reload_settings() queues, so it runs once the pulled presets are loaded.
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
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent());
@@ -7852,8 +7942,11 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
// Sync once immediately, then every 60 seconds.
while (!t.expired()) {
++tick_tock;
// Sync once immediately, then every 60s, or right away when a force-push asked for it.
if (tick_tock % 120 == 0 || m_sync_user_presets_now.exchange(false, std::memory_order_acq_rel)) {
// Sync once immediately, then every 60s, or right away when a force-push or a preset migration asked
// 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;
if (m_agent) {
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_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
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};
bool m_is_dark_mode{ 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
// newest request counts").
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 };
bool m_show_error_msgdlg{false};
wxString m_info_dialog_content;
@@ -466,6 +471,9 @@ public:
#if wxUSE_WEBVIEW_EDGE
void init_webview_runtime();
#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 bool dark_mode();
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
// signed out, otherwise mints a one-time token on a worker thread and loads the Complete URL
// 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 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);
@@ -589,7 +597,8 @@ public:
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
// 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);
bool process_network_msg(std::string dev_id, std::string msg);
void request_new_version(int by_user);
@@ -600,7 +609,8 @@ public:
std::string format_IP(const std::string& ip);
void show_dialog(wxString msg);
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();
bool maybe_migrate_user_presets_on_login();
File diff suppressed because it is too large. Load diff
@@ -428,6 +428,10 @@ private:
void render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max);
int m_picker_slot = -1;
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 import_custom_texture(TextureDisplacementLayer &layer);
@@ -508,120 +512,25 @@ private:
// is where the live adjustment is applied. Called whenever the panel moves or reverts it.
void invalidate_adjust_textures();
// "Adjust Texture" mode: instead of painting, the active layer's placement is a control lying
// on the model. The gizmo is one tile of the texture in the paint patch's own tangent plane:
// where it sits is the layer's offset, how it is turned is rotation_deg, how big it is is the
// tile extents - so each handle is one of the values it edits rather than a stand-in for it.
// Anchored to the centroid/average-normal of the active layer's paint patch (see
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn until something is painted.
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
// (a "pan" - drag anywhere on it for free 2D movement), plus two arrows along the patch's
// own U/V axes that constrain the drag to just that one axis for precise nudging. Anchored to
// the centroid/average-normal of the active layer's current paint patch (see
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn if it has nothing painted yet.
//
// Drawn through ImGui's foreground draw list rather than the "flat" shader, which is what gives
// it antialiased sub-pixel strokes, rounded joins, a contrast halo and the numeric readout. The
// cost is that render_adjust_texture_gizmo() has to be called from inside the gizmo's ImGui
// frame (see on_render_input_window()), not from on_render() with the rest of the GL drawing.
// Move is the whole interior, free in both axes. AxisMove is the four arrows, each holding a
// move to one of the tile's axes. EdgeScale is the four edge bars, one tile extent each, and
// CornerScale the four corner grips, which take both extents at once.
//
// The arrows and the bars share the same four rays out of the tile, so they are kept apart by
// distance rather than direction: the bar hugs its edge, then a clear gap, then the arrow well
// outside it. adjust_pick() leans on that - see the order it tests them in.
enum class AdjustHandle { None, Move, AxisMove, EdgeScale, CornerScale, Rotate };
// The gizmo's geometry for one frame, in screen pixels. Worked out once and shared by the
// drawing and the hit test, so the two cannot disagree about where a handle is. Everything
// except the frame itself is a fixed number of pixels, which is what keeps the control equally
// usable on a 5 mm part and a 500 mm one - the old handle scaled with the volume's bounding box
// and so was unusable at both ends.
struct AdjustFrame
{
bool valid = false;
Vec2d center = Vec2d::Zero();
// +u+v, +u-v, -u-v, -u+v. A scale grip sits on each.
std::array<Vec2d, 4> corner{};
// +u, -u, +v, -v: the middle of each edge, and the two ends of the bar that sits just
// outside it. The bar's ends are kept rather than recomputed so the drawing and the hit test
// cannot end up with it in two different places.
std::array<Vec2d, 4> edge_mid{};
std::array<Vec2d, 4> bar_a{};
std::array<Vec2d, 4> bar_b{};
// The axis arrow further out on the same ray: where its shaft starts and where its chevron
// ends. Kept, like the bar's ends, so the drawing and the hit test cannot disagree.
std::array<Vec2d, 4> arm_base{};
std::array<Vec2d, 4> arm_tip{};
std::array<Vec2d, 4> arm_dir{}; // unit, pointing outward from the centre
// A ring is placed off every corner, but only one is drawn and only that one can be picked:
// four marks for a single operation is clutter, and whichever corner the cursor is near is
// the one worth offering. `rotate_shown` says which, and see update_rotate_corner() for why
// it does not simply track the nearest.
std::array<Vec2d, 4> rotate_at{};
int rotate_shown = 0;
// The tile projects to less than ADJUST_MIN_TILE_PX across, so the frame was drawn at that
// floor instead. It is dashed to say so, and the readout still gives the true millimetres.
bool clamped = false;
// What the frame was built from, in mesh-local mm: the tile's own (rotated) edge directions
// and its extents. The drag arithmetic reuses these rather than recomputing them, so it
// cannot end up working against a different tile than the one on screen.
Vec3f e_u = Vec3f::UnitX(), e_v = Vec3f::UnitY();
Vec2f tile_mm = Vec2f::Ones(); // (u, v) as stored on the layer
Vec2f half_planar = Vec2f::Ones(); // half extents in planar mm, v already aspect-corrected
Vec2f center_planar = Vec2f::Zero();
};
// NOTE: the drag direction/sign below is this session's best-effort reasoning about which way
// the texture should appear to move as the handle is dragged - it could not be visually
// confirmed while writing it (no way to render/see pixels in this environment), so it may
// need a one-line sign flip once actually tested.
bool update_adjust_anchor(); // recomputes m_adjust_anchor_pos/normal; false if nothing painted
// The selected instance's volume-to-world transform, which the bake frame is derived from.
bool adjust_volume_to_world(Transform3d &out) const;
// The anchor, carried from mesh-local into the bake frame. Converted on every use rather than
// stored converted: the object can be moved, rotated or scaled while this mode is open, and a
// stored copy would quietly describe the shape it used to be. The expensive part - finding the
// paint patch's centroid - stays cached in the local members.
bool adjust_anchor_in_frame(Vec3f &pos, Vec3f &normal) const;
// Drops every scrap of live gizmo state. Called from each of the several ways out of the mode -
// the panel's toggle, collapsing the card, switching to seam marking, closing the gizmo - so a
// stale hover highlight or a half-finished drag cannot survive into the next time it is opened.
void end_adjust_texture_mode();
bool on_mouse_adjust_texture(const wxMouseEvent &mouse_event);
void render_adjust_texture_gizmo();
// This frame's gizmo geometry, or an invalid frame when there is nothing to draw.
AdjustFrame adjust_frame() const;
// Which handle is under `mouse`, with `index` set to the arm or corner it belongs to. Tested
// outermost first; see the hit-area board of the design for the order and the radii.
AdjustHandle adjust_pick(const Vec2d &mouse, const AdjustFrame &f, int &index) const;
// Chooses which corner carries the rotate ring. The nearest to the cursor, but it only moves once
// the newcomer is clearly nearer, because without that margin the ring flits between two corners
// every time the cursor wanders along the line equidistant from both. Never moves during a drag.
void update_rotate_corner(const AdjustFrame &f, const Vec2d &mouse);
// Where the tile's centre sits, in planar mm relative to the anchor, for a layer's placement.
// Not the patch centroid: the gizmo *is* the texture's placement, so it has to travel as the
// offset changes - pinning it to the centroid is what made the old handle look broken, the
// texture sliding while the handle stayed put.
static Vec2f adjust_handle_planar(const TextureDisplacementLayer &layer, float aspect);
// The exact inverse: the offset that puts the tile's centre at `planar`. Rotating or resizing
// the tile changes where a given offset places it, so every drag recomputes the offset from the
// centre it wants to keep - which is what makes rotate and scale act *in place* rather than
// swinging the tile around the anchor.
static Vec2f adjust_offset_for_planar(const Vec2f &planar, float rot_deg, const Vec2f &tile_mm,
const Vec2f &mirror_sign, float aspect);
// The mesh-local-to-world transform of the selected instance, the (unrotated) tangent basis and
// the lifted anchor the whole gizmo is built on. False when there is nothing to build on - which
// is the single place that decides the gizmo is not drawable, so the drawing, the hit test and
// the drag cannot disagree about it.
bool adjust_basis(Transform3d &trafo, Vec3f &u_axis, Vec3f &v_axis, Vec3f &base) const;
// Screen-space quad of a tile with the given placement. Used for the pre-drag ghost, which is
// drawn from the captured start placement rather than from the layer, the layer having already
// been moved by then.
bool adjust_quad_for(const Vec2f &center_planar, float rot_deg, const Vec2f &tile_mm,
std::array<Vec2d, 4> &out) const;
// Where the cursor meets the drag plane, in planar mm relative to the anchor. The plane is
// deliberately the *fixed* anchor plane rather than the tile's current position: measuring a
// drag against a plane the drag itself moves is a feedback loop.
bool adjust_planar_under(const Vec2d &mouse, Vec2f &planar) const;
// Draws a small '+'/'-' next to the mouse over the 3D view while painting/selecting, so it is
// obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's
// foreground draw list, so it must be called from inside the gizmo's ImGui frame.
void render_paint_cursor_hint();
// Bake-frame tangent-plane basis at m_adjust_anchor_normal: box_projection_axes(), so dragging
// Mesh-local tangent-plane basis at m_adjust_anchor_normal: box_projection_axes(), so dragging
// on-canvas maps consistently onto offset whichever face the anchor is on.
void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const;
@@ -1157,6 +1066,8 @@ private:
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
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
// every stroke end and every slider release - but depth/tiling/rotation/offset/blend change
@@ -1198,12 +1109,15 @@ private:
// The config key behind m_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).
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
// jolt rather than a scroll. The wheel is intercepted (ImGuiWindowFlags_NoScrollWithMouse) and
// moves a *target* offset instead; the real scroll is eased toward it over the following frames.
// Smooth scrolling for a child window, called inside it: the panel body (everything between the header
// and the pinned Bake footer) and the texture library's grid. ImGui jumps a fixed number of lines per
// wheel notch, which on tall layer cards reads as a hard jolt rather than a scroll. The child sets
// 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_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
// 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.
@@ -1213,47 +1127,23 @@ private:
// See the "Adjust Texture" block of private methods above.
bool m_adjust_texture_mode = false;
bool m_adjust_anchor_valid = false;
// Mesh-local, as compute_layer_paint_anchor() produces them. Everything else about the gizmo
// works in the bake frame - world orientation and scale, origin at the volume's own, which is
// where the texture is projected (texture_displacement_bake_frame()) - so these go through
// adjust_anchor_in_frame() at the point of use. A tile is so many *printed* millimetres whatever
// the instance's scale, which is why measuring the gizmo in mesh-local millimetres made its tile
// differ from the rendered one by exactly that scale.
Vec3f m_adjust_anchor_pos = Vec3f::Zero();
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ();
Vec3f m_adjust_anchor_pos = Vec3f::Zero(); // mesh-local
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ(); // mesh-local
// What the cursor is over, and what is being dragged. `index` picks the arm or the corner
// (0..3, in AdjustFrame's order); it is -1 for Move and Rotate, which have only one of each.
// Only ever one of each is set, so two handles can never both look live.
// Which corner currently carries the rotate ring. Persists between frames, which is what gives
// update_rotate_corner() something to apply its margin against.
int m_adjust_rotate_corner = 0;
AdjustHandle m_adjust_hover_kind = AdjustHandle::None;
int m_adjust_hover_index = -1;
AdjustHandle m_adjust_drag_kind = AdjustHandle::None;
int m_adjust_drag_index = -1;
// Pan: free drag anywhere on the flat panel, moves offset along both axes. AxisU/AxisV: drag
// the corresponding arrow, moves offset along only that one axis.
enum class AdjustHandle { None, Pan, AxisU, AxisV };
AdjustHandle m_adjust_drag_handle = AdjustHandle::None;
Vec2f m_adjust_drag_start_offset = Vec2f::Zero();
// Anchor-relative planar position (see project_planar()) of the point under the mouse at the
// moment the current drag started; every subsequent frame's delta is measured against this,
// rather than accumulated frame-to-frame, to avoid drift.
Vec2f m_adjust_drag_start_planar = Vec2f::Zero();
// Everything the live drag is measured against, captured once on mouse-down. Every frame's
// delta is taken from these rather than accumulated frame to frame, so a long drag cannot
// drift away from the cursor.
Vec2f m_adjust_drag_start_center = Vec2f::Zero(); // tile centre, planar mm
Vec2f m_adjust_drag_start_grab = Vec2f::Zero(); // the grabbed point, planar mm
// How far the grab landed from the handle's own position, in planar mm. A scale drag reads the
// extents straight off the cursor, so without subtracting this the tile would jump by however
// far inside the generous hit radius the click happened to fall.
Vec2f m_adjust_drag_grab_bias = Vec2f::Zero();
float m_adjust_drag_start_rot = 0.f;
Vec2f m_adjust_drag_start_tile = Vec2f::Ones(); // (u, v) extents as stored
float m_adjust_drag_start_angle = 0.f; // planar angle of the grab, radians
// The mirroring the layer had when the drag began. A width or height handle dragged back through
// the tile's middle toggles its axis against this, rather than against whatever the last frame
// left behind, so dragging back and forth across the middle cannot ratchet the flag.
bool m_adjust_drag_start_mirror_u = false;
bool m_adjust_drag_start_mirror_v = false;
// The live value to put in the readout pill, built by the drag and drawn by the renderer in the
// same frame. Empty when no drag is running, which is also when no pill is drawn.
std::string m_adjust_readout;
// Lazily-built unit quad (the pan panel) and unit line-with-arrowhead (reused, rotated, for
// both the U and V axis arrows), transformed into place at render time.
GLModel m_adjust_panel_glmodel;
GLModel m_adjust_arrow_glmodel;
std::map<std::string, wxString> m_desc;
+6 -4
View File
@@ -83,11 +83,13 @@ namespace Slic3r::GUI::GLGizmoUtils {
bool clicked = toolbar_circular_button(icon, "##wiki_guide_btn", scale);
if (url && *url && clicked)
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(url && *url)
ImGui::SetTooltip("%s\n%s", _u8L("Wiki Guide").c_str(), url);
wxGetApp().imgui()->tooltip(_u8L("Wiki Guide") + "\n" + url, -1.f);
else
ImGui::SetTooltip("%s", _u8L("Wiki Guide").c_str());
wxGetApp().imgui()->tooltip(_u8L("Wiki Guide"), -1.f);
}
return clicked; // for dynamically generated links
}
@@ -101,9 +103,9 @@ namespace Slic3r::GUI::GLGizmoUtils {
wxLaunchDefaultBrowser(wxString::FromUTF8(url));
if (ImGui::IsItemHovered()){
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
ImGui::SetTooltip("%s", _u8L("Video Guide").c_str());
wxGetApp().imgui()->tooltip(_u8L("Video Guide"), -1.f);
}
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)) {
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()) {
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);
}
ImGuiWrapper::pop_combo_style();
// ORCA use TextColored to match axes color
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))
m_align_to_parent = align_mode_idx + 1 == align_modes.size();
ImGui::PopID();
ImGuiWrapper::pop_combo_style();
// After pop_combo_style(): its light popup background would hide the tooltip's white text.
if (ImGui::IsItemHovered()) {
wxString tooltip_str = _L("Reference for alignment");
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();
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)) {
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()) {
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);
}
ImGuiWrapper::pop_combo_style();
// ORCA use TextColored to match axes color
float offset_to_center = (unit_size - ImGui::CalcTextSize("O").x) / 2;
+27 -3
View File
@@ -62,6 +62,7 @@
#include "Search.hpp"
#include "BitmapCache.hpp"
#include "GUI_App.hpp"
#include "KeyChord.hpp"
#include <nanosvg/nanosvg.h>
#include <nanosvg/nanosvgrast.h>
@@ -505,10 +506,12 @@ bool ImGuiWrapper::update_mouse_data(wxMouseEvent& evt)
io.MouseDown[0] = evt.LeftIsDown();
io.MouseDown[1] = evt.RightIsDown();
io.MouseDown[2] = evt.MiddleIsDown();
float wheel_delta = static_cast<float>(evt.GetWheelDelta());
if (wheel_delta != 0.0f) {
// Only a vertical wheel event with a direction is a wheel step. A macOS trackpad reports sideways motion
// 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;
}
unsigned buttons = (evt.LeftIsDown() ? 1 : 0) | (evt.RightIsDown() ? 2 : 0) | (evt.MiddleIsDown() ? 4 : 0);
m_mouse_buttons = buttons;
@@ -2132,6 +2135,27 @@ bool ImGuiWrapper::want_any_input() const
return io.WantCaptureMouse || io.WantCaptureKeyboard || io.WantTextInput;
}
bool ImGuiWrapper::text_input_active() const
{
const ImGuiID id = ImGui::GetActiveID();
return id != 0 && ImGui::GetInputTextState(id) != nullptr;
}
bool ImGuiWrapper::modal_popup_open() const
{
// A modal stays on the popup stack while its canvas is hidden, so it counts only if the last finished
// frame drew it, which NewFrame() moves from Active to WasActive.
const ImGuiWindow* modal = ImGui::GetTopMostPopupModal();
if (modal == nullptr)
return false;
return ImGui::GetCurrentContext()->WithinFrameScope ? modal->WasActive : modal->Active;
}
bool ImGuiWrapper::owns_key(const KeyChord& chord) const
{
return modal_popup_open() || (chord.edits_text() && text_input_active());
}
ImFontAtlasCustomRect* ImGuiWrapper::GetTextureCustomRect(const wchar_t& tex_id)
{
auto item = m_custom_glyph_rects_ids.find(tex_id);
+7
View File
@@ -35,6 +35,7 @@ struct ImRect;
namespace Slic3r {
namespace GUI {
struct KeyChord;
bool get_data_from_svg(const std::string &filename, unsigned int max_size_px, ThumbnailData &thumbnail_data);
@@ -219,6 +220,12 @@ public:
bool want_keyboard() const;
bool want_text_input() const;
bool want_any_input() const;
// True while an ImGui InputText is active, without the one-frame lag of want_text_input().
bool text_input_active() const;
// True while an ImGui modal popup is on screen.
bool modal_popup_open() const;
// True for every key while a modal is open, and for text-editing keys while an InputText is active.
bool owns_key(const KeyChord& chord) const;
// Optional inputs are used for set up value inside of an optional, with default value
//
+4 -5
View File
@@ -173,7 +173,7 @@ void KBShortcutsDialog::fill_pages()
using Section = ShortcutSection;
if (wxGetApp().is_editor()) {
page(_L("Global"), _L("Available anywhere in the window, even while typing in a text field."), ShortcutContext::Global, {
page(_L("Global"), _L("Available anywhere in the window."), ShortcutContext::Global, {
fixed(Section::SpeedDial, { alt, "1-9, 0" }, L("Run favorite 1 to 10")),
fixed(Section::SpeedDial, { ctrl, "B" }, L("Pin or unpin the selected action")),
// wx cycles notebook pages on Ctrl+Tab, which is Cmd+Tab on macOS and never arrives there.
@@ -438,17 +438,16 @@ ShortcutCaptureDialog::ShortcutCaptureDialog(wxWindow* parent, Shortcut shortcut
SetBackgroundColour(*wxWHITE);
wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
// A Global shortcut also runs while a text field has the focus, so its hint names the keys it can use.
// A Global shortcut runs before the focused control gets the key, so its hint names the keys it can use.
const bool global = (shortcut_info(shortcut).contexts & context_bit(ShortcutContext::Global)) != 0;
const wxString advice = global_key_advice();
const wxString typing = _L("Global shortcuts also apply while typing.");
const wxString rule = _L("A key that types a character cannot be a global shortcut.");
m_hint = global ? typing + "\n" + advice : _L("Esc cancels, Enter confirms.");
m_hint = global ? advice : _L("Esc cancels, Enter confirms.");
m_rejection = rule + "\n" + advice;
// Wide enough for each sentence on a line of its own where the translation allows, within limits.
int width = FromDIP(450);
for (const wxString& sentence : { typing, rule, advice }) {
for (const wxString& sentence : { rule, advice }) {
int extent = 0;
GetTextExtent(sentence, &extent, nullptr, nullptr, nullptr, &wxGetApp().normal_font());
width = std::max(width, extent);
+34
View File
@@ -187,6 +187,40 @@ bool KeyChord::is_menu_accelerator() const
return valid() && ((modifiers & (wxMOD_CONTROL | wxMOD_ALT | wxMOD_RAW_CONTROL)) != 0 || (!is_printable(key) && key != WXK_SPACE));
}
bool KeyChord::edits_text() const
{
if (!valid())
return false;
if (!is_menu_accelerator())
return true;
const int mods = modifiers & ~wxMOD_SHIFT;
switch (key) {
case WXK_LEFT: case WXK_RIGHT: case WXK_UP: case WXK_DOWN:
case WXK_HOME: case WXK_END: case WXK_BACK: case WXK_DELETE:
#ifdef __APPLE__
return mods == wxMOD_NONE || mods == wxMOD_CONTROL || mods == wxMOD_ALT; // Option moves by word
#else
return mods == wxMOD_NONE || mods == wxMOD_CONTROL;
#endif
case WXK_INSERT:
return mods == wxMOD_NONE || mods == wxMOD_CONTROL;
case WXK_RETURN: case WXK_TAB: case WXK_ESCAPE: case WXK_PAGEUP: case WXK_PAGEDOWN:
return mods == wxMOD_NONE;
default:
break;
}
if (!is_printable(key))
return false;
#ifdef __APPLE__
if (mods == wxMOD_ALT || mods == wxMOD_RAW_CONTROL)
return true; // Option types a character, Control runs the Cocoa text key bindings
#else
if (mods == (wxMOD_CONTROL | wxMOD_ALT))
return true; // AltGr types a character
#endif
return mods == wxMOD_CONTROL && (key == 'A' || key == 'C' || key == 'V' || key == 'X' || key == 'Y' || key == 'Z');
}
// Only a chord the desktop acts on while still delivering it to the app belongs here.
bool KeyChord::is_system_shortcut() const
{
+3
View File
@@ -35,6 +35,9 @@ struct KeyChord
// True when Ctrl or Alt is held or the key is non-printable, the chords a menu can own without
// swallowing typing in text fields.
bool is_menu_accelerator() const;
// True for a chord a focused text field acts on: typing a character (AltGr and macOS Option
// characters included), moving the caret, selecting, deleting, or the clipboard and undo chords.
bool edits_text() const;
// True for a chord the desktop acts on although the app receives it, so a binding would
// take it from the system.
bool is_system_shortcut() const;
+31 -16
View File
@@ -50,6 +50,7 @@
#include <wx/panel.h>
#include <wx/settings.h>
#include <wx/textentry.h>
#include <wx/spinctrl.h>
#include <wx/notebook.h>
#include <wx/listbook.h>
#include <wx/simplebook.h>
@@ -107,6 +108,8 @@
#include "../Utils/PrintHost.hpp"
#include "GUI_App.hpp"
#include "ImGuiWrapper.hpp"
#include "OpenGLManager.hpp"
#include "Shortcuts.hpp"
#include "UnsavedChangesDialog.hpp"
#include "PublishSettingsDialog.hpp"
@@ -184,18 +187,31 @@ enum class ERescaleTarget
namespace {
bool focus_is_text_field(wxWindow* focus)
{
if (dynamic_cast<wxTextEntryBase*>(focus) || dynamic_cast<wxWebView*>(focus))
return true; // a web page can have its own text fields
if (dynamic_cast<wxSpinCtrl*>(focus) || dynamic_cast<wxSpinCtrlDouble*>(focus))
return true; // the native Windows spin control is not a wxTextEntry
return false;
}
// Only a scene canvas draws ImGui, and a hidden one can keep the wx focus after a tab switch.
const ImGuiWrapper* focused_canvas_imgui(wxWindow* focus)
{
if (!OpenGLManager::is_scene_canvas(focus) || !focus->IsShownOnScreen())
return nullptr;
return wxGetApp().imgui();
}
// Space opens the speed dial, but it is the activation key for buttons, checkboxes and other
// controls. CHAR_HOOK runs before the focused child, so only take Space when the focused window has
// no keyboard-activation meaning of its own. Canvases (GLCanvas3D) and panels are not controls and
// fall through to "open"; the Notebook itself does too, so Space still opens the dial on any page.
// controls, so only take Space when the focused window has no keyboard-activation meaning of its
// own. Canvases (GLCanvas3D) and panels are not controls and fall through to "open"; the Notebook
// itself does too, so Space still opens the dial on any page.
bool focus_keeps_space(wxWindow* focus)
{
if (!focus)
return false;
if (dynamic_cast<wxTextEntryBase*>(focus))
return true; // typing a space into a text field
if (dynamic_cast<wxWebView*>(focus))
return true; // web content scrolls and hosts its own text fields
if (dynamic_cast<::Button*>(focus))
return true; // custom button: Space clicks it (it is a wxWindow, not a wxControl)
if (dynamic_cast<StaticBox*>(focus))
@@ -816,16 +832,12 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
bool MainFrame::handle_global_shortcut(const KeyChord& chord)
{
// ImGui draws its text fields (the text tool's box, a sketch value field) on the GL canvas, so
// while one of them is typed into the focused wx window is that canvas or an in-canvas frame:
// neither is a wxControl, so focus_keeps_space() cannot see it and CHAR_HOOK takes the chord
// here first -- Space opened the Speed Dial, Ctrl+P the Preferences, and the field never saw
// the key. ImGui raises WantCaptureKeyboard while one of its items owns the keyboard, so leave
// the key to it then. A focused wxControl (text field, list, web view) is untouched: those
// keep their own keys, and ImGui's flag can outlive the focus moving to one of them.
// CHAR_HOOK runs before the focused window, so leave text-editing keys to a focused text field and
// the keys ImGui owns to the focused canvas.
wxWindow* const focus = wxWindow::FindFocus();
ImGuiWrapper* const imgui = wxGetApp().imgui();
if (focus != nullptr && imgui != nullptr && imgui->want_keyboard() && dynamic_cast<wxControl*>(focus) == nullptr)
if (chord.edits_text() && focus_is_text_field(focus))
return false;
if (const ImGuiWrapper* imgui = focused_canvas_imgui(focus); imgui != nullptr && imgui->owns_key(chord))
return false;
const std::optional<Shortcut> shortcut = wxGetApp().shortcuts().lookup(ShortcutContext::Global, chord);
@@ -1739,6 +1751,9 @@ void MainFrame::fit_tab_labels()
for (size_t i = count - 1; i >= 1; --i) {
int right = ScreenToClient(m_slice_option_btn->ClientToScreen({})).x;
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;
ctrl->SetCompact(i, true);
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.
// an overflow indicator. Pass nullptr to remove it; ownership stays with the caller.
void SetOverflowButton(wxWindow* button);
wxWindow* GetOverflowButton() const { return m_overflow_button; }
private:
wxFlexGridSizer* m_buttons_sizer;
+8 -1
View File
@@ -427,6 +427,8 @@ wxGLContext* OpenGLManager::init_glcontext(wxGLCanvas& canvas, const std::pair<i
return m_context;
}
static const char* const SCENE_CANVAS_NAME = "OrcaSceneCanvas";
wxGLCanvas* OpenGLManager::create_wxglcanvas(wxWindow& parent)
{
int antialiasing_samples = 4;
@@ -470,12 +472,17 @@ wxGLCanvas* OpenGLManager::create_wxglcanvas(wxWindow& parent)
attribList[samples_value_idx] = 0;
}
wxGLCanvas* canvas = new wxGLCanvas(&parent, wxID_ANY, attribList, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS);
wxGLCanvas* canvas = new wxGLCanvas(&parent, wxID_ANY, attribList, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS, SCENE_CANVAS_NAME);
// The GL canvas paints its entire surface, so background erasing is unnecessary.
canvas->SetBackgroundStyle(wxBG_STYLE_PAINT);
return canvas;
}
bool OpenGLManager::is_scene_canvas(const wxWindow* window)
{
return window != nullptr && window->GetName() == SCENE_CANVAS_NAME;
}
void OpenGLManager::detect_multisample(int* attribList)
{
int wxVersion = wxMAJOR_VERSION * 10000 + wxMINOR_VERSION * 100 + wxRELEASE_NUMBER;
+2
View File
@@ -101,6 +101,8 @@ public:
static bool are_framebuffers_supported() { return (s_framebuffers_type != EFramebufferType::Unknown); }
static EFramebufferType get_framebuffers_type() { return s_framebuffers_type; }
static wxGLCanvas* create_wxglcanvas(wxWindow& parent);
// True for a canvas made by create_wxglcanvas(), the kind a GLCanvas3D draws its scene and ImGui into.
static bool is_scene_canvas(const wxWindow* window);
static const GLInfo& get_gl_info() { return s_gl_info; }
static bool force_power_of_two_textures() { return s_force_power_of_two_textures; }
+6 -1
View File
@@ -21178,7 +21178,12 @@ void Plater::print_job_finished(wxCommandEvent &evt)
Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return;
dev->set_selected_machine(evt.GetString().ToStdString());
const std::string dev_id = evt.GetString().ToStdString();
MachineObject* selected = dev->get_selected_machine();
// After a LAN print, the completion event reselects the same printer.
// Keep the active session while asynchronous status traffic may still be in flight.
if (!selected || selected->get_dev_id() != dev_id || selected->connection_type() != "lan")
dev->set_selected_machine(dev_id);
p->main_frame->request_select_tab(TAB_ID_MONITOR);
// Selects the status page on a built Device tab; one built by the switch starts there.
MonitorPanel* curr_monitor = MonitorPanel::if_built();
+3 -1
View File
@@ -2225,8 +2225,10 @@ void PublishSettingsDialog::refresh_tab_indicators()
if (section.variant_switch != nullptr) {
const wxString& title = m_categories[section.categories[i]].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.page->Layout();
}
}
any = any || on;
}
+1 -1
View File
@@ -4087,7 +4087,7 @@ void SelectMachineDialog::update_user_printer()
{
m_printer_last_select = "";
update_select_layout(nullptr);
m_printer_box->GetPrinterComboBox()->SetTextLabel("");
m_printer_box->GetPrinterComboBox()->SetValue(_L("No available printers"));
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "for send task, current printer id = " << m_printer_last_select << std::endl;
+29 -17
View File
@@ -474,17 +474,14 @@ void Selection::add_curr_plate()
if (!m_valid)
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!"));
m_mode = Instance;
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();
this->set_bounding_boxes_dirty();
@@ -511,20 +508,14 @@ void Selection::remove_curr_plate()
if (!m_valid)
return;
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
if (plate->empty())
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-Delete Curr Plate All"));
m_mode = Instance;
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();
this->set_bounding_boxes_dirty();
@@ -2270,6 +2261,27 @@ std::vector<unsigned int> Selection::get_volume_idxs_from_volume(unsigned int ob
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> idxs;
Loaded 100 of 149 files, more files were not shown because too many files have changed in this diff. Show more