Compare commits

..
Author SHA1 Message Date
raistlin7447 17c3bbb7b6 Merge branch 'main' into perf/slicing-optimizations 2026-10-19 11:33:03 -05:00
Sergei f0c59208e7 Apply post-processing scripts to G-code packed into .gcode.3mf (non-BBL) (#16464)
Post-processing scripts now run on a copy of each sliced plate's G-code before it is packed into a .gcode.3mf. This covers Export plate sliced file, sending a .3mf to a print host, and the print dialog for non-BBL printers. BBL printers are skipped, because their G-code is post-processed in place after slicing. Backups are skipped too.

Export plate sliced file no longer reports success when export_3mf() fails.
2026-10-19 10:47:03 -05:00
Ian Bassi 9f6a344162 Make the adaptive infill settings pattern-neutral (#16654) 2026-10-19 12:05:55 -03:00
Ian Bassi c5cfe40a1f Update translations: fill empty entries in all languages and fix color picker strings (#16652) 2026-10-19 10:19:06 -03:00
SoftFever e5b6cbf091 Let plugins make HTTPS requests on macOS (#16638)
* Let the bundled Python verify HTTPS certificates on macOS

* Restore SSL_CERT_FILE after the bundled Python override test
2026-10-19 20:06:14 +08:00
Alexandre Folle de Menezes 3b9b871ba6 Standardize the spelling of "G-code", "SVG", "GitHub" and "IP" (#16618) 2026-10-19 09:01:39 -03:00
Surfoo cb2260f057 i18n(fr): update translations (#16591) 2026-10-19 09:00:32 -03:00
Alexandre Folle de Menezes 3fcbb2adf9 Translate color names to pt-BR (#16585) 2026-10-19 08:59:35 -03:00
Sergei 252bbbf9f5 Make the exclude-object outline tolerance a user setting (#16503)
* Make the exclude-object outline tolerance configurable

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>

* fix(config): move exclude_object_tolerance to developer mode

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>

* fix(config): move exclude_object_tolerance to expert mode

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>

* feat(ui): hide exclude_object_tolerance for non-Klipper firmwares

mend

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>

* Test that a larger exclude object tolerance writes fewer outline points

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>

---------

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>
2026-10-19 08:17:20 -03:00
Lam Wei Lun 4c2ae6d202 Keep Auto Drop off for projects saved by 2.4.x (#16633)
2.4.0 to 2.4.2 wrote auto_drop on each build item, and 2.4.x objects
carry no auto_drop attribute in model_settings.config. The importer only
read the object attribute and treated a missing one as on, so floating
objects were dropped to the bed on load and the flag was rewritten as on
on the next save.

Record object ids with an item auto_drop="0" and use that as the fallback
when the object has no attribute. The attribute's presence is now tracked
so the fallback applies only when it is absent. Any item set to off turns
auto drop off for the object, since the flag is shared by all instances.
2026-10-19 16:08:59 +08:00
HanifKoh ac022b56ad Wait for the Close Frame in the Moonraker Status Stream Test (#16635)
The fake printer counts the agent's close frame in its read handler, but
Beast answers the close before that read completes, so stop() can return
and the test can read the counter before it is incremented. On a loaded
runner the check saw 0.

Wait for the counter like the other fake-printer counters do, then keep
the exact-count check.
2026-10-19 16:06:41 +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
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
Rodrigo Faselli c030131bcc Merge branch 'main' into perf/slicing-optimizations 2026-10-11 22:39:31 -03:00
Rodrigo Faselli ac8ac299a5 add missing headers
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-11 22:36:16 -03:00
Rodrigo Faselli 0f48982b78 Merge branch 'main' into perf/slicing-optimizations 2026-10-11 15:00:58 -03:00
SoftFever f6e8821b13 Merge branch 'main' into perf/slicing-optimizations 2026-10-10 01:09:46 +08:00
SoftFever 076d88f34e Merge branch 'main' into perf/slicing-optimizations 2026-10-09 22:19:27 +08:00
SoftFever 7f554fde04 Restore the internal bridge expansion from #16056 lost in the main merge 2026-10-09 22:11:47 +08:00
SoftFever d704ee5538 Merge branch 'main' into perf/slicing-optimizations
# Conflicts:
#	src/libslic3r/PerimeterGenerator.cpp
#	src/libslic3r/PrintObject.cpp
#	src/libslic3r/Support/TreeSupport.cpp
2026-10-09 19:48:26 +08:00
Rodrigo Faselli 7de1dc1548 Merge branch 'main' into perf/slicing-optimizations 2026-10-04 13:26:02 -03:00
Rodrigo Faselli b6140110f2 Merge branch 'main' into perf/slicing-optimizations 2026-10-03 10:57:31 -03:00
Ian Bassi 7b0e2f3ce5 Keep the G-code identical to main on Clipper2
Since main moved to Clipper2, parts of this branch no longer gave the same
G-code as main:

- bridge_over_infill dropped expand(limiting_area, 0.3 * flow.spacing()) as
  a no-op. The offset is below one unit, but Clipper2 still unites its
  result, which splits and merges touching polygons and so changes the
  anchor lines. Running it on the polygons next to the bridge gives main's
  anchors without the whole-layer pass.
- tsp_remove_crossings stopped at the first repeated ordering, where main
  runs on to its pn * pn cap. The loop is periodic from that point, so it
  now takes only the steps to the ordering main stops on.
- With a single tile, the tiled booleans now make the plain call instead of
  cutting the clip to the tile first.

The tiled boolean test compared rings exactly. With the safety offset a tile
unites only the clip polygons near it, and Clipper2 can then round a
crossing 1 unit differently, so that case allows 1 unit.

Comments that named ClipperLib now say Clipper, and
docs/HLSD/polygon-clipping.md describes the tiled booleans.

G-code of the five handy models in four configurations and of a baked
texture relief is byte-identical to main. Colour-painted models still
differ: segmenting each island on its own splits a colour's region into
different pieces than one diagram over the layer, which on one model also
changes the first layer's tool order.
2026-10-02 20:26:59 -03:00
ExPikaPaka d80c69341c Say what the code does, not what it replaced
The timings and the runs that never finished belong in the commit
messages, where they can be read against the change; a reader of the
code cannot check them. Kept the cost that still explains the design.

MultiPoint also spells out the consequence: a moved-from Polygon or
Polyline is now really empty where it used to silently keep its points.

The two wall spacing comments the parallel loop reindented are plain
ASCII now, so the whole file is.
2026-10-02 18:13:46 -03:00
ExPikaPaka 4a63a7d916 Compare the tiled booleans polygon by polygon
Area alone would pass on a result whose pieces were merged across tiles
or which kept the cut edges of the clip. The rings are compared after
rotating each to its lowest point and sorting, so only the ordering is
free. The fixture now also asserts it really is split into more than one
tile, which is the path being tested.
2026-10-02 18:13:46 -03:00
ExPikaPaka de1dfd0611 Hand the island's walls over instead of copying them
append(const ExtrusionEntity &) clones; the collection each island
produced was deep-copied into the layer's loops and then thrown away.
The no-overlap areas are moved as well.
2026-10-02 18:13:46 -03:00
ExPikaPaka 11a5971cef Include what the new code uses
The Windows build stopped on test_kdtree.cpp: it calls std::iota without
including <numeric>, which libstdc++ happens to pull in anyway. Added
there, and the same for <limits> and <algorithm>/<cmath> where the
recent changes rely on them being included by something else.
2026-10-02 18:13:45 -03:00
ExPikaPaka 82f37ddb3a Fix the Windows build: near and far are macros there
bridge_over_infill's helper for splitting polygons by proximity named
its locals near and far. The Windows headers define both as macros that
expand to nothing, so "Polygons near;" declared nothing and the uses of
it did not compile. Renamed; no behaviour change.
2026-10-02 18:13:37 -03:00
ExPikaPaka fb03d1a1cb Visit seam candidates as the search finds them
Collecting every candidate within the radius into a vector cost more
than the search itself. Same order, so the seams are unchanged;
align_seam_points ~19.6 s at 0.1 mm / 2000k, was ~21.
2026-10-02 18:13:37 -03:00
ExPikaPaka 2b4bdead73 Run a layer's regions in parallel where they are independent
detect_surfaces_type, process_external_surfaces and the vertical shells
each waited on their own heaviest layer in turn. The LOTR map plate
slices in ~10.5 min at 0.1 mm / 2000k, was ~11.5; ~87 s at normal
settings, was ~97.
2026-10-02 18:13:37 -03:00
ExPikaPaka b1f0d6c6f6 Move polygons instead of copying them on move
MultiPoint had no rvalue constructor, so the derived move constructors
bound to the const reference and copied; append reserved exactly, so
collecting pieces one by one was quadratic. Colour segmentation ~3 s at
0.1 mm / 2000k, was ~40, and ordinary prints gain too.
2026-10-02 18:13:37 -03:00
ExPikaPaka 14751a8b06 Project painted faces onto the shell layers per tile
Only the slices within the deepest shell offset decide the result, so
the work is done per tile of the face. Top and bottom segmentation
~130 s at 0.1 mm / 2000k, was ~180.
2026-10-02 18:13:36 -03:00
ExPikaPaka 9a86d79038 Tile the booleans on layers of many pieces
ClipperLib slows down with the number of edges on a scan line, and a
layer cut through a fine relief has tens of thousands of pieces.
detect_surfaces_type ~50 s at 0.1 mm / 2000k, was ~145.
2026-10-02 18:13:05 -03:00
ExPikaPaka d905f1a39b Merge colour and top/bottom regions per island
The merge took anything from 3 to 38 minutes at 0.1 mm / 2000k, now
~2.5. Every region is grouped with the islands it overlaps, so the
result is the same.
2026-10-02 18:12:41 -03:00
ExPikaPaka 84ec518f26 Slice fine texture relief without stalling
A colour texture baked at 0.1 mm / 2000k made the top layers thousands
of islands and slicing never finished. Colour segmentation runs per
island, the merge subtracts piece by piece, the support check tests only
nearby islands, and the travel ordering finds crossings through a grid.
2026-10-02 18:12:41 -03:00
ExPikaPaka 1fc153308f Generate walls and split solid infill in parallel
Same output, ~2.2 min for the LOTR map plate, was ~2.6.
2026-10-02 18:12:41 -03:00
ExPikaPaka 3167c3665a Run colour segmentation and vertical shells in parallel
Same output, ~2.6 min for the LOTR map plate, was ~2.9.
2026-10-02 18:12:40 -03:00
ExPikaPaka 6d34d83e78 Faster slicing of colour-painted layers
A layer split into ~1000 colour fragments (a colour texture baked over a
large top face) made several per-fragment loops redo whole-layer ClipperLib
work, so slicing took ~33 min; it now takes ~3 min with the same output.

- make_fills: clip the layer's no-overlap area to each expolygon's box
  before intersecting
- discover_vertical_shells: small-piece filter compares only against the
  nearby part of the layer
- bridge_over_infill: whole-layer union/diff/intersections restricted to the
  candidate's neighbourhood; fill boundary expanded once per spacing; anchor
  tree built only from lines crossing the scan range; bbox pre-check in the
  collision test; limiting outline taken directly instead of through
  expand(..., 0.3 * flow.spacing()), which offsets by 0.135 scaled units
  (flow.spacing() is in mm) and only cost a whole-layer pass per candidate
2026-10-02 18:11:17 -03:00
163 changed files with 24692 additions and 11117 deletions

No files matched your search

+4 -4
View File
@@ -1,6 +1,6 @@
--- ---
name: orca-translations name: orca-translations
description: Use when updating, filling, reviewing or fixing OrcaSlicer translations in localization/i18n — running scripts/run_gettext --full, translating empty or new msgids in the .po catalogs, AI translation passes and their "# AI Translated" / "# Needs human review" comments, plural forms, msgctxt homonyms, and keeping catalogs consistent with the localization glossary (OrcaSlicer_WIKI localization_glossary.tsv), including adding new glossary terms. Also use for msgfmt or run_gettext failures, placeholder or line-break mismatches, and reviewing a translation PR against the glossary. description: Use when updating, filling, reviewing or fixing OrcaSlicer translations in localization/i18n — running scripts/run_gettext --full, translating empty or new msgids in the .po catalogs, AI translation passes and their "# AI Translated" / "# Needs human review" comments, plural forms, msgctxt homonyms, and keeping catalogs consistent with the localization glossary (OrcaSlicer_WIKI developer_reference/glossary), including adding new glossary terms. Also use for msgfmt or run_gettext failures, placeholder or line-break mismatches, and reviewing a translation PR against the glossary.
--- ---
# OrcaSlicer translations # OrcaSlicer translations
@@ -9,7 +9,7 @@ The UI strings live in gettext catalogs: `localization/i18n/OrcaSlicer.pot` is t
`localization/i18n/<lang>/OrcaSlicer_<lang>.po` holds each language. The `en` catalog is empty on `localization/i18n/<lang>/OrcaSlicer_<lang>.po` holds each language. The `en` catalog is empty on
purpose, so skip it. Recurring terms are fixed by the purpose, so skip it. Recurring terms are fixed by the
[localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md), [localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md),
whose source is `developer_reference/localization_glossary.tsv` in the **OrcaSlicer_WIKI** repository. whose source is the per-language files in `developer_reference/glossary/` (`terms.tsv`, `languages.txt` and one `<lang>.tsv`) in the **OrcaSlicer_WIKI** repository.
A translation pass therefore touches two repositories: the catalogs here, and the glossary there. A translation pass therefore touches two repositories: the catalogs here, and the glossary there.
All catalog editing goes through [`scripts/po_tool.py`](scripts/po_tool.py) (standard library only). All catalog editing goes through [`scripts/po_tool.py`](scripts/po_tool.py) (standard library only).
@@ -153,7 +153,7 @@ Report:
- anything left empty, and why; - anything left empty, and why;
- the glossary rows added or filled. - the glossary rows added or filled.
The catalogs (and `OrcaSlicer.pot`) and the glossary (`localization_glossary.tsv` and `.md`) are The catalogs (and `OrcaSlicer.pot`) and the glossary (`developer_reference/glossary/*.tsv` and `localization_glossary.md`) are
separate changes in separate repositories. The wiki change should land with, or before, the catalog separate changes in separate repositories. The wiki change should land with, or before, the catalog
change. Commit or open PRs only when asked. change. Commit or open PRs only when asked.
@@ -210,7 +210,7 @@ with the translators, followed by the catalogs.
When this skill and the checkouts disagree, the checkouts win: When this skill and the checkouts disagree, the checkouts win:
- `scripts/run_gettext.sh` / `.bat` for extraction and merging; - `scripts/run_gettext.sh` / `.bat` for extraction and merging;
- `localization/i18n/list.txt` for the scanned sources; - `localization/i18n/list.txt` for the scanned sources;
- the wiki's `localization_guide.md` and `localization_glossary.md` / `.tsv` for the rules and terms; - the wiki's `localization_guide.md` and `localization_glossary.md` and `glossary/*.tsv` for the rules and terms;
- `AGENTS.md` (Localization & translations) for the repository's conventions. - `AGENTS.md` (Localization & translations) for the repository's conventions.
## Editing this skill ## Editing this skill
@@ -28,13 +28,14 @@ import sys
import tempfile import tempfile
import time import time
import unicodedata import unicodedata
import urllib.error
import urllib.request import urllib.request
from pathlib import Path from pathlib import Path
REPO = Path(__file__).resolve().parents[4] REPO = Path(__file__).resolve().parents[4]
I18N = REPO / "localization" / "i18n" I18N = REPO / "localization" / "i18n"
SKIP_LANGS = {"en"} # the English catalog is intentionally empty SKIP_LANGS = {"en"} # the English catalog is intentionally empty
GLOSSARY_TSV = Path("developer_reference") / "localization_glossary.tsv" GLOSSARY_DIR = Path("developer_reference") / "glossary" # terms.tsv, languages.txt and one <lang>.tsv per language
GLOSSARY_MD = Path("developer_reference") / "localization_glossary.md" GLOSSARY_MD = Path("developer_reference") / "localization_glossary.md"
MISSING = {"", "—"} MISSING = {"", "—"}
RAW_URL = "https://raw.githubusercontent.com/OrcaSlicer/OrcaSlicer_WIKI/main/" RAW_URL = "https://raw.githubusercontent.com/OrcaSlicer/OrcaSlicer_WIKI/main/"
@@ -252,30 +253,40 @@ def all_langs():
def find_wiki(arg): def find_wiki(arg):
for cand in [arg, os.environ.get("ORCA_WIKI_DIR"), REPO.parent / "OrcaSlicer_WIKI", REPO.parent / "OrcaSlicer.wiki"]: for cand in [arg, os.environ.get("ORCA_WIKI_DIR"), REPO.parent / "OrcaSlicer_WIKI", REPO.parent / "OrcaSlicer.wiki"]:
if cand and (Path(cand) / GLOSSARY_TSV).exists(): if cand and (Path(cand) / GLOSSARY_DIR / "terms.tsv").exists():
return Path(cand) return Path(cand)
return None return None
def _download(rel): def _download(rel, required=True):
cache = Path(tempfile.gettempdir()) / ("orca_glossary_" + rel.name) cache = Path(tempfile.gettempdir()) / "orca_glossary" / rel
if not cache.exists() or time.time() - cache.stat().st_mtime > 3600: if not cache.exists() or time.time() - cache.stat().st_mtime > 3600:
with urllib.request.urlopen(RAW_URL + rel.as_posix(), timeout=30) as r: try:
cache.write_bytes(r.read()) with urllib.request.urlopen(RAW_URL + rel.as_posix(), timeout=30) as r:
data = r.read()
except urllib.error.HTTPError:
if required:
raise
return None
cache.parent.mkdir(parents=True, exist_ok=True)
cache.write_bytes(data)
return cache return cache
class Glossary: class Glossary:
def __init__(self, wiki_arg=None, quiet=False): """The glossary: rows of [English, Description, <one translation per language loaded>]."""
def __init__(self, wiki_arg=None, quiet=False, langs=None):
self.wiki = find_wiki(wiki_arg) self.wiki = find_wiki(wiki_arg)
self.langs = langs # only these languages' files are read (and downloaded); None means all
if self.wiki: if self.wiki:
self.tsv, self.md = self.wiki / GLOSSARY_TSV, self.wiki / GLOSSARY_MD self.dir, self.md = self.wiki / GLOSSARY_DIR, self.wiki / GLOSSARY_MD
if not quiet: if not quiet:
self.report_clone_state() self.report_clone_state()
else: else:
if not quiet: if not quiet:
print("note: no OrcaSlicer_WIKI clone found; using the published glossary read-only", file=sys.stderr) print("note: no OrcaSlicer_WIKI clone found; using the published glossary read-only", file=sys.stderr)
self.tsv, self.md = _download(GLOSSARY_TSV), _download(GLOSSARY_MD) self.dir, self.md = _download(GLOSSARY_DIR / "terms.tsv").parent, _download(GLOSSARY_MD)
self.load() self.load()
def report_clone_state(self): def report_clone_state(self):
@@ -290,11 +301,23 @@ class Glossary:
if behind and behind != "0": if behind and behind != "0":
print(f"note: the wiki clone is {behind} commits behind origin/main (as of its last fetch)", file=sys.stderr) print(f"note: the wiki clone is {behind} commits behind origin/main (as of its last fetch)", file=sys.stderr)
def _read(self, name, required=True):
path = self.dir / name if self.wiki else _download(GLOSSARY_DIR / name, required)
return path.read_text(encoding="utf-8-sig").splitlines() if path and path.exists() else None
def load(self): def load(self):
rows = [l.split("\t") for l in self.tsv.read_text(encoding="utf-8-sig").splitlines() if l.strip()] terms = [l.split(" ") for l in self._read("terms.tsv")[1:] if l.strip()]
self.header = rows[0] langs = [l.strip() for l in self._read("languages.txt") if l.strip()]
# "—" marks a language with no established rendering yet langs = [l for l in langs if self.langs is None or l in self.langs]
self.rows = [[c if c.strip() not in MISSING else "" for c in r] + [""] * (len(rows[0]) - len(r)) for r in rows[1:]] self.header = ["English", "Description"] + langs
self.rows = [[r[0], r[1] if len(r) > 1 else ""] + [""] * len(langs) for r in terms]
index = {r[0]: r for r in self.rows}
for col, lang in enumerate(langs, start=2):
for line in (self._read(f"{lang}.tsv", required=False) or [])[1:]:
cells = line.split(" ")
# "—" marks a language with no established rendering yet
if len(cells) > 2 and cells[0] in index and cells[2].strip() not in MISSING:
index[cells[0]][col] = cells[2].strip()
self.matchers = [] self.matchers = []
for row in self.rows: for row in self.rows:
base = re.sub(r"\s*\([^)]*\)\s*$", "", row[0]).strip() base = re.sub(r"\s*\([^)]*\)\s*$", "", row[0]).strip()
@@ -494,7 +517,7 @@ def similar_entries(c, e, pool, cutoff=0.72):
def cmd_pending(a): def cmd_pending(a):
c = Catalog(catalog_path(a.lang)) c = Catalog(catalog_path(a.lang))
g = Glossary(a.wiki) g = Glossary(a.wiki, langs=[a.lang])
todo = [e for e in c.active() if needs_translation(e, c.nplurals, a.include_fuzzy)] todo = [e for e in c.active() if needs_translation(e, c.nplurals, a.include_fuzzy)]
batch = todo[a.offset:a.offset + a.limit] if a.limit else todo[a.offset:] batch = todo[a.offset:a.offset + a.limit] if a.limit else todo[a.offset:]
pool = [(x.msgid, x.msgstrs()[0], x.obsolete) for x in c.entries pool = [(x.msgid, x.msgstrs()[0], x.obsolete) for x in c.entries
@@ -533,7 +556,7 @@ def cmd_pending(a):
def cmd_apply(a): def cmd_apply(a):
c = Catalog(catalog_path(a.lang)) c = Catalog(catalog_path(a.lang))
g = None if a.no_glossary else Glossary(a.wiki, quiet=True) g = None if a.no_glossary else Glossary(a.wiki, quiet=True, langs=[a.lang])
data = json.loads(Path(a.input).read_text(encoding="utf-8")) data = json.loads(Path(a.input).read_text(encoding="utf-8"))
items = data["entries"] if isinstance(data, dict) else data items = data["entries"] if isinstance(data, dict) else data
idx = c.index() idx = c.index()
@@ -588,7 +611,7 @@ def find_msgfmt():
def cmd_check(a): def cmd_check(a):
g = None if a.no_glossary else Glossary(a.wiki, quiet=True) g = None if a.no_glossary else Glossary(a.wiki, quiet=True, langs=a.lang or None)
msgfmt = find_msgfmt() msgfmt = find_msgfmt()
status = 0 status = 0
for lang in a.lang or all_langs(): for lang in a.lang or all_langs():
@@ -646,7 +669,7 @@ def cmd_lookup(a):
def cmd_glossary(a): def cmd_glossary(a):
g = Glossary(a.wiki) g = Glossary(a.wiki, langs=[a.lang])
col = g.column(a.lang) col = g.column(a.lang)
if col is None: if col is None:
print(f"'{a.lang}' has no glossary column yet; add one with glossary-set --set {a.lang}=...") print(f"'{a.lang}' has no glossary column yet; add one with glossary-set --set {a.lang}=...")
@@ -733,6 +756,17 @@ def cmd_candidates(a):
print(f"{cnt:7} {langs:6} {text}{' [label]' if k in labels else ''} e.g. {ex[:70]}{'…' if len(ex) > 70 else ''}") print(f"{cnt:7} {langs:6} {text}{' [label]' if k in labels else ''} e.g. {ex[:70]}{'…' if len(ex) > 70 else ''}")
def _edit_tsv(path, edit):
"""Apply edit(lines) to a glossary TSV, keeping its BOM and line endings."""
data = path.read_bytes()
bom = data.startswith(b"\xef\xbb\xbf")
text = data.decode("utf-8-sig")
nl = "\r\n" if "\r\n" in text else "\n"
lines = text.splitlines()
edit(lines)
path.write_bytes((b"\xef\xbb\xbf" if bom else b"") + (nl.join(lines) + nl).encode("utf-8"))
def cmd_glossary_set(a): def cmd_glossary_set(a):
wiki = find_wiki(a.wiki) wiki = find_wiki(a.wiki)
if not wiki: if not wiki:
@@ -741,8 +775,21 @@ def cmd_glossary_set(a):
f" {a.term}: " + ", ".join(a.set) + "\n" f" {a.term}: " + ", ".join(a.set) + "\n"
"Clone https://github.com/OrcaSlicer/OrcaSlicer_WIKI next to this repository, or pass --wiki.", file=sys.stderr) "Clone https://github.com/OrcaSlicer/OrcaSlicer_WIKI next to this repository, or pass --wiki.", file=sys.stderr)
return 2 return 2
tsv = wiki / GLOSSARY_TSV gdir = wiki / GLOSSARY_DIR
lock = tsv.with_suffix(".lock") gen = wiki / "generate_glossary.py"
if not gen.exists():
sys.exit(f"{gen} not found")
run_gen = lambda *args: subprocess.run([sys.executable, str(gen), *args], cwd=wiki, capture_output=True, text=True)
langs = [l.strip() for l in (gdir / "languages.txt").read_text(encoding="utf-8-sig").splitlines() if l.strip()]
values = {}
for s in a.set:
lang, _, val = s.partition("=")
if lang not in langs:
sys.exit(f"no glossary language '{lang}' (add it to {GLOSSARY_DIR.as_posix()}/languages.txt)")
if "\t" in val or "\n" in val:
sys.exit("values cannot contain tabs or line breaks")
values[lang] = val.strip()
lock = gdir / ".lock"
for _ in range(100): for _ in range(100):
try: try:
fd = os.open(lock, os.O_CREAT | os.O_EXCL) fd = os.open(lock, os.O_CREAT | os.O_EXCL)
@@ -752,50 +799,51 @@ def cmd_glossary_set(a):
else: else:
sys.exit(f"could not lock {lock}; remove it if no other run is active") sys.exit(f"could not lock {lock}; remove it if no other run is active")
try: try:
data = tsv.read_bytes() terms = (gdir / "terms.tsv").read_text(encoding="utf-8-sig").splitlines()
bom = data.startswith(b"\xef\xbb\xbf") new = not any(l.split("\t")[0] == a.term for l in terms[1:])
text = data.decode("utf-8-sig") if new and not a.description:
nl = "\r\n" if "\r\n" in text else "\n" sys.exit("a new term needs --description (one sentence saying what it is)")
lines = text.splitlines() # an established translation is only replaced on request
header = lines[0].split("\t") conflicts = []
values = {} for lang, v in values.items():
for s in a.set: for line in (gdir / f"{lang}.tsv").read_text(encoding="utf-8-sig").splitlines()[1:]:
lang, _, val = s.partition("=") cells = line.split("\t")
if lang not in header: if cells[0] == a.term and len(cells) > 2 and cells[2].strip() not in MISSING and cells[2] != v:
sys.exit(f"no glossary column '{lang}'") conflicts.append(f"{lang}: '{cells[2]}' -> '{v}'")
if "\t" in val or "\n" in val:
sys.exit("values cannot contain tabs or line breaks")
values[header.index(lang)] = val.strip()
row_i = next((i for i, l in enumerate(lines[1:], 1) if l.split("\t")[0] == a.term), None)
new = row_i is None
if new:
if not a.description:
sys.exit("a new term needs --description (one sentence saying what it is)")
row = [a.term, a.description] + [""] * (len(header) - 2)
lines.append("")
row_i = len(lines) - 1
else:
row = lines[row_i].split("\t")
row += [""] * (len(header) - len(row))
if a.description:
row[1] = a.description
conflicts = [f"{header[i]}: '{row[i]}' -> '{v}'" for i, v in values.items() if row[i].strip() not in MISSING and row[i] != v]
if conflicts and not a.replace: if conflicts and not a.replace:
sys.exit("established translations differ (pass --replace, and update the catalogs to match):\n " + "\n ".join(conflicts)) sys.exit("established translations differ (pass --replace, and update the catalogs to match):\n " + "\n ".join(conflicts))
for i, v in values.items():
row[i] = v def edit_terms(lines):
lines[row_i] = "\t".join(row) for i, l in enumerate(lines):
tsv.write_bytes((b"\xef\xbb\xbf" if bom else b"") + (nl.join(lines) + nl).encode("utf-8")) if i and l.split("\t")[0] == a.term:
if a.description:
lines[i] = f"{a.term}\t{a.description}"
return
lines.append(f"{a.term}\t{a.description}")
if new or a.description:
_edit_tsv(gdir / "terms.tsv", edit_terms)
r = run_gen("--sync") # copies the term and description into every language file
if r.returncode:
sys.exit((r.stdout + r.stderr).strip())
def setter(lang, v):
def edit(lines):
for i, l in enumerate(lines):
cells = l.split("\t")
if i and cells[0] == a.term:
lines[i] = "\t".join(cells[:2] + [v])
return
sys.exit(f"{lang}.tsv has no row for '{a.term}'")
return edit
for lang, v in values.items():
_edit_tsv(gdir / f"{lang}.tsv", setter(lang, v))
finally: finally:
os.close(fd) os.close(fd)
os.unlink(lock) os.unlink(lock)
print(f"glossary: {'added' if new else 'updated'} '{a.term}'") print(f"glossary: {'added' if new else 'updated'} '{a.term}'")
gen = wiki / "generate_glossary.py" r = run_gen("--update")
if gen.exists(): print((r.stdout + r.stderr).strip())
r = subprocess.run([sys.executable, str(gen), "--update"], cwd=wiki, capture_output=True, text=True) return r.returncode
print((r.stdout + r.stderr).strip())
return r.returncode
return 0
def main(): def main():
@@ -574,7 +574,8 @@ DMABUF workaround and then branches on `GDK_BACKEND` (an `x11` prefix means the
paths, when `/proc/driver/nvidia/version` exists and the linked WebKitGTK honours the switch (>= 2.44, 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 `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 `WEBKIT_FORCE_DMABUF_RENDERER=1` (non-replacing): the proprietary driver cannot back WebKit's hardware
DMABUF renderer. On the default path it forces `GDK_BACKEND=x11` when wx lacks EGL and `WAYLAND_DISPLAY` 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 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 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 that runs through XWayland, which `DISPLAY` and `WAYLAND_DISPLAY` alone do not tell you (a native
@@ -568,13 +568,18 @@ vue-resize workaround script and sets a persistent SQLite cookie store on the vi
## WebKitGTK on Linux sessions ## WebKitGTK on Linux sessions
**Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises) **Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises)
sets the NVIDIA DMABUF workaround and then 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 - **Both paths**, before the split: when `/proc/driver/nvidia/version` exists and the linked WebKitGTK
honours the switch (>= 2.44, `WebView::SupportsForceSharedMemory()`), it sets honours the switch (>= 2.44, `WebView::SupportsForceSharedMemory()`), it sets
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WEBKIT_FORCE_DMABUF_RENDERER=1` non-replacing. The `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 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 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. 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 - **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`, and in that same branch it sets `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 `WEBKIT_DISABLE_COMPOSITING_MODE=1` non-replacing when the linked WebKitGTK predates 2.42
@@ -590,9 +595,9 @@ Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_
(see `references/platforms.md`), not these environment variables. (see `references/platforms.md`), not these environment variables.
**Pitfall.** **Pitfall.**
- **Rule:** On NVIDIA reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before - **Rule:** On NVIDIA and hypervisor guests reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver cannot back WebKit's hardware `WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver and virtual GPUs cannot back WebKit's
DMABUF renderer, leaving web views blank or slow; FORCE_SHM keeps accelerated compositing and only 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, 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 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 substitute — it is read first, leaves the transport set empty (no compositing from 2.44 on, where the
+25
View File
@@ -232,6 +232,31 @@ jobs:
mkdir -p ${{ runner.temp }}/profile-check-results mkdir -p ${{ runner.temp }}/profile-check-results
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt 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 - 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') }} 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: | run: |
+58 -3
View File
@@ -38,6 +38,7 @@ jobs:
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }} GH_REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: | run: |
if [ ! -f pr_number.txt ]; then if [ ! -f pr_number.txt ]; then
echo "No pr_number.txt in artifact, skipping." echo "No pr_number.txt in artifact, skipping."
@@ -49,18 +50,72 @@ jobs:
echo "Invalid PR number: $PR_NUMBER" echo "Invalid PR number: $PR_NUMBER"
exit 1 exit 1
fi 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. # 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" \ OLD_IDS=$(printf '%s\n' "$COMMENTS" | jq -r 'select((.body | startswith("<!-- profile-validation-comment -->")) or (.body | startswith("## :x: Profile Validation Errors"))) | .id')
--jq '.[] | select(.user.login == "github-actions[bot]") | select((.body | startswith("<!-- profile-validation-comment -->")) or (.body | startswith("## :x: Profile Validation Errors"))) | .id')
for comment_id in $OLD_IDS; do for comment_id in $OLD_IDS; do
echo "Deleting outdated profile-validation comment ${comment_id}" echo "Deleting outdated profile-validation comment ${comment_id}"
gh api -X DELETE "repos/${GH_REPO}/issues/comments/${comment_id}" || true gh api -X DELETE "repos/${GH_REPO}/issues/comments/${comment_id}" || true
done done
# Post a new comment only when validation failed (pr_comment.md present). # 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 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 else
echo "Validation succeeded; cleaned up prior comments without posting." echo "Validation succeeded; cleaned up prior comments without posting."
fi 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"
+1 -1
View File
@@ -91,7 +91,7 @@ See `.claude/skills/orca-translations/references/source-audit.md` for the full c
### Terminology ### Terminology
- Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated. - Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated.
- If a term's established translation changes, update both the affected `.po` files and the glossary (`localization_glossary.tsv`, then regenerate) so they stay in sync. - If a term's established translation changes, update both the affected `.po` files and the glossary (`developer_reference/glossary/<lang>.tsv` in the wiki, then regenerate) so they stay in sync.
- Translate the *meaning*, not the words. Check what the string actually controls before translating it — English reuses one word for different things. `Flow ratio` (multiplier), `Flow Rate` (throughput) and `Flow Dynamics` (pressure compensation) are three different terms; `extruder` may mean the toolhead, the feeder motor, or the nozzle depending on the string. - Translate the *meaning*, not the words. Check what the string actually controls before translating it — English reuses one word for different things. `Flow ratio` (multiplier), `Flow Rate` (throughput) and `Flow Dynamics` (pressure compensation) are three different terms; `extruder` may mean the toolhead, the feeder motor, or the nozzle depending on the string.
- Reuse one template per recurring message shape (`Failed to connect to …`, `Are you sure you want to …?`), even where the English wording varies. - Reuse one template per recurring message shape (`Failed to connect to …`, `Are you sure you want to …?`), even where the English wording varies.
+13
View File
@@ -288,6 +288,19 @@ if (APPLE)
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts. # Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts.
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO") set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO")
# The Xcode generator ignores CMAKE_<LANG>_COMPILER_LAUNCHER, so hand the launcher to
# Xcode's own Compiler Launcher setting, which wraps every clang compile (C, C++,
# Objective-C++ and the precompiled header) with one program: the C++ launcher wins when
# both are set. The setting takes a single program path, not a CMake launcher list.
# Xcode versions without it ignore it and build uncached.
if (CMAKE_GENERATOR STREQUAL "Xcode")
if (CMAKE_CXX_COMPILER_LAUNCHER)
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_CXX_COMPILER_LAUNCHER}")
elseif (CMAKE_C_COMPILER_LAUNCHER)
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_C_COMPILER_LAUNCHER}")
endif ()
endif ()
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}") message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(CMAKE_INSTALL_RPATH "$ORIGIN") set(CMAKE_INSTALL_RPATH "$ORIGIN")
+50
View File
@@ -111,6 +111,54 @@ else
export CMAKE_POLICY_COMPAT="" export CMAKE_POLICY_COMPAT=""
fi fi
# Compiler cache, as in build_linux.sh. The program comes from CMAKE_CCACHE, else from a
# launcher already set in the environment (the CI build sets ccache there), else sccache or
# ccache from PATH; one program serves both languages. Each run applies its own choice to the
# build directories, replacing a launcher an earlier run or a hand-run configure recorded.
CMAKE_CCACHE_ARGS=()
CCACHE_VAR=""
for var in CMAKE_CCACHE CMAKE_CXX_COMPILER_LAUNCHER CMAKE_C_COMPILER_LAUNCHER; do
if [ -n "${!var:-}" ]; then
CCACHE_VAR=$var
break
fi
done
if [ -n "$CCACHE_VAR" ]; then
CCACHE_PROGRAM=${!CCACHE_VAR}
echo "Checking ${CCACHE_VAR} environment variable for compiler cache program..."
CMAKE_CCACHE=$(command -v "${CCACHE_PROGRAM}") || {
echo "${CCACHE_VAR} environment variable is set to '${CCACHE_PROGRAM}' but it was not found in PATH."
CMAKE_CCACHE=""
}
elif command -v sccache >/dev/null 2>&1 ; then
CMAKE_CCACHE=$(command -v sccache)
elif command -v ccache >/dev/null 2>&1 ; then
CMAKE_CCACHE=$(command -v ccache)
fi
if [ -n "${CMAKE_CCACHE}" ] ; then
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
# The same ccache settings as the CI build. Without this sloppiness ccache refuses every
# compile that uses the precompiled header, which is most of OrcaSlicer, and skips headers
# a checkout has just written. It is added to the sloppiness already configured, and
# CCACHE_SLOPPINESS or CCACHE_(NO)DEPEND set in the environment win.
if [[ "$(basename "${CMAKE_CCACHE}")" == ccache ]] ; then
if [[ -z "${CCACHE_SLOPPINESS+x}" ]] ; then
CCACHE_SLOPPINESS=$("${CMAKE_CCACHE}" --get-config sloppiness 2>/dev/null || true)
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:+${CCACHE_SLOPPINESS},}pch_defines,time_macros,include_file_mtime,include_file_ctime"
fi
# Hash the includes the compiler reports instead of preprocessing every miss.
if [[ -z "${CCACHE_DEPEND+x}${CCACHE_NODEPEND+x}" ]] ; then
export CCACHE_DEPEND=1
fi
fi
else
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
# Clear a launcher recorded by an earlier configure. The entries must be set empty rather
# than removed: CMake fills a missing launcher entry from the environment.
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER:STRING= -DCMAKE_CXX_COMPILER_LAUNCHER:STRING=)
fi
echo "Build params:" echo "Build params:"
echo " - ARCH: $ARCH" echo " - ARCH: $ARCH"
echo " - BUILD_CONFIG: $BUILD_CONFIG" echo " - BUILD_CONFIG: $BUILD_CONFIG"
@@ -162,6 +210,7 @@ function build_deps() {
-DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \ -DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \ -DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \ -DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT} ${CMAKE_POLICY_COMPAT}
fi fi
cmake --build . --config "$BUILD_CONFIG" --target deps cmake --build . --config "$BUILD_CONFIG" --target deps
@@ -264,6 +313,7 @@ function build_slicer() {
-DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \ -DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \ -DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \ -DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT} ${CMAKE_POLICY_COMPAT}
fi fi
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET" cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
+17 -7
View File
@@ -16,6 +16,8 @@ else()
endif() endif()
endif() endif()
set(_openssl_dir ${DESTDIR})
if(WIN32) if(WIN32)
set(_openssl_msvc_env CC=cl CXX=cl RC=rc CL=/FS) set(_openssl_msvc_env CC=cl CXX=cl RC=rc CL=/FS)
# OpenSSL's perl Configure honors the CC environment variable, but the # OpenSSL's perl Configure honors the CC environment variable, but the
@@ -43,6 +45,14 @@ if(WIN32)
else() else()
if(APPLE) if(APPLE)
set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}) set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})
# OPENSSLDIR is only where OpenSSL looks at runtime for cert.pem, certs/
# and openssl.cnf; install_sw installs nothing there. Point it at the
# system CA bundle, the one libcurl already uses here, so the bundled
# Python's ssl module can verify HTTPS. Elsewhere it stays the prefix:
# Windows Python reads the system certificate store, and on Linux, where
# the bundle's location differs by distro, CurlGlobalInit points
# SSL_CERT_FILE at it at startup.
set(_openssl_dir /etc/ssl)
else() else()
# A static library that is embedded into a shared object must not export # A static library that is embedded into a shared object must not export
# its symbols. On Linux the running process also loads the system OpenSSL # its symbols. On Linux the running process also loads the system OpenSSL
@@ -81,7 +91,7 @@ ExternalProject_Add(dep_OpenSSL
# URL_HASH SHA256=8c776993154652d0bb393f506d850b811517c8bd8d24b1008aef57fbe55d3f31 # URL_HASH SHA256=8c776993154652d0bb393f506d850b811517c8bd8d24b1008aef57fbe55d3f31
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/OpenSSL DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/OpenSSL
CONFIGURE_COMMAND ${_conf_cmd} ${_cross_arch} CONFIGURE_COMMAND ${_conf_cmd} ${_cross_arch}
"--openssldir=${DESTDIR}" "--openssldir=${_openssl_dir}"
"--prefix=${DESTDIR}" "--prefix=${DESTDIR}"
# OpenSSL's linux-x86_64 target sets multilib=64, so it installs to # OpenSSL's linux-x86_64 target sets multilib=64, so it installs to
# <prefix>/lib64 while every other dep uses <prefix>/lib. CPython's # <prefix>/lib64 while every other dep uses <prefix>/lib. CPython's
@@ -115,12 +125,12 @@ ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
) )
if (NOT WIN32 AND NOT APPLE) if (NOT WIN32)
# OpenSSL's object rules do not depend on CFLAGS, so reconfiguring it (for # OpenSSL's object rules do not depend on the configured flags or paths, so
# example to add -fvisibility=hidden) relinks the archives from stale # reconfiguring it (a new compiler flag, a different --openssldir) relinks
# objects instead of recompiling them, and the change silently has no # the archives from stale objects instead of recompiling them, and the
# effect. Drop the objects whenever this recipe changes so the next build # change silently has no effect. Drop the objects whenever this recipe
# actually recompiles them. # changes so the next build actually recompiles them.
ExternalProject_Get_Property(dep_OpenSSL SOURCE_DIR) ExternalProject_Get_Property(dep_OpenSSL SOURCE_DIR)
ExternalProject_Add_Step(dep_OpenSSL clean_objects ExternalProject_Add_Step(dep_OpenSSL clean_objects
DEPENDEES configure DEPENDEES configure
+5 -4
View File
@@ -365,10 +365,11 @@ if(TARGET dep_ZLIB)
add_dependencies(dep_python3 dep_ZLIB) add_dependencies(dep_python3 dep_ZLIB)
endif() endif()
if (NOT WIN32 AND NOT APPLE) if (NOT WIN32)
# CPython's Makefile rules for _ssl and _hashlib depend only on their own # _ssl and _hashlib embed the static OpenSSL built in deps/ (Windows uses
# sources, not on the OpenSSL archives, so a rebuilt OpenSSL does not make # CPython's prebuilt OpenSSL instead). Their Makefile rules depend only on
# them relink and they keep the previous symbols. On an incremental tree, # their own sources, not on the OpenSSL archives, so a rebuilt OpenSSL does
# not make them relink and they keep the previous one. On an incremental tree,
# drop the built modules and relink them against the current OpenSSL; a # drop the built modules and relink them against the current OpenSSL; a
# fresh build is left alone (its PGO target builds them). "make" alone is a # fresh build is left alone (its PGO target builds them). "make" alone is a
# no-op once PGO has run, so sharedmods is invoked explicitly. # no-op once PGO has run, so sharedmods is invoked explicitly.
@@ -1,8 +1,8 @@
# Adaptive TPMS infill — High Level Design # Adaptive infill — High Level Design
## Purpose and scope ## Purpose and scope
`tpms_adaptive` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK `adaptive_infill_mode` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
patterns inside the object: the cells grow continuously from the surface patterns inside the object: the cells grow continuously from the surface
towards the center of the object. `distance_warp`, `smooth_blend` and towards the center of the object. `distance_warp`, `smooth_blend` and
`stepped_shells` follow the distance to the nearest surface, including the top `stepped_shells` follow the distance to the nearest surface, including the top
@@ -10,8 +10,8 @@ and bottom, like concentric shells; `lobes` follows the whole 3D shape towards
the center of each lobe of the object; `normal_z`, `normal_y` and `normal_x` the center of each lobe of the object; `normal_z`, `normal_y` and `normal_x`
follow each section of the object normal to that axis, so the grading does not follow each section of the object normal to that axis, so the grading does not
change along the axis, as suits a profile extruded along it. change along the axis, as suits a profile extruded along it.
`sparse_infill_density` is the density at the surface, `tpms_interior_density` `sparse_infill_density` is the density at the surface, `adaptive_infill_interior_density`
the density at the center, and `tpms_adaptive_gradient` picks how the density the density at the center, and `adaptive_infill_gradient` picks how the density
goes from one to the other. Only internal sparse infill is graded; the Gyroid goes from one to the other. Only internal sparse infill is graded; the Gyroid
Z-buckling optimization does not apply to it. Z-buckling optimization does not apply to it.
@@ -22,9 +22,9 @@ split into shells or blended between densities.
## Radial field ## Radial field
`TpmsRadialField` gives every point of an object the center of its lobe and a `AdaptiveInfillField` gives every point of an object the center of its lobe and a
radial coordinate: 0 at the center, 1 at the surface along the ray from the radial coordinate: 0 at the center, 1 at the surface along the ray from the
center. `PrintObject::prepare_tpms_radial_fields()` builds it in center. `PrintObject::prepare_adaptive_infill_fields()` builds it in
`bridge_over_infill()`, next to the adaptive cubic octree, because the anchoring `bridge_over_infill()`, next to the adaptive cubic octree, because the anchoring
infill generated there has to match the printed infill. A field is built for infill generated there has to match the printed infill. A field is built for
every mode a region uses, and is shared by the regions using that mode: the every mode a region uses, and is shared by the regions using that mode: the
@@ -202,7 +202,7 @@ built for them.
## Constraints ## Constraints
- With `tpms_adaptive` disabled, or for other patterns, the fill parameters are reset - With `adaptive_infill_mode` disabled, or for other patterns, the fill parameters are reset
to their defaults, so they neither change the infill nor split fill batches. to their defaults, so they neither change the infill nor split fill batches.
- `Layer::get_sparse_infill_max_void_area()` uses the sparser of the two - `Layer::get_sparse_infill_max_void_area()` uses the sparser of the two
densities, as the voids at the center are that large. densities, as the voids at the center are that large.
+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, `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. 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 ## Which event a chord matches
Letters, digits and special keys match on `wxEVT_KEY_DOWN`. Its key codes do not depend Letters, digits and special keys match on `wxEVT_KEY_DOWN`. Its key codes do not depend
+17
View File
@@ -62,6 +62,23 @@ Clipper2 behaves differently by default, the wrapper adjusts it.
clip do not leave slivers. clip do not leave slivers.
- Open polylines are clipped with the non-zero rule and keep their direction. - Open polylines are clipped with the non-zero rule and keep their direction.
### Tiled booleans
The sweep slows down with the number of edges crossing a scan line, so a layer
cut into thousands of pieces makes every whole-layer boolean expensive.
`diff_ex_by_piece()` and `intersection_ex_by_piece()` take a subject of
non-overlapping `ExPolygons`, group them into tiles with
`ClipperUtils::tile_expolygons()`, and run each tile in parallel against only
the clip polygons near it, cut to the tile's box. Below 128 pieces there is a
single tile, and they are the plain `diff_ex()` / `intersection_ex()`.
The result covers the same area as the plain call. Without the safety offset
the rings are the same. With it, each tile unites only the clip polygons near
it, so a clip edge that the whole-layer union splits where it crosses a distant
clip polygon stays whole, and a crossing with the subject can round 1 unit
differently. The tiles' results are concatenated in tile order, so the order of
the output `ExPolygons` differs from the plain call.
### Offsets ### Offsets
- Before offsetting, input vertices closer than - Before offsetting, input vertices closer than
+58 -21
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-15 17:40+0800\n" "POT-Creation-Date: 2026-10-19 09:48-0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -1637,16 +1637,16 @@ msgstr ""
msgid "Stroke of shape (%1%) contains unsupported: %2%." msgid "Stroke of shape (%1%) contains unsupported: %2%."
msgstr "" msgstr ""
msgid "Not valid state please report reproduction steps on github" msgid "Not valid state please report reproduction steps on GitHub"
msgstr "" msgstr ""
msgid "No embossed file" msgid "No embossed file"
msgstr "" msgstr ""
msgid "Missing svg file in embossed shape" msgid "Missing SVG file in embossed shape"
msgstr "" msgstr ""
msgid "Missing data of svg file" msgid "Missing data of SVG file"
msgstr "" msgstr ""
msgid "Face the camera" msgid "Face the camera"
@@ -4579,7 +4579,7 @@ msgstr ""
msgid "Unknown C++ exception." msgid "Unknown C++ exception."
msgstr "" msgstr ""
msgid "Processing G-Code from previous file…" msgid "Processing G-code from previous file…"
msgstr "" msgstr ""
msgid "Slicing complete" msgid "Slicing complete"
@@ -6049,7 +6049,7 @@ msgstr ""
msgid "Size:" msgid "Size:"
msgstr "" msgstr ""
#, possible-boost-format #, possible-c-format, possible-boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "" msgstr ""
@@ -10657,6 +10657,9 @@ msgstr ""
msgid "Send Failed" msgid "Send Failed"
msgstr "" msgstr ""
msgid "No available printers"
msgstr ""
msgid "The name length exceeds the limit." msgid "The name length exceeds the limit."
msgstr "" msgstr ""
@@ -12315,7 +12318,7 @@ msgstr ""
msgid "Left mouse" msgid "Left mouse"
msgstr "" msgstr ""
msgid "Available anywhere in the window, even while typing in a text field." msgid "Available anywhere in the window."
msgstr "" msgstr ""
msgid "Run favorite 1 to 10" msgid "Run favorite 1 to 10"
@@ -12412,9 +12415,6 @@ msgstr ""
msgid "The default %s is assigned to %s. Reassign it to %s?" msgid "The default %s is assigned to %s. Reassign it to %s?"
msgstr "" msgstr ""
msgid "Global shortcuts also apply while typing."
msgstr ""
msgid "A key that types a character cannot be a global shortcut." msgid "A key that types a character cannot be a global shortcut."
msgstr "" msgstr ""
@@ -15645,6 +15645,12 @@ msgstr ""
msgid "Enable this option to add EXCLUDE OBJECT command in G-code." msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
msgstr "" msgstr ""
msgid "Outline tolerance"
msgstr ""
msgid "Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the original outline. Larger values give fewer points, which helps firmware that stalls while parsing G-code with many objects. 0 keeps every point."
msgstr ""
msgid "Verbose G-code" msgid "Verbose G-code"
msgstr "" msgstr ""
@@ -20540,7 +20546,7 @@ msgid "test %s start..."
msgstr "" msgstr ""
#, possible-c-format, possible-boost-format #, possible-c-format, possible-boost-format
msgid "test %s ip resolved = %s" msgid "test %s IP resolved = %s"
msgstr "" msgstr ""
#, possible-c-format, possible-boost-format #, possible-c-format, possible-boost-format
@@ -26492,17 +26498,18 @@ msgstr ""
msgid "Basic Colors" msgid "Basic Colors"
msgstr "" msgstr ""
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "Orca Palette" msgid "Orca Palette"
msgstr "" msgstr ""
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor. #. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "RAL Classic" msgid "RAL Classic"
msgstr "" msgstr ""
#. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "Gradients" msgid "Gradients"
msgstr "" msgstr ""
msgctxt "Color picker"
msgid "Solid" msgid "Solid"
msgstr "" msgstr ""
@@ -26877,7 +26884,7 @@ msgstr ""
msgid "Medium violet red" msgid "Medium violet red"
msgstr "" msgstr ""
msgid "PaleV violet red" msgid "Pale violet red"
msgstr "" msgstr ""
msgid "Deep pink" msgid "Deep pink"
@@ -27039,7 +27046,7 @@ msgstr ""
msgid "Shallow sky" msgid "Shallow sky"
msgstr "" msgstr ""
msgid "Electric bel" msgid "Electric blue"
msgstr "" msgstr ""
msgid "Dolphin" msgid "Dolphin"
@@ -27108,7 +27115,7 @@ msgstr ""
msgid "Maize yellow" msgid "Maize yellow"
msgstr "" msgstr ""
msgid "Dalmatian yellow" msgid "Daffodil yellow"
msgstr "" msgstr ""
msgid "Brown beige" msgid "Brown beige"
@@ -27369,7 +27376,7 @@ msgstr ""
msgid "Turquoise blue" msgid "Turquoise blue"
msgstr "" msgstr ""
msgid "Capsanthin violet" msgid "Capri blue"
msgstr "" msgstr ""
msgid "Ocean blue" msgid "Ocean blue"
@@ -27483,7 +27490,7 @@ msgstr ""
msgid "Silver grey" msgid "Silver grey"
msgstr "" msgstr ""
msgid "Olivestone" msgid "Olive grey"
msgstr "" msgstr ""
msgid "Moss grey" msgid "Moss grey"
@@ -28071,10 +28078,19 @@ msgstr ""
msgid "Built-in" msgid "Built-in"
msgstr "" msgstr ""
msgid "My textures"
msgstr ""
msgid "No textures found." msgid "No textures found."
msgstr "" msgstr ""
msgid "My textures" msgid "Texture libraries"
msgstr ""
msgid "Explore more texture libraries"
msgstr ""
msgid "Opens OrcaCloud in the Home page to browse for plugins."
msgstr "" msgstr ""
msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer." msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer."
@@ -30127,6 +30143,9 @@ msgstr ""
msgid "Slicing Pipeline" msgid "Slicing Pipeline"
msgstr "" msgstr ""
msgid "Resource Texture"
msgstr ""
msgid "The plugin's configuration UI was empty. Showing the default editor." msgid "The plugin's configuration UI was empty. Showing the default editor."
msgstr "" msgstr ""
@@ -30242,6 +30261,24 @@ msgstr ""
msgid "Plugin directory does not contain a .py or .whl entry file" msgid "Plugin directory does not contain a .py or .whl entry file"
msgstr "" msgstr ""
msgid "Resource texture directory is not set"
msgstr ""
msgid "Resource texture directory could not be resolved"
msgstr ""
msgid "Resource texture path is not a directory"
msgstr ""
msgid "Plugin folder could not be resolved"
msgstr ""
msgid "Resource texture directory is outside the plugin folder"
msgstr ""
msgid "Resource texture directory could not be read"
msgstr ""
#, possible-boost-format #, possible-boost-format
msgid "Slicing pipeline plugin '%1%' error: %2%" msgid "Slicing pipeline plugin '%1%' error: %2%"
msgstr "" msgstr ""
@@ -30598,7 +30635,7 @@ msgstr ""
#. TRN %1% is the application name. #. TRN %1% is the application name.
#, possible-boost-format #, possible-boost-format
msgid "Not a gcode file generated by %1%." msgid "Not a G-code file generated by %1%."
msgstr "" msgstr ""
#. TRN %1% is a tag in the G-code file that must not be translated. #. TRN %1% is a tag in the G-code file that must not be translated.
@@ -30686,7 +30723,7 @@ msgstr ""
msgid "Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%" msgid "Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%"
msgstr "" msgstr ""
msgid "Post-processor can't find exported gcode file" msgid "Post-processor can't find exported G-code file"
msgstr "" msgstr ""
#, possible-boost-format #, possible-boost-format
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+58 -21
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-15 17:40+0800\n" "POT-Creation-Date: 2026-10-19 09:48-0300\n"
"PO-Revision-Date: 2026-06-17 15:44-0300\n" "PO-Revision-Date: 2026-06-17 15:44-0300\n"
"Last-Translator: Alexandre Folle de Menezes\n" "Last-Translator: Alexandre Folle de Menezes\n"
"Language-Team: \n" "Language-Team: \n"
@@ -1633,16 +1633,16 @@ msgstr ""
msgid "Stroke of shape (%1%) contains unsupported: %2%." msgid "Stroke of shape (%1%) contains unsupported: %2%."
msgstr "" msgstr ""
msgid "Not valid state please report reproduction steps on github" msgid "Not valid state please report reproduction steps on GitHub"
msgstr "" msgstr ""
msgid "No embossed file" msgid "No embossed file"
msgstr "" msgstr ""
msgid "Missing svg file in embossed shape" msgid "Missing SVG file in embossed shape"
msgstr "" msgstr ""
msgid "Missing data of svg file" msgid "Missing data of SVG file"
msgstr "" msgstr ""
msgid "Face the camera" msgid "Face the camera"
@@ -4575,7 +4575,7 @@ msgstr ""
msgid "Unknown C++ exception." msgid "Unknown C++ exception."
msgstr "" msgstr ""
msgid "Processing G-Code from previous file…" msgid "Processing G-code from previous file…"
msgstr "" msgstr ""
msgid "Slicing complete" msgid "Slicing complete"
@@ -6045,7 +6045,7 @@ msgstr ""
msgid "Size:" msgid "Size:"
msgstr "" msgstr ""
#, boost-format #, c-format, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "" msgstr ""
@@ -10653,6 +10653,9 @@ msgstr ""
msgid "Send Failed" msgid "Send Failed"
msgstr "" msgstr ""
msgid "No available printers"
msgstr ""
msgid "The name length exceeds the limit." msgid "The name length exceeds the limit."
msgstr "" msgstr ""
@@ -12311,7 +12314,7 @@ msgstr ""
msgid "Left mouse" msgid "Left mouse"
msgstr "" msgstr ""
msgid "Available anywhere in the window, even while typing in a text field." msgid "Available anywhere in the window."
msgstr "" msgstr ""
msgid "Run favorite 1 to 10" msgid "Run favorite 1 to 10"
@@ -12408,9 +12411,6 @@ msgstr ""
msgid "The default %s is assigned to %s. Reassign it to %s?" msgid "The default %s is assigned to %s. Reassign it to %s?"
msgstr "" msgstr ""
msgid "Global shortcuts also apply while typing."
msgstr ""
msgid "A key that types a character cannot be a global shortcut." msgid "A key that types a character cannot be a global shortcut."
msgstr "" msgstr ""
@@ -15641,6 +15641,12 @@ msgstr ""
msgid "Enable this option to add EXCLUDE OBJECT command in G-code." msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
msgstr "" msgstr ""
msgid "Outline tolerance"
msgstr ""
msgid "Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the original outline. Larger values give fewer points, which helps firmware that stalls while parsing G-code with many objects. 0 keeps every point."
msgstr ""
msgid "Verbose G-code" msgid "Verbose G-code"
msgstr "" msgstr ""
@@ -20536,7 +20542,7 @@ msgid "test %s start..."
msgstr "" msgstr ""
#, c-format, boost-format #, c-format, boost-format
msgid "test %s ip resolved = %s" msgid "test %s IP resolved = %s"
msgstr "" msgstr ""
#, c-format, boost-format #, c-format, boost-format
@@ -26488,17 +26494,18 @@ msgstr ""
msgid "Basic Colors" msgid "Basic Colors"
msgstr "" msgstr ""
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "Orca Palette" msgid "Orca Palette"
msgstr "" msgstr ""
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor. #. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "RAL Classic" msgid "RAL Classic"
msgstr "" msgstr ""
#. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "Gradients" msgid "Gradients"
msgstr "" msgstr ""
msgctxt "Color picker"
msgid "Solid" msgid "Solid"
msgstr "" msgstr ""
@@ -26873,7 +26880,7 @@ msgstr ""
msgid "Medium violet red" msgid "Medium violet red"
msgstr "" msgstr ""
msgid "PaleV violet red" msgid "Pale violet red"
msgstr "" msgstr ""
msgid "Deep pink" msgid "Deep pink"
@@ -27035,7 +27042,7 @@ msgstr ""
msgid "Shallow sky" msgid "Shallow sky"
msgstr "" msgstr ""
msgid "Electric bel" msgid "Electric blue"
msgstr "" msgstr ""
msgid "Dolphin" msgid "Dolphin"
@@ -27104,7 +27111,7 @@ msgstr ""
msgid "Maize yellow" msgid "Maize yellow"
msgstr "" msgstr ""
msgid "Dalmatian yellow" msgid "Daffodil yellow"
msgstr "" msgstr ""
msgid "Brown beige" msgid "Brown beige"
@@ -27365,7 +27372,7 @@ msgstr ""
msgid "Turquoise blue" msgid "Turquoise blue"
msgstr "" msgstr ""
msgid "Capsanthin violet" msgid "Capri blue"
msgstr "" msgstr ""
msgid "Ocean blue" msgid "Ocean blue"
@@ -27479,7 +27486,7 @@ msgstr ""
msgid "Silver grey" msgid "Silver grey"
msgstr "" msgstr ""
msgid "Olivestone" msgid "Olive grey"
msgstr "" msgstr ""
msgid "Moss grey" msgid "Moss grey"
@@ -28067,10 +28074,19 @@ msgstr ""
msgid "Built-in" msgid "Built-in"
msgstr "" msgstr ""
msgid "My textures"
msgstr ""
msgid "No textures found." msgid "No textures found."
msgstr "" msgstr ""
msgid "My textures" msgid "Texture libraries"
msgstr ""
msgid "Explore more texture libraries"
msgstr ""
msgid "Opens OrcaCloud in the Home page to browse for plugins."
msgstr "" msgstr ""
msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer." msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer."
@@ -30123,6 +30139,9 @@ msgstr ""
msgid "Slicing Pipeline" msgid "Slicing Pipeline"
msgstr "" msgstr ""
msgid "Resource Texture"
msgstr ""
msgid "The plugin's configuration UI was empty. Showing the default editor." msgid "The plugin's configuration UI was empty. Showing the default editor."
msgstr "" msgstr ""
@@ -30238,6 +30257,24 @@ msgstr ""
msgid "Plugin directory does not contain a .py or .whl entry file" msgid "Plugin directory does not contain a .py or .whl entry file"
msgstr "" msgstr ""
msgid "Resource texture directory is not set"
msgstr ""
msgid "Resource texture directory could not be resolved"
msgstr ""
msgid "Resource texture path is not a directory"
msgstr ""
msgid "Plugin folder could not be resolved"
msgstr ""
msgid "Resource texture directory is outside the plugin folder"
msgstr ""
msgid "Resource texture directory could not be read"
msgstr ""
#, boost-format #, boost-format
msgid "Slicing pipeline plugin '%1%' error: %2%" msgid "Slicing pipeline plugin '%1%' error: %2%"
msgstr "" msgstr ""
@@ -30594,7 +30631,7 @@ msgstr ""
#. TRN %1% is the application name. #. TRN %1% is the application name.
#, boost-format #, boost-format
msgid "Not a gcode file generated by %1%." msgid "Not a G-code file generated by %1%."
msgstr "" msgstr ""
#. TRN %1% is a tag in the G-code file that must not be translated. #. TRN %1% is a tag in the G-code file that must not be translated.
@@ -30682,7 +30719,7 @@ msgstr ""
msgid "Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%" msgid "Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%"
msgstr "" msgstr ""
msgid "Post-processor can't find exported gcode file" msgid "Post-processor can't find exported G-code file"
msgstr "" msgstr ""
#, boost-format #, boost-format
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+110 -47
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Slic3rPE\n" "Project-Id-Version: Slic3rPE\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-15 17:40+0800\n" "POT-Creation-Date: 2026-10-19 09:48-0300\n"
"PO-Revision-Date: 2026-06-11 12:37-0300\n" "PO-Revision-Date: 2026-06-11 12:37-0300\n"
"Last-Translator: Handle <mail@bysb.net>\n" "Last-Translator: Handle <mail@bysb.net>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -1751,7 +1751,7 @@ msgid "Stroke of shape (%1%) contains unsupported: %2%."
msgstr "形状(%1%)的描边包含不受支持的内容:%2%。" msgstr "形状(%1%)的描边包含不受支持的内容:%2%。"
# AI Translated # AI Translated
msgid "Not valid state please report reproduction steps on github" msgid "Not valid state please report reproduction steps on GitHub"
msgstr "状态无效,请在 GitHub 上报告复现步骤" msgstr "状态无效,请在 GitHub 上报告复现步骤"
# AI Translated # AI Translated
@@ -1759,11 +1759,11 @@ msgid "No embossed file"
msgstr "没有浮雕文件" msgstr "没有浮雕文件"
# AI Translated # AI Translated
msgid "Missing svg file in embossed shape" msgid "Missing SVG file in embossed shape"
msgstr "浮雕形状中缺少 SVG 文件" msgstr "浮雕形状中缺少 SVG 文件"
# AI Translated # AI Translated
msgid "Missing data of svg file" msgid "Missing data of SVG file"
msgstr "缺少 SVG 文件数据" msgstr "缺少 SVG 文件数据"
# AI Translated # AI Translated
@@ -5008,7 +5008,7 @@ msgid "Unknown C++ exception."
msgstr "未知的 C++ 异常。" msgstr "未知的 C++ 异常。"
# AI Translated # AI Translated
msgid "Processing G-Code from previous file…" msgid "Processing G-code from previous file…"
msgstr "正在处理之前文件的 G-code…" msgstr "正在处理之前文件的 G-code…"
msgid "Slicing complete" msgid "Slicing complete"
@@ -6699,7 +6699,7 @@ msgstr "体积:"
msgid "Size:" msgid "Size:"
msgstr "尺寸:" msgstr "尺寸:"
#, boost-format #, c-format, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "发现G-code路径在层%d,高度为%.2lf mm处有冲突。请将有冲突的对象分离得更远(%s <-> %s)。" msgstr "发现G-code路径在层%d,高度为%.2lf mm处有冲突。请将有冲突的对象分离得更远(%s <-> %s)。"
@@ -12013,6 +12013,10 @@ msgstr "上传文件到打印机存储失败,请重试。"
msgid "Send Failed" msgid "Send Failed"
msgstr "发送失败" msgstr "发送失败"
# AI Translated
msgid "No available printers"
msgstr "没有可用的打印机"
msgid "The name length exceeds the limit." msgid "The name length exceeds the limit."
msgstr "名称长度超过限制。" msgstr "名称长度超过限制。"
@@ -13922,8 +13926,8 @@ msgid "Left mouse"
msgstr "鼠标左键" msgstr "鼠标左键"
# AI Translated # AI Translated
msgid "Available anywhere in the window, even while typing in a text field." msgid "Available anywhere in the window."
msgstr "在窗口任意位置可用,即使正在文本框中输入。" msgstr "在窗口任意位置可用。"
# AI Translated # AI Translated
msgid "Run favorite 1 to 10" msgid "Run favorite 1 to 10"
@@ -14039,10 +14043,6 @@ msgstr "%s 已分配给 %s。是否将其重新分配给 %s?"
msgid "The default %s is assigned to %s. Reassign it to %s?" msgid "The default %s is assigned to %s. Reassign it to %s?"
msgstr "默认的 %s 已分配给 %s。是否将其重新分配给 %s?" msgstr "默认的 %s 已分配给 %s。是否将其重新分配给 %s?"
# AI Translated
msgid "Global shortcuts also apply while typing."
msgstr "全局快捷键在输入文字时同样生效。"
# AI Translated # AI Translated
msgid "A key that types a character cannot be a global shortcut." msgid "A key that types a character cannot be a global shortcut."
msgstr "会输入字符的按键不能用作全局快捷键。" msgstr "会输入字符的按键不能用作全局快捷键。"
@@ -18086,6 +18086,14 @@ msgstr "对象排除"
msgid "Enable this option to add EXCLUDE OBJECT command in G-code." msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
msgstr "启用此选项以在G-code中添加EXCLUDE OBJECT命令。" msgstr "启用此选项以在G-code中添加EXCLUDE OBJECT命令。"
# AI Translated
msgid "Outline tolerance"
msgstr "轮廓线公差"
# AI Translated
msgid "Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the original outline. Larger values give fewer points, which helps firmware that stalls while parsing G-code with many objects. 0 keeps every point."
msgstr "写入 EXCLUDE_OBJECT_DEFINE 的对象轮廓线与原始轮廓线之间的最大偏差。数值越大,点数越少,有助于避免固件在解析包含大量对象的 G-code 时卡顿。0 表示保留所有点。"
msgid "Verbose G-code" msgid "Verbose G-code"
msgstr "注释G-code" msgstr "注释G-code"
@@ -24004,7 +24012,7 @@ msgstr "开始测试 %s..."
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
msgid "test %s ip resolved = %s" msgid "test %s IP resolved = %s"
msgstr "测试 %s:IP 解析结果 = %s" msgstr "测试 %s:IP 解析结果 = %s"
# AI Translated # AI Translated
@@ -31498,20 +31506,21 @@ msgid "Basic Colors"
msgstr "基本颜色" msgstr "基本颜色"
# AI Translated # AI Translated
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "Orca Palette" msgid "Orca Palette"
msgstr "Orca 调色板" msgstr "Orca 调色板"
# AI Translated # AI Translated
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor. #. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "RAL Classic" msgid "RAL Classic"
msgstr "RAL 经典色" msgstr "RAL 经典色"
# AI Translated # AI Translated
#. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "Gradients" msgid "Gradients"
msgstr "渐变预设" msgstr "渐变预设"
# AI Translated # AI Translated
msgctxt "Color picker"
msgid "Solid" msgid "Solid"
msgstr "纯色" msgstr "纯色"
@@ -32010,7 +32019,7 @@ msgid "Medium violet red"
msgstr "中紫罗兰红" msgstr "中紫罗兰红"
# AI Translated # AI Translated
msgid "PaleV violet red" msgid "Pale violet red"
msgstr "浅紫罗兰红" msgstr "浅紫罗兰红"
# AI Translated # AI Translated
@@ -32226,8 +32235,7 @@ msgid "Shallow sky"
msgstr "浅空" msgstr "浅空"
# AI Translated # AI Translated
# Needs human review: The English “Electric bel” looks like a typo (Electric blue? Electric eel?); the color is #1E90FF, so 电光蓝 was chosen. Recheck once the source name is fixed. msgid "Electric blue"
msgid "Electric bel"
msgstr "电光蓝" msgstr "电光蓝"
# AI Translated # AI Translated
@@ -32319,9 +32327,8 @@ msgid "Maize yellow"
msgstr "玉米黄" msgstr "玉米黄"
# AI Translated # AI Translated
# Needs human review: RAL 1007 is “Daffodil yellow” (水仙黄); the English name in the palette looks wrong. Recheck once the source name is fixed. msgid "Daffodil yellow"
msgid "Dalmatian yellow" msgstr "水仙黄"
msgstr "达尔马提亚黄"
# AI Translated # AI Translated
msgid "Brown beige" msgid "Brown beige"
@@ -32668,8 +32675,8 @@ msgid "Turquoise blue"
msgstr "绿松石蓝" msgstr "绿松石蓝"
# AI Translated # AI Translated
msgid "Capsanthin violet" msgid "Capri blue"
msgstr "辣椒红素紫" msgstr "卡普里蓝"
# AI Translated # AI Translated
msgid "Ocean blue" msgid "Ocean blue"
@@ -32820,8 +32827,8 @@ msgid "Silver grey"
msgstr "银灰色" msgstr "银灰色"
# AI Translated # AI Translated
msgid "Olivestone" msgid "Olive grey"
msgstr "橄榄石色" msgstr "橄榄灰"
# AI Translated # AI Translated
msgid "Moss grey" msgid "Moss grey"
@@ -33597,13 +33604,25 @@ msgstr "从“我的纹理”中移除"
msgid "Built-in" msgid "Built-in"
msgstr "内置" msgstr "内置"
# AI Translated
msgid "My textures"
msgstr "我的纹理"
# AI Translated # AI Translated
msgid "No textures found." msgid "No textures found."
msgstr "未找到纹理。" msgstr "未找到纹理。"
# AI Translated # AI Translated
msgid "My textures" msgid "Texture libraries"
msgstr "我的纹理" msgstr "纹理库"
# AI Translated
msgid "Explore more texture libraries"
msgstr "探索更多纹理库"
# AI Translated
msgid "Opens OrcaCloud in the Home page to browse for plugins."
msgstr "在主页中打开 OrcaCloud,浏览插件。"
# AI Translated # AI Translated
msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer." msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer."
@@ -36285,6 +36304,10 @@ msgstr "脚本"
msgid "Slicing Pipeline" msgid "Slicing Pipeline"
msgstr "切片流水线" msgstr "切片流水线"
# AI Translated
msgid "Resource Texture"
msgstr "资源纹理"
# AI Translated # AI Translated
msgid "The plugin's configuration UI was empty. Showing the default editor." msgid "The plugin's configuration UI was empty. Showing the default editor."
msgstr "插件的配置界面为空。正在显示默认编辑器。" msgstr "插件的配置界面为空。正在显示默认编辑器。"
@@ -36433,6 +36456,30 @@ msgstr "插件目录同时包含 .py 和 .whl 入口文件"
msgid "Plugin directory does not contain a .py or .whl entry file" msgid "Plugin directory does not contain a .py or .whl entry file"
msgstr "插件目录不包含 .py 或 .whl 入口文件" msgstr "插件目录不包含 .py 或 .whl 入口文件"
# AI Translated
msgid "Resource texture directory is not set"
msgstr "未设置资源纹理目录"
# AI Translated
msgid "Resource texture directory could not be resolved"
msgstr "无法解析资源纹理目录"
# AI Translated
msgid "Resource texture path is not a directory"
msgstr "资源纹理路径不是目录"
# AI Translated
msgid "Plugin folder could not be resolved"
msgstr "无法解析插件文件夹"
# AI Translated
msgid "Resource texture directory is outside the plugin folder"
msgstr "资源纹理目录位于插件文件夹之外"
# AI Translated
msgid "Resource texture directory could not be read"
msgstr "无法读取资源纹理目录"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Slicing pipeline plugin '%1%' error: %2%" msgid "Slicing pipeline plugin '%1%' error: %2%"
@@ -36891,7 +36938,7 @@ msgstr "加载配置文件“%1%”失败:%2%"
# AI Translated # AI Translated
#. TRN %1% is the application name. #. TRN %1% is the application name.
#, boost-format #, boost-format
msgid "Not a gcode file generated by %1%." msgid "Not a G-code file generated by %1%."
msgstr "不是由 %1% 生成的 G-code 文件。" msgstr "不是由 %1% 生成的 G-code 文件。"
# AI Translated # AI Translated
@@ -37011,7 +37058,7 @@ msgid "Failed making a temporary copy of G-code file %1% before running a post-p
msgstr "运行后处理脚本之前,无法创建 G-code 文件 %1% 的临时副本:%2%" msgstr "运行后处理脚本之前,无法创建 G-code 文件 %1% 的临时副本:%2%"
# AI Translated # AI Translated
msgid "Post-processor can't find exported gcode file" msgid "Post-processor can't find exported G-code file"
msgstr "后处理器找不到导出的 G-code 文件" msgstr "后处理器找不到导出的 G-code 文件"
# AI Translated # AI Translated
@@ -37400,6 +37447,40 @@ msgstr ""
"避免翘曲\n" "避免翘曲\n"
"您知道吗?打印ABS这类易翘曲材料时,适当提高热床温度可以降低翘曲的概率。" "您知道吗?打印ABS这类易翘曲材料时,适当提高热床温度可以降低翘曲的概率。"
# AI Translated
# Needs human review: RAL 1007 is “Daffodil yellow” (水仙黄); the English name in the palette looks wrong. Recheck once the source name is fixed.
#~ msgid "Dalmatian yellow"
#~ msgstr "达尔马提亚黄"
# AI Translated
#~ msgid "Capsanthin violet"
#~ msgstr "辣椒红素紫"
# AI Translated
#~ msgid "Olivestone"
#~ msgstr "橄榄石色"
# AI Translated
#~ msgid "Available anywhere in the window, even while typing in a text field."
#~ msgstr "在窗口任意位置可用,即使正在文本框中输入。"
# AI Translated
#~ msgid "Global shortcuts also apply while typing."
#~ msgstr "全局快捷键在输入文字时同样生效。"
# AI Translated
#~ msgid "Solid"
#~ msgstr "纯色"
# AI Translated
#~ msgid "PaleV violet red"
#~ msgstr "浅紫罗兰红"
# AI Translated
# Needs human review: The English “Electric bel” looks like a typo (Electric blue? Electric eel?); the color is #1E90FF, so 电光蓝 was chosen. Recheck once the source name is fixed.
#~ msgid "Electric bel"
#~ msgstr "电光蓝"
#~ msgid "Please choose the filament color" #~ msgid "Please choose the filament color"
#~ msgstr "请选择耗材丝颜色" #~ msgstr "请选择耗材丝颜色"
@@ -37865,9 +37946,6 @@ msgstr ""
#~ msgid "no configured tool" #~ msgid "no configured tool"
#~ msgstr "未配置的工具头" #~ msgstr "未配置的工具头"
#~ msgid "Add end G-Code when finishing the entire print."
#~ msgstr "所有打印结束时的结尾G-code"
#~ msgid "Maximum X speed" #~ msgid "Maximum X speed"
#~ msgstr "X最大速度" #~ msgstr "X最大速度"
@@ -37884,26 +37962,11 @@ msgstr ""
#~ msgid "Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, including a final G-code post-processing step. Research/experimental." #~ msgid "Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, including a final G-code post-processing step. Research/experimental."
#~ msgstr "在每个切片流水线步骤中调用的 Python 插件,用于读取和修改中间切片数据,包括最终的 G-code 后处理步骤。研究/实验性。" #~ msgstr "在每个切片流水线步骤中调用的 Python 插件,用于读取和修改中间切片数据,包括最终的 G-code 后处理步骤。研究/实验性。"
# AI Translated
#~ msgid "When enabled, the slicer emits g-code with the primary zone's contents centered on bed origin (0,0). The printer firmware is responsible for placing copies/mirrors at each physical zone position. Enable only on printers whose firmware applies its own zone offsets in copy/mirror mode (e.g. RepRapFirmware IDEX duplication mode). Has no effect in Primary mode."
#~ msgstr "启用后,切片软件输出的 G-code 会以热床原点 (0,0) 为中心放置主区域的内容,由打印机固件负责把复制件/镜像件放到各个物理区域。仅当打印机固件在复制/镜像模式下会自行应用区域偏移时才启用(例如 RepRapFirmware 的 IDEX 复制模式)。主模式下无效。"
# AI Translated # AI Translated
# Needs human review: "pem-inversion defaults": confirm pem is the physical extruder map and that the default is its inverse. # Needs human review: "pem-inversion defaults": confirm pem is the physical extruder map and that the default is its inverse.
#~ msgid "Per-plate override mapping physical heads to filament slots (1-based). Empty means use pem-inversion defaults." #~ msgid "Per-plate override mapping physical heads to filament slots (1-based). Empty means use pem-inversion defaults."
#~ msgstr "按盘单独设置物理工具头到耗材丝槽位的映射(从 1 开始编号),覆盖默认映射。留空则使用由物理挤出机映射(pem)反推的默认值。" #~ msgstr "按盘单独设置物理工具头到耗材丝槽位的映射(从 1 开始编号),覆盖默认映射。留空则使用由物理挤出机映射(pem)反推的默认值。"
# AI Translated
#~ msgid "Axis the mesh is rotated about before slicing. This is the belt printer's tilt: an isometric (no distortion) rotation that also drives bed rendering and support gravity tilt, and that the g-code back-transform inverts before the machine-frame shear/scale and remap. X is the typical gantry tilt (belt travels along Y)."
#~ msgstr "切片前模型网格的旋转轴。这一旋转即传送带打印机的倾斜:它是保距(无畸变)旋转,同时用于热床渲染和支撑的重力倾斜;生成 G-code 时,逆变换会先还原这一旋转,再进行机器坐标系剪切/缩放和重映射。X 是典型的龙门倾斜轴(传送带沿 Y 方向移动)。"
# AI Translated
#~ msgid "Expert override: set the machine-frame (g-code shear/scale) tilt angle independently of the pre-slice rotation angle. When disabled, the machine-frame transform is derived from the belt tilt angle, so a single angle drives both stages. Enable only to compensate for a machine whose physical gantry tilt differs from the slicing rotation."
#~ msgstr "专家选项:单独设置机器坐标系(G-code 剪切/缩放)的倾斜角度,与切片前的旋转角度互不影响。禁用时,机器坐标系变换由传送带倾斜角度推导,两个阶段使用同一个角度。仅当机器实际的龙门倾角与切片旋转角度不一致时,才需启用此项进行补偿。"
#~ msgid "The maximum deviation allowed when reducing the resolution for the 'Maximum wall resolution' setting. If you increase this, the print will be less accurate, but the G-Code will be smaller. 'Maximum wall deviation' limits 'Maximum wall resolution', so if the two conflict, 'Maximum wall deviation' takes precedence."
#~ msgstr "在为“最大墙体分辨率”设置降低分辨率时所允许的最大偏差。如果增大此值,打印精度会降低,但 G-code 会更小。“最大墙体偏差”会限制“最大墙体分辨率”,因此当两者冲突时,以“最大墙体偏差”为准。"
#~ msgid " not in range " #~ msgid " not in range "
#~ msgstr " 不在合理范围内" #~ msgstr " 不在合理范围内"
+111 -48
View File
@@ -9,7 +9,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-15 17:40+0800\n" "POT-Creation-Date: 2026-10-19 09:48-0300\n"
"PO-Revision-Date: 2025-11-28 13:48-0600\n" "PO-Revision-Date: 2025-11-28 13:48-0600\n"
"Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n" "Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -1826,7 +1826,7 @@ msgid "Stroke of shape (%1%) contains unsupported: %2%."
msgstr "形狀(%1%)的筆劃包含不支援的內容:%2%。" msgstr "形狀(%1%)的筆劃包含不支援的內容:%2%。"
# AI Translated # AI Translated
msgid "Not valid state please report reproduction steps on github" msgid "Not valid state please report reproduction steps on GitHub"
msgstr "狀態無效,請在 GitHub 上回報重現步驟" msgstr "狀態無效,請在 GitHub 上回報重現步驟"
# AI Translated # AI Translated
@@ -1834,11 +1834,11 @@ msgid "No embossed file"
msgstr "沒有浮雕檔案" msgstr "沒有浮雕檔案"
# AI Translated # AI Translated
msgid "Missing svg file in embossed shape" msgid "Missing SVG file in embossed shape"
msgstr "浮雕形狀中缺少 SVG 檔案" msgstr "浮雕形狀中缺少 SVG 檔案"
# AI Translated # AI Translated
msgid "Missing data of svg file" msgid "Missing data of SVG file"
msgstr "缺少 SVG 檔案的資料" msgstr "缺少 SVG 檔案的資料"
msgid "Face the camera" msgid "Face the camera"
@@ -5219,7 +5219,7 @@ msgid "Unknown C++ exception."
msgstr "未知的 C++ 例外。" msgstr "未知的 C++ 例外。"
# AI Translated # AI Translated
msgid "Processing G-Code from previous file…" msgid "Processing G-code from previous file…"
msgstr "正在處理先前檔案的 G-code…" msgstr "正在處理先前檔案的 G-code…"
msgid "Slicing complete" msgid "Slicing complete"
@@ -6912,7 +6912,7 @@ msgid "Size:"
msgstr "尺寸:" msgstr "尺寸:"
# AI Translated # AI Translated
#, boost-format #, c-format, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "在第 %d 層(Z = %.2lf mm)發現 G-code 路徑衝突。請將有衝突的物件分開得更遠(%s <-> %s)。" msgstr "在第 %d 層(Z = %.2lf mm)發現 G-code 路徑衝突。請將有衝突的物件分開得更遠(%s <-> %s)。"
@@ -12298,6 +12298,10 @@ msgstr "無法將檔案上傳至列印裝置的儲存空間,請重試。"
msgid "Send Failed" msgid "Send Failed"
msgstr "傳送失敗" msgstr "傳送失敗"
# AI Translated
msgid "No available printers"
msgstr "沒有可用的列印裝置"
msgid "The name length exceeds the limit." msgid "The name length exceeds the limit."
msgstr "名稱長度超過限制。" msgstr "名稱長度超過限制。"
@@ -14251,8 +14255,8 @@ msgid "Left mouse"
msgstr "滑鼠左鍵" msgstr "滑鼠左鍵"
# AI Translated # AI Translated
msgid "Available anywhere in the window, even while typing in a text field." msgid "Available anywhere in the window."
msgstr "在視窗中任何位置皆可使用,即使正在文字欄位中輸入也可以。" msgstr "在視窗中任何位置皆可使用。"
# AI Translated # AI Translated
msgid "Run favorite 1 to 10" msgid "Run favorite 1 to 10"
@@ -14372,10 +14376,6 @@ msgstr "%s 已指派給 %s。要改為指派給 %s 嗎?"
msgid "The default %s is assigned to %s. Reassign it to %s?" msgid "The default %s is assigned to %s. Reassign it to %s?"
msgstr "預設的 %s 已指派給 %s。要改為指派給 %s 嗎?" msgstr "預設的 %s 已指派給 %s。要改為指派給 %s 嗎?"
# AI Translated
msgid "Global shortcuts also apply while typing."
msgstr "全域快速鍵在輸入文字時也會生效。"
# AI Translated # AI Translated
msgid "A key that types a character cannot be a global shortcut." msgid "A key that types a character cannot be a global shortcut."
msgstr "會輸入字元的按鍵不能作為全域快速鍵。" msgstr "會輸入字元的按鍵不能作為全域快速鍵。"
@@ -18513,6 +18513,14 @@ msgstr "物件排除"
msgid "Enable this option to add EXCLUDE OBJECT command in G-code." msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
msgstr "啟用此選項以在 G-code 中加入 EXCLUDE OBJECT 指令。" msgstr "啟用此選項以在 G-code 中加入 EXCLUDE OBJECT 指令。"
# AI Translated
msgid "Outline tolerance"
msgstr "輪廓線公差"
# AI Translated
msgid "Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the original outline. Larger values give fewer points, which helps firmware that stalls while parsing G-code with many objects. 0 keeps every point."
msgstr "寫入 EXCLUDE_OBJECT_DEFINE 的物件輪廓線與原始輪廓線之間的最大偏差。數值越大,點數越少,有助於避免韌體在解析包含大量物件的 G-code 時卡住。0 表示保留所有點。"
msgid "Verbose G-code" msgid "Verbose G-code"
msgstr "詳細 G-code" msgstr "詳細 G-code"
@@ -24597,7 +24605,7 @@ msgstr "測試 %s 開始..."
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
msgid "test %s ip resolved = %s" msgid "test %s IP resolved = %s"
msgstr "測試 %s IP 解析結果 = %s" msgstr "測試 %s IP 解析結果 = %s"
# AI Translated # AI Translated
@@ -32124,21 +32132,22 @@ msgid "Basic Colors"
msgstr "基本顏色" msgstr "基本顏色"
# AI Translated # AI Translated
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "Orca Palette" msgid "Orca Palette"
msgstr "Orca 調色盤" msgstr "Orca 調色盤"
# AI Translated # AI Translated
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor. #. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "RAL Classic" msgid "RAL Classic"
msgstr "RAL 經典色" msgstr "RAL 經典色"
# AI Translated # AI Translated
# Needs human review: Palette dropdown entry for the gradient presets (next to 基本顏色 / Orca 調色盤 / RAL 經典色); 漸層配色 keeps it apart from the Solid/Gradient switch label 漸層. Check it reads right in the dropdown. # Needs human review: Palette dropdown entry for the gradient presets (next to 基本顏色 / Orca 調色盤 / RAL 經典色); 漸層配色 keeps it apart from the Solid/Gradient switch label 漸層. Check it reads right in the dropdown.
#. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "Gradients" msgid "Gradients"
msgstr "漸層配色" msgstr "漸層配色"
# AI Translated # AI Translated
msgctxt "Color picker"
msgid "Solid" msgid "Solid"
msgstr "純色" msgstr "純色"
@@ -32637,7 +32646,7 @@ msgid "Medium violet red"
msgstr "中紫羅蘭紅" msgstr "中紫羅蘭紅"
# AI Translated # AI Translated
msgid "PaleV violet red" msgid "Pale violet red"
msgstr "淡紫羅蘭紅" msgstr "淡紫羅蘭紅"
# AI Translated # AI Translated
@@ -32853,8 +32862,7 @@ msgid "Shallow sky"
msgstr "淺色天空" msgstr "淺色天空"
# AI Translated # AI Translated
# Needs human review: Orca palette: the English is a typo for "Electric blue" (hex #1E90FF); translated as 電光藍. Revisit if the source keeps a different name. msgid "Electric blue"
msgid "Electric bel"
msgstr "電光藍" msgstr "電光藍"
# AI Translated # AI Translated
@@ -32946,9 +32954,8 @@ msgid "Maize yellow"
msgstr "玉米黃" msgstr "玉米黃"
# AI Translated # AI Translated
# Needs human review: RAL 1007 in the RAL Classic list: the English name looks wrong (1007 is Daffodil yellow, 水仙黃); translated literally. Revisit once the source name is fixed. msgid "Daffodil yellow"
msgid "Dalmatian yellow" msgstr "水仙黃"
msgstr "達爾馬提亞黃"
# AI Translated # AI Translated
msgid "Brown beige" msgid "Brown beige"
@@ -33296,9 +33303,8 @@ msgid "Turquoise blue"
msgstr "綠松石藍" msgstr "綠松石藍"
# AI Translated # AI Translated
# Needs human review: RAL 5019 in the RAL Classic list: the English name is not a RAL Classic name (5019 is Capri blue); translated literally. Revisit once the source name is fixed. msgid "Capri blue"
msgid "Capsanthin violet" msgstr "卡布里藍"
msgstr "辣椒紅素紫"
# AI Translated # AI Translated
msgid "Ocean blue" msgid "Ocean blue"
@@ -33449,8 +33455,8 @@ msgid "Silver grey"
msgstr "銀灰" msgstr "銀灰"
# AI Translated # AI Translated
msgid "Olivestone" msgid "Olive grey"
msgstr "橄欖石色" msgstr "橄欖灰"
# AI Translated # AI Translated
msgid "Moss grey" msgid "Moss grey"
@@ -34226,13 +34232,25 @@ msgstr "從「我的紋理」移除"
msgid "Built-in" msgid "Built-in"
msgstr "內建" msgstr "內建"
# AI Translated
msgid "My textures"
msgstr "我的紋理"
# AI Translated # AI Translated
msgid "No textures found." msgid "No textures found."
msgstr "找不到紋理。" msgstr "找不到紋理。"
# AI Translated # AI Translated
msgid "My textures" msgid "Texture libraries"
msgstr "我的紋理" msgstr "紋理庫"
# AI Translated
msgid "Explore more texture libraries"
msgstr "探索更多紋理庫"
# AI Translated
msgid "Opens OrcaCloud in the Home page to browse for plugins."
msgstr "在首頁中開啟 OrcaCloud,瀏覽外掛。"
# AI Translated # AI Translated
msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer." msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer."
@@ -36928,6 +36946,10 @@ msgstr "指令碼"
msgid "Slicing Pipeline" msgid "Slicing Pipeline"
msgstr "切片管線" msgstr "切片管線"
# AI Translated
msgid "Resource Texture"
msgstr "資源紋理"
# AI Translated # AI Translated
msgid "The plugin's configuration UI was empty. Showing the default editor." msgid "The plugin's configuration UI was empty. Showing the default editor."
msgstr "外掛的設定介面為空。將顯示預設編輯器。" msgstr "外掛的設定介面為空。將顯示預設編輯器。"
@@ -37075,6 +37097,30 @@ msgstr "外掛目錄同時包含 .py 和 .whl 進入點檔案"
msgid "Plugin directory does not contain a .py or .whl entry file" msgid "Plugin directory does not contain a .py or .whl entry file"
msgstr "外掛目錄不包含 .py 或 .whl 進入點檔案" msgstr "外掛目錄不包含 .py 或 .whl 進入點檔案"
# AI Translated
msgid "Resource texture directory is not set"
msgstr "未設定資源紋理目錄"
# AI Translated
msgid "Resource texture directory could not be resolved"
msgstr "無法解析資源紋理目錄"
# AI Translated
msgid "Resource texture path is not a directory"
msgstr "資源紋理路徑不是目錄"
# AI Translated
msgid "Plugin folder could not be resolved"
msgstr "無法解析外掛資料夾"
# AI Translated
msgid "Resource texture directory is outside the plugin folder"
msgstr "資源紋理目錄位於外掛資料夾之外"
# AI Translated
msgid "Resource texture directory could not be read"
msgstr "無法讀取資源紋理目錄"
# AI Translated # AI Translated
#, boost-format #, boost-format
msgid "Slicing pipeline plugin '%1%' error: %2%" msgid "Slicing pipeline plugin '%1%' error: %2%"
@@ -37533,7 +37579,7 @@ msgstr "載入設定檔「%1%」失敗:%2%"
# AI Translated # AI Translated
#. TRN %1% is the application name. #. TRN %1% is the application name.
#, boost-format #, boost-format
msgid "Not a gcode file generated by %1%." msgid "Not a G-code file generated by %1%."
msgstr "不是由 %1% 產生的 G-code 檔案。" msgstr "不是由 %1% 產生的 G-code 檔案。"
# AI Translated # AI Translated
@@ -37653,7 +37699,7 @@ msgid "Failed making a temporary copy of G-code file %1% before running a post-p
msgstr "執行後處理腳本前,無法建立 G-code 檔案 %1% 的暫存副本:%2%" msgstr "執行後處理腳本前,無法建立 G-code 檔案 %1% 的暫存副本:%2%"
# AI Translated # AI Translated
msgid "Post-processor can't find exported gcode file" msgid "Post-processor can't find exported G-code file"
msgstr "後處理程式找不到匯出的 G-code 檔案" msgstr "後處理程式找不到匯出的 G-code 檔案"
# AI Translated # AI Translated
@@ -38049,6 +38095,41 @@ msgstr ""
"避免翹曲\n" "避免翹曲\n"
"您知道嗎?當列印容易翹曲的材料(如 ABS)時,適當提高熱床溫度可以降低翹曲的機率。" "您知道嗎?當列印容易翹曲的材料(如 ABS)時,適當提高熱床溫度可以降低翹曲的機率。"
# AI Translated
# Needs human review: RAL 1007 in the RAL Classic list: the English name looks wrong (1007 is Daffodil yellow, 水仙黃); translated literally. Revisit once the source name is fixed.
#~ msgid "Dalmatian yellow"
#~ msgstr "達爾馬提亞黃"
# AI Translated
# Needs human review: RAL 5019 in the RAL Classic list: the English name is not a RAL Classic name (5019 is Capri blue); translated literally. Revisit once the source name is fixed.
#~ msgid "Capsanthin violet"
#~ msgstr "辣椒紅素紫"
# AI Translated
#~ msgid "Olivestone"
#~ msgstr "橄欖石色"
# AI Translated
#~ msgid "Available anywhere in the window, even while typing in a text field."
#~ msgstr "在視窗中任何位置皆可使用,即使正在文字欄位中輸入也可以。"
# AI Translated
#~ msgid "Global shortcuts also apply while typing."
#~ msgstr "全域快速鍵在輸入文字時也會生效。"
# AI Translated
#~ msgid "Solid"
#~ msgstr "純色"
# AI Translated
#~ msgid "PaleV violet red"
#~ msgstr "淡紫羅蘭紅"
# AI Translated
# Needs human review: Orca palette: the English is a typo for "Electric blue" (hex #1E90FF); translated as 電光藍. Revisit if the source keeps a different name.
#~ msgid "Electric bel"
#~ msgstr "電光藍"
#~ msgid "Please choose the filament color" #~ msgid "Please choose the filament color"
#~ msgstr "請選擇線材顏色" #~ msgstr "請選擇線材顏色"
@@ -38513,9 +38594,6 @@ msgstr ""
#~ msgid "no configured tool" #~ msgid "no configured tool"
#~ msgstr "未設定的工具頭" #~ msgstr "未設定的工具頭"
#~ msgid "Add end G-Code when finishing the entire print."
#~ msgstr "所有列印結束時的結尾 G-code"
#~ msgid "Maximum X speed" #~ msgid "Maximum X speed"
#~ msgstr "X 最大速度" #~ msgstr "X 最大速度"
@@ -38532,25 +38610,10 @@ msgstr ""
#~ msgid "Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, including a final G-code post-processing step. Research/experimental." #~ msgid "Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, including a final G-code post-processing step. Research/experimental."
#~ msgstr "在每個切片管線步驟呼叫的 Python 外掛,用於讀取與修改中介切片資料,包括最終的 G-code 後處理步驟。研究/實驗性質。" #~ msgstr "在每個切片管線步驟呼叫的 Python 外掛,用於讀取與修改中介切片資料,包括最終的 G-code 後處理步驟。研究/實驗性質。"
# AI Translated
#~ msgid "When enabled, the slicer emits g-code with the primary zone's contents centered on bed origin (0,0). The printer firmware is responsible for placing copies/mirrors at each physical zone position. Enable only on printers whose firmware applies its own zone offsets in copy/mirror mode (e.g. RepRapFirmware IDEX duplication mode). Has no effect in Primary mode."
#~ msgstr "啟用後,切片軟體輸出的 G-code 會將主區域的內容以列印板原點 (0,0) 為中心放置,並由列印裝置韌體負責將複製件/鏡像件放到各個實體區域位置。僅在韌體會於複製/鏡像模式下自行套用區域偏移的列印裝置上啟用(例如 RepRapFirmware 的 IDEX 複製模式)。在主模式下無效。"
# AI Translated # AI Translated
#~ msgid "Per-plate override mapping physical heads to filament slots (1-based). Empty means use pem-inversion defaults." #~ msgid "Per-plate override mapping physical heads to filament slots (1-based). Empty means use pem-inversion defaults."
#~ msgstr "依列印板覆寫實體工具頭到線材槽位的映射(從 1 開始編號)。留空表示使用由 pem 反推的預設映射。" #~ msgstr "依列印板覆寫實體工具頭到線材槽位的映射(從 1 開始編號)。留空表示使用由 pem 反推的預設映射。"
# AI Translated
#~ msgid "Axis the mesh is rotated about before slicing. This is the belt printer's tilt: an isometric (no distortion) rotation that also drives bed rendering and support gravity tilt, and that the g-code back-transform inverts before the machine-frame shear/scale and remap. X is the typical gantry tilt (belt travels along Y)."
#~ msgstr "切片前網格旋轉所繞的軸。這就是輸送帶列印裝置的傾斜:一種等距(無變形)旋轉,同時用於列印板渲染與支撐的重力傾斜;G-code 反向轉換會先還原此旋轉,再進行機器座標系的剪切/縮放與重新映射。X 是典型的龍門傾斜軸(輸送帶沿 Y 方向移動)。"
# AI Translated
#~ msgid "Expert override: set the machine-frame (g-code shear/scale) tilt angle independently of the pre-slice rotation angle. When disabled, the machine-frame transform is derived from the belt tilt angle, so a single angle drives both stages. Enable only to compensate for a machine whose physical gantry tilt differs from the slicing rotation."
#~ msgstr "專家覆寫選項:獨立於切片前的旋轉角度,單獨設定機器座標系(G-code 剪切/縮放)的傾斜角度。停用時,機器座標系變換由輸送帶傾斜角度推導,兩個階段使用同一個角度。僅在機器實際的龍門傾角與切片旋轉角度不同時,才啟用此選項進行補償。"
#~ msgid "The maximum deviation allowed when reducing the resolution for the 'Maximum wall resolution' setting. If you increase this, the print will be less accurate, but the G-Code will be smaller. 'Maximum wall deviation' limits 'Maximum wall resolution', so if the two conflict, 'Maximum wall deviation' takes precedence."
#~ msgstr "「最大牆解析度」設定在降低解析度時所允許的最大偏差。若增加此值,列印會較不精確,但 G-Code 會更小。「最大牆偏差」會限制「最大牆解析度」,因此若兩者衝突,將以「最大牆偏差」為準。"
#~ msgid " not in range " #~ msgid " not in range "
#~ msgstr " 不在合理的區間" #~ msgstr " 不在合理的區間"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "Snapmaker", "name": "Snapmaker",
"version": "02.04.00.17", "version": "02.04.00.18",
"force_update": "0", "force_update": "0",
"description": "Snapmaker configurations", "description": "Snapmaker configurations",
"machine_model_list": [ "machine_model_list": [
@@ -49,5 +49,8 @@
], ],
"pressure_advance": [ "pressure_advance": [
"0.02" "0.02"
],
"temperature_vitrification": [
"100"
] ]
} }
@@ -46,5 +46,8 @@
], ],
"fan_min_speed": [ "fan_min_speed": [
"0" "0"
],
"temperature_vitrification": [
"100"
] ]
} }
@@ -45,7 +45,7 @@
"250" "250"
], ],
"temperature_vitrification": [ "temperature_vitrification": [
"178" "70"
], ],
"fan_max_speed": [ "fan_max_speed": [
"20" "20"
@@ -4,5 +4,8 @@
"inherits": "fdm_filament_petg", "inherits": "fdm_filament_petg",
"from": "system", "from": "system",
"filament_id": "OFW8lqnb", "filament_id": "OFW8lqnb",
"instantiation": "false" "instantiation": "false",
"temperature_vitrification": [
"70"
]
} }
@@ -60,7 +60,7 @@
"250" "250"
], ],
"temperature_vitrification": [ "temperature_vitrification": [
"178" "70"
], ],
"fan_max_speed": [ "fan_max_speed": [
"20" "20"
@@ -34,5 +34,8 @@
], ],
"nozzle_temperature": [ "nozzle_temperature": [
"220" "220"
],
"temperature_vitrification": [
"45"
] ]
} }
@@ -34,5 +34,8 @@
], ],
"filament_cooling_final_speed": [ "filament_cooling_final_speed": [
"60" "60"
],
"temperature_vitrification": [
"45"
] ]
} }
@@ -34,5 +34,8 @@
], ],
"nozzle_temperature": [ "nozzle_temperature": [
"220" "220"
],
"temperature_vitrification": [
"45"
] ]
} }
@@ -40,5 +40,8 @@
], ],
"nozzle_temperature": [ "nozzle_temperature": [
"220" "220"
],
"temperature_vitrification": [
"45"
] ]
} }
@@ -31,5 +31,8 @@
], ],
"slow_down_layer_time": [ "slow_down_layer_time": [
"8" "8"
],
"temperature_vitrification": [
"45"
] ]
} }
@@ -50,5 +50,8 @@
"additional_cooling_fan_speed": [ "additional_cooling_fan_speed": [
"70", "70",
"70" "70"
],
"temperature_vitrification": [
"30"
] ]
} }
@@ -137,7 +137,7 @@
"240" "240"
], ],
"temperature_vitrification": [ "temperature_vitrification": [
"60" "30"
], ],
"nozzle_temperature_range_low": [ "nozzle_temperature_range_low": [
"200" "200"
@@ -302,7 +302,7 @@
"-1" "-1"
], ],
"temperature_vitrification": [ "temperature_vitrification": [
"60" "30"
], ],
"textured_cool_plate_temp": [ "textured_cool_plate_temp": [
"40" "40"
@@ -29,5 +29,8 @@
"additional_cooling_fan_speed": [ "additional_cooling_fan_speed": [
"70", "70",
"70" "70"
],
"temperature_vitrification": [
"30"
] ]
} }
@@ -49,5 +49,8 @@
"additional_cooling_fan_speed": [ "additional_cooling_fan_speed": [
"70", "70",
"70" "70"
],
"temperature_vitrification": [
"30"
] ]
} }
@@ -97,7 +97,7 @@
"20", "20",
"12" "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", "machine_tool_change_time": "5",
"max_layer_height": [ "max_layer_height": [
"0.14", "0.14",
@@ -97,7 +97,7 @@
"20", "20",
"12" "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", "machine_tool_change_time": "5",
"max_layer_height": [ "max_layer_height": [
"0.32", "0.32",
@@ -97,7 +97,7 @@
"20", "20",
"12" "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", "machine_tool_change_time": "5",
"max_layer_height": [ "max_layer_height": [
"0.42", "0.42",
@@ -97,7 +97,7 @@
"20", "20",
"12" "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", "machine_tool_change_time": "5",
"max_layer_height": [ "max_layer_height": [
"0.56", "0.56",
@@ -109,7 +109,7 @@ const cssColors = [
{hex: "#800080", name: "Purple"}, {hex: "#800080", name: "Purple"},
{hex: "#663399", name: "Rebecca purple"}, {hex: "#663399", name: "Rebecca purple"},
{hex: "#C71585", name: "Medium violet red"}, {hex: "#C71585", name: "Medium violet red"},
{hex: "#DB7093", name: "PaleV violet red"}, {hex: "#DB7093", name: "Pale violet red"},
{hex: "#FF1493", name: "Deep pink"}, {hex: "#FF1493", name: "Deep pink"},
{hex: "#FF69B4", name: "Hot pink"}, {hex: "#FF69B4", name: "Hot pink"},
{hex: "#FFB6C1", name: "Light pink"}, {hex: "#FFB6C1", name: "Light pink"},
@@ -193,7 +193,7 @@ const palettes = {
{hex: "#40E0D0", name: "Turquoise bay"}, {hex: "#40E0D0", name: "Turquoise bay"},
{hex: "#2EBDEF", name: "Sky over reef"}, {hex: "#2EBDEF", name: "Sky over reef"},
{hex: "#87CEEB", name: "Shallow sky"}, {hex: "#87CEEB", name: "Shallow sky"},
{hex: "#1E90FF", name: "Electric bel"}, {hex: "#1E90FF", name: "Electric blue"},
{hex: "#4169E1", name: "Dolphin"}, {hex: "#4169E1", name: "Dolphin"},
{hex: "#0000CD", name: "Ocean depths"}, {hex: "#0000CD", name: "Ocean depths"},
{hex: "#191970", name: "Whale"}, {hex: "#191970", name: "Whale"},
@@ -219,7 +219,7 @@ const palettes = {
{hex: "#EFBF04", name: "1004 Golden yellow"}, {hex: "#EFBF04", name: "1004 Golden yellow"},
{hex: "#D7A500", name: "1005 Honey yellow"}, {hex: "#D7A500", name: "1005 Honey yellow"},
{hex: "#F2D185", name: "1006 Maize yellow"}, {hex: "#F2D185", name: "1006 Maize yellow"},
{hex: "#FDD185", name: "1007 Dalmatian yellow"}, {hex: "#FDD185", name: "1007 Daffodil yellow"},
{hex: "#8B7D6B", name: "1011 Brown beige"}, {hex: "#8B7D6B", name: "1011 Brown beige"},
{hex: "#F5DB50", name: "1012 Lemon yellow"}, {hex: "#F5DB50", name: "1012 Lemon yellow"},
{hex: "#F3E5AB", name: "1013 Oyster white"}, {hex: "#F3E5AB", name: "1013 Oyster white"},
@@ -318,7 +318,7 @@ const palettes = {
{hex: "#0384A7", name: "5015 Sky blue"}, {hex: "#0384A7", name: "5015 Sky blue"},
{hex: "#006A9E", name: "5017 Traffic blue"}, {hex: "#006A9E", name: "5017 Traffic blue"},
{hex: "#005F87", name: "5018 Turquoise blue"}, {hex: "#005F87", name: "5018 Turquoise blue"},
{hex: "#5A3D5B", name: "5019 Capsanthin violet"}, {hex: "#1B5583", name: "5019 Capri blue"},
{hex: "#04749F", name: "5020 Ocean blue"}, {hex: "#04749F", name: "5020 Ocean blue"},
{hex: "#009AAA", name: "5021 Water blue"}, {hex: "#009AAA", name: "5021 Water blue"},
{hex: "#1A3D5F", name: "5022 Night blue"}, {hex: "#1A3D5F", name: "5022 Night blue"},
@@ -366,7 +366,7 @@ const palettes = {
{hex: "#00A550", name: "6039 Mint green"}, {hex: "#00A550", name: "6039 Mint green"},
{hex: "#7D7D7D", name: "7000 Squirrel grey"}, {hex: "#7D7D7D", name: "7000 Squirrel grey"},
{hex: "#A8A9AD", name: "7001 Silver grey"}, {hex: "#A8A9AD", name: "7001 Silver grey"},
{hex: "#6F7368", name: "7002 Olivestone"}, {hex: "#6F7368", name: "7002 Olive grey"},
{hex: "#8F9080", name: "7003 Moss grey"}, {hex: "#8F9080", name: "7003 Moss grey"},
{hex: "#8E9291", name: "7004 Signal grey"}, {hex: "#8E9291", name: "7004 Signal grey"},
{hex: "#8B8788", name: "7005 Mouse grey"}, {hex: "#8B8788", name: "7005 Mouse grey"},
+251 -4
View File
@@ -12,6 +12,7 @@ commands:
trim delete profile files no <vendor>.json list references trim delete profile files no <vendor>.json list references
update-index regenerate the *_list sections of <vendor>.json update-index regenerate the *_list sections of <vendor>.json
legacy-compat write the printers user presets can be migrated back to 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: options shared by several commands:
--vendor VENDOR act on one vendor bundle only; repeatable, empty means all --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 argparse
import difflib
import functools import functools
import html
import json import json
import os import os
import posixpath import posixpath
import re import re
import subprocess
import sys import sys
import uuid import uuid
from collections import Counter, defaultdict from collections import Counter, defaultdict
@@ -192,6 +196,7 @@ VECTOR_KEYS = {
PRINT_CONFIG_CPP = os.path.normpath( PRINT_CONFIG_CPP = os.path.normpath(
os.path.join(SCRIPTS_DIR, "..", "src", "libslic3r", "PrintConfig.cpp")) 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) @functools.lru_cache(maxsize=None)
@@ -313,11 +318,11 @@ REMEDY_HINTS = {
} }
def print_error(msg): def print_error(msg, file=None):
print(f"\033[91m[ERROR]\033[0m {msg}") # Red print(f"\033[91m[ERROR]\033[0m {msg}", file=file) # Red
def print_warning(msg): def print_warning(msg, file=None):
print(f"\033[93m[WARNING]\033[0m {msg}") # Yellow print(f"\033[93m[WARNING]\033[0m {msg}", file=file) # Yellow
def print_info(msg): def print_info(msg):
print(f"\033[94m[INFO]\033[0m {msg}") # Blue 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 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 # CLI
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -3233,6 +3455,9 @@ examples:
orca_profile_tool.py legacy-compat --base /tmp/v2.4.2/resources/profiles 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 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) 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: after adding, renaming or deleting profile files, run in this order:
normalize -> update-index -> generate-id -> check normalize -> update-index -> generate-id -> check
@@ -3387,6 +3612,25 @@ def build_parser():
legacy_cmd.add_argument("--check", action="store_true", legacy_cmd.add_argument("--check", action="store_true",
help="fail if the map differs from what --base implies; write nothing") 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 return parser
@@ -3423,6 +3667,9 @@ def main(argv=None):
if args.command == "legacy-compat": if args.command == "legacy-compat":
return run_legacy_compat(args.base, profiles_dir, args.output, args.check) 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": if args.command == "fix-variant":
_changed, errors = fix_variant_arrays(profiles_dir, vendors, dry_run=args.dry_run, _changed, errors = fix_variant_arrays(profiles_dir, vendors, dry_run=args.dry_run,
strict=args.strict) strict=args.strict)
+309 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Tests for the tree-maintenance half of scripts/orca_profile_tool.py: the """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 normalize, trim, update-index, fix-variant, check and gcode-diff commands, and the
dispatch that reaches them (stdlib unittest, no external deps). 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. The id halves are covered by test_filament_id.py and test_setting_id.py.
@@ -14,9 +14,12 @@ import json
import os import os
import re import re
import shutil import shutil
import stat
import subprocess
import sys import sys
import tempfile import tempfile
import unittest import unittest
from unittest import mock
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
@@ -41,7 +44,24 @@ class Tree:
os.makedirs(self.profiles) os.makedirs(self.profiles)
def cleanup(self): 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): def index_path(self, vendor):
return os.path.join(self.profiles, vendor + ".json") 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") 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 # the real tree
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+16
View File
@@ -1126,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) int CLI::run(int argc, char **argv)
{ {
// Mark the main thread for the debugger and for runtime checks. // Mark the main thread for the debugger and for runtime checks.
@@ -1176,6 +1189,9 @@ int CLI::run(int argc, char **argv)
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "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) { if (x11_opt_in) {
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) ===== // ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds."; BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
+3 -7
View File
@@ -812,7 +812,8 @@ bool WallToolPaths::removeEmptyToolPaths(std::vector<VariableWidthLines> &toolpa
* Get the order constraints of the insets when printing walls per region / hole. * Get the order constraints of the insets when printing walls per region / hole.
* Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right. * Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right.
* *
* Odd walls should always go after their enclosing wall polygons. * Odd walls follow the same order as even ones, so that the centre line of a narrow wall is an inner wall
* like any other when printing inner to outer.
* *
* \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one. * \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one.
*/ */
@@ -886,12 +887,7 @@ WallToolPaths::ExtrusionLineSet WallToolPaths::getRegionOrder(const std::vector<
continue; // not directly adjacent continue; // not directly adjacent
if (!shorter_then(loc_here - lineloc_nearby.j.p, (lineloc_here.j.w + lineloc_nearby.j.w) / 2 * diagonal_extension)) if (!shorter_then(loc_here - lineloc_nearby.j.p, (lineloc_here.j.w + lineloc_nearby.j.w) / 2 * diagonal_extension))
continue; // points are too far away from each other continue; // points are too far away from each other
if (here->is_odd || nearby->is_odd) { if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
if (here->is_odd && !nearby->is_odd && nearby->inset_idx < here->inset_idx)
order_requirements.emplace(std::make_pair(nearby, here));
if (nearby->is_odd && !here->is_odd && here->inset_idx < nearby->inset_idx)
order_requirements.emplace(std::make_pair(here, nearby));
} else if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
order_requirements.emplace(std::make_pair(nearby, here)); order_requirements.emplace(std::make_pair(nearby, here));
} else { } else {
assert((nearby->inset_idx > here->inset_idx) == outer_to_inner); assert((nearby->inset_idx > here->inset_idx) == outer_to_inner);
+2 -2
View File
@@ -182,8 +182,8 @@ set(lisbslic3r_sources
Fill/FillPlanePath.hpp Fill/FillPlanePath.hpp
Fill/FillRectilinear.cpp Fill/FillRectilinear.cpp
Fill/FillRectilinear.hpp Fill/FillRectilinear.hpp
Fill/FillTpmsAdaptive.cpp Fill/AdaptiveInfillField.cpp
Fill/FillTpmsAdaptive.hpp Fill/AdaptiveInfillField.hpp
Fill/FillTpmsD.cpp Fill/FillTpmsD.cpp
Fill/FillTpmsD.hpp Fill/FillTpmsD.hpp
Fill/FillTpmsFK.cpp Fill/FillTpmsFK.cpp
+68
View File
@@ -9,6 +9,8 @@
#include <numeric> #include <numeric>
#include <unordered_map> #include <unordered_map>
#include <tbb/parallel_for.h>
#include "ClipperUtils.hpp" #include "ClipperUtils.hpp"
#include "BoundingBox.hpp" #include "BoundingBox.hpp"
#include "ExPolygon.hpp" #include "ExPolygon.hpp"
@@ -800,6 +802,72 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r
{ return _clipper_ex(ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); } { return _clipper_ex(ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); }
Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset)
{ return _clipper_ex(ctIntersection, ClipperUtils::SurfacesPtrProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } { return _clipper_ex(ctIntersection, ClipperUtils::SurfacesPtrProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); }
namespace ClipperUtils {
std::vector<ExPolygonsTile> tile_expolygons(const ExPolygons &expolygons, size_t per_tile)
{
BoundingBox extent;
std::vector<BoundingBox> bboxes;
bboxes.reserve(expolygons.size());
for (const ExPolygon &expoly : expolygons) {
bboxes.emplace_back(get_extents(expoly));
extent.merge(bboxes.back());
}
if (! extent.defined)
return {};
const int tiles = std::clamp(int(std::sqrt(double(expolygons.size()) / double(std::max<size_t>(per_tile, 1)))), 1, 32);
const Point size = extent.size();
const coord_t tile_w = std::max<coord_t>(1, size.x() / tiles + 1), tile_h = std::max<coord_t>(1, size.y() / tiles + 1);
std::vector<ExPolygonsTile> out(size_t(tiles * tiles));
for (size_t i = 0; i < expolygons.size(); ++ i) {
const Point c = bboxes[i].center();
ExPolygonsTile &tile = out[size_t(std::clamp(int((c.y() - extent.min.y()) / tile_h), 0, tiles - 1) * tiles +
std::clamp(int((c.x() - extent.min.x()) / tile_w), 0, tiles - 1))];
tile.members.emplace_back(i);
tile.bbox.merge(bboxes[i]);
}
out.erase(std::remove_if(out.begin(), out.end(), [](const ExPolygonsTile &tile) { return tile.members.empty(); }), out.end());
return out;
}
}
static Slic3r::ExPolygons clipper_ex_by_piece(ClipType clipType, const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
{
// A few dozen subject ExPolygons to a tile, each tile one Clipper call with the clip cut to the tile's box.
const std::vector<ClipperUtils::ExPolygonsTile> tiles = ClipperUtils::tile_expolygons(subject, 32);
// One tile is the plain call: cutting the clip would only cost time.
if (tiles.size() <= 1)
return _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset);
std::vector<BoundingBox> clip_bboxes;
clip_bboxes.reserve(clip.size());
for (const Polygon &polygon : clip)
clip_bboxes.emplace_back(get_extents(polygon));
std::vector<Slic3r::ExPolygons> out_tiles(tiles.size());
tbb::parallel_for(size_t(0), tiles.size(), [&](size_t tile_idx) {
const ClipperUtils::ExPolygonsTile &tile = tiles[tile_idx];
Slic3r::ExPolygons local_subject;
local_subject.reserve(tile.members.size());
for (size_t i : tile.members)
local_subject.emplace_back(subject[i]);
// Grown so that the cut edges of the clip stay clear of the subject, also after the safety offset.
const BoundingBox bbox = tile.bbox.inflated(SCALED_EPSILON);
Polygons local_clip;
for (size_t i = 0; i < clip.size(); ++i)
if (clip_bboxes[i].overlap(bbox))
if (Polygon clipped = ClipperUtils::clip_clipper_polygon_with_subject_bbox(clip[i], bbox); ! clipped.empty())
local_clip.emplace_back(std::move(clipped));
out_tiles[tile_idx] = _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(local_subject), ClipperUtils::PolygonsProvider(local_clip), do_safety_offset);
});
Slic3r::ExPolygons out;
for (Slic3r::ExPolygons &out_tile : out_tiles)
append(out, std::move(out_tile));
return out;
}
Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
{ return clipper_ex_by_piece(ctDifference, subject, clip, do_safety_offset); }
Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
{ return clipper_ex_by_piece(ctIntersection, subject, clip, do_safety_offset); }
// May be used to "heal" unusual models (3DLabPrints etc.) by providing fill_type (pftEvenOdd, pftNonZero, pftPositive, pftNegative). // May be used to "heal" unusual models (3DLabPrints etc.) by providing fill_type (pftEvenOdd, pftNonZero, pftPositive, pftNegative).
Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, PolyFillType fill_type) Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, PolyFillType fill_type)
{ return _clipper_ex(ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No, fill_type); } { return _clipper_ex(ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No, fill_type); }
+15
View File
@@ -5,6 +5,7 @@
#include "Polyline.hpp" #include "Polyline.hpp"
#include "Line.hpp" #include "Line.hpp"
#include "libslic3r.h" #include "libslic3r.h"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp" #include "ExPolygon.hpp"
#include "Polygon.hpp" #include "Polygon.hpp"
#include "Surface.hpp" #include "Surface.hpp"
@@ -332,6 +333,15 @@ namespace ClipperUtils {
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygon &src, const BoundingBox &bbox, const bool get_entire_polygons = false); [[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygon &src, const BoundingBox &bbox, const bool get_entire_polygons = false);
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygons &src, const BoundingBox &bbox, const bool get_entire_polygons = false); [[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygons &src, const BoundingBox &bbox, const bool get_entire_polygons = false);
// Splits ExPolygons into tiles by the centres of their boxes, about `per_tile` of them to a tile, to run Clipper on a
// layer of many pieces tile by tile. Returns the non-empty tiles, each with the indices of its ExPolygons and their box.
struct ExPolygonsTile
{
BoundingBox bbox;
std::vector<size_t> members;
};
[[nodiscard]] std::vector<ExPolygonsTile> tile_expolygons(const ExPolygons &expolygons, size_t per_tile);
} }
// offset Polygons // offset Polygons
@@ -527,6 +537,11 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r
Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::Surfaces &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::Surfaces &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
// diff_ex() / intersection_ex() of the subject split into tiles, each against only the part of the clip near it, the tiles in
// parallel. The same area as the operation on the whole subject when its ExPolygons do not overlap, and much faster for a
// subject of thousands of pieces spread over a layer: Clipper slows down with the number of edges crossing a scan line.
Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::Polygon &clip); Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::Polygon &clip);
Slic3r::Polylines intersection_pl(const Slic3r::Polyline &subject, const Slic3r::ExPolygon &clip); Slic3r::Polylines intersection_pl(const Slic3r::Polyline &subject, const Slic3r::ExPolygon &clip);
Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygon &clip); Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygon &clip);
+1 -1
View File
@@ -1432,7 +1432,7 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
} }
if (!header_found) { if (!header_found) {
// TRN %1% is the application name. // TRN %1% is the application name.
std::string error_message = format(_u8L("Not a gcode file generated by %1%."), SLIC3R_APP_FULL_NAME); std::string error_message = format(_u8L("Not a G-code file generated by %1%."), SLIC3R_APP_FULL_NAME);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << error_message; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << error_message;
throw Slic3r::RuntimeError(error_message.c_str()); throw Slic3r::RuntimeError(error_message.c_str());
} }
@@ -1,4 +1,4 @@
#include "FillTpmsAdaptive.hpp" #include "AdaptiveInfillField.hpp"
#include <algorithm> #include <algorithm>
#include <array> #include <array>
@@ -146,11 +146,11 @@ void rasterize(const ExPolygons &expolygons, const Vec2d &origin, double cell, i
} }
// Scale of the pattern relative to the surface at a depth from 0 at the surface to 1 at the deepest point. // Scale of the pattern relative to the surface at a depth from 0 at the surface to 1 at the deepest point.
double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth) double target_scale(double ratio, AdaptiveInfillGradient gradient, double depth)
{ {
switch (gradient) { switch (gradient) {
case TpmsAdaptiveGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth; case AdaptiveInfillGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
case TpmsAdaptiveGradient::Exponential: return std::pow(ratio, depth); case AdaptiveInfillGradient::Exponential: return std::pow(ratio, depth);
default: return 1. + (ratio - 1.) * depth; default: return 1. + (ratio - 1.) * depth;
} }
} }
@@ -159,7 +159,7 @@ double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth)
// continuous level of the target of a depth. // continuous level of the target of a depth.
struct DensityLevels struct DensityLevels
{ {
DensityLevels(double ratio, double step, TpmsAdaptiveGradient gradient) DensityLevels(double ratio, double step, AdaptiveInfillGradient gradient)
: ratio(ratio), gradient(gradient), count(int(std::ceil(std::abs(std::log(ratio)) / std::log(step) - EPSILON))) : ratio(ratio), gradient(gradient), count(int(std::ceil(std::abs(std::log(ratio)) / std::log(step) - EPSILON)))
{} {}
@@ -170,7 +170,7 @@ struct DensityLevels
} }
double ratio; double ratio;
TpmsAdaptiveGradient gradient; AdaptiveInfillGradient gradient;
int count; int count;
}; };
@@ -212,12 +212,12 @@ private:
} // namespace } // namespace
TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode, AdaptiveInfillField::AdaptiveInfillField(const std::vector<Slice> &slices, const BoundingBox &bbox, AdaptiveInfillMode mode,
const std::function<void()> &throw_if_canceled) const std::function<void()> &throw_if_canceled)
: m_mode(mode) : m_mode(mode)
, m_axis(mode == TpmsAdaptiveMode::NormalX ? 0 : mode == TpmsAdaptiveMode::NormalY ? 1 : mode == TpmsAdaptiveMode::NormalZ ? 2 : -1) , m_axis(mode == AdaptiveInfillMode::NormalX ? 0 : mode == AdaptiveInfillMode::NormalY ? 1 : mode == AdaptiveInfillMode::NormalZ ? 2 : -1)
{ {
assert(!slices.empty() && mode != TpmsAdaptiveMode::Disabled); assert(!slices.empty() && mode != AdaptiveInfillMode::Disabled);
const Vec3d min(unscaled(bbox.min.x()), unscaled(bbox.min.y()), slices.front().bottom_z); const Vec3d min(unscaled(bbox.min.x()), unscaled(bbox.min.y()), slices.front().bottom_z);
const Vec3d extent = Vec3d(unscaled(bbox.max.x()), unscaled(bbox.max.y()), slices.back().top_z) - min; const Vec3d extent = Vec3d(unscaled(bbox.max.x()), unscaled(bbox.max.y()), slices.back().top_z) - min;
@@ -269,7 +269,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
// Bodies are the connected inside nodes, none of which is on the border. The deepest nodes of a body are the // Bodies are the connected inside nodes, none of which is on the border. The deepest nodes of a body are the
// centers of its lobes, unless the depth between them stays above NeckRatio; where the depth ties, the center // centers of its lobes, unless the depth between them stays above NeckRatio; where the depth ties, the center
// is the node nearest to the middle of the tied nodes. // is the node nearest to the middle of the tied nodes.
const bool lobes = m_mode != TpmsAdaptiveMode::SteppedShells && m_mode != TpmsAdaptiveMode::SmoothBlend; const bool lobes = m_mode != AdaptiveInfillMode::SteppedShells && m_mode != AdaptiveInfillMode::SmoothBlend;
m_body.assign(depth.size(), -1); m_body.assign(depth.size(), -1);
std::vector<size_t> body_nodes; std::vector<size_t> body_nodes;
for (size_t seed = 0; seed < depth.size(); ++seed) { for (size_t seed = 0; seed < depth.size(); ++seed) {
@@ -341,7 +341,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
} }
throw_if_canceled(); throw_if_canceled();
if (m_mode == TpmsAdaptiveMode::SteppedShells || m_mode == TpmsAdaptiveMode::SmoothBlend || m_mode == TpmsAdaptiveMode::DistanceWarp) { if (m_mode == AdaptiveInfillMode::SteppedShells || m_mode == AdaptiveInfillMode::SmoothBlend || m_mode == AdaptiveInfillMode::DistanceWarp) {
m_depth.assign(depth.size(), 0.f); m_depth.assign(depth.size(), 0.f);
for (size_t i = 0; i < depth.size(); ++i) for (size_t i = 0; i < depth.size(); ++i)
if (m_body[i] >= 0) if (m_body[i] >= 0)
@@ -421,7 +421,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
lobe.reach.resize(log_reach.size()); lobe.reach.resize(log_reach.size());
std::transform(log_reach.begin(), log_reach.end(), lobe.reach.begin(), [](double v) { return float(std::exp(v)); }); std::transform(log_reach.begin(), log_reach.end(), lobe.reach.begin(), [](double v) { return float(std::exp(v)); });
if (m_mode == TpmsAdaptiveMode::DistanceWarp) { if (m_mode == AdaptiveInfillMode::DistanceWarp) {
std::vector<double> mean(log_reach.size() * (Samples + 1)); std::vector<double> mean(log_reach.size() * (Samples + 1));
for (size_t d = 0; d < log_reach.size(); ++d) { for (size_t d = 0; d < log_reach.size(); ++d) {
const Vec3d dir = direction(int(d) / Azimuth, int(d) % Azimuth); const Vec3d dir = direction(int(d) / Azimuth, int(d) % Azimuth);
@@ -488,7 +488,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
} }
} }
double TpmsRadialField::depth(const Vec3d &pt) const double AdaptiveInfillField::depth(const Vec3d &pt) const
{ {
assert(!m_depth.empty()); assert(!m_depth.empty());
const Vec3d f = (pt - m_origin) / m_cell; const Vec3d f = (pt - m_origin) / m_cell;
@@ -508,7 +508,7 @@ double TpmsRadialField::depth(const Vec3d &pt) const
return value; return value;
} }
Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d &center) const Vec3d AdaptiveInfillField::offset(const Vec3d &pt, const Vec3d &center) const
{ {
Vec3d d = pt - center; Vec3d d = pt - center;
if (m_axis >= 0) if (m_axis >= 0)
@@ -516,7 +516,7 @@ Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d &center) const
return d; return d;
} }
double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const double AdaptiveInfillField::radial(const Lobe &lobe, const Vec3d &pt) const
{ {
const Vec3d d = this->offset(pt, lobe.center); const Vec3d d = this->offset(pt, lobe.center);
const double r = d.norm(); const double r = d.norm();
@@ -562,7 +562,7 @@ double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const
return warp(mean); return warp(mean);
} }
size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const size_t AdaptiveInfillField::radial(const Vec3d &pt, Radials &out) const
{ {
assert(!this->empty()); assert(!this->empty());
Vec3i32 idx; Vec3i32 idx;
@@ -588,7 +588,7 @@ size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const
return count; return count;
} }
size_t TpmsRadialField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const size_t AdaptiveInfillField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const
{ {
const Body &body = m_bodies[m_body[node]]; const Body &body = m_bodies[m_body[node]];
if (body.lobes == 1) { if (body.lobes == 1) {
@@ -641,7 +641,7 @@ struct AdaptiveTpmsField
const long gsize = std::lround(gsizef / rsizef); const long gsize = std::lround(gsizef / rsizef);
const AdaptiveTpms &tpms; const AdaptiveTpms &tpms;
const TpmsRadialField &radial_field; const AdaptiveInfillField &radial_field;
RadialScale scale; RadialScale scale;
DensityLevels levels; DensityLevels levels;
Point size; Point size;
@@ -650,7 +650,7 @@ struct AdaptiveTpmsField
double cos_angle; double cos_angle;
double sin_angle; double sin_angle;
AdaptiveTpmsField(const AdaptiveTpms &tpms, const TpmsRadialField &radial_field, const BoundingBox &bbox, coordf_t z, float angle) AdaptiveTpmsField(const AdaptiveTpms &tpms, const AdaptiveInfillField &radial_field, const BoundingBox &bbox, coordf_t z, float angle)
: tpms(tpms), radial_field(radial_field), scale(tpms, radial_field.axis() < 0 ? 3 : 2) : tpms(tpms), radial_field(radial_field), scale(tpms, radial_field.axis() < 0 ? 3 : 2)
, levels(std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3), BlendRatio, tpms.gradient) , levels(std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3), BlendRatio, tpms.gradient)
, size(bbox.size()), offs(bbox.min), z(z) , size(bbox.size()), offs(bbox.min), z(z)
@@ -666,7 +666,7 @@ struct AdaptiveTpmsField
const double x = unscaled(pt.x()); const double x = unscaled(pt.x());
const double y = unscaled(pt.y()); const double y = unscaled(pt.y());
const Vec3d obj(cos_angle * x - sin_angle * y, sin_angle * x + cos_angle * y, z); const Vec3d obj(cos_angle * x - sin_angle * y, sin_angle * x + cos_angle * y, z);
if (radial_field.mode() == TpmsAdaptiveMode::SmoothBlend) { if (radial_field.mode() == AdaptiveInfillMode::SmoothBlend) {
// The regular patterns of the two levels around the target of the depth, blended by a smoothstep. // The regular patterns of the two levels around the target of the depth, blended by a smoothstep.
auto lattice = [this, x, y](int level) { auto lattice = [this, x, y](int level) {
const double frequency = tpms.surface_frequency * levels.scale(level); const double frequency = tpms.surface_frequency * levels.scale(level);
@@ -681,7 +681,7 @@ struct AdaptiveTpmsField
return float((w < 1. ? (1. - w) * lattice(k) : 0.) + (w > 0. ? w * lattice(k + 1) : 0.)); return float((w < 1. ? (1. - w) * lattice(k) : 0.) + (w > 0. ? w * lattice(k + 1) : 0.));
} }
const int axis = radial_field.axis(); const int axis = radial_field.axis();
TpmsRadialField::Radials radials; AdaptiveInfillField::Radials radials;
const size_t count = radial_field.radial(obj, radials); const size_t count = radial_field.radial(obj, radials);
float value = 0.f; float value = 0.f;
for (size_t i = 0; i < count; ++i) { for (size_t i = 0; i < count; ++i) {
@@ -708,20 +708,20 @@ template<> struct _RasterTraits<AdaptiveTpmsField>
}; };
// Continuous density level of the depth over a layer, in the object frame. // Continuous density level of the depth over a layer, in the object frame.
struct TpmsLevelField struct AdaptiveLevelField
{ {
static constexpr float gsizef = 0.5f; static constexpr float gsizef = 0.5f;
static constexpr float rsizef = 0.05f; static constexpr float rsizef = 0.05f;
const coord_t rsize = scaled(rsizef); const coord_t rsize = scaled(rsizef);
const long gsize = std::lround(gsizef / rsizef); const long gsize = std::lround(gsizef / rsizef);
const TpmsRadialField &field; const AdaptiveInfillField &field;
const DensityLevels &levels; const DensityLevels &levels;
Point size; Point size;
Point offs; Point offs;
double z; double z;
TpmsLevelField(const TpmsRadialField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z) AdaptiveLevelField(const AdaptiveInfillField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
: field(field), levels(levels), size(bbox.size()), offs(bbox.min), z(z) : field(field), levels(levels), size(bbox.size()), offs(bbox.min), z(z)
{} {}
@@ -736,19 +736,19 @@ struct TpmsLevelField
inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); } inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); }
}; };
template<> struct _RasterTraits<TpmsLevelField> template<> struct _RasterTraits<AdaptiveLevelField>
{ {
using ValueType = float; using ValueType = float;
static float get(const TpmsLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); } static float get(const AdaptiveLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
static size_t rows(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.y()); } static size_t rows(const AdaptiveLevelField &sf) { return sf.to_coordr(sf.size.y()); }
static size_t cols(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.x()); } static size_t cols(const AdaptiveLevelField &sf) { return sf.to_coordr(sf.size.x()); }
}; };
} // namespace marchsq } // namespace marchsq
namespace Slic3r { namespace Slic3r {
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox, Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField &field, BoundingBox bbox,
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle) coordf_t z, coordf_t layer_height, coordf_t spacing, float angle)
{ {
// A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region. // A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region.
@@ -775,21 +775,21 @@ Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &fi
return polylines; return polylines;
} }
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, TpmsAdaptiveGradient gradient) float surface_density, float interior_density, AdaptiveInfillGradient gradient)
{ {
const DensityLevels levels(interior_density / surface_density, ShellRatio, gradient); const DensityLevels levels(interior_density / surface_density, ShellRatio, gradient);
if (levels.count == 0) if (levels.count == 0)
return {{surface_density, {expolygon}}}; return {{surface_density, {expolygon}}};
// A fixed sampling grid, so that every region of a layer gets the same shells. // A fixed sampling grid, so that every region of a layer gets the same shells.
const coord_t cell = scaled(marchsq::TpmsLevelField::gsizef); const coord_t cell = scaled(marchsq::AdaptiveLevelField::gsizef);
BoundingBox bbox = get_extents(expolygon); BoundingBox bbox = get_extents(expolygon);
bbox.offset(cell); bbox.offset(cell);
bbox.merge(align_to_grid(bbox.min, Point(cell, cell))); bbox.merge(align_to_grid(bbox.min, Point(cell, cell)));
const marchsq::TpmsLevelField raster(field, levels, bbox, z); const marchsq::AdaptiveLevelField raster(field, levels, bbox, z);
// Each level takes the part deeper than the middle between it and the previous one. // Each level takes the part deeper than the middle between it and the previous one.
std::vector<TpmsShell> shells; std::vector<AdaptiveInfillShell> shells;
ExPolygons remaining{expolygon}; ExPolygons remaining{expolygon};
for (int level = 0; level < levels.count && !remaining.empty(); ++level) { for (int level = 0; level < levels.count && !remaining.empty(); ++level) {
Polygons deeper; Polygons deeper;
@@ -808,12 +808,12 @@ std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPo
return shells; return shells;
} }
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing, void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell) const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell)
{ {
FillParams shell_params = params; FillParams shell_params = params;
shell_params.tpms_adaptive = TpmsAdaptiveMode::Disabled; shell_params.adaptive_infill_mode = AdaptiveInfillMode::Disabled;
for (const TpmsShell &shell : make_tpms_shells(field, expolygon, z, params.density, params.tpms_interior_density, params.tpms_adaptive_gradient)) { for (const AdaptiveInfillShell &shell : make_adaptive_infill_shells(field, expolygon, z, params.density, params.adaptive_infill_interior_density, params.adaptive_infill_gradient)) {
shell_params.density = shell.density; shell_params.density = shell.density;
for (const ExPolygon &part : offset_ex(shell.expolygons, -float(scale_(0.5 * spacing)))) for (const ExPolygon &part : offset_ex(shell.expolygons, -float(scale_(0.5 * spacing))))
fill_shell(shell_params, part); fill_shell(shell_params, part);
@@ -21,7 +21,7 @@ namespace Slic3r {
// lobe, 1 at its surface. Lobes are parts of a body separated by a neck, like two spheres united. In the 2D modes, // lobe, 1 at its surface. Lobes are parts of a body separated by a neck, like two spheres united. In the 2D modes,
// every section normal to the axis has its own bodies and lobes, and distances are measured within the section. The // every section normal to the axis has its own bodies and lobes, and distances are measured within the section. The
// modes following the distance to the surface use the depth of every point instead; Distance warp also the lobes. // modes following the distance to the surface use the depth of every point instead; Distance warp also the lobes.
class TpmsRadialField class AdaptiveInfillField
{ {
public: public:
struct Slice struct Slice
@@ -39,7 +39,7 @@ public:
}; };
// Slices sorted by z, in the XY coordinates of the fill and the print Z. // Slices sorted by z, in the XY coordinates of the fill and the print Z.
TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode, AdaptiveInfillField(const std::vector<Slice> &slices, const BoundingBox &bbox, AdaptiveInfillMode mode,
const std::function<void()> &throw_if_canceled); const std::function<void()> &throw_if_canceled);
// Lobes blended near the sides between them, from a body or from each of the two sections around a point. // Lobes blended near the sides between them, from a body or from each of the two sections around a point.
@@ -56,7 +56,7 @@ public:
// Axis normal to the sections in the 2D modes, -1 in the modes graded in 3D. // Axis normal to the sections in the 2D modes, -1 in the modes graded in 3D.
int axis() const { return m_axis; } int axis() const { return m_axis; }
TpmsAdaptiveMode mode() const { return m_mode; } AdaptiveInfillMode mode() const { return m_mode; }
// In the modes following the distance to the surface: depth relative to the deepest point of the body, from 0 at // In the modes following the distance to the surface: depth relative to the deepest point of the body, from 0 at
// the surface to 1. // the surface to 1.
@@ -91,7 +91,7 @@ private:
static constexpr int Polar = 24; static constexpr int Polar = 24;
static constexpr int Azimuth = 48; static constexpr int Azimuth = 48;
TpmsAdaptiveMode m_mode; AdaptiveInfillMode m_mode;
int m_axis; int m_axis;
Vec3d m_origin; Vec3d m_origin;
double m_cell; double m_cell;
@@ -106,9 +106,9 @@ private:
std::vector<float> m_depth; std::vector<float> m_depth;
}; };
using TpmsRadialFieldPtr = std::unique_ptr<TpmsRadialField>; using AdaptiveInfillFieldPtr = std::unique_ptr<AdaptiveInfillField>;
// A field for every adaptive mode in use, indexed by the mode. // A field for every adaptive mode in use, indexed by the mode.
using TpmsRadialFields = std::array<TpmsRadialFieldPtr, size_t(TpmsAdaptiveMode::Count)>; using AdaptiveInfillFields = std::array<AdaptiveInfillFieldPtr, size_t(AdaptiveInfillMode::Count)>;
struct AdaptiveTpms struct AdaptiveTpms
{ {
@@ -117,14 +117,14 @@ struct AdaptiveTpms
// Pattern frequencies at the surface and at the center, in radians per mm. // Pattern frequencies at the surface and at the center, in radians per mm.
double surface_frequency; double surface_frequency;
double interior_frequency; double interior_frequency;
TpmsAdaptiveGradient gradient; AdaptiveInfillGradient gradient;
}; };
// Infill lines in the fill frame, the object frame rotated by -angle; z is the print_z of the layer. // Infill lines in the fill frame, the object frame rotated by -angle; z is the print_z of the layer.
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox, Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField &field, BoundingBox bbox,
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle); coordf_t z, coordf_t layer_height, coordf_t spacing, float angle);
struct TpmsShell struct AdaptiveInfillShell
{ {
float density; float density;
ExPolygons expolygons; ExPolygons expolygons;
@@ -132,12 +132,12 @@ struct TpmsShell
// Stepped shells: the parts of an expolygon in the object frame at each density, from the surface inwards; z is the // Stepped shells: the parts of an expolygon in the object frame at each density, from the surface inwards; z is the
// middle of the layer. // middle of the layer.
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, TpmsAdaptiveGradient gradient); float surface_density, float interior_density, AdaptiveInfillGradient gradient);
// Stepped shells: fills every shell with fill_shell at its density, each shrunk by half a line like a filled region, // Stepped shells: fills every shell with fill_shell at its density, each shrunk by half a line like a filled region,
// so the lines connected along the boundaries of two shells don't overlap. // so the lines connected along the boundaries of two shells don't overlap.
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing, void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell); const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell);
} // namespace Slic3r } // namespace Slic3r
+54 -35
View File
@@ -22,6 +22,8 @@
#include "../PrintConfig.hpp" #include "../PrintConfig.hpp"
#include "../Surface.hpp" #include "../Surface.hpp"
#include <tbb/parallel_for.h>
#include "AABBTreeLines.hpp" #include "AABBTreeLines.hpp"
#include "libslic3r/Config.hpp" #include "libslic3r/Config.hpp"
#include "libslic3r/ExPolygon.hpp" #include "libslic3r/ExPolygon.hpp"
@@ -312,9 +314,9 @@ struct SurfaceFillParams
bool gyroid_optimized = false; bool gyroid_optimized = false;
// For TPMS: grade the density from the surface to the interior of the object. // For TPMS: grade the density from the surface to the interior of the object.
TpmsAdaptiveMode tpms_adaptive = TpmsAdaptiveMode::Disabled; AdaptiveInfillMode adaptive_infill_mode = AdaptiveInfillMode::Disabled;
float tpms_interior_density = 0.f; float adaptive_infill_interior_density = 0.f;
TpmsAdaptiveGradient tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear; AdaptiveInfillGradient adaptive_infill_gradient = AdaptiveInfillGradient::Linear;
// Orca: corner smoothing factor in the range [0, 1]. // Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. }; double smooth_factor { 0. };
@@ -358,9 +360,9 @@ struct SurfaceFillParams
RETURN_COMPARE_NON_EQUAL(skin_infill_depth); RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle); RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
RETURN_COMPARE_NON_EQUAL(gyroid_optimized); RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
RETURN_COMPARE_NON_EQUAL(tpms_adaptive); RETURN_COMPARE_NON_EQUAL(adaptive_infill_mode);
RETURN_COMPARE_NON_EQUAL(tpms_interior_density); RETURN_COMPARE_NON_EQUAL(adaptive_infill_interior_density);
RETURN_COMPARE_NON_EQUAL(tpms_adaptive_gradient); RETURN_COMPARE_NON_EQUAL(adaptive_infill_gradient);
RETURN_COMPARE_NON_EQUAL(smooth_factor); RETURN_COMPARE_NON_EQUAL(smooth_factor);
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern); RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
RETURN_COMPARE_NON_EQUAL(separated_infills); RETURN_COMPARE_NON_EQUAL(separated_infills);
@@ -394,9 +396,9 @@ struct SurfaceFillParams
this->center_of_surface_pattern == rhs.center_of_surface_pattern && this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
this->separated_infills == rhs.separated_infills && this->separated_infills == rhs.separated_infills &&
this->gyroid_optimized == rhs.gyroid_optimized && this->gyroid_optimized == rhs.gyroid_optimized &&
this->tpms_adaptive == rhs.tpms_adaptive && this->adaptive_infill_mode == rhs.adaptive_infill_mode &&
this->tpms_interior_density == rhs.tpms_interior_density && this->adaptive_infill_interior_density == rhs.adaptive_infill_interior_density &&
this->tpms_adaptive_gradient == rhs.tpms_adaptive_gradient && this->adaptive_infill_gradient == rhs.adaptive_infill_gradient &&
this->smooth_factor == rhs.smooth_factor && this->smooth_factor == rhs.smooth_factor &&
this->fill_order == rhs.fill_order; this->fill_order == rhs.fill_order;
} }
@@ -675,24 +677,28 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
if (!line_based_pattern) { if (!line_based_pattern) {
const coord_t scaled_spacing = scaled<coord_t>(fill.params.spacing); const coord_t scaled_spacing = scaled<coord_t>(fill.params.spacing);
for (const ExPolygon &expolygon : fill.expolygons) { // Each expolygon is split on its own, so they run in parallel and are collected in their original order.
std::vector<std::pair<ExPolygons, ExPolygons>> split_parts(fill.expolygons.size()); // normal, narrow
tbb::parallel_for(size_t(0), fill.expolygons.size(), [&](size_t idx) {
const ExPolygon &expolygon = fill.expolygons[idx];
Polygons filled_area = to_polygons(expolygon); Polygons filled_area = to_polygons(expolygon);
// "Core" area: open (erode+dilate) to drop thin features, then clamp back to the original polygon. // "Core" area: open (erode+dilate) to drop thin features, then clamp back to the original polygon.
Polygons inner_area = intersection(filled_area, opening(filled_area, scaled_spacing, scaled_spacing)); Polygons inner_area = intersection(filled_area, opening(filled_area, scaled_spacing, scaled_spacing));
if (inner_area.empty()) { if (inner_area.empty()) {
narrow_infill.emplace_back(expolygon); split_parts[idx].second.emplace_back(expolygon);
continue; return;
} }
ExPolygons inner_ex = union_ex(inner_area); ExPolygons inner_ex = union_ex(inner_area);
ExPolygons expolys{expolygon}; ExPolygons expolys{expolygon};
ExPolygons narrow_ex = diff_ex(expolys, inner_ex); split_parts[idx].second = diff_ex(expolys, inner_ex); // narrow infill area
ExPolygons normal_ex = intersection_ex(expolys, inner_ex); split_parts[idx].first = intersection_ex(expolys, inner_ex); // normal infill area
});
append(normal_infill, normal_ex); // normal infill area for (auto &[normal_ex, narrow_ex] : split_parts) {
append(narrow_infill, narrow_ex); // narrow infill area append(normal_infill, std::move(normal_ex));
append(narrow_infill, std::move(narrow_ex));
} }
return; return;
@@ -714,7 +720,10 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
} }
const double aligning_angle = -base_angle + PI; const double aligning_angle = -base_angle + PI;
for (const ExPolygon &expolygon : fill.expolygons) { // Each expolygon is reconstructed on its own, so they run in parallel and are collected in their original order.
std::vector<Polygons> split_reconstructed(fill.expolygons.size());
tbb::parallel_for(size_t(0), fill.expolygons.size(), [&](size_t expolygon_idx) {
const ExPolygon &expolygon = fill.expolygons[expolygon_idx];
Polygons filled_area = to_polygons(expolygon); Polygons filled_area = to_polygons(expolygon);
polygons_rotate(filled_area, aligning_angle); polygons_rotate(filled_area, aligning_angle);
BoundingBox bb = get_extents(filled_area); BoundingBox bb = get_extents(filled_area);
@@ -845,8 +854,10 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
} }
} }
polygons_append(normal_fill_areas, reconstructed_area); split_reconstructed[expolygon_idx] = std::move(reconstructed_area);
} });
for (Polygons &reconstructed_area : split_reconstructed)
polygons_append(normal_fill_areas, std::move(reconstructed_area));
polygons_rotate(normal_fill_areas, -aligning_angle); polygons_rotate(normal_fill_areas, -aligning_angle);
@@ -1010,17 +1021,17 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface; params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface;
// Orca: only the TPMS sparse infill is graded; reset otherwise, as params is reused. // Orca: only the TPMS sparse infill is graded; reset otherwise, as params is reused.
params.tpms_adaptive = is_tpms_adaptive_pattern(params.pattern) && params.extrusion_role == erInternalInfill ? params.adaptive_infill_mode = is_adaptive_infill_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
region_config.tpms_adaptive.value : TpmsAdaptiveMode::Disabled; region_config.adaptive_infill_mode.value : AdaptiveInfillMode::Disabled;
const bool tpms_adaptive = params.tpms_adaptive != TpmsAdaptiveMode::Disabled; const bool adaptive_infill_mode = params.adaptive_infill_mode != AdaptiveInfillMode::Disabled;
params.tpms_interior_density = tpms_adaptive ? std::max(1.f, float(region_config.tpms_interior_density)) : 0.f; params.adaptive_infill_interior_density = adaptive_infill_mode ? std::max(1.f, float(region_config.adaptive_infill_interior_density)) : 0.f;
params.tpms_adaptive_gradient = tpms_adaptive ? region_config.tpms_adaptive_gradient.value : TpmsAdaptiveGradient::Linear; params.adaptive_infill_gradient = adaptive_infill_mode ? region_config.adaptive_infill_gradient.value : AdaptiveInfillGradient::Linear;
// Pass through gyroid_optimized only when the effective pattern is Gyroid, // Pass through gyroid_optimized only when the effective pattern is Gyroid,
// so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag // so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag
// (which would unnecessarily split fill batching). Adaptive density replaces it. // (which would unnecessarily split fill batching). Adaptive density replaces it.
// Stored on SurfaceFillParams; copied to FillParams during conversion. // Stored on SurfaceFillParams; copied to FillParams during conversion.
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !tpms_adaptive; params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !adaptive_infill_mode;
if (params.extrusion_role == erInternalInfill) { if (params.extrusion_role == erInternalInfill) {
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value, params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
@@ -1373,7 +1384,7 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
f->angle = surface_fill.params.angle; f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle; f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr; const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive); f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
f->print_config = &this->object()->print()->config(); f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config(); f->print_object_config = &this->object()->config();
if (surface_fill.params.pattern == ipConcentricInternal) { if (surface_fill.params.pattern == ipConcentricInternal) {
@@ -1423,9 +1434,9 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2; params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle; params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.gyroid_optimized = surface_fill.params.gyroid_optimized; params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.tpms_adaptive = surface_fill.params.tpms_adaptive; params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density); params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient; params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
params.smooth_factor = surface_fill.params.smooth_factor; params.smooth_factor = surface_fill.params.smooth_factor;
// BBS // BBS
@@ -1473,7 +1484,15 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox); f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox);
f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr; f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr;
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes); // Only the part of the layer-wide no-overlap area under this expolygon matters, so clip it to the
// expolygon's box first (padded past the safety offset, which grows the clip side). The result is
// identical; the cost is not: a layer split into many small fills, e.g. by colour painting,
// otherwise intersects every one of them with the whole layer.
BoundingBox no_overlap_bbox = get_extents(expoly);
no_overlap_bbox.offset(SCALED_EPSILON);
f->no_overlap_expolygons = intersection_ex(
ClipperUtils::clip_clipper_polygons_with_subject_bbox(surface_fill.no_overlap_expolygons, no_overlap_bbox),
ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
if (params.symmetric_infill_y_axis) { if (params.symmetric_infill_y_axis) {
params.symmetric_y_axis = f->extended_object_bounding_box().center().x(); params.symmetric_y_axis = f->extended_object_bounding_box().center().x();
expoly.symmetric_y(params.symmetric_y_axis); expoly.symmetric_y(params.symmetric_y_axis);
@@ -1598,7 +1617,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
f->angle = surface_fill.params.angle; f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle; f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr; const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive); f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
f->print_config = &this->object()->print()->config(); f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config(); f->print_object_config = &this->object()->config();
@@ -1638,9 +1657,9 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle; params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.multiline = surface_fill.params.multiline; params.multiline = surface_fill.params.multiline;
params.gyroid_optimized = surface_fill.params.gyroid_optimized; params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.tpms_adaptive = surface_fill.params.tpms_adaptive; params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density); params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient; params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
params.smooth_factor = surface_fill.params.smooth_factor; params.smooth_factor = surface_fill.params.smooth_factor;
// Orca: Match make_fills() when choosing the origin of plane-path patterns. // Orca: Match make_fills() when choosing the origin of plane-path patterns.
// Without the sparse extrusion role, the filler uses each surface's bounds // Without the sparse extrusion role, the filler uses each surface's bounds
+5 -5
View File
@@ -33,7 +33,7 @@ namespace Slic3r { class ExtrusionEntityCollection; }
namespace Slic3r { namespace Slic3r {
class Surface; class Surface;
class TpmsRadialField; class AdaptiveInfillField;
enum InfillPattern : int; enum InfillPattern : int;
namespace FillAdaptive { namespace FillAdaptive {
@@ -93,9 +93,9 @@ struct FillParams
bool gyroid_optimized { false }; bool gyroid_optimized { false };
// For TPMS: grade the density from the surface to the interior of the object. Density fraction. // For TPMS: grade the density from the surface to the interior of the object. Density fraction.
TpmsAdaptiveMode tpms_adaptive { TpmsAdaptiveMode::Disabled }; AdaptiveInfillMode adaptive_infill_mode { AdaptiveInfillMode::Disabled };
float tpms_interior_density { 0.f }; float adaptive_infill_interior_density { 0.f };
TpmsAdaptiveGradient tpms_adaptive_gradient { TpmsAdaptiveGradient::Linear }; AdaptiveInfillGradient adaptive_infill_gradient { AdaptiveInfillGradient::Linear };
// Orca: corner smoothing factor in the range [0, 1]. // Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. }; double smooth_factor { 0. };
@@ -162,7 +162,7 @@ public:
FillAdaptive::Octree* adapt_fill_octree = nullptr; FillAdaptive::Octree* adapt_fill_octree = nullptr;
// Radial coordinate inside the object for the adaptive TPMS infill // Radial coordinate inside the object for the adaptive TPMS infill
const TpmsRadialField* tpms_radial_field = nullptr; const AdaptiveInfillField* adaptive_infill_field = nullptr;
// PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring). // PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring).
// Orca: also used by gap fill function. // Orca: also used by gap fill function.
+6 -6
View File
@@ -18,7 +18,7 @@
#include "FillGyroid.hpp" #include "FillGyroid.hpp"
#include "libslic3r/Polygon.hpp" #include "libslic3r/Polygon.hpp"
#include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfig.hpp"
#include "FillTpmsAdaptive.hpp" #include "AdaptiveInfillField.hpp"
namespace Slic3r { namespace Slic3r {
@@ -315,8 +315,8 @@ void FillGyroid::_fill_surface_single(
ExPolygon expolygon, ExPolygon expolygon,
Polylines &polylines_out) Polylines &polylines_out)
{ {
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) { if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing, fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) { [&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out); this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
}); });
@@ -342,11 +342,11 @@ void FillGyroid::_fill_surface_single(
// generate pattern // generate pattern
Polylines polylines; Polylines polylines;
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) { if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
// Radians per mm of the regular pattern at a density. // Radians per mm of the regular pattern at a density.
auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); }; auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient}, polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.adaptive_infill_interior_density), params.adaptive_infill_gradient},
*this->tpms_radial_field, bb, this->z, params.layer_height, this->spacing, infill_angle); *this->adaptive_infill_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
} else if (params.gyroid_optimized) { } else if (params.gyroid_optimized) {
// Marching-squares path on the gyroid implicit field. Base period matches // Marching-squares path on the gyroid implicit field. Base period matches
// the standard parametric path's wavelength: 2*pi * spacing / density_adj. // the standard parametric path's wavelength: 2*pi * spacing / density_adj.
+6 -6
View File
@@ -17,7 +17,7 @@
#include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "FillTpmsD.hpp" #include "FillTpmsD.hpp"
#include "FillTpmsAdaptive.hpp" #include "AdaptiveInfillField.hpp"
namespace Slic3r { namespace Slic3r {
@@ -117,8 +117,8 @@ void FillTpmsD::_fill_surface_single(
ExPolygon expolygon, ExPolygon expolygon,
Polylines &polylines_out) Polylines &polylines_out)
{ {
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) { if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing, fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) { [&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out); this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
}); });
@@ -130,13 +130,13 @@ void FillTpmsD::_fill_surface_single(
expolygon.rotate(-infill_angle); expolygon.rotate(-infill_angle);
Polylines polylines; Polylines polylines;
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) { if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
// Radians per mm of the regular pattern at a density. // Radians per mm of the regular pattern at a density.
auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); }; auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
BoundingBox bbox = expolygon.contour.bounding_box(); BoundingBox bbox = expolygon.contour.bounding_box();
bbox.offset(scale_((params.multiline + 1) * this->spacing)); bbox.offset(scale_((params.multiline + 1) * this->spacing));
polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient}, polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.adaptive_infill_interior_density), params.adaptive_infill_gradient},
*this->tpms_radial_field, bbox, this->z, params.layer_height, this->spacing, infill_angle); *this->adaptive_infill_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
} else { } else {
BoundingBox bb = expolygon.contour.bounding_box(); BoundingBox bb = expolygon.contour.bounding_box();
// Density adjusted to have a good %of weight. // Density adjusted to have a good %of weight.
+7 -7
View File
@@ -8,7 +8,7 @@
#include "libslic3r/Fill/FillBase.hpp" #include "libslic3r/Fill/FillBase.hpp"
#include "libslic3r/ExPolygon.hpp" #include "libslic3r/ExPolygon.hpp"
#include "FillTpmsFK.hpp" #include "FillTpmsFK.hpp"
#include "FillTpmsAdaptive.hpp" #include "AdaptiveInfillField.hpp"
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <cstddef> #include <cstddef>
@@ -132,8 +132,8 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
ExPolygon expolygon, ExPolygon expolygon,
Polylines& polylines_out) Polylines& polylines_out)
{ {
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) { if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing, fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) { [&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out); this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
}); });
@@ -151,10 +151,10 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
// Enlarge the bounding box by the multi-line width to avoid artifacts at the edges. // Enlarge the bounding box by the multi-line width to avoid artifacts at the edges.
bbox.offset(scale_((params.multiline + 1) * spacing)); bbox.offset(scale_((params.multiline + 1) * spacing));
Polylines polylines; Polylines polylines;
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) { if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.tpms_interior_density), polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.adaptive_infill_interior_density),
params.tpms_adaptive_gradient}, params.adaptive_infill_gradient},
*this->tpms_radial_field, bbox, this->z, params.layer_height, spacing, infill_angle); *this->adaptive_infill_field, bbox, this->z, params.layer_height, spacing, infill_angle);
} else { } else {
marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, period(params.density)); marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, period(params.density));
// Get simplified lines using coarse tolerance of 0.1mm (this is infill). // Get simplified lines using coarse tolerance of 0.1mm (this is infill).
+15 -5
View File
@@ -988,7 +988,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
MetadataList metadata; MetadataList metadata;
VolumeMetadataList volumes; VolumeMetadataList volumes;
bool auto_drop { true }; std::optional<bool> auto_drop;
}; };
struct CutObjectInfo struct CutObjectInfo
@@ -1208,6 +1208,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
//IdToGeometryMap m_orig_geometries; // backup & restore //IdToGeometryMap m_orig_geometries; // backup & restore
CurrentConfig m_curr_config; CurrentConfig m_curr_config;
IdToMetadataMap m_objects_metadata; IdToMetadataMap m_objects_metadata;
std::set<int> m_items_auto_drop_off; // object ids with a build item auto_drop="0" (2.4.x layout)
IdToCutObjectInfoMap m_cut_object_infos; IdToCutObjectInfoMap m_cut_object_infos;
IdToLayerHeightsProfileMap m_layer_heights_profiles; IdToLayerHeightsProfileMap m_layer_heights_profiles;
IdToLayerConfigRangesMap m_layer_config_ranges; IdToLayerConfigRangesMap m_layer_config_ranges;
@@ -1463,6 +1464,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_objects.clear(); m_objects.clear();
m_instances.clear(); m_instances.clear();
m_objects_metadata.clear(); m_objects_metadata.clear();
m_items_auto_drop_off.clear();
m_curr_metadata_name.clear(); m_curr_metadata_name.clear();
m_curr_characters.clear(); m_curr_characters.clear();
@@ -1502,6 +1504,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_curr_config.object_id = -1; m_curr_config.object_id = -1;
m_curr_config.volume_id = -1; m_curr_config.volume_id = -1;
m_objects_metadata.clear(); m_objects_metadata.clear();
m_items_auto_drop_off.clear();
m_layer_heights_profiles.clear(); m_layer_heights_profiles.clear();
m_layer_config_ranges.clear(); m_layer_config_ranges.clear();
m_brim_ear_points.clear(); m_brim_ear_points.clear();
@@ -2257,11 +2260,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
ObjectMetadata::VolumeMetadataList volumes; ObjectMetadata::VolumeMetadataList volumes;
ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr; ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr;
// 2.4.x stored auto_drop on build items, shared by all instances of an object; any "off" wins
const bool legacy_auto_drop = !m_items_auto_drop_off.count(object.first.second);
IdToMetadataMap::iterator obj_metadata = m_objects_metadata.find(object.first.second); IdToMetadataMap::iterator obj_metadata = m_objects_metadata.find(object.first.second);
if (obj_metadata != m_objects_metadata.end()) { if (obj_metadata != m_objects_metadata.end()) {
// config data has been found, this model was saved using slic3r pe // config data has been found, this model was saved using slic3r pe
model_object->auto_drop = obj_metadata->second.auto_drop; model_object->auto_drop = obj_metadata->second.auto_drop.value_or(legacy_auto_drop);
// apply object's name and config data // apply object's name and config data
for (const Metadata& metadata : obj_metadata->second.metadata) { for (const Metadata& metadata : obj_metadata->second.metadata) {
@@ -2279,6 +2285,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
} }
else { else {
// config data not found, this model was not saved using slic3r pe // config data not found, this model was not saved using slic3r pe
model_object->auto_drop = legacy_auto_drop;
// add the entire geometry as the single volume to generate // add the entire geometry as the single volume to generate
//volumes.emplace_back(0, (int)obj_geometry->second.triangles.size() - 1); //volumes.emplace_back(0, (int)obj_geometry->second.triangles.size() - 1);
@@ -4121,6 +4128,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
std::string path = bbs_get_attribute_value_string(attributes, num_attributes, PPATH_ATTR); std::string path = bbs_get_attribute_value_string(attributes, num_attributes, PPATH_ATTR);
Transform3d transform = bbs_get_transform_from_3mf_specs_string(bbs_get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR)); Transform3d transform = bbs_get_transform_from_3mf_specs_string(bbs_get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR));
int printable = bbs_get_attribute_value_bool(attributes, num_attributes, PRINTABLE_ATTR); int printable = bbs_get_attribute_value_bool(attributes, num_attributes, PRINTABLE_ATTR);
if (const char* auto_drop = bbs_get_attribute_value_charptr(attributes, num_attributes, AUTO_DROP_ATTR); auto_drop != nullptr && !::atoi(auto_drop))
m_items_auto_drop_off.insert(object_id);
return !m_load_model || _create_object_instance(path, object_id, transform, printable, 1); return !m_load_model || _create_object_instance(path, object_id, transform, printable, 1);
} }
@@ -4489,7 +4498,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// int instances_count_id = bbs_get_attribute_value_int(attributes, num_attributes, INSTANCESCOUNT_ATTR); // int instances_count_id = bbs_get_attribute_value_int(attributes, num_attributes, INSTANCESCOUNT_ATTR);
ObjectMetadata metadata; ObjectMetadata metadata;
metadata.auto_drop = bbs_get_attribute_value_bool(attributes, num_attributes, AUTO_DROP_ATTR); if (bbs_get_attribute_value_charptr(attributes, num_attributes, AUTO_DROP_ATTR) != nullptr)
metadata.auto_drop = bbs_get_attribute_value_bool(attributes, num_attributes, AUTO_DROP_ATTR);
m_objects_metadata.insert({ object_id, std::move(metadata) }); m_objects_metadata.insert({ object_id, std::move(metadata) });
m_curr_config.object_id = object_id; m_curr_config.object_id = object_id;
return true; return true;
@@ -10110,7 +10120,7 @@ bool to_xml(std::stringstream &stream, const EmbossShape::SvgFile &svg, const Mo
assert(file_data != nullptr); assert(file_data != nullptr);
if (file_data == nullptr && !svg.path.empty()) file_data = read_from_disk(svg.path); if (file_data == nullptr && !svg.path.empty()) file_data = read_from_disk(svg.path);
if (file_data == nullptr) { if (file_data == nullptr) {
BOOST_LOG_TRIVIAL(warning) << "Can't write svg file no filedata"; BOOST_LOG_TRIVIAL(warning) << "Can't write SVG file no filedata";
return false; return false;
} }
const std::string &file_data_str = *file_data; const std::string &file_data_str = *file_data;
@@ -10126,7 +10136,7 @@ void to_xml(std::stringstream &stream, const EmbossShape &es, const ModelVolume
stream << " <" << SHAPE_TAG << " "; stream << " <" << SHAPE_TAG << " ";
if (es.svg_file.has_value()) if (es.svg_file.has_value())
if (!to_xml(stream, *es.svg_file, volume, archive, export_full_path)) { if (!to_xml(stream, *es.svg_file, volume, archive, export_full_path)) {
BOOST_LOG_TRIVIAL(warning) << "Can't write svg file defiden embossed shape into 3mf"; BOOST_LOG_TRIVIAL(warning) << "Can't write SVG file defiden embossed shape into 3mf";
} }
stream << SHAPE_SCALE_ATTR << "=\"" << es.scale << "\" "; stream << SHAPE_SCALE_ATTR << "=\"" << es.scale << "\" ";
+10
View File
@@ -3985,6 +3985,16 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder))); this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder))); this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder))); this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
// The final print statistics are known only after the whole G-code is generated, so they are filled in during post-processing too.
// The running extruded_* totals are left alone; profiles use them as numbers.
this->placeholder_parser().set("used_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
this->placeholder_parser().set("print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
this->placeholder_parser().set("normal_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
this->placeholder_parser().set("silent_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Silent_Print_Time_Placeholder)));
this->placeholder_parser().set("total_weight", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Weight_Placeholder)));
this->placeholder_parser().set("total_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Cost_Placeholder)));
this->placeholder_parser().set("total_wipe_tower_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Cost_Placeholder)));
this->placeholder_parser().set("total_wipe_tower_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Filament_Placeholder)));
// Sync variant-mapped params into placeholder_parser before processing start gcode // Sync variant-mapped params into placeholder_parser before processing start gcode
update_placeholder_parser_with_variant_params(); update_placeholder_parser_with_variant_params();
+47 -71
View File
@@ -120,7 +120,13 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags = {
"@PRINT_TIME_HOUR@", "@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@", "@PRINT_TIME_MINUTE@",
"@PRINT_TIME_SEC@", "@PRINT_TIME_SEC@",
"@USED_FILAMENT_LENGTH@" "@USED_FILAMENT_LENGTH@",
"@NORMAL_PRINT_TIME@",
"@SILENT_PRINT_TIME@",
"@TOTAL_WEIGHT@",
"@TOTAL_COST@",
"@TOTAL_WIPE_TOWER_COST@",
"@TOTAL_WIPE_TOWER_FILAMENT@"
}; };
const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = { const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
@@ -147,7 +153,13 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
"@PRINT_TIME_HOUR@", "@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@", "@PRINT_TIME_MINUTE@",
"@PRINT_TIME_SEC@", "@PRINT_TIME_SEC@",
"@USED_FILAMENT_LENGTH@" "@USED_FILAMENT_LENGTH@",
"@NORMAL_PRINT_TIME@",
"@SILENT_PRINT_TIME@",
"@TOTAL_WEIGHT@",
"@TOTAL_COST@",
"@TOTAL_WIPE_TOWER_COST@",
"@TOTAL_WIPE_TOWER_FILAMENT@"
}; };
@@ -1264,19 +1276,22 @@ void GCodeProcessor::run_post_process()
return ret; return ret;
}; };
// Process inline placeholders (print_time_total_sec, print_time_day, print_time_hour, print_time_minute, print_time_sec and used_filament_length) // Process inline placeholders (print_time_*, used_filament_length and the print statistics known only after export)
auto process_inline_placeholders = [&](std::string& gcode_line) { auto process_inline_placeholders = [&](std::string& gcode_line) {
bool processed = false; bool processed = false;
// Every inline placeholder contains '@', so a line without one has nothing to replace. // Every inline placeholder contains '@', so a line without one has nothing to replace.
if (gcode_line.find('@') == std::string::npos) if (gcode_line.find('@') == std::string::npos)
return processed; return processed;
const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder); auto replace_tag = [&gcode_line, &processed](ETags tag, const std::string& value) {
const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder); const std::string& placeholder = reserved_tag(tag);
const std::string& print_time_hour_placeholder = reserved_tag(ETags::Print_Time_Hour_Placeholder); size_t pos = gcode_line.find(placeholder);
const std::string& print_time_minute_placeholder = reserved_tag(ETags::Print_Time_Minute_Placeholder); while (pos != std::string::npos) {
const std::string& print_time_sec_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder); gcode_line.replace(pos, placeholder.length(), value);
const std::string& used_filament_placeholder = reserved_tag(ETags::Used_Filament_Length_Placeholder); processed = true;
pos = gcode_line.find(placeholder, pos + value.length());
}
};
double print_time_total_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time; double print_time_total_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time;
if (print_time_total_sec < 0.0) if (print_time_total_sec < 0.0)
@@ -1289,70 +1304,31 @@ void GCodeProcessor::run_post_process()
int print_time_minute = (day_remainder_seconds % 3600) / 60; int print_time_minute = (day_remainder_seconds % 3600) / 60;
int print_time_sec = day_remainder_seconds % 60; int print_time_sec = day_remainder_seconds % 60;
// Replace print_time_total_sec double total_filament_mm = 0.0;
size_t pos = gcode_line.find(print_time_total_placeholder); for (const auto& mm : filament_mm) {
while (pos != std::string::npos) { total_filament_mm += mm;
char buf[64];
sprintf(buf, "%.2f", print_time_total_sec);
gcode_line.replace(pos, print_time_total_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_total_placeholder, pos + strlen(buf));
} }
// Replace print_time_day replace_tag(ETags::Print_Time_Total_Sec_Placeholder, Slic3r::float_to_string_decimal_point(print_time_total_sec, 2));
pos = gcode_line.find(print_time_day_placeholder); replace_tag(ETags::Print_Time_Day_Placeholder, std::to_string(print_time_day));
while (pos != std::string::npos) { replace_tag(ETags::Print_Time_Hour_Placeholder, std::to_string(print_time_hour));
char buf[64]; replace_tag(ETags::Print_Time_Minute_Placeholder, std::to_string(print_time_minute));
sprintf(buf, "%d", print_time_day); replace_tag(ETags::Print_Time_Sec_Placeholder, std::to_string(print_time_sec));
gcode_line.replace(pos, print_time_day_placeholder.length(), buf); replace_tag(ETags::Used_Filament_Length_Placeholder,
processed = true; Slic3r::float_to_string_decimal_point(total_filament_mm / 1000.0, 2)); // Convert mm to m
pos = gcode_line.find(print_time_day_placeholder, pos + strlen(buf)); // 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 print_time_hour replace_tag(ETags::Silent_Print_Time_Placeholder, is_stealth_time_estimator_enabled() ?
pos = gcode_line.find(print_time_hour_placeholder); get_time_dhm(PrintEstimatedStatistics::ETimeMode::Stealth) :
while (pos != std::string::npos) { short_time("N/A"));
char buf[64]; const double total_cost = filament_total_cost + m_print->config().time_cost.value * print_time_total_sec / 3600.0;
sprintf(buf, "%d", print_time_hour); replace_tag(ETags::Total_Weight_Placeholder, Slic3r::float_to_string_decimal_point(filament_total_g, 2));
gcode_line.replace(pos, print_time_hour_placeholder.length(), buf); replace_tag(ETags::Total_Cost_Placeholder, Slic3r::float_to_string_decimal_point(total_cost, 2));
processed = true; replace_tag(ETags::Total_Wipe_Tower_Cost_Placeholder,
pos = gcode_line.find(print_time_hour_placeholder, pos + strlen(buf)); 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));
// 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));
}
return processed; return processed;
}; };
+6
View File
@@ -601,6 +601,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
Print_Time_Minute_Placeholder, Print_Time_Minute_Placeholder,
Print_Time_Sec_Placeholder, Print_Time_Sec_Placeholder,
Used_Filament_Length_Placeholder, Used_Filament_Length_Placeholder,
Normal_Print_Time_Placeholder,
Silent_Print_Time_Placeholder,
Total_Weight_Placeholder,
Total_Cost_Placeholder,
Total_Wipe_Tower_Cost_Placeholder,
Total_Wipe_Tower_Filament_Placeholder,
}; };
static const std::string& reserved_tag(ETags tag) { return s_IsBBLPrinter ? Reserved_Tags[static_cast<unsigned char>(tag)] : Reserved_Tags_compatible[static_cast<unsigned char>(tag)]; } static const std::string& reserved_tag(ETags tag) { return s_IsBBLPrinter ? Reserved_Tags[static_cast<unsigned char>(tag)] : Reserved_Tags_compatible[static_cast<unsigned char>(tag)]; }
+78 -5
View File
@@ -6,6 +6,7 @@
#include "../ShortestPath.hpp" #include "../ShortestPath.hpp"
#include "libslic3r/Point.hpp" #include "libslic3r/Point.hpp"
#include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
@@ -140,15 +141,87 @@ bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()}; return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
}; };
// For many islands, the same scan with the edges binned in a uniform grid over their boxes, so each edge is only tested against the edges sharing a
// cell with it - two edges whose boxes overlap always do. It returns the same crossing as the all-pairs scan
// (smallest i, then smallest j), so the result is unchanged. The all-pairs scan is quadratic in the edge count and
// runs again after every reversal; rebuilding the grid costs more than it saves below the threshold.
constexpr size_t grid_min_size = 500;
BoundingBox extent;
for (size_t idx : path)
extent.merge(centers[idx]);
const int grid_n = std::clamp(int(std::sqrt(double(pn))), 1, 256);
const coord_t cell_w = std::max<coord_t>(1, (extent.max.x() - extent.min.x()) / grid_n + 1);
const coord_t cell_h = std::max<coord_t>(1, (extent.max.y() - extent.min.y()) / grid_n + 1);
const auto for_cells = [&](const Point& a, const Point& b, auto&& fn) {
const int x0 = int((std::min(a.x(), b.x()) - extent.min.x()) / cell_w), x1 = int((std::max(a.x(), b.x()) - extent.min.x()) / cell_w);
const int y0 = int((std::min(a.y(), b.y()) - extent.min.y()) / cell_h), y1 = int((std::max(a.y(), b.y()) - extent.min.y()) / cell_h);
for (int y = y0; y <= y1; ++y)
for (int x = x0; x <= x1; ++x)
fn(y * grid_n + x);
};
std::vector<std::vector<size_t>> edge_cells(size_t(grid_n) * grid_n);
auto find_crossing_grid = [&]() -> std::pair<size_t, size_t> {
for (std::vector<size_t>& cell : edge_cells)
cell.clear();
for (size_t j = 0; j < n_edges; ++j)
for_cells(centers[path[j]], centers[path[(j + 1) % pn]], [&](int cell) { edge_cells[cell].emplace_back(j); });
for (size_t i = 0; i < n_edges; ++i) {
const Point& ai = centers[path[i]];
const Point& bi = centers[path[(i + 1) % pn]];
size_t first_j = std::numeric_limits<size_t>::max();
for_cells(ai, bi, [&](int cell) {
for (size_t j : edge_cells[cell]) {
if (j < i + 2 || j >= first_j) continue;
// Skip the (0, pn-1) pair: edges (0,1) and (pn-1,0) share node 0.
if (i == 0 && j == pn - 1) continue;
const Point& aj = centers[path[j]];
const Point& bj = centers[path[(j + 1) % pn]];
if (!bboxes_overlap(ai, bi, aj, bj)) continue;
if (Geometry::segments_intersect(ai, bi, aj, bj))
first_j = j;
}
});
if (first_j != std::numeric_limits<size_t>::max())
return {i, first_j};
}
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
};
// Process crossings one at a time: find first, reverse it, restart scan. // Process crossings one at a time: find first, reverse it, restart scan.
// Cap iterations to prevent infinite loops on collinear/overlapping segments. // Cap iterations to prevent infinite loops on collinear/overlapping segments.
int max_iters = static_cast<int>(pn * pn); const int max_iters = static_cast<int>(pn * pn);
bool improved = false; bool improved = false;
while (max_iters-- > 0) { // Reversing between two segments that only touch or overlap along a line need not remove the intersection, so on
auto [ci, cj] = find_crossing(); // islands laid out on a regular grid (a tiled texture, an array of parts) the loop can cycle through the same
if (ci == std::numeric_limits<size_t>::max()) break; // orderings until the pn * pn cap. Once an ordering repeats the rest of the loop is periodic, so only the steps
improved = true; // to the ordering the capped loop would have stopped on are taken.
std::unordered_map<uint64_t, int> seen_paths; // path hash -> reversals done when it was reached
const auto path_hash = [&path]() {
uint64_t h = 1469598103934665603ull; // FNV-1a
for (size_t idx : path)
h = (h ^ uint64_t(idx)) * 1099511628211ull;
return h;
};
const auto reverse_first_crossing = [&]() {
auto [ci, cj] = pn >= grid_min_size ? find_crossing_grid() : find_crossing();
if (ci == std::numeric_limits<size_t>::max())
return false;
std::reverse(path.begin() + ci + 1, path.begin() + cj + 1); std::reverse(path.begin() + ci + 1, path.begin() + cj + 1);
return true;
};
seen_paths.emplace(path_hash(), 0);
for (int iter = 1; iter <= max_iters && reverse_first_crossing(); ++iter) {
improved = true;
if (auto [it, inserted] = seen_paths.emplace(path_hash(), iter); !inserted) {
for (int steps = (max_iters - iter) % (iter - it->second); steps > 0; --steps)
reverse_first_crossing();
break;
}
} }
return improved; return improved;
} }
+18 -13
View File
@@ -29,6 +29,7 @@
#include <memory> #include <memory>
#include <random> #include <random>
#include <algorithm> #include <algorithm>
#include <limits>
#include <queue> #include <queue>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
@@ -1207,21 +1208,21 @@ std::optional<std::pair<size_t, size_t>> SeamPlacer::find_next_seam_in_layer(
const size_t layer_idx, const float max_distance, const size_t layer_idx, const float max_distance,
const SeamPlacerImpl::SeamComparator &comparator) const { const SeamPlacerImpl::SeamComparator &comparator) const {
using namespace SeamPlacerImpl; using namespace SeamPlacerImpl;
std::vector<size_t> nearby_points_indices = find_nearby_points(*layers[layer_idx].points_tree, projected_position, // Find the best nearby point and the nearest one. A layer of a fine relief has tens of thousands of candidates within
max_distance); // the radius, so they are looked at as the search finds them rather than collected into a vector first.
constexpr size_t none = std::numeric_limits<size_t>::max();
if (nearby_points_indices.empty()) { size_t best_nearby_point_index = none;
return {}; size_t nearest_point_index = none;
} visit_nearby_points(*layers[layer_idx].points_tree, projected_position, max_distance,
[&layers, &comparator, &projected_position, layer_idx, &best_nearby_point_index, &nearest_point_index]
size_t best_nearby_point_index = nearby_points_indices[0]; (size_t nearby_point_index) {
size_t nearest_point_index = nearby_points_indices[0]; if (best_nearby_point_index == none) {
// The first point found starts both, as the first of the collected ones did.
// Now find best nearby point, nearest point, and corresponding indices best_nearby_point_index = nearest_point_index = nearby_point_index;
for (const size_t &nearby_point_index : nearby_points_indices) { }
const SeamCandidate &point = layers[layer_idx].points[nearby_point_index]; const SeamCandidate &point = layers[layer_idx].points[nearby_point_index];
if (point.perimeter.finalized) { if (point.perimeter.finalized) {
continue; // skip over finalized perimeters, try to find some that is not finalized return; // skip over finalized perimeters, try to find some that is not finalized
} }
if (comparator.is_first_better(point, layers[layer_idx].points[best_nearby_point_index], if (comparator.is_first_better(point, layers[layer_idx].points[best_nearby_point_index],
projected_position.head<2>()) projected_position.head<2>())
@@ -1233,6 +1234,10 @@ std::optional<std::pair<size_t, size_t>> SeamPlacer::find_next_seam_in_layer(
|| layers[layer_idx].points[nearest_point_index].perimeter.finalized) { || layers[layer_idx].points[nearest_point_index].perimeter.finalized) {
nearest_point_index = nearby_point_index; nearest_point_index = nearby_point_index;
} }
});
if (best_nearby_point_index == none) {
return {};
} }
const SeamCandidate &best_nearby_point = layers[layer_idx].points[best_nearby_point_index]; const SeamCandidate &best_nearby_point = layers[layer_idx].points[best_nearby_point_index];
+30
View File
@@ -318,6 +318,36 @@ std::vector<size_t> find_nearby_points(const KDTreeIndirectType &kdtree, const P
return visitor.result; return visitor.result;
} }
// Visits the points within max_distance of center, in the order find_nearby_points() would collect them, and hands
// each of them to `visitor_fn` instead of returning them all: a search over a dense set spends more on collecting the
// points into a vector than on the search itself, and its caller usually keeps only a few of them.
template<typename KDTreeIndirectType, typename PointType, typename VisitorFn>
void visit_nearby_points(const KDTreeIndirectType &kdtree, const PointType &center,
const typename KDTreeIndirectType::CoordType &max_distance, VisitorFn visitor_fn)
{
using CoordType = typename KDTreeIndirectType::CoordType;
struct Visitor {
const KDTreeIndirectType &kdtree;
const PointType center;
const CoordType max_distance_squared;
VisitorFn visitor_fn;
unsigned int operator()(size_t idx, size_t dimension) {
auto dist = CoordType(0);
for (size_t i = 0; i < KDTreeIndirectType::NumDimensions; ++i) {
CoordType d = center[i] - kdtree.coordinate(idx, i);
dist += d * d;
}
if (dist < max_distance_squared)
visitor_fn(idx);
return kdtree.descent_mask(center[dimension], max_distance_squared, idx, dimension);
}
} visitor { kdtree, center, max_distance * max_distance, visitor_fn };
kdtree.visit(visitor);
}
template<typename KDTreeIndirectType, typename PointType> template<typename KDTreeIndirectType, typename PointType>
std::vector<size_t> find_nearby_points(const KDTreeIndirectType &kdtree, const PointType &center, std::vector<size_t> find_nearby_points(const KDTreeIndirectType &kdtree, const PointType &center,
const typename KDTreeIndirectType::CoordType& max_distance) const typename KDTreeIndirectType::CoordType& max_distance)
+2 -2
View File
@@ -443,8 +443,8 @@ coordf_t Layer::get_sparse_infill_max_void_area()
if (density == 0.) if (density == 0.)
return -1; return -1;
// Orca: the adaptive TPMS infill is as sparse as its interior density. // Orca: the adaptive TPMS infill is as sparse as its interior density.
if (density < 100.f && config.tpms_adaptive != TpmsAdaptiveMode::Disabled && is_tpms_adaptive_pattern(pattern)) if (density < 100.f && config.adaptive_infill_mode != AdaptiveInfillMode::Disabled && is_adaptive_infill_pattern(pattern))
density = std::min(density, std::max(1.f, float(config.tpms_interior_density))); density = std::min(density, std::max(1.f, float(config.adaptive_infill_interior_density)));
//BBS: rough estimation and need to be optimized //BBS: rough estimation and need to be optimized
double spacing = flow.scaled_spacing() * (100 - density) / density; double spacing = flow.scaled_spacing() * (100 - density) / density;
+2 -1
View File
@@ -99,10 +99,11 @@ void LayerRegion::slices_to_fill_surfaces_clipped()
by_surface[size_t(surface.surface_type)].emplace_back(&surface); by_surface[size_t(surface.surface_type)].emplace_back(&surface);
// Trim surfaces by the fill_boundaries. // Trim surfaces by the fill_boundaries.
this->fill_surfaces.surfaces.clear(); this->fill_surfaces.surfaces.clear();
const Polygons fill_boundaries = to_polygons(this->fill_expolygons);
for (size_t surface_type = 0; surface_type < size_t(stCount); ++ surface_type) { for (size_t surface_type = 0; surface_type < size_t(stCount); ++ surface_type) {
const SurfacesPtr &this_surfaces = by_surface[surface_type]; const SurfacesPtr &this_surfaces = by_surface[surface_type];
if (! this_surfaces.empty()) if (! this_surfaces.empty())
this->fill_surfaces.append(intersection_ex(this_surfaces, this->fill_expolygons), SurfaceType(surface_type)); this->fill_surfaces.append(intersection_ex_by_piece(to_expolygons(this_surfaces), fill_boundaries), SurfaceType(surface_type));
} }
} }
+272 -72
View File
@@ -22,6 +22,7 @@
#include "Surface.hpp" #include "Surface.hpp"
#include "libslic3r.h" #include "libslic3r.h"
#include <numeric>
#include <cmath> #include <cmath>
#include <cstddef> #include <cstddef>
#include <list> #include <list>
@@ -1361,10 +1362,15 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
} }
#endif // MM_SEGMENTATION_DEBUG_TOP_BOTTOM #endif // MM_SEGMENTATION_DEBUG_TOP_BOTTOM
// When the upper surface of an object is occluded, it should no longer be considered the upper surface // When the upper surface of an object is occluded, it should no longer be considered the upper surface.
// Every (colour, layer) pair is trimmed on its own, so they all run at once: the painted faces of a finely
// textured part project hundreds of thousands of triangles onto one layer, which used to be trimmed serially.
{ {
for (size_t extruder_idx = 0; extruder_idx < num_facets_states; ++extruder_idx) { const size_t occluded_pairs = num_facets_states * layers.size();
for (size_t layer_idx = 0; layer_idx < layers.size(); ++layer_idx) { tbb::parallel_for(tbb::blocked_range<size_t>(0, occluded_pairs), [&](const tbb::blocked_range<size_t> &range) {
for (size_t pair_idx = range.begin(); pair_idx < range.end(); ++pair_idx) {
const size_t extruder_idx = pair_idx / layers.size();
const size_t layer_idx = pair_idx % layers.size();
if (!top_raw[extruder_idx].empty() && !top_raw[extruder_idx][layer_idx].empty() && layer_idx + 1 < layers.size()) { if (!top_raw[extruder_idx].empty() && !top_raw[extruder_idx][layer_idx].empty() && layer_idx + 1 < layers.size()) {
top_raw[extruder_idx][layer_idx] = diff(top_raw[extruder_idx][layer_idx], input_expolygons[layer_idx + 1]); top_raw[extruder_idx][layer_idx] = diff(top_raw[extruder_idx][layer_idx], input_expolygons[layer_idx + 1]);
} }
@@ -1372,7 +1378,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
bottom_raw[extruder_idx][layer_idx] = diff(bottom_raw[extruder_idx][layer_idx], input_expolygons[layer_idx - 1]); bottom_raw[extruder_idx][layer_idx] = diff(bottom_raw[extruder_idx][layer_idx], input_expolygons[layer_idx - 1]);
} }
} }
} });
} }
std::vector<std::vector<ExPolygons>> triangles_by_color_bottom(num_facets_states); std::vector<std::vector<ExPolygons>> triangles_by_color_bottom(num_facets_states);
@@ -1430,11 +1436,58 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
return out; return out;
}; };
// Projects a painted top or bottom face `ex` of layer `layer_idx` onto the shell layers below or above it (in
// `shell_layers`, nearest first), one more perimeter in on each, stopping at the first layer where nothing is left.
// Only the slices within the deepest offset of `ex` (three times that with the miter joins) decide the result, so the
// work is done per tile of `ex`'s ExPolygons on the slices cut to the tile's box grown by that much: the same result, but
// each Clipper call stays the size of a tile rather than of a layer cut through a fine relief, and the tiles run in
// parallel.
const auto project_to_shells = [&input_expolygons](const ExPolygons &ex, size_t layer_idx, const std::vector<size_t> &shell_layers,
const LayerColorStat &stat, ShellProjections &dst) {
std::vector<float> offsets(shell_layers.size());
float offset = 0.f;
for (size_t i = 0; i < shell_layers.size(); ++i) {
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
offset -= (stat.extrusion_spacing + stat.extrusion_width);
offsets[i] = offset;
}
if (offsets.empty())
return;
const coord_t reach = coord_t(std::ceil(DefaultMiterLimit * std::abs(offsets.back()))) + 10 * SCALED_EPSILON;
const std::vector<ClipperUtils::ExPolygonsTile> tiles = ClipperUtils::tile_expolygons(ex, 16);
// [shell layer][tile]
std::vector<std::vector<ExPolygons>> shells(shell_layers.size(), std::vector<ExPolygons>(tiles.size()));
tbb::parallel_for(size_t(0), tiles.size(), [&](size_t tile_idx) {
const ClipperUtils::ExPolygonsTile &tile = tiles[tile_idx];
const BoundingBox bbox = tile.bbox.inflated(reach);
ExPolygons tile_ex;
tile_ex.reserve(tile.members.size());
for (size_t i : tile.members)
tile_ex.emplace_back(ex[i]);
Polygons layer_slices_trimmed = ClipperUtils::clip_clipper_polygons_with_subject_bbox(input_expolygons[layer_idx], bbox);
for (size_t i = 0; i < shell_layers.size() && ! layer_slices_trimmed.empty(); ++i) {
const ExPolygons trimmed = intersection_ex(layer_slices_trimmed, ClipperUtils::clip_clipper_polygons_with_subject_bbox(input_expolygons[shell_layers[i]], bbox));
shells[i][tile_idx] = opening_ex(intersection_ex(tile_ex, offset_ex(trimmed, offsets[i])), stat.small_region_threshold);
layer_slices_trimmed = to_polygons(trimmed);
}
});
for (size_t i = 0; i < shell_layers.size(); ++i) {
ExPolygons shell;
for (ExPolygons &tile_shell : shells[i])
append(shell, std::move(tile_shell));
if (shell.empty())
break;
dst.emplace_back(shell_layers[i], std::move(shell));
}
};
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top, tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top,
&throw_on_cancel_callback, &input_expolygons, &bottom_raw, &triangles_by_color_bottom, &throw_on_cancel_callback, &bottom_raw, &triangles_by_color_bottom, &project_to_shells,
&shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range<size_t> &range) { &shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range<size_t> &range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) { // Each colour writes only its own vectors, so the colours run in parallel: a painted top or bottom face
// projects onto a single layer, which otherwise did all of its colours on one thread.
tbb::parallel_for(size_t(0), size_t(num_facets_states), [&](size_t color_idx) {
throw_on_cancel_callback(); throw_on_cancel_callback();
LayerColorStat stat = layer_color_stat(layer_idx, color_idx); LayerColorStat stat = layer_color_stat(layer_idx, color_idx);
if (std::vector<Polygons> &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty()) if (std::vector<Polygons> &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty())
@@ -1443,18 +1496,10 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
top_ex = opening_ex(top_ex, stat.small_region_threshold); top_ex = opening_ex(top_ex, stat.small_region_threshold);
if (! top_ex.empty()) { if (! top_ex.empty()) {
append(triangles_by_color_top[color_idx][layer_idx], top_ex); append(triangles_by_color_top[color_idx][layer_idx], top_ex);
float offset = 0.f; std::vector<size_t> shell_layers;
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx]; for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx)
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx) { shell_layers.emplace_back(size_t(last_idx));
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line project_to_shells(top_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_top[color_idx][layer_idx]);
//offset -= stat.extrusion_width ;
offset -= (stat.extrusion_spacing + stat.extrusion_width);
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
ExPolygons last = opening_ex(intersection_ex(top_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
if (last.empty())
break;
shell_triangles_by_color_top[color_idx][layer_idx].emplace_back(size_t(last_idx), std::move(last));
}
} }
} }
if (std::vector<Polygons> &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty()) if (std::vector<Polygons> &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty())
@@ -1463,21 +1508,13 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold); bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold);
if (! bottom_ex.empty()) { if (! bottom_ex.empty()) {
append(triangles_by_color_bottom[color_idx][layer_idx], bottom_ex); append(triangles_by_color_bottom[color_idx][layer_idx], bottom_ex);
float offset = 0.f; std::vector<size_t> shell_layers;
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx]; for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx)
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx) { shell_layers.emplace_back(last_idx);
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line project_to_shells(bottom_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_bottom[color_idx][layer_idx]);
//offset -= stat.extrusion_width;
offset -= (stat.extrusion_spacing + stat.extrusion_width);
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
ExPolygons last = opening_ex(intersection_ex(bottom_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
if (last.empty())
break;
shell_triangles_by_color_bottom[color_idx][layer_idx].emplace_back(last_idx, std::move(last));
}
} }
} }
} });
} }
}); });
@@ -1498,20 +1535,23 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
&shell_top_by_layer, &shell_bottom_by_layer](const tbb::blocked_range<size_t> &range) { &shell_top_by_layer, &shell_bottom_by_layer](const tbb::blocked_range<size_t> &range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
throw_on_cancel_callback(); throw_on_cancel_callback();
ExPolygons painted_exploys; // The per-colour unions below are independent of each other, so they run in parallel (a painted top or
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) { // bottom face puts all of its colours on one layer); whatever combines the colours stays in colour order.
const auto merge_colour_union = [&](size_t color_idx) {
auto &self = triangles_by_color_merged[color_idx][layer_idx]; auto &self = triangles_by_color_merged[color_idx][layer_idx];
append(self, std::move(triangles_by_color_bottom[color_idx][layer_idx])); append(self, std::move(triangles_by_color_bottom[color_idx][layer_idx]));
append(self, std::move(triangles_by_color_top[color_idx][layer_idx])); append(self, std::move(triangles_by_color_top[color_idx][layer_idx]));
self = union_ex(self); self = union_ex(self);
};
tbb::parallel_for(size_t(0), triangles_by_color_merged.size(), merge_colour_union);
append(painted_exploys, self); ExPolygons painted_exploys;
} for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx)
append(painted_exploys, triangles_by_color_merged[color_idx][layer_idx]);
painted_exploys = union_ex(painted_exploys); painted_exploys = union_ex(painted_exploys);
//BBS: merge the top and bottom shell layers //BBS: merge the top and bottom shell layers
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) { tbb::parallel_for(size_t(0), triangles_by_color_merged.size(), [&](size_t color_idx) {
auto &self = triangles_by_color_merged[color_idx][layer_idx]; auto &self = triangles_by_color_merged[color_idx][layer_idx];
auto top_area = diff_ex(union_ex(shell_top_by_layer[color_idx][layer_idx]), painted_exploys); auto top_area = diff_ex(union_ex(shell_top_by_layer[color_idx][layer_idx]), painted_exploys);
@@ -1520,7 +1560,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
append(self, top_area); append(self, top_area);
append(self, bottom_area); append(self, bottom_area);
self = union_ex(self); self = union_ex(self);
} });
// Trim one region by the other if some of the regions overlap. // Trim one region by the other if some of the regions overlap.
ExPolygons painted_regions; ExPolygons painted_regions;
for (size_t color_idx = 1; color_idx < triangles_by_color_merged.size(); ++color_idx) { for (size_t color_idx = 1; color_idx < triangles_by_color_merged.size(); ++color_idx) {
@@ -1887,7 +1927,69 @@ static void remove_multiple_edges_in_vertices(MMU_Graph &graph, const std::vecto
} }
} }
static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::vector<std::vector<ExPolygons>> &segmented_regions,
// Finds the islands (layer ExPolygons) a region piece overlaps. A top or bottom region is projected from the neighbouring
// layers and may reach past the island it belongs to, or over several islands.
class IslandLocator
{
public:
explicit IslandLocator(const ExPolygons &islands) : m_islands(islands)
{
m_bboxes.reserve(islands.size());
for (const ExPolygon &island : islands) {
m_bboxes.emplace_back(get_extents(island));
m_extent.merge(m_bboxes.back());
}
if (!m_extent.defined)
return;
const Point size = m_extent.size();
m_cell_w = std::max<coord_t>(1, size.x() / GRID + 1);
m_cell_h = std::max<coord_t>(1, size.y() / GRID + 1);
m_grid.assign(GRID * GRID, {});
for (size_t i = 0; i < m_bboxes.size(); ++i)
for_cells(m_bboxes[i], [&](int cell) { m_grid[cell].emplace_back(i); });
}
void find(const ExPolygon &piece, std::vector<size_t> &out) const
{
out.clear();
const BoundingBox bbox = get_extents(piece);
if (!m_extent.defined || !m_extent.overlap(bbox))
return;
for_cells(bbox, [&](int cell) {
for (size_t i : m_grid[cell])
if (m_bboxes[i].overlap(bbox))
out.emplace_back(i);
});
sort_remove_duplicates(out);
if (out.size() > 1)
out.erase(std::remove_if(out.begin(), out.end(), [&](size_t i) {
const BoundingBox common(m_bboxes[i].min.cwiseMax(bbox.min), m_bboxes[i].max.cwiseMin(bbox.max));
return intersection(ClipperUtils::clip_clipper_polygons_with_subject_bbox(piece, common.inflated(SCALED_EPSILON)),
ClipperUtils::clip_clipper_polygons_with_subject_bbox(m_islands[i], common.inflated(SCALED_EPSILON))).empty();
}), out.end());
}
private:
static constexpr int GRID = 64;
template<typename Fn> void for_cells(const BoundingBox &bb, Fn &&fn) const
{
const int x0 = std::clamp(int((bb.min.x() - m_extent.min.x()) / m_cell_w), 0, GRID - 1), x1 = std::clamp(int((bb.max.x() - m_extent.min.x()) / m_cell_w), 0, GRID - 1);
const int y0 = std::clamp(int((bb.min.y() - m_extent.min.y()) / m_cell_h), 0, GRID - 1), y1 = std::clamp(int((bb.max.y() - m_extent.min.y()) / m_cell_h), 0, GRID - 1);
for (int y = y0; y <= y1; ++y)
for (int x = x0; x <= x1; ++x)
fn(y * GRID + x);
}
const ExPolygons &m_islands;
std::vector<BoundingBox> m_bboxes;
BoundingBox m_extent;
coord_t m_cell_w = 1, m_cell_h = 1;
std::vector<std::vector<size_t>> m_grid;
};
static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::vector<ExPolygons> &input_expolygons,
const std::vector<std::vector<ExPolygons>> &segmented_regions,
std::vector<std::vector<ExPolygons>> &&top_and_bottom_layers, std::vector<std::vector<ExPolygons>> &&top_and_bottom_layers,
const size_t num_facets_states, const size_t num_facets_states,
const std::function<void()> &throw_on_cancel_callback) const std::function<void()> &throw_on_cancel_callback)
@@ -1898,33 +2000,91 @@ static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::ve
assert(!top_and_bottom_layers.size() || num_facets_states == top_and_bottom_layers.size()); assert(!top_and_bottom_layers.size() || num_facets_states == top_and_bottom_layers.size());
BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Merging segmented layers in parallel - Begin"; BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Merging segmented layers in parallel - Begin";
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&segmented_regions, &top_and_bottom_layers, &segmented_regions_merged, &num_facets_states, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) { // Every region of a layer is merged together with the regions of the islands it overlaps, and the islands are further
// apart than the dimple removal below reaches, so this gives the same result as merging the layer at once. On a layer
// cut through a fine relief every region shares thousands of hole contours with every other, and Clipper, splitting
// and re-linking one huge polygon over and over, took anything up to half an hour for a layer; per island each operation
// stays the size of the island, and the islands run in parallel.
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&](const tbb::blocked_range<size_t> &range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) {
assert(segmented_regions[layer_idx].size() == num_facets_states); assert(segmented_regions[layer_idx].size() == num_facets_states);
// Zero is skipped because it is the default color of the volume throw_on_cancel_callback();
// Group the islands joined by a region overlapping several of them; the last group takes the regions lying
// outside every island.
const ExPolygons &islands = input_expolygons[layer_idx];
const IslandLocator locator(islands);
std::vector<size_t> parent(islands.size() + 1);
std::iota(parent.begin(), parent.end(), 0);
const auto root = [&parent](size_t i) {
while (parent[i] != i)
i = parent[i] = parent[parent[i]];
return i;
};
// Islands of every piece: side regions of colours 1.., then top/bottom regions of colours 0..
std::vector<const ExPolygon *> pieces;
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id)
for (const ExPolygon &piece : segmented_regions[layer_idx][extruder_id])
pieces.emplace_back(&piece);
if (!top_and_bottom_layers.empty())
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx)
for (const ExPolygon &piece : top_and_bottom_layers[color_idx][layer_idx])
pieces.emplace_back(&piece);
std::vector<std::vector<size_t>> overlapped(pieces.size());
tbb::parallel_for(size_t(0), pieces.size(), [&](size_t i) { locator.find(*pieces[i], overlapped[i]); });
std::vector<size_t> piece_island(pieces.size());
for (size_t i = 0; i < pieces.size(); ++i) {
piece_island[i] = overlapped[i].empty() ? islands.size() : overlapped[i].front();
for (size_t island : overlapped[i])
parent[root(island)] = root(piece_island[i]);
}
std::vector<size_t> bucket_of(parent.size(), size_t(-1));
size_t num_buckets = 0;
for (size_t i = 0; i < parent.size(); ++i)
if (size_t &b = bucket_of[root(i)]; b == size_t(-1))
b = num_buckets++;
// [bucket][colour]
std::vector<std::vector<ExPolygons>> sides(num_buckets, std::vector<ExPolygons>(num_facets_states));
std::vector<std::vector<ExPolygons>> tops(num_buckets, std::vector<ExPolygons>(num_facets_states));
size_t piece_idx = 0;
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id)
for (const ExPolygon &piece : segmented_regions[layer_idx][extruder_id])
sides[bucket_of[root(piece_island[piece_idx++])]][extruder_id].emplace_back(piece);
if (!top_and_bottom_layers.empty())
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx)
for (const ExPolygon &piece : top_and_bottom_layers[color_idx][layer_idx])
tops[bucket_of[root(piece_island[piece_idx++])]][color_idx].emplace_back(piece);
// Side regions minus the top/bottom regions of every colour.
std::vector<std::vector<ExPolygons>> merged(num_buckets, std::vector<ExPolygons>(num_facets_states));
tbb::parallel_for(size_t(0), num_buckets, [&](size_t bucket) {
Polygons tops_all;
for (const ExPolygons &t : tops[bucket])
polygons_append(tops_all, t);
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id)
if (!sides[bucket][extruder_id].empty())
merged[bucket][extruder_id] = tops_all.empty() ? std::move(sides[bucket][extruder_id]) :
diff_ex_by_piece(sides[bucket][extruder_id], tops_all);
});
// Then this colour's top/bottom regions, with the dimples removed (#7235) when the layer has side regions left.
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) { for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) {
throw_on_cancel_callback(); if (top_and_bottom_layers.empty() || top_and_bottom_layers[extruder_id][layer_idx].empty()) {
if (!segmented_regions[layer_idx][extruder_id].empty()) { for (size_t bucket = 0; bucket < num_buckets; ++bucket)
ExPolygons segmented_regions_trimmed = segmented_regions[layer_idx][extruder_id]; append(segmented_regions_merged[layer_idx][extruder_id - 1], std::move(merged[bucket][extruder_id]));
if (!top_and_bottom_layers.empty()) { continue;
for (const std::vector<ExPolygons> &top_and_bottom_by_extruder : top_and_bottom_layers) {
if (!top_and_bottom_by_extruder[layer_idx].empty() && !segmented_regions_trimmed.empty()) {
segmented_regions_trimmed = diff_ex(segmented_regions_trimmed, top_and_bottom_by_extruder[layer_idx]);
}
}
}
segmented_regions_merged[layer_idx][extruder_id - 1] = std::move(segmented_regions_trimmed);
}
if (!top_and_bottom_layers.empty() && !top_and_bottom_layers[extruder_id][layer_idx].empty()) {
bool was_top_and_bottom_empty = segmented_regions_merged[layer_idx][extruder_id - 1].empty();
append(segmented_regions_merged[layer_idx][extruder_id - 1], top_and_bottom_layers[extruder_id][layer_idx]);
// Remove dimples (#7235) appearing after merging side segmentation of the model with tops and bottoms painted layers.
if (!was_top_and_bottom_empty)
segmented_regions_merged[layer_idx][extruder_id - 1] = offset2_ex(union_ex(segmented_regions_merged[layer_idx][extruder_id - 1]), float(SCALED_EPSILON), -float(SCALED_EPSILON));
} }
bool was_top_and_bottom_empty = true;
for (size_t bucket = 0; bucket < num_buckets && was_top_and_bottom_empty; ++bucket)
was_top_and_bottom_empty = merged[bucket][extruder_id].empty();
tbb::parallel_for(size_t(0), num_buckets, [&](size_t bucket) {
ExPolygons &region = merged[bucket][extruder_id];
append(region, tops[bucket][extruder_id]);
if (!was_top_and_bottom_empty && !region.empty())
region = offset2_ex(union_ex(region), float(SCALED_EPSILON), -float(SCALED_EPSILON));
});
for (size_t bucket = 0; bucket < num_buckets; ++bucket)
append(segmented_regions_merged[layer_idx][extruder_id - 1], std::move(merged[bucket][extruder_id]));
} }
} }
}); // end of parallel_for }); // end of parallel_for
@@ -2212,16 +2372,56 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
assert(!color_poly.empty()); assert(!color_poly.empty());
assert(!color_poly.front().empty()); assert(!color_poly.front().empty());
if (has_layer_only_one_color(color_poly)) { // Each island (an ExPolygon with its holes) is segmented on its own. Any point of an island is closer to
// If the whole layer is painted using the same color, it is not needed to construct a Voronoi diagram for the segmentation of this layer. // that island's contours than to any other island's - the way out crosses its own boundary first - so its
segmented_regions[layer_idx][size_t(color_poly.front().front().color)] = input_expolygons[layer_idx]; // Voronoi cells, and with them its colour regions, depend on nothing else. A layer cut through a fine relief
} else { // has thousands of islands, and one Voronoi diagram over all of them degenerated into overlapping regions
MMU_Graph graph = build_graph(layer_idx, color_poly); // that every boolean afterwards had to untangle. Per island the diagrams stay small and the islands run in
remove_multiple_edges_in_vertices(graph, color_poly); // parallel; an island in a single colour needs no diagram at all.
graph.remove_nodes_with_one_arc(); const ExPolygons &islands = input_expolygons[layer_idx];
segmented_regions[layer_idx] = extract_colored_segments(graph, num_facets_states); std::vector<std::pair<size_t, size_t>> island_contours(islands.size()); // [first, last) into color_poly
//segmented_regions[layer_idx] = extract_colored_segments(color_poly, num_extruders, layer_idx); {
// The same order EdgeGrid::Grid::create() lists the contours in, and so colorize_contours().
size_t idx = 0;
for (size_t island_idx = 0; island_idx < islands.size(); ++island_idx) {
const size_t first = idx;
if (!islands[island_idx].contour.empty())
++idx;
for (const Polygon &hole : islands[island_idx].holes)
if (!hole.empty())
++idx;
island_contours[island_idx] = {first, idx};
}
assert(idx == color_poly.size());
} }
std::vector<std::vector<ExPolygons>> island_regions(islands.size());
tbb::parallel_for(size_t(0), islands.size(), [&](size_t island_idx) {
const auto [first, last] = island_contours[island_idx];
if (first == last)
return;
const std::vector<ColoredLines> island_poly(color_poly.begin() + first, color_poly.begin() + last);
std::vector<ExPolygons> &regions = island_regions[island_idx];
if (has_layer_only_one_color(island_poly)) {
regions.assign(num_facets_states, ExPolygons());
regions[size_t(island_poly.front().front().color)].emplace_back(islands[island_idx]);
} else {
MMU_Graph graph = build_graph(layer_idx, island_poly);
remove_multiple_edges_in_vertices(graph, island_poly);
graph.remove_nodes_with_one_arc();
regions = extract_colored_segments(graph, num_facets_states);
// The faces of one colour tile it without overlapping; merged here, where an island is small,
// every later boolean gets a few regions instead of thousands of faces sharing their edges. An
// island with many holes keeps its faces: merged, each colour would be one region with thousands
// of holes, and subtracting from that is far slower than from the faces one at a time.
if (island_poly.size() <= 64)
for (ExPolygons &faces : regions)
if (faces.size() > 1)
faces = union_ex(faces);
}
});
for (std::vector<ExPolygons> &regions : island_regions)
for (size_t color_idx = 0; color_idx < regions.size(); ++color_idx)
append(segmented_regions[layer_idx][color_idx], std::move(regions[color_idx]));
#ifdef MM_SEGMENTATION_DEBUG_REGIONS #ifdef MM_SEGMENTATION_DEBUG_REGIONS
export_regions_to_svg(debug_out_path("3-mm-regions-sides-%d-%d.svg", layer_idx, iRun), segmented_regions[layer_idx], input_expolygons[layer_idx]); export_regions_to_svg(debug_out_path("3-mm-regions-sides-%d-%d.svg", layer_idx, iRun), segmented_regions[layer_idx], input_expolygons[layer_idx]);
@@ -2244,7 +2444,7 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
throw_on_cancel_callback(); throw_on_cancel_callback();
} }
std::vector<std::vector<ExPolygons>> segmented_regions_merged = merge_segmented_layers(segmented_regions, std::move(top_and_bottom_layers), num_facets_states, throw_on_cancel_callback); std::vector<std::vector<ExPolygons>> segmented_regions_merged = merge_segmented_layers(input_expolygons, segmented_regions, std::move(top_and_bottom_layers), num_facets_states, throw_on_cancel_callback);
throw_on_cancel_callback(); throw_on_cancel_callback();
#ifdef MM_SEGMENTATION_DEBUG_REGIONS #ifdef MM_SEGMENTATION_DEBUG_REGIONS
+6 -2
View File
@@ -23,11 +23,15 @@ public:
MultiPoint() {} MultiPoint() {}
MultiPoint(const MultiPoint &other) : points(other.points) {} MultiPoint(const MultiPoint &other) : points(other.points) {}
MultiPoint(MultiPoint &&other) : points(std::move(other.points)) {} MultiPoint(MultiPoint &&other) noexcept : points(std::move(other.points)) {}
MultiPoint(std::initializer_list<Point> list) : points(list) {} MultiPoint(std::initializer_list<Point> list) : points(list) {}
explicit MultiPoint(const Points &_points) : points(_points) {} explicit MultiPoint(const Points &_points) : points(_points) {}
// Without it, the derived classes' move constructors passing std::move(points) here copied them, which
// also means a moved-from Polygon or Polyline is now really empty where it used to silently keep its
// points: a use-after-move anywhere in the tree that happened to work before now sees nothing.
explicit MultiPoint(Points &&_points) noexcept : points(std::move(_points)) {}
MultiPoint& operator=(const MultiPoint &other) { points = other.points; return *this; } MultiPoint& operator=(const MultiPoint &other) { points = other.points; return *this; }
MultiPoint& operator=(MultiPoint &&other) { points = std::move(other.points); return *this; } MultiPoint& operator=(MultiPoint &&other) noexcept { points = std::move(other.points); return *this; }
virtual ~MultiPoint() = default; virtual ~MultiPoint() = default;
void scale(double factor); void scale(double factor);
void scale(double factor_x, double factor_y); void scale(double factor_x, double factor_y);
+29
View File
@@ -211,6 +211,35 @@ bool decode_colored_png(const ReadBuf &in_buf, ImageColorscale &out_img)
return decode_colored_png(stream, out_img); return decode_colored_png(stream, out_img);
} }
bool is_decodable_8bit_png(const ReadBuf &pngbuf)
{
static const constexpr int PNG_SIG_BYTES = 8;
struct ReadBufStream stream{pngbuf};
std::vector<png_byte> sig(PNG_SIG_BYTES, 0);
stream.read(sig.data(), PNG_SIG_BYTES);
if (!png_check_sig(sig.data(), PNG_SIG_BYTES))
return false;
PNGDescr dsc;
dsc.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!dsc.png)
return false;
dsc.info = png_create_info_struct(dsc.png);
if (!dsc.info)
return false;
if (!png_read_header_guarded(dsc.png, dsc.info, &stream, PNG_SIG_BYTES))
return false;
const size_t color_type = png_get_color_type(dsc.png, dsc.info);
const size_t bit_depth = png_get_bit_depth(dsc.png, dsc.info);
const bool grey = color_type == PNG_COLOR_TYPE_GRAY;
const bool colour = color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA;
return png_get_image_width(dsc.png, dsc.info) > 0 && png_get_image_height(dsc.png, dsc.info) > 0 &&
bit_depth == 8 && (grey || colour);
}
// Down to earth function to store a packed RGB image to file. Mostly useful for debugging purposes. // Down to earth function to store a packed RGB image to file. Mostly useful for debugging purposes.
// Based on https://www.lemoda.net/c/write-png/ // Based on https://www.lemoda.net/c/write-png/
+3
View File
@@ -50,6 +50,9 @@ struct ReadBuf { const void *buf = nullptr; const size_t sz = 0; };
bool is_png(const ReadBuf &pngbuf); bool is_png(const ReadBuf &pngbuf);
// True if decode_png() or decode_colored_png() would accept these bytes, judged from the header alone.
bool is_decodable_8bit_png(const ReadBuf &pngbuf);
struct ReadBufStream: public IStream { struct ReadBufStream: public IStream {
const ReadBuf &rbuf_ref; const ReadBuf &rbuf_ref;
size_t pos = 0; size_t pos = 0;
File diff suppressed because it is too large. Load diff
+6
View File
@@ -164,6 +164,8 @@ public:
Polygons lower_slices_polygons() const { return m_lower_slices_polygons; } Polygons lower_slices_polygons() const { return m_lower_slices_polygons; }
// ORCA: the slices less the slivers the wall generator prints nothing for, so they never count as support. // ORCA: the slices less the slivers the wall generator prints nothing for, so they never count as support.
ExPolygons printable_slices(const ExPolygons &slices) const; ExPolygons printable_slices(const ExPolygons &slices) const;
// ORCA: whether this layer prints its outer walls first for the brim, see brim_puts_outer_walls_first().
bool outer_walls_first_for_brim() const;
private: private:
std::vector<Polygons> generate_lower_polygons_series(float width); std::vector<Polygons> generate_lower_polygons_series(float width);
@@ -183,6 +185,10 @@ private:
Polygons m_lower_slices_polygons; Polygons m_lower_slices_polygons;
}; };
// ORCA: a brim against the outer walls makes the first layer print them first, whatever the wall sequence, so the
// brim runs straight on into the outer wall. Auto, ear and painted brims may leave the outer walls bare.
bool brim_puts_outer_walls_first(BrimType brim_type, double brim_width);
} }
#endif #endif
+2 -2
View File
@@ -34,7 +34,7 @@ public:
explicit Polygon(const Points &points) : MultiPoint(points) {} explicit Polygon(const Points &points) : MultiPoint(points) {}
Polygon(std::initializer_list<Point> points) : MultiPoint(points) {} Polygon(std::initializer_list<Point> points) : MultiPoint(points) {}
Polygon(const Polygon &other) : MultiPoint(other.points) {} Polygon(const Polygon &other) : MultiPoint(other.points) {}
Polygon(Polygon &&other) : MultiPoint(std::move(other.points)) {} Polygon(Polygon &&other) noexcept : MultiPoint(std::move(other.points)) {}
static Polygon new_scale(const std::vector<Vec2d> &points) { static Polygon new_scale(const std::vector<Vec2d> &points) {
Polygon pgn; Polygon pgn;
pgn.points.reserve(points.size()); pgn.points.reserve(points.size());
@@ -43,7 +43,7 @@ public:
return pgn; return pgn;
} }
Polygon& operator=(const Polygon &other) { points = other.points; return *this; } Polygon& operator=(const Polygon &other) { points = other.points; return *this; }
Polygon& operator=(Polygon &&other) { points = std::move(other.points); return *this; } Polygon& operator=(Polygon &&other) noexcept { points = std::move(other.points); return *this; }
Point& operator[](Points::size_type idx) { return this->points[idx]; } Point& operator[](Points::size_type idx) { return this->points[idx]; }
const Point& operator[](Points::size_type idx) const { return this->points[idx]; } const Point& operator[](Points::size_type idx) const { return this->points[idx]; }
+2 -2
View File
@@ -26,7 +26,7 @@ class Polyline : public MultiPoint {
public: public:
Polyline() {}; Polyline() {};
Polyline(const Polyline& other) : MultiPoint(other.points), fitting_result(other.fitting_result) {} Polyline(const Polyline& other) : MultiPoint(other.points), fitting_result(other.fitting_result) {}
Polyline(Polyline &&other) : MultiPoint(std::move(other.points)), fitting_result(std::move(other.fitting_result)) {} Polyline(Polyline &&other) noexcept : MultiPoint(std::move(other.points)), fitting_result(std::move(other.fitting_result)) {}
Polyline(std::initializer_list<Point> list) : MultiPoint(list) { Polyline(std::initializer_list<Point> list) : MultiPoint(list) {
fitting_result.clear(); fitting_result.clear();
} }
@@ -47,7 +47,7 @@ public:
fitting_result = other.fitting_result; fitting_result = other.fitting_result;
return *this; return *this;
} }
Polyline& operator=(Polyline&& other) { Polyline& operator=(Polyline&& other) noexcept {
points = std::move(other.points); points = std::move(other.points);
fitting_result = std::move(other.fitting_result); fitting_result = std::move(other.fitting_result);
return *this; return *this;
+5 -3
View File
@@ -1183,9 +1183,9 @@ static std::vector<std::string> s_Preset_print_options{
"is_infill_first", "is_infill_first",
"sparse_infill_density", "sparse_infill_density",
"fill_multiline", "fill_multiline",
"tpms_adaptive", "adaptive_infill_mode",
"tpms_interior_density", "adaptive_infill_interior_density",
"tpms_adaptive_gradient", "adaptive_infill_gradient",
"gyroid_optimized", "gyroid_optimized",
"sparse_infill_pattern", "sparse_infill_pattern",
"sparse_infill_smooth_factor", "sparse_infill_smooth_factor",
@@ -1421,6 +1421,7 @@ static std::vector<std::string> s_Preset_print_options{
"initial_layer_travel_acceleration", "initial_layer_travel_acceleration",
"initial_layer_travel_jerk", "initial_layer_travel_jerk",
"exclude_object", "exclude_object",
"exclude_object_tolerance",
"slow_down_layers", "slow_down_layers",
"infill_anchor", "infill_anchor",
"infill_anchor_max", "infill_anchor_max",
@@ -2293,6 +2294,7 @@ void PresetCollection::load_project_embedded_presets(std::vector<Preset*>& proje
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file; BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file;
continue; continue;
} }
extend_default_config_length(config, false, {});
preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
//preset->config.apply(std::move(config)); //preset->config.apply(std::move(config));
Preset::normalize(preset->config); Preset::normalize(preset->config);
+8 -1
View File
@@ -756,7 +756,14 @@ public:
Preset& get_edited_preset() { return m_edited_preset; } Preset& get_edited_preset() { return m_edited_preset; }
const Preset& get_edited_preset() const { 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. // Return the last saved preset.
// const Preset& get_saved_preset() const { return m_saved_preset; } // const Preset& get_saved_preset() const { return m_saved_preset; }
+2 -1
View File
@@ -309,6 +309,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
"gcode_comments", "gcode_comments",
"gcode_label_objects", "gcode_label_objects",
"exclude_object", "exclude_object",
"exclude_object_tolerance",
"support_material_interface_fan_speed", "support_material_interface_fan_speed",
"internal_bridge_fan_speed", // ORCA: Add support for separate internal bridge fan speed control "internal_bridge_fan_speed", // ORCA: Add support for separate internal bridge fan speed control
"ironing_fan_speed", "ironing_fan_speed",
@@ -6929,7 +6930,7 @@ BoundingBoxf3 PrintInstance::get_bounding_box() const {
Polygon PrintInstance::get_convex_hull_2d() { Polygon PrintInstance::get_convex_hull_2d() {
Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix()); Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix());
poly.douglas_peucker(scale_(0.1)); poly.douglas_peucker(scale_(print_object->print()->config().exclude_object_tolerance.value));
return poly; return poly;
} }
+4 -4
View File
@@ -12,7 +12,7 @@
#include "PrintBase.hpp" #include "PrintBase.hpp"
#include "Fill/FillAdaptive.hpp" #include "Fill/FillAdaptive.hpp"
#include "Fill/FillLightning.hpp" #include "Fill/FillLightning.hpp"
#include "Fill/FillTpmsAdaptive.hpp" #include "Fill/AdaptiveInfillField.hpp"
#include "BoundingBox.hpp" #include "BoundingBox.hpp"
#include "ExtrusionEntityCollection.hpp" #include "ExtrusionEntityCollection.hpp"
@@ -407,7 +407,7 @@ public:
double max_z() const { return m_max_z; } double max_z() const { return m_max_z; }
// Centering offset of the sliced mesh from the scaled and rotated mesh of the model. // Centering offset of the sliced mesh from the scaled and rotated mesh of the model.
const Point& center_offset() const { return m_center_offset; } const Point& center_offset() const { return m_center_offset; }
const TpmsRadialField* tpms_radial_field(TpmsAdaptiveMode mode) const { return m_tpms_radial_fields[size_t(mode)].get(); } const AdaptiveInfillField* adaptive_infill_field(AdaptiveInfillMode mode) const { return m_adaptive_infill_fields[size_t(mode)].get(); }
// BBS // BBS
void generate_support_preview(); void generate_support_preview();
@@ -655,7 +655,7 @@ private:
FillAdaptive::RegionOctrees prepare_adaptive_infill_data( FillAdaptive::RegionOctrees prepare_adaptive_infill_data(
const std::vector<std::pair<const Surface*, const Layer*>>& surfaces_w_layer) const; const std::vector<std::pair<const Surface*, const Layer*>>& surfaces_w_layer) const;
FillLightning::GeneratorPtr prepare_lightning_infill_data(); FillLightning::GeneratorPtr prepare_lightning_infill_data();
TpmsRadialFields prepare_tpms_radial_fields() const; AdaptiveInfillFields prepare_adaptive_infill_fields() const;
// BBS // BBS
SupportNecessaryType is_support_necessary(); SupportNecessaryType is_support_necessary();
@@ -706,7 +706,7 @@ private:
FillAdaptive::RegionOctrees m_adaptive_fill_octrees; FillAdaptive::RegionOctrees m_adaptive_fill_octrees;
std::vector<BoundingBox> m_separated_body_bboxes; std::vector<BoundingBox> m_separated_body_bboxes;
FillLightning::GeneratorPtr m_lightning_generator; FillLightning::GeneratorPtr m_lightning_generator;
TpmsRadialFields m_tpms_radial_fields; AdaptiveInfillFields m_adaptive_infill_fields;
std::vector < VolumeSlices > firstLayerObjSliceByVolume; std::vector < VolumeSlices > firstLayerObjSliceByVolume;
std::vector<groupedVolumeSlices> firstLayerObjSliceByGroups; std::vector<groupedVolumeSlices> firstLayerObjSliceByGroups;
+39 -22
View File
@@ -366,24 +366,24 @@ static t_config_enum_values s_keys_map_SurfaceFillOrder{
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SurfaceFillOrder) CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SurfaceFillOrder)
//Orca //Orca
static t_config_enum_values s_keys_map_TpmsAdaptiveMode{ static t_config_enum_values s_keys_map_AdaptiveInfillMode{
{ "disabled", int(TpmsAdaptiveMode::Disabled) }, { "disabled", int(AdaptiveInfillMode::Disabled) },
{ "distance_warp", int(TpmsAdaptiveMode::DistanceWarp) }, { "distance_warp", int(AdaptiveInfillMode::DistanceWarp) },
{ "smooth_blend", int(TpmsAdaptiveMode::SmoothBlend) }, { "smooth_blend", int(AdaptiveInfillMode::SmoothBlend) },
{ "stepped_shells", int(TpmsAdaptiveMode::SteppedShells) }, { "stepped_shells", int(AdaptiveInfillMode::SteppedShells) },
{ "lobes", int(TpmsAdaptiveMode::Lobes) }, { "lobes", int(AdaptiveInfillMode::Lobes) },
{ "normal_z", int(TpmsAdaptiveMode::NormalZ) }, { "normal_z", int(AdaptiveInfillMode::NormalZ) },
{ "normal_y", int(TpmsAdaptiveMode::NormalY) }, { "normal_y", int(AdaptiveInfillMode::NormalY) },
{ "normal_x", int(TpmsAdaptiveMode::NormalX) }, { "normal_x", int(AdaptiveInfillMode::NormalX) },
}; };
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveMode) CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(AdaptiveInfillMode)
static t_config_enum_values s_keys_map_TpmsAdaptiveGradient{ static t_config_enum_values s_keys_map_AdaptiveInfillGradient{
{ "linear", int(TpmsAdaptiveGradient::Linear) }, { "linear", int(AdaptiveInfillGradient::Linear) },
{ "quadratic", int(TpmsAdaptiveGradient::Quadratic) }, { "quadratic", int(AdaptiveInfillGradient::Quadratic) },
{ "exponential", int(TpmsAdaptiveGradient::Exponential) }, { "exponential", int(AdaptiveInfillGradient::Exponential) },
}; };
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveGradient) CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(AdaptiveInfillGradient)
//BBS //BBS
static t_config_enum_values s_keys_map_PrintSequence { static t_config_enum_values s_keys_map_PrintSequence {
@@ -3660,7 +3660,7 @@ void PrintConfigDef::init_fff_params()
def->max = 10; // Maximum number of lines for infill pattern def->max = 10; // Maximum number of lines for infill pattern
def->set_default_value(new ConfigOptionInt(1)); def->set_default_value(new ConfigOptionInt(1));
def = this->add("tpms_adaptive", coEnum); def = this->add("adaptive_infill_mode", coEnum);
def->label = L("Adaptive density (experimental)"); def->label = L("Adaptive density (experimental)");
def->category = L("Strength"); def->category = L("Strength");
def->tooltip = L("Grades the Gyroid and TPMS infill inside the object: its cells grow from the surface of the " def->tooltip = L("Grades the Gyroid and TPMS infill inside the object: its cells grow from the surface of the "
@@ -3678,7 +3678,7 @@ void PrintConfigDef::init_fff_params()
"joined to the rest by a narrower neck, is graded towards its own center.\n" "joined to the rest by a narrower neck, is graded towards its own center.\n"
" - Normal Z, Y or X: follows the sections of the object normal to that axis, so the density " " - Normal Z, Y or X: follows the sections of the object normal to that axis, so the density "
"does not change along it."); "does not change along it.");
def->enum_keys_map = &ConfigOptionEnum<TpmsAdaptiveMode>::get_enum_values(); def->enum_keys_map = &ConfigOptionEnum<AdaptiveInfillMode>::get_enum_values();
def->enum_values.push_back("disabled"); def->enum_values.push_back("disabled");
def->enum_values.push_back("distance_warp"); def->enum_values.push_back("distance_warp");
def->enum_values.push_back("smooth_blend"); def->enum_values.push_back("smooth_blend");
@@ -3696,9 +3696,9 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.push_back(L("Normal Y")); def->enum_labels.push_back(L("Normal Y"));
def->enum_labels.push_back(L("Normal X")); def->enum_labels.push_back(L("Normal X"));
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionEnum<TpmsAdaptiveMode>(TpmsAdaptiveMode::Disabled)); def->set_default_value(new ConfigOptionEnum<AdaptiveInfillMode>(AdaptiveInfillMode::Disabled));
def = this->add("tpms_interior_density", coPercent); def = this->add("adaptive_infill_interior_density", coPercent);
def->label = L("Interior density"); def->label = L("Interior density");
def->category = L("Strength"); def->category = L("Strength");
def->tooltip = L("Density of the adaptive infill at the center of the object."); def->tooltip = L("Density of the adaptive infill at the center of the object.");
@@ -3708,7 +3708,7 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionPercent(5)); def->set_default_value(new ConfigOptionPercent(5));
def = this->add("tpms_adaptive_gradient", coEnum); def = this->add("adaptive_infill_gradient", coEnum);
def->label = L("Adaptive gradient"); def->label = L("Adaptive gradient");
def->category = L("Strength"); def->category = L("Strength");
def->tooltip = L("How the density changes from the surface to the center of the object.\n" def->tooltip = L("How the density changes from the surface to the center of the object.\n"
@@ -3717,7 +3717,7 @@ void PrintConfigDef::init_fff_params()
"changes faster towards the center.\n" "changes faster towards the center.\n"
"Exponential: the density changes quickly just below the surface and levels off towards " "Exponential: the density changes quickly just below the surface and levels off towards "
"the center."); "the center.");
def->enum_keys_map = &ConfigOptionEnum<TpmsAdaptiveGradient>::get_enum_values(); def->enum_keys_map = &ConfigOptionEnum<AdaptiveInfillGradient>::get_enum_values();
def->enum_values.push_back("linear"); def->enum_values.push_back("linear");
def->enum_values.push_back("quadratic"); def->enum_values.push_back("quadratic");
def->enum_values.push_back("exponential"); def->enum_values.push_back("exponential");
@@ -3725,7 +3725,7 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.push_back(L("Quadratic")); def->enum_labels.push_back(L("Quadratic"));
def->enum_labels.push_back(L("Exponential")); def->enum_labels.push_back(L("Exponential"));
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionEnum<TpmsAdaptiveGradient>(TpmsAdaptiveGradient::Linear)); def->set_default_value(new ConfigOptionEnum<AdaptiveInfillGradient>(AdaptiveInfillGradient::Linear));
// Z-buckling bias optimization (experimental). Tightens the gyroid wave along the Z // Z-buckling bias optimization (experimental). Tightens the gyroid wave along the Z
// (vertical) axis at low infill density to shorten the effective column length under // (vertical) axis at low infill density to shorten the effective column length under
@@ -4629,6 +4629,17 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false)); def->set_default_value(new ConfigOptionBool(false));
def = this->add("exclude_object_tolerance", coFloat);
def->label = L("Outline tolerance");
def->tooltip = L("Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the "
"original outline. Larger values give fewer points, which helps firmware that stalls "
"while parsing G-code with many objects. 0 keeps every point.");
def->sidetext = L("mm");
def->min = 0;
def->max = 5;
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloat(0.1));
def = this->add("gcode_comments", coBool); def = this->add("gcode_comments", coBool);
def->label = L("Verbose G-code"); def->label = L("Verbose G-code");
def->tooltip = L("Enable this to get a commented G-code file, with each line explained by a descriptive text. " def->tooltip = L("Enable this to get a commented G-code file, with each line explained by a descriptive text. "
@@ -9654,6 +9665,12 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
opt_key = "change_filament_gcode"; opt_key = "change_filament_gcode";
} else if (opt_key == "bridge_fan_speed") { } else if (opt_key == "bridge_fan_speed") {
opt_key = "overhang_fan_speed"; opt_key = "overhang_fan_speed";
} else if (opt_key == "tpms_adaptive") {
opt_key = "adaptive_infill_mode";
} else if (opt_key == "tpms_interior_density") {
opt_key = "adaptive_infill_interior_density";
} else if (opt_key == "tpms_adaptive_gradient") {
opt_key = "adaptive_infill_gradient";
} else if (opt_key == "infill_extruder" || opt_key == "sparse_infill_filament") { } else if (opt_key == "infill_extruder" || opt_key == "sparse_infill_filament") {
// ORCA: legacy feature-filament selector. Pre-2.4.0-dev these keys were 1-based and the // ORCA: legacy feature-filament selector. Pre-2.4.0-dev these keys were 1-based and the
// default value "1" meant "the first/active filament". The current scheme uses a dedicated // default value "1" meant "the first/active filament". The current scheme uses a dedicated
+12 -11
View File
@@ -172,8 +172,8 @@ inline bool is_separable_infill_pattern(InfillPattern pattern)
// Orca: Infill patterns laid out by an octree, which each connected body always gets of its own. // Orca: Infill patterns laid out by an octree, which each connected body always gets of its own.
inline bool is_octree_infill_pattern(InfillPattern pattern) { return pattern == ipAdaptiveCubic || pattern == ipSupportCubic; } inline bool is_octree_infill_pattern(InfillPattern pattern) { return pattern == ipAdaptiveCubic || pattern == ipSupportCubic; }
// Orca: Infill patterns graded by the "tpms_adaptive" option. // Orca: Infill patterns graded by the "adaptive_infill_mode" option.
inline bool is_tpms_adaptive_pattern(InfillPattern pattern) { return pattern == ipGyroid || pattern == ipTpmsD || pattern == ipTpmsFK; } inline bool is_adaptive_infill_pattern(InfillPattern pattern) { return pattern == ipGyroid || pattern == ipTpmsD || pattern == ipTpmsFK; }
// Orca: Infill patterns that round their corners by the "sparse_infill_smooth_factor" option. // Orca: Infill patterns that round their corners by the "sparse_infill_smooth_factor" option.
// Grid, Triangles and Tri-hexagon only do so in their trapezoidal form, which is generated with more // Grid, Triangles and Tri-hexagon only do so in their trapezoidal form, which is generated with more
@@ -279,8 +279,8 @@ enum class SurfaceFillOrder {
Count, Count,
}; };
// Orca: what the adaptive TPMS density follows: the 3D shape of the object, or its 2D sections normal to an axis. // Orca: what the adaptive infill density follows: the 3D shape of the object, or its 2D sections normal to an axis.
enum class TpmsAdaptiveMode { enum class AdaptiveInfillMode {
Disabled, Disabled,
DistanceWarp, DistanceWarp,
SmoothBlend, SmoothBlend,
@@ -292,8 +292,8 @@ enum class TpmsAdaptiveMode {
Count, Count,
}; };
// Orca: how the adaptive TPMS density changes from the object surface to its deepest point. // Orca: how the adaptive infill density changes from the object surface to its deepest point.
enum class TpmsAdaptiveGradient { enum class AdaptiveInfillGradient {
Linear, Linear,
Quadratic, Quadratic,
Exponential, Exponential,
@@ -816,8 +816,8 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PerimeterGeneratorType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ToolChangeOrderingType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ToolChangeOrderingType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PowerLossRecoveryMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PowerLossRecoveryMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TpmsAdaptiveMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AdaptiveInfillMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TpmsAdaptiveGradient) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AdaptiveInfillGradient)
#undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS #undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS
@@ -1501,9 +1501,9 @@ PRINT_CONFIG_CLASS_DEFINE(
// Orca: // Orca:
((ConfigOptionFloatOrPercent, infill_combination_max_layer_height)) ((ConfigOptionFloatOrPercent, infill_combination_max_layer_height))
((ConfigOptionInt, fill_multiline)) ((ConfigOptionInt, fill_multiline))
((ConfigOptionEnum<TpmsAdaptiveMode>, tpms_adaptive)) ((ConfigOptionEnum<AdaptiveInfillMode>, adaptive_infill_mode))
((ConfigOptionPercent, tpms_interior_density)) ((ConfigOptionPercent, adaptive_infill_interior_density))
((ConfigOptionEnum<TpmsAdaptiveGradient>, tpms_adaptive_gradient)) ((ConfigOptionEnum<AdaptiveInfillGradient>, adaptive_infill_gradient))
((ConfigOptionBool, gyroid_optimized)) ((ConfigOptionBool, gyroid_optimized))
// Ironing options // Ironing options
((ConfigOptionEnum<IroningType>, ironing_type)) ((ConfigOptionEnum<IroningType>, ironing_type))
@@ -2130,6 +2130,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionPercents, filament_shrinkage_compensation_z)) ((ConfigOptionPercents, filament_shrinkage_compensation_z))
((ConfigOptionBool, gcode_label_objects)) ((ConfigOptionBool, gcode_label_objects))
((ConfigOptionBool, exclude_object)) ((ConfigOptionBool, exclude_object))
((ConfigOptionFloat, exclude_object_tolerance))
((ConfigOptionFloats, grab_length)) ((ConfigOptionFloats, grab_length))
((ConfigOptionBool, gcode_comments)) ((ConfigOptionBool, gcode_comments))
((ConfigOptionInt, slow_down_layers)) ((ConfigOptionInt, slow_down_layers))
+373 -206
View File
@@ -17,6 +17,7 @@
#include "I18N.hpp" #include "I18N.hpp"
#include "Layer.hpp" #include "Layer.hpp"
#include "MutablePolygon.hpp" #include "MutablePolygon.hpp"
#include "PerimeterGenerator.hpp"
#include "PrintBase.hpp" #include "PrintBase.hpp"
#include "PrintConfig.hpp" #include "PrintConfig.hpp"
#include "SLA/IndexedMesh.hpp" #include "SLA/IndexedMesh.hpp"
@@ -32,7 +33,7 @@
#include "Fill/FillAdaptive.hpp" #include "Fill/FillAdaptive.hpp"
#include "Fill/Fill.hpp" #include "Fill/Fill.hpp"
#include "Fill/FillLightning.hpp" #include "Fill/FillLightning.hpp"
#include "Fill/FillTpmsAdaptive.hpp" #include "Fill/AdaptiveInfillField.hpp"
#include "format.hpp" #include "format.hpp"
#include "AABBTreeIndirect.hpp" #include "AABBTreeIndirect.hpp"
#include "AABBTreeLines.hpp" #include "AABBTreeLines.hpp"
@@ -49,6 +50,7 @@
#include <cstdlib> #include <cstdlib>
#include <cstdint> #include <cstdint>
#include <float.h> #include <float.h>
#include <array>
#include <functional> #include <functional>
#include <ios> #include <ios>
#include <iomanip> #include <iomanip>
@@ -75,6 +77,7 @@
#include <Eigen/Core> #include <Eigen/Core>
#include <tbb/parallel_for.h> #include <tbb/parallel_for.h>
#include <tbb/parallel_invoke.h>
#include <tbb/spin_mutex.h> #include <tbb/spin_mutex.h>
#include <tbb/concurrent_unordered_set.h> #include <tbb/concurrent_unordered_set.h>
@@ -1251,19 +1254,19 @@ FillLightning::GeneratorPtr PrintObject::prepare_lightning_infill_data()
return has_lightning_infill ? FillLightning::build_generator(std::as_const(*this), [this]() -> void { this->throw_if_canceled(); }) : FillLightning::GeneratorPtr(); return has_lightning_infill ? FillLightning::build_generator(std::as_const(*this), [this]() -> void { this->throw_if_canceled(); }) : FillLightning::GeneratorPtr();
} }
TpmsRadialFields PrintObject::prepare_tpms_radial_fields() const AdaptiveInfillFields PrintObject::prepare_adaptive_infill_fields() const
{ {
TpmsRadialFields fields; AdaptiveInfillFields fields;
std::array<bool, size_t(TpmsAdaptiveMode::Count)> modes{}; std::array<bool, size_t(AdaptiveInfillMode::Count)> modes{};
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id)
if (const PrintRegionConfig &config = this->printing_region(region_id).config(); if (const PrintRegionConfig &config = this->printing_region(region_id).config();
config.sparse_infill_density > 0 && config.sparse_infill_density < 100 && is_tpms_adaptive_pattern(config.sparse_infill_pattern)) config.sparse_infill_density > 0 && config.sparse_infill_density < 100 && is_adaptive_infill_pattern(config.sparse_infill_pattern))
modes[size_t(config.tpms_adaptive.value)] = true; modes[size_t(config.adaptive_infill_mode.value)] = true;
modes[size_t(TpmsAdaptiveMode::Disabled)] = false; modes[size_t(AdaptiveInfillMode::Disabled)] = false;
if (std::find(modes.begin(), modes.end(), true) == modes.end() || m_layers.empty()) if (std::find(modes.begin(), modes.end(), true) == modes.end() || m_layers.empty())
return fields; return fields;
std::vector<TpmsRadialField::Slice> slices; std::vector<AdaptiveInfillField::Slice> slices;
slices.reserve(m_layers.size()); slices.reserve(m_layers.size());
BoundingBox bbox; BoundingBox bbox;
for (const Layer *layer : m_layers) { for (const Layer *layer : m_layers) {
@@ -1276,7 +1279,7 @@ TpmsRadialFields PrintObject::prepare_tpms_radial_fields() const
if (!modes[mode]) if (!modes[mode])
continue; continue;
// Without a field, the infill falls back to the regular pattern. // Without a field, the infill falls back to the regular pattern.
auto field = std::make_unique<TpmsRadialField>(slices, bbox, TpmsAdaptiveMode(mode), [this]() { m_print->throw_if_canceled(); }); auto field = std::make_unique<AdaptiveInfillField>(slices, bbox, AdaptiveInfillMode(mode), [this]() { m_print->throw_if_canceled(); });
if (!field->empty()) if (!field->empty())
fields[mode] = std::move(field); fields[mode] = std::move(field);
} }
@@ -1464,15 +1467,13 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "top_surface_speed") { || opt_key == "top_surface_speed") {
// Brim is printed below supports, support invalidates brim and skirt. // Brim is printed below supports, support invalidates brim and skirt.
steps.emplace_back(posSupportMaterial); steps.emplace_back(posSupportMaterial);
if (opt_key == "brim_type") { if (opt_key == "brim_type" || opt_key == "brim_width") {
const auto* old_brim_type = old_config.option<ConfigOptionEnum<BrimType>>(opt_key); // Regenerate the walls when the brim starts or stops putting the first layer's outer walls first.
const auto* new_brim_type = new_config.option<ConfigOptionEnum<BrimType>>(opt_key); auto outer_walls_first = [](const ConfigOptionResolver &config) {
//BBS: When switch to manual brim, the object must have brim, then re-generate perimeter return brim_puts_outer_walls_first(config.option<ConfigOptionEnum<BrimType>>("brim_type")->value,
//to make the wall order of first layer to be outer-first config.option<ConfigOptionFloat>("brim_width")->value);
// btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it };
// takes part in the same first-layer wall order rule. if (outer_walls_first(old_config) != outer_walls_first(new_config))
if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly ||
old_brim_type->value == btLeadingEdgeOnly || new_brim_type->value == btLeadingEdgeOnly)
steps.emplace_back(posPerimeters); steps.emplace_back(posPerimeters);
} }
} else if ( } else if (
@@ -1686,9 +1687,9 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "infill_overhang_angle") { || opt_key == "infill_overhang_angle") {
steps.emplace_back(posInfill); steps.emplace_back(posInfill);
} else if (opt_key == "sparse_infill_pattern" } else if (opt_key == "sparse_infill_pattern"
|| opt_key == "tpms_adaptive" || opt_key == "adaptive_infill_mode"
|| opt_key == "tpms_interior_density" || opt_key == "adaptive_infill_interior_density"
|| opt_key == "tpms_adaptive_gradient" || opt_key == "adaptive_infill_gradient"
// Orca: Body centering now also determines bridge anchors during preparation. // Orca: Body centering now also determines bridge anchors during preparation.
// Invalidating preparation also invalidates infill, including top/bottom surfaces. // Invalidating preparation also invalidates infill, including top/bottom surfaces.
|| opt_key == "center_of_surface_pattern" || opt_key == "center_of_surface_pattern"
@@ -1931,7 +1932,9 @@ void PrintObject::detect_surfaces_type()
bool interface_shells = ! spiral_mode && m_config.interface_shells.value; bool interface_shells = ! spiral_mode && m_config.interface_shells.value;
size_t num_layers = spiral_mode ? std::min(size_t(this->printing_region(0).config().bottom_shell_layers), m_layers.size()) : m_layers.size(); size_t num_layers = spiral_mode ? std::min(size_t(this->printing_region(0).config().bottom_shell_layers), m_layers.size()) : m_layers.size();
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) { // The regions of a layer do not see each other here, and a layer cut through a fine relief takes far longer than the
// others, so the regions run next to each other instead of one after another, each still over all layers.
tbb::parallel_for(size_t(0), this->num_printing_regions(), [&](size_t region_id) {
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " in parallel - start"; BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " in parallel - start";
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING #ifdef SLIC3R_DEBUG_SLICE_PROCESSING
for (Layer *layer : m_layers) for (Layer *layer : m_layers)
@@ -1989,7 +1992,7 @@ void PrintObject::detect_surfaces_type()
if (upper_layer) { if (upper_layer) {
ExPolygons upper_slices = interface_shells ? ExPolygons upper_slices = interface_shells ?
diff_ex(layerm_slices_surfaces, upper_layer->m_regions[region_id]->slices.surfaces, ApplySafetyOffset::Yes) : diff_ex(layerm_slices_surfaces, upper_layer->m_regions[region_id]->slices.surfaces, ApplySafetyOffset::Yes) :
diff_ex(layerm_slices_surfaces, upper_layer->lslices, ApplySafetyOffset::Yes); diff_ex_by_piece(layerm_slices_surfaces, to_polygons(upper_layer->lslices), ApplySafetyOffset::Yes);
surfaces_append(top, opening_ex(upper_slices, offset), stTop); surfaces_append(top, opening_ex(upper_slices, offset), stTop);
} else { } else {
// if no upper layer, all surfaces of this one are solid // if no upper layer, all surfaces of this one are solid
@@ -2015,7 +2018,7 @@ void PrintObject::detect_surfaces_type()
surfaces_append( surfaces_append(
bottom, bottom,
opening_ex( opening_ex(
diff_ex(layerm_slices_surfaces, lower_layer->lslices, ApplySafetyOffset::Yes), diff_ex_by_piece(layerm_slices_surfaces, to_polygons(lower_layer->lslices), ApplySafetyOffset::Yes),
offset), offset),
surface_type_bottom_other); surface_type_bottom_other);
// if user requested internal shells, we need to identify surfaces // if user requested internal shells, we need to identify surfaces
@@ -2046,34 +2049,44 @@ void PrintObject::detect_surfaces_type()
// and top surfaces; let's do an intersection to discover them and consider them // and top surfaces; let's do an intersection to discover them and consider them
// as bottom surfaces (to allow for bridge detection) // as bottom surfaces (to allow for bridge detection)
if (! top.empty() && ! bottom.empty()) { if (! top.empty() && ! bottom.empty()) {
const auto cracks = intersection_ex(top, bottom); const auto cracks = intersection_ex_by_piece(to_expolygons(top), to_polygons(bottom));
if (!cracks.empty()) { if (!cracks.empty()) {
if (lower_layer) { // Only detect small cracks for non-first layer, because first layer should always be bottom if (lower_layer) { // Only detect small cracks for non-first layer, because first layer should always be bottom
const float small_crack_threshold = -layerm->flow(frExternalPerimeter).scaled_width() * 1.5; const float small_crack_threshold = -layerm->flow(frExternalPerimeter).scaled_width() * 1.5;
// Only the bottom surfaces near a crack can take part: one that contains it must contain its box,
// and one whose box misses the grown crack is left unchanged by removing it. A layer cut through
// a fine relief has thousands of both, which made this loop quadratic.
for (const auto& crack : cracks) { for (const auto& crack : cracks) {
if (offset_ex(crack, small_crack_threshold).empty()) { if (offset_ex(crack, small_crack_threshold).empty()) {
// For small cracks, if it's part of a large bottom surface, then it should be added to bottom as well // For small cracks, if it's part of a large bottom surface, then it should be added to bottom as well
if (std::any_of(bottom.begin(), bottom.end(), [&crack, small_crack_threshold](const Surface& s) { const BoundingBox crack_bbox = get_extents(crack);
if (std::any_of(bottom.begin(), bottom.end(), [&crack, &crack_bbox, small_crack_threshold](const Surface& s) {
const auto& se = s.expolygon; const auto& se = s.expolygon;
return diff_ex(crack, se, ApplySafetyOffset::Yes).empty() return get_extents(se).inflated(SCALED_EPSILON).contains(crack_bbox)
&& diff_ex(crack, se, ApplySafetyOffset::Yes).empty()
&& se.area() > crack.area() * 2 && se.area() > crack.area() * 2
&& !offset_ex(diff_ex(se, crack), small_crack_threshold).empty(); && !offset_ex(diff_ex(se, crack), small_crack_threshold).empty();
})) continue; })) continue;
// Crack too small, leave it as part of the top surface, remove it from bottom surfaces // Crack too small, leave it as part of the top surface, remove it from bottom surfaces
const ExPolygons grown_crack = offset_ex(crack, -small_crack_threshold);
const BoundingBox grown_bbox = get_extents(grown_crack);
Surfaces bot_tmp; Surfaces bot_tmp;
for (auto& b : bottom) { for (auto& b : bottom) {
surfaces_append(bot_tmp, diff_ex(b.expolygon, offset_ex(crack, -small_crack_threshold)), b.surface_type); if (get_extents(b.expolygon).overlap(grown_bbox))
surfaces_append(bot_tmp, diff_ex(b.expolygon, grown_crack), b.surface_type);
else
bot_tmp.emplace_back(std::move(b));
} }
bottom = std::move(bot_tmp); bottom = std::move(bot_tmp);
} }
} }
} }
Polygons top_polygons = to_polygons(std::move(top)); ExPolygons top_expolygons = to_expolygons(std::move(top));
top.clear(); top.clear();
surfaces_append(top, diff_ex(top_polygons, bottom), stTop); surfaces_append(top, diff_ex_by_piece(top_expolygons, to_polygons(bottom)), stTop);
} }
} }
@@ -2164,7 +2177,7 @@ void PrintObject::detect_surfaces_type()
{ {
Polygons topbottom = to_polygons(top); Polygons topbottom = to_polygons(top);
polygons_append(topbottom, to_polygons(bottom)); polygons_append(topbottom, to_polygons(bottom));
surfaces_append(surfaces_out, diff_ex(surfaces_prev_expolys, topbottom), stInternal); surfaces_append(surfaces_out, diff_ex_by_piece(surfaces_prev_expolys, topbottom), stInternal);
} }
surfaces_append(surfaces_out, std::move(top)); surfaces_append(surfaces_out, std::move(top));
@@ -2341,29 +2354,31 @@ void PrintObject::detect_surfaces_type()
} }
} }
); );
// ==============================================================================================================
// === ORCA: Interim workaround - for now the new stInternalAfterExternalBridge surfaace is re-classified ==============
// === back to a bottom bridge. As a starting point, this improves bridging reliability as it extrudes ==========
// === two external bridge layers. However, TODO: Implement a new surface type throughout the codebase ==========
// ==============================================================================================================
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) {
tbb::parallel_for( tbb::blocked_range<size_t>(0, m_layers.size()), [this, region_id](const tbb::blocked_range<size_t> &range) {
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer) {
Surfaces &surfs = m_layers[idx_layer]->m_regions[region_id]->slices.surfaces;
for (Surface &s : surfs) {
if (s.surface_type == stInternalAfterExternalBridge) {
s.surface_type = stBottomBridge;
}
}
}
}
);
}
} }
// ============================================================================================================== // ==============================================================================================================
// === ORCA: End of second external bridge layer changes ======================================================= // === ORCA: End of second external bridge layer changes =======================================================
// ============================================================================================================== // ==============================================================================================================
}); // for each this->print->region_count
// ==============================================================================================================
// === ORCA: Interim workaround - for now the new stInternalAfterExternalBridge surfaace is re-classified ==============
// === back to a bottom bridge. As a starting point, this improves bridging reliability as it extrudes ==========
// === two external bridge layers. However, TODO: Implement a new surface type throughout the codebase ==========
// ==============================================================================================================
// Once all the regions have their second bridge layer, and before their slices are trimmed into fill surfaces below.
if ((this->config().enable_extra_bridge_layer.value == eblApplyToAll) || (this->config().enable_extra_bridge_layer.value == eblExternalBridgeOnly)) {
tbb::parallel_for(tbb::blocked_range<size_t>(0, m_layers.size()), [this](const tbb::blocked_range<size_t> &range) {
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer)
for (LayerRegion *layerm : m_layers[idx_layer]->regions())
for (Surface &s : layerm->slices.surfaces)
if (s.surface_type == stInternalAfterExternalBridge)
s.surface_type = stBottomBridge;
});
m_print->throw_if_canceled();
}
tbb::parallel_for(size_t(0), this->num_printing_regions(), [&](size_t region_id) {
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - start"; BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - start";
// Fill in layerm->fill_surfaces by trimming the layerm->slices by the cummulative layerm->fill_surfaces. // Fill in layerm->fill_surfaces by trimming the layerm->slices by the cummulative layerm->fill_surfaces.
tbb::parallel_for( tbb::parallel_for(
@@ -2380,7 +2395,7 @@ void PrintObject::detect_surfaces_type()
}); });
m_print->throw_if_canceled(); m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - end"; BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - end";
} // for each this->print->region_count });
// Mark the object to have the region slices classified (typed, which also means they are split based on whether they are supported, bridging, top layers etc.) // Mark the object to have the region slices classified (typed, which also means they are split based on whether they are supported, bridging, top layers etc.)
m_typed_slices = true; m_typed_slices = true;
@@ -2447,8 +2462,10 @@ void PrintObject::process_external_surfaces()
BOOST_LOG_TRIVIAL(debug) << "Collecting surfaces covered with extrusions in parallel - end"; BOOST_LOG_TRIVIAL(debug) << "Collecting surfaces covered with extrusions in parallel - end";
} }
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) { BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces in parallel - start";
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces for region " << region_id << " in parallel - start"; // The regions of a layer do not see each other here, and a layer cut through a fine relief takes far longer than the
// others, so the regions run next to each other instead of one after another, each still over all layers.
tbb::parallel_for(size_t(0), this->num_printing_regions(), [this, &surfaces_covered](size_t region_id) {
tbb::parallel_for( tbb::parallel_for(
tbb::blocked_range<size_t>(0, m_layers.size()), tbb::blocked_range<size_t>(0, m_layers.size()),
[this, &surfaces_covered, region_id](const tbb::blocked_range<size_t>& range) { [this, &surfaces_covered, region_id](const tbb::blocked_range<size_t>& range) {
@@ -2463,9 +2480,9 @@ void PrintObject::process_external_surfaces()
} }
} }
); );
m_print->throw_if_canceled(); });
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces for region " << region_id << " in parallel - end"; m_print->throw_if_canceled();
} BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces in parallel - end";
} }
void PrintObject::discover_vertical_shells() void PrintObject::discover_vertical_shells()
@@ -2504,10 +2521,10 @@ void PrintObject::discover_vertical_shells()
// The "ensure vertical wall thickness" feature is not applicable to any of the regions. Quit. // The "ensure vertical wall thickness" feature is not applicable to any of the regions. Quit.
return; return;
BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - start : cache top / bottom"; BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - start : cache top / bottom";
//FIXME Improve the heuristics for a grain size. // One layer per task: on a layer cut through a fine relief the unions below take far longer than elsewhere, and a
size_t grain_size = std::max(num_layers / 16, size_t(1)); // few such layers next to each other must not end up in one task.
tbb::parallel_for( tbb::parallel_for(
tbb::blocked_range<size_t>(0, num_layers, grain_size), tbb::blocked_range<size_t>(0, num_layers, 1),
[this, &cache_top_botom_regions](const tbb::blocked_range<size_t>& range) { [this, &cache_top_botom_regions](const tbb::blocked_range<size_t>& range) {
const std::initializer_list<SurfaceType> surfaces_bottom { stBottom, stBottomBridge }; const std::initializer_list<SurfaceType> surfaces_bottom { stBottom, stBottomBridge };
const size_t num_regions = this->num_printing_regions(); const size_t num_regions = this->num_printing_regions();
@@ -2515,67 +2532,198 @@ void PrintObject::discover_vertical_shells()
m_print->throw_if_canceled(); m_print->throw_if_canceled();
const Layer &layer = *m_layers[idx_layer]; const Layer &layer = *m_layers[idx_layer];
DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[idx_layer]; DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[idx_layer];
// Simulate single set of perimeters over all merged regions. const auto top_bottom_expansion = [&layer](size_t region_id) {
float perimeter_offset = 0.f; return float(layer.m_regions[region_id]->flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff;
float perimeter_min_spacing = FLT_MAX; };
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING #ifdef SLIC3R_DEBUG_SLICE_PROCESSING
static size_t debug_idx = 0; static size_t debug_idx = 0;
++ debug_idx; ++ debug_idx;
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
for (size_t region_id = 0; region_id < num_regions; ++ region_id) { // The top surfaces, the bottom surfaces and the holes are independent of each other.
LayerRegion &layerm = *layer.m_regions[region_id]; tbb::parallel_invoke(
float top_bottom_expansion = float(layerm.flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff; [&]() {
// Top surfaces. for (size_t region_id = 0; region_id < num_regions; ++ region_id)
append(cache.top_surfaces, offset(layerm.slices.filter_by_type(stTop), top_bottom_expansion)); append(cache.top_surfaces, offset(layer.m_regions[region_id]->slices.filter_by_type(stTop), top_bottom_expansion(region_id)));
// append(cache.top_surfaces, offset(layerm.fill_surfaces.filter_by_type(stTop), top_bottom_expansion)); // append(cache.top_surfaces, offset(layerm.fill_surfaces.filter_by_type(stTop), top_bottom_expansion));
// Bottom surfaces. // Save some computing time by reducing the number of polygons.
append(cache.bottom_surfaces, offset(layerm.slices.filter_by_types(surfaces_bottom), top_bottom_expansion)); cache.top_surfaces = union_(cache.top_surfaces);
// append(cache.bottom_surfaces, offset(layerm.fill_surfaces.filter_by_types(surfaces_bottom), top_bottom_expansion)); },
// Calculate the maximum perimeter offset as if the slice was extruded with a single extruder only. [&]() {
// First find the maxium number of perimeters per region slice. for (size_t region_id = 0; region_id < num_regions; ++ region_id)
unsigned int perimeters = 0; append(cache.bottom_surfaces, offset(layer.m_regions[region_id]->slices.filter_by_types(surfaces_bottom), top_bottom_expansion(region_id)));
for (Surface &s : layerm.slices.surfaces) // append(cache.bottom_surfaces, offset(layerm.fill_surfaces.filter_by_types(surfaces_bottom), top_bottom_expansion));
perimeters = std::max<unsigned int>(perimeters, s.extra_perimeters); cache.bottom_surfaces = union_(cache.bottom_surfaces);
perimeters += layerm.region().config().wall_loops.value; },
// Then calculate the infill offset. [&]() {
if (perimeters > 0) { // Simulate single set of perimeters over all merged regions.
Flow extflow = layerm.flow(frExternalPerimeter); float perimeter_offset = 0.f;
Flow flow = layerm.flow(frPerimeter); float perimeter_min_spacing = FLT_MAX;
perimeter_offset = std::max(perimeter_offset, for (size_t region_id = 0; region_id < num_regions; ++ region_id) {
0.5f * float(extflow.scaled_width() + extflow.scaled_spacing()) + (float(perimeters) - 1.f) * flow.scaled_spacing()); const LayerRegion &layerm = *layer.m_regions[region_id];
perimeter_min_spacing = std::min(perimeter_min_spacing, float(std::min(extflow.scaled_spacing(), flow.scaled_spacing()))); // Calculate the maximum perimeter offset as if the slice was extruded with a single extruder only.
} // First find the maxium number of perimeters per region slice.
polygons_append(cache.holes, to_polygons(layerm.fill_expolygons)); unsigned int perimeters = 0;
} for (const Surface &s : layerm.slices.surfaces)
// Save some computing time by reducing the number of polygons. perimeters = std::max<unsigned int>(perimeters, s.extra_perimeters);
cache.top_surfaces = union_(cache.top_surfaces); perimeters += layerm.region().config().wall_loops.value;
cache.bottom_surfaces = union_(cache.bottom_surfaces); // Then calculate the infill offset.
// For a multi-material print, simulate perimeter / infill split as if only a single extruder has been used for the whole print. if (perimeters > 0) {
if (perimeter_offset > 0.) { Flow extflow = layerm.flow(frExternalPerimeter);
// The layer.lslices are forced to merge by expanding them first. Flow flow = layerm.flow(frPerimeter);
polygons_append(cache.holes, offset2(layer.lslices, 0.3f * perimeter_min_spacing, - perimeter_offset - 0.3f * perimeter_min_spacing)); perimeter_offset = std::max(perimeter_offset,
0.5f * float(extflow.scaled_width() + extflow.scaled_spacing()) + (float(perimeters) - 1.f) * flow.scaled_spacing());
perimeter_min_spacing = std::min(perimeter_min_spacing, float(std::min(extflow.scaled_spacing(), flow.scaled_spacing())));
}
polygons_append(cache.holes, to_polygons(layerm.fill_expolygons));
}
// For a multi-material print, simulate perimeter / infill split as if only a single extruder has been used for the whole print.
if (perimeter_offset > 0.) {
// The layer.lslices are forced to merge by expanding them first.
polygons_append(cache.holes, offset2(layer.lslices, 0.3f * perimeter_min_spacing, - perimeter_offset - 0.3f * perimeter_min_spacing));
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING #ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{ {
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.lslices)); Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.lslices));
svg.draw(layer.lslices, "blue"); svg.draw(layer.lslices, "blue");
svg.draw(union_ex(cache.holes), "red"); svg.draw(union_ex(cache.holes), "red");
svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05)); svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05));
svg.Close(); svg.Close();
} }
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
} }
cache.holes = union_(cache.holes); cache.holes = union_(cache.holes);
});
} }
}); });
m_print->throw_if_canceled(); m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - end : cache top / bottom"; BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - end : cache top / bottom";
} }
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) { // With one top/bottom cache for all regions, the shell and hole accumulation in the loop below depends on nothing
// region-specific but the shell settings and the external perimeter spacing, so a region sharing them with an earlier
// one reuses its result instead of repeating it: that accumulation is a union over several layers of top/bottom
// surfaces, and a multi-material print has a region per filament.
using AccumulationKey = std::array<double, 5>;
struct ShellAccumulation
{
AccumulationKey key;
Polygons shell;
Polygons holes;
};
const auto accumulation_key = [](const PrintRegionConfig &region_config, const LayerRegion *layerm) {
return AccumulationKey{ double(region_config.top_shell_layers.value), region_config.top_shell_thickness.value,
double(region_config.bottom_shell_layers.value), region_config.bottom_shell_thickness.value,
double(layerm->flow(frExternalPerimeter).scaled_spacing()) };
};
const auto accumulate_shell = [this, &cache_top_botom_regions](size_t idx_layer, const PrintRegionConfig &region_config,
const LayerRegion *layerm, Polygons &shell, Polygons &holes) {
const Layer *layer = m_layers[idx_layer];
polygons_append(holes, cache_top_botom_regions[idx_layer].holes);
auto combine_holes = [&holes](const Polygons &holes2) {
if (holes.empty() || holes2.empty())
holes.clear();
else
holes = intersection(holes, holes2);
};
auto combine_shells = [&shell](const Polygons &shells2) {
if (shell.empty())
shell = std::move(shells2);
else if (! shells2.empty()) {
polygons_append(shell, shells2);
// Running the union_ using the Clipper library piece by piece is cheaper
// than running the union_ all at once.
shell = union_(shell);
}
};
static constexpr const bool one_more_layer_below_top_bottom_surfaces = false;
if (int n_top_layers = region_config.top_shell_layers.value; n_top_layers > 0) {
// Gather top regions projected to this layer.
coordf_t print_z = layer->print_z;
int i = int(idx_layer) + 1;
int itop = int(idx_layer) + n_top_layers;
bool at_least_one_top_projected = false;
for (; i < int(cache_top_botom_regions.size()) &&
(i < itop || m_layers[i]->print_z - print_z < region_config.top_shell_thickness - EPSILON);
++ i) {
at_least_one_top_projected = true;
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
combine_holes(cache.holes);
combine_shells(cache.top_surfaces);
}
if (!at_least_one_top_projected && i < int(cache_top_botom_regions.size())) {
// Lets consider this a special case - with only 1 top solid and minimal shell thickness settings, the
// boundaries of solid layers are not anchored over/under perimeters, so lets fix it by adding at least one
// perimeter width of area
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].top_surfaces,
layerm->flow(frExternalPerimeter).scaled_spacing()),
to_polygons(m_layers[i]->lslices));
combine_shells(anchor_area);
}
if (one_more_layer_below_top_bottom_surfaces)
if (i < int(cache_top_botom_regions.size()) &&
(i <= itop || m_layers[i]->bottom_z() - print_z < region_config.top_shell_thickness - EPSILON))
combine_holes(cache_top_botom_regions[i].holes);
}
if (int n_bottom_layers = region_config.bottom_shell_layers.value; n_bottom_layers > 0) {
// Gather bottom regions projected to this layer.
coordf_t bottom_z = layer->bottom_z();
int i = int(idx_layer) - 1;
int ibottom = int(idx_layer) - n_bottom_layers;
bool at_least_one_bottom_projected = false;
for (; i >= 0 &&
(i > ibottom || bottom_z - m_layers[i]->bottom_z() < region_config.bottom_shell_thickness - EPSILON);
-- i) {
at_least_one_bottom_projected = true;
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
combine_holes(cache.holes);
combine_shells(cache.bottom_surfaces);
}
if (!at_least_one_bottom_projected && i >= 0) {
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].bottom_surfaces,
layerm->flow(frExternalPerimeter).scaled_spacing()),
to_polygons(m_layers[i]->lslices));
combine_shells(anchor_area);
}
if (one_more_layer_below_top_bottom_surfaces)
if (i >= 0 &&
(i > ibottom || bottom_z - m_layers[i]->print_z < region_config.bottom_shell_thickness - EPSILON))
combine_holes(cache_top_botom_regions[i].holes);
}
};
std::vector<std::vector<ShellAccumulation>> shell_accumulations(top_bottom_surfaces_all_regions ? num_layers : 0);
if (! shell_accumulations.empty()) {
// Every (layer, key) pair is accumulated once, before the regions, so that nothing in the loop below is shared
// between them and they can run next to each other.
std::vector<std::array<size_t, 3>> todo; // layer, its slot, a region holding the key
for (size_t idx_layer = 0; idx_layer < num_layers; ++ idx_layer) {
std::vector<ShellAccumulation> &accumulations = shell_accumulations[idx_layer];
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) {
if (this->printing_region(region_id).config().ensure_vertical_shell_thickness.value != evstAll)
continue;
const LayerRegion *layerm = m_layers[idx_layer]->m_regions[region_id];
const AccumulationKey key = accumulation_key(layerm->region().config(), layerm);
if (std::none_of(accumulations.begin(), accumulations.end(), [&key](const ShellAccumulation &a) { return a.key == key; })) {
todo.push_back({ idx_layer, accumulations.size(), region_id });
accumulations.push_back({ key, {}, {} });
}
}
}
tbb::parallel_for(size_t(0), todo.size(), [this, &todo, &shell_accumulations, &accumulate_shell](size_t i) {
m_print->throw_if_canceled();
const LayerRegion *layerm = m_layers[todo[i][0]]->m_regions[todo[i][2]];
ShellAccumulation &out = shell_accumulations[todo[i][0]][todo[i][1]];
accumulate_shell(todo[i][0], layerm->region().config(), layerm, out.shell, out.holes);
});
m_print->throw_if_canceled();
}
const auto process_region = [&](size_t region_id) {
const PrintRegion &region = this->printing_region(region_id); const PrintRegion &region = this->printing_region(region_id);
if (region.config().ensure_vertical_shell_thickness.value != evstAll ) if (region.config().ensure_vertical_shell_thickness.value != evstAll )
// This region will be handled by discover_horizontal_shells(). // This region will be handled by discover_horizontal_shells().
continue; return;
//FIXME Improve the heuristics for a grain size. //FIXME Improve the heuristics for a grain size.
size_t grain_size = std::max(num_layers / 16, size_t(1)); size_t grain_size = std::max(num_layers / 16, size_t(1));
@@ -2615,7 +2763,7 @@ void PrintObject::discover_vertical_shells()
grain_size = 1; grain_size = 1;
tbb::parallel_for( tbb::parallel_for(
tbb::blocked_range<size_t>(0, num_layers, grain_size), tbb::blocked_range<size_t>(0, num_layers, grain_size),
[this, region_id, &cache_top_botom_regions] [this, region_id, &shell_accumulations, &accumulation_key, &accumulate_shell]
(const tbb::blocked_range<size_t>& range) { (const tbb::blocked_range<size_t>& range) {
// printf("discover_vertical_shells from %d to %d\n", range.begin(), range.end()); // printf("discover_vertical_shells from %d to %d\n", range.begin(), range.end());
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) { for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) {
@@ -2665,80 +2813,19 @@ void PrintObject::discover_vertical_shells()
} }
} }
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
polygons_append(holes, cache_top_botom_regions[idx_layer].holes); const AccumulationKey key = accumulation_key(region_config, layerm);
auto combine_holes = [&holes](const Polygons &holes2) { const ShellAccumulation *reused = shell_accumulations.empty() ? nullptr :
if (holes.empty() || holes2.empty()) [&]() -> const ShellAccumulation * {
holes.clear(); for (const ShellAccumulation &a : shell_accumulations[idx_layer])
else if (a.key == key)
holes = intersection(holes, holes2); return &a;
}; return nullptr;
auto combine_shells = [&shell](const Polygons &shells2) { }();
if (shell.empty()) if (reused != nullptr) {
shell = std::move(shells2); shell = reused->shell;
else if (! shells2.empty()) { holes = reused->holes;
polygons_append(shell, shells2); } else
// Running the union_ using the Clipper library piece by piece is cheaper accumulate_shell(idx_layer, region_config, layerm, shell, holes);
// than running the union_ all at once.
shell = union_(shell);
}
};
static constexpr const bool one_more_layer_below_top_bottom_surfaces = false;
if (int n_top_layers = region_config.top_shell_layers.value; n_top_layers > 0) {
// Gather top regions projected to this layer.
coordf_t print_z = layer->print_z;
int i = int(idx_layer) + 1;
int itop = int(idx_layer) + n_top_layers;
bool at_least_one_top_projected = false;
for (; i < int(cache_top_botom_regions.size()) &&
(i < itop || m_layers[i]->print_z - print_z < region_config.top_shell_thickness - EPSILON);
++ i) {
at_least_one_top_projected = true;
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
combine_holes(cache.holes);
combine_shells(cache.top_surfaces);
}
if (!at_least_one_top_projected && i < int(cache_top_botom_regions.size())) {
// Lets consider this a special case - with only 1 top solid and minimal shell thickness settings, the
// boundaries of solid layers are not anchored over/under perimeters, so lets fix it by adding at least one
// perimeter width of area
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].top_surfaces,
layerm->flow(frExternalPerimeter).scaled_spacing()),
to_polygons(m_layers[i]->lslices));
combine_shells(anchor_area);
}
if (one_more_layer_below_top_bottom_surfaces)
if (i < int(cache_top_botom_regions.size()) &&
(i <= itop || m_layers[i]->bottom_z() - print_z < region_config.top_shell_thickness - EPSILON))
combine_holes(cache_top_botom_regions[i].holes);
}
if (int n_bottom_layers = region_config.bottom_shell_layers.value; n_bottom_layers > 0) {
// Gather bottom regions projected to this layer.
coordf_t bottom_z = layer->bottom_z();
int i = int(idx_layer) - 1;
int ibottom = int(idx_layer) - n_bottom_layers;
bool at_least_one_bottom_projected = false;
for (; i >= 0 &&
(i > ibottom || bottom_z - m_layers[i]->bottom_z() < region_config.bottom_shell_thickness - EPSILON);
-- i) {
at_least_one_bottom_projected = true;
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
combine_holes(cache.holes);
combine_shells(cache.bottom_surfaces);
}
if (!at_least_one_bottom_projected && i >= 0) {
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].bottom_surfaces,
layerm->flow(frExternalPerimeter).scaled_spacing()),
to_polygons(m_layers[i]->lslices));
combine_shells(anchor_area);
}
if (one_more_layer_below_top_bottom_surfaces)
if (i >= 0 &&
(i > ibottom || bottom_z - m_layers[i]->print_z < region_config.bottom_shell_thickness - EPSILON))
combine_holes(cache_top_botom_regions[i].holes);
}
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING #ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{ {
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-before-union-%d.svg", debug_idx), get_extents(shell)); Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-before-union-%d.svg", debug_idx), get_extents(shell));
@@ -2832,11 +2919,8 @@ void PrintObject::discover_vertical_shells()
Polygons object_volume; Polygons object_volume;
Polygons internal_volume; Polygons internal_volume;
{ {
Polygons shrinked_bottom_slice = idx_layer > 0 ? to_polygons(m_layers[idx_layer - 1]->lslices) : Polygons{}; if (idx_layer > 0 && idx_layer + 1 < m_layers.size())
Polygons shrinked_upper_slice = (idx_layer + 1) < m_layers.size() ? object_volume = to_polygons(intersection_ex_by_piece(m_layers[idx_layer - 1]->lslices, to_polygons(m_layers[idx_layer + 1]->lslices)));
to_polygons(m_layers[idx_layer + 1]->lslices) :
Polygons{};
object_volume = intersection(shrinked_bottom_slice, shrinked_upper_slice);
internal_volume = closing(polygonsInternal, SCALED_EPSILON); internal_volume = closing(polygonsInternal, SCALED_EPSILON);
} }
@@ -2847,15 +2931,34 @@ void PrintObject::discover_vertical_shells()
// the in-model condition is there due to small sloping surfaces, e.g. top of the hull of the benchy // the in-model condition is there due to small sloping surfaces, e.g. top of the hull of the benchy
// 2. the area does not fully cover an internal polygon // 2. the area does not fully cover an internal polygon
// This is there mainly for a very thin parts, where the solid layers would be missing if the part area is quite small // This is there mainly for a very thin parts, where the solid layers would be missing if the part area is quite small
// Both tests below compare a small piece against the whole layer. Done literally, that is
// quadratic in the number of pieces, which is what a layer split up by colour painting has,
// so each is restricted to the part of the layer near the piece with an identical result:
// object_volume is clipped to the piece's box, and only the internal polygons whose box meets
// the expanded piece take part in the count, since the others pass through the difference
// unchanged and add the same number to both sides of it.
std::vector<BoundingBox> internal_bboxes;
internal_bboxes.reserve(internal_volume.size());
for (const Polygon &poly : internal_volume)
internal_bboxes.emplace_back(get_extents(poly));
regularized_shell.erase(std::remove_if(regularized_shell.begin(), regularized_shell.end(), regularized_shell.erase(std::remove_if(regularized_shell.begin(), regularized_shell.end(),
[&internal_volume, &min_perimeter_infill_spacing, [&internal_volume, &internal_bboxes, &min_perimeter_infill_spacing,
&object_volume](const ExPolygon &p) { &object_volume](const ExPolygon &p) {
return (p.area() < min_perimeter_infill_spacing * scaled(1.5) || const bool small = p.area() < min_perimeter_infill_spacing * scaled(1.5) ||
(p.area() < min_perimeter_infill_spacing * scaled(8.0) && (p.area() < min_perimeter_infill_spacing * scaled(8.0) &&
diff(to_polygons(p), object_volume).empty())) && diff(to_polygons(p),
diff(internal_volume, ClipperUtils::clip_clipper_polygons_with_subject_bbox(
expand(to_polygons(p), min_perimeter_infill_spacing)) object_volume, get_extents(p).inflated(SCALED_EPSILON)))
.size() >= internal_volume.size(); .empty());
if (!small)
return false;
const Polygons expanded = expand(to_polygons(p), min_perimeter_infill_spacing);
const BoundingBox bbox = get_extents(expanded);
Polygons nearby;
for (size_t i = 0; i < internal_volume.size(); ++i)
if (internal_bboxes[i].overlap(bbox))
nearby.emplace_back(internal_volume[i]);
return diff(nearby, expanded).size() >= nearby.size();
}), }),
regularized_shell.end()); regularized_shell.end());
} }
@@ -2877,8 +2980,9 @@ void PrintObject::discover_vertical_shells()
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
// Trim the internal & internalvoid by the shell. // Trim the internal & internalvoid by the shell.
Slic3r::ExPolygons new_internal = diff_ex(layerm->fill_surfaces.filter_by_type(stInternal), regularized_shell); const Polygons regularized_shell_polygons = to_polygons(regularized_shell);
Slic3r::ExPolygons new_internal_void = diff_ex(layerm->fill_surfaces.filter_by_type(stInternalVoid), regularized_shell); Slic3r::ExPolygons new_internal = diff_ex_by_piece(to_expolygons(layerm->fill_surfaces.filter_by_type(stInternal)), regularized_shell_polygons);
Slic3r::ExPolygons new_internal_void = diff_ex_by_piece(to_expolygons(layerm->fill_surfaces.filter_by_type(stInternalVoid)), regularized_shell_polygons);
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING #ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{ {
@@ -2905,7 +3009,15 @@ void PrintObject::discover_vertical_shells()
layerm->export_region_fill_surfaces_to_svg_debug("3_discover_vertical_shells-final"); layerm->export_region_fill_surfaces_to_svg_debug("3_discover_vertical_shells-final");
} }
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
} // for each region }; // for each region
if (top_bottom_surfaces_all_regions)
// Nothing is shared between the regions, and a layer cut through a fine relief takes far longer than the others,
// so they run next to each other instead of one after another.
tbb::parallel_for(size_t(0), this->num_printing_regions(), process_region);
else
// Here every region fills the one top/bottom cache with its own surfaces first.
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id)
process_region(region_id);
} // void PrintObject::discover_vertical_shells() } // void PrintObject::discover_vertical_shells()
// #define DEBUG_BRIDGE_OVER_INFILL // #define DEBUG_BRIDGE_OVER_INFILL
@@ -3196,7 +3308,7 @@ void PrintObject::bridge_over_infill()
} }
this->m_adaptive_fill_octrees = this->prepare_adaptive_infill_data(surfaces_w_layer); this->m_adaptive_fill_octrees = this->prepare_adaptive_infill_data(surfaces_w_layer);
this->m_tpms_radial_fields = this->prepare_tpms_radial_fields(); this->m_adaptive_infill_fields = this->prepare_adaptive_infill_fields();
std::vector<size_t> layers_to_generate_infill; std::vector<size_t> layers_to_generate_infill;
for (const auto &pair : surfaces_by_layer) { for (const auto &pair : surfaces_by_layer) {
@@ -3426,6 +3538,16 @@ void PrintObject::bridge_over_infill()
vertical_lines[i].b = Point{x, y_max}; vertical_lines[i].b = Point{x, y_max};
} }
// The vertical lines only span the bridged area's x range, so anchors entirely outside it can never be
// hit. Leaving them out gives the same intersections without building a tree over the whole layer's
// boundary for every bridge.
const coord_t scan_x_min = bb_x.min.x();
const coord_t scan_x_max = bb_x.min.x() + coord_t(n_vlines) * scan_spacing;
anchors.erase(std::remove_if(anchors.begin(), anchors.end(),
[scan_x_min, scan_x_max](const Line &l) {
return std::max(l.a.x(), l.b.x()) < scan_x_min || std::min(l.a.x(), l.b.x()) > scan_x_max;
}),
anchors.end());
auto anchors_and_walls_tree = AABBTreeLines::LinesDistancer<Line>{std::move(anchors)}; auto anchors_and_walls_tree = AABBTreeLines::LinesDistancer<Line>{std::move(anchors)};
auto bridged_area_tree = AABBTreeLines::LinesDistancer<Line>{to_lines(bridged_area)}; auto bridged_area_tree = AABBTreeLines::LinesDistancer<Line>{to_lines(bridged_area)};
@@ -3670,26 +3792,58 @@ void PrintObject::bridge_over_infill()
std::vector<CandidateSurface> expanded_surfaces; std::vector<CandidateSurface> expanded_surfaces;
expanded_surfaces.reserve(surfaces_by_layer[lidx].size()); expanded_surfaces.reserve(surfaces_by_layer[lidx].size());
// The expanded fill boundary depends only on the bridging flow, and total_fill_area is not
// modified below, so build it once per spacing rather than once per candidate. A layer split
// into many candidates (e.g. by colour painting) otherwise repeats a layer-wide offset for each.
std::map<coord_t, Polylines> boundary_by_spacing;
// expansion_area is a clean, non-overlapping set, so cutting a bridge out of it only changes the
// polygons near that bridge. The rest are passed through untouched instead of being fed to Clipper
// with the whole layer again for every candidate.
// Not `near`/`far`: the Windows headers still define those as macros, and they expand to
// nothing, which turns the declaration below into an empty one.
const auto split_near = [](const Polygons &polys, const BoundingBox &bbox, Polygons &rest) {
Polygons nearby;
for (const Polygon &p : polys)
(get_extents(p).overlap(bbox) ? nearby : rest).emplace_back(p);
return nearby;
};
for (const CandidateSurface &candidate : surfaces_by_layer[lidx]) { for (const CandidateSurface &candidate : surfaces_by_layer[lidx]) {
const auto &region_config = candidate.region->region().config(); const auto &region_config = candidate.region->region().config();
const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve || const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve ||
region_config.sparse_infill_pattern == ipOctagramSpiral; region_config.sparse_infill_pattern == ipOctagramSpiral;
const Flow &flow = candidate.region->bridging_flow(frSolidInfill, true); const Flow &flow = candidate.region->bridging_flow(frSolidInfill, true);
ExPolygons bridge_components = intersection_ex(expand(candidate.new_polys, flow.scaled_spacing()), deep_infill_area); const Polygons expanded_polys = expand(candidate.new_polys, flow.scaled_spacing());
// deep_infill_area and internal_unsupported_area cover the whole layer; only their part under
// this candidate can change the results, so they are clipped to its box first.
ExPolygons bridge_components;
if (!expanded_polys.empty())
bridge_components = intersection_ex(expanded_polys,
ClipperUtils::clip_clipper_polygons_with_subject_bbox(
deep_infill_area, get_extents(expanded_polys).inflated(SCALED_EPSILON)));
// Orca: Filter whole bridge areas so their holes remain holes. // Orca: Filter whole bridge areas so their holes remain holes.
bridge_components.erase(std::remove_if(bridge_components.begin(), bridge_components.end(), bridge_components.erase(std::remove_if(bridge_components.begin(), bridge_components.end(),
[&internal_unsupported_area](const ExPolygon &component) { [&internal_unsupported_area](const ExPolygon &component) {
return intersection_ex(component, internal_unsupported_area).empty(); return intersection_ex(component, ClipperUtils::clip_clipper_polygons_with_subject_bbox(
internal_unsupported_area,
get_extents(component).inflated(SCALED_EPSILON)))
.empty();
}), }),
bridge_components.end()); bridge_components.end());
Polygons area_to_be_bridge = to_polygons(std::move(bridge_components)); Polygons area_to_be_bridge = to_polygons(std::move(bridge_components));
Polygons limiting_area = union_(area_to_be_bridge, expansion_area);
if (area_to_be_bridge.empty()) if (area_to_be_bridge.empty())
continue; continue;
Polylines boundary_plines = to_polylines(expand(total_fill_area, 1.3f * flow.scaled_spacing())); // Not split like the cut of expansion_area below: the whole limiting area is grown by 30% of the spacing,
// which merges neighbouring polygons, and any of its boundary can anchor the bridge.
Polygons limiting_area = union_(area_to_be_bridge, expansion_area);
auto boundary_it = boundary_by_spacing.find(flow.scaled_spacing());
if (boundary_it == boundary_by_spacing.end())
boundary_it = boundary_by_spacing
.emplace(flow.scaled_spacing(), to_polylines(expand(total_fill_area, 1.3f * flow.scaled_spacing())))
.first;
Polylines boundary_plines = boundary_it->second;
{ {
Polylines limiting_plines = to_polylines(expand(limiting_area, 0.3f * flow.scaled_spacing())); Polylines limiting_plines = to_polylines(expand(limiting_area, 0.3f * flow.scaled_spacing()));
boundary_plines.insert(boundary_plines.end(), limiting_plines.begin(), limiting_plines.end()); boundary_plines.insert(boundary_plines.end(), limiting_plines.begin(), limiting_plines.end());
@@ -3765,9 +3919,12 @@ void PrintObject::bridge_over_infill()
// Check collision with other expanded surfaces // Check collision with other expanded surfaces
{ {
bool reconstruct = false; bool reconstruct = false;
Polygons tmp_expanded_area = expand(bridging_area, 3.0f * flow.scaled_spacing()); Polygons tmp_expanded_area = expand(bridging_area, 3.0 * flow.scaled_spacing());
const BoundingBox tmp_expanded_bbox = get_extents(tmp_expanded_area);
for (const CandidateSurface &s : expanded_surfaces) { for (const CandidateSurface &s : expanded_surfaces) {
if (!intersection(s.new_polys, tmp_expanded_area).empty()) { // Surfaces whose boxes miss each other cannot intersect, which is most pairs on a busy layer.
if (get_extents(s.new_polys).overlap(tmp_expanded_bbox) &&
!intersection(s.new_polys, tmp_expanded_area).empty()) {
bridging_angle = s.bridge_angle; bridging_angle = s.bridge_angle;
reconstruct = true; reconstruct = true;
break; break;
@@ -3791,10 +3948,20 @@ void PrintObject::bridge_over_infill()
bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow, bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow,
bridging_angle, scan_spacing, true)); bridging_angle, scan_spacing, true));
} }
bridging_area = intersection(bridging_area, limiting_area); // Each of these meets one bridge with the whole layer, so the layer side is first cut down to the
bridging_area = intersection(bridging_area, total_fill_area); // bridge's box (and expansion_area split as above); the result is the same.
bridging_area = diff(bridging_area, total_top_area); if (!bridging_area.empty()) {
expansion_area = diff(expansion_area, bridging_area); const BoundingBox bridging_bbox = get_extents(bridging_area).inflated(SCALED_EPSILON);
bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(limiting_area, bridging_bbox));
bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_fill_area, bridging_bbox));
bridging_area = diff(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_top_area, bridging_bbox));
}
if (!bridging_area.empty()) {
Polygons kept;
const Polygons cut = split_near(expansion_area, get_extents(bridging_area).inflated(SCALED_EPSILON), kept);
append(kept, diff(cut, bridging_area));
expansion_area = std::move(kept);
}
#ifdef DEBUG_BRIDGE_OVER_INFILL #ifdef DEBUG_BRIDGE_OVER_INFILL
debug_draw(std::to_string(lidx) + "_" + std::to_string(cluster_idx) + "_" + std::to_string(job_idx) + "_" + "_expanded_bridging" + std::to_string(r), debug_draw(std::to_string(lidx) + "_" + std::to_string(cluster_idx) + "_" + std::to_string(job_idx) + "_" + "_expanded_bridging" + std::to_string(r),
+5 -8
View File
@@ -707,8 +707,10 @@ void tree_supports_generate_paths(
false); false);
} }
if (eec) { if (eec) {
std::reverse(eec->entities.begin(), eec->entities.end()); if (!eec->empty()) {
dst.emplace_back(eec.release()); std::reverse(eec->entities.begin(), eec->entities.end());
dst.emplace_back(eec.release());
}
} }
} }
} }
@@ -1825,12 +1827,7 @@ void generate_support_toolpaths(
no_sort = true; no_sort = true;
} else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic && } else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic &&
(config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) { (config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) {
// Orca: A special case for the hollow Organic supports tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params);
// Orca: If the tree supports are too tall, use a double wall to make it stronger
SupportParameters support_params2 = support_params;
if (support_layer.print_z > 100.0)
support_params2.tree_branch_diameter_double_wall_area_scaled = 0.1;
tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params2);
done = true; done = true;
} }
if (! done) if (! done)
+1 -1
View File
@@ -1004,9 +1004,9 @@ public:
::fread(&y, sizeof(coord_t), 1, file); ::fread(&y, sizeof(coord_t), 1, file);
poly.points.emplace_back(Point(x * scale, y * scale)); poly.points.emplace_back(Point(x * scale, y * scale));
} }
printf("Polygon %d, area: %lf\n", i, area(poly.points));
if (which == -1 || which == i) if (which == -1 || which == i)
m_support_polygons_deserialized.emplace_back(std::move(poly)); m_support_polygons_deserialized.emplace_back(std::move(poly));
printf("Polygon %d, area: %lf\n", i, area(poly.points));
} }
::fread(&n_polygons, 4, 1, file); ::fread(&n_polygons, 4, 1, file);
m_trimming_polygons_deserialized.reserve(n_polygons); m_trimming_polygons_deserialized.reserve(n_polygons);
Loaded 100 of 163 files, more files were not shown because too many files have changed in this diff. Show more