diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index f29d7f0cc6..e49e157358 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -341,12 +341,12 @@ jobs: # Notarize main DMG xcrun notarytool submit "OrcaSlicer_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait - xcrun stapler staple OrcaSlicer_Mac_universal_${{ env.ver }}.dmg + retry xcrun stapler staple OrcaSlicer_Mac_universal_${{ env.ver }}.dmg # Notarize profile validator DMG if it exists if [ -f "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" ]; then xcrun notarytool submit "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait - xcrun stapler staple OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg + retry xcrun stapler staple OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg fi - name: Create DMG without notary @@ -460,10 +460,6 @@ jobs: - name: Build slicer Win if: runner.os == 'Windows' working-directory: ${{ github.workspace }} - # Orca: Removed Netfabb STL fixing service support in favor of CGAL. - # env: - # WindowsSdkDir: 'C:\Program Files (x86)\Windows Kits\10\' - # WindowsSDKVersion: '10.0.26100.0\' # --tests builds the unit tests too; the unit_tests_windows_* jobs run them. run: | # cache-path is the install directory inside the deps build directory. diff --git a/.github/workflows/clang_tidy.yml b/.github/workflows/clang_tidy.yml index 2f27a0eaff..445f294fcc 100644 --- a/.github/workflows/clang_tidy.yml +++ b/.github/workflows/clang_tidy.yml @@ -16,6 +16,15 @@ on: permissions: contents: read + # Listing caches and builds while waiting for a deps cache. + actions: read + +# Read by scripts/ci_deps_cache.sh. +env: + REPO: ${{ github.repository }} + WORKFLOW: build_all.yml + BASE_REF: ${{ github.base_ref }} + JOB: build_linux (${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04' }}) concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} @@ -35,7 +44,7 @@ jobs: # The PR merge commit plus its first parent, the base it is diffed against. fetch-depth: 2 - - name: Look for changed C++ files + - name: Look for changed files id: changes run: | if git -c core.quotePath=false diff --name-only HEAD^1 -- src tests | grep -qE '\.(cpp|cc|cxx|hpp|h|hxx)$'; then @@ -43,18 +52,75 @@ jobs: else echo "No C++ changes under src/ or tests/." fi + if ! git diff --quiet HEAD^1 -- deps; then + echo "deps=true" >> "$GITHUB_OUTPUT" + fi + echo "deps-key=linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}" >> "$GITHUB_OUTPUT" - # Parsing needs the dependency headers, not a build of this PR's deps/, so - # a PR that changes deps/ falls back to the newest cache main has. + # Parsing needs the dependency headers, not a build of this PR's deps/. + # Without an exact match the job waits for the build that saves it: this + # PR's own when it changes deps/, otherwise a running push build of the + # base branch with the same deps. With neither, a PR that keeps deps/ + # falls back to the newest cache. - name: Restore cached deps + id: deps_cache if: steps.changes.outputs.cpp == 'true' uses: actions/cache/restore@v6 with: path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep - key: linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }} + key: ${{ steps.changes.outputs.deps-key }} + + - name: Look for a build to wait for + id: wait_check + if: steps.changes.outputs.cpp == 'true' && steps.deps_cache.outputs.cache-hit != 'true' + env: + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: bash scripts/ci_deps_cache.sh ${{ steps.changes.outputs.deps == 'true' && 'own' || 'check' }} + + # Linux Build Deps takes about 35 minutes on GitHub-hosted runners. The + # wait ends early when the deps job fails. + - name: Wait for the deps cache + if: steps.wait_check.outputs.wait-for != '' + timeout-minutes: 100 + env: + GH_TOKEN: ${{ github.token }} + KEY: ${{ steps.changes.outputs.deps-key }} + CACHE_REF: ${{ steps.changes.outputs.deps == 'true' && github.ref || format('refs/heads/{0}', github.base_ref) }} + KEY_INPUTS: ${{ steps.wait_check.outputs.key-inputs }} + WAIT_FOR: ${{ steps.wait_check.outputs.wait-for }} + WAIT_SINCE: ${{ steps.wait_check.outputs.wait-since }} + WAIT_MINUTES: 90 + run: bash scripts/ci_deps_cache.sh wait + + - name: Restore the deps cache after waiting + id: waited_cache + if: steps.wait_check.outputs.wait-for != '' + uses: actions/cache/restore@v6 + with: + path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep + key: ${{ steps.changes.outputs.deps-key }} + + - name: Fail without this PR's deps cache + if: steps.changes.outputs.deps == 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true' + run: | + echo "::error::No deps cache ${{ steps.changes.outputs.deps-key }}. Check this PR's Linux Build Deps job, then rerun clang-tidy." + exit 1 + + - name: Restore the newest cached deps + id: newest_cache + if: steps.changes.outputs.deps != 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true' + uses: actions/cache/restore@v6 + with: + path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep + key: ${{ steps.changes.outputs.deps-key }} restore-keys: linux-clang-cache-orcaslicer_deps-build- fail-on-cache-miss: true + - name: Warn about older deps + if: steps.newest_cache.outcome == 'success' && steps.newest_cache.outputs.cache-hit != 'true' + run: echo "::warning title=Older deps::No cache matches this PR's deps/, so clang-tidy parsed against ${{ steps.newest_cache.outputs.cache-matched-key }}. If it reports errors in code that uses a dependency, rerun clang-tidy once the base branch's Build all has saved the new deps." + - name: Apt-Install Dependencies if: steps.changes.outputs.cpp == 'true' && !vars.SELF_HOSTED uses: ./.github/actions/apt-install-deps diff --git a/CMakeLists.txt b/CMakeLists.txt index a208c5e7a2..7f1586f222 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -497,47 +497,6 @@ endif() # Once we get rid of Perl and libslic3r is linked statically, we can get rid of -fPIC set(CMAKE_POSITION_INDEPENDENT_CODE ON) -# WIN10SDK_PATH is used to point CMake to the WIN10 SDK installation directory. -# We pick it from environment if it is not defined in another way -# ORCA: Removed Netfabb STL fixing service support in favor of CGAL. -# if(WIN32) -# if(NOT DEFINED WIN10SDK_PATH) -# if(DEFINED ENV{WIN10SDK_PATH}) -# set(WIN10SDK_PATH "$ENV{WIN10SDK_PATH}") -# endif() -# endif() -# if(DEFINED WIN10SDK_PATH) -# #BBS: modify win10sdk_path -# if (EXISTS "${WIN10SDK_PATH}/winrt/windows.graphics.printing3d.h") -# set(WIN10SDK_INCLUDE_PATH "${WIN10SDK_PATH}") -# else() -# message("WIN10SDK_PATH is invalid: ${WIN10SDK_PATH}") -# message("${WIN10SDK_PATH}/winrt/windows.graphics.printing3d.h was not found") -# message("STL fixing by the Netfabb service will not be compiled") -# unset(WIN10SDK_PATH) -# endif() -# else() -# # Try to use the default Windows 10 SDK path. -# if (DEFINED ENV{WindowsSdkDir} AND DEFINED ENV{WindowsSDKVersion}) -# set(WIN10SDK_INCLUDE_PATH "$ENV{WindowsSdkDir}/Include/$ENV{WindowsSDKVersion}") -# else () -# set(WIN10SDK_INCLUDE_PATH "C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0") -# endif () -# if (NOT EXISTS "${WIN10SDK_INCLUDE_PATH}/winrt/windows.graphics.printing3d.h") -# message("${WIN10SDK_INCLUDE_PATH}/winrt/windows.graphics.printing3d.h was not found") -# message("STL fixing by the Netfabb service will not be compiled") -# unset(WIN10SDK_INCLUDE_PATH) -# endif() -# endif() -# if(WIN10SDK_INCLUDE_PATH) -# message("Building with Win10 Netfabb STL fixing service support") -# add_definitions(-DHAS_WIN10SDK) -# include_directories(SYSTEM "${WIN10SDK_INCLUDE_PATH}") -# else() -# message("Building without Win10 Netfabb STL fixing service support") -# endif() -# endif() - if (APPLE) message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}") if (CMAKE_OSX_DEPLOYMENT_TARGET) diff --git a/cmake/modules/ClangClShowIncludes.cmake b/cmake/modules/ClangClShowIncludes.cmake index 91b9a18069..fcc48fe3f7 100644 --- a/cmake/modules/ClangClShowIncludes.cmake +++ b/cmake/modules/ClangClShowIncludes.cmake @@ -1,10 +1,18 @@ # ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style # depfile, so a cache hit writes the object and no depfile, and Ninja then records # no headers for that object. ccache reproduces /showIncludes output on a hit. +# +# Clang also records the modification time of every input in a precompiled header +# and rejects the header once one differs. ccache caches the compile that creates +# the header, so a tree that regenerates cmake_pch.cxx gets back a header stamped +# with the old file's time, and every compile that uses it fails. The build system +# already rebuilds the header when an input changes. This covers the dependencies, +# such as wxWidgets; PrecompiledHeader.cmake does the same for OrcaSlicer's targets. foreach (_lang C CXX) if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes") set(CMAKE_${_lang}_DEPFILE_FORMAT msvc) + list(APPEND CMAKE_${_lang}_COMPILE_OPTIONS_CREATE_PCH -Xclang -fno-pch-timestamp) endif () endforeach () diff --git a/docs/HLSD/mesh-repair.md b/docs/HLSD/mesh-repair.md new file mode 100644 index 0000000000..145c904769 --- /dev/null +++ b/docs/HLSD/mesh-repair.md @@ -0,0 +1,135 @@ +# Mesh repair — High Level Design + +## Purpose and scope + +Mesh repair turns a broken triangle mesh into closed, outward oriented shells that bound a solid, so that the +mesh slices predictably and the other mesh tools (booleans, cut, painting) get valid input. It runs on demand: +the *Fix Model* command of the context menu of an object or part and of the warning icon in the object list, *Fix +All* and *Fix All Plates* in the context menu of a plate, the prompt the Cut tool shows when a cut leaves open edges, +the *Repair and import* choice of texture import, and the `--repair` option of the command line. It is built on CGAL +and is the only repair the application has, on every platform. + +`repair_mesh()` in `src/libslic3r/MeshRepair.cpp` does the work on one `TriangleMesh`, and `ModelVolume::repair()` +applies it to a part: it replaces the mesh only when the repair changed it, and remaps or drops the painting of a +replaced mesh. The GUI side, `fix_model_with_cgal_gui()` in `src/slic3r/Utils/FixModelByCgal.cpp`, runs it on a +worker thread for each volume of an object, or for one volume, behind a progress dialog. `repair_mesh()` reports its +progress between stages through a callback, which cancels the repair by throwing. + +## What the repaired mesh is + +The repair keeps what the slicer would print from the input. The slicer fills a layer where the winding number of +the input surface is not zero (`MeshSlicingParams::SlicingMode::Regular` uses Clipper's nonzero fill rule), so: + +- Shells crossing each other print as their union. The repair keeps them as separate shells, as the parts of an + object are kept, and makes each of them an outward oriented solid. +- A shell inside a solid that is oriented like it adds nothing, and the repair removes it. +- A shell inside a solid that is oriented inward is a cavity, and stays one. +- A whole mesh turned inside out prints as a solid, and the repair turns it outward. + +On top of that, it closes holes and cracks, gives inconsistently oriented triangles the orientation of their +neighbours, drops degenerate, duplicate and stray flat geometry, and removes small self-intersections inside a shell, +such as slivers and folds. Where none of this applies, it reports the mesh as unmodified and the caller keeps the +original mesh, its painting and its object id. + +A volume is repaired in place and is never split into several parts: splitting by connectivity would turn a cavity +into a separate solid part and fill it. Each solid of the result is a shell of its own, so *Split to parts* still +separates them. Open pieces of the input whose borders meet exactly, which are pieces of one surface rather than +solids, come out as the one shell they form. + +## Pipeline + +The input is treated as a triangle soup. Every stage after the first only runs on what the previous ones could +not settle, so a valid mesh passes through quickly. + +### Valid parts first + +Parts of the input that are connected by vertex indices and are already closed, consistently oriented 2-manifolds +skip the soup stages and go straight to the shell stages. Merging their points with coincident points of other +parts would only make non-manifold junctions out of solids that touch, for example parts of an assembly exported +together or voxels written as separate cubes. + +### Soup clean-up + +The rest of the soup has its coincident points merged. Triangles with repeated corners are dropped. Coincident +triangles of opposite orientation are a wall between two solids touching face to face, so they cancel in pairs; +of the remaining copies of a triangle one is kept. A connected region that would vanish entirely through +cancelling is a two-sided export instead (every face written in both orientations), and keeps one copy of each +triangle. + +Open edges then get their vertices welded within a tolerance of 1e-4 of the bounding box diagonal, which closes the +cracks some exporters leave between triangles that should share corners. A vertex is never welded further than a +quarter of its shortest edge, so welding cannot collapse fine detail. A soup that stays mostly open after welding has +cracks wider than that tolerance, and is welded again with ten and then a hundred times the tolerance. + +### Work units + +The soup is cut into units repaired independently and in parallel: its connected parts, except that open parts whose +bounding boxes touch share a unit, so that cracks and T-junctions between separately exported patches still close. + +### Building the mesh + +A unit whose edges are each shared by at most two triangles, running in opposite directions, and whose vertices each +have a single fan of triangles, becomes a `Surface_mesh` directly. Any other unit is oriented first. Triangles are +flipped so that neighbours across an edge shared by two of them agree, each part keeping the orientation the +majority of its area had, which keeps inward oriented cavities inward. Edges where that is impossible or that more +than two triangles share are cut, and each fan of triangles around a vertex gets its own copy of the vertex. If that +still leaves an invalid mesh, CGAL's `orient_polygon_soup()` orients the unit instead, and the parts it flipped +against their majority are reversed back. The linear pass exists because `orient_polygon_soup()` is several times +slower on large soups that need only a few fixes. + +### Closing + +Compatible borders are stitched, parts that can be stitched once reversed are merged, and borders still open are +snapped onto each other within the weld tolerance, which closes T-junctions. Exactly degenerate faces are removed. + +Holes are filled last. Planar border loops that face each other in one plane, one inside the other, bound a single +region, such as a missing annulus at the end of a tube or a cap with features standing on it. Such a group is +triangulated at once with a 2D constrained Delaunay triangulation that fills only the faces at odd nesting depth, +so the openings inside the region stay open. The patch is used only if it closes every loop with the orientation +of the triangles around it. Every other loop is filled on its own with `triangulate_hole()`, which uses a 2D +constrained Delaunay triangulation when the loop is planar. Otherwise it searches the best triangulation among all +triangles over the loop for loops of up to 100 edges, and among the triangles of a 3D Delaunay triangulation for +longer ones, as the full search is cubic in the length of the loop. Beyond 200 edges the full search is not even +used as a fallback, and a fan around the loop's centroid closes a loop nothing else could. + +The closed unit is split into its shells. The largest stays in place and the others are copied out, as converting +the whole mesh again costs seconds on large models. + +### Shells + +The shells of all units, together with the valid parts set aside at the start, are handled independently and in +parallel. Shells enclosing no volume, which hole filling makes out of stray flat patches, are dropped. + +Self-intersections inside a shell are removed locally with `experimental::remove_self_intersections()`, which +remeshes the region around them. It is used only for local defects: a shell whose intersections involve more than +5 % of its faces is left as it is, and a result that changes the enclosed volume by more than 0.1 % is undone. On +heavily overlapping ornamental models the remeshing otherwise cuts parts down to slivers, while on genuine defects +it changes the volume by thousandths of a percent. Overlaps left in place slice correctly. Remeshing can cut +small pieces off a shell, so a shell it changed is split again and its pieces enclosing no volume are dropped. + +Shells crossing others are found through their bounding boxes and an AABB tree per shell, and are made outward +oriented solids. For every other shell the winding number just outside it is summed from the orientation of the +shells containing it, tested from a point on its largest face. The shell bounds the solid only if the winding +number is zero on exactly one of its sides; it is then oriented so that the solid is inside, or dropped otherwise. + +Crossing shells are not united. Slicing unites them anyway, uniting them exactly costs tens of seconds on models +made of many overlapping pieces, and it would fuse parts that *Split to parts* should keep apart. + +## Robustness + +The repair avoids `experimental::autorefine_and_remove_self_intersections()` and corefinement, which can crash on +messy input, and uses only steps whose failures it can detect. A step that fails keeps its input. CGAL precondition +failures surface as `repair_mesh()` returning false with the message as the error. The output is written in float, +as every mesh in the application is. + +The result does not depend on thread scheduling. `libslic3r_cgal` is built with `CGAL_LINKED_WITH_TBB`. Units and +shells are processed in parallel, but each on its own data, and their results are gathered in a fixed order; +sorts compare complete keys, and the detected self-intersections are put in order before they are used. Snapping +stays sequential, as its parallel version gives different results from run to run. Holes are filled with the full +search wherever it is affordable, because the search among Delaunay triangles can choose between equally good fills +differently from run to run, so only non-planar holes of more than 100 edges may still vary. On some inputs +`experimental::remove_self_intersections()` itself places a few vertices differently from run to run. + +## Verification + +`tests/libslic3r/test_mesh_repair.cpp` covers each kind of defect above on small meshes with known volumes. diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index 57860cf67d..b4d3f0f85b 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -4,7 +4,7 @@ msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-10-11 16:21+0800\n" -"PO-Revision-Date: 2026-07-26 11:14-0300\n" +"PO-Revision-Date: 2026-10-11 16:22-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: Portuguese, Brazilian\n" "Language: pt_BR\n" @@ -251,7 +251,6 @@ msgstr "Padrão" msgid "TPU High Flow" msgstr "TPU Alto fluxo" -# AI Translated msgid "E3D High Flow" msgstr "E3D Alto Fluxo" @@ -519,7 +518,6 @@ msgstr "Pintar" msgid "Choose filament" msgstr "Escolher filamento" -# AI Translated msgid "Edge detection" msgstr "Detecção de arestas" @@ -1569,19 +1567,15 @@ msgstr "O traço da forma (%1%) é muito fino (largura mínima é %2% mm)." msgid "Stroke of shape (%1%) contains unsupported: %2%." msgstr "O traço da forma (%1%) contém não suportado: %2%." -# AI Translated msgid "Not valid state please report reproduction steps on github" msgstr "Estado inválido, informe os passos para reproduzir no GitHub" -# AI Translated msgid "No embossed file" msgstr "Nenhum arquivo em relevo" -# AI Translated msgid "Missing svg file in embossed shape" msgstr "Arquivo SVG ausente na forma em relevo" -# AI Translated msgid "Missing data of svg file" msgstr "Dados do arquivo SVG ausentes" @@ -1671,7 +1665,6 @@ msgstr "Espelhar horizontalmente" msgid "Change SVG Type" msgstr "Alterar tipo de SVG" -# AI Translated msgid "SVG shape" msgstr "Forma SVG" @@ -1704,14 +1697,12 @@ msgstr "Sem recurso" msgid "Vertex" msgstr "Vértice" -# AI Translated msgid "Edge" msgstr "Aresta" msgid "Plane" msgstr "Plano" -# AI Translated msgid "Point on edge" msgstr "Ponto na aresta" @@ -1721,7 +1712,6 @@ msgstr "Ponto no círculo" msgid "Point on plane" msgstr "Ponto no plano" -# AI Translated msgid "Center of edge" msgstr "Centro da aresta" @@ -1946,7 +1936,6 @@ msgstr "" "\n" "Abra apenas anexos de projetos em que você confia. Abrir mesmo assim?" -# AI Translated msgid "Open attachment" msgstr "Abrir anexo" @@ -3445,10 +3434,9 @@ msgstr "Verificar o estado dos serviços de sistema atuais" msgid "code" msgstr "código" -# AI Translated #. TRN Short for "description" of a connection error msgid "desc" -msgstr "desc." +msgstr "descr" #. TRN Short for "additional information" about a connection error msgid "info" @@ -4808,7 +4796,7 @@ msgid "" msgstr "" "A torre de purga não funciona quando a Altura de Camada Adaptativa ou a Altura de Camada de Suporte Independente está ativada.\n" "Qual você deseja manter?\n" -"SIM — Manter a Torre de Preparo\n" +"SIM — Manter a Torre de Purga\n" "NÃO — Manter a Altura de Camada Adaptativa e a Altura de Camada de Suporte Independente" msgid "" @@ -4819,7 +4807,7 @@ msgid "" msgstr "" "A torre de purga não funciona quando a Altura de Camada Adaptativa está ativa.\n" "Qual você deseja manter?\n" -"SIM — Manter a Torre de Preparo\n" +"SIM — Manter a Torre de Purga\n" "NÃO — Manter a Altura de Camada Adaptativa" msgid "" @@ -4830,7 +4818,7 @@ msgid "" msgstr "" "A torre de purga não funciona quando a Altura da Camada de Suporte Independente está ativa.\n" "Qual você deseja manter?\n" -"SIM — Manter a Torre de Preparo\n" +"SIM — Manter a Torre de Purga\n" "NÃO — Manter a Altura da Camada de Suporte Independente" msgid "" @@ -5777,7 +5765,7 @@ msgid "Printed up to layer %1%" msgstr "" msgid "Weight" -msgstr "" +msgstr "Peso" msgid "Volume" msgstr "Volume" @@ -5868,7 +5856,6 @@ msgctxt "Axis" msgid "Y" msgstr "Y" -# AI Translated msgctxt "Axis" msgid "Z" msgstr "Z" @@ -8159,7 +8146,7 @@ msgid "Tools" msgstr "Ferramentas" msgid "G-code" -msgstr "" +msgstr "G-code" msgid "How the mode is labeled in the plate's IDEX/IQEX mode selector. Required — a mode with no name cannot be selected." msgstr "" @@ -8461,7 +8448,7 @@ msgid "Enabling traditional timelapse photography may cause surface imperfection msgstr "A habilitação da fotografia tradicional em timelapse pode causar imperfeições na superfície. É recomendado mudar para o modo suave." msgid "Smooth mode for timelapse is enabled, but the prime tower is off, which may cause print defects. Please enable the prime tower, re-slice and print again." -msgstr "O modo suave para timelapse está ativado, mas a torre de purga está desativada, o que pode causar defeitos na impressão. Ative a torre de purga, fatie novamente e imprima de novo." +msgstr "O modo suave para timelapse está ativado, mas a torre de purga está desativada, o que pode causar defeitos na impressão. Ative a torre de purga, refatie e imprima de novo." msgid "Expand sidebar" msgstr "Expandir barra lateral" @@ -9073,7 +9060,7 @@ msgid "Unable to log in to the Flashforge printer." msgstr "Não é possível fazer login na impressora Flashforge." msgid "Is the printer ready? Is the print sheet in place, empty and clean?" -msgstr "A impressora está pronta? O folha de impressão está no lugar, vazia e limpa?" +msgstr "A impressora está pronta? A placa de impressão está no lugar, vazia e limpa?" msgid "Upload and Print" msgstr "Enviar e Imprimir" @@ -9600,7 +9587,7 @@ msgid "Auto slice after changes" msgstr "Fatiar automaticamente após mudanças" msgid "If enabled, OrcaSlicer will re-slice automatically whenever slicing-related settings change." -msgstr "Se ativado, o OrcaSlicer irá re-fatiar automaticamente sempre que as configurações relacionadas ao fatiamento forem alteradas." +msgstr "Se ativado, o OrcaSlicer irá refatiar automaticamente sempre que as configurações relacionadas ao fatiamento forem alteradas." msgid "Delay in seconds before auto slicing starts, allowing multiple edits to be grouped. Use 0 to slice immediately." msgstr "Atraso em segundos antes do início do fatiamento automático, permitindo que várias edições sejam agrupadas. Use 0 para fatiar imediatamente." @@ -10749,15 +10736,12 @@ msgstr "Descrição do erro" msgid "Extra info" msgstr "Informação extra" -# AI Translated msgid "The Filament Track Switch installed on the printer does not match the slicing file. Please re-slice to avoid print quality issues." -msgstr "O Filament Track Switch instalado na impressora não corresponde ao arquivo de fatiamento. Por favor, refaça o fatiamento para evitar problemas de qualidade de impressão." +msgstr "O Filament Track Switch instalado na impressora não corresponde ao arquivo de fatiamento. Por favor, refatie para evitar problemas de qualidade de impressão." -# AI Translated msgid "This print requires a Filament Track Switch. Please install it first." msgstr "Esta impressão requer um Filament Track Switch. Por favor, instale-o primeiro." -# AI Translated msgid "The Filament Track Switch has not been setup. Please setup it first." msgstr "O Filament Track Switch não foi configurado. Por favor, configure-o primeiro." @@ -10798,7 +10782,6 @@ msgstr "O filamento não corresponde ao filamento no espaço do AMS. Atualize o msgid "The selected printer (%s) is incompatible with the print file configuration (%s). Please adjust the printer preset in the prepare page or choose a compatible printer on this page." msgstr "A impressora selecionada (%s) é incompatível com a configuração do arquivo de impressão (%s). Ajuste a predefinição da impressora na página de preparo ou escolha uma impressora compatível nesta página." -# AI Translated #, c-format, boost-format msgid "The selected printer (%s) has an unknown model, so compatibility with the print file configuration (%s) cannot be verified. Please verify the printer preset before sending." msgstr "A impressora selecionada (%s) tem um modelo desconhecido, então a compatibilidade com a configuração do arquivo de impressão (%s) não pode ser verificada. Verifique a predefinição da impressora antes de enviar." @@ -10876,11 +10859,9 @@ msgstr "Limpar" msgid "Preparing print job" msgstr "Preparando trabalho de impressão" -# AI Translated msgid "Failed to upload the file to the printer's storage. Please try again." msgstr "Falha ao enviar o arquivo para o armazenamento da impressora. Tente novamente." -# AI Translated msgid "Send Failed" msgstr "Falha no envio" @@ -10891,11 +10872,9 @@ msgstr "O comprimento do nome excede o limite." msgid "Cost %dg filament and %d changes more than optimal grouping." msgstr "O custo de %dg de filamento e %d muda mais do que o agrupamento ideal." -# AI Translated msgid "Right-Ext" msgstr "Ext-Direita" -# AI Translated msgid "Left-Ext" msgstr "Ext-Esquerda" @@ -10916,7 +10895,7 @@ msgid "Please refresh the nozzle information and try again." msgstr "Por favor, atualize as informações do bico e tente novamente." msgid "Please re-slice to avoid filament waste." -msgstr "Por favor, fatie novamente para evitar desperdício de filamento." +msgstr "Por favor, refatie para evitar desperdício de filamento." msgid "The reported hotend information may be unreliable." msgstr "As informações reportadas sobre o hotend podem não ser confiáveis." @@ -11060,7 +11039,6 @@ msgstr "Não é possível enviar um trabalho de impressão para uma placa vazia. msgid "Storage needs to be inserted to record timelapse." msgstr "O armazenamento precisa estar inserido para gravar o timelapse." -# AI Translated msgid "The selected printer model could not be identified, so compatibility with the print file configuration cannot be verified. Please verify the printer preset before sending." msgstr "O modelo da impressora selecionada não pôde ser identificado, então a compatibilidade com a configuração do arquivo de impressão não pode ser verificada. Verifique a predefinição da impressora antes de enviar." @@ -11128,7 +11106,6 @@ msgstr "A impressora deve estar na mesma LAN do OrcaSlicer." msgid "The printer does not support sending to printer storage." msgstr "A impressora não suporta envio para o armazenamento da impressora." -# AI Translated msgid "Failed to initialize the printer file transfer. Please check the connection and try again." msgstr "Falha ao inicializar a transferência de arquivos da impressora. Verifique a conexão e tente novamente." @@ -11284,13 +11261,13 @@ msgid "A prime tower is required for clumping detection. There may be flaws on t msgstr "Uma torre de purga é necessária para a detecção de aglomeração. Podem ocorrer falhas no modelo sem a torre de purga. Tem certeza de que deseja desativar a torre de purga?" msgid "Enabling both precise Z height and the prime tower may cause slicing errors. Do you still want to enable?" -msgstr "Habilitar a altura Z precisa e a torre de preparação juntas pode causar erros de fatiamento. Deseja habilitar mesmo assim?" +msgstr "Habilitar a altura Z precisa e a torre de purga juntas pode causar erros de fatiamento. Deseja habilitar mesmo assim?" msgid "A prime tower is required for clumping detection. There may be flaws on the model without prime tower. Do you still want to enable clumping detection?" msgstr "Uma torre de purga é necessária para a detecção de aglomeração. Podem ocorrer falhas no modelo sem o Prime Tower. Tem certeza de que deseja ativar a detecção de aglomeração?" msgid "Enabling both precise Z height and the prime tower may cause slicing errors. Do you still want to enable precise Z height?" -msgstr "Habilitar a altura Z precisa e a torre de preparação juntas pode causar erros de fatiamento. Deseja habilitar a altura Z precisa mesmo assim?" +msgstr "Habilitar a altura Z precisa e a torre de purga juntas pode causar erros de fatiamento. Deseja habilitar a altura Z precisa mesmo assim?" msgid "Smooth timelapse needs a prime tower on every layer, which is not compatible with \"No sparse layers\". \"No sparse layers\" has been turned off." msgstr "O timelapse suave precisa de uma torre de purga em todas as camadas, o que não é compatível com \"Sem camadas esparsas\". \"Sem camadas esparsas\" foi desativado." @@ -11404,10 +11381,9 @@ msgstr "Informação adicional:" msgid "vendor" msgstr "fornecedor" -# AI Translated #. TRN Short for "version" msgid "ver" -msgstr "versão" +msgstr "ver" msgid "printer model" msgstr "modelo de impressora" @@ -12526,11 +12502,9 @@ msgstr "[Ação Necessária] " msgid "[Action Required]" msgstr "[Ação Necessária]" -# AI Translated msgid "Printer update service is unavailable." msgstr "O serviço de atualização de impressoras não está disponível." -# AI Translated msgid "Failed to check for new printers." msgstr "Falha ao verificar novas impressoras." @@ -12546,12 +12520,10 @@ msgstr "Mover Vista" msgid "Rotate View" msgstr "Rotacionar Vista" -# AI Translated #, c-format, boost-format msgid "Use %s or %s, or a key that does not type a character." msgstr "Use %s ou %s, ou uma tecla que não digite um caractere." -# AI Translated msgctxt "Keyboard Shortcut" msgid "Key" msgstr "Tecla" @@ -12559,15 +12531,12 @@ msgstr "Tecla" msgid "Left mouse" msgstr "Mouse esquerdo" -# AI Translated msgid "Available anywhere in the window, even while typing in a text field." msgstr "Disponível em qualquer lugar da janela, mesmo ao digitar em um campo de texto." -# AI Translated msgid "Run favorite 1 to 10" msgstr "Executar favorito 1 a 10" -# AI Translated msgid "Pin or unpin the selected action" msgstr "Fixar ou desafixar a ação selecionada" @@ -12575,11 +12544,9 @@ msgctxt "Keyboard Shortcut" msgid "Tab" msgstr "Tab" -# AI Translated msgid "Switch to the next main tab" msgstr "Mudar para a próxima aba principal" -# AI Translated msgid "Available while the 3D view on the Prepare tab has focus." msgstr "Disponível enquanto a vista 3D da aba Preparar estiver em foco." @@ -12610,11 +12577,9 @@ msgstr "Mouse direito" msgid "Zoom View" msgstr "Aproximar Vista" -# AI Translated msgid "Painting" msgstr "Pintura" -# AI Translated msgid "Available while a painting gizmo is open: supports, seam, fuzzy skin or color painting." msgstr "Disponível enquanto um gizmo de pintura estiver aberto: suportes, costura, textura difusa ou pintura de cores." @@ -12627,7 +12592,6 @@ msgstr "Suporte/Pintura em cores: ajustar o raio da caneta" msgid "Objects list" msgstr "Lista de objetos" -# AI Translated msgid "Available while the object list has focus." msgstr "Disponível enquanto a lista de objetos estiver em foco." @@ -12647,43 +12611,35 @@ msgstr "Clique do mouse" msgid "Select the object/part and mouse click to change the name" msgstr "Selecione o objeto/peça e clique do mouse para mudar o nome" -# AI Translated msgid "Available while the 3D view on the Preview tab has focus." msgstr "Disponível enquanto a vista 3D da aba Pré-visualizar estiver em foco." msgid "Move slider 5x faster" -msgstr "Mover o controle deslizante 5 vezes mais rápido" +msgstr "Mover o controle deslizante 5x mais rápido" -# AI Translated msgid "Scroll slider 5x faster" -msgstr "Rolar o controle deslizante 5 vezes mais rápido" +msgstr "Rolar o controle deslizante 5x mais rápido" msgid "Not customizable" msgstr "Não personalizável" -# AI Translated #, c-format, boost-format msgid "%s is assigned to %s. Reassign it to %s?" msgstr "%s está atribuído a %s. Reatribuí-lo a %s?" -# AI Translated #, c-format, boost-format msgid "The default %s is assigned to %s. Reassign it to %s?" msgstr "O %s padrão está atribuído a %s. Reatribuí-lo a %s?" -# AI Translated msgid "Global shortcuts also apply while typing." msgstr "Os atalhos globais também funcionam durante a digitação." -# AI Translated msgid "A key that types a character cannot be a global shortcut." msgstr "Uma tecla que digita um caractere não pode ser um atalho global." -# AI Translated msgid "Esc cancels, Enter confirms." msgstr "Esc cancela, Enter confirma." -# AI Translated #, c-format, boost-format msgid "" "Press the new shortcut for\n" @@ -12692,20 +12648,16 @@ msgstr "" "Pressione o novo atalho para\n" "\"%s\"" -# AI Translated msgid "Unbind" msgstr "Desvincular" -# AI Translated msgid "The system uses this shortcut, so it cannot be assigned." msgstr "O sistema usa este atalho, então ele não pode ser atribuído." -# AI Translated #, c-format, boost-format msgid "Shift and Ctrl with this key belong to %s and cannot be assigned." msgstr "Shift e Ctrl com esta tecla pertencem a %s e não podem ser atribuídos." -# AI Translated #, c-format, boost-format msgid "Already assigned to %s. Press OK to reassign it." msgstr "Já atribuído a %s. Pressione OK para reatribuí-lo." @@ -12785,9 +12737,8 @@ msgstr "Abrir arquivo de certificado CA" msgid "HTTPS CA file is optional. It is only needed if you use HTTPS with a self-signed certificate." msgstr "O arquivo CA HTTPS é opcional. É necessário apenas se você usar HTTPS com um certificado autoassinado." -# AI Translated msgid "This system uses HTTPS certificates from the system Certificate Store or Keychain. To use a custom CA file, import it there." -msgstr "Este sistema usa certificados HTTPS do repositório de certificados do sistema ou do Keychain. Para usar um arquivo CA personalizado, importe-o lá." +msgstr "Este sistema usa certificados HTTPS do Repositório de Certificados ou Chaveiro do sistema. Para usar um arquivo CA personalizado, importe-o lá." msgid "Printer model" msgstr "Modelo de impressora" @@ -12862,18 +12813,15 @@ msgstr "Laser 40W" msgid "Cutting Module" msgstr "Módulo de Corte" -# AI Translated msgid "Active Cutting Module" msgstr "Módulo de Corte Ativo" -# AI Translated msgid "Ultrasonic Cutting Module" msgstr "Módulo de Corte Ultrassônico" msgid "Auto Fire Extinguishing System" msgstr "Sistema Automático de Extinção de Incêndio" -# AI Translated msgid "Filament Track Switch" msgstr "Filament Track Switch" @@ -13018,32 +12966,26 @@ msgstr "Erro de agrupamento: %1% não pode ser colocado no bico direito" msgid "Group error in manual mode. Please check nozzle count or regroup." msgstr "Erro de agrupamento no modo manual. Por favor, verifique o número de bicos ou reagrupe." -# AI Translated #, boost-format msgid "failed to process some intersections (%1%)" msgstr "falha ao processar algumas interseções (%1%)" -# AI Translated #, boost-format msgid "multiple intersections with a perimeter, only one was used (%1%)" msgstr "múltiplas interseções com um perímetro, apenas uma foi usada (%1%)" -# AI Translated #, boost-format msgid "a perimeter is fully inside a modifier, the modifier was not applied to it (%1%)" -msgstr "um perímetro está totalmente dentro de um modificador; o modificador não foi aplicado a ele (%1%)" +msgstr "um perímetro está totalmente dentro de um modificador, o modificador não foi aplicado a ele (%1%)" -# AI Translated #, boost-format msgid "modifier \"%1%\" of \"%2%\" had no effect on the seam (it might not reach the centerline of the printed perimeter)" msgstr "o modificador \"%1%\" de \"%2%\" não teve efeito na costura (talvez não alcance a linha central do perímetro impresso)" -# AI Translated #, boost-format msgid "modifier \"%1%\" of \"%2%\" (%3% in total) had no effect on the seam (it might not reach the centerline of the printed perimeter)" msgstr "o modificador \"%1%\" de \"%2%\" (%3% no total) não teve efeito na costura (talvez não alcance a linha central do perímetro impresso)" -# AI Translated msgid "Seam placement may differ from expected." msgstr "A posição da costura pode diferir do esperado." @@ -13185,7 +13127,7 @@ msgid " is too close to clumping detection area, there may be collisions when pr msgstr " está muito perto da área de detecção de aglomeração, podendo haver colisões durante a impressão." msgid "Prime Tower" -msgstr "Torre de Preparo" +msgstr "Torre de Purga" msgid " is too close to others, and collisions may be caused.\n" msgstr " está muito perto de outros, e colisões podem ocorrer.\n" @@ -13267,7 +13209,7 @@ msgid "The project contains multiple managed belt purge towers. Reload the plate msgstr "" msgid "Enabling both precise Z height and the prime tower may cause slicing errors." -msgstr "Habilitar a altura Z precisa e a torre de preparação juntas pode causar erros de fatiamento." +msgstr "Habilitar a altura Z precisa e a torre de purga juntas pode causar erros de fatiamento." msgid "A prime tower is required for clumping detection; otherwise, there may be flaws on the model." msgstr "Uma torre de purga é necessária para a detecção de aglomeração, podem ocorrer falhas no modelo." @@ -14323,9 +14265,8 @@ msgstr "Combine várias bordas em uma só quando estiverem próximas umas das ou msgid "Brim ears" msgstr "Orelhas da borda" -# AI Translated msgid "Only draw brim over the sharp edges of the model." -msgstr "Desenhe a borda apenas sobre as arestas afiadas do modelo." +msgstr "Desenhe a borda apenas sobre as arestas vivas do modelo." msgid "Brim ear max angle" msgstr "Ângulo máximo da orelha da borda" @@ -14913,19 +14854,15 @@ msgstr "" "\n" "A taxa de fluxo final do objeto é este valor multiplicado pela taxa de fluxo do filamento." -# AI Translated msgid "Enable pressure advance" msgstr "Habilitar Pressure Advance" -# AI Translated msgid "Enable pressure advance, auto calibration result will be overwritten once enabled." msgstr "Habilitar Pressure Advance, o resultado da calibração automática será sobrescrito uma vez habilitado." -# AI Translated msgid "Pressure advance (Klipper) AKA Linear advance factor (Marlin)." -msgstr "Pressure Advance (Klipper), também conhecido como fator de Linear Advance (Marlin)." +msgstr "Pressure Advance (Klipper), também conhecido como Fator de Linear Advance (Marlin)." -# AI Translated msgid "Enable adaptive pressure advance (beta)" msgstr "Habilitar Pressure Advance adaptativo (beta)" @@ -14946,7 +14883,6 @@ msgstr "" "Quando habilitado, o valor do Pressure Advance acima é substituído. No entanto, um valor padrão razoável acima é fortemente recomendado para atuar como um fallback e para quando a ferramenta for trocada.\n" "\n" -# AI Translated msgid "Adaptive pressure advance measurements (beta)" msgstr "Medidas do Pressure Advance adaptativo (beta)" @@ -14975,7 +14911,6 @@ msgstr "" "2. Anote o valor de PA ideal para cada velocidade e aceleração de fluxo volumétrico. Você pode encontrar o número do fluxo selecionando fluxo no menu suspenso do esquema de cores e movendo o controle deslizante horizontal sobre as linhas do padrão de PA. O número deve estar visível na parte inferior da página. O valor de PA ideal deve diminuir quanto maior for o fluxo volumétrico. Se não estiver, confirme se sua extrusora está funcionando corretamente. Quanto mais lento e com menos aceleração você imprimir, maior será o intervalo de valores de PA aceitáveis. Se nenhuma diferença for visível, use o valor de PA do teste mais rápido\n" "3. Insira os tripletos de valores de PA, Fluxo e Acelerações na caixa de texto aqui e salve seu perfil de filamento." -# AI Translated msgid "Enable adaptive pressure advance within features (beta)" msgstr "Habilitar Pressure Advance adaptativo nos recursos (beta)" @@ -14992,9 +14927,8 @@ msgstr "" "\n" "Esta é uma opção experimental, pois se o perfil de PA não for definido com precisão, causará problemas de uniformidade." -# AI Translated msgid "Static pressure advance for bridges" -msgstr "Pressure Advance estático para pontes" +msgstr "Pressure advance estático para pontes" # AI Translated msgid "" @@ -15254,13 +15188,13 @@ msgid "Interface layer pre-extrusion distance" msgstr "Distância de pré-extrusão da camada de interface" msgid "Pre-extrusion distance for prime tower interface layer (where different materials meet)." -msgstr "Distância de pré-extrusão para a camada de interface da torre de preparação (onde diferentes materiais se encontram)." +msgstr "Distância de pré-extrusão para a camada de interface da torre de purga (onde diferentes materiais se encontram)." msgid "Interface layer pre-extrusion length" msgstr "Comprimento de pré-extrusão da camada de interface" msgid "Pre-extrusion length for prime tower interface layer (where different materials meet)." -msgstr "Comprimento de pré-extrusão para a camada de interface da torre de preparação (onde diferentes materiais se encontram)." +msgstr "Comprimento de pré-extrusão para a camada de interface da torre de purga (onde diferentes materiais se encontram)." msgid "Tower ironing area" msgstr "Area de alisamento da torre" @@ -15275,13 +15209,13 @@ msgid "Interface layer purge length" msgstr "Comprimento de purga da camada de interface" msgid "Purge length for prime tower interface layer (where different materials meet)." -msgstr "Comprimento de purga para a camada de interface da torre de preparação (onde diferentes materiais se encontram)." +msgstr "Comprimento de purga para a camada de interface da torre de purga (onde diferentes materiais se encontram)." msgid "Interface layer print temperature" msgstr "Temperatura de impressão da camada de interface" msgid "Print temperature for prime tower interface layer (where different materials meet). If set to -1, use max recommended nozzle temperature." -msgstr "Temperatura de impressão para a camada de interface da torre de preparação (onde diferentes materiais se encontram). Se definida como -1, use a temperatura máxima recomendada do bico." +msgstr "Temperatura de impressão para a camada de interface da torre de purga (onde diferentes materiais se encontram). Se definida como -1, use a temperatura máxima recomendada do bico." msgid "Speed of the last cooling move" msgstr "Velocidade do último movimento de resfriamento" @@ -15534,10 +15468,10 @@ msgid "Linear" msgstr "Linear" msgid "Quadratic" -msgstr "" +msgstr "Quadrático" msgid "Exponential" -msgstr "" +msgstr "Exponencial" msgid "Z-buckling bias optimization (experimental)" msgstr "Otimização de tendência à flambagem em Z (experimental)" @@ -16252,11 +16186,9 @@ msgstr "Preenchimento simétrico no eixo Y" msgid "If the model has two parts that are symmetric about the Y axis, and you want these parts to have symmetric textures, please click this option on one of the parts." msgstr "Se o modelo tiver duas partes simétricas em relação ao eixo Y e você quiser que essas partes tenham texturas simétricas, clique nesta opção em uma das partes." -# AI Translated msgid "Fill pattern tops" msgstr "Preencher os topos do padrão" -# AI Translated msgid "Choose this option if you want to completely fill in the tops of the infill pattern" msgstr "Escolha esta opção se quiser preencher completamente os topos do padrão de preenchimento" @@ -17295,7 +17227,6 @@ msgstr "Bowden" msgid "Hybrid" msgstr "Híbrido" -# AI Translated msgid "Extra High Flow" msgstr "Fluxo Extra Alto" @@ -17708,7 +17639,6 @@ msgstr "Passos de pré-aquecimento" msgid "Insert multiple preheat commands (e.g. M104.1). Only useful for Prusa XL. For other printers, please set it to 1." msgstr "Insire múltiplos comandos de pré-aquecimento (ex.: M104.1). Útil apenas para Prusa XL. Para outras impressoras, defina como 1." -# AI Translated msgid "G-code written at the very top of the output file, before any other content. Useful for adding metadata that printer firmware reads from the first lines of the file (e.g. estimated print time, filament usage). Supports placeholders like {print_time_total_sec}, {print_time_day}, {print_time_hour}, {print_time_minute}, {print_time_sec} and {used_filament_length}." msgstr "G-code escrito no início do arquivo de saída, antes de qualquer outro conteúdo. Útil para adicionar metadados que o firmware da impressora lê nas primeiras linhas do arquivo (ex.: tempo estimado de impressão, uso de filamento). Suporta marcadores como {print_time_total_sec}, {print_time_day}, {print_time_hour}, {print_time_minute}, {print_time_sec} e {used_filament_length}." @@ -17865,7 +17795,6 @@ msgstr "Sem camadas esparsas (beta)" msgid "If enabled, the wipe tower will not be printed on layers with no tool changes. On layers with a tool change, extruder will travel downward to print the wipe tower, so the tower ends up below the model and the toolhead has to reach down to it. Layouts where that would collide with an already printed object are rejected. Has no effect with smooth timelapse or clumping detection, which need a tower on every layer." msgstr "Se ativado, a torre de purga não será impressa nas camadas sem mudanças de ferramenta. Nas camadas com mudança de ferramenta, a extrusora descerá para imprimir a torre de purga, de modo que a torre fica abaixo do modelo e a cabeça da ferramenta precisa descer até ela. Disposições em que isso colidiria com um objeto já impresso são rejeitadas. Não tem efeito com o timelapse suave nem com a detecção de aglomeração no bico, que precisam de uma torre em todas as camadas." -# AI Translated msgid "Combine sparse layers" msgstr "Combinar camadas esparsas" @@ -18662,11 +18591,9 @@ msgstr "Largura da nervura" msgid "Rib width is always less than half the prime tower side length." msgstr "A largura da nervura sempre é menor do que a metade da largura da torre de purga." -# AI Translated msgid "Fillet wall" msgstr "Parede com filete" -# AI Translated msgid "The wall of prime tower will fillet." msgstr "A parede da torre de purga terá um filete." @@ -19246,11 +19173,9 @@ msgstr "Permitir rotações ao arranjar" msgid "If enabled, Arrange will allow rotation when placing objects." msgstr "Se habilitado, o arranjo permitirá rotações ao posicionar objetos." -# AI Translated msgid "Align to Y axis when arranging" msgstr "Alinhar ao eixo Y ao organizar" -# AI Translated msgid "If enabled, Arrange will turn each object so its long side runs along the Y axis before placing it. When not given, it is on for i3 printers and off for the others, as in the GUI." msgstr "Se ativado, Organizar girará cada objeto para que seu lado mais longo fique ao longo do eixo Y antes de colocá-lo. Quando não informado, fica ativado para impressoras i3 e desativado para as demais, como na interface gráfica." @@ -19440,42 +19365,33 @@ msgstr "Fil. usado" msgid "Total length of filament used in the print." msgstr "Comprimento total de filamento usado na impressão." -# AI Translated msgid "Print time (total seconds)" msgstr "Tempo de impressão (total em segundos)" msgid "Total estimated print time in seconds. Replaced with actual value during post-processing." msgstr "Tempo total estimado de impressão em segundos. Substituído pelo valor real durante o pós-processamento." -# AI Translated msgid "Print time (days component)" msgstr "Tempo de impressão (componente de dias)" -# AI Translated msgid "Estimated print time day component (normal mode). Replaced with actual value during post-processing." msgstr "Componente de dias do tempo estimado de impressão (modo normal). Substituído pelo valor real durante o pós-processamento." -# AI Translated msgid "Print time (hours component)" msgstr "Tempo de impressão (componente de horas)" -# AI Translated msgid "Estimated print time hour component (normal mode). Replaced with actual value during post-processing." msgstr "Componente de horas do tempo estimado de impressão (modo normal). Substituído pelo valor real durante o pós-processamento." -# AI Translated msgid "Print time (minutes component)" msgstr "Tempo de impressão (componente de minutos)" -# AI Translated msgid "Estimated print time minute component (normal mode). Replaced with actual value during post-processing." msgstr "Componente de minutos do tempo estimado de impressão (modo normal). Substituído pelo valor real durante o pós-processamento." -# AI Translated msgid "Print time (seconds component)" msgstr "Tempo de impressão (componente de segundos)" -# AI Translated msgid "Estimated print time second component (normal mode). Replaced with actual value during post-processing." msgstr "Componente de segundos do tempo estimado de impressão (modo normal). Substituído pelo valor real durante o pós-processamento." @@ -19935,20 +19851,17 @@ msgstr "material com significativa contração/expansão térmica, como…" msgid "materials with inaccurate filament diameter" msgstr "materiais com diâmetro de filamento impreciso" -# AI Translated #, boost-format msgid "%1% Flow Rate Calibrated" -msgstr "%1% Taxa de fluxo calibrada" +msgstr "%1% Taxa de Fluxo Calibrada" -# AI Translated #, boost-format msgid "%1% Temperature Calibrated" -msgstr "%1% Temperatura calibrada" +msgstr "%1% Temperatura Calibrada" -# AI Translated #, boost-format msgid "%1% Max Vol Speed Calibrated" -msgstr "%1% Vel. vol. máx. calibrada" +msgstr "%1% Vel. Vol. Máx. Calibrada" msgid "We found the best Flow Dynamics Calibration Factor" msgstr "Encontramos o melhor Fator de Calibração de Dinâmica de Fluxo" @@ -20309,7 +20222,6 @@ msgstr "Temp. final: " msgid "Temp step: " msgstr "Passo de Temperatura: " -# AI Translated #. TRN %s is the temperature unit #, c-format, boost-format msgid "Supported range: 170%s - 500%s" @@ -20696,7 +20608,6 @@ msgstr "Calibrar antes da impressão" msgid "Filament Mapping:" msgstr "Mapeamnto de Filamnto:" -# AI Translated #. TRN External spool slot, followed by its filament type #, c-format, boost-format msgid "Ext - %s" @@ -20777,23 +20688,19 @@ msgstr "Testar bing.com:" msgid "Log Info" msgstr "Informações do Registro" -# AI Translated #. TRN %s is the name of the tested website #, c-format, boost-format msgid "test %s start..." msgstr "iniciando teste de %s..." -# AI Translated #, c-format, boost-format msgid "test %s ip resolved = %s" msgstr "teste de %s: IP resolvido = %s" -# AI Translated #, c-format, boost-format msgid "test %s failed" msgstr "teste de %s falhou" -# AI Translated #, c-format, boost-format msgid "test %s ok" msgstr "teste de %s ok" @@ -21338,15 +21245,12 @@ msgstr "Sessão válida não detectada. Prosseguir com o login no 3DPrinterOS?" msgid "Success!" msgstr "Sucesso!" -# AI Translated msgid "Generate API Key" msgstr "Gerar Chave da API" -# AI Translated msgid "API Key created. Go to the physical printer and hit \"authorize\" on the screen, then run \"Test\" again." msgstr "Chave da API criada. Vá até a impressora física e toque em \"authorize\" (autorizar) na tela; depois execute \"Testar\" novamente." -# AI Translated msgid "API Key created." msgstr "Chave da API criada." @@ -21361,10 +21265,10 @@ msgstr "Substituir a aba do dispositivo do BambuLab pela webui do host de impres #, c-format, boost-format msgid "On this system, %s uses HTTPS certificates from the system Certificate Store or Keychain." -msgstr "Neste sistema, %s usa certificados HTTPS da loja de certificados do sistema ou do Keychain." +msgstr "Neste sistema, %s usa certificados HTTPS do Repositório de Certificados ou Chaveiro do sistema." msgid "To use a custom CA file, please import your CA file into Certificate Store / Keychain." -msgstr "Para usar um arquivo CA personalizado, importe seu arquivo CA para a loja de certificados / Keychain." +msgstr "Para usar um arquivo CA personalizado, importe seu arquivo CA para o Repositório de Certificados / Chaveiro." msgid "Login/Test" msgstr "Login/Teste" @@ -21376,9 +21280,8 @@ msgid "Detect Creality K-series printer" msgstr "Detectar impressora Creality da série K" msgid "Click Scan to look for K-series printers on your network." -msgstr "Clique para buscar impressora Creality da série K na sua rede." +msgstr "Clique em Buscar para procurar impressoras Creality da série K na sua rede." -# AI Translated msgid "Scan" msgstr "Buscar" @@ -21388,7 +21291,6 @@ msgstr "Usar Selecionada" msgid "Scanning the LAN for K-series printers... this takes a few seconds." msgstr "Buscando impressoras série K na rede… isso leva alguns segundos." -# AI Translated msgid "(unknown K-series)" msgstr "(série K desconhecida)" @@ -21492,7 +21394,6 @@ msgstr "log(s)" msgid "Choose where to save the exported JSON file" msgstr "Escolha onde salvar o arquivo JSON exportado" -# AI Translated msgid "JSON files (*.json)|*.json" msgstr "Arquivos JSON (*.json)|*.json" @@ -21766,7 +21667,6 @@ msgstr "Nome da tarefa" msgid "Actions" msgstr "Ações" -# AI Translated #. TRN Button that jumps to the page number typed next to it msgid "Go" msgstr "Ir" @@ -21877,7 +21777,6 @@ msgstr " para definir o número de bicos" msgid "Set the physical nozzle count..." msgstr "Definir o número de bicos físicos…" -# AI Translated #, c-format, boost-format msgid "(Std: %d, HF: %d)" msgstr "(Pad.: %d, AF: %d)" @@ -22313,7 +22212,6 @@ msgstr "" msgid "Save preset bundle" msgstr "Salvar pacote de predefinições" -# AI Translated msgid "Orca Preset Bundle (*.orca_bundle)|*.orca_bundle" msgstr "Pacote de predefinições Orca (*.orca_bundle)|*.orca_bundle" @@ -22860,7 +22758,6 @@ msgstr "Referência / Curva (plano / eixo / sist. coord. / hélice / projeção) msgid "Reference plane (offset / tilt / midplane / tangent / two edges / coincident)" msgstr "Plano de referência (deslocamento / inclinado / plano médio / tangente / duas arestas / coincidente)" -# AI Translated msgid "Axis" msgstr "Eixo" @@ -23606,17 +23503,14 @@ msgstr "Arestas" msgid "Size (r/dist)" msgstr "Tamanho (r/dist)" -# AI Translated msgctxt "Axis" msgid "XY" msgstr "XY" -# AI Translated msgctxt "Axis" msgid "XZ" msgstr "XZ" -# AI Translated msgctxt "Axis" msgid "YZ" msgstr "YZ" @@ -24110,9 +24004,8 @@ msgstr "Expressão" msgid "Field" msgstr "Campo" -# AI Translated msgid "Expr" -msgstr "Expr." +msgstr "Expr" # AI Translated msgid "(no bindings)" @@ -25118,11 +25011,9 @@ msgstr "Fixo" msgid "Revolute" msgstr "De revolução" -# AI Translated msgid "Slider" -msgstr "Deslizante" +msgstr "Controle Deslizante" -# AI Translated msgid "Cylindrical" msgstr "Cilíndrico" @@ -25301,89 +25192,68 @@ msgstr "recurso %d" msgid "Deleted %s, the feature this body was made from — Ctrl+Z restores it" msgstr "Recurso excluído: %s, aquele a partir do qual este corpo foi criado — Ctrl+Z o restaura" -# AI Translated msgid "Select the FEATURE that created this body (or use New Design)" msgstr "Selecione o RECURSO que criou este corpo (ou use Novo design)" -# AI Translated msgid "Select a feature in the tree first" -msgstr "Selecione primeiro um recurso na árvore" +msgstr "Selecione um recurso na árvore primeiro" -# AI Translated #, c-format, boost-format msgid "Body %d shown" msgstr "Corpo %d exibido" -# AI Translated #, c-format, boost-format msgid "Body %d hidden" msgstr "Corpo %d oculto" -# AI Translated msgid "Feature shown" msgstr "Recurso exibido" -# AI Translated msgid "Feature hidden" msgstr "Recurso oculto" -# AI Translated msgid "Draw a sketch first, then Constrain" -msgstr "Desenhe primeiro um esboço e depois use Restringir" +msgstr "Desenhe um esboço primeiro e depois use Restringir" -# AI Translated msgid "Select a sketch in the tree first" -msgstr "Selecione primeiro um esboço na árvore" +msgstr "Selecione um esboço na árvore primeiro" -# AI Translated msgid "Selected feature is not a sketch" msgstr "O recurso selecionado não é um esboço" -# AI Translated msgid "Pick 1-2 lines, then a constraint; right-click exits" msgstr "Selecione 1-2 linhas e depois uma restrição; clique com o botão direito para sair" -# AI Translated msgid "Pick 1-2 entities, then a constraint or dimension; right-click exits" msgstr "Selecione 1-2 entidades e depois uma restrição ou cota; clique com o botão direito para sair" -# AI Translated msgid "Pick an entity first" -msgstr "Selecione primeiro uma entidade" +msgstr "Selecione uma entidade primeiro" -# AI Translated msgid "Pick two entities first" -msgstr "Selecione primeiro duas entidades" +msgstr "Selecione duas entidades primeiro" -# AI Translated msgid "Horizontal and Vertical apply to a line" msgstr "Horizontal e Vertical se aplicam a uma linha" -# AI Translated msgid "Angle applies between two lines" msgstr "Ângulo se aplica entre duas linhas" -# AI Translated msgid "Collinear needs two lines" msgstr "Colinear precisa de duas linhas" -# AI Translated msgid "Parallel, perpendicular and equal length apply to two lines" msgstr "Paralelo, perpendicular e comprimento igual se aplicam a duas linhas" -# AI Translated msgid "Equal radius needs two circles or arcs" -msgstr "Raio igual precisa de dois círculos ou arcos" +msgstr "Mesmo raio precisa de dois círculos ou arcos" -# AI Translated msgid "Concentric needs two circles or arcs" msgstr "Concêntrico precisa de dois círculos ou arcos" -# AI Translated msgid "Tangent needs a line and a circle/arc, or two circles/arcs" msgstr "Tangente precisa de uma linha e um círculo/arco, ou de dois círculos/arcos" -# AI Translated msgid "This constraint needs two entities with a point to join" msgstr "Esta restrição precisa de duas entidades com um ponto para unir" @@ -25391,37 +25261,29 @@ msgstr "Esta restrição precisa de duas entidades com um ponto para unir" msgid "This dimension needs two entities with a point to measure between" msgstr "Esta cota precisa de duas entidades com pontos entre os quais medir" -# AI Translated msgid "Midpoint needs a point and a line" msgstr "Ponto médio precisa de um ponto e uma linha" -# AI Translated msgid "Symmetric needs two points or two lines + an axis" msgstr "Simétrico precisa de dois pontos ou duas linhas + um eixo" -# AI Translated msgid "Symmetric needs two points or two lines" msgstr "Simétrico precisa de dois pontos ou duas linhas" -# AI Translated msgid "Symmetric: pick two entities, then an axis line" msgstr "Simétrico: selecione duas entidades e depois uma linha de eixo" -# AI Translated msgid "Radius/Diameter needs a circle or arc" msgstr "Raio/Diâmetro precisa de um círculo ou arco" -# AI Translated msgid "Unsupported constraint" msgstr "Restrição não suportada" -# AI Translated msgid "Constraint rejected (over-constrained)" -msgstr "Restrição rejeitada (esboço superrestrito)" +msgstr "Restrição rejeitada (restrito demais)" -# AI Translated msgid "Applied constraint · its badge is on the sketch — click the badge to remove it" -msgstr "Restrição aplicada · o ícone dela está no esboço — clique no ícone para removê-la" +msgstr "Restrição aplicada · o distintivo dela está no esboço — clique no distintivo para removê-la" # AI Translated #, c-format, boost-format @@ -25430,26 +25292,21 @@ msgid_plural "%s — %d selected. Press Esc for the Select tool, click one lin msgstr[0] "%s — %d selecionado. Pressione Esc para a ferramenta Selecionar, clique em uma linha e depois Shift+clique ou Ctrl+clique na outra." msgstr[1] "%s — %d selecionados. Pressione Esc para a ferramenta Selecionar, clique em uma linha e depois Shift+clique ou Ctrl+clique na outra." -# AI Translated msgid "Applied constraint" msgstr "Restrição aplicada" -# AI Translated #. TRN Design sketch: abbreviation of "center", appended to a point tag such as "L3·Ctr" msgid "·Ctr" -msgstr "·Ctr" +msgstr "·Cntr" -# AI Translated #, c-format, boost-format msgid "Fix %s" msgstr "Fixar %s" -# AI Translated #, c-format, boost-format msgid "Lock X %s" msgstr "Travar X %s" -# AI Translated #, c-format, boost-format msgid "Lock Y %s" msgstr "Travar Y %s" @@ -25458,71 +25315,55 @@ msgctxt "Sketch Constraint" msgid "Equal" msgstr "Igual" -# AI Translated #, c-format, boost-format msgid "Symmetric %s — %s / %s" msgstr "Simétrico %s — %s / %s" -# AI Translated #, c-format, boost-format msgid "Symmetric about Y axis %s — %s" msgstr "Simétrico em relação ao eixo Y %s — %s" -# AI Translated #, c-format, boost-format msgid "Symmetric about X axis %s — %s" msgstr "Simétrico em relação ao eixo X %s — %s" -# AI Translated msgid "On line" msgstr "Na linha" -# AI Translated msgid "On edge" msgstr "Na aresta" -# AI Translated msgid "Constraint" msgstr "Restrição" -# AI Translated #, c-format, boost-format msgid "Constraints (%d)" msgstr "Restrições (%d)" -# AI Translated msgid "No constraints yet" msgstr "Nenhuma restrição ainda" -# AI Translated msgid "Delete constraint" msgstr "Excluir restrição" -# AI Translated msgid "Constraint deleted" msgstr "Restrição excluída" -# AI Translated msgid "Press Constrain on a sketch first" msgstr "Pressione Restringir em um esboço primeiro" -# AI Translated msgid "Pick the entity, then a mirror-axis line" msgstr "Selecione a entidade e depois uma linha de eixo de espelhamento" -# AI Translated msgid "Mirror axis must be a line" msgstr "O eixo de espelhamento deve ser uma linha" -# AI Translated msgid "Mirror produced nothing" msgstr "O espelhamento não produziu nada" -# AI Translated msgid "Offset distance (+left / -right of direction)" msgstr "Distância de deslocamento (+esquerda / -direita da direção)" -# AI Translated msgid "Offset collapsed the entity" msgstr "O deslocamento colapsou a entidade" @@ -25542,19 +25383,15 @@ msgstr "Raio do filete" msgid "Fillet failed (parallel lines or radius too large)" msgstr "Falha no filete (linhas paralelas ou raio grande demais)" -# AI Translated msgid "Pick two lines to chamfer" msgstr "Selecione duas linhas para o chanfro" -# AI Translated msgid "Chamfer needs two lines" msgstr "O chanfro precisa de duas linhas" -# AI Translated msgid "Chamfer distance" msgstr "Distância do chanfro" -# AI Translated msgid "Chamfer failed (parallel lines or distance too large)" msgstr "Falha no chanfro (linhas paralelas ou distância grande demais)" @@ -26048,30 +25885,24 @@ msgctxt "Design" msgid "Loft" msgstr "Loft" -# AI Translated msgctxt "Design" msgid "Thicken" msgstr "Espessar" -# AI Translated msgctxt "Design" msgid "Join" msgstr "Unir" -# AI Translated msgid "Fuse the tool body into the target — one solid, no seam" -msgstr "Fundir o corpo da ferramenta ao alvo — um único sólido, sem emenda" +msgstr "Fundir o corpo da ferramenta ao alvo — um único sólido, sem costura" -# AI Translated msgctxt "Design" msgid "Subtract" msgstr "Subtrair" -# AI Translated msgid "Cut the tool body out of the target" msgstr "Recortar o corpo da ferramenta do alvo" -# AI Translated msgctxt "Design" msgid "Intersect" msgstr "Intersectar" @@ -26802,10 +26633,10 @@ msgid "Horizontal slider - Move active thumb Right" msgstr "Controle deslizante horizontal - Mover o polegar ativo para a direita" msgid "Horizontal slider - Move to start position" -msgstr "Barra deslizante horizontal — Mover para a posição inicial" +msgstr "Controle deslizante horizontal — Mover para a posição inicial" msgid "Horizontal slider - Move to last position" -msgstr "Barra deslizante horizontal — Mover para a última posição" +msgstr "Controle deslizante horizontal — Mover para a última posição" msgid "Camera view - Default" msgstr "Vista da câmera - Padrão" @@ -28020,19 +27851,15 @@ msgstr "*Dicas: se você mudou a posição do carretel na máquina, feche esta p msgid "Summary: This is currently the most suitable position for placement." msgstr "Resumo: esta é atualmente a posição mais adequada para o posicionamento." -# AI Translated msgid "?" msgstr "?" -# AI Translated msgid "Wiki->" msgstr "Wiki->" -# AI Translated msgid "Note:" -msgstr "Observação:" +msgstr "Nota:" -# AI Translated msgctxt "Texture Displacement" msgid "Bake" msgstr "Assar" @@ -28051,11 +27878,11 @@ msgstr "Pintar deslocamento de textura" # AI Translated msgid "Mark texture seam" -msgstr "Marcar emenda da textura" +msgstr "Marcar costura da textura" # AI Translated msgid "Mark texture seam path" -msgstr "Marcar caminho de emenda da textura" +msgstr "Marcar caminho de costura da textura" # AI Translated #, boost-format @@ -28070,7 +27897,6 @@ msgstr "Escala média das ilhas" msgid "Select an island in the UV editor first." msgstr "Selecione primeiro uma ilha no editor UV." -# AI Translated msgid "Cut texture island" msgstr "Cortar ilha da textura" @@ -28078,31 +27904,24 @@ msgstr "Cortar ilha da textura" msgid "This island has no neighbour it shares an edge with." msgstr "Esta ilha não tem nenhuma vizinha com a qual compartilhe uma aresta." -# AI Translated msgid "Join texture island" msgstr "Unir ilha da textura" -# AI Translated msgid "Unjoin texture island" msgstr "Desunir ilha da textura" -# AI Translated msgid "Clear texture seams" -msgstr "Limpar emendas da textura" +msgstr "Limpar costuras da textura" -# AI Translated msgid "Clear texture UV edits" msgstr "Limpar edições UV da textura" -# AI Translated msgid "Apply texture projection frame" msgstr "Aplicar moldura de projeção da textura" -# AI Translated msgid "Move texture island" msgstr "Mover ilha da textura" -# AI Translated msgid "Edit texture UV" msgstr "Editar UV da textura" @@ -28130,29 +27949,23 @@ msgstr "Imagens (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp" msgid "Choose a texture image (height map)" msgstr "Escolha uma imagem de textura (mapa de altura)" -# AI Translated msgid "Choose a texture" msgstr "Escolha uma textura" -# AI Translated msgid "Remove from My textures" msgstr "Remover de Minhas texturas" -# AI Translated msgid "Built-in" msgstr "Integradas" -# AI Translated msgid "No textures found." msgstr "Nenhuma textura encontrada." -# AI Translated msgid "My textures" msgstr "Minhas texturas" -# 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." -msgstr "Importe sua própria imagem como mapa de altura. Ela é convertida para o formato a partir do qual o fatiador assa e salva na sua pasta pessoal de texturas, separada das texturas fornecidas com o OrcaSlicer." +msgstr "Importe sua própria imagem como mapa de altura. Ela é convertida para o formato a partir do qual o fatiador assa e salva na sua pasta pessoal de texturas, mantida separada das texturas fornecidas com o OrcaSlicer." # AI Translated msgid "PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it." @@ -28213,111 +28026,84 @@ msgstr "Pinte primeiro a área que deseja suavizar - a suavização só afeta a msgid "Nothing to smooth - the painted area is only one triangle deep, so with \"Ignore outer ring\" on there are no interior vertices to relax." msgstr "Nada a suavizar - a área pintada tem apenas um triângulo de profundidade, então, com \"Ignorar anel externo\" ativado, não há vértices internos para relaxar." -# AI Translated msgid "Smooth texture displacement" msgstr "Suavizar deslocamento de textura" -# AI Translated msgid "Remesh model for texture displacement" msgstr "Remalhar modelo para deslocamento de textura" -# AI Translated msgid "Remeshing did not change the model. It may be non-manifold (open edges or edges shared by more than two triangles), or the target edge length may already be met." msgstr "O remalhamento não alterou o modelo. Ele pode ser não-manifold (arestas abertas ou arestas compartilhadas por mais de dois triângulos), ou o comprimento de aresta alvo pode já ter sido atingido." -# AI Translated msgid "Nothing is painted, there is nothing to bake." msgstr "Nada está pintado, não há nada para assar." -# AI Translated msgid "Bake texture displacement" msgstr "Assar deslocamento de textura" -# AI Translated msgid "Nothing is painted, so there are no bake stages to capture." msgstr "Nada está pintado, então não há etapas de assamento para capturar." -# AI Translated msgid "Bake stages (debug)" msgstr "Etapas do assamento (depuração)" -# AI Translated msgid "Capturing..." msgstr "Capturando..." -# AI Translated msgid "Capture stages" msgstr "Capturar etapas" -# AI Translated msgid "Run the same bake the Bake button runs, keeping the mesh after every stage, and step through them here. Nothing is committed to the model - this is for finding which stage a bad result came from." msgstr "Executa o mesmo assamento que o botão Assar executa, mantendo a malha após cada etapa, e permite percorrê-las aqui. Nada é aplicado ao modelo - serve para descobrir de qual etapa veio um resultado ruim." -# AI Translated msgid "Check topology" msgstr "Verificar topologia" -# AI Translated msgid "Count open and non-manifold edges after each stage, which is how a stage that tore the mesh is spotted. It scans every edge, so it adds noticeably to the capture on a fine bake." msgstr "Conta as arestas abertas e não-manifold após cada etapa, que é como se identifica uma etapa que rasgou a malha. Analisa todas as arestas, então aumenta consideravelmente a captura em um assamento fino." -# AI Translated msgid "< Previous" msgstr "< Anterior" -# AI Translated msgid "Next >" msgstr "Próximo >" -# AI Translated msgid "triangles" msgstr "triângulos" -# AI Translated msgid "vertices" msgstr "vértices" -# AI Translated msgid "Open edges" msgstr "Arestas abertas" -# AI Translated msgid "Non-manifold" msgstr "Não-manifold" -# AI Translated msgid "Degenerate" msgstr "Degenerados" -# AI Translated msgid "Write stage meshes" msgstr "Gravar malhas das etapas" -# AI Translated msgid "Wrote the stage meshes as OBJ files to:" msgstr "As malhas das etapas foram gravadas como arquivos OBJ em:" -# AI Translated msgid "Could not write the stage meshes." msgstr "Não foi possível gravar as malhas das etapas." -# AI Translated msgid "Write every captured stage out as an OBJ file, so they can be opened side by side in a mesh viewer." msgstr "Grava cada etapa capturada como um arquivo OBJ, para que possam ser abertas lado a lado em um visualizador de malhas." -# AI Translated msgid "The painted area could not be carried onto the remeshed model, so nothing was changed. Switch to Pro mode and remesh before painting." msgstr "A área pintada não pôde ser transferida para o modelo remalhado, então nada foi alterado. Mude para o modo Pro e remalhe antes de pintar." -# AI Translated msgid "Pro" msgstr "Pro" -# AI Translated msgid "Standard - pick a texture, paint where it goes, press Bake. Everything the mesh needs is done for you in that one step." msgstr "Padrão - escolha uma textura, pinte onde ela vai e pressione Assar. Tudo o que a malha precisa é feito por você nessa única etapa." -# AI Translated msgid "Pro - the mesh preparation is yours to run: Remesh, Subdivide and Bake as separate steps, with every setting on show. For when Standard's result is not what you wanted and you know why." msgstr "Pro - a preparação da malha fica por sua conta: Remalhar, Subdividir e Assar como etapas separadas, com todas as configurações à mostra. Para quando o resultado do Padrão não é o que você queria e você sabe por quê." @@ -28358,7 +28144,6 @@ msgstr "Pincel - pinte sobre a superfície arrastando" msgid "Face - click individual triangles" msgstr "Face - clique em triângulos individuais" -# AI Translated msgid "Connected area" msgstr "Área conectada" @@ -28366,7 +28151,6 @@ msgstr "Área conectada" msgid "Connected area - flood-fill the region reachable without crossing an edge sharper than the angle threshold" msgstr "Área conectada - preenche a região alcançável sem cruzar uma aresta mais acentuada que o limiar de ângulo" -# AI Translated msgid "Wait for the bake to finish." msgstr "Aguarde o assamento terminar." @@ -28410,14 +28194,12 @@ msgstr "Esfera - pinta apenas dentro de uma esfera ao redor do ponto sob o curso msgid "Angle threshold - the fill stops at edges sharper than this" msgstr "Limiar de ângulo - o preenchimento para em arestas mais acentuadas que isto" -# AI Translated msgid "Click a triangle to paint it" msgstr "Clique em um triângulo para pintá-lo" msgid "Only for a layer mapped with Unwrap (LSCM) - set the active layer's Mapping to Unwrap to use this view." msgstr "" -# AI Translated msgid "Normal - the real geometry, exactly what Bake will produce. Slower to update" msgstr "Normal - a geometria real, exatamente o que Assar vai produzir. Atualiza mais devagar" @@ -28425,14 +28207,12 @@ msgstr "Normal - a geometria real, exatamente o que Assar vai produzir. Atualiza msgid "Fast - the relief is only shaded on, not built, and only for the layer you are editing. Updates instantly while you paint" msgstr "Rápido - o relevo é apenas sombreado, não construído, e só para a camada que você está editando. Atualiza instantaneamente enquanto você pinta" -# AI Translated msgid "Checker" msgstr "Xadrez" msgid "Checker - a test grid instead of the texture. Where the squares stay square the texture is undistorted; where they stretch, it will too. Opens the UV editor if it is closed" msgstr "" -# AI Translated msgid "Distortion" msgstr "Distorção" @@ -28447,7 +28227,6 @@ msgstr "Malha - sobrepõe as arestas da malha; independente da vista acima" msgid "Rebuilds the preview as soon as anything changes. Turn it off on a heavy model if painting or dragging a slider starts to stutter - the preview then waits until you let go." msgstr "Reconstrói a pré-visualização assim que algo muda. Desative em um modelo pesado se pintar ou arrastar um controle deslizante começar a travar - a pré-visualização então espera até você soltar." -# AI Translated msgid "Texture layers" msgstr "Camadas de textura" @@ -28559,37 +28338,31 @@ msgstr "" msgid "The most mixed filaments a bake may add. They are picked from the texture's colors, and only the ones the bake actually uses are created." msgstr "" -# AI Translated #, boost-format msgid "%1% printable colors from %2% filaments" msgstr "%1% cores imprimíveis a partir de %2% filamentos" -# AI Translated msgid "Denoise" -msgstr "Reduzir ruído" +msgstr "Reduzir Ruído" # AI Translated msgid "Cleans up single stray triangles of the wrong color, which detail finer than the mesh leaves behind. Raise it if the result looks speckled, lower it if small features are being swallowed." msgstr "Remove triângulos isolados com a cor errada, deixados por detalhes mais finos que a malha. Aumente se o resultado parecer salpicado; diminua se pequenos detalhes estiverem sendo engolidos." -# AI Translated msgctxt "Texture Displacement" msgid "Mapping" msgstr "Mapeamento" -# AI Translated msgid "Triplanar (blended)" msgstr "Triplanar (mesclado)" -# AI Translated msgid "Triplanar - projects the texture from all three axes at once and blends between them, so a patch wrapping around a sharp edge has no seam." -msgstr "Triplanar - projeta a textura a partir dos três eixos ao mesmo tempo e mescla entre eles, de modo que uma área que envolve uma aresta viva não tenha emenda." +msgstr "Triplanar - projeta a textura a partir dos três eixos ao mesmo tempo e mescla entre eles, de modo que uma área que envolve uma aresta viva não tenha costura." # AI Translated msgid "Cylindrical - wraps the texture around the painted area's own center, for round shapes." msgstr "Cilíndrico - envolve a textura ao redor do próprio centro da área pintada, para formas redondas." -# AI Translated msgid "Spherical" msgstr "Esférico" @@ -28597,15 +28370,12 @@ msgstr "Esférico" msgid "Spherical - wraps the texture around the painted area's own center in both directions." msgstr "Esférico - envolve a textura ao redor do próprio centro da área pintada, nas duas direções." -# AI Translated msgid "Unwrap (LSCM)" msgstr "Desdobrar (LSCM)" -# AI Translated msgid "Unwrap - flattens the painted area and maps the texture onto it with as little stretching as possible. The area is cut into pieces at its sharp edges first (see Seam angle), so each piece can lie flat on its own." -msgstr "Desdobrar - achata a área pintada e mapeia a textura sobre ela com o mínimo de estiramento possível. Primeiro a área é cortada em pedaços nas suas arestas vivas (veja Ângulo de emenda), para que cada pedaço possa ficar plano por conta própria." +msgstr "Desdobrar - achata a área pintada e mapeia a textura sobre ela com o mínimo de estiramento possível. Primeiro a área é cortada em pedaços nas suas arestas vivas (veja Ângulo de costura), para que cada pedaço possa ficar plano por conta própria." -# AI Translated msgid "From view" msgstr "Da vista" @@ -28613,13 +28383,12 @@ msgstr "Da vista" msgid "From view - projects straight onto the painted area from where you are looking, like a slide projector." msgstr "Da vista - projeta diretamente sobre a área pintada a partir de onde você está olhando, como um projetor de slides." -# AI Translated msgid "Open UV editor" msgstr "Abrir editor UV" # AI Translated msgid "Opens the UV editor, where you can see how the texture is laid out over the painted area, cut seams and move the pieces around." -msgstr "Abre o editor UV, onde você vê como a textura está distribuída sobre a área pintada, corta emendas e move os pedaços." +msgstr "Abre o editor UV, onde você vê como a textura está distribuída sobre a área pintada, corta costuras e move os pedaços." # AI Translated msgid "Not unwrapped yet" @@ -28786,9 +28555,8 @@ msgstr "Seguir detalhes da textura" msgid "Spends the triangles where the texture actually bends - packed along ridges and edges, sparse over flat ground - instead of spreading them evenly. The same detail for fewer triangles on most textures." msgstr "Gasta os triângulos onde a textura realmente se curva - concentrados ao longo de cristas e arestas, esparsos em áreas planas - em vez de distribuí-los uniformemente. O mesmo detalhe com menos triângulos na maioria das texturas." -# AI Translated msgid "Max edge" -msgstr "Aresta máx." +msgstr "Aresta máx" # AI Translated msgid "Target edge" @@ -28810,9 +28578,8 @@ msgstr "Detalhe" msgid "How far the mesh may sit from the shape the texture describes, in millimetres. Smaller follows fine detail and costs triangles; larger only chases the big features." msgstr "Quanto a malha pode se afastar da forma descrita pela textura, em milímetros. Menor segue detalhes finos e custa triângulos; maior só acompanha as formas grandes." -# AI Translated msgid "Min edge" -msgstr "Aresta mín." +msgstr "Aresta mín" # AI Translated msgid "A floor on triangle size, so a sharp step in the texture cannot be chased forever. Lower it for finer relief, raise it if the count runs away at hard edges." @@ -28863,19 +28630,15 @@ msgstr "Pré-visualizar subdivisão" msgid "Shows what the refinement would produce as a wireframe, without touching the model." msgstr "Mostra o que o refinamento produziria como malha, sem alterar o modelo." -# AI Translated msgid "Subdivide" msgstr "Subdividir" -# AI Translated msgid "Refines the painted area to the target edge length and carries your paint onto the finer mesh. The rest of the model is left as it is." msgstr "Refina a área pintada até o comprimento de aresta alvo e transfere sua pintura para a malha mais fina. O restante do modelo fica como está." -# AI Translated msgid "Replaces the model's geometry with the subdivided mesh and clears any not-yet-baked paint on it (already-baked relief is unaffected)." msgstr "Substitui a geometria do modelo pela malha subdividida e limpa qualquer pintura ainda não assada nela (o relevo já assado não é afetado)." -# AI Translated msgid "Remeshing" msgstr "Remalhamento" @@ -28883,23 +28646,18 @@ msgstr "Remalhamento" msgid "Triangle size the whole model is rebuilt with, in millimetres. Displacement works best on an even mesh; this is what makes one out of an uneven import." msgstr "Tamanho dos triângulos com que o modelo inteiro é reconstruído, em milímetros. O deslocamento funciona melhor em uma malha uniforme; isto cria uma a partir de uma importação irregular." -# AI Translated msgid "Keep sharp edges" msgstr "Manter arestas vivas" -# AI Translated msgid "Holds hard edges and open borders in place while the rest is remeshed. Without it the remesher slides vertices along the surface and rounds every crisp edge off - a cube comes back with wobbly edges." -msgstr "Mantém arestas vivas e bordas abertas no lugar enquanto o restante é remalhado. Sem isso, o remalhador desliza os vértices ao longo da superfície e arredonda toda aresta nítida - um cubo volta com arestas onduladas." +msgstr "Mantém arestas vivas e bordas abertas fixas enquanto o restante remalhado. Sem isso, o remalhador desliza os vértices ao longo da superfície e arredonda todas as arestas nítidas - um cubo voltaria com arestas onduladas." -# AI Translated msgid "How sharp a fold has to be, in degrees, before the remesher treats it as an edge worth keeping. Lower protects more of the model's shape; higher rebuilds more of it evenly." -msgstr "Quão acentuada uma dobra precisa ser, em graus, para que o remalhador a trate como uma aresta a ser mantida. Menor protege mais da forma do modelo; maior reconstrói mais dele de forma uniforme." +msgstr "Quão acentuada uma dobra precisa ser, em graus, para que o remalhador a trate como uma aresta a ser mantida. Menor protege mais da forma do modelo; maior reconstrói mais dele uniformemente." -# AI Translated msgid "Remesh" msgstr "Remalhar" -# AI Translated msgid "Rebuilds the whole model with triangles close to this edge length - splitting the big ones and merging the small ones - so displacement has an even density to work with. Replaces the geometry; your paint is carried onto the new triangles spatially, so it survives (already-baked relief is kept too)." msgstr "Reconstrói o modelo inteiro com triângulos próximos deste comprimento de aresta - dividindo os grandes e mesclando os pequenos - para que o deslocamento tenha uma densidade uniforme para trabalhar. Substitui a geometria; sua pintura é transferida espacialmente para os novos triângulos, então ela sobrevive (o relevo já assado também é mantido)." @@ -28944,19 +28702,15 @@ msgstr "Ignorar anel externo" msgid "Keeps the outer ring of the painted area out of the smoothing. Its neighbours outside the paint never move, so smoothing it drags the relief down and leaves the pattern half-melted at the border." msgstr "Mantém o anel externo da área pintada fora da suavização. Seus vizinhos fora da pintura nunca se movem, então suavizá-lo puxa o relevo para baixo e deixa o padrão meio derretido na borda." -# AI Translated msgid "Smooth baked mesh now" msgstr "Suavizar malha assada agora" -# AI Translated msgid "Applies the smoothing above to the model itself, right now, within the painted area. For relief that is already baked in - anything not yet baked is smoothed by Bake." msgstr "Aplica a suavização acima ao próprio modelo, agora mesmo, dentro da área pintada. Para relevo que já foi assado - o que ainda não foi assado é suavizado por Assar." -# AI Translated msgid "Experimental: classic bake pipeline" msgstr "Experimental: pipeline de assamento clássico" -# AI Translated msgid "Bake by moving the vertices the mesh already has, after preparing it (remesh, adaptive subdivision, and a cut along sharp steps in the texture). Keeps the topology, which is what colors need. The default pipeline instead refines, cleans up sliver triangles, displaces and simplifies in one run; nothing needs preparing first, but it does not produce colors yet." msgstr "Assa movendo os vértices que a malha já tem, depois de prepará-la (remalhamento, subdivisão adaptativa e um corte ao longo dos degraus bruscos da textura). Mantém a topologia, que é o que as cores precisam. O pipeline padrão, em vez disso, refina, limpa triângulos estreitos, desloca e simplifica de uma só vez; nada precisa ser preparado antes, mas ainda não produz cores." @@ -28970,9 +28724,8 @@ msgstr "Orçamento" msgid "%d k (auto)" msgstr "%d k (automático)" -# AI Translated msgid "How many thousand triangles this bake may spend on the area you painted. Relief already baked into the rest of the model is kept on top of it, so a second bake somewhere else gets the same budget as the first. Raise it if the warning below Resolution says the detail will not fit; 0 keeps every triangle the refinement produced, however many that is." -msgstr "Quantos milhares de triângulos este assamento pode gastar na área que você pintou. O relevo já assado no restante do modelo é mantido além disso, então um segundo assamento em outro lugar recebe o mesmo orçamento que o primeiro. Aumente se o aviso abaixo de Resolução disser que o detalhe não cabe; 0 mantém todos os triângulos que o refinamento produziu, quantos forem." +msgstr "Quantos milhares de triângulos este assamento pode gastar na área que você pintou. O relevo já assado no restante do modelo é mantido além disso, então um segundo assamento em outro lugar recebe o mesmo orçamento que o primeiro. Aumente se o aviso abaixo de Resolução disser que o detalhe não cabe; 0 preserva todos os triângulos produzidos pelo refinamento, independentemente de quantos sejam." # AI Translated msgid "Align mesh to texture edges" @@ -29004,16 +28757,13 @@ msgstr "%.2f mm (automático)" msgid "Chosen from the size of the model: %1$.2f mm triangles, budget %2% k. Untick Auto to set them yourself." msgstr "Escolhido pelo tamanho do modelo: triângulos de %1$.2f mm, orçamento de %2% k. Desmarque Auto para defini-los você mesmo." -# AI Translated msgid "How fine the mesh is made under the paint, in millimetres. It has to be smaller than the detail you want out of the texture - a 0.5 mm groove needs triangles well under 0.5 mm. Smaller costs triangles fast: halving it needs four times as many, and once they no longer fit the budget the bake simplifies back down and the detail goes with it." -msgstr "Quão fina a malha fica sob a pintura, em milímetros. Precisa ser menor que o detalhe que você quer tirar da textura - um sulco de 0,5 mm precisa de triângulos bem abaixo de 0,5 mm. Menor custa triângulos rapidamente: reduzir pela metade exige quatro vezes mais, e quando eles não cabem mais no orçamento, o assamento simplifica de volta e o detalhe vai junto." +msgstr "Quão fina a malha fica sob a pintura, em milímetros. Precisa ser menor que o detalhe que você quer tirar da textura - um sulco de 0,5 mm precisa de triângulos bem abaixo de 0,5 mm. Menor custa triângulos rapidamente: reduzir pela metade exige quatro vezes mais e quando eles não cabem mais no orçamento o assamento simplifica de volta e o detalhe vai junto." -# AI Translated #, boost-format msgid "Needs about %1% triangles, budget %2% - the bake will simplify back down and lose detail." msgstr "Precisa de cerca de %1% triângulos, orçamento de %2% - o assamento vai simplificar de volta e perder detalhes." -# AI Translated msgid "How many thousand triangles the refinement may add to the model. The triangle that fits worst is always split first, so even a run that spends the lot has spent it where it shows most. Raise it if the relief still looks coarse." msgstr "Quantos milhares de triângulos o refinamento pode adicionar ao modelo. O triângulo que se ajusta pior é sempre dividido primeiro, então mesmo uma execução que gaste tudo terá gasto onde mais aparece. Aumente se o relevo ainda parecer grosseiro." @@ -29021,25 +28771,20 @@ msgstr "Quantos milhares de triângulos o refinamento pode adicionar ao modelo. msgid "Preparing..." msgstr "Preparando..." -# AI Translated msgid "Baking..." msgstr "Assando..." -# AI Translated msgid "Nothing is painted yet. The UV editor's seam tool is on, so strokes on the model mark seams instead of painting - turn it off in the pane to paint." -msgstr "Nada foi pintado ainda. A ferramenta de emenda do editor UV está ativa, então os traços no modelo marcam emendas em vez de pintar - desative-a no painel para pintar." +msgstr "Nada foi pintado ainda. A ferramenta de costura do editor UV está ativa, então os traços no modelo marcam costuras em vez de pintar - desative-a no painel para pintar." -# AI Translated msgid "Nothing is painted yet." msgstr "Nada foi pintado ainda." -# AI Translated msgid "Turn the painted height maps into real geometry, by moving the vertices that are already there. Use Subdivide first if the mesh is too coarse to show the detail." msgstr "Transforma os mapas de altura pintados em geometria real, movendo os vértices que já existem. Use Subdividir antes se a malha for grosseira demais para mostrar o detalhe." -# AI Translated msgid "Turn the painted height maps into real geometry. The mesh is remeshed to an even density and refined where the texture bends first, so the detail has vertices to land on - all in one step." -msgstr "Transforma os mapas de altura pintados em geometria real. Primeiro a malha é remalhada para uma densidade uniforme e refinada onde a textura se curva, para que o detalhe tenha vértices onde cair - tudo em uma etapa." +msgstr "Transforma os mapas de altura pintados em geometria real. A malha é remalhada para uma densidade uniforme e refinada onde a textura se curva primeiro, para que o detalhe tenha vértices onde cair - tudo em uma etapa." # AI Translated msgid "Stops the bake. Whatever it had already finished stays on the model, and can be undone." @@ -29049,22 +28794,18 @@ msgstr "Interrompe o assamento. O que já tiver sido concluído permanece no mod msgid "Closes the tool without baking. Your paint, layers and settings stay with the model." msgstr "Fecha a ferramenta sem assar. Sua pintura, camadas e configurações permanecem com o modelo." -# AI Translated #, boost-format msgid "%1% k" msgstr "%1% k" -# AI Translated #, boost-format msgid "Model: %1% triangles - the bake refines to the resolution above, up to its budget" msgstr "Modelo: %1% triângulos - o assamento refina até a resolução acima, dentro do seu orçamento" -# AI Translated #, boost-format msgid "Bake moves existing vertices - the model stays at %1% triangles" msgstr "Assar move os vértices existentes - o modelo continua com %1% triângulos" -# AI Translated #, boost-format msgid "Bake result up to ~%1% k triangles" msgstr "Resultado do assamento até ~%1% k triângulos" @@ -29091,43 +28832,33 @@ msgstr "Saindo da pintura de deslocamento de textura" msgid "Texture displacement editing" msgstr "Edição de deslocamento de textura" -# AI Translated msgid "Choose SLA archive:" msgstr "Escolha o arquivo SLA:" -# AI Translated msgid "Import file" msgstr "Importar arquivo" -# AI Translated msgid "Import model and profile" msgstr "Importar modelo e perfil" -# AI Translated msgid "Import profile only" msgstr "Importar apenas o perfil" -# AI Translated msgid "Import model only" msgstr "Importar apenas o modelo" -# AI Translated msgid "Accurate" msgstr "Precisa" -# AI Translated msgid "Balanced" msgstr "Equilibrada" -# AI Translated msgid "Quick" msgstr "Rápida" -# AI Translated msgid "Baking texture displacement" msgstr "Assando deslocamento de textura" -# AI Translated msgid "The bake produced no displacement, so nothing was changed and the paint was kept. Check that the painted layer has a texture and a non-zero depth." msgstr "O assamento não produziu deslocamento, então nada foi alterado e a pintura foi mantida. Verifique se a camada pintada tem uma textura e uma profundidade diferente de zero." @@ -29136,7 +28867,6 @@ msgstr "O assamento não produziu deslocamento, então nada foi alterado e a pin msgid "The triangle budget limited the detail: this resolution needs %1% triangles, the budget kept %2%. Raise Budget or use a coarser Resolution for the full detail." msgstr "O orçamento de triângulos limitou o detalhe: esta resolução precisa de %1% triângulos, o orçamento manteve %2%. Aumente Orçamento ou use uma Resolução mais grosseira para obter o detalhe completo." -# AI Translated msgid "Capturing bake stages" msgstr "Capturando etapas do assamento" @@ -30169,7 +29899,7 @@ msgstr "Desdobrar" # AI Translated msgid "Seam angle" -msgstr "Ângulo de emenda" +msgstr "Ângulo de costura" # AI Translated msgid "Edges sharper than this are cut, and the pieces either side of them are flattened separately. Lower it to cut more: each piece then lies flat with less stretching, at the cost of the texture not running continuously across the cut. Takes effect at the next Unwrap." @@ -30194,15 +29924,13 @@ msgstr "" # AI Translated msgid "Mark seams - click edges on the model to cut the unwrap along them. The edge under the cursor is highlighted yellow; click to mark it red, click a red edge again to unmark it. Painting is paused while this is on." -msgstr "Marcar emendas - clique em arestas no modelo para cortar o desdobramento ao longo delas. A aresta sob o cursor fica destacada em amarelo; clique para marcá-la em vermelho, clique novamente em uma aresta vermelha para desmarcá-la. A pintura fica pausada enquanto isto estiver ativo." +msgstr "Marcar costuras - clique em arestas no modelo para cortar o desdobramento ao longo delas. A aresta sob o cursor fica destacada em amarelo; clique para marcá-la em vermelho, clique novamente em uma aresta vermelha para desmarcá-la. A pintura fica pausada enquanto isto estiver ativo." -# AI Translated msgid "Path - instead of clicking every edge, click a start point and then an end point: the whole shortest path between them is seamed at once. Available while marking seams." -msgstr "Caminho - em vez de clicar em cada aresta, clique em um ponto inicial e depois em um ponto final: todo o caminho mais curto entre eles recebe a emenda de uma vez. Disponível ao marcar emendas." +msgstr "Caminho - em vez de clicar em cada aresta, clique em um ponto inicial e depois em um ponto final: todo o caminho mais curto entre eles recebe a costura de uma vez. Disponível ao marcar costuras." -# AI Translated msgid "Clear seams - remove every seam marked on this layer" -msgstr "Limpar emendas - remove todas as emendas marcadas nesta camada" +msgstr "Limpar costuras - remove todas as costuras marcadas nesta camada" # AI Translated msgid "Average scale - give every island the same texel density" @@ -30236,11 +29964,9 @@ msgstr "" msgid "What is selected, and the exact figures of the move, rotation or scale while you drag one." msgstr "" -# AI Translated msgid "No layer mapped with Unwrap" msgstr "Nenhuma camada mapeada com Desdobrar" -# AI Translated #, c-format, boost-format msgid "%.1f mm tile" msgstr "ladrilho de %.1f mm" @@ -30250,11 +29976,10 @@ msgstr "" # AI Translated msgid "Out of date - the paint, the seams or the seam angle changed since this unwrap was made. Press to unwrap again." -msgstr "Desatualizado - a pintura, as emendas ou o ângulo de emenda mudaram desde que este desdobramento foi feito. Pressione para desdobrar novamente." +msgstr "Desatualizado - a pintura, as costuras ou o ângulo de costura mudaram desde que este desdobramento foi feito. Pressione para desdobrar novamente." -# AI Translated msgid "Flatten the painted area into UV islands. It is computed only when you press this, not on every edit - so paint, change the seam angle or mark seams first, then press Unwrap." -msgstr "Achata a área pintada em ilhas UV. Só é calculado quando você pressiona isto, não a cada edição - então pinte, altere o ângulo de emenda ou marque emendas primeiro e depois pressione Desdobrar." +msgstr "Achata a área pintada em ilhas UV. Só é calculado quando você pressiona isso, não a cada edição - então pinte, altere o ângulo de costura ou marque costuras primeiro e depois pressione Desdobrar." msgid "Turn Mark seams on first - Path is a quicker way of marking them." msgstr "" @@ -30685,7 +30410,6 @@ msgstr "" "O plugin de pós-processamento %1% falhou.\n" "Erro: %2%" -# AI Translated #, boost-format msgid "" "Post-processing plugin %1% failed.\n" @@ -30695,19 +30419,16 @@ msgid "" msgstr "" "O plugin de pós-processamento %1% falhou.\n" "\n" -"Espera-se que o plugin de pós-processamento modifique o arquivo G-code %2% no próprio local, mas o arquivo G-code foi excluído.\n" +"É esperado que o plugin de pós-processamento modifique o arquivo G-code %2% diretamente, mas o arquivo G-code foi excluído.\n" "Verifique a implementação do plugin.\n" -# AI Translated #, boost-format msgid "Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%" msgstr "Falha ao criar uma cópia temporária do arquivo G-code %1% antes de executar um script de pós-processamento: %2%" -# AI Translated msgid "Post-processor can't find exported gcode file" msgstr "O pós-processador não consegue encontrar o arquivo G-code exportado" -# AI Translated #, boost-format msgid "" "Post-processing script %1% on file %2% failed.\n" @@ -30716,7 +30437,6 @@ msgstr "" "O script de pós-processamento %1% no arquivo %2% falhou.\n" "Código de erro: %3%" -# AI Translated #, boost-format msgid "" "Post-processing script %1% on file %2% failed.\n" @@ -30729,7 +30449,6 @@ msgstr "" "Saída:\n" "%4%" -# AI Translated #, boost-format msgid "" "Post-processing script %1% failed.\n" @@ -30739,36 +30458,29 @@ msgid "" msgstr "" "O script de pós-processamento %1% falhou.\n" "\n" -"Espera-se que o script de pós-processamento altere o arquivo G-code %2% no próprio local, mas o arquivo G-code foi excluído e provavelmente salvo com um novo nome.\n" -"Ajuste o script de pós-processamento para alterar o G-code no próprio local e consulte o manual para saber como, opcionalmente, renomear o arquivo G-code pós-processado.\n" +"Espera-se que o script de pós-processamento altere o arquivo G-code %2% diretamente, mas o arquivo G-code foi excluído e provavelmente salvo com um novo nome.\n" +"Ajuste o script de pós-processamento para alterar o G-code diretamente e consulte o manual para saber como renomear opcionalmente o arquivo G-code pós-processado.\n" -# AI Translated msgid "Unable to parse desired new path from output name file" msgstr "Não foi possível analisar o novo caminho desejado a partir do arquivo de nome de saída" -# AI Translated #, boost-format msgid "Output directory does not exist: %1%" msgstr "O diretório de saída não existe: %1%" -# AI Translated #, boost-format msgid "Failed reading a file %1% carrying the final name / path of a G-code file: %2%" -msgstr "Falha ao ler o arquivo %1% que contém o nome / caminho final de um arquivo G-code: %2%" +msgstr "Falha ao ler o arquivo %1% que contém o nome/caminho final de um arquivo G-code: %2%" -# AI Translated msgid "The printing task exceeds the limit, supporting a maximum of 6 printers." -msgstr "A tarefa de impressão excede o limite; são suportadas no máximo 6 impressoras." +msgstr "A tarefa de impressão excede o limite, são suportadas no máximo 6 impressoras." -# AI Translated msgid "Elapsed time:" msgstr "Tempo decorrido:" -# AI Translated msgid "Estimated time:" msgstr "Tempo estimado:" -# AI Translated msgid "Remaining time:" msgstr "Tempo restante:" @@ -30982,8 +30694,7 @@ msgstr "" "Divida suas impressões em placas\n" "Você sabia que pode dividir um modelo que tem diversas peças em placas individuais distintas prontas para imprimir? Isso simplifica o processo de manter o controle de todas as peças." -#: resources/data/hints.ini: [hint:Speed up your print with Adaptive Layer -#: Height] +#: resources/data/hints.ini: [hint:Speed up your print with Adaptive Layer Height] msgid "" "Speed up your print with Adaptive Layer Height\n" "Did you know that you can print a model even faster by using the Adaptive Layer Height option? Check it out!" @@ -31055,8 +30766,7 @@ msgstr "" "Melhorar a resistência\n" "Você sabia que pode usar mais voltas de parede e maior densidade de preenchimento esparso mais alta para melhorar a resistência do modelo?" -#: resources/data/hints.ini: [hint:When do you need to print with the printer -#: door opened] +#: resources/data/hints.ini: [hint:When do you need to print with the printer door opened] msgid "" "When do you need to print with the printer door opened?\n" "Did you know that opening the printer door can reduce the probability of extruder/hotend clogging when printing lower temperature filament with a higher enclosure temperature? There is more info about this in the Wiki." @@ -31104,10 +30814,6 @@ msgstr "" #~ msgid "The 3MF was created by BambuStudio. Some settings may differ from OrcaSlicer." #~ msgstr "O arquivo 3MF foi criado pelo Bambu Studio. Algumas configurações podem ser diferentes do OrcaSlicer." -# AI Translated -#~ msgid "Cannot undo or redo while an operation is running. Stop it first." -#~ msgstr "Não é possível desfazer ou refazer enquanto uma operação está em andamento. Interrompa-a primeiro." - #, c-format, boost-format #~ msgid "%s: %s" #~ msgstr "%s: %s" @@ -31129,340 +30835,9 @@ msgstr "" #~ msgid "Video tutorial" #~ msgstr "Tutorial em vídeo" -# AI Translated -#~ msgid "Primitive" -#~ msgstr "Primitivo" - -# AI Translated -#~ msgid "Empty mesh generated" -#~ msgstr "Malha vazia gerada" - -# AI Translated -#~ msgid "Box" -#~ msgstr "Caixa" - -# AI Translated -#~ msgid "Quick:" -#~ msgstr "Rápido:" - -# AI Translated -#~ msgid "Width (X)" -#~ msgstr "Largura (X)" - -# AI Translated -#~ msgid "Depth (Y)" -#~ msgstr "Profundidade (Y)" - -# AI Translated -#~ msgid "Height (Z)" -#~ msgstr "Altura (Z)" - -# AI Translated -#~ msgid "Bottom R" -#~ msgstr "R inferior" - -# AI Translated -#~ msgid "Top R" -#~ msgstr "R superior" - -# AI Translated -#~ msgid "Major R" -#~ msgstr "R maior" - -# AI Translated -#~ msgid "Minor R" -#~ msgstr "R menor" - -# AI Translated -#~ msgid "All edges" -#~ msgstr "Todas as arestas" - -# AI Translated -#~ msgid "Top edges" -#~ msgstr "Arestas superiores" - -# AI Translated -#~ msgid "Bottom edges" -#~ msgstr "Arestas inferiores" - -# AI Translated -#~ msgid "Lateral edges" -#~ msgstr "Arestas laterais" - -# AI Translated -#~ msgid "Mesh resolution" -#~ msgstr "Resolução da malha" - -# AI Translated -#~ msgid "Add Shape" -#~ msgstr "Adicionar forma" - -# AI Translated -#~ msgid "No outer profile" -#~ msgstr "Nenhum perfil externo" - -# AI Translated -#~ msgid "Failed to make outer face" -#~ msgstr "Falha ao criar a face externa" - -# AI Translated -#~ msgid "Failed to build face with holes" -#~ msgstr "Falha ao construir a face com furos" - -# AI Translated -#~ msgid "Revolve failed" -#~ msgstr "Falha na revolução" - -# AI Translated -#~ msgid "Profile" -#~ msgstr "Perfil" - -# AI Translated -#~ msgid "Start new profile (for holes)" -#~ msgstr "Iniciar novo perfil (para furos)" - -# AI Translated -#~ msgid "Segments" -#~ msgstr "Segmentos" - -# AI Translated -#~ msgid "Sides" -#~ msgstr "Lados" - -# AI Translated -#~ msgid "Click on canvas to draw" -#~ msgstr "Clique na tela para desenhar" - -# AI Translated -#~ msgid "Snap to grid" -#~ msgstr "Ajustar à grade" - -# AI Translated -#~ msgid "Grid step" -#~ msgstr "Passo da grade" - -# AI Translated -#~ msgid "Outer" -#~ msgstr "Externo" - -# AI Translated -#~ msgid "closed" -#~ msgstr "fechado" - -# AI Translated -#, boost-format -#~ msgid "%1% %2%: %3% points %4%" -#~ msgstr "%1% %2%: %3% pontos %4%" - -# AI Translated -#~ msgid "XY (Top)" -#~ msgstr "XY (Topo)" - -# AI Translated -#~ msgid "XZ (Front)" -#~ msgstr "XZ (Frente)" - -# AI Translated -#~ msgid "YZ (Side)" -#~ msgstr "YZ (Lateral)" - -# AI Translated -#~ msgid "Revolve deg" -#~ msgstr "Revolução (graus)" - -# AI Translated -#~ msgid "Set to 0 for extrude, >0 for revolve" -#~ msgstr "Defina como 0 para extrusão, >0 para revolução" - -# AI Translated -#~ msgid "Pocket (cut)" -#~ msgstr "Cavidade (corte)" - -# AI Translated -#, c-format -#~ msgid "%zu closed profile" -#~ msgid_plural "%zu closed profiles" -#~ msgstr[0] "%zu perfil fechado" -#~ msgstr[1] "%zu perfis fechados" - -# AI Translated -#~ msgid "Draw a closed profile to enable" -#~ msgstr "Desenhe um perfil fechado para ativar" - -# AI Translated -#~ msgid "Empty result" -#~ msgstr "Resultado vazio" - -# AI Translated -#, boost-format -#~ msgid "Extrude: %1%" -#~ msgstr "Extrusão: %1%" - -# AI Translated -#, boost-format -#~ msgid "Revolve: %1%" -#~ msgstr "Revolução: %1%" - -# AI Translated -#~ msgid "No object selected" -#~ msgstr "Nenhum objeto selecionado" - -# AI Translated -#~ msgid "Face failed" -#~ msgstr "Falha na face" - -# AI Translated -#~ msgid "Face with holes failed" -#~ msgstr "Falha na face com furos" - -# AI Translated -#~ msgid "Tool mesh empty" -#~ msgstr "Malha da ferramenta vazia" - -# AI Translated -#~ msgid "Pocket added (negative volume)" -#~ msgstr "Cavidade adicionada (volume negativo)" - -# AI Translated -#, boost-format -#~ msgid "Pocket: %1%" -#~ msgstr "Cavidade: %1%" - -# AI Translated -#~ msgid "Needs the active layer mapped with Unwrap (LSCM)." -#~ msgstr "Requer a camada ativa mapeada com Desdobrar (LSCM)." - -# AI Translated -#~ msgid "Checker - a test grid instead of the texture. Where the squares stay square the texture is undistorted; where they stretch, it will too" -#~ msgstr "Xadrez - uma grade de teste no lugar da textura. Onde os quadrados continuam quadrados, a textura não está distorcida; onde eles esticam, ela também vai esticar" - -# AI Translated -#~ msgid "Distortion - blue-to-red stretch heatmap over the unwrap" -#~ msgstr "Distorção - mapa de calor de estiramento, de azul a vermelho, sobre o desdobramento" - -# AI Translated -#~ msgid "Mix by" -#~ msgstr "Misturar por" - -# AI Translated -#~ msgctxt "Texture Displacement" -#~ msgid "Surface" -#~ msgstr "Superfície" - -# AI Translated -#~ msgid "" -#~ "Layers: the two filaments alternate between print layers, which blends smoothly on upright surfaces but disappears on flat-facing ones, where a whole layer is a single band.\n" -#~ "Surface: a fine checkerboard across the surface, which works at any angle but can read as texture rather than as a blend.\n" -#~ "Automatic: layers on upright faces; flat-facing faces take the nearer single filament, since a checkerboard there shows as a pattern." -#~ msgstr "" -#~ "Camadas: os dois filamentos se alternam entre as camadas de impressão, o que mistura suavemente em superfícies verticais, mas desaparece nas horizontais, onde uma camada inteira é uma única faixa.\n" -#~ "Superfície: um xadrez fino sobre a superfície, que funciona em qualquer ângulo, mas pode parecer textura em vez de mistura.\n" -#~ "Automático: camadas nas faces verticais; as faces horizontais recebem o filamento único mais próximo, já que ali um xadrez aparece como padrão." - -# AI Translated -#~ msgid "Island - move, rotate and scale whole islands" -#~ msgstr "Ilha - mover, girar e escalar ilhas inteiras" - -# AI Translated -#~ msgid "Vertex - drag vertices to reshape; Shift/Ctrl to multi-select" -#~ msgstr "Vértice - arraste vértices para remodelar; Shift/Ctrl para seleção múltipla" - -# AI Translated -#~ msgid "Edge - drag edges to reshape; Shift/Ctrl to multi-select" -#~ msgstr "Aresta - arraste arestas para remodelar; Shift/Ctrl para seleção múltipla" - -# AI Translated -#~ msgid "Frame all islands (Home)" -#~ msgstr "Enquadrar todas as ilhas (Home)" - #~ msgid "(Experimental) Use printer agents instead of print hosts" #~ msgstr "(Experimental) Usar agentes de impressora em vez de hosts de impressão" -# AI Translated -#~ msgid "FEATURES" -#~ msgstr "RECURSOS" - -# AI Translated -#~ msgid "Click a face or a reference plane in the viewport, then a sketch tool" -#~ msgstr "Clique em uma face ou em um plano de referência na janela de visualização e depois em uma ferramenta de esboço" - -# AI Translated -#~ msgid "Click a face or a reference plane, then a sketch tool." -#~ msgstr "Clique em uma face ou em um plano de referência e depois em uma ferramenta de esboço." - -# AI Translated -#, c-format, boost-format -#~ msgid "%s plane selected — right-click for the drawing tools" -#~ msgstr "Plano %s selecionado — clique com o botão direito para ver as ferramentas de desenho" - -# AI Translated -#~ msgid "" -#~ "Nothing yet — import a STEP or a mesh from the toolbar,\n" -#~ "or click a reference plane and right-click it to start a sketch." -#~ msgstr "" -#~ "Nada ainda — importe um STEP ou uma malha pela barra de ferramentas,\n" -#~ "ou clique em um plano de referência e clique nele com o botão direito para iniciar um esboço." - -# AI Translated -#~ msgid "Click a face or a reference plane, then pick a drawing tool" -#~ msgstr "Clique em uma face ou em um plano de referência e depois escolha uma ferramenta de desenho" - -# AI Translated -#~ msgid "XY" -#~ msgstr "XY" - -# AI Translated -#~ msgid "XZ" -#~ msgstr "XZ" - -# AI Translated -#~ msgid "YZ" -#~ msgstr "YZ" - -# AI Translated -#~ msgid "Equal" -#~ msgstr "Igual" - -# AI Translated -#~ msgid "Perpendicular" -#~ msgstr "Perpendicular" - -# AI Translated -#~ msgid "Midpoint" -#~ msgstr "Ponto médio" - -# AI Translated -#~ msgid "Equal length" -#~ msgstr "Comprimento igual" - -# AI Translated -#~ msgid "Equal radius" -#~ msgstr "Raio igual" - -# AI Translated -#~ msgid "Collinear" -#~ msgstr "Colinear" - -# AI Translated -#~ msgid "Symmetric about the vertical axis" -#~ msgstr "Simétrico em relação ao eixo vertical" - -# AI Translated -#~ msgid "Symmetric about the horizontal axis" -#~ msgstr "Simétrico em relação ao eixo horizontal" - -# AI Translated -#~ msgid "Fix point (anchor in place)" -#~ msgstr "Fixar ponto (ancorar no lugar)" - -# AI Translated -#~ msgid "Horizontal distance" -#~ msgstr "Distância horizontal" - -# AI Translated -#~ msgid "Vertical distance" -#~ msgstr "Distância vertical" - #~ msgid "Face recognition" #~ msgstr "Reconhecimento facial" @@ -31500,10 +30875,6 @@ msgstr "" #~ msgid "Please resolve the slicing errors and publish again." #~ msgstr "Por favor, resolva os erros de fatiamento e publique novamente." -# AI Translated -#~ msgid "Treat sketch endpoints within 0.001 mm as one joint and weld the loop shut. Off: only exactly coincident endpoints join, so a loop with a tiny gap is shown as open instead of being closed for you." -#~ msgstr "Trata extremidades do esboço a menos de 0,001 mm umas das outras como uma só junção e fecha o laço. Desativado: somente extremidades exatamente coincidentes se unem, então um laço com uma pequena folga é mostrado como aberto em vez de ser fechado para você." - #~ msgid "Slice all plates to obtain time and filament estimation" #~ msgstr "Fatiar todas as placas para obter estimativa de tempo e filamento" @@ -31751,860 +31122,21 @@ msgstr "" #~ msgid "Standard profile for 0.8mm nozzle, prioritizing speed." #~ msgstr "Perfil padrão para bico de 0,8 mm, priorizando a velocidade." -# AI Translated -#~ msgid "Slot — two centerline ends, then end radius" -#~ msgstr "Rasgo — as duas extremidades da linha central e depois o raio da extremidade" - -# AI Translated -#~ msgid "Isometric view, fitted" -#~ msgstr "Vista isométrica, ajustada" - -# AI Translated -#~ msgid "Coord Sys" -#~ msgstr "Sist. coord." - -# AI Translated -#~ msgid "Assembly: align two CoordSys features (fastened, planar, revolute, slider, cylindrical)" -#~ msgstr "Montagem: alinhar dois recursos de sistema de coordenadas (fixo, planar, de revolução, deslizante, cilíndrico)" - -# AI Translated -#~ msgid "Click points; click the first point to close the loop, right-click to end the chain" -#~ msgstr "Clique nos pontos; clique no primeiro ponto para fechar o laço, clique com o botão direito para encerrar a cadeia" - -# AI Translated -#~ msgid "Click two centerline ends, then a point for the end radius" -#~ msgstr "Clique nas duas extremidades da linha central e depois em um ponto para o raio da extremidade" - -# AI Translated -#~ msgid "Click control points; double-click or right-click to finish" -#~ msgstr "Clique nos pontos de controle; clique duplo ou clique com o botão direito para terminar" - -# AI Translated -#~ msgid "Click a segment to trim it back to its nearest intersection; right-click exits" -#~ msgstr "Clique em um segmento para apará-lo até a interseção mais próxima; clique com o botão direito para sair" - -# AI Translated -#~ msgid "Click a line or arc to extend it to the nearest entity; right-click exits" -#~ msgstr "Clique em uma linha ou arco para estendê-lo até a entidade mais próxima; clique com o botão direito para sair" - -# AI Translated -#~ msgid "✓ Confirm" -#~ msgstr "✓ Confirmar" - -# AI Translated -#~ msgid "✗ Cancel" -#~ msgstr "✗ Cancelar" - -# AI Translated -#~ msgid "Undo (Ctrl+Z)" -#~ msgstr "Desfazer (Ctrl+Z)" - -# AI Translated -#~ msgid "Redo (Ctrl+Shift+Z)" -#~ msgstr "Refazer (Ctrl+Shift+Z)" - -# AI Translated -#~ msgid "Taper °" -#~ msgstr "Conicidade °" - -# AI Translated -#~ msgid "Total angle°" -#~ msgstr "Ângulo total°" - -# AI Translated -#~ msgid "Union (join)" -#~ msgstr "Unir (juntar)" - -# AI Translated -#~ msgid "Subtract (cut)" -#~ msgstr "Subtrair (cortar)" - -# AI Translated -#~ msgid "Angle°" -#~ msgstr "Ângulo°" - -# AI Translated -#~ msgid "CS A (fixed)" -#~ msgstr "SC A (fixo)" - -# AI Translated -#~ msgid "CS B (moves)" -#~ msgstr "SC B (móvel)" - -# AI Translated -#~ msgid "Move body" -#~ msgstr "Mover corpo" - -# AI Translated -#~ msgid "Select a body row first, then move it" -#~ msgstr "Selecione primeiro a linha de um corpo e depois mova-o" - -# AI Translated -#~ msgid "+" -#~ msgstr "+" - -# AI Translated -#, c-format, boost-format -#~ msgid "Body %d: edge %d selected — Fillet/Chamfer to dress it, or click again for the whole body" -#~ msgstr "Corpo %d: aresta %d selecionada — Filete/Chanfro para arredondá-la ou chanfrá-la, ou clique novamente para o corpo inteiro" - -# AI Translated -#, c-format, boost-format -#~ msgid "Edge %d selected — Fillet/Chamfer to dress it, or click again for the whole body" -#~ msgstr "Aresta %d selecionada — Filete/Chanfro para arredondá-la ou chanfrá-la, ou clique novamente para o corpo inteiro" - -# AI Translated -#~ msgid "Text to insert:" -#~ msgstr "Texto a inserir:" - -# AI Translated -#~ msgid "STEP import failed: " -#~ msgstr "Falha ao importar STEP: " - -# AI Translated -#~ msgid "Could not read the OBJ file: " -#~ msgstr "Não foi possível ler o arquivo OBJ: " - -# AI Translated -#~ msgid "Mesh conversion failed: " -#~ msgstr "Falha na conversão da malha: " - -# AI Translated -#~ msgid "Mesh import failed: " -#~ msgstr "Falha ao importar a malha: " - -# AI Translated -#~ msgid "Drag a corner to scale, the center to move — right-click when done" -#~ msgstr "Arraste um canto para dimensionar, o centro para mover — clique com o botão direito ao terminar" - -# AI Translated -#~ msgid "Recompute error: " -#~ msgstr "Erro de recálculo: " - -# AI Translated -#~ msgid "Mate needs two CoordSys features — create them first" -#~ msgstr "O posicionamento precisa de dois recursos de sistema de coordenadas — crie-os primeiro" - -# AI Translated -#~ msgid "Mate: CS A and CS B must be different CoordSys features" -#~ msgstr "Posicionamento: SC A e SC B devem ser recursos de sistema de coordenadas diferentes" - -# AI Translated -#~ msgid "No interference — need at least two solid bodies to check" -#~ msgstr "Sem interferência — são necessários pelo menos dois corpos sólidos para verificar" - -# AI Translated -#, c-format -#~ msgid "%zu interference pair, worst %.2f mm³" -#~ msgid_plural "%zu interference pairs, worst %.2f mm³" -#~ msgstr[0] "%zu par com interferência, pior %.2f mm³" -#~ msgstr[1] "%zu pares com interferência, pior %.2f mm³" - -# AI Translated -#~ msgid "" -#~ "Interference pairs:\n" -#~ "\n" -#~ msgstr "" -#~ "Pares com interferência:\n" -#~ "\n" - -# AI Translated -#~ msgid "Interference" -#~ msgstr "Interferência" - -# AI Translated -#~ msgid "Select a solid body first — its mass properties are what is reported" -#~ msgstr "Selecione primeiro um corpo sólido — são as propriedades de massa dele que são informadas" - -# AI Translated -#~ msgid "Mass properties could not be computed for this body" -#~ msgstr "Não foi possível calcular as propriedades de massa deste corpo" - -# AI Translated -#, c-format, boost-format -#~ msgid "%s: sheet body — %.2f cm² of surface, no volume" -#~ msgstr "%s: corpo de superfície — %.2f cm² de superfície, sem volume" - -# AI Translated -#, c-format, boost-format -#~ msgid "" -#~ "%s\n" -#~ "\n" -#~ "Sheet body (open shell)\n" -#~ "Surface area: %.2f cm²\n" -#~ "\n" -#~ "A sheet encloses no material, so it has no volume. Thicken it into a solid to get one." -#~ msgstr "" -#~ "%s\n" -#~ "\n" -#~ "Corpo de superfície (casca aberta)\n" -#~ "Área de superfície: %.2f cm²\n" -#~ "\n" -#~ "Uma superfície não envolve material, então não tem volume. Espesse-a para transformá-la em um sólido e obter um volume." - -# AI Translated -#~ msgid "Mass properties" -#~ msgstr "Propriedades de massa" - -# AI Translated -#, c-format, boost-format -#~ msgid "%s: %.3f cm³, %.2f cm²" -#~ msgstr "%s: %.3f cm³, %.2f cm²" - -# AI Translated -#, c-format, boost-format -#~ msgid "" -#~ "%s\n" -#~ "\n" -#~ "Volume: %.3f cm³\n" -#~ "Surface area: %.2f cm²" -#~ msgstr "" -#~ "%s\n" -#~ "\n" -#~ "Volume: %.3f cm³\n" -#~ "Área de superfície: %.2f cm²" - -# AI Translated -#~ msgid "Boolean needs two solid bodies — add or import a second one" -#~ msgstr "A operação booleana precisa de dois corpos sólidos — adicione ou importe um segundo" - -# AI Translated -#~ msgid "Line — click the end point, or type the length" -#~ msgstr "Linha — clique no ponto final ou digite o comprimento" - -# AI Translated -#~ msgid "Polyline — click the next point · click the start point to close it · right-click to end the chain" -#~ msgstr "Polilinha — clique no próximo ponto · clique no ponto inicial para fechá-la · clique com o botão direito para encerrar a cadeia" - -# AI Translated -#~ msgid "Circle — click to set the radius, or type it" -#~ msgstr "Círculo — clique para definir o raio ou digite-o" - -# AI Translated -#~ msgid "Spline — click the next control point · right-click to finish the curve" -#~ msgstr "Spline — clique no próximo ponto de controle · clique com o botão direito para terminar a curva" - -# AI Translated -#~ msgid "Trim — click a segment where it crosses another entity · right-click to exit" -#~ msgstr "Aparar — clique em um segmento onde ele cruza outra entidade · clique com o botão direito para sair" - -# AI Translated -#~ msgid "Extend — click a line or arc to grow it out to the nearest entity · right-click to exit" -#~ msgstr "Estender — clique em uma linha ou arco para prolongá-lo até a entidade mais próxima · clique com o botão direito para sair" - -# AI Translated -#~ msgid "Fillet — drag the arrow, or click the number to type the radius · click empty space to apply · right-click cancels" -#~ msgstr "Filete — arraste a seta ou clique no número para digitar o raio · clique em um espaço vazio para aplicar · o botão direito cancela" - -# AI Translated -#~ msgid "Chamfer — drag the arrow, or click the number to type the setback · click empty space to apply · right-click cancels" -#~ msgstr "Chanfro — arraste a seta ou clique no número para digitar o recuo · clique em um espaço vazio para aplicar · o botão direito cancela" - -# AI Translated -#~ msgid "Offset — click the entity to offset" -#~ msgstr "Deslocamento — clique na entidade a deslocar" - -# AI Translated -#~ msgid "Offset — drag the arrow to either side, or click the number to type the distance · click empty space to apply · right-click cancels" -#~ msgstr "Deslocamento — arraste a seta para qualquer lado ou clique no número para digitar a distância · clique em um espaço vazio para aplicar · o botão direito cancela" - -# AI Translated -#~ msgid "Mirror — axis set · now click the entities to mirror · right-click cancels" -#~ msgstr "Espelhar — eixo definido · agora clique nas entidades a espelhar · o botão direito cancela" - -# AI Translated -#, c-format, boost-format -#~ msgid "Mirror — axis set · %d to mirror · click another to add or remove it · click empty space to apply" -#~ msgstr "Espelhar — eixo definido · %d a espelhar · clique em outra para adicioná-la ou removê-la · clique em um espaço vazio para aplicar" - -# AI Translated -#~ msgid "Move — drag the handle, or click the number to type the distance · click empty space to apply" -#~ msgstr "Mover — arraste a alça ou clique no número para digitar a distância · clique em um espaço vazio para aplicar" - -# AI Translated -#~ msgid "Rotate — drag the handle, or click the number to type the angle · click empty space to apply" -#~ msgstr "Rotacionar — arraste a alça ou clique no número para digitar o ângulo · clique em um espaço vazio para aplicar" - -# AI Translated -#~ msgid "Scale — drag the handle, or click the number to type the factor · click empty space to apply" -#~ msgstr "Dimensionar — arraste a alça ou clique no número para digitar o fator · clique em um espaço vazio para aplicar" - -# AI Translated -#~ msgid "Array — drag the handle to set the step, click the count to type it · click empty space to apply" -#~ msgstr "Matriz — arraste a alça para definir o passo, clique na quantidade para digitá-la · clique em um espaço vazio para aplicar" - -# AI Translated -#~ msgid "Polar array — drag the handle to set the sweep, click the count to type it · click empty space to apply" -#~ msgstr "Matriz polar — arraste a alça para definir o ângulo total, clique na quantidade para digitá-la · clique em um espaço vazio para aplicar" - -# AI Translated -#~ msgid "Drag a corner to scale, the center to move · right-click when done" -#~ msgstr "Arraste um canto para dimensionar, o centro para mover · clique com o botão direito ao terminar" - -# AI Translated -#~ msgid "Coord Sys added" -#~ msgstr "Sist. coord. adicionado" - -# AI Translated -#~ msgid "Could not restore the CAD model: " -#~ msgstr "Não foi possível restaurar o modelo CAD: " - -# AI Translated -#~ msgid "Boolean needs two bodies — create or import a second solid" -#~ msgstr "A operação booleana precisa de dois corpos — crie ou importe um segundo sólido" - -# AI Translated -#~ msgid "Edit rejected: " -#~ msgstr "Edição rejeitada: " - -# AI Translated -#, c-format, boost-format -#~ msgid "" -#~ "Delete %s?\n" -#~ "\n" -#~ "That is the feature this body was made from. Features built on it may be removed or stop working." -#~ msgstr "" -#~ "Excluir %s?\n" -#~ "\n" -#~ "Esse é o recurso a partir do qual este corpo foi criado. Os recursos construídos sobre ele podem ser removidos ou parar de funcionar." - -# AI Translated -#~ msgid "Delete body" -#~ msgstr "Excluir corpo" - -# AI Translated -#~ msgid "STEP export failed: " -#~ msgstr "Falha ao exportar STEP: " - -# AI Translated -#~ msgid "Coord Sys ready" -#~ msgstr "Sist. coord. pronto" - -# AI Translated -#~ msgid "Mate needs at least two CoordSys features" -#~ msgstr "O posicionamento precisa de pelo menos dois recursos de sistema de coordenadas" - -# AI Translated -#~ msgid "Mate: CS A and CS B must be different" -#~ msgstr "Posicionamento: SC A e SC B devem ser diferentes" - -# AI Translated -#~ msgid "Invalid: " -#~ msgstr "Inválido: " - -# AI Translated -#~ msgid "Variable name:" -#~ msgstr "Nome da variável:" - -# AI Translated -#, c-format, boost-format -#~ msgid "Expression for '%s':" -#~ msgstr "Expressão para '%s':" - -# AI Translated -#~ msgid "Seg" -#~ msgstr "Seg." - -# AI Translated -#~ msgid "CLOSED" -#~ msgstr "FECHADO" - -# AI Translated -#, boost-format -#~ msgid "%1% %2%: %3% pts %4%" -#~ msgstr "%1% %2%: %3% pts %4%" - -# AI Translated -#~ msgid "Pocket (Cut)" -#~ msgstr "Cavidade (Corte)" - -# AI Translated -#~ msgid "Clear All" -#~ msgstr "Limpar tudo" - -# AI Translated -#~ msgid "Hollow this object" -#~ msgstr "Esvaziar este objeto" - -# AI Translated -#~ msgid "Preview hollowed and drilled model" -#~ msgstr "Pré-visualizar modelo esvaziado e perfurado" - -# AI Translated -#~ msgid "Closing distance" -#~ msgstr "Distância de fechamento" - -# AI Translated -#~ msgid "Hole diameter" -#~ msgstr "Diâmetro do furo" - -# AI Translated -#~ msgid "Hole depth" -#~ msgstr "Profundidade do furo" - -# AI Translated -#~ msgid "Remove selected holes" -#~ msgstr "Remover furos selecionados" - -# AI Translated -#~ msgid "Remove all holes" -#~ msgstr "Remover todos os furos" - -# AI Translated -#~ msgid "Clipping of view" -#~ msgstr "Recorte da vista" - #~ msgid "Reset direction" #~ msgstr "Redefinir direção" -# AI Translated -#~ msgid "Show supports" -#~ msgstr "Mostrar suportes" - -# AI Translated -#~ msgid "Hollow and drill" -#~ msgstr "Esvaziar e perfurar" - #~ msgid "Head diameter" #~ msgstr "Diâmetro da cabeça" -# AI Translated -#~ msgid "Lock supports under new islands" -#~ msgstr "Travar suportes sob novas ilhas" - #~ msgid "Remove selected points" #~ msgstr "Remover pontos selecionados" -# AI Translated -#~ msgid "Remove all points" -#~ msgstr "Remover todos os pontos" - -# AI Translated -#~ msgid "Apply changes" -#~ msgstr "Aplicar alterações" - -# AI Translated -#~ msgid "Discard changes" -#~ msgstr "Descartar alterações" - -# AI Translated -#~ msgid "Minimal points distance" -#~ msgstr "Distância mínima entre pontos" - -# AI Translated -#~ msgid "Support points density" -#~ msgstr "Densidade dos pontos de suporte" - -#~ msgid "Auto-generate points" -#~ msgstr "Pontos gerados automaticamente" - -# AI Translated -#~ msgid "Manual editing" -#~ msgstr "Edição manual" - -# AI Translated -#~ msgid "SLA Support Points" -#~ msgstr "Pontos de Suporte SLA" - -# AI Translated -#~ msgid "Do you want to save your manually edited support points?" -#~ msgstr "Deseja salvar os pontos de suporte editados manualmente?" - -# AI Translated -#~ msgid "Save support points?" -#~ msgstr "Salvar pontos de suporte?" - -# AI Translated -#~ msgid "Autogeneration will erase all manually edited points." -#~ msgstr "A geração automática apagará todos os pontos editados manualmente." - -# AI Translated -#~ msgid "Are you sure you want to do it?" -#~ msgstr "Tem certeza de que deseja fazer isso?" - -# AI Translated -#~ msgid "SLA gizmo keyboard shortcuts" -#~ msgstr "Atalhos de teclado do gizmo SLA" - -# AI Translated -#~ msgid "Note: some shortcuts work in (non)editing mode only." -#~ msgstr "Observação: alguns atalhos funcionam apenas dentro ou apenas fora do modo de edição." - #~ msgid "Left click" #~ msgstr "Clique esquerdo" -# AI Translated -#~ msgid "Add point" -#~ msgstr "Adicionar ponto" - #~ msgid "Right click" #~ msgstr "Clique direito" -# AI Translated -#~ msgid "Remove point" -#~ msgstr "Remover ponto" - -# AI Translated -#~ msgid "Move point" -#~ msgstr "Mover ponto" - -# AI Translated -#~ msgid "Add point to selection" -#~ msgstr "Adicionar ponto à seleção" - -# AI Translated -#~ msgid "Remove point from selection" -#~ msgstr "Remover ponto da seleção" - -# AI Translated -#~ msgid "Select by rectangle" -#~ msgstr "Selecionar por retângulo" - -# AI Translated -#~ msgid "Deselect by rectangle" -#~ msgstr "Desmarcar por retângulo" - -# AI Translated -#~ msgid "Select all points" -#~ msgstr "Selecionar todos os pontos" - -# AI Translated -#~ msgid "Move clipping plane" -#~ msgstr "Mover plano de recorte" - -# AI Translated -#~ msgid "Reset clipping plane" -#~ msgstr "Redefinir plano de recorte" - -# AI Translated -#~ msgid "Switch to editing mode" -#~ msgstr "Mudar para o modo de edição" - -# AI Translated -#~ msgctxt "Design" -#~ msgid "Union" -#~ msgstr "Unir" - -# AI Translated -#~ msgid "target is not a sheet body" -#~ msgstr "o alvo não é um corpo de superfície" - -# AI Translated -#~ msgid "Drill a hole, centred on a picked face or placed on a plane" -#~ msgstr "Fazer um furo, centralizado em uma face selecionada ou colocado em um plano" - -# AI Translated -#~ msgid "Shell needs a solid body" -#~ msgstr "A casca precisa de um corpo sólido" - -# AI Translated -#~ msgid "Split needs a solid body" -#~ msgstr "A divisão precisa de um corpo sólido" - -# AI Translated -#~ msgid "Pattern on curve needs a body and a curve" -#~ msgstr "O padrão em curva precisa de um corpo e de uma curva" - -# AI Translated -#~ msgid "A mate needs two coordinate systems" -#~ msgstr "Um posicionamento precisa de dois sistemas de coordenadas" - -# AI Translated -#~ msgid "Align needs a body" -#~ msgstr "O alinhamento precisa de um corpo" - -# AI Translated -#~ msgctxt "Design" -#~ msgid "Coord Sys" -#~ msgstr "Sist. coord." - -# AI Translated -#~ msgctxt "Design" -#~ msgid "Mass" -#~ msgstr "Massa" - -# AI Translated -#~ msgctxt "Design" -#~ msgid "Colour" -#~ msgstr "Cor" - -# AI Translated -#~ msgid "Set the selected body's display colour" -#~ msgstr "Definir a cor de exibição do corpo selecionado" - -# AI Translated -#~ msgctxt "Design" -#~ msgid "Centre rectangle" -#~ msgstr "Retângulo pelo centro" - -# AI Translated -#~ msgctxt "Design" -#~ msgid "Centre circle" -#~ msgstr "Círculo pelo centro" - -# AI Translated -#~ msgctxt "Design" -#~ msgid "Centre-point arc" -#~ msgstr "Arco pelo centro" - -# AI Translated -#~ msgid "Triangle — click centre, then a vertex" -#~ msgstr "Triângulo — clique no centro e depois em um vértice" - -# AI Translated -#~ msgid "Square — click centre, then a vertex" -#~ msgstr "Quadrado — clique no centro e depois em um vértice" - -# AI Translated -#~ msgid "Pentagon — click centre, then a vertex" -#~ msgstr "Pentágono — clique no centro e depois em um vértice" - -# AI Translated -#~ msgid "Hexagon — click centre, then a vertex" -#~ msgstr "Hexágono — clique no centro e depois em um vértice" - -# AI Translated -#~ msgid "Octagon — click centre, then a vertex" -#~ msgstr "Octógono — clique no centro e depois em um vértice" - -# AI Translated -#~ msgid "Dodecagon — click centre, then a vertex" -#~ msgstr "Dodecágono — clique no centro e depois em um vértice" - -# AI Translated -#~ msgid "Type text; its outline is added to this sketch as editable lines" -#~ msgstr "Digite um texto; o contorno dele é adicionado a este esboço como linhas editáveis" - -# AI Translated -#~ msgid "Major" -#~ msgstr "Eixo maior" - -# AI Translated -#~ msgid "Minor" -#~ msgstr "Eixo menor" - -# AI Translated -#~ msgid "Extend: click a line/arc that can reach another live entity" -#~ msgstr "Estender: clique em uma linha/arco que possa alcançar outra entidade ativa" - -# AI Translated -#~ msgid "Trim: click a segment where it crosses another live entity" -#~ msgstr "Aparar: clique em um segmento onde ele cruza outra entidade ativa" - -# AI Translated -#~ msgid "closed profile(s)" -#~ msgstr "perfil(is) fechado(s)" - -#~ msgid "Failed to generate cali G-code" -#~ msgstr "Falha ao gerar o G-code de calibração" - -# AI Translated -#~ msgid "Multiply" -#~ msgstr "Multiplicar" - -# AI Translated -#~ msgid " and " -#~ msgstr " e " - -# AI Translated -#~ msgid "cannot be mixed. Please select the same filament type." -#~ msgstr "não podem ser misturados. Selecione o mesmo tipo de filamento." - -# AI Translated -#~ msgid "An error occurred loading " -#~ msgstr "Ocorreu um erro ao carregar " - -# AI Translated -#~ msgid "/" -#~ msgstr "/" - -# AI Translated -#, c-format, boost-format -#~ msgid "Load %s to " -#~ msgstr "Carregar %s em " - -# AI Translated -#~ msgid "In the Design tab, draw a mate connector as a small face instead of the conventional disc with a roll quadrant. A face's orientation is read without being learned. Turn this off for the conventional CAD representation." -#~ msgstr "Na aba Design, desenha um conector de posicionamento como uma pequena face em vez do disco convencional com um quadrante de giro. A orientação de uma face é lida sem precisar ser aprendida. Desative para a representação CAD convencional." - -# AI Translated -#~ msgid "Expert" -#~ msgstr "Especialista" - -# AI Translated -#~ msgid "Simple" -#~ msgstr "Simples" - -# AI Translated -#~ msgid "Click points; click first / right-click to close the loop" -#~ msgstr "Clique nos pontos; clique no primeiro / clique com o botão direito para fechar o laço" - -# AI Translated -#~ msgid "X hint X" -#~ msgstr "Dica de X: X" - -# AI Translated -#~ msgid "X hint Y" -#~ msgstr "Dica de X: Y" - -# AI Translated -#~ msgid "X hint Z" -#~ msgstr "Dica de X: Z" - -# AI Translated -#~ msgid " — the eye suppresses this mate" -#~ msgstr " — o olho suprime este posicionamento" - -# AI Translated -#~ msgid "Del" -#~ msgstr "Excl." - -# AI Translated -#~ msgid "Sketch kept — Finish to commit it, Cancel to discard" -#~ msgstr "Esboço mantido — Terminar para confirmá-lo, Cancelar para descartá-lo" - -# AI Translated -#, c-format, boost-format -#~ msgid "Editing: Sketch %d · N = look normal to the plane · Finish or Cancel in the toolbar" -#~ msgstr "Editando: Esboço %d · N = olhar normal ao plano · Terminar ou Cancelar na barra de ferramentas" - -# AI Translated -#~ msgid "Editing sketch — drag a handle or click a quote to edit" -#~ msgstr "Editando esboço — arraste uma alça ou clique em uma cota para editar" - -# AI Translated -#, c-format, boost-format -#~ msgid "Showing %s actions" -#~ msgstr "Mostrando %s ações" - -# AI Translated -#, c-format, boost-format -#~ msgid "%s actions" -#~ msgstr "%s ações" - -# AI Translated -#, c-format, boost-format -#~ msgid "%s tabs" -#~ msgstr "%s abas" - -# AI Translated -#, c-format, boost-format -#~ msgid "%s matches" -#~ msgstr "%s correspondências" - -# AI Translated -#, c-format, boost-format -#~ msgid "%s · on %s" -#~ msgstr "%s · em %s" - -# AI Translated -#, c-format, boost-format -#~ msgid "Sketching on %s — pick a tool" -#~ msgstr "Esboçando em %s — escolha uma ferramenta" - -# AI Translated -#, c-format, boost-format -#~ msgid "" -#~ "Drawing on %s.\n" -#~ "Pick a tool, or press Menu for the list." -#~ msgstr "" -#~ "Desenhando em %s.\n" -#~ "Escolha uma ferramenta ou pressione Menu para ver a lista." - -# AI Translated -#~ msgid "Colour" -#~ msgstr "Cor" - -# AI Translated -#, c-format, boost-format -#~ msgid "Body %d " -#~ msgstr "Corpo %d " - -# AI Translated -#~ msgid "vertex selected — click again for the whole body" -#~ msgstr "vértice selecionado — clique novamente para o corpo inteiro" - -# AI Translated -#~ msgid "selected (whole body) — right-click for what applies to it" -#~ msgstr "selecionado (corpo inteiro) — clique com o botão direito para ver o que se aplica a ele" - -# AI Translated -#, c-format, boost-format -#~ msgid "face %d selected — right-click to push/pull it, or click again for the whole body" -#~ msgstr "face %d selecionada — clique com o botão direito para empurrá-la/puxá-la, ou clique novamente para o corpo inteiro" - -# AI Translated -#, c-format, boost-format -#~ msgid "edge %d selected — Fillet/Chamfer to dress it, or click again for the whole body" -#~ msgstr "aresta %d selecionada — Filete/Chanfro para arredondá-la ou chanfrá-la, ou clique novamente para o corpo inteiro" - -# AI Translated -#, c-format, boost-format -#~ msgid "placed (%.1f, %.1f, %.1f) mm — drag arrows to move, rings to rotate" -#~ msgstr "colocado em (%.1f, %.1f, %.1f) mm — arraste as setas para mover, os anéis para rotacionar" - -# AI Translated -#, c-format, boost-format -#~ msgid "%d degrees of freedom" -#~ msgstr "%d graus de liberdade" - -# AI Translated -#, c-format, boost-format -#~ msgid "Imported %d solid(s) — pick a face or edge, then Fillet / Cut / Shell to modify" -#~ msgstr "%d sólido(s) importado(s) — selecione uma face ou aresta e depois Filete / Cortar / Casca para modificar" - -# AI Translated -#, c-format, boost-format -#~ msgid "Imported as an open shell (not watertight): %d boundary edge(s), %d non-manifold edge(s) — %d triangles → %d faces. The source mesh has holes or duplicated geometry; boolean features may fail on it" -#~ msgstr "Importado como uma casca aberta (não estanque): %d aresta(s) de borda, %d aresta(s) não-manifold — %d triângulos → %d faces. A malha de origem tem furos ou geometria duplicada; recursos booleanos podem falhar nela" - -# AI Translated -#, c-format, boost-format -#~ msgid "Sketch added on %s — select it, then right-click to Extrude" -#~ msgstr "Esboço adicionado em %s — selecione-o e clique com o botão direito para extrudar" - -# AI Translated -#, c-format -#~ msgid "%zu interference pairs, worst %.2f mm³" -#~ msgstr "%zu pares com interferência, pior %.2f mm³" - -# AI Translated -#, c-format, boost-format -#~ msgid "the picked face of Body %d" -#~ msgstr "a face selecionada do Corpo %d" - -# AI Translated -#~ msgid "the picked face" -#~ msgstr "a face selecionada" - -# AI Translated -#, c-format, boost-format -#~ msgid "%d selected · Del removes them · Shift-click adds · double-click takes the whole loop" -#~ msgstr "%d selecionado(s) · Del os remove · Shift+clique adiciona · clique duplo seleciona o laço inteiro" - -# AI Translated -#, c-format, boost-format -#~ msgid "Section view — showing the %s half" -#~ msgstr "Vista de seção — mostrando a metade %s" - -# AI Translated -#~ msgid "upper" -#~ msgstr "superior" - -# AI Translated -#~ msgid "lower" -#~ msgstr "inferior" - -# AI Translated -#, c-format -#~ msgid "Redo (%zu more)" -#~ msgstr "Refazer (mais %zu)" - -# AI Translated -#, c-format -#~ msgid "Undo (%zu more)" -#~ msgstr "Desfazer (mais %zu)" - #~ msgid "Section View" #~ msgstr "Vista de Seção" @@ -32617,10 +31149,6 @@ msgstr "" #~ msgid "Switch Camera View" #~ msgstr "Trocar Vista da Câmera" -# AI Translated -#~ msgid "Renders cast shadows on the plate, other objects, and each object onto itself in realistic view." -#~ msgstr "Renderiza sombras projetadas na mesa, em outros objetos e de cada objeto sobre si mesmo na visualização realista." - #~ msgid "Support/Color Painting: adjust section position" #~ msgstr "Suporte/Pintura em cores: ajustar a posição da seção" @@ -32630,14 +31158,6 @@ msgstr "" #~ msgid "Displays current viewport FPS in the top-right corner." #~ msgstr "Exibe o FPS atual da janela de visualização no canto superior direito." -# AI Translated -#~ msgid "Publish 3MF Wiki" -#~ msgstr "Documentação de Publicar 3MF" - -# AI Translated -#~ msgid "Publish 3MF Video Guide" -#~ msgstr "Guia em vídeo de Publicar 3MF" - #~ msgid "Toolbar" #~ msgstr "Barra de Ferramentas" @@ -32722,18 +31242,6 @@ msgstr "" #~ msgid " minimum drying temperature is " #~ msgstr " temperatura mínima de secagem é " -# AI Translated -#~ msgid "needs" -#~ msgstr "precisa de" - -# AI Translated -#~ msgid "not enabled" -#~ msgstr "não ativado" - -# AI Translated -#~ msgid "material not published" -#~ msgstr "material não publicado" - #~ msgid "Select the language" #~ msgstr "Selecione o idioma" @@ -32918,7 +31426,7 @@ msgstr "" #~ msgstr "O conteúdo da predefinição é muito grande para ser sincronizado com a nuvem (excede 1 MB). Reduza o tamanho da predefinição removendo configurações personalizadas ou use-o apenas localmente." #~ msgid "Enable adaptive pressure advance for overhangs (beta)" -#~ msgstr "Habilitar pressure advance adaptativo para saliências (beta)" +#~ msgstr "Habilitar Pressure Advance adaptativo para saliências (beta)" #~ msgid "Pressure advance for bridges" #~ msgstr "Pressure advance para pontes" @@ -32928,7 +31436,7 @@ msgstr "" #~ "\n" #~ "A lower PA value when printing bridges helps reduce the appearance of slight under extrusion immediately after bridges. This is caused by the pressure drop in the nozzle when printing in the air and a lower PA helps counteract this." #~ msgstr "" -#~ "Valor do pressure advance para pontes. Defina como 0 para desabilitar.\n" +#~ "Valor do Pressure Advance para pontes. Defina como 0 para desabilitar.\n" #~ "\n" #~ "Um valor de PA mais baixo ao imprimir pontes ajuda a reduzir a aparência de leve subextrusão imediatamente após as pontes. Isso é causado pela queda de pressão no bico ao imprimir no ar e um PA mais baixo ajuda a neutralizar isso." @@ -33137,7 +31645,7 @@ msgstr "" #~ msgstr "" #~ "Habilite esta opção para desacelerar a impressão em áreas onde os perímetros podem ter se curvado para cima. Por exemplo, uma desaceleração adicional será aplicada ao imprimir saliências em cantos afiados, como a parte frontal do casco Benchy, reduzindo a curvatura que se acumula por várias camadas.\n" #~ "\n" -#~ "Geralmente, é recomendável ter esta opção ativada, a menos que o resfriamento da impressora seja potente o suficiente ou a velocidade de impressão lenta o suficiente para que a curvatura do perímetro não aconteça. Se estiver imprimindo com uma alta velocidade de perímetro externo, este parâmetro pode introduzir pequenos artefatos ao desacelerar devido à grande variação nas velocidades de impressão. Se você notar artefatos, certifique-se de que seu pressure advance esteja ajustado corretamente.\n" +#~ "Geralmente, é recomendável ter esta opção ativada, a menos que o resfriamento da impressora seja potente o suficiente ou a velocidade de impressão lenta o suficiente para que a curvatura do perímetro não aconteça. Se estiver imprimindo com uma alta velocidade de perímetro externo, este parâmetro pode introduzir pequenos artefatos ao desacelerar devido à grande variação nas velocidades de impressão. Se você notar artefatos, certifique-se de que seu Pressure Advance esteja ajustado corretamente.\n" #~ "\n" #~ "Nota: quando esta opção estiver habilitada, os perímetros de saliência são tratados como saliências, o que significa que a velocidade de saliência é aplicada mesmo se o perímetro de saliência for parte de uma ponte. Por exemplo, quando os perímetros estiverem 100% salientes, sem nenhuma parede apoiando-os por baixo, a velocidade de saliência de 100% será aplicada." @@ -33424,7 +31932,7 @@ msgstr "" #~ msgstr "A posição da torre de purga ultrapassou os limites da placa de impressão e foi reposicionada para a borda válida mais próxima." #~ msgid "Enabling both precise Z height and the prime tower may cause the size of prime tower to increase. Do you still want to enable?" -#~ msgstr "Habilitar a altura Z precisa e a torre de preparação juntas pode causar o aumento de tamanho da torre de purga. Deseja habilitar mesmo assim?" +#~ msgstr "Habilitar a altura Z precisa e a torre de purga juntas pode causar o aumento de tamanho da torre de purga. Deseja habilitar mesmo assim?" #~ msgid "Missing BambuSource component registered for media playing! Please re-install BambuStudio or seek after-sales help." #~ msgstr "Componente BambuSource ausente registrado para reprodução de mídia! Reinstale o Bambu Studio ou procure ajuda pós-venda." @@ -34351,7 +32859,7 @@ msgstr "" #~ msgstr "Diâmetros de bico diferentes e diâmetros de filamento diferentes não são permitidos quando a torre de purga está ativa." #~ msgid "Ooze prevention is currently not supported with the prime tower enabled." -#~ msgstr "A prevenção de vazamento atualmente não é suportada com a torre de prepao ativa." +#~ msgstr "A prevenção de vazamento atualmente não é suportada com a torre de purga ativa." #~ msgid "Interlocking depth of a segmented region. Zero disables this feature." #~ msgstr "Profundidade de intertravamento de uma região segmentada. Zero desativa essa funcionalidade." diff --git a/resources/images/score_star_outline.svg b/resources/images/score_star_outline.svg new file mode 100644 index 0000000000..eb52be9e82 --- /dev/null +++ b/resources/images/score_star_outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/profiles/BBL.json b/resources/profiles/BBL.json index 6c6567d37d..da56f36ccd 100644 --- a/resources/profiles/BBL.json +++ b/resources/profiles/BBL.json @@ -1,7 +1,7 @@ { "name": "Bambulab", "url": "http://www.bambulab.com/Parameters/vendor/BBL.json", - "version": "02.08.00.14", + "version": "02.08.00.15", "force_update": "0", "description": "the initial version of BBL configurations", "machine_model_list": [ diff --git a/resources/profiles/BBL/machine/Bambu Lab H2C 0.4 nozzle template machine_start_gcode.json b/resources/profiles/BBL/machine/Bambu Lab H2C 0.4 nozzle template machine_start_gcode.json index 5132ccee2e..8d620f168d 100644 --- a/resources/profiles/BBL/machine/Bambu Lab H2C 0.4 nozzle template machine_start_gcode.json +++ b/resources/profiles/BBL/machine/Bambu Lab H2C 0.4 nozzle template machine_start_gcode.json @@ -2,5 +2,5 @@ "type": "machine", "name": "Bambu Lab H2C 0.4 nozzle template machine_start_gcode", "instantiation": "false", - "machine_start_gcode": ";===== machine: H2C =========================\n;===== date: 20260608 =====================\n\n;M1002 set_flag extrude_cali_flag=1\n;M1002 set_flag g29_before_print_flag=1\n;M1002 set_flag auto_cali_toolhead_offset_flag=1\n;M1002 set_flag build_plate_detect_flag=1\n\nM993 A0 B0 C0 ; nozzle cam detection not allowed.\n\nM400\n;M73 P99\n\nM960 S10 P1 ; ext fan led\n\n;=====printer start sound ===================\nM17\nM400 S1\nM1006 S1\nM1006 A53 B9 L99 C53 D9 M99 E53 F9 N99\nM1006 A56 B9 L99 C56 D9 M99 E56 F9 N99\nM1006 A61 B9 L99 C61 D9 M99 E61 F9 N99\nM1006 A53 B9 L99 C53 D9 M99 E53 F9 N99\nM1006 A56 B9 L99 C56 D9 M99 E56 F9 N99\nM1006 A61 B18 L99 C61 D18 M99 E61 F18 N99\nM1006 W\n;=====printer start sound ===================\n\n;===== reset machine status =================\nM204 S10000\nM630 S0 P0\n\nG90\nM17 D ; reset motor current to default\nM960 S5 P1 ; turn on logo lamp\nG90\nM1002 set_gcode_claim_speed_level 5 ;Reset speed level\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\nM73.2 R1.0 ;Reset left time magnitude\nG29.1 Z{+0.0} ; clear z-trim value first\nM983.1 M1\nM901 D4\nM481 S0 ; turn off cutter pos comp\nG28.140 D0; reset pre-extrude z pos\n;===== reset machine status =================\n\nM620 M ;enable remap\nM620 N ;enable hotend remap\nM620 T0 ;print tmpr\nM620 T1 ;sn -> pos\nM620 T2 ;filament_id\n\n;===== start to heat heatbed & hotend==========\n\n M104 O-80 A\n M140 D[bed_temperature_initial_layer_single]\n\n;===== start to heat heatbead & hotend==========\n\n;===== avoid end stop =================\nG91\nG380 S2 Z42 F1200\nG380 S2 Z-12 F1200\nG90\n;===== avoid end stop =================\n\n;==== set airduct mode ====\n\n{if (overall_chamber_temperature >= 40)}\n\n M145 P1 ; set airduct mode to heating mode for heating\n M106 P2 S0 ; turn off auxiliary fan\n M106 P3 S0 ; turn off chamber fan\n\n{else}\n M145 P0 ; set airduct mode to cooling mode for cooling\n M106 P2 S178 ; turn on auxiliary fan for cooling\n M106 P3 S127 ; turn on chamber fan for cooling\n\n M1002 gcode_claim_action : 29\n M191 S0 ; wait for chamber temp\n M106 P2 S0 ; turn off auxiliary fan\n {if (min_vitrification_temperature <= 50)}\n {if (nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2)}\n M142 P1 R30 S35 T40 U0.3 V0.5 W0.8 O40 ; set PLA/TPU ND0.2 chamber autocooling\n {else}\n M142 P1 R30 S40 T45 U0.3 V0.5 W0.8 O45; set PLA/TPU ND0.4 chamber autocooling\n {endif}\n {else}\n {if (!is_all_bbl_filament)}\n M142 P1 R35 S40 T45 U0.3 V0.5 W0.8 O45 L1 ; set third-party PETG chamber autocooling\n {else}\n {if (nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2)}\n M142 P1 R35 S45 T50 U0.3 V0.5 W0.8 O50 L1 ; set PETG ND0.2 chamber autocooling\n {else}\n M142 P1 R35 S50 T55 U0.3 V0.5 W0.8 O55 L1 ; set PETG ND0.4 chamber autocooling\n {endif}\n {endif}\n {endif}\n{if(cooling_filter_enabled)}\nM145.2 P0 F0\n{else}\nM145.2 P0 F1\n{endif}\n{endif}\n;==== set airduct mode ====\n\n\n;====== cog noise reduction=================\nM982.2 S1 ; turn on cog noise reduction\n\n;===== first homing start =====\nM1002 gcode_claim_action : 13\n\nM640 S\nM640.1 R\nM641\nG28 X T300\nT1000\n\nG150.3\nM972 S24 P0 T2000 ; live-view camera foolproof\nM972 S46 P0 T5000 ; vortek anti-collision\nM640.1 S\nM640.4\n{if (first_non_support_filaments[0] != -1)}\nM640 S\nM640.8 T A{first_non_support_filaments[0]} H{first_non_support_hotend[0]}\nM640.7 L\nM641\n{endif}\n\n\n{if wipe_tower_center_pos_valid}\n M620.14 X[wipe_tower_center_pos_x] Y[wipe_tower_center_pos_y]\n{else}\n M620.14 X95.5 Y336\n{endif}\n\n\nG150.1 F18000 ; wipe mouth to avoid filament stick to heatbed\nG150.3 F18000\nM400 P200\n\nM1002 gcode_claim_action : 74 ; Heatbed surface foreign object detection\nM104 S0\n{if curr_bed_type==\"Textured PEI Plate\"}\nM972 S26 P0 C0\n{else}\nM972 S36 P0 C0 X1\n{endif}\nM972 S35 P0 C0\n\nM972 S41 P0 T5000 ; trash can anti-collision\n\nM1009 Q1 L1\nG91\nG380 S2 Z30 F1200 ; lower heatbed to move toolhead\nG90\nG1 X175 Y160 F30000\nG28 Z P0 T250\nM1009 Q1 L0\n\n\n;===== first homing end =====\n\n;===== detection start =====\n\nM1002 judge_flag build_plate_detect_flag\nM104 S0\nM622 S1\n ;M1002 gcode_claim_action : 11 ; Indentifying build plate type\n M972 S19 P0 C0 ; heatbed presence detection\n M972 S31 P0 T5000 ; toolhead camera dirty detection\n ;M1002 gcode_claim_action : 73 ; Build plate alignment detection\n M972 S34 P0 T5000 ; heatbed plate offset detection\nM623\n\nM1002 gcode_claim_action : 72 ; Hotend Type Detection\nT1001\nM972 S14 P0 T5000 ; nozzle type detection\n\nM104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]} T{filament_map[initial_no_support_filament_id] % 2} ; rise temp in advance\n\nG151 P{filament_map[initial_no_support_filament_id] % 2} M ; plug the heat nozzle\n\n{if max_print_z >= 145}\nM1002 gcode_claim_action : 75 ; Heatbed underside foreign object detection\nG3811 Z{max_print_z} ; Detect obstacles at the bottom of the heated bed\n{endif}\n\n;===== detection end =====\n\nM400\n;M73 P99\n\n;===== prepare print temperature and material ==========\nM400\nM211 X0 Y0 Z0 ;turn off soft endstop\nM975 S1 ; turn on input shaping\n\nG29.2 S0 ; avoid invalid abl data\n\n{if ((filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG\")) && (nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2)}\nM620.10 A0 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{else}\nM620.10 A0 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60*0.8} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60*0.8} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{endif}\n\nM620.11 P1 I[initial_no_support_filament_id] B[initial_no_support_hotend] E0\n\n{if long_retraction_when_ec }\nM620.11 K1 I[initial_no_support_filament_id] B[initial_no_support_hotend] R{retraction_distance_when_ec} F{max((flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60), 200)}\n{else}\nM620.11 K0 I[initial_no_support_filament_id] B[initial_no_support_hotend] R0\n{endif}\n\nM628 S1\n{if filament_type[initial_no_support_filament_id] == \"TPU\"}\n M620.11 S0 L0 I[initial_no_support_filament_id] B[initial_no_support_hotend] E-{retraction_distances_when_cut[initial_no_support_filament_id]} F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60}\n{else}\n{if (filament_type[initial_no_support_filament_id] == \"PA\") || (filament_type[initial_no_support_filament_id] == \"PA-GF\")}\n M620.11 S1 L0 I[initial_no_support_filament_id] B[initial_no_support_hotend] R4 D2 E-{retraction_distances_when_cut[initial_no_support_filament_id]} F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60}\n{else}\n M620.11 S1 L0 I[initial_no_support_filament_id] B[initial_no_support_hotend] R10 D8 E-{retraction_distances_when_cut[initial_no_support_filament_id]} F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60}\n{endif}\n{endif}\nM629\n\nM620 S[initial_no_support_filament_id]A H[initial_no_support_hotend] ; switch material if AMS exist\nM1002 gcode_claim_action : 4\nM1002 set_filament_type:UNKNOWN\nM400\nT[initial_no_support_filament_id] H[initial_no_support_hotend]\nM400\nM628 S0\nM629\nM400\nM1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\nM621 S[initial_no_support_filament_id]A\n\nM104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\nM400\nM106 P1 S0\n\nG91\nG1 Y-16 F60000\nG90\n\nG29.2 S1\n;===== prepare print temperature and material ==========\n\n\nM400\n;M73 P99\n\n;===== xy ofst cali start =====\n\nM1002 judge_flag auto_cali_toolhead_offset_flag\n\nM622 J2\n M1002 gcode_claim_action : 54\n M190 D[bed_temperature_initial_layer_single]\n\n M1002 gcode_claim_action : 39\n M620 D[initial_no_support_hotend]\n G383.3 U140 L{initial_no_support_filament_id}\nM623\n\nM622 J1\n M1002 gcode_claim_action : 54\n M190 D[bed_temperature_initial_layer_single]\n\n M1002 gcode_claim_action : 39\n M620 D[initial_no_support_hotend]\n G383 O2 U140 L{initial_no_support_filament_id}\nM623\n\n;===== xy ofst cali end =====\n\n;===== start to heat heatbed & hotend==========\n\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n G150.3\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M140 S[bed_temperature_initial_layer_single]\n\n ;===== set chamber temperature ==========\n {if (overall_chamber_temperature >= 40)}\n M145 P1 ; set airduct mode to heating mode\n M141 S[overall_chamber_temperature] ; Let Chamber begin to heat\n {endif}\n ;===== set chamber temperature ==========\n\n;===== start to heat heatbead & hotend==========\n\n\n\nM400\n;M73 P99\n\n;===== auto extrude cali start =========================\nM975 S1\nM1002 judge_flag extrude_cali_flag\n\nM622 J0\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\nM623\n\nM622 J1\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\nM623\n\nM622 J2\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\nM623\n\n;===== auto extrude cali end =========================\n\n{if filament_type[initial_no_support_filament_id] == \"TPU\"}\n G150.2\n G150.1\n G150.2\n G150.1\n G150.2\n G150.1\n{else}\n G150.3\n M106 P1 S0\n M400 S2\n M109 S{nozzle_temperature[initial_no_support_filament_id]} ; wait tmpr to extrude\n M83\n {if(nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.8)}\n G1 E60 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4053*60}\n {else}\n G1 E45 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4053*60}\n {endif}\n G1 E-3 F1800\n M400 P500\n G150.2\n G150.1\n{endif}\n\nG91\nG1 Y-16 F12000 ; move away from the trash bin\nG90\n\nM400\n;M73 P99\n\n;===== wipe right nozzle start =====\n\nM1002 gcode_claim_action : 14\n G150 T{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n {if (overall_chamber_temperature >= 40)}\n G150 T{nozzle_temperature_initial_layer[initial_no_support_filament_id] - 80}\n {endif}\nM106 S255 ; turn on fan to cool the nozzle\n\n;===== wipe left nozzle end =====\n\nM400\n;M73 P99\n\nM1002 judge_flag auto_cali_toolhead_offset_flag\nM622 J0\n G91\n G1 Z5 F1200\n G90\n M1012.7\n G383.7 U140 J0\nM623\n\n{if (overall_chamber_temperature >= 40)}\n M1002 gcode_claim_action : 49\n M191 S[overall_chamber_temperature] ; wait for chamber temp\n{endif}\n\nM400\n;M73 P99\n\n;===== bed leveling ==================================\n\nM1002 judge_flag g29_before_print_flag\n\nM190 S[bed_temperature_initial_layer_single]; ensure bed temp\nM109 S140 A\nM106 S0 ; turn off fan , too noisy\n\nG91\nG1 Z10 F1200\n{if (first_non_support_filaments[0] != -1) && filament_type[initial_no_support_filament_id] != \"TPU\"}\nM640 S\nM640.7 U\nM640.7 L\nM640.2 R1\nM641\n{endif}\nG90\nG1 X175 Y160 F30000\n\nM622 J1\n M1002 gcode_claim_action : 1\n G29.20 A3\n G29 A1 O X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]} R\n M400\nM623\n\nM622 J2\n M1002 gcode_claim_action : 1\n {if has_tpu_in_first_layer}\n G29.20 A3\n G29 A1 O X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]} R\n {else}\n G29.20 A4\n G29 A2 O X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]} R\n {endif}\n M400\nM623\n\nM622 J0\n G28 R\nM623\n\n;===== bed leveling end ================================\n\nG39.1 ; cali nozzle wrapped detection pos\n\nG90\nG1 Z5 F1200\nG1 X270 Y-0.5 F60000\nG28.140 S0 ; cali pre-extrude z pos\n\n;============switch again==================\nM211 X0 Y0 Z0 ;turn off soft endstop\nG91\nG1 Z6 F1200\nG90\nM1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\nM620 S[initial_no_support_filament_id]A H[initial_no_support_hotend]\nM400\nT[initial_no_support_filament_id] H[initial_no_support_hotend]\nM400\nM628 S0\nM629\nM400\nM621 S[initial_no_support_filament_id]A\n\n;============switch again==================\n\nM400\n;M73 P99\n\nM141 S[overall_chamber_temperature]\n\n;===== mech mode sweep start =====\n M1002 gcode_claim_action : 3\n\n G90\n G1 Z5 F1200\n G1 X165 Y160 F20000\n M400 P200\n\n M970.3 Q1 A5 K0 O1\n M974 Q1 S2 P0\n\n M970.3 Q0 A5 K0 O1\n M974 Q0 S2 P0\n\n M970.2 Q2 K0 W38 Z0.01\n M974 Q2 S2 P0\n\n M975 S1\n;===== mech mode sweep end =====\n\n;===== wait temperature reaching the reference value =======\n\nM140 S[bed_temperature_initial_layer_single]\nM190 S[bed_temperature_initial_layer_single]\n\n ;========turn off light and fans =============\n M960 S1 P0 ; turn off laser\n M960 S2 P0 ; turn off laser\n M106 S0 ; turn off fan\n M106 P2 S0 ; turn off big fan\n ;==== set ext toodhead cooling fan ====\n {if (min_vitrification_temperature <= 50)}\n M106 P9 S255\n {endif}\n ;============set motor current==================\n M400 S1\n\n;===== wait temperature reaching the reference value =======\n\nM400\n;M73 P99\n\n;===== for Textured PEI Plate , lower the nozzle as the nozzle was touching topmost of the texture when homing ==\n {if curr_bed_type==\"Textured PEI Plate\"}\n {if nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2}\n G29.1 Z{-0.01} ; for Textured PEI Plate\n {else}\n G29.1 Z{-0.02} ; for Textured PEI Plate\n {endif}\n {else}\n {if nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2}\n G29.1 Z{0.01}\n {endif}\n {endif}\nG150.1\n\nM975 S1 ; turn on mech mode supression\nM983.4 S1 ; turn on deformation compensation\nG29.2 S1 ; turn on pos comp\nG29.7 S1\n\nG90\nG1 Z5 F1200\nG1 Y295 F30000\nG1 Y265 F18000\n\n;===== nozzle load line ===============================\n G29.2 S1 ; ensure z comp turn on\n G90\n M83\n G1 Z5 F1200\n G1 X270 Y-0.5 F60000\n G28.14 R0\n G29.2 S0\n G91\n G1 Z0.8 F1200\n G90\n G1 X250 F60000\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n\n ========== record data ==========\n M1026\n M1012.8\n M1012.9\n G29.9\n ========== record data ==========\n\n M400 P50\n M500 D1\n M400 S3\n M109 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M83\n{if nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.8}\n G1 E5 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4053*60}\n{endif}\n{if (filament_type[initial_no_support_filament_id] == \"TPU\")}\n G1 E5 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4053*60}\n{endif}\n G1 E5 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4053*60}\n G1 X290 E10 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4053*60}\n G91\n G3 Z0.4 I1.217 J0 P1 F60000\n G90\n M83\n G29.2 S1 ; ensure z comp turn on\n;===== noozle load line end ===========================\n\nM400\n;M73 P99\n\nM993 A1 B1 C1 ; nozzle cam detection allowed.\n\n{if (filament_type[initial_no_support_filament_id] == \"TPU\")}\nM1015.3 S1;enable tpu clog detect\n{else}\nM1015.3 S0;disable tpu clog detect\n{endif}\n\n{if (filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PETG\")\n || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG-CF\")}\nM1015.4 S1 K1 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;enable E air printing detect\n{else}\nM1015.4 S0 K0 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;disable E air printing detect\n{endif}\n\nM620.6 I[initial_no_support_filament_id] H[initial_no_support_hotend] W1 ;enable ams air printing detect\nM620 O1\n\nM211 Z1\nG29.99\nM1002 gcode_claim_action : 0\nM400\n" + "machine_start_gcode": ";===== machine: H2C =========================\n;===== date: 20260608 =====================\n\n;M1002 set_flag extrude_cali_flag=1\n;M1002 set_flag g29_before_print_flag=1\n;M1002 set_flag auto_cali_toolhead_offset_flag=1\n;M1002 set_flag build_plate_detect_flag=1\n\nM993 A0 B0 C0 ; nozzle cam detection not allowed.\n\nM400\n;M73 P99\n\nM960 S10 P1 ; ext fan led\n\n;=====printer start sound ===================\nM17\nM400 S1\nM1006 S1\nM1006 A53 B9 L99 C53 D9 M99 E53 F9 N99\nM1006 A56 B9 L99 C56 D9 M99 E56 F9 N99\nM1006 A61 B9 L99 C61 D9 M99 E61 F9 N99\nM1006 A53 B9 L99 C53 D9 M99 E53 F9 N99\nM1006 A56 B9 L99 C56 D9 M99 E56 F9 N99\nM1006 A61 B18 L99 C61 D18 M99 E61 F18 N99\nM1006 W\n;=====printer start sound ===================\n\n;===== reset machine status =================\nM204 S10000\nM630 S0 P0\n\nG90\nM17 D ; reset motor current to default\nM960 S5 P1 ; turn on logo lamp\nG90\nM1002 set_gcode_claim_speed_level 5 ;Reset speed level\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\nM73.2 R1.0 ;Reset left time magnitude\nG29.1 Z{+0.0} ; clear z-trim value first\nM983.1 M1\nM901 D4\nM481 S0 ; turn off cutter pos comp\nG28.140 D0; reset pre-extrude z pos\n;===== reset machine status =================\n\nM620 M ;enable remap\nM620 N ;enable hotend remap\nM620 T0 ;print tmpr\nM620 T1 ;sn -> pos\nM620 T2 ;filament_id\n\n;===== start to heat heatbed & hotend==========\n\n M104 O-80 A\n M140 D[bed_temperature_initial_layer_single]\n\n;===== start to heat heatbead & hotend==========\n\n;===== avoid end stop =================\nG91\nG380 S2 Z42 F1200\nG380 S2 Z-12 F1200\nG90\n;===== avoid end stop =================\n\n;==== set airduct mode ====\n\n{if (overall_chamber_temperature >= 40)}\n\n M145 P1 ; set airduct mode to heating mode for heating\n M106 P2 S0 ; turn off auxiliary fan\n M106 P3 S0 ; turn off chamber fan\n\n{else}\n M145 P0 ; set airduct mode to cooling mode for cooling\n M106 P2 S178 ; turn on auxiliary fan for cooling\n M106 P3 S127 ; turn on chamber fan for cooling\n\n M1002 gcode_claim_action : 29\n M191 S0 ; wait for chamber temp\n M106 P2 S0 ; turn off auxiliary fan\n {if (min_vitrification_temperature <= 50)}\n {if (nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2)}\n M142 P1 R30 S35 T40 U0.3 V0.5 W0.8 O40 ; set PLA/TPU ND0.2 chamber autocooling\n {else}\n M142 P1 R30 S40 T45 U0.3 V0.5 W0.8 O45; set PLA/TPU ND0.4 chamber autocooling\n {endif}\n {else}\n {if (!is_all_bbl_filament)}\n M142 P1 R35 S40 T45 U0.3 V0.5 W0.8 O45 L1 ; set third-party PETG chamber autocooling\n {else}\n {if (nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2)}\n M142 P1 R35 S45 T50 U0.3 V0.5 W0.8 O50 L1 ; set PETG ND0.2 chamber autocooling\n {else}\n M142 P1 R35 S50 T55 U0.3 V0.5 W0.8 O55 L1 ; set PETG ND0.4 chamber autocooling\n {endif}\n {endif}\n {endif}\n{if(cooling_filter_enabled)}\nM145.2 P0 F0\n{else}\nM145.2 P0 F1\n{endif}\n{endif}\n;==== set airduct mode ====\n\n\n;====== cog noise reduction=================\nM982.2 S1 ; turn on cog noise reduction\n\n;===== first homing start =====\nM1002 gcode_claim_action : 13\n\nM640 S\nM640.1 R\nM641\nG28 X T300\nT1000\n\nG150.3\nM972 S24 P0 T2000 ; live-view camera foolproof\nM972 S46 P0 T5000 ; vortek anti-collision\nM640.1 S\nM640.4\n{if (first_non_support_filaments[0] != -1)}\nM640 S\nM640.8 T A{first_non_support_filaments[0]} H{first_non_support_hotend[0]}\nM640.7 L\nM641\n{endif}\n\n\n{if wipe_tower_center_pos_valid}\n M620.14 X[wipe_tower_center_pos_x] Y[wipe_tower_center_pos_y]\n{else}\n M620.14 X95.5 Y336\n{endif}\n\n\nG150.1 F18000 ; wipe mouth to avoid filament stick to heatbed\nG150.3 F18000\nM400 P200\n\nM1002 gcode_claim_action : 74 ; Heatbed surface foreign object detection\nM104 S0\n{if curr_bed_type==\"Textured PEI Plate\"}\nM972 S26 P0 C0\n{else}\nM972 S36 P0 C0 X1\n{endif}\nM972 S35 P0 C0\n\nM972 S41 P0 T5000 ; trash can anti-collision\n\nM1009 Q1 L1\nG91\nG380 S2 Z30 F1200 ; lower heatbed to move toolhead\nG90\nG1 X175 Y160 F30000\nG28 Z P0 T250\nM1009 Q1 L0\n\n\n;===== first homing end =====\n\n;===== detection start =====\n\nM1002 judge_flag build_plate_detect_flag\nM104 S0\nM622 S1\n ;M1002 gcode_claim_action : 11 ; Indentifying build plate type\n M972 S19 P0 C0 ; heatbed presence detection\n M972 S31 P0 T5000 ; toolhead camera dirty detection\n ;M1002 gcode_claim_action : 73 ; Build plate alignment detection\n M972 S34 P0 T5000 ; heatbed plate offset detection\nM623\n\nM1002 gcode_claim_action : 72 ; Hotend Type Detection\nT1001\nM972 S14 P0 T5000 ; nozzle type detection\n\nM104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]} T{filament_map[initial_no_support_filament_id] % 2} ; rise temp in advance\n\nG151 P{filament_map[initial_no_support_filament_id] % 2} M ; plug the heat nozzle\n\n{if max_print_z >= 145}\nM1002 gcode_claim_action : 75 ; Heatbed underside foreign object detection\nG3811 Z{max_print_z} ; Detect obstacles at the bottom of the heated bed\n{endif}\n\n;===== detection end =====\n\nM400\n;M73 P99\n\n;===== prepare print temperature and material ==========\nM400\nM211 X0 Y0 Z0 ;turn off soft endstop\nM975 S1 ; turn on input shaping\n\nG29.2 S0 ; avoid invalid abl data\n\n{if ((filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG\")) && (nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2)}\nM620.10 A0 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{else}\nM620.10 A0 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60*0.8} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60*0.8} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{endif}\n\nM620.11 P1 I[initial_no_support_filament_id] B[initial_no_support_hotend] E0\n\n{if long_retraction_when_ec }\nM620.11 K1 I[initial_no_support_filament_id] B[initial_no_support_hotend] R{retraction_distance_when_ec} F{max((flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60), 200)}\n{else}\nM620.11 K0 I[initial_no_support_filament_id] B[initial_no_support_hotend] R0\n{endif}\n\nM628 S1\n{if filament_type[initial_no_support_filament_id] == \"TPU\"}\n M620.11 S0 L0 I[initial_no_support_filament_id] B[initial_no_support_hotend] E-{retraction_distances_when_cut[initial_no_support_filament_id]} F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60}\n{else}\n{if (filament_type[initial_no_support_filament_id] == \"PA\") || (filament_type[initial_no_support_filament_id] == \"PA-GF\")}\n M620.11 S1 L0 I[initial_no_support_filament_id] B[initial_no_support_hotend] R4 D2 E-{retraction_distances_when_cut[initial_no_support_filament_id]} F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60}\n{else}\n M620.11 S1 L0 I[initial_no_support_filament_id] B[initial_no_support_hotend] R10 D8 E-{retraction_distances_when_cut[initial_no_support_filament_id]} F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60}\n{endif}\n{endif}\nM629\n\nM620 S[initial_no_support_filament_id]A H[initial_no_support_hotend] ; switch material if AMS exist\nM1002 gcode_claim_action : 4\nM1002 set_filament_type:UNKNOWN\nM400\nT[initial_no_support_filament_id] H[initial_no_support_hotend]\nM400\nM628 S0\nM629\nM400\nM1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\nM621 S[initial_no_support_filament_id]A\n\nM104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\nM400\nM106 P1 S0\n\nG91\nG1 Y-16 F60000\nG90\n\nG29.2 S1\n;===== prepare print temperature and material ==========\n\n\nM400\n;M73 P99\n\n;===== xy ofst cali start =====\n\nM1002 judge_flag auto_cali_toolhead_offset_flag\n\nM622 J2\n M1002 gcode_claim_action : 54\n M190 D[bed_temperature_initial_layer_single]\n\n M1002 gcode_claim_action : 39\n M620 D[initial_no_support_hotend]\n G383.3 U140 L{initial_no_support_filament_id}\nM623\n\nM622 J1\n M1002 gcode_claim_action : 54\n M190 D[bed_temperature_initial_layer_single]\n\n M1002 gcode_claim_action : 39\n M620 D[initial_no_support_hotend]\n G383 O2 U140 L{initial_no_support_filament_id}\nM623\n\n;===== xy ofst cali end =====\n\n;===== start to heat heatbed & hotend==========\n\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n G150.3\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M140 S[bed_temperature_initial_layer_single]\n\n ;===== set chamber temperature ==========\n {if (overall_chamber_temperature >= 40)}\n M145 P1 ; set airduct mode to heating mode\n M141 S[overall_chamber_temperature] ; Let Chamber begin to heat\n {endif}\n ;===== set chamber temperature ==========\n\n;===== start to heat heatbead & hotend==========\n\n\n\nM400\n;M73 P99\n\n;===== auto extrude cali start =========================\nM975 S1\nM1002 judge_flag extrude_cali_flag\n\nM622 J0\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\nM623\n\nM622 J1\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\nM623\n\nM622 J2\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\nM623\n\n;===== auto extrude cali end =========================\n\n{if filament_type[initial_no_support_filament_id] == \"TPU\"}\n G150.2\n G150.1\n G150.2\n G150.1\n G150.2\n G150.1\n{else}\n G150.3\n M106 P1 S0\n M400 S2\n M109 S{nozzle_temperature[initial_no_support_filament_id]} ; wait tmpr to extrude\n M83\n {if(nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.8)}\n G1 E60 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4053*60}\n {else}\n G1 E45 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4053*60}\n {endif}\n G1 E-3 F1800\n M400 P500\n G150.2\n G150.1\n{endif}\n\nG91\nG1 Y-16 F12000 ; move away from the trash bin\nG90\n\nM400\n;M73 P99\n\n;===== wipe right nozzle start =====\n\nM1002 gcode_claim_action : 14\n G150 T{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n {if (overall_chamber_temperature >= 40)}\n G150 T{nozzle_temperature_initial_layer[initial_no_support_filament_id] - 80}\n {endif}\nM106 S255 ; turn on fan to cool the nozzle\n\n;===== wipe left nozzle end =====\n\nM400\n;M73 P99\n\nM1002 judge_flag auto_cali_toolhead_offset_flag\nM622 J0\n G91\n G1 Z5 F1200\n G90\n M1012.7\n G383.7 U140 J0\nM623\n\n{if (overall_chamber_temperature >= 40)}\n M1002 gcode_claim_action : 49\n M191 S[overall_chamber_temperature] ; wait for chamber temp\n{endif}\n\nM400\n;M73 P99\n\n;===== bed leveling ==================================\n\nM1002 judge_flag g29_before_print_flag\n\nM190 S[bed_temperature_initial_layer_single]; ensure bed temp\nM109 S140 A\nM106 S0 ; turn off fan , too noisy\n\nG91\nG1 Z10 F1200\n{if (first_non_support_filaments[0] != -1) && filament_type[initial_no_support_filament_id] != \"TPU\"}\nM640 S\nM640.7 U\nM640.7 L\nM640.2 R1\nM641\n{endif}\nG90\nG1 X175 Y160 F30000\n\nM622 J1\n M1002 gcode_claim_action : 1\n G29.20 A3\n G29 A1 O X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]} R\n M400\nM623\n\nM622 J2\n M1002 gcode_claim_action : 1\n {if has_tpu_in_first_layer}\n G29.20 A3\n G29 A1 O X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]} R\n {else}\n G29.20 A4\n G29 A2 O X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]} R\n {endif}\n M400\nM623\n\nM622 J0\n G28 R\nM623\n\n;===== bed leveling end ================================\n\nG39.1 ; cali nozzle wrapped detection pos\n\nG90\nG1 Z5 F1200\nG1 X270 Y-0.5 F60000\nG28.140 S0 ; cali pre-extrude z pos\n\n;============switch again==================\nM211 X0 Y0 Z0 ;turn off soft endstop\nG91\nG1 Z6 F1200\nG90\nM1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\nM620 S[initial_no_support_filament_id]A H[initial_no_support_hotend]\nM400\nT[initial_no_support_filament_id] H[initial_no_support_hotend]\nM400\nM628 S0\nM629\nM400\nM621 S[initial_no_support_filament_id]A\n\n;============switch again==================\n\nM400\n;M73 P99\n\nM141 S[overall_chamber_temperature]\n\n;===== mech mode sweep start =====\n M1002 gcode_claim_action : 3\n\n G90\n G1 Z5 F1200\n G1 X165 Y160 F20000\n M400 P200\n\n M970.3 Q1 A5 K0 O1\n M974 Q1 S2 P0\n\n M970.3 Q0 A5 K0 O1\n M974 Q0 S2 P0\n\n M970.2 Q2 K0 W38 Z0.01\n M974 Q2 S2 P0\n\n M975 S1\n;===== mech mode sweep end =====\n\n;===== wait temperature reaching the reference value =======\n\nM140 S[bed_temperature_initial_layer_single]\nM190 S[bed_temperature_initial_layer_single]\n\n ;========turn off light and fans =============\n M960 S1 P0 ; turn off laser\n M960 S2 P0 ; turn off laser\n M106 S0 ; turn off fan\n M106 P2 S0 ; turn off big fan\n ;==== set ext toodhead cooling fan ====\n {if (min_vitrification_temperature <= 50)}\n M106 P9 S255\n {endif}\n ;============set motor current==================\n M400 S1\n\n;===== wait temperature reaching the reference value =======\n\nM400\n;M73 P99\n\n;===== for Textured PEI Plate , lower the nozzle as the nozzle was touching topmost of the texture when homing ==\n {if curr_bed_type==\"Textured PEI Plate\"}\n {if nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2}\n G29.1 Z{-0.01} ; for Textured PEI Plate\n {else}\n G29.1 Z{-0.02} ; for Textured PEI Plate\n {endif}\n {else}\n {if nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2}\n G29.1 Z{0.01}\n {endif}\n {endif}\nG150.1\n\nM975 S1 ; turn on mech mode supression\nM983.4 S1 ; turn on deformation compensation\nG29.2 S1 ; turn on pos comp\nG29.7 S1\n\nG90\nG1 Z5 F1200\nG1 Y295 F30000\nG1 Y265 F18000\n\n;===== nozzle load line ===============================\n G29.2 S1 ; ensure z comp turn on\n G90\n M83\n G1 Z5 F1200\n G1 X270 Y-0.5 F60000\n G28.14 R0\n G29.2 S0\n G91\n G1 Z0.8 F1200\n G90\n G1 X250 F60000\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n\n ;========== record data ==========\n M1026\n M1012.8\n M1012.9\n G29.9\n ;========== record data ==========\n\n M400 P50\n M500 D1\n M400 S3\n M109 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M83\n{if nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.8}\n G1 E5 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4053*60}\n{endif}\n{if (filament_type[initial_no_support_filament_id] == \"TPU\")}\n G1 E5 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4053*60}\n{endif}\n G1 E5 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4053*60}\n G1 X290 E10 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4053*60}\n G91\n G3 Z0.4 I1.217 J0 P1 F60000\n G90\n M83\n G29.2 S1 ; ensure z comp turn on\n;===== noozle load line end ===========================\n\nM400\n;M73 P99\n\nM993 A1 B1 C1 ; nozzle cam detection allowed.\n\n{if (filament_type[initial_no_support_filament_id] == \"TPU\")}\nM1015.3 S1;enable tpu clog detect\n{else}\nM1015.3 S0;disable tpu clog detect\n{endif}\n\n{if (filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PETG\")\n || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG-CF\")}\nM1015.4 S1 K1 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;enable E air printing detect\n{else}\nM1015.4 S0 K0 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;disable E air printing detect\n{endif}\n\nM620.6 I[initial_no_support_filament_id] H[initial_no_support_hotend] W1 ;enable ams air printing detect\nM620 O1\n\nM211 Z1\nG29.99\nM1002 gcode_claim_action : 0\nM400\n" } diff --git a/resources/profiles/Qidi.json b/resources/profiles/Qidi.json index 4b6496df31..dcb39ee957 100644 --- a/resources/profiles/Qidi.json +++ b/resources/profiles/Qidi.json @@ -1,6 +1,6 @@ { "name": "Qidi", - "version": "02.04.00.16", + "version": "02.04.00.17", "force_update": "0", "description": "Qidi configurations", "machine_model_list": [ diff --git a/resources/profiles/Qidi/machine/Qidi Q1 Pro 0.4 nozzle.json b/resources/profiles/Qidi/machine/Qidi Q1 Pro 0.4 nozzle.json index bd2398ae7a..30db0fc68b 100644 --- a/resources/profiles/Qidi/machine/Qidi Q1 Pro 0.4 nozzle.json +++ b/resources/profiles/Qidi/machine/Qidi Q1 Pro 0.4 nozzle.json @@ -83,7 +83,7 @@ "extruder_clearance_height_to_lid": "120", "layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nLOG_Z\nG92 E0\n", "machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}", - "machine_start_gcode": "PRINT_START BED=[hot_plate_temp_initial_layer] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[hot_plate_temp_initial_layer]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n", + "machine_start_gcode": "PRINT_START BED=[bed_temperature_initial_layer_single] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n", "thumbnails_format": "PNG", "default_filament_profile": [ "Generic PLA @Qidi Q1 Pro 0.4 nozzle" diff --git a/resources/profiles/Qidi/machine/Qidi X-CF Pro 0.4 nozzle.json b/resources/profiles/Qidi/machine/Qidi X-CF Pro 0.4 nozzle.json index a41e2b6a11..968f7f63a9 100644 --- a/resources/profiles/Qidi/machine/Qidi X-CF Pro 0.4 nozzle.json +++ b/resources/profiles/Qidi/machine/Qidi X-CF Pro 0.4 nozzle.json @@ -100,7 +100,7 @@ "default_filament_profile": [ "Generic PLA @Qidi" ], - "machine_start_gcode": "G28\nM140 S[hot_plate_temp_initial_layer]\nM190 S[hot_plate_temp_initial_layer]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n", + "machine_start_gcode": "G28\nM140 S[bed_temperature_initial_layer_single]\nM190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n", "machine_end_gcode": "M104 S0\nM140 S0\n;Retract the filament\nG92 E0\nG1 E-3 F300\nG28\nM84\n", "scan_first_layer": "0" } diff --git a/resources/profiles/Qidi/machine/fdm_qidi_x3_common.json b/resources/profiles/Qidi/machine/fdm_qidi_x3_common.json index 9796856379..8fe450c3c6 100644 --- a/resources/profiles/Qidi/machine/fdm_qidi_x3_common.json +++ b/resources/profiles/Qidi/machine/fdm_qidi_x3_common.json @@ -24,7 +24,7 @@ "210x210/COLPIC", "110x110/PNG" ], - "machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[hot_plate_temp_initial_layer]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n", + "machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[bed_temperature_initial_layer_single]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n", "machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}", "scan_first_layer": "0" } diff --git a/scripts/ci_deps_cache.sh b/scripts/ci_deps_cache.sh index a27b7e2522..0c3ae85415 100644 --- a/scripts/ci_deps_cache.sh +++ b/scripts/ci_deps_cache.sh @@ -1,21 +1,27 @@ #!/usr/bin/env bash -# Lets a pull request's macOS deps build wait for a push build of the base -# branch that builds the same deps, from a Linux job. `check` runs after a cache -# miss and names a queued or running base build with the same deps/ and .github/ -# to wait for, unless the pull request changes either. `wait` waits until that -# build saves KEY. actions/cache then confirms the entry can be restored. +# Lets a pull request's macOS deps build, or its clang-tidy job, wait for a +# build that saves the deps cache it needs, from a Linux job. `check` runs after +# a cache miss and names a queued or running push build of the base branch with +# the same deps/ and .github/ to wait for, unless the pull request changes either. +# `own` names the pull request's own build instead, for clang-tidy when the pull +# request changes deps/. `wait` waits until that build saves KEY under CACHE_REF, +# or its deps job fails. actions/cache then confirms the entry can be restored. # -# Environment: GH_TOKEN, REPO, WORKFLOW_REF and BASE_REF, plus KEY_INPUTS, -# WAIT_FOR, WAIT_SINCE, WAIT_MINUTES, KEY and ARCH for `wait`. +# Environment: GH_TOKEN, REPO, WORKFLOW_REF (or WORKFLOW, the build workflow's +# file name) and BASE_REF, plus HEAD_SHA for `own`, and KEY_INPUTS, WAIT_FOR, +# WAIT_SINCE, WAIT_MINUTES, KEY, CACHE_REF (default the base branch) and ARCH +# (or JOB, the matrix job's name) for `wait`. # Writes key-inputs, wait-for and wait-since to $GITHUB_OUTPUT. set -uo pipefail -workflow=${WORKFLOW_REF%%@*} -workflow=${workflow##*/} +workflow=${WORKFLOW_REF:-} +workflow=${workflow%%@*} +workflow=${WORKFLOW:-${workflow##*/}} # Must match the job names in build_all.yml, build_check_cache.yml and build_deps.yml. -check_job_name="build_macos_arch (${ARCH:-}) / Check Cache" -deps_job_name="build_macos_arch (${ARCH:-}) / Build Deps / Build Deps" +job=${JOB:-build_macos_arch (${ARCH:-})} +check_job_name="$job / Check Cache" +deps_job_name="$job / Build Deps / Build Deps" out() { echo "$1" >> "${GITHUB_OUTPUT:-/dev/stdout}"; } @@ -63,8 +69,27 @@ check() { fi } +own() { + out "key-inputs=$(local_key_inputs HEAD)" + local id + # The build can be created a moment after this job. + for _ in 1 2 3 4 5; do + id=$(gh api -X GET "repos/$REPO/actions/workflows/$workflow/runs" \ + -f head_sha="$HEAD_SHA" -f event=pull_request -f per_page=1 --jq '.workflow_runs[0].id // empty' < /dev/null) + [ -n "$id" ] && break + sleep 30 + done + if [ -n "$id" ]; then + echo "Waiting for the deps cache of build $id of this pull request." + out "wait-for=$id" + out "wait-since=$(date +%s)" + else + echo "No build of this pull request is building its deps." + fi +} + key_saved() { - gh cache list -R "$REPO" --ref "refs/heads/$BASE_REF" --key "$KEY" --json key < /dev/null \ + gh cache list -R "$REPO" --ref "${CACHE_REF:-refs/heads/$BASE_REF}" --key "$KEY" --json key < /dev/null \ | jq -e --arg key "$KEY" 'any(.[]; .key == $key)' > /dev/null } @@ -87,11 +112,11 @@ wait_for() { key_saved && { echo "Found $KEY."; return; } sleep 20 done - echo "Build $id of $BASE_REF finished its deps without saving $KEY." + echo "Build $id finished its deps without saving $KEY." return ;; "completed cancelled") ;; completed*) - echo "The deps job of build $id of $BASE_REF did not succeed." + echo "The deps job of build $id did not succeed." return ;; # The deps job exists only once the build's cache check has succeeded. "") @@ -100,7 +125,7 @@ wait_for() { completed*) check=$(jq -r --arg name "$check_job_name" 'first(.jobs[] | select(.name == $name) | .conclusion) // ""' <<< "$jobs") if [ -z "$warned" ] && { [ -z "$check" ] || [ "$check" = success ]; }; then - echo "::warning title=Deps wait::Build $id of $BASE_REF has no job named \"$deps_job_name\". If the build jobs were renamed, update scripts/ci_deps_cache.sh. This does not affect this pull request." + echo "::warning title=Deps wait::Build $id has no job named \"$deps_job_name\". If the build jobs were renamed, update scripts/ci_deps_cache.sh. This does not affect this pull request." warned=1 fi ;; *) @@ -118,11 +143,12 @@ wait_for() { seen="$seen $id" echo "Following build $id of $BASE_REF." done - echo "No build of $BASE_REF saved $KEY." + echo "No build saved $KEY." } case "${1:-}" in check) check ;; + own) own ;; wait) wait_for ;; - *) echo "usage: $0 check|wait" >&2; exit 2 ;; + *) echo "usage: $0 check|own|wait" >&2; exit 2 ;; esac diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 1b764fe468..2a158f4b12 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -99,6 +99,7 @@ #include "libslic3r/Preset.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/GCode.hpp" +#include "libslic3r/MeshRepair.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/ModelArrange.hpp" #include "libslic3r/Platform.hpp" @@ -206,7 +207,7 @@ std::map cli_errors = { {CLI_OBJECT_ORIENT_FAILED, "An error occurred when auto-orienting object(s)."}, {CLI_MODIFIED_PARAMS_TO_PRINTER, "Found modified parameter in printer preset in the 3mf file, which should not be changed."}, {CLI_FILE_VERSION_NOT_SUPPORTED, "Unsupported 3MF version. Please make sure the 3MF file was created with the official version of Bambu Studio, not a beta version."}, - {CLI_NO_SUITABLE_OBJECTS, "One of the plate is empty or has no object fully inside it. Please check that the 3mf contains no empty plate in Orca Slicer before uploading."}, + {CLI_NO_SUITABLE_OBJECTS, "The plate has nothing to slice: it is empty, or no object on it is both printable and fully inside the print volume."}, {CLI_VALIDATE_ERROR, "There are some incorrect slicing parameters in the 3mf. Please verify the slicing of all plates in Orca Slicer before uploading."}, {CLI_OBJECTS_PARTLY_INSIDE, "Some objects are located over the boundary of the heated bed."}, {CLI_EXPORT_CACHE_DIRECTORY_CREATE_FAILED, "Failed creating directory when exporting cache data."}, @@ -1463,7 +1464,7 @@ int CLI::run(int argc, char **argv) //int arrange_option; int plate_to_slice = 0, filament_count = 0, duplicate_count = 0, real_duplicate_count = 0, current_extruder_count = 1, new_extruder_count = 1, current_printer_variant_count = 1, current_print_variant_count = 1, new_printer_variant_count = 1; bool first_file = true, is_bbl_3mf = false, need_arrange = true, has_thumbnails = false, up_config_to_date = false, normative_check = true, duplicate_single_object = false, use_first_fila_as_default = false, minimum_save = false, enable_timelapse = false; - bool allow_rotations = true, skip_modified_gcodes = false, avoid_extrusion_cali_region = false, skip_useless_pick = false, allow_newer_file = false, current_is_multi_extruder = false, new_is_multi_extruder = false, allow_mix_temp = false, enable_wrapping_detect = false; + bool allow_rotations = false, skip_modified_gcodes = false, avoid_extrusion_cali_region = false, skip_useless_pick = false, allow_newer_file = false, current_is_multi_extruder = false, new_is_multi_extruder = false, allow_mix_temp = false, enable_wrapping_detect = false; Semver file_version; std::map orients_requirement; std::vector project_presets; @@ -5130,9 +5131,19 @@ int CLI::run(int argc, char **argv) } } } else if (opt_key == "repair") { - // Models are repaired by default. - //for (auto &model : m_models) - // model.repair(); + if (m_config.opt_bool(opt_key)) + for (Model &model : m_models) + for (ModelObject *object : model.objects) { + for (ModelVolume *volume : object->volumes) { + MeshRepairStats stats; + std::string error; + if (!volume->repair(true, &stats, &error)) + BOOST_LOG_TRIVIAL(warning) << "repair of " << object->name << " / " << volume->name << " failed: " << error; + else if (stats.modified) + BOOST_LOG_TRIVIAL(info) << "repaired " << object->name << " / " << volume->name; + } + object->invalidate_bounding_box(); + } } else { boost::nowide::cerr << "error: option not implemented yet: " << opt_key << std::endl; record_exit_reson(outfile_dir, CLI_UNSUPPORTED_OPERATION, 0, cli_errors[CLI_UNSUPPORTED_OPERATION], sliced_info); @@ -5142,6 +5153,7 @@ int CLI::run(int argc, char **argv) BOOST_LOG_TRIVIAL(info) << "finished model pre-process commands\n"; bool oriented_or_arranged = false; + bool arranged_without_rotations = false; //BBS: add orient and arrange logic here for (auto& model : m_models) { @@ -5409,6 +5421,7 @@ int CLI::run(int argc, char **argv) BOOST_LOG_TRIVIAL(info) << boost::format("start plate %1%'s arranging...") % (i + 1); arrangement::arrange(selected, unselected, beds, arrange_cfg); //arrangement::arrange(unprintable, {}, beds, arrange_cfg); + arranged_without_rotations |= !arrange_cfg.allow_rotations; BOOST_LOG_TRIVIAL(info) << boost::format("finished plate %1%'s arranging") % (i + 1); //step-4: postprocess the bed index and result @@ -5881,6 +5894,7 @@ int CLI::run(int argc, char **argv) BOOST_LOG_TRIVIAL(info) << boost::format("start %1% th arranging...")%arrange_count; arrangement::arrange(selected, unselected, beds, arrange_cfg); arrangement::arrange(unprintable, {}, beds, arrange_cfg); + arranged_without_rotations |= !arrange_cfg.allow_rotations; BOOST_LOG_TRIVIAL(info) << boost::format("finished %1% th arranging...")%arrange_count; //Step-4:postprocess by partplate list&&apply the result @@ -6425,7 +6439,15 @@ int CLI::run(int argc, char **argv) if (count == 0) { BOOST_LOG_TRIVIAL(error) << "plate "<< index+1<< ": Nothing to be sliced, Either the print is empty or no object is fully inside the print volume before apply." << std::endl; - record_exit_reson(outfile_dir, CLI_NO_SUITABLE_OBJECTS, index+1, cli_errors[CLI_NO_SUITABLE_OBJECTS], sliced_info); + std::string no_objects_reason = cli_errors[CLI_NO_SUITABLE_OBJECTS]; + if (arranged_without_rotations) { + //ORCA: result.json is only written on Linux, so also print the reason to stderr. + no_objects_reason = "No object is fully inside the print volume. Arrange ran without rotations." + " If an object only fits the bed when rotated, pass --allow-rotations." + " Otherwise the plate is empty or an object is larger than the printable area or height."; + boost::nowide::cerr << "plate " << index+1 << ": " << no_objects_reason << std::endl; + } + record_exit_reson(outfile_dir, CLI_NO_SUITABLE_OBJECTS, index+1, no_objects_reason, sliced_info); flush_and_exit(CLI_NO_SUITABLE_OBJECTS); } else if ((plate_to_slice != 0) || pre_check) { diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 1a7080d5c7..afa3b61bc1 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -625,6 +625,7 @@ add_library(libslic3r_cgal STATIC CutSurface.hpp CutSurface.cpp IntersectionPoints.hpp IntersectionPoints.cpp MeshBoolean.hpp MeshBoolean.cpp + MeshRepair.hpp MeshRepair.cpp TryCatchSignal.hpp TryCatchSignal.cpp Triangulation.hpp Triangulation.cpp ) @@ -652,7 +653,9 @@ if (IS_CLANG_CL) target_compile_options(libslic3r_cgal PRIVATE -Wno-overriding-option) endif () -target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs) +# Lets CGAL algorithms given CGAL::Parallel_if_available_tag run on TBB. +target_compile_definitions(libslic3r_cgal PRIVATE CGAL_LINKED_WITH_TBB) +target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs TBB::tbb) if (MSVC AND "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # 32 bit MSVC workaround target_compile_definitions(libslic3r_cgal PRIVATE CGAL_DO_NOT_USE_MPZF) diff --git a/src/libslic3r/Format/STEP.cpp b/src/libslic3r/Format/STEP.cpp index 962c4cffb9..4f40093c73 100644 --- a/src/libslic3r/Format/STEP.cpp +++ b/src/libslic3r/Format/STEP.cpp @@ -236,19 +236,29 @@ static void getNamedSolids(const TopLoc_Location& location, int i = 0; switch (shape_type) { case TopAbs_COMPOUND: - if (!isSplitCompound) { - namedSolids.emplace_back(TopoDS::Compound(transform.Shape()), fullName); - break; - } case TopAbs_COMPSOLID: if (!isSplitCompound) { - namedSolids.emplace_back(TopoDS::CompSolid(transform.Shape()), fullName); + namedSolids.emplace_back(transform.Shape(), fullName); } else { for (explorer.Init(transform.Shape(), TopAbs_SOLID); explorer.More(); explorer.Next()) { i++; const TopoDS_Shape& currentShape = explorer.Current(); namedSolids.emplace_back(TopoDS::Solid(currentShape), fullName + "-SOLID-" + std::to_string(i)); } + // Surface models have no solids: each shell outside a solid is a part, and loose faces stay together. + i = 0; + for (explorer.Init(transform.Shape(), TopAbs_SHELL, TopAbs_SOLID); explorer.More(); explorer.Next()) + namedSolids.emplace_back(explorer.Current(), fullName + "-SHELL-" + std::to_string(++i)); + TopoDS_Compound loose_faces; + TopoDS_Builder builder; + builder.MakeCompound(loose_faces); + bool has_loose_faces = false; + for (explorer.Init(transform.Shape(), TopAbs_FACE, TopAbs_SHELL); explorer.More(); explorer.Next()) { + builder.Add(loose_faces, explorer.Current()); + has_loose_faces = true; + } + if (has_loose_faces) + namedSolids.emplace_back(loose_faces, fullName); } break; case TopAbs_SOLID: diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 8a22201c51..24c2c027e5 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -22,6 +22,7 @@ #include "libslic3r/BrimEarsPoint.hpp" #include "libslic3r/Config.hpp" #include "libslic3r/CustomGCode.hpp" +#include "libslic3r/TextureDisplacement.hpp" #include "libslic3r_version.h" #include "bbs_3mf.hpp" @@ -354,6 +355,13 @@ static constexpr const char* CUSTOM_SUPPORTS_ATTR = "paint_supports"; static constexpr const char* CUSTOM_FUZZY_SKIN_ATTR = "paint_fuzzy_skin"; static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam"; static constexpr const char* MMU_SEGMENTATION_ATTR = "paint_color"; +// Texture displacement. One paint mask per layer slot, mirroring paint_color; the layer stack itself is +// a JSON file in the archive, named by the volume metadata key below (see add_texture_displacement()). +static constexpr const char* TEXTURE_DISPLACEMENT_ATTRS[Slic3r::TEXTURE_DISPLACEMENT_MAX_LAYERS] = { + "paint_texture_0", "paint_texture_1", "paint_texture_2", "paint_texture_3", + "paint_texture_4", "paint_texture_5", "paint_texture_6", "paint_texture_7" }; +static constexpr const char* TEXTURE_DISPLACEMENT_KEY = "texture_displacement"; +static constexpr const char* TEXTURE_DISPLACEMENT_DIR = "Metadata/texture_displacement/"; // BBS static constexpr const char* FACE_PROPERTY_ATTR = "face_property"; @@ -847,6 +855,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) std::vector custom_seam; std::vector mmu_segmentation; std::vector fuzzy_skin; + // One per texture displacement layer slot, indexed like `triangles` but stored only as far as the + // slot's last painted triangle: empty when the slot has no paint, which is nearly every file. + std::vector texture_displacement[TEXTURE_DISPLACEMENT_MAX_LAYERS]; // BBS std::vector face_properties; @@ -867,6 +878,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) custom_seam.clear(); mmu_segmentation.clear(); fuzzy_skin.clear(); + for (std::vector &slot : texture_displacement) + slot.clear(); } }; @@ -1200,6 +1213,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) /*IdToSlaSupportPointsMap m_sla_support_points; IdToSlaDrainHolesMap m_sla_drain_holes;*/ PathToEmbossShapeFileMap m_path_to_emboss_shape_files; + // Texture displacement: every file under Metadata/texture_displacement/, by archive path. Volumes + // are built only after the whole archive has been walked, so by the time a volume names its JSON + // every file it could refer to is already in here, whatever order the archive happened to be in. + std::map m_texture_displacement_files; + void _apply_texture_displacement(ModelVolume &volume, const std::string &json_path); std::string m_curr_metadata_name; std::string m_curr_characters; std::string m_name; @@ -1729,8 +1747,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) //plate->pattern_file = it->second->pattern_file; plate->no_light_thumbnail_file = it->second->no_light_thumbnail_file; plate->top_file = it->second->top_file; - plate->pick_file = it->second->pick_file.empty(); - plate->pattern_bbox_file = it->second->pattern_bbox_file.empty(); + plate->pick_file = it->second->pick_file; + plate->pattern_bbox_file = it->second->pattern_bbox_file; plate->config = it->second->config; if (!plate->thumbnail_file.empty()) @@ -2073,6 +2091,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) else if (_is_svg_shape_file(name)) { _extract_embossed_svg_shape_file(name, archive, stat); } + else if (boost::algorithm::istarts_with(name, TEXTURE_DISPLACEMENT_DIR)) { + std::string contents(stat.m_uncomp_size, '\0'); + if (mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, contents.data(), stat.m_uncomp_size, 0)) + m_texture_displacement_files.emplace(name, std::move(contents)); + else + add_error("Error while reading texture displacement data"); + } else if (!dont_load_config && boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) { m_parsing_slice_info = true; //extract slice info from archive @@ -3310,6 +3335,45 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) } }*/ + // Restores one volume's texture displacement stack from the JSON it named. Called while the volume is + // being built, which is after the whole archive has been walked - so every file it can refer to is + // already extracted, and no deferral is needed. + // + // A missing or malformed JSON leaves the volume with no layers but keeps the paint mask it already + // loaded, which is the same state as a project saved by a build without the feature: recoverable by + // picking the texture again rather than a hard failure. + void _BBS_3MF_Importer::_apply_texture_displacement(ModelVolume &volume, const std::string &json_path) + { + const auto json = m_texture_displacement_files.find(json_path); + if (json == m_texture_displacement_files.end()) { + add_error("Missing texture displacement data: " + json_path); + return; + } + std::vector layers; + TextureDisplacementOptions options; + if (!texture_displacement_layers_from_json(json->second, layers, options)) { + add_error("Malformed texture displacement data: " + json_path); + return; + } + // The archive is the only source of a layer's pixels. `path` comes from the project file, so loading + // the project must never read the file it names: that would let whoever made the project pick any + // local (or, on Windows, network) file to read, and the next save would embed it in the archive. + // `path` stays a label of where the texture came from; a layer the archive carries no image for + // loads without one, its settings intact, until the user picks a texture again. + for (TextureDisplacementLayer &layer : layers) { + if (layer.path_in_3mf.empty()) + continue; + const auto image = m_texture_displacement_files.find(layer.path_in_3mf); + if (image == m_texture_displacement_files.end()) { + add_error("Missing texture displacement image: " + layer.path_in_3mf); + continue; + } + layer.image_data = std::make_shared>(image->second.begin(), image->second.end()); + } + volume.texture_displacement_layers = std::move(layers); + volume.texture_displacement_options = options; + } + void _BBS_3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat){ assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end()); auto file = std::make_unique(stat.m_uncomp_size, '\0'); @@ -3966,6 +4030,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) m_curr_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR)); m_curr_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR)); m_curr_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR)); + for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) + if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) { + std::vector &mask = m_curr_object->geometry.texture_displacement[slot]; + mask.resize(m_curr_object->geometry.triangles.size()); + mask.back() = paint; + } // BBS m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR)); } @@ -5317,6 +5387,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) volume->mmu_segmentation_facets.touch(); volume->fuzzy_skin_facets.shrink_to_fit(); volume->fuzzy_skin_facets.touch(); + for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) { + const std::vector &mask = sub_object->geometry.texture_displacement[slot]; + if (mask.empty()) + continue; // no triangle painted on this slot + FacetsAnnotation &facets = volume->texture_displacement_facet(slot); + facets.reserve(triangles_count); + for (size_t i = 0; i < triangles_count && i < mask.size(); ++i) + if (!mask[i].empty()) + facets.set_triangle_from_string(i, mask[i]); + facets.shrink_to_fit(); + facets.touch(); + } } volume->set_type(volume_data->part_type); @@ -5332,6 +5414,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) for (const Metadata& metadata : volume_data->metadata) { if (metadata.key == NAME_KEY) volume->name = metadata.value; + else if (metadata.key == TEXTURE_DISPLACEMENT_KEY) + _apply_texture_displacement(*volume, metadata.value); //else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1")) // volume->set_type(ModelVolumeType::PARAMETER_MODIFIER); //for old format @@ -5496,12 +5580,28 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) volume->seam_facets.shrink_to_fit(); volume->mmu_segmentation_facets.shrink_to_fit(); + for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) { + const std::vector &mask = geometry.texture_displacement[slot]; + if (mask.empty()) + continue; // written by a build without the feature + FacetsAnnotation &facets = volume->texture_displacement_facet(slot); + facets.reserve(triangles_count); + for (size_t i = 0; i < triangles_count; ++i) { + const size_t index = volume_data.first_triangle_id + i; + if (index < mask.size() && !mask[index].empty()) + facets.set_triangle_from_string(i, mask[index]); + } + facets.shrink_to_fit(); + facets.touch(); + } volume->set_type(volume_data.part_type); // apply the remaining volume's metadata for (const Metadata& metadata : volume_data.metadata) { if (metadata.key == NAME_KEY) volume->name = metadata.value; + else if (metadata.key == TEXTURE_DISPLACEMENT_KEY) + _apply_texture_displacement(*volume, metadata.value); //else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1")) // volume->set_type(ModelVolumeType::PARAMETER_MODIFIER); //for old format @@ -5805,6 +5905,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) current_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR)); current_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR)); current_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR)); + for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) + if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) { + std::vector &mask = current_object->geometry.texture_displacement[slot]; + mask.resize(current_object->geometry.triangles.size()); + mask.back() = paint; + } // BBS current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR)); } @@ -6808,6 +6914,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) stream << " \n"; stream << " \n"; stream << " \n"; + stream << " \n"; + stream << " \n"; + stream << " \n"; stream << " \n"; stream << ""; @@ -7232,10 +7341,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (iter != m_shared_meshes.end()) { const ModelVolume* shared_volume = iter->second.second; + bool same_texture_paint = true; + for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS) && same_texture_paint; ++slot) + same_texture_paint = shared_volume->texture_displacement_facet(slot).equals(volume->texture_displacement_facet(slot)); if ((shared_volume->supported_facets.equals(volume->supported_facets)) && (shared_volume->seam_facets.equals(volume->seam_facets)) && (shared_volume->mmu_segmentation_facets.equals(volume->mmu_segmentation_facets)) - && (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets))) + && (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets)) + && same_texture_paint) { auto data = iter->second.first; const_cast<_BBS_3MF_Exporter *>(this)->m_volume_paths.insert({volume, {data->sub_path, data->volumes_objectID.find(iter->second.second)->second}}); @@ -7651,6 +7764,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) output_buffer += "\""; } + // One attribute per texture displacement layer slot. Older readers ignore attributes they do + // not know, so a project written here still opens in a build without the feature - it just + // loses the paint, which is also all it could have done with it. + for (int slot = 0; slot < TEXTURE_DISPLACEMENT_MAX_LAYERS; ++slot) { + const std::string texture_paint = volume->texture_displacement_facet(slot).get_triangle_as_string(i); + if (texture_paint.empty()) + continue; + output_buffer += " "; + output_buffer += TEXTURE_DISPLACEMENT_ATTRS[slot]; + output_buffer += "=\""; + output_buffer += texture_paint; + output_buffer += "\""; + } + // BBS if (i < its.properties.size()) { std::string prop_str = its.properties[i].to_string(); @@ -8100,7 +8227,42 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return file_path; } - bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id) + // Writes a volume's texture displacement stack into the archive and points the volume's metadata at it. +// The layer settings go in as JSON and each layer's texture as the image file it was loaded from, both +// referenced by path - the same split EmbossShape makes for its SVG. The image deliberately stays out of +// the XML: it is binary and routinely megabytes, and base64 in an attribute would bloat the one file +// every reader has to parse just to list the objects. +static void add_texture_displacement(std::stringstream &stream, const ModelVolume &volume, mz_zip_archive &archive, + const std::string &id) +{ + if (volume.texture_displacement_layers.empty()) + return; + + // A copy, because path_in_3mf is only meaningful inside the archive being written and the volume + // being exported is const (and may be saved again, elsewhere, with different paths). + std::vector layers = volume.texture_displacement_layers; + for (TextureDisplacementLayer &layer : layers) { + layer.path_in_3mf.clear(); + if (!layer.image_data || layer.image_data->empty()) + continue; + // Always .png: the importer converts whatever the user picked (jpg, bmp, ...) to an 8-bit + // greyscale PNG, so image_data is PNG whatever layer.path happens to be named. + const std::string path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + "_" + std::to_string(layer.slot) + ".png"; + // No deflate: a PNG is already compressed, so a second pass only costs time. + if (mz_zip_writer_add_mem(&archive, path.c_str(), layer.image_data->data(), layer.image_data->size(), + MZ_NO_COMPRESSION)) + layer.path_in_3mf = path; + } + + const std::string json = texture_displacement_layers_to_json(layers, volume.texture_displacement_options); + const std::string json_path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + ".json"; + if (!mz_zip_writer_add_mem(&archive, json_path.c_str(), json.data(), json.size(), MZ_DEFAULT_COMPRESSION)) + return; + stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << TEXTURE_DISPLACEMENT_KEY << "\" " + << VALUE_ATTR << "=\"" << xml_escape(json_path) << "\"/>\n"; +} + +bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id) { std::stringstream stream; // Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back @@ -8211,6 +8373,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (const std::optional &es = volume->emboss_shape; es.has_value()) { to_xml(stream, *es, *volume, archive, m_fullpath_sources); } + + add_texture_displacement(stream, *volume, archive, std::to_string(volume->id().id)); if (const std::optional &tc = volume->text_configuration; tc.has_value()) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index cab75d5034..f423681c9d 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1033,6 +1033,15 @@ static std::vector get_path_of_change_filament(const Print& print) return buf; } + // FanMover consumes these markers (and drops them) to avoid splitting a G1 inside custom gcode. + // Only written when FanMover runs, the same gate as its construction in GCode::process_layers. + static std::string wrap_custom_gcode_for_fan_mover(const FullPrintConfig &config, const std::string &gcode) + { + if (config.fan_speedup_time.value == 0 && config.fan_kickstart.value <= 0) + return gcode; + return ";_FAN_MOVER_SKIP_START\n" + gcode + ";_FAN_MOVER_SKIP_END\n"; + } + std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_filament_id, double z) const { if (new_filament_id != -1 && new_filament_id != tcr.new_tool) @@ -1384,6 +1393,9 @@ static std::vector get_path_of_change_filament(const Print& print) toolchange_gcode_str = gcodegen.placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &config); check_add_eol(toolchange_gcode_str); + // Type1 wipe tower path (every BBL printer): bracket the same way set_extruder() does, + // so FanMover doesn't split a G1 inside this custom gcode either. + toolchange_gcode_str = wrap_custom_gcode_for_fan_mover(gcodegen.config(), toolchange_gcode_str); //BBS { @@ -3295,7 +3307,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_is_role_based_fan_on.fill(false); m_role_based_fan_marker_layer.fill(-1); - m_fan_mover.release(); + m_fan_mover.reset(); m_ordering_cache.clear(); // Belt printer: initialize belt-specific writer via virtual hook. @@ -4250,7 +4262,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato config.set_key_value("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get()))); config.set_key_value("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get()))); config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); - std::string filament_start_gcode = this->placeholder_parser_process("filament_start_gcode", print.config().filament_start_gcode.values.at(initial_non_support_extruder_id), initial_non_support_extruder_id,&config); + std::string filament_start_gcode = this->placeholder_parser_process("filament_start_gcode", print.config().filament_start_gcode.get_at(initial_non_support_extruder_id), initial_non_support_extruder_id,&config); file.writeln(filament_start_gcode); // Mark the first filament used in print. Multi-nozzle printers (H2C) get ";VT%d H%d" where // H = dynamic ? nozzle_id : -1; existing single-nozzle printers keep the bare ";VT%d" so their @@ -10895,7 +10907,8 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo toolchange_gcode_parsed = placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &dyn_config); check_add_eol(toolchange_gcode_parsed); - gcode += toolchange_gcode_parsed; + // toolchange_gcode_parsed is still read below (get_last_z_from_gcode, custom_gcode_changes_tool). + gcode += wrap_custom_gcode_for_fan_mover(m_config, toolchange_gcode_parsed); //BBS { diff --git a/src/libslic3r/GCode/FanMover.cpp b/src/libslic3r/GCode/FanMover.cpp index d303cbbac6..d49ca8b114 100644 --- a/src/libslic3r/GCode/FanMover.cpp +++ b/src/libslic3r/GCode/FanMover.cpp @@ -445,6 +445,11 @@ void FanMover::_process_gcode_line(GCodeReader& reader, const GCodeReader::GCode std::string extrusion_string = line.raw().substr(6, line.raw().size() - 6); current_role = ExtrusionEntity::string_to_role(extrusion_string); } + if (line.raw().rfind(";_FAN_MOVER_SKIP_", 0) == 0) { + // Internal markers: read here, then dropped from the output. + m_is_custom_gcode = line.raw().rfind(";_FAN_MOVER_SKIP_START", 0) == 0; + time = -1; + } if (line.raw().size() > 16) { if (line.raw().rfind("; custom gcode", 0) != std::string::npos) { if (line.raw().rfind("; custom gcode end", 0) != std::string::npos) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 8e7d5e7557..f41a4443fb 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3598,6 +3598,10 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config) if (has_scarf_joint_seam != nullptr) m_detect_layer_based_on_tag = m_detect_layer_based_on_tag || has_scarf_joint_seam->value; + const ConfigOptionBool* manual_filament_change = config.option("manual_filament_change"); + if (manual_filament_change != nullptr) + m_manual_filament_change = manual_filament_change->value; + const ConfigOptionEnumGeneric *bed_type = config.option("curr_bed_type"); if (bed_type != nullptr) m_result.bed_type = (BedType)bed_type->value; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index f5b9985f0e..270aceb1c3 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -1228,7 +1228,7 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f; // m_result.nozzle_group_result, which is handed over only after the stream for the // pre-heat injector's second pass and gates the richer change-time model. std::shared_ptr m_nozzle_group_result; - bool m_manual_filament_change; + bool m_manual_filament_change{ false }; //BBS: x, y offset for gcode generated double m_x_offset{ 0 }; diff --git a/src/libslic3r/JumpPointSearch.cpp b/src/libslic3r/JumpPointSearch.cpp deleted file mode 100644 index 9eb18017fb..0000000000 --- a/src/libslic3r/JumpPointSearch.cpp +++ /dev/null @@ -1,349 +0,0 @@ -#include "JumpPointSearch.hpp" -#include "BoundingBox.hpp" -#include "ExPolygon.hpp" -#include "Point.hpp" -#include "libslic3r/AStar.hpp" -#include "libslic3r/KDTreeIndirect.hpp" -#include "libslic3r/Polygon.hpp" -#include "libslic3r/Polyline.hpp" -#include "libslic3r/libslic3r.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -//#define DEBUG_FILES -#ifdef DEBUG_FILES -#include "libslic3r/SVG.hpp" -#endif - -namespace Slic3r { - -// execute fn for each pixel on the line. If fn returns false, terminate the iteration -template void dda(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn) -{ - coord_t dx = abs(x1 - x0); - coord_t dy = abs(y1 - y0); - coord_t x = x0; - coord_t y = y0; - coord_t n = 1 + dx + dy; - coord_t x_inc = (x1 > x0) ? 1 : -1; - coord_t y_inc = (y1 > y0) ? 1 : -1; - coord_t error = dx - dy; - dx *= 2; - dy *= 2; - - for (; n > 0; --n) { - if (!fn(x, y)) return; - - if (error > 0) { - x += x_inc; - error -= dy; - } else { - y += y_inc; - error += dx; - } - } -} - -// will draw the line twice, second time with and offset of 1 in the direction of normal -// may call the fn on the same coordiantes multiple times! -template void double_dda_with_offset(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn) -{ - Vec2d normal = Point{y1 - y0, x1 - x0}.cast().normalized(); - normal.x() = ceil(normal.x()); - normal.y() = ceil(normal.y()); - Point start_offset = Point(x0, y0) + (normal).cast(); - Point end_offset = Point(x1, y1) + (normal).cast(); - - dda(x0, y0, x1, y1, fn); - dda(start_offset.x(), start_offset.y(), end_offset.x(), end_offset.y(), fn); -} - -template class JPSTracer -{ -public: - // Use incoming_dir [0,0] for starting points, so that all directions are checked from that point - struct Node - { - CellPositionType position; - CellPositionType incoming_dir; - }; - - JPSTracer(CellPositionType target, CellQueryFn is_passable) : target(target), is_passable(is_passable) {} - -private: - CellPositionType target; - CellQueryFn is_passable; // should return boolean whether the cell is passable or not - - CellPositionType find_jump_point(CellPositionType start, CellPositionType forward_dir) const - { - CellPositionType next = start + forward_dir; - while (next != target && is_passable(next) && !(is_jump_point(next, forward_dir))) { next = next + forward_dir; } - - if (is_passable(next)) { - return next; - } else { - return start; - } - } - - bool is_jump_point(CellPositionType pos, CellPositionType forward_dir) const - { - if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) { - // diagonal - CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0}; - CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()}; - - if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) { return true; } - - if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) { return true; } - - if (find_jump_point(pos, horizontal_check_dir) != pos) { return true; } - - if (find_jump_point(pos, vertical_check_dir) != pos) { return true; } - - return false; - } else { // horizontal or vertical - CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x()); - - if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { return true; } - - if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { return true; } - - return false; - } - } - -public: - template void foreach_reachable(const Node &from, Fn &&fn) const - { - const CellPositionType & pos = from.position; - const CellPositionType & forward_dir = from.incoming_dir; - std::vector dirs_to_check{}; - - if (abs(forward_dir.x()) + abs(forward_dir.y()) == 0) { // special case for starting point - dirs_to_check = all_directions; - } else if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) { - // diagonal - CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0}; - CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()}; - - if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) { - dirs_to_check.push_back(forward_dir - 2 * horizontal_check_dir); - } - - if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) { - dirs_to_check.push_back(forward_dir - 2 * vertical_check_dir); - } - - dirs_to_check.push_back(horizontal_check_dir); - dirs_to_check.push_back(vertical_check_dir); - dirs_to_check.push_back(forward_dir); - - } else { // horizontal or vertical - CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x()); - - if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { dirs_to_check.push_back(forward_dir + side_dir); } - - if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { dirs_to_check.push_back(forward_dir - side_dir); } - dirs_to_check.push_back(forward_dir); - } - - for (const CellPositionType &dir : dirs_to_check) { - CellPositionType jp = find_jump_point(pos, dir); - if (jp != pos) fn(Node{jp, dir}); - } - } - - float distance(Node a, Node b) const { return (a.position - b.position).template cast().norm(); } - - float goal_heuristic(Node n) const { return n.position == target ? -1.f : (target - n.position).template cast().norm(); } - - size_t unique_id(Node n) const { return (static_cast(uint16_t(n.position.x())) << 16) + static_cast(uint16_t(n.position.y())); } - - const std::vector all_directions{{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}}; -}; - -void JPSPathFinder::clear() -{ - inpassable.clear(); - max_search_box.max = Pixel(std::numeric_limits::min(), std::numeric_limits::min()); - max_search_box.min = Pixel(std::numeric_limits::max(), std::numeric_limits::max()); - add_obstacles(bed_shape); -} - -void JPSPathFinder::add_obstacles(const Lines &obstacles) -{ - auto store_obstacle = [&](coord_t x, coord_t y) { - max_search_box.max.x() = std::max(max_search_box.max.x(), x); - max_search_box.max.y() = std::max(max_search_box.max.y(), y); - max_search_box.min.x() = std::min(max_search_box.min.x(), x); - max_search_box.min.y() = std::min(max_search_box.min.y(), y); - inpassable.insert(Pixel{x, y}); - return true; - }; - - for (const Line &l : obstacles) { - Pixel start = pixelize(l.a); - Pixel end = pixelize(l.b); - double_dda_with_offset(start.x(), start.y(), end.x(), end.y(), store_obstacle); - } -} - -Polyline JPSPathFinder::find_path(const Point &p0, const Point &p1) -{ - Pixel start = pixelize(p0); - Pixel end = pixelize(p1); - if (inpassable.empty() || (start - end).cast().norm() < 3.0) { return Polyline{p0, p1}; } - - if (inpassable.find(start) != inpassable.end()) { - dda(start.x(), start.y(), end.x(), end.y(), [&](coord_t x, coord_t y) { - if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable - start = Pixel(x, y); - return false; - } - return true; - }); - } - - if (inpassable.find(end) != inpassable.end()) { - dda(end.x(), end.y(), start.x(), start.y(), [&](coord_t x, coord_t y) { - if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable - end = Pixel(x, y); - return false; - } - return true; - }); - } - - BoundingBox search_box = max_search_box; - search_box.max -= Pixel(1, 1); - search_box.min += Pixel(1, 1); - - BoundingBox bounding_square(Points{start, end}); - bounding_square.max += Pixel(5, 5); - bounding_square.min -= Pixel(5, 5); - coord_t bounding_square_size = 2 * std::max(bounding_square.size().x(), bounding_square.size().y()); - bounding_square.max.x() += (bounding_square_size - bounding_square.size().x()) / 2; - bounding_square.min.x() -= (bounding_square_size - bounding_square.size().x()) / 2; - bounding_square.max.y() += (bounding_square_size - bounding_square.size().y()) / 2; - bounding_square.min.y() -= (bounding_square_size - bounding_square.size().y()) / 2; - - // Intersection - limit the search box to a square area around the start and end, to fasten the path searching - search_box.max = search_box.max.cwiseMin(bounding_square.max); - search_box.min = search_box.min.cwiseMax(bounding_square.min); - - auto cell_query = [&](Pixel pixel) { return search_box.contains(pixel) && (pixel == start || pixel == end || inpassable.find(pixel) == inpassable.end()); }; - - JPSTracer tracer(end, cell_query); - using QNode = astar::QNode>; - - std::unordered_map astar_cache{}; - std::vector> out_path; - std::vector out_nodes; - - if (!astar::search_route(tracer, {start, {0, 0}}, std::back_inserter(out_nodes), astar_cache)) { - // path not found - just reconstruct the best path from astar cache. - // Note that astar_cache is NOT empty - at least the starting point should always be there - auto coordiante_func = [&astar_cache](size_t idx, size_t dim) { return float(astar_cache[idx].node.position[dim]); }; - std::vector keys; - keys.reserve(astar_cache.size()); - for (const auto &pair : astar_cache) { keys.push_back(pair.first); } - KDTreeIndirect<2, float, decltype(coordiante_func)> kd_tree(coordiante_func, keys); - size_t closest_qnode = find_closest_point(kd_tree, end.cast()); - - out_path.push_back(end); - while (closest_qnode != astar::Unassigned) { - out_path.push_back(astar_cache[closest_qnode].node.position); - closest_qnode = astar_cache[closest_qnode].parent; - } - } else { - for (const auto &node : out_nodes) { out_path.push_back(node.position); } - out_path.push_back(start); - } - -#ifdef DEBUG_FILES - auto scaled_points = [](const Points &ps) { - Points r; - for (const Point &p : ps) { r.push_back(Point::new_scale(p.x(), p.y())); } - return r; - }; - auto scaled_point = [](const Point &p) { return Point::new_scale(p.x(), p.y()); }; - ::Slic3r::SVG svg(debug_out_path(("path_jps" + std::to_string(print_z) + "_" + std::to_string(rand() % 1000)).c_str()).c_str(), - BoundingBox(scaled_point(search_box.min), scaled_point(search_box.max))); - for (const auto &p : inpassable) { svg.draw(scaled_point(p), "black", scale_(0.4)); } - for (const auto &qn : astar_cache) { svg.draw(scaled_point(qn.second.node.position), "blue", scale_(0.3)); } - svg.draw(Polyline(scaled_points(out_path)), "yellow", scale_(0.25)); - svg.draw(scaled_point(end), "purple", scale_(0.4)); - svg.draw(scaled_point(start), "green", scale_(0.4)); -#endif - - std::vector> tmp_path; - tmp_path.reserve(out_path.size()); - // Some path found, reverse and remove points that do not change direction - std::reverse(out_path.begin(), out_path.end()); - { - tmp_path.push_back(out_path.front()); // first point - for (size_t i = 1; i < out_path.size() - 1; i++) { - if ((out_path[i] - out_path[i - 1]).cast().normalized() != (out_path[i + 1] - out_path[i]).cast().normalized()) { tmp_path.push_back(out_path[i]); } - } - tmp_path.push_back(out_path.back()); // last_point - out_path = tmp_path; - } - -#ifdef DEBUG_FILES - svg.draw(Polyline(scaled_points(out_path)), "orange", scale_(0.20)); -#endif - - tmp_path.clear(); - // remove redundant jump points - there are points that change direction but are not needed - this inefficiency arises from the - // usage of grid search The removal alg tries to find the longest Px Px+k path without obstacles. If Px Px+k+1 is blocked, it will - // insert the Px+k point to result and continue search from Px+k - { - tmp_path.push_back(out_path.front()); // first point - size_t index_of_last_stored_point = 0; - for (size_t i = 1; i < out_path.size(); i++) { - if (i - index_of_last_stored_point < 2) continue; - bool passable = true; - auto store_obstacle = [&](coord_t x, coord_t y) { - if (Pixel(x, y) != start && Pixel(x, y) != end && inpassable.find(Pixel(x, y)) != inpassable.end()) { - passable = false; - return false; - } - return true; - }; - dda(tmp_path.back().x(), tmp_path.back().y(), out_path[i].x(), out_path[i].y(), store_obstacle); - if (!passable) { - tmp_path.push_back(out_path[i - 1]); - index_of_last_stored_point = i - 1; - } - } - tmp_path.push_back(out_path.back()); // last_point - out_path = tmp_path; - } - -#ifdef DEBUG_FILES - svg.draw(Polyline(scaled_points(out_path)), "red", scale_(0.15)); - svg.Close(); -#endif - - // before returing the path, transform it from pixels back to points. - // Also replace the first and last pixel by input points so that result path patches input params exactly. - for (Pixel &p : out_path) { p = unpixelize(p); } - out_path.front() = p0; - out_path.back() = p1; - - return Polyline(out_path); -} - -} // namespace Slic3r diff --git a/src/libslic3r/JumpPointSearch.hpp b/src/libslic3r/JumpPointSearch.hpp deleted file mode 100644 index b5fbf6aaf0..0000000000 --- a/src/libslic3r/JumpPointSearch.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once -#include "Line.hpp" -#ifndef SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_ -#define SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_ - -#include "BoundingBox.hpp" -#include "Polygon.hpp" -#include "libslic3r/Layer.hpp" -#include "libslic3r/Point.hpp" -#include "libslic3r/Polyline.hpp" -#include "libslic3r/libslic3r.h" -#include -#include - -namespace Slic3r { - -class JPSPathFinder -{ - using Pixel = Point; - std::unordered_set inpassable; - coordf_t print_z; - BoundingBox max_search_box; - Lines bed_shape; - - const coord_t resolution = scaled(1.5); - Pixel pixelize(const Point &p) { return p / resolution; } - Point unpixelize(const Pixel &p) { return p * resolution; } - -public: - JPSPathFinder() = default; - void init_bed_shape(const Points &bed_shape) { this->bed_shape = (to_lines(Polygon{bed_shape})); }; - void clear(); - void add_obstacles(const Lines &obstacles); - Polyline find_path(const Point &start, const Point &end); -}; - -} // namespace Slic3r - -#endif /* SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_ */ diff --git a/src/libslic3r/MeshBoolean.cpp b/src/libslic3r/MeshBoolean.cpp index 37e3c84b3f..074eff3121 100644 --- a/src/libslic3r/MeshBoolean.cpp +++ b/src/libslic3r/MeshBoolean.cpp @@ -29,8 +29,6 @@ #include #include #include -#include -#include #include #include #include @@ -50,7 +48,6 @@ #include #include #include -#include #include // BBS: for segment #include @@ -658,91 +655,6 @@ bool empty(const CGALMesh &mesh) return mesh.m.is_empty(); } -bool repair(TriangleMesh& mesh, RepairedMeshErrors* repaired_errors, std::string* error) -{ - namespace PMP = CGAL::Polygon_mesh_processing; - - if (mesh.empty()) - return true; - - try { - // 1) Convert to polygon soup - std::vector<_EpicMesh::Point> points; - std::vector> polygons; - - points.reserve(mesh.its.vertices.size()); - polygons.reserve(mesh.its.indices.size()); - - for (const auto& v : mesh.its.vertices) - points.emplace_back(v.x(), v.y(), v.z()); - - for (const auto& f : mesh.its.indices) - polygons.push_back({size_t(f[0]), size_t(f[1]), size_t(f[2])}); - - // 2) Aggressive soup cleanup - PMP::repair_polygon_soup(points, polygons); - - // 3) Convert soup → mesh - _EpicMesh cgal_mesh; - PMP::polygon_soup_to_polygon_mesh(points, polygons, cgal_mesh); - - // 4) Remove degenerate geometry - PMP::remove_degenerate_faces(cgal_mesh); - PMP::remove_isolated_vertices(cgal_mesh); - - // 5) Fix remaining non-manifold vertices - PMP::duplicate_non_manifold_vertices(cgal_mesh); - - // 6) Boolean union (keeps only outer shell) - _EpicMesh tmp; - if (PMP::corefine_and_compute_union(cgal_mesh, cgal_mesh, tmp)) { - cgal_mesh = std::move(tmp); - } - // If it fails, continue anyway with previous mesh - - // 7) Fill holes - if (!CGAL::is_closed(cgal_mesh)) { - using halfedge_descriptor = boost::graph_traits<_EpicMesh>::halfedge_descriptor; - - std::vector borders; - CGAL::extract_boundary_cycles(cgal_mesh, std::back_inserter(borders)); - - for (halfedge_descriptor h : borders) { - PMP::triangulate_and_refine_hole(cgal_mesh, h); - } - } - - // 8) Final validity check - if (!CGAL::is_closed(cgal_mesh)) { - if (error) - *error = "Repair failed: mesh still open after hole filling."; - return false; - } - - // 9) Ensure outward orientation - if (!PMP::does_bound_a_volume(cgal_mesh)) - PMP::orient_to_bound_a_volume(cgal_mesh); - - // 10) Convert back - indexed_triangle_set its = cgal_to_indexed_triangle_set(cgal_mesh); - - RepairedMeshErrors errs{}; - errs.facets_removed = 0; - errs.edges_fixed = 0; - - mesh = TriangleMesh(std::move(its), errs); - - if (repaired_errors) - *repaired_errors = errs; - - return true; - } catch (const std::exception& e) { - if (error) - *error = e.what(); - return false; - } -} - CGALMeshPtr clone(const CGALMesh &m) { return CGALMeshPtr{new CGALMesh{m}}; diff --git a/src/libslic3r/MeshBoolean.hpp b/src/libslic3r/MeshBoolean.hpp index 6ced9b4646..b229a4e724 100644 --- a/src/libslic3r/MeshBoolean.hpp +++ b/src/libslic3r/MeshBoolean.hpp @@ -77,9 +77,6 @@ TriangleMesh merge(std::vector meshes); bool does_bound_a_volume(const CGALMesh &mesh); bool empty(const CGALMesh &mesh); -// Repair a mesh using CGAL. Returns true on success. Optionally returns a summary of repairs and an error string. -bool repair(TriangleMesh &mesh, RepairedMeshErrors *repaired_errors = nullptr, std::string *error = nullptr); - // Real UV unwrap of an open mesh patch via CGAL's LSCM (Least Squares Conformal Maps) surface // parameterization. Returns one UV coordinate per input vertex (same indexing as `mesh.vertices`), // or nullopt if `mesh` isn't a single topological disk -- LSCM needs exactly one connected diff --git a/src/libslic3r/MeshRepair.cpp b/src/libslic3r/MeshRepair.cpp new file mode 100644 index 0000000000..df76836109 --- /dev/null +++ b/src/libslic3r/MeshRepair.cpp @@ -0,0 +1,1215 @@ +#include "MeshRepair.hpp" + +#include "I18N.hpp" +#include "TriangleMesh.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + +namespace { + +namespace PMP = CGAL::Polygon_mesh_processing; + +using Kernel = CGAL::Exact_predicates_inexact_constructions_kernel; +using Point = Kernel::Point_3; +using Vector = Kernel::Vector_3; +using Mesh = CGAL::Surface_mesh; +using Vertex = Mesh::Vertex_index; +using Halfedge = Mesh::Halfedge_index; +using Face = Mesh::Face_index; +using Triangle = std::array; +using Tree = CGAL::AABB_tree>>; +using Box = CGAL::Box_intersection_d::Box_with_info_d; + +constexpr std::size_t NONE = std::size_t(-1); + +class UnionFind +{ +public: + explicit UnionFind(std::size_t n) : m_parent(n) { std::iota(m_parent.begin(), m_parent.end(), std::size_t(0)); } + + std::size_t find(std::size_t i) + { + while (m_parent[i] != i) + i = m_parent[i] = m_parent[m_parent[i]]; + return i; + } + + // The smaller index becomes the root, so results do not depend on the order of unions. + void unite(std::size_t a, std::size_t b) + { + a = find(a); + b = find(b); + if (a != b) + m_parent[std::max(a, b)] = std::min(a, b); + } + +private: + std::vector m_parent; +}; + +template void erase_marked(Container &items, const Mask &marked) +{ + std::size_t out = 0; + for (std::size_t i = 0; i < items.size(); ++i) + if (!marked[i]) + items[out++] = std::move(items[i]); + items.resize(out); +} + +template void parallel_for_each(std::size_t n, Fn &&fn) +{ + tbb::parallel_for(tbb::blocked_range(0, n, 1), [&fn](const tbb::blocked_range &range) { + for (std::size_t i = range.begin(); i < range.end(); ++i) + fn(i); + }); +} + +// Triangles sharing a vertex belong to one part. +UnionFind connect_vertices(std::size_t num_points, const std::vector &triangles) +{ + UnionFind parts(num_points); + for (const Triangle &t : triangles) { + parts.unite(t[0], t[1]); + parts.unite(t[0], t[2]); + } + return parts; +} + +bool bbox_contains(const CGAL::Bbox_3 &outer, const CGAL::Bbox_3 &inner, double tolerance = 0.) +{ + return outer.xmin() - tolerance <= inner.xmin() && outer.ymin() - tolerance <= inner.ymin() && outer.zmin() - tolerance <= inner.zmin() && + outer.xmax() + tolerance >= inner.xmax() && outer.ymax() + tolerance >= inner.ymax() && outer.zmax() + tolerance >= inner.zmax(); +} + +std::vector> overlapping_boxes(std::vector &boxes) +{ + std::vector> pairs; + CGAL::box_self_intersection_d(boxes.begin(), boxes.end(), [&pairs](const Box &a, const Box &b) { + pairs.emplace_back(std::min(a.info(), b.info()), std::max(a.info(), b.info())); + }, std::ptrdiff_t(10), CGAL::Box_intersection_d::CLOSED); + std::sort(pairs.begin(), pairs.end()); + return pairs; +} + +double bbox_diagonal(const std::vector &points) +{ + CGAL::Bbox_3 bb; + for (const Point &p : points) + bb += p.bbox(); + return points.empty() ? 0. : std::sqrt(CGAL::square(bb.x_span()) + CGAL::square(bb.y_span()) + CGAL::square(bb.z_span())); +} + +// One use of an edge by a triangle: a <= b are its vertices, corner = 3 * triangle + position of the edge's first vertex. +struct EdgeUse +{ + std::size_t a, b, corner; + bool operator<(const EdgeUse &rhs) const { return std::tie(a, b, corner) < std::tie(rhs.a, rhs.b, rhs.corner); } +}; + +std::vector sorted_edge_uses(const std::vector &triangles) +{ + std::vector uses; + uses.reserve(3 * triangles.size()); + for (std::size_t f = 0; f < triangles.size(); ++f) + for (std::size_t k = 0; k < 3; ++k) { + const auto [a, b] = std::minmax(triangles[f][k], triangles[f][(k + 1) % 3]); + uses.push_back({a, b, 3 * f + k}); + } + tbb::parallel_sort(uses.begin(), uses.end()); + return uses; +} + +// Calls fn(first, last) with the range of uses of each edge. +template void for_each_edge(const std::vector &uses, Fn &&fn) +{ + for (std::size_t i = 0; i < uses.size();) { + std::size_t j = i + 1; + while (j < uses.size() && uses[j].a == uses[i].a && uses[j].b == uses[i].b) + ++j; + fn(i, j); + i = j; + } +} + +bool same_direction(const std::vector &triangles, const EdgeUse &e1, const EdgeUse &e2) +{ + return triangles[e1.corner / 3][e1.corner % 3] == triangles[e2.corner / 3][e2.corner % 3]; +} + +struct SoupTopology +{ + std::vector open_face; // on an edge used by one triangle + std::vector bad_face; // on an edge used by more than two triangles, or twice in one direction + std::vector pinched; // vertex whose triangles form several fans + std::vector border; // vertices of the edges used by one triangle + double open_fraction = 0.; +}; + +// The fans around each vertex are found by joining the corners of triangles across their shared edges. +SoupTopology analyze_topology(std::size_t num_points, const std::vector &triangles) +{ + SoupTopology topology{std::vector(triangles.size(), 0), std::vector(triangles.size(), 0), std::vector(num_points, 0)}; + const std::vector uses = sorted_edge_uses(triangles); + UnionFind fans(3 * triangles.size()); + std::size_t num_edges = 0; + for_each_edge(uses, [&](std::size_t i, std::size_t j) { + const EdgeUse &e = uses[i]; + ++num_edges; + if (j - i == 1 && e.a != e.b) { + topology.open_face[e.corner / 3] = 1; + topology.border.push_back(e.a); + topology.border.push_back(e.b); + } else if (j - i != 2 || e.a == e.b || same_direction(triangles, e, uses[i + 1])) { + for (std::size_t k = i; k < j; ++k) + topology.bad_face[uses[k].corner / 3] = 1; + } else { + const std::size_t c = e.corner, n = uses[i + 1].corner; + fans.unite(c, n - n % 3 + (n + 1) % 3); + fans.unite(c - c % 3 + (c + 1) % 3, n); + } + }); + topology.open_fraction = num_edges == 0 ? 0. : double(topology.border.size() / 2) / double(num_edges); + std::sort(topology.border.begin(), topology.border.end()); + topology.border.erase(std::unique(topology.border.begin(), topology.border.end()), topology.border.end()); + + std::vector fan_of(num_points, NONE); + for (std::size_t c = 0; c < 3 * triangles.size(); ++c) { + const std::size_t v = triangles[c / 3][c % 3], root = fans.find(c); + if (fan_of[v] == NONE) + fan_of[v] = root; + else if (fan_of[v] != root) + topology.pinched[v] = 1; + } + return topology; +} + +// orient_polygon_soup() needs resizable polygons. +void orient_with_cgal(std::vector &points, std::vector &triangles) +{ + std::vector> polygons(triangles.size()); + for (std::size_t i = 0; i < triangles.size(); ++i) + polygons[i].assign(triangles[i].begin(), triangles[i].end()); + PMP::orient_polygon_soup(points, polygons); + for (std::size_t i = 0; i < triangles.size(); ++i) + std::copy(polygons[i].begin(), polygons[i].end(), triangles[i].begin()); +} + +// The triangles of a group renumbered over the vertices they use. +void extract_group(const std::vector &points, const std::vector &triangles, const std::vector &group, + std::vector &group_points, std::vector &group_triangles) +{ + std::vector vertices; + vertices.reserve(3 * group.size()); + for (std::size_t f : group) + vertices.insert(vertices.end(), triangles[f].begin(), triangles[f].end()); + std::sort(vertices.begin(), vertices.end()); + vertices.erase(std::unique(vertices.begin(), vertices.end()), vertices.end()); + group_points.clear(); + for (std::size_t v : vertices) + group_points.push_back(points[v]); + group_triangles.clear(); + for (std::size_t f : group) { + Triangle &t = group_triangles.emplace_back(); + for (std::size_t k = 0; k < 3; ++k) + t[k] = std::lower_bound(vertices.begin(), vertices.end(), triangles[f][k]) - vertices.begin(); + } +} + +// Groups not known to be valid polygon meshes are made into ones first. +std::vector soup_to_meshes(const std::vector &points, const std::vector &triangles, + const std::vector> &groups, bool known_valid) +{ + std::vector meshes(groups.size()); + parallel_for_each(groups.size(), [&](std::size_t i) { + std::vector group_points; + std::vector group_triangles; + extract_group(points, triangles, groups[i], group_points, group_triangles); + if (!known_valid && !PMP::is_polygon_soup_a_polygon_mesh(group_triangles)) + orient_with_cgal(group_points, group_triangles); + PMP::polygon_soup_to_polygon_mesh(group_points, group_triangles, meshes[i]); + }); + return meshes; +} + +std::vector split_shells(Mesh &mesh) +{ + mesh.collect_garbage(); + std::vector face_part(mesh.number_of_faces()); + const std::size_t num_parts = PMP::connected_components(mesh, CGAL::make_property_map(face_part)); + std::vector part_size(num_parts, 0); + for (std::size_t part : face_part) + ++part_size[part]; + const std::size_t largest = std::max_element(part_size.begin(), part_size.end()) - part_size.begin(); + + // The small shells are copied out and the largest one stays in place. + std::vector shells; + if (num_parts > 1) { + std::vector points; + for (Vertex v : mesh.vertices()) + points.push_back(mesh.point(v)); + std::vector triangles; + std::vector> groups(num_parts); + for (Face f : mesh.faces()) + if (face_part[f] != largest) { + Triangle &t = triangles.emplace_back(); + std::size_t k = 0; + for (Vertex v : CGAL::vertices_around_face(mesh.halfedge(f), mesh)) + t[k++] = v; + groups[face_part[f]].push_back(triangles.size() - 1); + } + groups.erase(groups.begin() + largest); + std::vector nonmanifold; + PMP::non_manifold_vertices(mesh, std::back_inserter(nonmanifold)); + shells = soup_to_meshes(points, triangles, groups, nonmanifold.empty()); + PMP::keep_connected_components(mesh, std::vector{largest}, CGAL::make_property_map(face_part)); + mesh.collect_garbage(); + } + shells.insert(shells.begin(), std::move(mesh)); + return shells; +} + +// Valid closed parts skip the soup repair, which would weld them to coincident parts into non-manifold junctions. +std::vector take_closed_shells(const std::vector &points, std::vector &triangles) +{ + const SoupTopology topology = analyze_topology(points.size(), triangles); + UnionFind parts = connect_vertices(points.size(), triangles); + std::vector part_ok(points.size(), 1); + for (std::size_t f = 0; f < triangles.size(); ++f) + for (std::size_t v : triangles[f]) + if (topology.open_face[f] || topology.bad_face[f] || topology.pinched[v]) + part_ok[parts.find(v)] = 0; + + std::vector group_of(points.size(), NONE); + std::vector> groups; + std::vector taken(triangles.size(), 0); + for (std::size_t f = 0; f < triangles.size(); ++f) + if (const std::size_t r = parts.find(triangles[f][0]); part_ok[r]) { + if (group_of[r] == NONE) { + group_of[r] = groups.size(); + groups.emplace_back(); + } + groups[group_of[r]].push_back(f); + taken[f] = 1; + } + std::vector shells = soup_to_meshes(points, triangles, groups, true); + erase_marked(triangles, taken); + return shells; +} + +// Opposite coincident triangles cancel in pairs, unless that empties a whole region (a two-sided export). +void remove_duplicate_triangles(std::size_t num_points, std::vector &triangles) +{ + struct Entry + { + std::array key; + bool odd; + std::size_t idx; + }; + std::vector entries; + for (std::size_t i = 0; i < triangles.size(); ++i) { + const Triangle &t = triangles[i]; + const std::size_t r = std::min_element(t.begin(), t.end()) - t.begin(); + const std::size_t b = t[(r + 1) % 3], c = t[(r + 2) % 3]; + entries.push_back({{t[r], std::min(b, c), std::max(b, c)}, b > c, i}); + } + tbb::parallel_sort(entries.begin(), entries.end(), [](const Entry &l, const Entry &r) { return std::tie(l.key, l.idx) < std::tie(r.key, r.idx); }); + + std::vector remove(triangles.size(), 0); + std::vector cancelled; + for (std::size_t i = 0; i < entries.size();) { + std::size_t j = i + 1; + while (j < entries.size() && entries[j].key == entries[i].key) + ++j; + if (j - i > 1) { + std::size_t num_odd = 0; + for (std::size_t k = i; k < j; ++k) + num_odd += entries[k].odd; + const bool odd_wins = 2 * num_odd > j - i; + bool kept = 2 * num_odd == j - i; + if (kept) + cancelled.push_back(entries[i].idx); + for (std::size_t k = i; k < j; ++k) { + if (!kept && entries[k].odd == odd_wins) + kept = true; + else + remove[entries[k].idx] = 1; + } + } + i = j; + } + if (!cancelled.empty()) { + UnionFind regions = connect_vertices(num_points, triangles); + std::vector survives(num_points, 0); + for (std::size_t i = 0; i < triangles.size(); ++i) + if (!remove[i]) + survives[regions.find(triangles[i][0])] = 1; + for (std::size_t i : cancelled) + if (!survives[regions.find(triangles[i][0])]) + remove[i] = 0; + } + erase_marked(triangles, remove); +} + +void clean_soup(std::vector &points, std::vector &triangles) +{ + std::vector order(points.size()); + std::iota(order.begin(), order.end(), std::size_t(0)); + tbb::parallel_sort(order.begin(), order.end(), [&points](std::size_t a, std::size_t b) { + return std::make_tuple(points[a].x(), points[a].y(), points[a].z(), a) < std::make_tuple(points[b].x(), points[b].y(), points[b].z(), b); + }); + std::vector merged(points.size()); + for (std::size_t i = 0; i < order.size(); ++i) + merged[order[i]] = i > 0 && points[order[i]] == points[order[i - 1]] ? merged[order[i - 1]] : order[i]; + std::vector degenerate(triangles.size(), 0); + for (std::size_t f = 0; f < triangles.size(); ++f) { + Triangle &t = triangles[f]; + for (std::size_t &v : t) + v = merged[v]; + degenerate[f] = t[0] == t[1] || t[1] == t[2] || t[0] == t[2]; + } + erase_marked(triangles, degenerate); + remove_duplicate_triangles(points.size(), triangles); + + std::vector compact(points.size(), NONE); + std::vector used; + for (Triangle &t : triangles) + for (std::size_t &v : t) { + if (compact[v] == NONE) { + compact[v] = used.size(); + used.push_back(points[v]); + } + v = compact[v]; + } + points = std::move(used); +} + +// Closes cracks between open edges; no vertex moves by more than a quarter of its shortest edge. +std::size_t weld_border_vertices(const std::vector &points, std::vector &triangles, const std::vector &border, + double tolerance) +{ + if (border.empty()) + return 0; + std::vector radius(points.size(), tolerance); + for (const Triangle &t : triangles) + for (std::size_t k = 0; k < 3; ++k) { + const double r = 0.25 * std::sqrt(CGAL::squared_distance(points[t[k]], points[t[(k + 1) % 3]])); + radius[t[k]] = std::min(radius[t[k]], r); + radius[t[(k + 1) % 3]] = std::min(radius[t[(k + 1) % 3]], r); + } + + using Cell = std::array; + auto cell_of = [tolerance](const Point &p) { + return Cell{int64_t(std::floor(p.x() / tolerance)), int64_t(std::floor(p.y() / tolerance)), int64_t(std::floor(p.z() / tolerance))}; + }; + std::vector> grid; + for (std::size_t v : border) + grid.emplace_back(cell_of(points[v]), v); + tbb::parallel_sort(grid.begin(), grid.end()); + + UnionFind uf(points.size()); + for (const auto &[c, v] : grid) + for (int64_t dx = -1; dx <= 1; ++dx) + for (int64_t dy = -1; dy <= 1; ++dy) + for (int64_t dz = -1; dz <= 1; ++dz) { + const Cell n{c[0] + dx, c[1] + dy, c[2] + dz}; + for (auto it = std::lower_bound(grid.begin(), grid.end(), std::make_pair(n, std::size_t(0))); it != grid.end() && it->first == n; ++it) + if (const std::size_t u = it->second; + u < v && CGAL::squared_distance(points[u], points[v]) <= CGAL::square(std::min(radius[u], radius[v]))) + uf.unite(u, v); + } + std::size_t welded = 0; + for (std::size_t v : border) + welded += uf.find(v) != v; + if (welded > 0) + for (Triangle &t : triangles) + for (std::size_t &v : t) + v = uf.find(v); + return welded; +} + +// Orients each part as most of its area was and gives each fan its own vertex; false if the mesh is still invalid. +bool orient_soup(std::vector &points, std::vector &triangles, std::size_t &reversed_parts) +{ + const std::size_t num_faces = triangles.size(); + const std::vector uses = sorted_edge_uses(triangles); + std::vector twin(3 * num_faces, NONE); + std::vector> cut; // edges used by more than two triangles + for_each_edge(uses, [&](std::size_t i, std::size_t j) { + if (j - i == 2) { + twin[uses[i].corner] = uses[i + 1].corner; + twin[uses[i + 1].corner] = uses[i].corner; + } else if (j - i > 2) + cut.emplace_back(i, j); + }); + + std::vector flip(num_faces, -1); + std::vector part_of(num_faces); + std::vector vote; + std::vector conflict(3 * num_faces, 0); + std::vector stack; + for (std::size_t seed = 0; seed < num_faces; ++seed) { + if (flip[seed] >= 0) + continue; + const std::size_t part = vote.size(); + vote.push_back(0.); + flip[seed] = 0; + stack.push_back(seed); + while (!stack.empty()) { + const std::size_t f = stack.back(); + stack.pop_back(); + part_of[f] = part; + const Triangle &t = triangles[f]; + const double area = std::sqrt(CGAL::squared_area(points[t[0]], points[t[1]], points[t[2]])); + vote[part] += flip[f] ? -area : area; + for (std::size_t k = 0; k < 3; ++k) { + const std::size_t c = 3 * f + k, n = twin[c]; + if (n == NONE) + continue; + const std::size_t g = n / 3; + // Triangles agree when they run along their shared edge in opposite directions. + const signed char want = flip[f] ^ (t[k] == triangles[g][n % 3]); + if (flip[g] < 0) { + flip[g] = want; + stack.push_back(g); + } else if (flip[g] != want) + conflict[c] = conflict[n] = 1; + } + } + } + + // Corners keep their input vertex in original, through the flips below. + std::vector original(3 * num_faces); + std::vector part_reversed(vote.size(), 0); + for (std::size_t f = 0; f < num_faces; ++f) { + if (flip[f] != (vote[part_of[f]] < 0.)) { + std::swap(triangles[f][1], triangles[f][2]); + part_reversed[part_of[f]] = 1; + } + for (std::size_t k = 0; k < 3; ++k) + original[3 * f + k] = triangles[f][k]; + } + reversed_parts = std::count(part_reversed.begin(), part_reversed.end(), 1); + + auto corner_of = [&original](std::size_t f, std::size_t v) { return 3 * f + (original[3 * f] == v ? 0 : original[3 * f + 1] == v ? 1 : 2); }; + UnionFind fans(3 * num_faces); + for (const EdgeUse &e : uses) + if (const std::size_t n = twin[e.corner]; n != NONE && e.corner < n && !conflict[e.corner]) { + fans.unite(corner_of(e.corner / 3, e.a), corner_of(n / 3, e.a)); + fans.unite(corner_of(e.corner / 3, e.b), corner_of(n / 3, e.b)); + } + std::vector fan_vertex(3 * num_faces, NONE); + std::vector used(points.size(), 0); + for (std::size_t c = 0; c < 3 * num_faces; ++c) { + std::size_t © = fan_vertex[fans.find(c)]; + if (copy == NONE) { + const std::size_t v = original[c]; + copy = used[v] ? points.size() : v; + if (used[v]) + points.push_back(points[v]); + used[v] = 1; + } + triangles[c / 3][c % 3] = copy; + } + + // Only edges that were cut can still be shared wrongly: by more than two triangles, or twice in one direction. + for (std::size_t k = 0; k < uses.size(); ++k) + if (conflict[uses[k].corner] && uses[k].corner < twin[uses[k].corner]) + cut.emplace_back(k, k + 2); + for (const auto &[i, j] : cut) { + // Each use as its copied endpoints and whether it runs from a to b. + std::vector> copies; + for (std::size_t k = i; k < j; ++k) { + const std::size_t f = uses[k].corner / 3, ka = corner_of(f, uses[k].a) % 3, kb = corner_of(f, uses[k].b) % 3; + const auto [ca, cb] = std::minmax(triangles[f][ka], triangles[f][kb]); + copies.push_back({ca, cb, std::size_t((ka + 1) % 3 == kb)}); + } + std::sort(copies.begin(), copies.end()); + for (std::size_t k = 0; k + 1 < copies.size(); ++k) + if (copies[k][0] == copies[k + 1][0] && copies[k][1] == copies[k + 1][1] && + (copies[k][2] == copies[k + 1][2] || (k + 2 < copies.size() && copies[k + 2][0] == copies[k][0] && copies[k + 2][1] == copies[k][1]))) + return false; + } + return true; +} + +// orient_polygon_soup() orients each part arbitrarily; give it back the orientation most of its input had. +std::size_t keep_input_orientation(Mesh &mesh, const std::vector &points_before, const std::vector &triangles_before, + const std::vector &points, const std::vector &triangles) +{ + if (mesh.number_of_faces() != triangles.size()) + return 0; + auto fcc = mesh.add_property_map("f:repair_part").first; + const std::size_t num_parts = PMP::connected_components(mesh, fcc); + std::vector vote(num_parts, 0.); + std::size_t i = 0; + for (Face f : mesh.faces()) { + const Triangle &a = triangles_before[i]; + const Triangle &b = triangles[i++]; + bool same = false; + for (std::size_t r = 0; r < 3 && !same; ++r) { + same = true; + for (std::size_t k = 0; k < 3 && same; ++k) + same = points_before[a[k]] == points[b[(k + r) % 3]]; + } + vote[fcc[f]] += same ? PMP::face_area(f, mesh) : -PMP::face_area(f, mesh); + } + std::vector to_reverse; + for (Face f : mesh.faces()) + if (vote[fcc[f]] < 0.) + to_reverse.push_back(f); + mesh.remove_property_map(fcc); + if (!to_reverse.empty()) + PMP::reverse_face_orientations(to_reverse, mesh); + return std::count_if(vote.begin(), vote.end(), [](double v) { return v < 0.; }); +} + +void close_cracks(Mesh &mesh, double tolerance) +{ + PMP::merge_reversible_connected_components(mesh); + PMP::stitch_borders(mesh); + if (CGAL::is_closed(mesh)) + return; + // T-junctions, which welding cannot close. + PMP::experimental::snap_borders(mesh, CGAL::Constant_property_map(tolerance)); + PMP::stitch_borders(mesh); +} + +struct BorderLoop +{ + Halfedge halfedge; + std::vector halfedges; + Vector normal; // unit normal a face closing the loop would have + double offset = 0.; + bool planar = false; + CGAL::Bbox_3 bbox; +}; + +struct CdtFaceInfo +{ + int nesting = -1; +}; +using CdtVb = CGAL::Triangulation_vertex_base_with_info_2; +using CdtFb = CGAL::Constrained_triangulation_face_base_2>; +using Cdt = CGAL::Constrained_Delaunay_triangulation_2, CGAL::No_constraint_intersection_tag>; + +// Number of constrained edges crossed to reach each face from the outside. +void mark_nesting(Cdt &cdt) +{ + std::vector> seeds{{cdt.infinite_face(), 0}}; + while (!seeds.empty()) { + auto [seed, level] = seeds.back(); + seeds.pop_back(); + if (seed->info().nesting != -1) + continue; + seed->info().nesting = level; + std::vector queue{seed}; + while (!queue.empty()) { + Cdt::Face_handle f = queue.back(); + queue.pop_back(); + for (int i = 0; i < 3; ++i) { + Cdt::Face_handle n = f->neighbor(i); + if (n->info().nesting != -1) + continue; + if (cdt.is_constrained({f, i})) + seeds.emplace_back(n, level + 1); + else { + n->info().nesting = level; + queue.push_back(n); + } + } + } + } +} + +// Fills a planar region bounded by nested loops, such as a missing annulus, leaving its openings open. +bool fill_planar_region(Mesh &mesh, const std::vector &loops) +{ + const Vector &n = loops.front()->normal; + const Vector u = std::abs(n.x()) < 0.9 ? CGAL::cross_product(n, Vector(1, 0, 0)) : CGAL::cross_product(n, Vector(0, 1, 0)); + const Vector e1 = u / std::sqrt(u.squared_length()); + const Vector e2 = CGAL::cross_product(n, e1); + + Cdt cdt; + std::size_t num_vertices = 0; + std::set> border_edges; + try { + for (const BorderLoop *loop : loops) { + std::vector vh; + for (Halfedge h : loop->halfedges) { + const Vector p = mesh.point(mesh.source(h)) - CGAL::ORIGIN; + vh.push_back(cdt.insert(Kernel::Point_2(p * e1, p * e2))); + vh.back()->info() = mesh.source(h); + border_edges.emplace(mesh.source(h), mesh.target(h)); + } + num_vertices += vh.size(); + for (std::size_t k = 0; k < vh.size(); ++k) + cdt.insert_constraint(vh[k], vh[(k + 1) % vh.size()]); + } + } catch (const std::exception &) { + return false; + } + // Loop vertices projecting onto the same point were merged by the triangulation. + if (cdt.number_of_vertices() != num_vertices) + return false; + mark_nesting(cdt); + + std::vector> patch; + std::set> patch_edges; + for (Cdt::Face_handle f : cdt.finite_face_handles()) + if (f->info().nesting % 2 == 1) { + const std::array &t = patch.emplace_back(std::array{f->vertex(0)->info(), f->vertex(1)->info(), f->vertex(2)->info()}); + for (int k = 0; k < 3; ++k) + patch_edges.emplace(t[k], t[(k + 1) % 3]); + } + // The patch has to close every loop with the orientation of the triangles around it. + for (const auto &e : border_edges) + if (patch_edges.count(e) == 0) + return false; + + std::vector added; + for (const auto &t : patch) { + const Face f = mesh.add_face(t[0], t[1], t[2]); + if (f == Mesh::null_face()) { + for (Face a : added) + mesh.remove_face(a); + return false; + } + added.push_back(f); + } + return true; +} + +// Last resort for a loop the hole filling could not close: a fan around its centroid. +void fill_with_fan(Mesh &mesh, const BorderLoop &loop) +{ + Vector sum(0, 0, 0); + for (Halfedge h : loop.halfedges) + sum = sum + (mesh.point(mesh.source(h)) - CGAL::ORIGIN); + const Vertex c = mesh.add_vertex(CGAL::ORIGIN + sum / double(loop.halfedges.size())); + for (Halfedge h : loop.halfedges) + mesh.add_face(mesh.source(h), mesh.target(h), c); +} + +std::size_t fill_holes(Mesh &mesh, double tolerance) +{ + std::vector borders; + CGAL::extract_boundary_cycles(mesh, std::back_inserter(borders)); + std::vector loops(borders.size()); + for (std::size_t i = 0; i < borders.size(); ++i) { + BorderLoop &loop = loops[i]; + loop.halfedge = borders[i]; + Vector newell(0, 0, 0); + for (Halfedge h : CGAL::halfedges_around_face(borders[i], mesh)) { + loop.halfedges.push_back(h); + newell = newell + CGAL::cross_product(mesh.point(mesh.source(h)) - CGAL::ORIGIN, mesh.point(mesh.target(h)) - CGAL::ORIGIN); + loop.bbox += mesh.point(mesh.source(h)).bbox(); + } + if (const double len = std::sqrt(newell.squared_length()); len > 0.) { + loop.normal = newell / len; + loop.offset = (mesh.point(mesh.source(loop.halfedge)) - CGAL::ORIGIN) * loop.normal; + loop.planar = std::all_of(loop.halfedges.begin(), loop.halfedges.end(), [&](Halfedge h) { + return std::abs((mesh.point(mesh.source(h)) - CGAL::ORIGIN) * loop.normal - loop.offset) <= tolerance; + }); + } + } + + // Planar loops facing each other in the same plane, one inside the other, bound one region. + UnionFind regions(loops.size()); + std::vector grouped(loops.size(), 0); + for (std::size_t a = 0; a < loops.size(); ++a) + for (std::size_t b = 0; b < loops.size(); ++b) { + const BorderLoop &la = loops[a], &lb = loops[b]; + if (a == b || !la.planar || !lb.planar || la.normal * lb.normal > -0.99 || !bbox_contains(la.bbox, lb.bbox, tolerance)) + continue; + if (std::all_of(lb.halfedges.begin(), lb.halfedges.end(), [&](Halfedge h) { + return std::abs((mesh.point(mesh.source(h)) - CGAL::ORIGIN) * la.normal - la.offset) <= tolerance; + })) { + regions.unite(a, b); + grouped[a] = grouped[b] = 1; + } + } + std::map> groups; + for (std::size_t i = 0; i < loops.size(); ++i) + if (grouped[i]) + groups[regions.find(i)].push_back(i); + + std::size_t filled = 0; + std::vector done(loops.size(), 0); + for (auto &[root, members] : groups) { + // The outermost loop sets the orientation of the region. + auto extent = [&loops](std::size_t i) { return loops[i].bbox.x_span() + loops[i].bbox.y_span() + loops[i].bbox.z_span(); }; + std::sort(members.begin(), members.end(), [&extent](std::size_t l, std::size_t r) { return extent(l) > extent(r); }); + std::vector group; + for (std::size_t i : members) + group.push_back(&loops[i]); + if (fill_planar_region(mesh, group)) { + filled += members.size(); + for (std::size_t i : members) + done[i] = 1; + } + } + + for (std::size_t i = 0; i < loops.size(); ++i) { + if (done[i]) + continue; + ++filled; + std::vector patch; + // Searching all triangles is cubic in the loop length; the search among Delaunay ones varies from run to run. + const std::size_t size = loops[i].halfedges.size(); + PMP::triangulate_hole(mesh, loops[i].halfedge, + CGAL::parameters::use_2d_constrained_delaunay_triangulation(true) + .use_delaunay_triangulation(size > 100) + .do_not_use_cubic_algorithm(size > 200) + .face_output_iterator(std::back_inserter(patch))); + if (patch.empty() && CGAL::is_border(loops[i].halfedge, mesh)) + fill_with_fan(mesh, loops[i]); + } + return filled; +} + +double signed_volume(const Mesh &mesh) +{ + double vol = 0.; + for (Face f : mesh.faces()) { + const Halfedge h = mesh.halfedge(f); + vol += CGAL::determinant(mesh.point(mesh.source(h)) - CGAL::ORIGIN, mesh.point(mesh.target(h)) - CGAL::ORIGIN, + mesh.point(mesh.target(mesh.next(h))) - CGAL::ORIGIN); + } + return vol / 6.; +} + +// Returns the pairs removed. Widespread overlaps are left to slicing: remeshing them can cut a part down to a sliver. +std::size_t remove_local_self_intersections(Mesh &shell) +{ + std::vector> pairs; + PMP::self_intersections(shell, std::back_inserter(pairs)); + if (pairs.empty()) + return 0; + std::set faces; + for (const auto &[a, b] : pairs) { + faces.insert(a); + faces.insert(b); + } + if (20 * faces.size() > shell.number_of_faces()) + return 0; + const double volume = signed_volume(shell); + Mesh backup = shell; + try { + PMP::experimental::remove_self_intersections(faces, shell, CGAL::parameters::preserve_genus(false)); + shell.collect_garbage(); + if (CGAL::is_valid_polygon_mesh(shell) && CGAL::is_closed(shell) && std::abs(signed_volume(shell) - volume) <= 1e-3 * std::abs(volume)) + return pairs.size(); + } catch (const std::exception &) { + } + shell = std::move(backup); + return 0; +} + +bool surfaces_intersect(const Mesh &mesh, const Tree &other) +{ + for (Face f : mesh.faces()) { + const Halfedge h = mesh.halfedge(f); + if (other.do_intersect(Kernel::Triangle_3(mesh.point(mesh.source(h)), mesh.point(mesh.target(h)), mesh.point(mesh.target(mesh.next(h)))))) + return true; + } + return false; +} + +Point inner_sample(const Mesh &shell) +{ + Face best; + double best_area = -1.; + for (Face f : shell.faces()) + if (const double a = PMP::face_area(f, shell); a > best_area) { + best_area = a; + best = f; + } + const Halfedge h = shell.halfedge(best); + return CGAL::centroid(shell.point(shell.source(h)), shell.point(shell.target(h)), shell.point(shell.target(shell.next(h)))); +} + +// Orients and filters the shells by the nonzero winding rule of slicing. Crossing shells stay apart, as parts do. +void resolve_shells(std::vector &shells, MeshRepairStats &stats, const std::function &progress) +{ + const std::size_t n = shells.size(); + std::vector bbox(n); + std::vector sign(n); + std::vector boxes; + for (std::size_t i = 0; i < n; ++i) { + bbox[i] = PMP::bbox(shells[i]); + sign[i] = signed_volume(shells[i]) < 0. ? -1 : 1; + boxes.emplace_back(bbox[i], i); + } + const std::vector> overlaps = overlapping_boxes(boxes); + + std::vector> enclosing(n); + std::vector needs_tree(n, 0); + for (const auto &[a, b] : overlaps) { + needs_tree[shells[a].number_of_faces() < shells[b].number_of_faces() ? b : a] = 1; + if (bbox_contains(bbox[a], bbox[b])) + enclosing[b].push_back(a); + if (bbox_contains(bbox[b], bbox[a])) + enclosing[a].push_back(b); + } + for (const std::vector &outer : enclosing) + for (std::size_t k : outer) + needs_tree[k] = 1; + // Built before any query: an AABB tree finishes its construction lazily on the first one. + std::vector> trees(n); + parallel_for_each(n, [&](std::size_t i) { + if (needs_tree[i]) { + trees[i] = std::make_unique(shells[i].faces().begin(), shells[i].faces().end(), shells[i]); + trees[i]->build(); + } + }); + + std::vector hit(overlaps.size(), 0); + parallel_for_each(overlaps.size(), [&](std::size_t i) { + const auto [a, b] = overlaps[i]; + const bool a_smaller = shells[a].number_of_faces() < shells[b].number_of_faces(); + hit[i] = surfaces_intersect(shells[a_smaller ? a : b], *trees[a_smaller ? b : a]); + }); + std::vector crossing(n, 0); + for (std::size_t i = 0; i < overlaps.size(); ++i) + if (hit[i]) + crossing[overlaps[i].first] = crossing[overlaps[i].second] = 1; + progress(95); + + // Crossing shells are solids; any other shell must separate zero from nonzero winding. + std::vector drop(n, 0); + std::vector target(n, 1); + parallel_for_each(n, [&](std::size_t j) { + if (crossing[j]) + return; + const Point sample = inner_sample(shells[j]); + int winding = 0; + for (std::size_t k : enclosing[j]) + if (CGAL::Side_of_triangle_mesh(*trees[k])(sample) == CGAL::ON_BOUNDED_SIDE) + winding += crossing[k] ? 1 : sign[k]; + const bool solid_outside = winding != 0; + const bool solid_inside = winding + sign[j] != 0; + drop[j] = solid_outside == solid_inside; + target[j] = solid_inside ? 1 : -1; + }); + trees.clear(); + + for (std::size_t i = 0; i < n; ++i) + if (drop[i]) + ++stats.removed_shells; + else if (target[i] != sign[i]) { + PMP::reverse_face_orientations(shells[i]); + ++stats.reversed_shells; + } + erase_marked(shells, drop); +} + +// Connected parts of the soup; open parts with touching bounding boxes share a unit, so cracks between them close. +std::vector> soup_units(const std::vector &points, const std::vector &triangles, + const SoupTopology &topology, double tolerance) +{ + UnionFind parts = connect_vertices(points.size(), triangles); + std::vector part_index(points.size(), NONE); + std::vector part_of_face(triangles.size()); + std::vector part_bbox; + std::vector part_open; + for (std::size_t f = 0; f < triangles.size(); ++f) { + std::size_t &index = part_index[parts.find(triangles[f][0])]; + if (index == NONE) { + index = part_bbox.size(); + part_bbox.emplace_back(); + part_open.push_back(0); + } + part_of_face[f] = index; + for (std::size_t v : triangles[f]) + part_bbox[index] += points[v].bbox(); + part_open[index] = part_open[index] || topology.open_face[f]; + } + + std::vector boxes; + for (std::size_t i = 0; i < part_bbox.size(); ++i) + if (part_open[i]) { + const CGAL::Bbox_3 &b = part_bbox[i]; + boxes.emplace_back(CGAL::Bbox_3(b.xmin() - tolerance, b.ymin() - tolerance, b.zmin() - tolerance, b.xmax() + tolerance, + b.ymax() + tolerance, b.zmax() + tolerance), i); + } + UnionFind units(part_bbox.size()); + for (const auto &[a, b] : overlapping_boxes(boxes)) + units.unite(a, b); + + std::vector unit_index(part_bbox.size(), NONE); + std::vector> out; + for (std::size_t f = 0; f < triangles.size(); ++f) { + std::size_t &index = unit_index[units.find(part_of_face[f])]; + if (index == NONE) { + index = out.size(); + out.emplace_back(); + } + out[index].push_back(f); + } + return out; +} + +struct UnitResult +{ + std::vector shells; + std::size_t reversed = 0; + std::size_t filled = 0; + bool closed = true; +}; + +UnitResult repair_unit(const std::vector &points, const std::vector &triangles, const std::vector &faces, + const SoupTopology &topology, double tolerance) +{ + std::vector unit_points; + std::vector unit_triangles; + extract_group(points, triangles, faces, unit_points, unit_triangles); + bool oriented_manifold = true; + for (std::size_t f : faces) { + oriented_manifold = oriented_manifold && !topology.bad_face[f]; + for (std::size_t v : triangles[f]) + oriented_manifold = oriented_manifold && !topology.pinched[v]; + } + + UnitResult result; + Mesh mesh; + if (oriented_manifold) + PMP::polygon_soup_to_polygon_mesh(unit_points, unit_triangles, mesh); + else { + std::vector oriented_points = unit_points; + std::vector oriented_triangles = unit_triangles; + if (orient_soup(oriented_points, oriented_triangles, result.reversed)) + PMP::polygon_soup_to_polygon_mesh(oriented_points, oriented_triangles, mesh); + else { + const std::vector points_before = unit_points; + const std::vector triangles_before = unit_triangles; + orient_with_cgal(unit_points, unit_triangles); + PMP::polygon_soup_to_polygon_mesh(unit_points, unit_triangles, mesh); + result.reversed = keep_input_orientation(mesh, points_before, triangles_before, unit_points, unit_triangles); + } + } + + if (!CGAL::is_closed(mesh)) + close_cracks(mesh, tolerance); + PMP::remove_degenerate_faces(mesh); + if (!CGAL::is_closed(mesh)) { + // Rejoin vertices the orientation split where holes break their fan, so each gap is one loop. + PMP::merge_duplicated_vertices_in_boundary_cycles(mesh); + result.filled = fill_holes(mesh, tolerance); + PMP::remove_degenerate_faces(mesh); + } + PMP::remove_isolated_vertices(mesh); + mesh.collect_garbage(); + result.closed = CGAL::is_closed(mesh); + if (result.closed && !mesh.is_empty()) + result.shells = split_shells(mesh); + return result; +} + +// Returns false when no closed shell was left. +bool repair_soup(std::vector &points, std::vector &triangles, std::vector &shells, MeshRepairStats &stats, + const std::function &progress) +{ + const double tolerance = 1e-4 * bbox_diagonal(points); + shells = take_closed_shells(points, triangles); + stats.modified = !triangles.empty(); + progress(10); + + if (!triangles.empty()) { + const std::size_t num_triangles = triangles.size(); + clean_soup(points, triangles); + SoupTopology topology = analyze_topology(points.size(), triangles); + // A soup that stays mostly open has cracks wider than the tolerance, so try wider ones. + for (double t = tolerance; t > 0. && t <= 100. * tolerance; t *= 10.) { + if (const std::size_t welded = weld_border_vertices(points, triangles, topology.border, t); welded > 0) { + stats.welded_vertices += welded; + clean_soup(points, triangles); + topology = analyze_topology(points.size(), triangles); + } + if (topology.open_fraction < 0.5) + break; + } + stats.removed_triangles = num_triangles - triangles.size(); + progress(25); + + // Independent parts of the soup are repaired in parallel. + const std::vector> units = soup_units(points, triangles, topology, tolerance); + std::vector results(units.size()); + parallel_for_each(units.size(), [&](std::size_t i) { results[i] = repair_unit(points, triangles, units[i], topology, tolerance); }); + for (UnitResult &result : results) { + if (!result.closed) + return false; + stats.reversed_shells += result.reversed; + stats.filled_holes += result.filled; + for (Mesh &shell : result.shells) + shells.emplace_back(std::move(shell)); + } + progress(60); + } + + // Shells are independent from here on. Hole filling and remeshing leave some that enclose nothing. + auto flat = [tolerance](const Mesh &shell) { + // Fewer than four faces are two triangles back to back, whose volumes need not cancel exactly. + return shell.number_of_faces() < 4 || std::abs(signed_volume(shell)) <= 0.1 * tolerance * PMP::area(shell); + }; + std::vector> pieces(shells.size()); + std::vector pairs(shells.size(), 0), removed(shells.size(), 0); + parallel_for_each(shells.size(), [&](std::size_t i) { + if (flat(shells[i])) + removed[i] = 1; + else if ((pairs[i] = remove_local_self_intersections(shells[i])) == 0) + pieces[i].emplace_back(std::move(shells[i])); + else + for (Mesh &piece : split_shells(shells[i])) + if (flat(piece)) + ++removed[i]; + else + pieces[i].emplace_back(std::move(piece)); + }); + stats.removed_shells += std::accumulate(removed.begin(), removed.end(), std::size_t(0)); + stats.self_intersections += std::accumulate(pairs.begin(), pairs.end(), std::size_t(0)); + shells.clear(); + for (std::vector &shell_pieces : pieces) + std::move(shell_pieces.begin(), shell_pieces.end(), std::back_inserter(shells)); + progress(90); + resolve_shells(shells, stats, progress); + + stats.modified = stats.modified || stats.removed_shells > 0 || stats.reversed_shells > 0 || stats.self_intersections > 0; + return !shells.empty(); +} + +// Repairs its into result, which is left empty when the input needs no change. +bool repair_its(const indexed_triangle_set &its, indexed_triangle_set &result, MeshRepairStats &stats, const std::function &progress) +{ + std::vector points; + points.reserve(its.vertices.size()); + for (const stl_vertex &v : its.vertices) + points.emplace_back(v.x(), v.y(), v.z()); + std::vector triangles; + triangles.reserve(its.indices.size()); + for (const stl_triangle_vertex_indices &f : its.indices) + if (its.vertices[f[0]].allFinite() && its.vertices[f[1]].allFinite() && its.vertices[f[2]].allFinite()) + triangles.push_back({std::size_t(f[0]), std::size_t(f[1]), std::size_t(f[2])}); + const bool dropped = triangles.size() != its.indices.size(); + + std::vector shells; + const bool ok = repair_soup(points, triangles, shells, stats, progress); + stats.modified = stats.modified || dropped; + if (!ok || !stats.modified) + return ok; + + for (Mesh &shell : shells) { + shell.collect_garbage(); + const int offset = int(result.vertices.size()); + for (Vertex v : shell.vertices()) { + const Point &p = shell.point(v); + result.vertices.emplace_back(float(p.x()), float(p.y()), float(p.z())); + } + for (Face f : shell.faces()) { + const Halfedge h = shell.halfedge(f); + result.indices.emplace_back(offset + int(shell.source(h)), offset + int(shell.target(h)), offset + int(shell.target(shell.next(h)))); + } + } + return true; +} + +} // namespace + +bool repair_mesh(TriangleMesh &mesh, MeshRepairStats *stats_out, std::string *error, const std::function &progress) +{ + if (mesh.its.indices.empty()) + return true; + + MeshRepairStats stats; + indexed_triangle_set result; + bool ok = false; + try { + ok = repair_its(mesh.its, result, stats, progress ? progress : [](int) {}); + } catch (const CGAL::Failure_exception &ex) { + if (error) + *error = ex.what(); + return false; + } + if (stats_out) + *stats_out = stats; + if (!ok) { + if (error) + *error = _u8L("The mesh does not enclose any volume."); + return false; + } + BOOST_LOG_TRIVIAL(info) << "repair_mesh: welded " << stats.welded_vertices << " vertices, removed " << stats.removed_triangles + << " triangles, filled " << stats.filled_holes << " holes, reversed " << stats.reversed_shells << " and removed " + << stats.removed_shells << " shells, fixed " << stats.self_intersections << " self-intersections"; + if (stats.modified) + mesh = TriangleMesh(std::move(result)); + return true; +} + +} // namespace Slic3r diff --git a/src/libslic3r/MeshRepair.hpp b/src/libslic3r/MeshRepair.hpp new file mode 100644 index 0000000000..0bbb6e94a5 --- /dev/null +++ b/src/libslic3r/MeshRepair.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +namespace Slic3r { + +class TriangleMesh; + +struct MeshRepairStats +{ + std::size_t welded_vertices = 0; + std::size_t removed_triangles = 0; + std::size_t reversed_shells = 0; + std::size_t filled_holes = 0; + std::size_t removed_shells = 0; + std::size_t self_intersections = 0; + // False when the input already was closed, consistently oriented and free of self-intersections. + bool modified = false; +}; + +// Returns false, leaving the mesh untouched, if no solid can be built. progress gets the percentage done and may +// throw to cancel. See docs/HLSD/mesh-repair.md. +bool repair_mesh(TriangleMesh &mesh, MeshRepairStats *stats = nullptr, std::string *error = nullptr, + const std::function &progress = {}); + +} // namespace Slic3r diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 6cca66e44b..d096bd2158 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -29,6 +29,7 @@ #include "TriangleMeshSlicer.hpp" #include "TriangleSelector.hpp" #include "MaterialType.hpp" +#include "MeshRepair.hpp" #include "Format/AMF.hpp" #include "Format/svg.hpp" @@ -2946,6 +2947,18 @@ size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint) if (text_configuration.has_value()) text_configuration.reset(); + // Parts without a convex hull enclose no volume; drop them here, as deleting volumes later could delete this one. + std::vector> parts; + for (TriangleMesh &mesh : meshes) + if (!mesh.empty()) { + std::unique_ptr part(new ModelVolume(object, *this, std::move(mesh))); + if (const std::shared_ptr &hull = part->get_convex_hull_shared_ptr(); + hull != nullptr && !hull->its.vertices.empty() && !hull->its.indices.empty()) + parts.emplace_back(std::move(part)); + } + if (parts.empty()) + return 1; + std::optional saved_painting = remap_paint ? save_painting() : std::optional{}; @@ -2956,26 +2969,22 @@ size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint) unsigned int extruder_counter = 0; const Vec3d offset = this->get_offset(); - for (TriangleMesh &mesh : meshes) { - if (mesh.empty()) - // Repair may have removed unconnected triangles, thus emptying the mesh. - continue; - + for (std::unique_ptr &part : parts) { if (idx == 0) { - this->set_mesh(std::move(mesh)); - this->calculate_convex_hull(); + this->set_mesh(part->m_mesh); + m_convex_hull = part->m_convex_hull; this->invalidate_convex_hull_2d(); // Assign a new unique ID, so that a new GLVolume will be generated. this->set_new_unique_id(); - // reset the source to disable reload from disk - this->source = ModelVolume::Source(); // BBS: reset facet annotations this->reset_extra_facets(); } else - this->object->volumes.insert(this->object->volumes.begin() + (++ivolume), new ModelVolume(object, *this, std::move(mesh))); + this->object->volumes.insert(this->object->volumes.begin() + (++ivolume), part.release()); + // reset the source to disable reload from disk + this->object->volumes[ivolume]->source = ModelVolume::Source(); this->object->volumes[ivolume]->set_offset(Vec3d::Zero()); this->object->volumes[ivolume]->center_geometry_after_creation(); this->object->volumes[ivolume]->translate(offset); @@ -2989,21 +2998,29 @@ size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint) ++ idx; } - // discard volumes for which the convex hull was not generated or is degenerate - size_t i = 0; - while (i < this->object->volumes.size()) { - const std::shared_ptr &hull = this->object->volumes[i]->get_convex_hull_shared_ptr(); - if (hull == nullptr || hull->its.vertices.empty() || hull->its.indices.empty()) { - this->object->delete_volume(i); - --idx; - --i; - } - ++i; - } - return idx; } +bool ModelVolume::repair(bool keep_painting, MeshRepairStats *stats_out, std::string *error, const std::function &progress) +{ + TriangleMesh mesh = this->mesh(); + MeshRepairStats stats; + const bool ok = repair_mesh(mesh, &stats, error, progress); + if (stats_out) + *stats_out = stats; + if (!ok || !stats.modified) + return ok; + + const std::optional saved_painting = keep_painting ? save_painting() : std::nullopt; + this->set_mesh(std::move(mesh)); + this->calculate_convex_hull(); + this->invalidate_convex_hull_2d(); + // Assign a new unique ID, so that a new GLVolume will be generated. + this->set_new_unique_id(); + this->restore_painting(saved_painting); + return true; +} + void ModelVolume::translate(const Vec3d& displacement) { set_offset(get_offset() + displacement); diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 4bb1794b46..dcaea5ee97 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -77,6 +77,7 @@ class Preset; class BBLProject; class KeyStore; +struct MeshRepairStats; namespace UndoRedo { class StackImpl; @@ -887,13 +888,13 @@ public: // The triangular model. const TriangleMesh& mesh() const { return *m_mesh.get(); } std::shared_ptr mesh_ptr() const { return m_mesh; } - void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared(mesh); } - void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared(std::move(mesh)); } - void set_mesh(const indexed_triangle_set &mesh) { m_mesh = std::make_shared(mesh); } - void set_mesh(indexed_triangle_set &&mesh) { m_mesh = std::make_shared(std::move(mesh)); } - void set_mesh(std::shared_ptr &mesh) { m_mesh = mesh; } - void set_mesh(std::unique_ptr &&mesh) { m_mesh = std::move(mesh); } - void reset_mesh() { m_mesh = std::make_shared(); } + void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared(mesh); m_is_splittable = -1; } + void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared(std::move(mesh)); m_is_splittable = -1; } + void set_mesh(const indexed_triangle_set &mesh) { m_mesh = std::make_shared(mesh); m_is_splittable = -1; } + void set_mesh(indexed_triangle_set &&mesh) { m_mesh = std::make_shared(std::move(mesh)); m_is_splittable = -1; } + void set_mesh(std::shared_ptr &mesh) { m_mesh = mesh; m_is_splittable = -1; } + void set_mesh(std::unique_ptr &&mesh) { m_mesh = std::move(mesh); m_is_splittable = -1; } + void reset_mesh() { m_mesh = std::make_shared(); m_is_splittable = -1; } const std::shared_ptr& get_mesh_shared_ptr() const { return m_mesh; } // Configuration parameters specific to an object model geometry or a modifier volume, // overriding the global Slic3r settings and the ModelObject settings. @@ -1043,6 +1044,9 @@ public: // Return the number of volumes created from this one. // This is useful to assign different materials to different volumes of an object. size_t split(unsigned int max_extruders, bool remap_paint); + // Replaces the mesh by its repair_mesh() result if that changed it. False if no solid can be built. + bool repair(bool keep_painting, MeshRepairStats *stats = nullptr, std::string *error = nullptr, + const std::function &progress = {}); void translate(double x, double y, double z) { translate(Vec3d(x, y, z)); } void translate(const Vec3d& displacement); void scale(const Vec3d& scaling_factors); diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 78fa0785ef..19110cd2d2 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -2164,6 +2164,16 @@ int PresetCollection::get_differed_values_to_update(Preset& preset, std::map dirty_options = preset.config.diff(parent_preset->config); + // Send the variant layout even when it matches the parent's, as the local save does: the loader + // sizes the per-variant values by it and would otherwise keep only the first one. + std::string extruder_id_name, extruder_variant_name; + std::set *key_set1 = nullptr, *key_set2 = nullptr; + Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2); + if (!extruder_id_name.empty()) + dirty_options.emplace_back(extruder_id_name); + if (!extruder_variant_name.empty()) + dirty_options.emplace_back(extruder_variant_name); + for (auto option: dirty_options) { ConfigOption *opt_src = preset.config.option(option); @@ -2628,15 +2638,34 @@ bool PresetCollection::load_user_preset(std::string name, std::map *key_set1 = nullptr, *key_set2 = nullptr; + Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2); + + // Older uploads left out a variant layout equal to the parent's but still sent one value per + // parent variant. Read those values against the parent's layout instead of cutting them to one. + if (inherit_preset && !cloud_config.has(extruder_variant_name)) { + const auto *parent_variants = inherit_preset->config.option(extruder_variant_name); + const size_t variant_count = parent_variants ? parent_variants->size() : 0; + bool values_per_variant = false; + for (const std::string &key : cloud_config.keys()) { + const size_t stride = key_set2->count(key) ? 2 : key_set1->count(key) ? 1 : 0; + const auto *values = dynamic_cast(cloud_config.option(key)); + if (stride > 0 && key != extruder_id_name && values && variant_count > 1 && values->size() == variant_count * stride) { + values_per_variant = true; + break; + } + } + if (values_per_variant) + for (const std::string &key : {extruder_id_name, extruder_variant_name}) + if (!key.empty() && !cloud_config.has(key) && inherit_preset->config.has(key)) + cloud_config.set_key_value(key, inherit_preset->config.option(key)->clone()); + } + extend_default_config_length(cloud_config, false, {}); - if (inherit_preset) { - std::string extruder_id_name, extruder_variant_name; - std::set *key_set1 = nullptr, *key_set2 = nullptr; - Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2); - + if (inherit_preset) new_config.update_diff_values_to_child_config(cloud_config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); - } else{ new_config.apply(std::move(cloud_config)); extend_default_config_length(new_config, true, default_preset.config); diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 026a6fce9b..e15d9b18cd 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -2991,6 +2991,15 @@ void PrintObject::clear_shared_object() void PrintObject::copy_layers_from_shared_object() { if (m_shared_object) { + // Orca: Print::process() calls this once per slice for each sharing object, after clear_shared_object() has + // emptied any earlier alias, so the vectors hold only layers this object sliced itself before it started sharing + // (or nothing). Free them here, as clear_support_layers() / clear_layers() would; those skip deletion while + // m_shared_object is set. The belt-only data they also free is always empty here, because belt printers never + // share layers. + for (SupportLayer *layer : m_support_layers) + delete layer; + for (Layer *layer : m_layers) + delete layer; m_layers.clear(); m_support_layers.clear(); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 9e3958bc25..ac584e5b77 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -12866,10 +12866,11 @@ CLITransformConfigDef::CLITransformConfigDef() //def->cli = "orient|o"; def->set_default_value(new ConfigOptionInt(0)); - /*def = this->add("repair", coBool); + def = this->add("repair", coBool); def->label = L("Repair"); - def->tooltip = L("Repair the model's meshes if it is non-manifold mesh."); - def->set_default_value(new ConfigOptionBool(false));*/ + def->tooltip = L("Repair the meshes of the models as Fix Model does: close holes and cracks, orient them outward and " + "remove self-intersections."); + def->set_default_value(new ConfigOptionBool(false)); def = this->add("rotate", coFloat); def->label = L("Rotate"); @@ -13085,7 +13086,7 @@ CLIMiscConfigDef::CLIMiscConfigDef() def = this->add("allow_rotations", coBool); def->label = L("Allow rotation when arranging"); def->tooltip = L("If enabled, Arrange will allow rotation when placing objects."); - def->set_default_value(new ConfigOptionBool(true)); + def->set_default_value(new ConfigOptionBool(false)); def = this->add("align_to_y_axis", coBool); def->label = L("Align to Y axis when arranging"); diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 9da74d7986..6c05e069a9 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -982,6 +982,9 @@ void PrintObject::generate_support_material(bool with_belt_brim) this->_generate_support_material(); m_print->throw_if_canceled(); } + // Orca: the tree support collision/avoidance caches and support nodes are only used while this step runs + // (detect_overhangs() rebuilds them from scratch), so don't keep them resident until the next slice. + this->clear_tree_support_preview_cache(); // Belt brim rides here rather than in the brim step because its apron // prologue introduces print_z values below the object's first layer, and // those must exist before ToolOrdering is built at psWipeTower - one step diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index facce01787..2bbe68e4b2 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1532,7 +1532,7 @@ void PrintObject::slice_volumes() if (min_growth < 0.f || elfoot > 0.f) { // Apply the negative XY compensation. (the ones that is <0) ExPolygons trimming; - static const float eps = float(scale_(m_config.slice_closing_radius.value) * 1.5); + const float eps = float(scale_(m_config.slice_closing_radius.value) * 1.5); if (elfoot > 0.f) { ExPolygons expolygons_to_compensate = offset_ex(layer->merged(eps), -eps); lslices_elfoot_uncompensated[layer_id] = expolygons_to_compensate; diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index b1d0e0d1e8..5187e73a25 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1976,37 +1976,15 @@ coordf_t TreeSupport::get_radius(const SupportNode* node) return node->radius; } -ExPolygons TreeSupport::get_avoidance(coordf_t radius, size_t obj_layer_nr) +// Orca: drop_nodes() calls these several times per node per layer, so they return a reference into the +// TreeSupportData cache. The cache only ever inserts, so its entries never move while nodes drop in parallel. +const ExPolygons& TreeSupport::get_avoidance(coordf_t radius, size_t obj_layer_nr) { -#if USE_SUPPORT_3D - if (m_model_volumes) { - bool on_build_plate = m_object_config->support_on_build_plate_only.value; - const Polygons& avoid_polys = m_model_volumes->getAvoidance(radius, obj_layer_nr, TreeSupport3D::TreeModelVolumes::AvoidanceType::FastSafe, on_build_plate, true); - ExPolygons expolys; - for (auto& poly : avoid_polys) - expolys.emplace_back(std::move(poly)); - return expolys; - } - return ExPolygons(); -#else return m_ts_data->get_avoidance(radius, obj_layer_nr); -#endif } -ExPolygons TreeSupport::get_collision(coordf_t radius, size_t layer_nr) +const ExPolygons& TreeSupport::get_collision(coordf_t radius, size_t layer_nr) { -#if USE_SUPPORT_3D - if (m_model_volumes) { - bool on_build_plate = m_object_config->support_on_build_plate_only.value; - const Polygons& collision_polys = m_model_volumes->getCollision(radius, layer_nr, true); - ExPolygons expolys; - for (auto& poly : collision_polys) - expolys.emplace_back(std::move(poly)); - return expolys; - } -#else return m_ts_data->get_collision(radius, layer_nr); -#endif - return ExPolygons(); } Polygons TreeSupport::get_collision_polys(coordf_t radius, size_t layer_nr) { @@ -2827,7 +2805,12 @@ void TreeSupport::draw_circles() #endif // SUPPORT_TREE_DEBUG_TO_SVG SupportLayerPtrs& ts_layers = m_object->support_layers(); - auto iter = std::remove_if(ts_layers.begin(), ts_layers.end(), [](SupportLayer* ts_layer) { return ts_layer->height < EPSILON; }); + // Orca: the vector owns its layers, so the dropped ones are deleted before they are erased. + // std::stable_partition (unlike std::remove_if) leaves exactly the dropped layers in the tail. + // Only heights below EPSILON are dropped; written as !(height < EPSILON), a NaN height is kept. + auto iter = std::stable_partition(ts_layers.begin(), ts_layers.end(), [](SupportLayer* ts_layer) { return !(ts_layer->height < EPSILON); }); + for (auto it = iter; it != ts_layers.end(); ++it) + delete *it; ts_layers.erase(iter, ts_layers.end()); for (int layer_nr = 0; layer_nr < ts_layers.size(); layer_nr++) { ts_layers[layer_nr]->upper_layer = layer_nr != ts_layers.size() - 1 ? ts_layers[layer_nr + 1] : nullptr; @@ -3071,7 +3054,7 @@ void TreeSupport::drop_nodes() //Insert a completely new node and let both original nodes fade. Point next_position = (node.position + neighbours[0]) / 2; //Average position of the two nodes. coordf_t next_radius = calc_radius(node.dist_mm_to_top+height_next); - auto avoid_layer = get_avoidance(next_radius, obj_layer_nr_next); + const ExPolygons& avoid_layer = get_avoidance(next_radius, obj_layer_nr_next); if (group_index == 0) { //Avoid collisions. @@ -3275,7 +3258,7 @@ void TreeSupport::drop_nodes() } #endif coordf_t next_radius = calc_radius(node.dist_mm_to_top + height_next); - auto avoidance_next = get_avoidance(next_radius, obj_layer_nr_next); + const ExPolygons& avoidance_next = get_avoidance(next_radius, obj_layer_nr_next); Point to_outside = projection_onto(avoidance_next, node.position); Point direction_to_outer = to_outside - node.position; @@ -3337,7 +3320,7 @@ void TreeSupport::drop_nodes() p_node->to_buildplate = false; return; // from parallel_for_each lambda } - auto next_collision = get_collision(0, obj_layer_nr_next); + const ExPolygons& next_collision = get_collision(0, obj_layer_nr_next); const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex); // don't increase radius if next node will collide partially with the object (STUDIO-7883) to_outside = projection_onto(next_collision, next_layer_vertex); diff --git a/src/libslic3r/Support/TreeSupport.hpp b/src/libslic3r/Support/TreeSupport.hpp index c6acd806e0..d8bb241fee 100644 --- a/src/libslic3r/Support/TreeSupport.hpp +++ b/src/libslic3r/Support/TreeSupport.hpp @@ -536,9 +536,9 @@ private: coordf_t calc_branch_radius(coordf_t base_radius, coordf_t mm_to_top, double diameter_angle_scale_factor, bool use_min_distance=true); coordf_t calc_radius(coordf_t mm_to_top); coordf_t get_radius(const SupportNode* node); - ExPolygons get_avoidance(coordf_t radius, size_t obj_layer_nr); + const ExPolygons& get_avoidance(coordf_t radius, size_t obj_layer_nr); // layer's expolygon expanded by radius+m_xy_distance - ExPolygons get_collision(coordf_t radius, size_t layer_nr); + const ExPolygons& get_collision(coordf_t radius, size_t layer_nr); // get Polygons instead of ExPolygons Polygons get_collision_polys(coordf_t radius, size_t layer_nr); diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp index 42bf84cd6c..c7ba6b83ea 100644 --- a/src/libslic3r/TextureDisplacement.cpp +++ b/src/libslic3r/TextureDisplacement.cpp @@ -19,6 +19,7 @@ #include #include #include +#include "nlohmann/json.hpp" #include #include #include @@ -5460,5 +5461,260 @@ indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const s return out; } +// --------------------------------------------------------------------------------------------- +// Project persistence. See the declarations in TextureDisplacement.hpp for why this is JSON rather +// than the cereal stream next to them. +// --------------------------------------------------------------------------------------------- +namespace { + +nlohmann::json vec2_to_json(const Vec2f &v) { return nlohmann::json::array({ v.x(), v.y() }); } +nlohmann::json vec3_to_json(const Vec3f &v) { return nlohmann::json::array({ v.x(), v.y(), v.z() }); } + +Vec2f vec2_from_json(const nlohmann::json &j, const Vec2f &fallback) +{ + if (!j.is_array() || j.size() != 2) + return fallback; + return Vec2f(j[0].get(), j[1].get()); +} +Vec3f vec3_from_json(const nlohmann::json &j, const Vec3f &fallback) +{ + if (!j.is_array() || j.size() != 3) + return fallback; + return Vec3f(j[0].get(), j[1].get(), j[2].get()); +} + +// Reads key `k` into `out` when it is present and of the expected type, leaving `out` alone otherwise. +// That "leave it alone" is the whole point: every member starts at its struct default, so a project +// written by an older build simply keeps the defaults for whatever it did not know about. +template void read(const nlohmann::json &j, const char *k, T &out) +{ + const auto it = j.find(k); + if (it == j.end()) + return; + try { + out = it->get(); + } catch (...) { + } +} +void read_enum(const nlohmann::json &j, const char *k, int &out) +{ + const auto it = j.find(k); + if (it != j.end() && it->is_number_integer()) + out = it->get(); +} + +} // namespace + +std::string texture_displacement_layers_to_json(const std::vector &layers, + const TextureDisplacementOptions &options) +{ + nlohmann::json root; + root["version"] = 1; + + nlohmann::json &opt = root["options"]; + opt["displace_border"] = options.displace_border; + opt["smooth_enabled"] = options.smooth_enabled; + opt["smooth_strength"] = options.smooth_strength; + opt["smooth_iterations"] = options.smooth_iterations; + opt["smooth_skip_border"] = options.smooth_skip_border; + opt["pipeline_v2"] = options.pipeline_v2; + opt["v2_refine_mm"] = options.v2_refine_mm; + opt["v2_regularize"] = options.v2_regularize; + opt["v2_max_triangles_k"] = options.v2_max_triangles_k; + opt["v2_relocate"] = options.v2_relocate; + opt["v2_flip_edges"] = options.v2_flip_edges; + opt["color_mix_enabled"] = options.color_mix_enabled; + opt["color_despeckle"] = options.color_despeckle; + opt["color_mix_count"] = options.color_mix_count; + opt["active_slot"] = options.active_slot; + opt["panel_mode"] = options.panel_mode; + + nlohmann::json &arr = root["layers"]; + arr = nlohmann::json::array(); + for (const TextureDisplacementLayer &l : layers) { + nlohmann::json j; + j["slot"] = l.slot; + j["name"] = l.name; + j["path"] = l.path; + j["path_in_3mf"] = l.path_in_3mf; + + j["depth_mm"] = l.depth_mm; + j["tiling_scale"] = l.tiling_scale; + j["rotation_deg"] = l.rotation_deg; + j["offset"] = vec2_to_json(l.offset); + j["invert"] = l.invert; + j["midlevel"] = l.midlevel; + j["smoothing"] = l.smoothing; + + j["edge_smoothing"] = l.edge_smoothing; + j["edge_smoothing_amount"] = l.edge_smoothing_amount; + j["auto_connect_islands"] = l.auto_connect_islands; + + j["tile_enabled"] = l.tile_enabled; + j["tile_method"] = int(l.tile_method); + j["projection_method"] = int(l.projection_method); + j["blend_mode"] = int(l.blend_mode); + j["color_enabled"] = l.color_enabled; + + j["lscm_seam_angle_deg"] = l.lscm_seam_angle_deg; + j["island_padding_mm"] = l.island_padding_mm; + + j["view_project_right"] = vec3_to_json(l.view_project_right); + j["view_project_up"] = vec3_to_json(l.view_project_up); + j["view_project_projective"] = l.view_project_projective; + j["view_project_matrix"] = l.view_project_matrix; + + // Flat pairs rather than nested arrays: shorter, and the reader can simply ignore a trailing + // odd element instead of having to validate every sub-array. + nlohmann::json &seams = j["lscm_seam_edges"]; + seams = nlohmann::json::array(); + for (const auto &[a, b] : l.lscm_seam_edges) { + seams.push_back(a); + seams.push_back(b); + } + + nlohmann::json &islands = j["islands"]; + islands = nlohmann::json::array(); + for (const TextureIsland &island : l.islands) + islands.push_back(nlohmann::json::array( + { island.offset.x(), island.offset.y(), island.rotation_deg, island.scale })); + + j["island_groups"] = l.island_groups; + + nlohmann::json &overrides = j["lscm_uv_overrides"]; + overrides = nlohmann::json::array(); + for (const auto &[key, uv] : l.lscm_uv_overrides) + overrides.push_back(nlohmann::json::array({ key, uv.x(), uv.y() })); + + arr.push_back(std::move(j)); + } + return root.dump(); +} + +bool texture_displacement_layers_from_json(const std::string &text, + std::vector &layers, + TextureDisplacementOptions &options) +{ + nlohmann::json root; + try { + root = nlohmann::json::parse(text); + } catch (...) { + return false; + } + if (!root.is_object()) + return false; + + TextureDisplacementOptions out_options; + if (const auto it = root.find("options"); it != root.end() && it->is_object()) { + const nlohmann::json &opt = *it; + read(opt, "displace_border", out_options.displace_border); + read(opt, "smooth_enabled", out_options.smooth_enabled); + read(opt, "smooth_strength", out_options.smooth_strength); + read(opt, "smooth_iterations", out_options.smooth_iterations); + read(opt, "smooth_skip_border", out_options.smooth_skip_border); + read(opt, "pipeline_v2", out_options.pipeline_v2); + read(opt, "v2_refine_mm", out_options.v2_refine_mm); + read(opt, "v2_regularize", out_options.v2_regularize); + read(opt, "v2_max_triangles_k", out_options.v2_max_triangles_k); + read(opt, "v2_relocate", out_options.v2_relocate); + read(opt, "v2_flip_edges", out_options.v2_flip_edges); + read(opt, "color_mix_enabled", out_options.color_mix_enabled); + read(opt, "color_despeckle", out_options.color_despeckle); + read(opt, "color_mix_count", out_options.color_mix_count); + read(opt, "active_slot", out_options.active_slot); + read(opt, "panel_mode", out_options.panel_mode); + out_options.active_slot = std::clamp(out_options.active_slot, 0, int(TEXTURE_DISPLACEMENT_MAX_LAYERS) - 1); + out_options.panel_mode = std::clamp(out_options.panel_mode, 0, 1); + } + + std::vector out_layers; + // read() skips a value of the wrong type, but the vector, matrix, seam, island and UV-override arrays + // below read their numbers with get<>(), which throws on one; that rejects the document, like text that + // does not parse. + try { + if (const auto it = root.find("layers"); it != root.end() && it->is_array()) { + for (const nlohmann::json &j : *it) { + if (!j.is_object()) + continue; + TextureDisplacementLayer l; + read(j, "slot", l.slot); + // The panel and the paint masks are indexed by slot, so a layer without a valid one rejects the + // document rather than reaching them. + if (l.slot < 0 || l.slot >= int(TEXTURE_DISPLACEMENT_MAX_LAYERS)) + return false; + read(j, "name", l.name); + read(j, "path", l.path); + read(j, "path_in_3mf", l.path_in_3mf); + + read(j, "depth_mm", l.depth_mm); + read(j, "tiling_scale", l.tiling_scale); + read(j, "rotation_deg", l.rotation_deg); + if (const auto o = j.find("offset"); o != j.end()) + l.offset = vec2_from_json(*o, l.offset); + read(j, "invert", l.invert); + read(j, "midlevel", l.midlevel); + read(j, "smoothing", l.smoothing); + + read(j, "edge_smoothing", l.edge_smoothing); + read(j, "edge_smoothing_amount", l.edge_smoothing_amount); + read(j, "auto_connect_islands", l.auto_connect_islands); + + read(j, "tile_enabled", l.tile_enabled); + read(j, "color_enabled", l.color_enabled); + int tile_method = int(l.tile_method), projection = int(l.projection_method), blend = int(l.blend_mode); + read_enum(j, "tile_method", tile_method); + read_enum(j, "projection_method", projection); + read_enum(j, "blend_mode", blend); + l.tile_method = TextureTileMethod(tile_method); + l.projection_method = TextureProjectionMethod(std::clamp(projection, 0, 4)); + l.blend_mode = TextureBlendMode(blend); + + read(j, "lscm_seam_angle_deg", l.lscm_seam_angle_deg); + read(j, "island_padding_mm", l.island_padding_mm); + + if (const auto v = j.find("view_project_right"); v != j.end()) + l.view_project_right = vec3_from_json(*v, l.view_project_right); + if (const auto v = j.find("view_project_up"); v != j.end()) + l.view_project_up = vec3_from_json(*v, l.view_project_up); + read(j, "view_project_projective", l.view_project_projective); + if (const auto v = j.find("view_project_matrix"); v != j.end() && v->is_array() && v->size() == 12) + for (size_t i = 0; i < 12; ++i) + l.view_project_matrix[i] = (*v)[i].get(); + + if (const auto v = j.find("lscm_seam_edges"); v != j.end() && v->is_array()) + for (size_t i = 0; i + 1 < v->size(); i += 2) + l.lscm_seam_edges.emplace_back((*v)[i].get(), (*v)[i + 1].get()); + + if (const auto v = j.find("islands"); v != j.end() && v->is_array()) + for (const nlohmann::json &e : *v) { + if (!e.is_array() || e.size() != 4) + continue; + TextureIsland island; + island.offset = Vec2f(e[0].get(), e[1].get()); + island.rotation_deg = e[2].get(); + island.scale = e[3].get(); + l.islands.push_back(island); + } + + read(j, "island_groups", l.island_groups); + + if (const auto v = j.find("lscm_uv_overrides"); v != j.end() && v->is_array()) + for (const nlohmann::json &e : *v) { + if (!e.is_array() || e.size() != 3) + continue; + l.lscm_uv_overrides.emplace_back(e[0].get(), Vec2f(e[1].get(), e[2].get())); + } + + out_layers.push_back(std::move(l)); + } + } + } catch (const nlohmann::json::exception &) { + return false; + } + + layers = std::move(out_layers); + options = out_options; + return true; +} } // namespace Slic3r diff --git a/src/libslic3r/TextureDisplacement.hpp b/src/libslic3r/TextureDisplacement.hpp index d6c189ffa7..48fb4f20cf 100644 --- a/src/libslic3r/TextureDisplacement.hpp +++ b/src/libslic3r/TextureDisplacement.hpp @@ -426,14 +426,41 @@ struct TextureDisplacementOptions // finer than the mesh leaves behind, without eating features that are genuinely a facet wide. int color_despeckle = 2; + // Where the panel was left, rather than anything the bake reads. It lives with the volume because + // that is what it describes: reopening a project should put the user back on the layer they were + // editing, in the mode they were editing it in, not on slot 0 in Standard. + int active_slot = 0; + int panel_mode = 0; // 0 Standard, 1 Pro + template void serialize(Archive &ar) { ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border, pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k, - v2_relocate, color_mix_enabled, color_despeckle, color_mix_count); + v2_relocate, v2_flip_edges, color_mix_enabled, color_despeckle, color_mix_count, + active_slot, panel_mode); } }; +// Project persistence (see bbs_3mf.cpp). The layer stack and the per-volume options are written to the +// .3mf as JSON rather than through the cereal save()/load() above: those two are positional and +// unversioned, which is right for the undo/redo stack they serve (one session, one binary) but would +// make every future field a project-breaking change. A JSON object tolerates both directions - an +// unknown key is ignored, a missing one keeps the member's default - so old projects keep loading and +// new ones degrade gracefully in older builds. +// +// The texture image itself is *not* in here. It is a binary blob that belongs in the archive as a file +// of its own, exactly as EmbossShape stores its SVG; `path_in_3mf` names that file, and the caller is +// responsible for writing it and for filling `image_data` back in on load. +std::string texture_displacement_layers_to_json(const std::vector &layers, + const TextureDisplacementOptions &options); +// Returns false and leaves both outputs untouched when the text is not a valid JSON object, a number in a +// layer's vector, matrix, seam, island or UV-override array has the wrong type, or a layer's slot is +// missing or outside [0, TEXTURE_DISPLACEMENT_MAX_LAYERS). Any other value of the wrong type or shape +// keeps its default. +bool texture_displacement_layers_from_json(const std::string &text, + std::vector &layers, + TextureDisplacementOptions &options); + // How much detail a height texture carries: central differences of the grey image, the mean gradient // and the share of texels steeper than 30 grey levels, mapped to how many texels one mesh edge may // span (1 for a hard-edged image, 4 for a smooth one). Cached per image, like the decode. diff --git a/src/libslic3r/TextureToColor/TextureToColor.cpp b/src/libslic3r/TextureToColor/TextureToColor.cpp index 15a2a641af..e016a549a2 100644 --- a/src/libslic3r/TextureToColor/TextureToColor.cpp +++ b/src/libslic3r/TextureToColor/TextureToColor.cpp @@ -673,14 +673,14 @@ static bool repair_cluster_smooth( [&]() { return cancelled(); }, &repair_error); if (repaired) { if (cancelled()) return false; - BOOST_LOG_TRIVIAL(info) << log_prefix << ": Windows 3D mesh repair finished."; + BOOST_LOG_TRIVIAL(info) << log_prefix << ": mesh repair finished."; if (!resample_face_colors(TriMesh(std::move(repaired_its)))) return false; } else { - BOOST_LOG_TRIVIAL(warning) << log_prefix << ": Windows 3D mesh repair failed: " << repair_error; + BOOST_LOG_TRIVIAL(warning) << log_prefix << ": mesh repair failed: " << repair_error; } } else { - BOOST_LOG_TRIVIAL(info) << log_prefix << ": importing mesh without Windows 3D repair."; + BOOST_LOG_TRIVIAL(info) << log_prefix << ": importing mesh without repair."; } } } diff --git a/src/libslic3r/TriangleMeshDeal.hpp b/src/libslic3r/TriangleMeshDeal.hpp index b1da431229..89d613bb85 100644 --- a/src/libslic3r/TriangleMeshDeal.hpp +++ b/src/libslic3r/TriangleMeshDeal.hpp @@ -1,5 +1,4 @@ -#ifndef libslic3r_Timer_hpp_ -#define libslic3r_Timer_hpp_ +#pragma once #include "TriangleMesh.hpp" @@ -10,5 +9,3 @@ public: static TriangleMesh smooth_triangle_mesh(const TriangleMesh &mesh,bool& ok); }; } // namespace Slic3r - -#endif // libslic3r_Timer_hpp_ diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 24ebe9a337..d9d30573fc 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -109,7 +109,6 @@ extern std::string format_memsize_MB(size_t n); extern std::string format_memsize(size_t bytes, unsigned int decimals = 1); // Return string to be added to the boost::log output to inform about the current process memory allocation. // The string is non-empty if the loglevel >= info (3) or ignore_loglevel==true. -// Latter is used to get the memory info from SysInfoDialog. extern std::string log_memory_info(bool ignore_loglevel = false); extern void disable_multi_threading(); // Returns the size of physical memory (RAM) in bytes. diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index cd9ecf100d..e8cc1759d5 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -1744,9 +1744,7 @@ std::string format_diameter_to_str(double diameter, int precision) return oss.str(); } -// Returns platform-specific string to be used as log output or parsed in SysInfoDialog. -// The latter parses the string with (semi)colons as separators, it should look about as -// "desc1: value1; desc2: value2" or similar (spaces should not matter). +// Returns platform-specific string to be used as log output. std::string log_memory_info(bool ignore_loglevel) { std::string out; diff --git a/src/libvgcode/src/SegmentTemplate.cpp b/src/libvgcode/src/SegmentTemplate.cpp index f925e0a860..3887f4128f 100644 --- a/src/libvgcode/src/SegmentTemplate.cpp +++ b/src/libvgcode/src/SegmentTemplate.cpp @@ -21,7 +21,9 @@ namespace libvgcode { // the triangles that share it: the shader runs 8 times per segment instead of 24. static constexpr const std::array VERTEX_DATA = { 0, 1, 2, 3, 4, 5, 6, 7 }; -static constexpr const std::array INDEX_DATA = { +// 16-bit indices: GCN 1/2 GPUs have no 8-bit index support, and some of their drivers +// emulate it wrongly in instanced draws. +static constexpr const std::array INDEX_DATA = { 0, 1, 2, // front spike 0, 2, 3, // front spike 0, 3, 4, // right/bottom body @@ -37,7 +39,7 @@ void SegmentTemplate::init() if (m_vao_id != 0) return; - m_size_in_bytes_gpu += (VERTEX_DATA.size() + INDEX_DATA.size()) * sizeof(uint8_t); + m_size_in_bytes_gpu += VERTEX_DATA.size() * sizeof(uint8_t) + INDEX_DATA.size() * sizeof(uint16_t); int curr_vertex_array; glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array)); @@ -61,7 +63,7 @@ void SegmentTemplate::init() // and restored together with the vao. glsafe(glGenBuffers(1, &m_ibo_id)); glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ibo_id)); - glsafe(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint8_t), INDEX_DATA.data(), GL_STATIC_DRAW)); + glsafe(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint16_t), INDEX_DATA.data(), GL_STATIC_DRAW)); glsafe(glBindBuffer(GL_ARRAY_BUFFER, curr_array_buffer)); glsafe(glBindVertexArray(curr_vertex_array)); @@ -94,7 +96,7 @@ void SegmentTemplate::render(size_t count) glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array)); glsafe(glBindVertexArray(m_vao_id)); - glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast(INDEX_DATA.size()), GL_UNSIGNED_BYTE, + glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast(INDEX_DATA.size()), GL_UNSIGNED_SHORT, nullptr, static_cast(count))); glsafe(glBindVertexArray(curr_vertex_array)); } diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 20e255ad57..1b13231e33 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -418,8 +418,6 @@ set(SLIC3R_GUI_SOURCES GUI/MultiSendMachineModel.hpp GUI/MultiTaskManagerPage.cpp GUI/MultiTaskManagerPage.hpp - GUI/MultiTaskModel.cpp - GUI/MultiTaskModel.hpp GUI/NetworkTestDialog.cpp GUI/NetworkTestDialog.hpp GUI/Notebook.cpp @@ -804,7 +802,6 @@ set(SLIC3R_GUI_SOURCES Utils/PrintHost.hpp Utils/Process.cpp Utils/Process.hpp - Utils/Profile.hpp Utils/RaycastManager.cpp Utils/RaycastManager.hpp Utils/Repetier.cpp diff --git a/src/slic3r/GUI/AMSMaterialsSetting.cpp b/src/slic3r/GUI/AMSMaterialsSetting.cpp index 903524caaa..41047dffd9 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.cpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.cpp @@ -16,6 +16,7 @@ #include #include "slic3r/GUI/GUI_Utils.hpp" #include "slic3r/GUI/Widgets/Button.hpp" +#include "slic3r/GUI/Widgets/DropDown.hpp" #include "slic3r/GUI/Widgets/Label.hpp" #include #include "slic3r/GUI/wxExtensions.hpp" @@ -172,7 +173,26 @@ void AMSMaterialsSetting::create() Bind(wxEVT_PAINT, &AMSMaterialsSetting::paintEvent, this); Bind(EVT_SELECTED_COLOR, &AMSMaterialsSetting::on_picker_color, this); - m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this); + m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this); + m_comboBox_filament->Bind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this); + + // Favorites toggled in the open list are regrouped just before it opens again, so rows never move + // under the cursor. These handlers run ahead of ComboBox's own, which then measure and show the + // rebuilt rows; wxEVT_COMBOBOX_DROPDOWN would be too late, as ComboBox sends it after showing the list. + auto regroup_favorites = [this]() { + if (!m_favorites_regroup || m_comboBox_filament->is_drop_down()) + return; + const FilamentInfos* info = selected_filament_info(); + const std::string filament_id = info ? info->filament_id : std::string(); + rebuild_filament_dropdown_items(filament_id); + }; + m_comboBox_filament->Bind(wxEVT_LEFT_DOWN, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); }); + m_comboBox_filament->Bind(wxEVT_LEFT_DCLICK, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); }); + m_comboBox_filament->Bind(wxEVT_KEY_DOWN, [regroup_favorites](wxKeyEvent& e) { + if (e.GetKeyCode() == WXK_RETURN || e.GetKeyCode() == WXK_SPACE) // the keys that open the list + regroup_favorites(); + e.Skip(); + }); m_comboBox_cali_result->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this); } @@ -191,11 +211,9 @@ void AMSMaterialsSetting::create_panel_normal(wxWindow* parent) m_sizer_filament->Add(0, 0, 0, wxEXPAND, 0); -#ifdef __APPLE__ - m_comboBox_filament = new wxComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY); -#else m_comboBox_filament = new ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY); -#endif + m_star_filled = ScalableBitmap(this, "score_star_light", 16); + m_star_empty = ScalableBitmap(this, "score_star_outline", 16); m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0); @@ -420,6 +438,7 @@ void AMSMaterialsSetting::paintEvent(wxPaintEvent &evt) AMSMaterialsSetting::~AMSMaterialsSetting() { m_comboBox_filament->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this); + m_comboBox_filament->Unbind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this); m_comboBox_cali_result->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this); } @@ -531,10 +550,9 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) { std::string selected_ams_id; PresetBundle *preset_bundle = wxGetApp().preset_bundle; if (preset_bundle) { + const FilamentInfos* selected_info = selected_filament_info(); for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) { - auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()]; - std::string filament_id = filament_item.filament_id; - if (it->filament_id.compare(filament_id) == 0) { + if (selected_info && it->filament_id.compare(selected_info->filament_id) == 0) { selected_ams_id = it->filament_id; break; } @@ -682,12 +700,13 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event) ams_setting_id = ""; // the combobox item - auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()]; + const FilamentInfos* selected_info = selected_filament_info(); + const std::string filament_id = selected_info ? selected_info->filament_id : std::string(); // check filament info (rack-gated per-nozzle blacklist evaluation) PresetBundle* preset_bundle = wxGetApp().preset_bundle; const auto& fila_check_res = sCheckFilamentInfo(preset_bundle, obj, ams_id, slot_id, - filament_item.filament_id, ams_filament_id, ams_setting_id); + filament_id, ams_filament_id, ams_setting_id); if (const auto& prohibit_items = fila_check_res.get_items_by_action("prohibition"); !prohibit_items.empty()) { wxString info_msg; @@ -986,6 +1005,133 @@ static void _collect_filament_info(const wxString& shown_name, query_filament_types[shown_name] = filament.config.get_filament_type(); } +static const char* AMS_FAVORITE_FILAMENT_IDS_KEY = "ams_favorite_filament_ids"; + +const AMSMaterialsSetting::FilamentInfos* AMSMaterialsSetting::selected_filament_info() const +{ + const int selection = m_comboBox_filament->GetSelection(); + if (selection < 0 || selection >= int(m_filament_list_items.size()) || !m_filament_list_items[selection].is_filament) + return nullptr; + return &m_filament_list_items[selection].filament; +} + +std::set AMSMaterialsSetting::load_favorite_filament_ids() const +{ + std::set favorite_ids; + std::vector values; + const std::string serialized = wxGetApp().app_config->get(AMS_FAVORITE_FILAMENT_IDS_KEY); + if (!serialized.empty() && Slic3r::unescape_strings_cstyle(serialized, values)) { + favorite_ids.insert(values.begin(), values.end()); + } + return favorite_ids; +} + +void AMSMaterialsSetting::save_favorite_filament_ids(const std::set& favorite_ids) +{ + std::vector values(favorite_ids.begin(), favorite_ids.end()); + wxGetApp().app_config->set(AMS_FAVORITE_FILAMENT_IDS_KEY, Slic3r::escape_strings_cstyle(values)); + wxGetApp().app_config->save(); +} + +void AMSMaterialsSetting::rebuild_filament_dropdown_items(const std::string& selected_filament_id) +{ + std::vector items; + m_filament_list_items.clear(); + + auto append_header = [&items, this](const wxString& label) { + DropDown::Item item; + item.text = label; + item.style = DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED; + items.push_back(item); + m_filament_list_items.push_back(FilamentListItem{}); + }; + + auto append_filament = [&items, this](const wxString& name) { + auto info_it = map_filament_items.find(name); + if (info_it == map_filament_items.end()) + return; + + DropDown::Item item; + item.text = name; + items.push_back(item); + m_filament_list_items.push_back(FilamentListItem{info_it->second, true}); + }; + + // Favorites are listed again at the top under their own header. Without any, the list is the + // plain filament list. + const std::set favorite_ids = load_favorite_filament_ids(); + std::vector favorites; + for (const wxString& name : m_sorted_filament_items) { + auto info_it = map_filament_items.find(name); + if (info_it != map_filament_items.end() && favorite_ids.count(info_it->second.filament_id) > 0) + favorites.push_back(name); + } + if (!favorites.empty()) { + append_header(_L("Favorite filaments")); + for (const wxString& name : favorites) + append_filament(name); + append_header(_L("All filaments")); + } + for (const wxString& name : m_sorted_filament_items) + append_filament(name); + + int selection_idx = -1; + if (!selected_filament_id.empty()) { + for (int i = 0; i < int(m_filament_list_items.size()); ++i) { + if (m_filament_list_items[i].is_filament && m_filament_list_items[i].filament.filament_id == selected_filament_id) { + selection_idx = i; + break; + } + } + } + + // Clear() first: SetItems() keeps the old selection, and SetSelection() ignores an index equal to it, + // which would leave the previous filament's name on the combo. + m_comboBox_filament->Clear(); + m_comboBox_filament->SetItems(items); + update_favorite_stars(); + m_comboBox_filament->SetSelection(selection_idx); + m_favorites_regroup = false; +} + +void AMSMaterialsSetting::update_favorite_stars() +{ + const std::set favorite_ids = load_favorite_filament_ids(); + for (size_t i = 0; i < m_filament_list_items.size(); ++i) { + const FilamentListItem& row = m_filament_list_items[i]; + if (row.is_filament) { + const bool favorite = favorite_ids.count(row.filament.filament_id) > 0; + m_comboBox_filament->SetItemActionBitmap(i, favorite ? m_star_filled.bmp() : m_star_empty.bmp()); + } + } +} + +void AMSMaterialsSetting::toggle_favorite_filament(const std::string& filament_id) +{ + if (filament_id.empty()) + return; + + std::set favorite_ids = load_favorite_filament_ids(); + auto iter = favorite_ids.find(filament_id); + if (iter == favorite_ids.end()) + favorite_ids.insert(filament_id); + else + favorite_ids.erase(iter); + + save_favorite_filament_ids(favorite_ids); +} + +void AMSMaterialsSetting::on_toggle_favorite_filament(wxCommandEvent& evt) +{ + const int index = evt.GetInt(); + if (index < 0 || index >= int(m_filament_list_items.size()) || !m_filament_list_items[index].is_filament) + return; + + toggle_favorite_filament(m_filament_list_items[index].filament.filament_id); + update_favorite_stars(); + m_favorites_regroup = true; +} + void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_min, wxString temp_max, wxString k, wxString n) { if (!obj) return; @@ -999,13 +1145,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi m_input_k_val->GetTextCtrl()->SetValue(k); m_input_n_val->GetTextCtrl()->SetValue(n); - wxArrayString filament_items; wxString bambu_filament_name; - wxString hint_filament_name; // the hint type to be selected std::unordered_map query_filament_vendors;// some information for sort std::unordered_map query_filament_types; // std::set filament_id_set; + m_sorted_filament_items.clear(); // The alias keyed map has to start empty: it is a member, so a stale alias left by an earlier // popup (a different printer, a different nozzle) would resolve to that printer's filament_id. map_filament_items.clear(); @@ -1035,7 +1180,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi if (alias.empty()) continue; - filament_items.push_back(alias); + m_sorted_filament_items.push_back(alias); _collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types); FilamentInfos filament_infos; @@ -1044,7 +1189,6 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi map_filament_items[alias] = filament_infos; if (filament_it->filament_id == ams_filament_id) { - hint_filament_name = from_u8(alias); bambu_filament_name = from_u8(alias); @@ -1215,24 +1359,11 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi return name_compare != 0 ? name_compare < 0 : left < right; }; - std::sort(filament_items.begin(), filament_items.end(), _filament_sorter); + std::sort(m_sorted_filament_items.begin(), m_sorted_filament_items.end(), _filament_sorter); } - // traverse the hint selection idx - int selection_idx = -1; - { - for(int i = 0; i < filament_items.size(); i++) - { - if (hint_filament_name == filament_items[i]) - { - selection_idx = i; - break; - } - } - } - - m_comboBox_filament->Set(filament_items); - m_comboBox_filament->SetSelection(selection_idx); + rebuild_filament_dropdown_items(ams_filament_id); + int selection_idx = m_comboBox_filament->GetSelection(); post_select_event(selection_idx); if (selection_idx < 0) { @@ -1289,6 +1420,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) int* from_printer = static_cast(m_comboBox_filament->GetClientData()); m_filament_type = ""; + const FilamentInfos* selected_info = selected_filament_info(); PresetBundle* preset_bundle = wxGetApp().preset_bundle; if (preset_bundle) { std::ostringstream stream; @@ -1309,9 +1441,8 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) std::string nozzle_diameter_str = stream.str(); // Resolve the selection against the same list Popup() built the dropdown from, so the two // halves of the dialog cannot disagree about which filaments this machine can use. - const std::string selected = m_comboBox_filament->GetValue().ToStdString(); - if (!selected.empty()) { - const std::string filament_id = map_filament_items[selected].filament_id; + if (selected_info) { + const std::string& filament_id = selected_info->filament_id; for (Preset *it : preset_bundle->get_filament_presets_for_machine( DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) { if (it->filament_id != filament_id) @@ -1364,7 +1495,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) //reset cali int cali_select_idx = -1; - if ( !this->obj || m_filament_selection < 0) { + if ( !this->obj || m_filament_selection < 0 || !selected_info) { m_input_k_val->Enable(false); m_input_n_val->Enable(false); m_button_confirm->Disable(); // ORCA No need to change style @@ -1383,21 +1514,9 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) ams_filament_id = ""; ams_setting_id = ""; - if (preset_bundle) { - for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) { - auto itor = map_filament_items.find(m_comboBox_filament->GetValue().ToStdString()); - if ( itor != map_filament_items.end()) { - ams_filament_id = itor->second.filament_id; - ams_setting_id = itor->second.setting_id; - break; - } - - if (it->alias.compare(m_comboBox_filament->GetValue().ToStdString()) == 0) { - ams_filament_id = it->filament_id; - ams_setting_id = it->setting_id; - break; - } - } + if (selected_info) { + ams_filament_id = selected_info->filament_id; + ams_setting_id = selected_info->setting_id; } wxArrayString items; @@ -1529,6 +1648,11 @@ void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect) m_button_reset->Rescale(); // ORCA m_button_confirm->Rescale(); // ORCA m_button_close->Rescale(); // ORCA + // An open list would keep the measurements of the old stars, so close it before replacing them. + m_comboBox_filament->CloseDropdown(); + m_star_filled.msw_rescale(); + m_star_empty.msw_rescale(); + update_favorite_stars(); this->Refresh(); } diff --git a/src/slic3r/GUI/AMSMaterialsSetting.hpp b/src/slic3r/GUI/AMSMaterialsSetting.hpp index 2628e6caf4..6fa215063a 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.hpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.hpp @@ -31,6 +31,7 @@ #include "slic3r/GUI/Widgets/StateColor.hpp" #include #include +#include #define AMS_MATERIALS_SETTING_DEF_COLOUR wxColour(255, 255, 255) #define AMS_MATERIALS_SETTING_GREY900 wxColour(38, 46, 48) @@ -156,6 +157,7 @@ protected: void create_panel_kn(wxWindow* parent); void on_dpi_changed(const wxRect &suggested_rect) override; void on_select_filament(wxCommandEvent& evt); + void on_toggle_favorite_filament(wxCommandEvent& evt); void on_select_cali_result(wxCommandEvent &evt); void on_select_ok(wxCommandEvent &event); void on_select_reset(wxCommandEvent &event); @@ -163,6 +165,11 @@ protected: void on_clr_picker(wxMouseEvent &event); bool is_virtual_tray(); void update_widgets(); + void rebuild_filament_dropdown_items(const std::string& selected_filament_id); + void update_favorite_stars(); + std::set load_favorite_filament_ids() const; + void save_favorite_filament_ids(const std::set& favorite_ids); + void toggle_favorite_filament(const std::string& filament_id); void update_filament_editing(bool is_printing); // Orca: the variant index of the filament's per-variant options on the nozzle this tray feeds @@ -202,12 +209,7 @@ protected: int m_pa_cali_select_id = 0; -// Orca: native wxComboBox on macOS (the custom ComboBox mis-renders the filament dropdown there) -#ifdef __APPLE__ - wxComboBox *m_comboBox_filament; -#else ComboBox *m_comboBox_filament; -#endif ComboBox * m_comboBox_cali_result; TextInput* m_readonly_filament; @@ -215,7 +217,20 @@ protected: std::string filament_id; std::string setting_id; }; - std::map map_filament_items; + struct FilamentListItem { + FilamentInfos filament; + bool is_filament{false}; + }; + const FilamentInfos* selected_filament_info() const; + + // Keyed by the same wxString the dropdown shows, so a row's name never has to be converted back. + std::map map_filament_items; + std::vector m_sorted_filament_items; + std::vector m_filament_list_items; + ScalableBitmap m_star_filled; + ScalableBitmap m_star_empty; + // Favorites changed while the list was open; their rows are regrouped before it opens again. + bool m_favorites_regroup{false}; }; wxDECLARE_EVENT(EVT_SELECTED_COLOR, wxCommandEvent); diff --git a/src/slic3r/GUI/CloneDialog.hpp b/src/slic3r/GUI/CloneDialog.hpp index 7d83b93d62..3233eb3b36 100644 --- a/src/slic3r/GUI/CloneDialog.hpp +++ b/src/slic3r/GUI/CloneDialog.hpp @@ -1,5 +1,4 @@ -#ifndef slic3r_GUI_SingleChoice_hpp_ -#define slic3r_GUI_SingleChoice_hpp_ +#pragma once #include "GUI_Utils.hpp" #include "Plater.hpp" @@ -33,5 +32,3 @@ private: }; }} // namespace Slic3r::GUI - -#endif \ No newline at end of file diff --git a/src/slic3r/GUI/ConfigExceptions.hpp b/src/slic3r/GUI/ConfigExceptions.hpp deleted file mode 100644 index e3c5b08cea..0000000000 --- a/src/slic3r/GUI/ConfigExceptions.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#include -#include - -#include "libslic3r/Exception.hpp" - -namespace Slic3r { - -class ConfigError : public Slic3r::RuntimeError { - using Slic3r::RuntimeError::RuntimeError; -}; - -namespace GUI { - -class ConfigGUITypeError : public ConfigError { - using ConfigError::ConfigError; -}; - -} // namespace GUI -} // namespace Slic3r diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index da5acf8272..cd7e44b5d6 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -1105,6 +1105,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_field("support_filament", have_support_material || have_skirt); toggle_line("raft_contact_distance", have_raft && !have_support_soluble); + // Orca:Hide raft expansion when raft is not enabled + toggle_line("raft_expansion", have_raft); + // Orca: Slim, Strong and Hybrid tree supports size their raft by first layer expansion only. + toggle_field("raft_expansion", !(is_tree(support_type) && (support_style == smsTreeSlim || support_style == smsTreeStrong || support_style == smsTreeHybrid))); // Orca: First-layer density is available for supports broadly. toggle_field("raft_first_layer_density", have_support_material); diff --git a/src/slic3r/GUI/GLTexture.cpp b/src/slic3r/GUI/GLTexture.cpp index f83dd1f854..90cae850c9 100644 --- a/src/slic3r/GUI/GLTexture.cpp +++ b/src/slic3r/GUI/GLTexture.cpp @@ -719,9 +719,6 @@ void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right, // does not treat anti-aliased icon edges as window transparency. glsafe(::glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA)); - glsafe(::glEnable(GL_TEXTURE_2D)); - glsafe(::glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE)); - glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint)tex_id)); GLModel::Geometry init_data; @@ -753,7 +750,6 @@ void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right, glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); - glsafe(::glDisable(GL_TEXTURE_2D)); glsafe(::glDisable(GL_BLEND)); } diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 8940e13e6e..11a436e1ae 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3232,9 +3232,16 @@ bool GUI_App::on_init_inner() #ifdef _MSW_DARK_MODE #ifndef __WINDOWS__ + // The label colours and the dark mode flag were set above from the saved mode. When the system theme + // differs from it, refresh them, or dialogs that map their colours stay in the old mode for this session. + const bool saved_dark_mode = dark_mode(); wxSystemAppearance app = wxSystemSettings::GetAppearance(); GUI::wxGetApp().app_config->set("dark_color_mode", app.IsDark() ? "1" : "0"); GUI::wxGetApp().app_config->save(); + if (dark_mode() != saved_dark_mode) { + init_label_colours(); + Update_dark_mode_flag(); + } #endif // __APPLE__ @@ -4855,12 +4862,6 @@ void GUI_App::recreate_GUI(const wxString &msg_name) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI exit"; } -void GUI_App::system_info() -{ - //SysInfoDialog dlg; - //dlg.ShowModal(); -} - void GUI_App::keyboard_shortcuts(ShortcutContext page, wxWindow* parent) { KBShortcutsDialog dlg(parent != nullptr ? parent : mainframe, page); diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 0a2ddc5f17..e03c3d280a 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -519,7 +519,6 @@ public: void check_printer_presets(); void recreate_GUI(const wxString& message); - void system_info(); void keyboard_shortcuts(ShortcutContext page, wxWindow* parent = nullptr); // the main frame when null void troubleshoot(); void load_project(wxWindow *parent, wxString& input_file) const; diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 5cbda924b5..f4508f47ac 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -1952,6 +1952,16 @@ void MenuFactory::create_plate_menu() }, "", nullptr, []() { return !plater()->get_partplate_list().get_selected_plate()->get_objects().empty(); }, m_parent); + // fix objects on current plate + append_menu_item(menu, wxID_ANY, _L("Fix All"), _L("Fix all models on the current plate"), + [](wxCommandEvent&) { obj_list()->fix_all_through_cgal(false); }, "", nullptr, + []() { return !plater()->get_partplate_list().get_selected_plate()->empty(); }, m_parent); + + // fix objects on all plates + append_menu_item(menu, wxID_ANY, _L("Fix All Plates"), _L("Fix all models on all plates"), + [](wxCommandEvent&) { obj_list()->fix_all_through_cgal(true); }, "", nullptr, + []() { return !plater()->model().objects.empty(); }, m_parent); + // orient objects on current plate append_menu_item(menu, wxID_ANY, _L("Auto Rotate"), _L("Auto rotate current plate"), [](wxCommandEvent&) { diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 21210b06b7..40ba495349 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1617,7 +1617,7 @@ void ObjectList::list_manipulation(const wxPoint& mouse_pos, bool evt_context_me int obj_idx, vol_idx; get_selected_item_indexes(obj_idx, vol_idx, item); if (obj_idx != -1) { - Plater::TakeSnapshot(plater, _u8L("Shift objects to bed")); + Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed")); (*m_objects)[obj_idx]->ensure_on_bed(); cnv->reload_scene(true, true); update_info_items(obj_idx); @@ -2779,12 +2779,13 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type) GLCanvas3D* cnv = plater->canvas3D(); switch (type) { - case InfoItemType::CustomSupports: + case InfoItemType::CustomSupports: { cnv->get_gizmos_manager().reset_all_states(); - Plater::TakeSnapshot(plater, "Remove support painting"); + Plater::TakeSnapshot snapshot(plater, "Remove support painting"); for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes) mv->supported_facets.reset(); break; + } // BBS: remove CustomSeam @@ -2795,19 +2796,21 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type) } break; - case InfoItemType::MmSegmentation: + case InfoItemType::MmSegmentation: { cnv->get_gizmos_manager().reset_all_states(); - Plater::TakeSnapshot(plater, "Remove color painting"); + Plater::TakeSnapshot snapshot(plater, "Remove color painting"); for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes) mv->mmu_segmentation_facets.reset(); break; + } - case InfoItemType::FuzzySkin: + case InfoItemType::FuzzySkin: { cnv->get_gizmos_manager().reset_all_states(); - Plater::TakeSnapshot(plater, _u8L("Remove paint-on fuzzy skin")); + Plater::TakeSnapshot snapshot(plater, _u8L("Remove paint-on fuzzy skin")); for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes) mv->fuzzy_skin_facets.reset(); break; + } // BBS: remove Sinking case InfoItemType::Undef : assert(false); break; @@ -6294,30 +6297,44 @@ void ObjectList::rename_item() } void ObjectList::fix_through_cgal() +{ + std::vector obj_idxs, vol_idxs; + get_selection_indexes(obj_idxs, vol_idxs); + fix_through_cgal(obj_idxs, vol_idxs); +} + +void ObjectList::fix_all_through_cgal(bool all_plates) +{ + std::vector obj_idxs; + if (all_plates) { + for (int obj_idx = 0; obj_idx < int(m_objects->size()); ++obj_idx) + obj_idxs.push_back(obj_idx); + } else { + for (const ModelObject *mo : wxGetApp().plater()->get_partplate_list().get_selected_plate()->get_objects_on_this_plate()) + obj_idxs.push_back(int(std::find(m_objects->begin(), m_objects->end(), mo) - m_objects->begin())); + std::sort(obj_idxs.begin(), obj_idxs.end()); + obj_idxs.erase(std::unique(obj_idxs.begin(), obj_idxs.end()), obj_idxs.end()); + } + fix_through_cgal(obj_idxs, {}); +} + +void ObjectList::fix_through_cgal(const std::vector &obj_idxs, const std::vector &vol_idxs) { // Do not fix anything when a gizmo is open. There might be issues with updates // and what is worse, the snapshot time would refer to the internal stack. - if (!wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::Undefined)) + if (obj_idxs.empty() || !wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::Undefined)) return; // model_name std::vector succes_models; // model_name failing reason std::vector> failed_models; - - std::vector obj_idxs, vol_idxs; - get_selection_indexes(obj_idxs, vol_idxs); + size_t unchanged_models = 0; + bool painting_removed = false; std::vector model_names; - // clear selections from the non-broken models if any exists - // and than fill names of models to repairing if (vol_idxs.empty()) { -#if !FIX_THROUGH_CGAL_ALWAYS - for (int i = int(obj_idxs.size())-1; i >= 0; --i) - if (object(obj_idxs[i])->get_repaired_errors_count() == 0) - obj_idxs.erase(obj_idxs.begin()+i); -#endif // FIX_THROUGH_CGAL_ALWAYS for (int obj_idx : obj_idxs) if (object(obj_idx)) model_names.push_back(object(obj_idx)->name); @@ -6325,31 +6342,26 @@ void ObjectList::fix_through_cgal() else { ModelObject* obj = object(obj_idxs.front()); if (obj) { -#if !FIX_THROUGH_CGAL_ALWAYS - for (int i = int(vol_idxs.size()) - 1; i >= 0; --i) - if (obj->get_repaired_errors_count(vol_idxs[i]) == 0) - vol_idxs.erase(vol_idxs.begin() + i); -#endif // FIX_THROUGH_CGAL_ALWAYS for (int vol_idx : vol_idxs) model_names.push_back(obj->volumes[vol_idx]->name); } } auto plater = wxGetApp().plater(); + const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting"); - auto fix_and_update_progress = [this, plater, model_names](const int obj_idx, const int vol_idx, - int model_idx, - ProgressDialog& progress_dlg, - std::vector& succes_models, - std::vector>& failed_models) + auto fix_and_update_progress = [&](const int obj_idx, const int vol_idx, int model_idx, ProgressDialog& progress_dlg) { - if (!object(obj_idx)) + ModelObject *mo = object(obj_idx); + if (!mo) return false; const std::string& model_name = model_names[model_idx]; wxString msg = _L("Repairing model object"); if (model_names.size() == 1) msg += ": " + from_u8(model_name) + "\n"; + else if (model_names.size() > 10) + msg += ": " + from_u8(model_name) + wxString::Format(" (%d/%d)", model_idx + 1, int(model_names.size())) + "\n"; else { msg += ":\n"; for (int i = 0; i < int(model_names.size()); ++i) @@ -6357,33 +6369,29 @@ void ObjectList::fix_through_cgal() msg += "\n"; } - const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting"); - if (!keep_painting) { - plater->clear_before_change_mesh(obj_idx); - } - const size_t volumes_before = object(obj_idx)->volumes.size(); + auto count_painted = [mo]() { return std::count_if(mo->volumes.begin(), mo->volumes.end(), [](const ModelVolume *mv) { return mv->is_any_painted(); }); }; + const auto painted_before = count_painted(); std::string res; - if (!fix_model_with_cgal_gui(*(object(obj_idx)), vol_idx, progress_dlg, msg, res, keep_painting)) + bool modified = false; + if (!fix_model_with_cgal_gui(*mo, vol_idx, progress_dlg, msg, res, keep_painting, &modified)) return false; - //wxGetApp().plater()->changed_mesh(obj_idx); - object(obj_idx)->ensure_on_bed(); - plater->changed_mesh(obj_idx); - const size_t volumes_after = object(obj_idx)->volumes.size(); - if (volumes_after != volumes_before) - add_volumes_to_object_in_list(obj_idx); + if (modified) { + painting_removed = painting_removed || count_painted() < painted_before; + mo->ensure_on_bed(); + plater->changed_mesh(obj_idx); + plater->get_partplate_list().notify_instance_update(obj_idx, 0); + plater->sidebar().obj_list()->update_plate_values_for_items(); + update_item_error_icon(obj_idx, vol_idx); + update_info_items(obj_idx); + } - plater->get_partplate_list().notify_instance_update(obj_idx, 0); - plater->sidebar().obj_list()->update_plate_values_for_items(); - - if (res.empty()) + if (!res.empty()) + failed_models.push_back({ model_name, res }); + else if (modified) succes_models.push_back(model_name); else - failed_models.push_back({ model_name, res }); - - update_item_error_icon(obj_idx, vol_idx); - update_info_items(obj_idx); - + ++unchanged_models; return true; }; @@ -6396,11 +6404,7 @@ void ObjectList::fix_through_cgal() if (vol_idxs.empty()) { int vol_idx{ -1 }; for (int obj_idx : obj_idxs) { -#if !FIX_THROUGH_CGAL_ALWAYS - if (object(obj_idx)->get_repaired_errors_count(vol_idx) == 0) - continue; -#endif // FIX_THROUGH_CGAL_ALWAYS - if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg, succes_models, failed_models)) + if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg)) break; model_idx++; } @@ -6408,7 +6412,7 @@ void ObjectList::fix_through_cgal() else { int obj_idx{ obj_idxs.front() }; for (int vol_idx : vol_idxs) { - if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg, succes_models, failed_models)) + if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg)) break; model_idx++; } @@ -6416,20 +6420,29 @@ void ObjectList::fix_through_cgal() // Close the progress dialog progress_dlg.Update(100, ""); + if (painting_removed) + plater->notify_painting_removed(); + // Show info notification - wxString msg; + std::vector blocks; wxString bullet_suf = "\n - "; if (!succes_models.empty()) { - msg = _L_PLURAL("The following model object has been repaired", "The following model objects have been repaired", succes_models.size()) + ":"; + wxString block = _L_PLURAL("The following model object has been repaired", "The following model objects have been repaired", succes_models.size()) + ":"; for (auto& model : succes_models) - msg += bullet_suf + from_u8(model); - msg += "\n\n"; + block += bullet_suf + from_u8(model); + blocks.push_back(block); } if (!failed_models.empty()) { - msg += _L_PLURAL("Failed to repair the following model object", "Failed to repair the following model objects", failed_models.size()) + ":\n"; + wxString block = _L_PLURAL("Failed to repair the following model object", "Failed to repair the following model objects", failed_models.size()) + ":"; for (auto& model : failed_models) - msg += bullet_suf + from_u8(model.first) + ": " + _(model.second); + block += bullet_suf + from_u8(model.first) + ": " + _(model.second); + blocks.push_back(block); } + if (unchanged_models > 0) + blocks.push_back(format_wxstr(_L_PLURAL("%1$d model object needed no repair.", "%1$d model objects needed no repair.", unchanged_models), unchanged_models)); + wxString msg; + for (const wxString &block : blocks) + msg += (msg.IsEmpty() ? "" : "\n\n") + block; if (msg.IsEmpty()) msg = _L("Repairing was canceled"); plater->get_notification_manager()->push_notification(NotificationType::CgalFinished, NotificationManager::NotificationLevel::PrintInfoShortNotificationLevel, into_u8(msg)); @@ -6718,7 +6731,7 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event) else if (col == colSinking) { Plater * plater = wxGetApp().plater(); GLCanvas3D *cnv = plater->canvas3D(); - Plater::TakeSnapshot(plater, _u8L("Shift objects to bed")); + Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed")); int obj_idx, vol_idx; get_selected_item_indexes(obj_idx, vol_idx, item); (*m_objects)[obj_idx]->ensure_on_bed(); diff --git a/src/slic3r/GUI/GUI_ObjectList.hpp b/src/slic3r/GUI/GUI_ObjectList.hpp index 03bffb6cdc..85dc3943f9 100644 --- a/src/slic3r/GUI/GUI_ObjectList.hpp +++ b/src/slic3r/GUI/GUI_ObjectList.hpp @@ -47,9 +47,6 @@ typedef double coordf_t; typedef std::pair t_layer_height_range; typedef std::map t_layer_config_ranges; -// Manifold mesh may contain self-intersections, so we want to always allow fixing the mesh. -#define FIX_THROUGH_CGAL_ALWAYS 1 - namespace GUI { enum class Shortcut : uint8_t; @@ -463,6 +460,9 @@ public: void split_instances(); void rename_item(); void fix_through_cgal(); + // Fixes every object on the selected plate, or on all plates. + void fix_all_through_cgal(bool all_plates); + void fix_through_cgal(const std::vector &obj_idxs, const std::vector &vol_idxs); void simplify(); void smooth_mesh(); void update_item_error_icon(const int obj_idx, int vol_idx) const ; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 4bae9a28fe..388b04312d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -1036,6 +1036,11 @@ bool GLGizmoPainterBase::on_mouse(const wxMouseEvent &mouse_event) Vec2d mouse_pos = mouse_coord.cast(); if (mouse_event.Moving()) { + // No button is down, yet a stroke is still open: its release went elsewhere, as a paint drag does not + // capture the mouse. End the stroke as that release would have. + if (m_button_down != Button::None) + gizmo_event(m_button_down == Button::Left ? SLAGizmoEventType::LeftUp : SLAGizmoEventType::RightUp, mouse_pos, + mouse_event.ShiftDown(), mouse_event.AltDown(), false); gizmo_event(SLAGizmoEventType::Moving, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false); return false; } @@ -1205,6 +1210,10 @@ void GLGizmoPainterBase::on_set_state() m_old_mo_id = -1; //m_iva.release_geometry(); m_triangle_selectors.clear(); + // A stroke still open goes with the selectors. Its state must not carry into the next session, where the + // first move would end it as if released and the first press would paint a line from its last position. + m_button_down = Button::None; + m_last_mouse_click = Vec2d::Zero(); //Camera& camera = wxGetApp().plater()->get_camera(); //camera.look_at(camera.get_position(), m_previous_target, Vec3d::UnitZ()); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index cda48ea06c..24e8c19218 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -502,7 +502,17 @@ bool GLGizmoTextureDisplacement::on_init() {ctrl + _L("Mouse wheel"), m_desc["gap_area"]}, toggle_wireframe_shortcut }; - + + // Both ways into the same input box on any slider in the panel: ImGui turns a slider into one when + // it is Ctrl+clicked or when it is given the keyboard focus, which is what the edit icon asks for. + m_desc["type_value"] = _L("Type an exact value into a slider"); + m_shortcuts_sliders = { + {ctrl + _L("Left mouse button"), m_desc["type_value"]}, + {_L("Edit icon at a slider's right end"), m_desc["type_value"]}, + }; + for (std::vector> *list : {&m_shortcuts_brush, &m_shortcuts_bucket_fill, &m_shortcuts_gap_fill}) + list->insert(list->end(), m_shortcuts_sliders.begin(), m_shortcuts_sliders.end()); + return true; } @@ -561,6 +571,11 @@ void GLGizmoTextureDisplacement::on_shutdown() m_shaded_island_delta = Eigen::Matrix::Identity(); m_island_drag_active = false; m_island_move_set.clear(); + // The pane closes below, taking the highlight with it, and the view must not open the next session in a + // Fast the user never picked. Set directly: nothing is to be queued against a gizmo that is closing. + if (m_island_view.switched) + m_use_shaded_preview = false; + m_island_view = IslandViewFollow{}; m_adjust_texture_mode = false; m_seam_edit_mode = false; m_adjust_drag_handle = AdjustHandle::None; @@ -728,18 +743,13 @@ void GLGizmoTextureDisplacement::render_painter_gizmo() // The UV editor's island selection, shown on the model. Polled here rather than pushed: the pane // changes its selection in its own mouse handling, and a compare of a few ints per frame is free. - { - const TextureDisplacementLayer *al = active_layer(); - const UVEditorCanvas *uv_canvas = wxGetApp().plater()->get_uv_editor_canvas(); - if (m_show_uv_editor && al != nullptr && al->projection_method == TextureProjectionMethod::LSCM && - uv_canvas != nullptr && !m_uv_editor_unwrap.empty()) { - if (uv_canvas->selected_islands() != m_island_overlay_selection) - rebuild_island_overlay(uv_canvas->selected_islands()); - render_island_overlay(); - } else if (m_island_overlay_glmodel.is_initialized()) { - m_island_overlay_glmodel.reset(); - m_island_overlay_selection.clear(); - } + if (const std::vector *islands = island_highlight_selection()) { + if (*islands != m_island_overlay_selection) + rebuild_island_overlay(*islands); + render_island_overlay(); + } else if (m_island_overlay_glmodel.is_initialized()) { + m_island_overlay_glmodel.reset(); + m_island_overlay_selection.clear(); } // Diagnostic overlays, drawn on top of whatever preview is active (both pull toward the camera @@ -1534,7 +1544,7 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh() // is box-filtered down to 128 px for a ~48 px row, and feeding it to the shader cost the preview // three quarters of the height map's detail - and, since height_tex_texel is derived from it, also // flattened the shading gradient and made the parallax march skip itself at angles where it should - // run. Cached on the image_data pointer + smoothing, so no PNG is decoded per frame. + // run. Cached per image_data and smoothing, so no PNG is decoded per frame. GLTexture *tex = get_layer_height_texture(*layer); if (tex == nullptr || tex->get_width() <= 0 || tex->get_height() <= 0) return; @@ -1943,6 +1953,16 @@ void GLGizmoTextureDisplacement::render_paint_overlay(GLModel &overlay) shader->stop_using(); } +const std::vector *GLGizmoTextureDisplacement::island_highlight_selection() +{ + const TextureDisplacementLayer *al = active_layer(); + const UVEditorCanvas *uv_canvas = wxGetApp().plater()->get_uv_editor_canvas(); + if (m_show_uv_editor && al != nullptr && al->projection_method == TextureProjectionMethod::LSCM && + uv_canvas != nullptr && !m_uv_editor_unwrap.empty()) + return &uv_canvas->selected_islands(); + return nullptr; +} + void GLGizmoTextureDisplacement::rebuild_island_overlay(const std::vector &selection) { m_island_overlay_glmodel.reset(); @@ -2668,6 +2688,37 @@ void GLGizmoTextureDisplacement::apply_view_mode(int mode) m_parent.set_as_dirty(); } +int GLGizmoTextureDisplacement::view_mode() const +{ + return m_use_shaded_preview ? 1 : + m_uv_check_mode == UVCheckMode::Checker ? 2 : + m_uv_check_mode == UVCheckMode::Distortion ? 3 : 0; +} + +int GLGizmoTextureDisplacement::IslandViewFollow::step(bool shown_now, int view, bool debug_open) +{ + shown = shown_now; + if (!shown) { + // The view is back to Normal already if a debug stage took it, and Normal applied over a stage would + // queue a preview job that replaces the stage being inspected. + const bool restore = switched && view == 1; + switched = keep_normal = false; + return restore ? 0 : -1; + } + if (view != 0 || keep_normal || debug_open) + return -1; + switched = true; + return 1; +} + +void GLGizmoTextureDisplacement::follow_island_highlight() +{ + const std::vector *islands = island_highlight_selection(); + const int view = m_island_view.step(islands != nullptr && !islands->empty(), view_mode(), m_debug_stage >= 0); + if (view >= 0) + apply_view_mode(view); +} + void GLGizmoTextureDisplacement::push_uv_pane_state() { UVEditorCanvas *canvas = wxGetApp().plater()->get_uv_editor_canvas(); @@ -2908,7 +2959,8 @@ void GLGizmoTextureDisplacement::run_uv_command(int cmd, float value) rebuild_preview(); } } - // FrameAll/ToggleSnap are handled inside the canvas; ProjectFromView is not wired yet. + // FrameAll/ToggleSnap are handled inside the canvas; ProjectFromView is not wired yet. SelectionChanged + // only asks for this frame: follow_island_highlight() and the overlay read the selection back themselves. m_parent.set_as_dirty(); } @@ -3585,6 +3637,7 @@ const ModelVolume* GLGizmoTextureDisplacement::texture_volume() const void GLGizmoTextureDisplacement::update_model_object() { + store_panel_state(); bool updated = false; ModelObject *mo = m_c->selection_info()->model_object(); int idx = -1; @@ -3625,6 +3678,24 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update) { wxBusyCursor wait; + // Opening the gizmo puts the panel back where it was left: the layer and the mode come off the + // volume, because they are the user's place in this model's stack, and the dock state off the + // application config, because it is about their window. Only on open - this also runs on an + // undo/redo and on a layer switch, where overriding what the user is doing now would be wrong. + if (first_update) { + if (const ModelVolume *mv = texture_volume(); mv != nullptr) { + const TextureDisplacementOptions &opts = mv->texture_displacement_options; + m_panel_mode = std::clamp(opts.panel_mode, 0, 1); + // Only if that slot still holds a layer: the stack can have been edited by anything that + // does not go through the panel, and a selection pointing at nothing shows an empty panel. + const auto &stack = mv->texture_displacement_layers; + if (std::any_of(stack.begin(), stack.end(), + [&opts](const TextureDisplacementLayer &l) { return l.slot == opts.active_slot; })) + m_active_layer_slot = opts.active_slot; + } + m_undocked = wxGetApp().app_config->get_bool(UNDOCKED_CONFIG_KEY); + } + const ModelObject *mo = m_c->selection_info()->model_object(); m_triangle_selectors.clear(); // The base keeps the last mesh a fill tool hovered, and render_painter_gizmo() reads it as a hover that is @@ -3673,6 +3744,14 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update) rebuild_preview(); } +void GLGizmoTextureDisplacement::store_panel_state() +{ + if (ModelVolume *mv = texture_volume(); mv != nullptr) { + mv->texture_displacement_options.active_slot = m_active_layer_slot; + mv->texture_displacement_options.panel_mode = m_panel_mode; + } +} + void GLGizmoTextureDisplacement::set_active_layer(int slot) { if (slot == m_active_layer_slot) @@ -3681,6 +3760,7 @@ void GLGizmoTextureDisplacement::set_active_layer(int slot) // reflect - otherwise they would be silently lost. update_model_object(); m_active_layer_slot = slot; + store_panel_state(); update_from_model_object(false); // The on-canvas gizmo (if on) is anchored to whichever layer is active - keep it in sync // instead of leaving it pointing at the previous layer's (now stale) paint patch. @@ -3713,6 +3793,8 @@ void GLGizmoTextureDisplacement::ensure_panel_icons() "texture_displacement_move_up.svg", "texture_displacement_move_down.svg", "texture_displacement_drag.svg", "texture_displacement_select_all.svg", "texture_displacement_erase_all.svg", + // The "type a value instead" icon at the right end of every slider. + "edit.svg", }; std::vector paths; paths.reserve(names.size()); @@ -4051,8 +4133,8 @@ void GLGizmoTextureDisplacement::remove_texture_layer(int slot) if (slot >= 0 && slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS)) { mv->texture_displacement_facet(slot).reset(); m_thumbnails[size_t(slot)].reset(); - m_thumbnail_source[size_t(slot)] = nullptr; - m_layer_expanded[size_t(slot)] = false; + m_thumbnail_source[size_t(slot)].reset(); + m_layer_expanded[size_t(slot)] = false; } if (m_picker_slot == slot) m_picker_slot = -1; @@ -4226,7 +4308,7 @@ int GLGizmoTextureDisplacement::select_visible_faces(const std::array selector.reset(); for (unsigned i : unobscured) selector.set_facet(int(front_facing[i]), EnforcerBlockerType::ENFORCER); - selector.request_update_render_data(); + selector.request_update_render_data(true); update_model_object(); m_parent.set_as_dirty(); @@ -4249,7 +4331,7 @@ void GLGizmoTextureDisplacement::select_whole_model() const size_t facet_count = v->mesh().its.indices.size(); for (size_t i = 0; i < facet_count; ++i) m_triangle_selectors[idx]->set_facet(int(i), EnforcerBlockerType::ENFORCER); - m_triangle_selectors[idx]->request_update_render_data(); + m_triangle_selectors[idx]->request_update_render_data(true); } update_model_object(); m_parent.set_as_dirty(); @@ -5365,7 +5447,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_thumbnail(const TextureDisplace return nullptr; const size_t slot = size_t(layer.slot); - if (m_thumbnails[slot] && m_thumbnail_source[slot] == layer.image_data.get() && + if (m_thumbnails[slot] && m_thumbnail_source[slot].lock() == layer.image_data && m_thumbnail_smoothing[slot] == layer.smoothing) return m_thumbnails[slot].get(); @@ -5378,7 +5460,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_thumbnail(const TextureDisplace return nullptr; m_thumbnails[slot] = std::move(texture); - m_thumbnail_source[slot] = layer.image_data.get(); + m_thumbnail_source[slot] = layer.image_data; m_thumbnail_smoothing[slot] = layer.smoothing; return m_thumbnails[slot].get(); } @@ -5388,7 +5470,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_color_texture(const TextureDisp if (layer.empty() || !layer.color_enabled) return nullptr; const ColorAdjustKey adjust = ColorAdjustKey::of(layer); - if (m_color_tex && m_color_tex_source == layer.image_data.get() && m_color_tex_smoothing == layer.smoothing && + if (m_color_tex && m_color_tex_source.lock() == layer.image_data && m_color_tex_smoothing == layer.smoothing && m_color_tex_adjust == adjust) return m_color_tex.get(); @@ -5399,11 +5481,11 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_color_texture(const TextureDisp std::unique_ptr texture = upload_color_texture(tex, HEIGHT_TEX_MAX_PX); if (!texture) { m_color_tex.reset(); - m_color_tex_source = nullptr; + m_color_tex_source.reset(); return nullptr; } m_color_tex = std::move(texture); - m_color_tex_source = layer.image_data.get(); + m_color_tex_source = layer.image_data; m_color_tex_smoothing = layer.smoothing; m_color_tex_adjust = adjust; return m_color_tex.get(); @@ -5416,7 +5498,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_height_texture(const TextureDis // One slot, not one per layer: the preview shader only ever shades the *active* layer, so a single // full-resolution upload is enough and the VRAM cost stays at one texture rather than eight. - if (m_height_tex && m_height_tex_source == layer.image_data.get() && m_height_tex_smoothing == layer.smoothing) + if (m_height_tex && m_height_tex_source.lock() == layer.image_data && m_height_tex_smoothing == layer.smoothing) return m_height_tex.get(); std::unique_ptr texture = upload_height_thumbnail(decode_height_texture(layer), HEIGHT_TEX_MAX_PX); @@ -5424,7 +5506,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_height_texture(const TextureDis return nullptr; m_height_tex = std::move(texture); - m_height_tex_source = layer.image_data.get(); + m_height_tex_source = layer.image_data; m_height_tex_smoothing = layer.smoothing; return m_height_tex.get(); } @@ -5930,6 +6012,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float GizmoImguiBegin(get_name(), flags); ensure_panel_icons(); process_uv_commands(); // clicks from the UV editor pane, run here where the GL context is current + // After the pane's commands, which can close it or change the view, and before the View row, so the row + // lights the view this frame ends up in and a click on it has the last word. + follow_island_highlight(); const float previous_body_h = m_panel_body_h; const float previous_footer_h = m_panel_footer_h; @@ -6054,8 +6139,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float } return s + "..."; }; - // Label column + a slider filling the rest of the row, value printed inside the track. Ctrl+click on - // any of these types a value in. `right_inset` keeps a layer card's inner padding. + // Label column + a slider filling the rest of the row, value printed inside the track. + // `right_inset` keeps a layer card's inner padding. const auto slider_label = [&](const wxString &label) { const float x0 = ImGui::GetCursorPosX(); ImGui::AlignTextToFramePadding(); @@ -6063,17 +6148,107 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float ImGui::SameLine(); ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w)); }; + + // ---- Typed values on sliders ---- + // Every slider in the panel goes through typed_slider(), so all of them take a number the same + // two ways: Ctrl+click on the track, or the edit icon at the right end for the same box without + // the modifier. Both end up in ImGui's own "temp input" mode - the icon only asks ImGui to focus + // the slider, which is what turns it into an input box (see SliderScalar) - so the parsing, + // the clamping and the Enter/click-away behaviour are ImGui's, not a second implementation. + + // Retires a click whose row is no longer being drawn - the panel changed mode in between - so that + // it cannot fire later when that row comes back. Before any row is drawn, so a live one still wins. + if (!m_slider_type_request.empty() && ImGui::GetFrameCount() > m_slider_type_request_frame + 1) + m_slider_type_request.clear(); + + // Exactly as tall as the slider it sits beside, so it lines up with the track without the row + // having to be re-centred around it; icon_button() pads the glyph itself down to about 0.7 of this. + const float edit_icon_w = frame_h; + // What the slider's own width has to give up for the icon beside it. + const float edit_icon_room = edit_icon_w + gap_s; + + // One end of a range, through the slider's own format, so the bounds in the tooltip read exactly + // like the value in the track. A parenthesised note in the format - "(auto)" - is dropped: it + // describes the current state, not the bound. + const auto fmt_bound = [](const char *format, auto v) { + char buf[64]; + std::snprintf(buf, sizeof(buf), format, v); + std::string out(buf); + const size_t paren = out.find('('); + if (paren != std::string::npos) + out.erase(paren); + while (!out.empty() && out.back() == ' ') + out.pop_back(); + return out; + }; + const auto range_text = [](const std::string &lo, const std::string &hi) { + return Slic3r::format(_u8L("Range: %1% to %2%"), lo, hi); + }; + const auto edit_icon_tip = [&](const std::string &range) { + return _u8L("Type a value instead of dragging. Ctrl+click on the slider does the same.") + "\n\n" + range; + }; + + // Draws `slider` at `item_w` (SetNextItemWidth's convention: negative measures back from the right + // edge, and the caller has already left edit_icon_room in it), then the edit icon. `tip` and the + // range go on both. Returns whatever the slider returned. + const auto typed_row = [&](const char *id, float item_w, const wxString &tip, const std::string &range, + const std::function &slider) -> bool { + // Deferred from the click a frame ago: SetKeyboardFocusHere() is a request for the *next* + // frame, so it has to be issued before the slider it names, not from the icon beside it. + if (m_slider_type_request == id) { + ImGui::SetKeyboardFocusHere(); + m_slider_type_request.clear(); + m_parent.set_as_dirty(); // the focus only lands on a frame that actually gets drawn + } + ImGui::SetNextItemWidth(item_w); + const bool changed = slider(); + // AllowWhenDisabled: several of these rows are drawn inside disabled_begin(), and a greyed + // slider is exactly when the user needs to be told what it is and why it is not moving. + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { + wxString full = tip; + if (!full.IsEmpty()) + full += "\n\n"; + m_imgui->tooltip(full + from_u8(range), wrap_w); + } + // SameLine() puts the cursor back at the slider's own top (ItemSize records the Y the item was + // drawn at), so an icon of the same height lands on the track wherever the row placed it. + ImGui::SameLine(0.f, gap_s); + if (icon_button(int(ImGui::GetID(id)), "edit.svg", edit_icon_w, _L("Edit"), from_u8(edit_icon_tip(range)))) { + m_slider_type_request = id; + m_slider_type_request_frame = ImGui::GetFrameCount(); + m_parent.set_as_dirty(); // the next frame is where the request above is turned into focus + } + return changed; + }; + + // The two forms every slider in the panel is one of. They derive the range from the bounds they are + // already given, so no row states its bounds or its format twice and the tooltip cannot drift from + // the track. `item_w` is as typed_row() takes it. + const auto typed_slider = [&](const char *id, float item_w, float *v, float v_min, float v_max, const char *format, + bool log, const wxString &tip) -> bool { + return typed_row(id, item_w, tip, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] { + return ImGui::SliderFloat(id, v, v_min, v_max, format, + ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0)); + }); + }; + const auto typed_slider_int = [&](const char *id, float item_w, int *v, int v_min, int v_max, const char *format, + bool log, const wxString &tip) -> bool { + return typed_row(id, item_w, tip, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] { + return ImGui::SliderInt(id, v, v_min, v_max, format, + ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0)); + }); + }; + + // Label column + the slider filling the rest of the row. const auto float_row = [&](const char *id, const wxString &label, float *v, float v_min, float v_max, const char *format, - bool log, float right_inset) -> bool { + bool log, float right_inset, const wxString &tip) -> bool { slider_label(label); - ImGui::SetNextItemWidth(-std::max(right_inset, 1.f)); - return ImGui::SliderFloat(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0)); + return typed_slider(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, log, tip); }; const auto int_row = [&](const char *id, const wxString &label, int *v, int v_min, int v_max, const char *format, - float right_inset) -> bool { + float right_inset, const wxString &tip) -> bool { slider_label(label); - ImGui::SetNextItemWidth(-std::max(right_inset, 1.f)); - return ImGui::SliderInt(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp); + return typed_slider_int(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, false, tip); }; const auto layer_name = [](const TextureDisplacementLayer &l) { return l.name.empty() ? Slic3r::format(_u8L("Layer %1%"), l.slot + 1) : l.name; @@ -6121,6 +6296,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float "not what you wanted and you know why.")); if (clicked && !on) { m_panel_mode = m; + store_panel_state(); if (!pro_mode()) { // Leaving the subdivision preview open would strand a wireframe whose controls just // disappeared, so close it as part of the switch. @@ -6143,6 +6319,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float _L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it back beside " "the toolbar."))) m_undocked = !m_undocked; + wxGetApp().app_config->set_bool(UNDOCKED_CONFIG_KEY, m_undocked); } ImGui::Separator(); @@ -6284,7 +6461,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float if (v->is_model_part()) { ++idx; m_triangle_selectors[idx]->reset(); - m_triangle_selectors[idx]->request_update_render_data(); + m_triangle_selectors[idx]->request_update_render_data(true); } update_model_object(); m_parent.set_as_dirty(); @@ -6295,11 +6472,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float const auto centre_on_row = [&](float h) { ImGui::SetCursorPosY(row_y + std::round((icon_md - h) * 0.5f)); }; vsep(icon_md); if (is_brush_mode) { - ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm))); centre_on_row(frame_h); - ImGui::SliderFloat("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm", - ImGuiSliderFlags_AlwaysClamp); - hover_tip(m_desc.at("cursor_size")); + typed_slider("##cursor_radius", + std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm) - edit_icon_room), + &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm", false, m_desc.at("cursor_size")); vsep(icon_sm); const bool is_circle = m_cursor_type == TriangleSelector::CursorType::CIRCLE; centre_on_row(icon_sm); @@ -6312,11 +6488,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float _L("Sphere - paints only within a ball around the point under the cursor"))) m_cursor_type = TriangleSelector::CursorType::SPHERE; } else if (is_area_mode) { - ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX())); centre_on_row(frame_h); - ImGui::SliderFloat("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", - ImGuiSliderFlags_AlwaysClamp); - hover_tip(_u8L("Angle threshold - the fill stops at edges sharper than this")); + typed_slider("##smart_fill_angle", std::max(1.f, row_end - ImGui::GetCursorPosX() - edit_icon_room), + &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", false, + _L("Angle threshold - the fill stops at edges sharper than this")); } else { centre_on_row(frame_h); ImGui::AlignTextToFramePadding(); @@ -6328,10 +6503,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float // ---- View: Normal / Fast / Checker / Distortion as one group, Wireframe on its own ---- // The underlying state stays m_use_shaded_preview + m_uv_check_mode. { - const int cur_mode = m_use_shaded_preview ? 1 : - m_uv_check_mode == UVCheckMode::Checker ? 2 : - m_uv_check_mode == UVCheckMode::Distortion ? 3 : 0; + const int cur_mode = view_mode(); int new_mode = cur_mode; + int picked = -1; // the button clicked, the active one included bool wf_toggle = false; bool open_uv_editor = false; // Checker and Distortion both draw *the unwrap* - the first the texture grid laid over it, the second @@ -6354,19 +6528,19 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + m_imgui->scaled(2.6f))); if (icon_toggle(701, "texture_displacement_real_preview.svg", cur_mode == 0, icon_md, _L("Normal"), _L("Normal - the real geometry, exactly what Bake will produce. Slower to update"))) - new_mode = 0; + new_mode = picked = 0; ImGui::SameLine(0.f, gap_s); if (icon_toggle(702, "texture_displacement_fast_preview.svg", cur_mode == 1, icon_md, _L("Fast"), _L("Fast - the relief is only shaded on, not built, and only for the layer you are " "editing. Updates instantly while you paint"))) - new_mode = 1; + new_mode = picked = 1; ImGui::SameLine(0.f, gap_s); if (icon_toggle(703, "texture_displacement_checker.svg", cur_mode == 2, icon_md, _L("Checker"), _L("Checker - a test grid instead of the texture. Where the squares stay square the " "texture is undistorted; where they stretch, it will too. Opens the UV editor if " "it is closed"), uv_view_na)) { - new_mode = 2; + new_mode = picked = 2; open_uv_editor = true; } ImGui::SameLine(0.f, gap_s); @@ -6374,7 +6548,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float _L("Distortion - blue-to-red stretch heatmap over the unwrap. Opens the UV editor if " "it is closed"), uv_view_na)) { - new_mode = 3; + new_mode = picked = 3; open_uv_editor = true; } vsep(icon_md); @@ -6398,6 +6572,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float if (new_mode == cur_mode) update_uv_editor(); // otherwise apply_view_mode() below does it } + if (picked >= 0) + m_island_view.picked(picked); if (new_mode != cur_mode) apply_view_mode(new_mode); if (wf_toggle) { @@ -6586,16 +6762,16 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float // The three controls nearly every layer needs; everything else waits behind "More settings". // Depth and tile size are logarithmic, so both ends of their wide ranges stay usable. - m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad); - hover_tip(_u8L("Height of the relief: how far white in the texture lifts the surface, in " - "millimetres. Black does not move it at all, unless Midlevel says otherwise.")); - m_preview_params_dirty |= float_row("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad); - hover_tip(_u8L("How wide one copy of the texture is on the model. Smaller repeats the pattern " - "more often and makes its detail finer; with Tile off, this is the size of the " - "single copy.")); - m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad); - hover_tip(_u8L("Turns the texture on the surface, in degrees - for lining a pattern up with an " - "edge of the model.")); + m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad, + _L("Height of the relief: how far white in the texture lifts the surface, in " + "millimetres. Black does not move it at all, unless Midlevel says otherwise.")); + m_preview_params_dirty |= float_row("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad, + _L("How wide one copy of the texture is on the model. Smaller repeats the pattern " + "more often and makes its detail finer; with Tile off, this is the size of the " + "single copy.")); + m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad, + _L("Turns the texture on the surface, in degrees - for lining a pattern up with an " + "edge of the model.")); if (m_layer_expanded[size_t(layer.slot)]) { { @@ -6610,22 +6786,22 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float // Midlevel: the height that means "don't move". At 0 the surface only ever bulges outwards; // at 0.5 mid-grey is neutral and darker texels cut inwards. - m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad); - hover_tip(_u8L("Which gray stays where the surface already is. At 0 the texture only pushes " - "outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones " - "still push out - one image both embosses and engraves.\n\n" - "What cuts in has to fit: inside a sharp corner or through a thin wall, a deep " - "cut can pass through the other side.")); + m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad, + _L("Which gray stays where the surface already is. At 0 the texture only pushes " + "outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones " + "still push out - one image both embosses and engraves.\n\n" + "What cuts in has to fit: inside a sharp corner or through a thin wall, a deep " + "cut can pass through the other side.")); if (layer.midlevel > 0.f && layer.depth_mm > 1.f) { ImGui::PushTextWrapPos(content_rx - card_pad); m_imgui->warning_text(_L("Deep inward displacement may self-intersect.")); ImGui::PopTextWrapPos(); } - m_preview_params_dirty |= float_row("##smoothing", _L("Smoothing"), &layer.smoothing, 0.f, 1.f, "%.2f", false, card_pad); - hover_tip(_u8L("Blurs the image before it is used, which rounds off hard steps and removes " - "speckle from a noisy photo. Raise it if the relief looks harsh or grainy; it " - "costs fine detail.")); + m_preview_params_dirty |= float_row("##smoothing", _L("Smoothing"), &layer.smoothing, 0.f, 1.f, "%.2f", false, card_pad, + _L("Blurs the image before it is used, which rounds off hard steps and removes " + "speckle from a noisy photo. Raise it if the relief looks harsh or grainy; it " + "costs fine detail.")); // Edge fade: the relief flattens toward the boundary of the painted area. m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Edge fade") + "##edge_smoothing", layer.edge_smoothing); @@ -6633,12 +6809,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float "blends into the bare surface instead of stopping at a step.")); ImGui::SameLine(); m_imgui->disabled_begin(!layer.edge_smoothing); - ImGui::SetNextItemWidth(-card_pad); - m_preview_params_dirty |= ImGui::SliderFloat("##edge_amount", &layer.edge_smoothing_amount, 0.02f, 1.f, "%.2f", - ImGuiSliderFlags_AlwaysClamp); + m_preview_params_dirty |= typed_slider("##edge_amount", -(card_pad + edit_icon_room), + &layer.edge_smoothing_amount, 0.02f, 1.f, "%.2f", false, + _L("How far in the fade reaches, as a share of the painted area. Small " + "values soften a narrow band at the edge; 1 flattens almost all of it.")); m_imgui->disabled_end(); - hover_tip(_u8L("How far in the fade reaches, as a share of the painted area. Small values " - "soften a narrow band at the edge; 1 flattens almost all of it.")); // Invert and Colours share a row. { @@ -6683,19 +6858,19 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float // Every mix can become a filament slot, and there are only so many beside the // physical filaments. const int max_mixes = std::max(1, int(EnforcerBlockerType::ExtruderMax) - int(m_palette_filaments.size())); - if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad)) + if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad, + _L("The most mixed filaments a bake may add. They are picked from the " + "texture's colors, and only the ones the bake actually uses are created."))) m_preview_params_dirty = true; - hover_tip(_u8L("The most mixed filaments a bake may add. They are picked from the " - "texture's colors, and only the ones the bake actually uses are created.")); // After the slider, so a change shows in the same frame. ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colors from %2% filaments"), int(cached_palette().size()), int(m_palette_filaments.size())).c_str()); } - if (int_row("##color_despeckle", _L("Denoise"), &opts.color_despeckle, 0, 6, "%d", card_pad)) - m_preview_params_dirty = true; - hover_tip(_u8L("Cleans up single stray triangles of the wrong color, which detail finer " + if (int_row("##color_despeckle", _L("Denoise"), &opts.color_despeckle, 0, 6, "%d", card_pad, + _L("Cleans up single stray triangles of the wrong color, which detail finer " "than the mesh leaves behind. Raise it if the result looks speckled, " - "lower it if small features are being swallowed.")); + "lower it if small features are being swallowed."))) + m_preview_params_dirty = true; } } @@ -6800,10 +6975,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float "gets the texture, and its border becomes the edge of the projection.")); if (projector_open) { - if (int_row("##projector_opacity", _L("Opacity"), &m_projector_opacity, 20, 255, "%d", card_pad)) + if (int_row("##projector_opacity", _L("Opacity"), &m_projector_opacity, 20, 255, "%d", card_pad, + _L("How solid the frame window looks while you place it. Lower to see the " + "model through it; it does not affect the result."))) m_projector_frame->set_opacity(m_projector_opacity); - hover_tip(_u8L("How solid the frame window looks while you place it. Lower to see the " - "model through it; it does not affect the result.")); if (m_imgui->button(_u8L("Apply projection frame"))) { const int painted_count = apply_projection_frame(); @@ -7104,57 +7279,57 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float // pattern can sample four points that all happen to land at similar heights, report no error, // and stall before refinement ever starts. if (float_row("##subdiv_target", m_subdivide_feature ? _L("Max edge") : _L("Target edge"), &m_subdivide_target_mm, - 0.001f, 20.f, "%.3f mm", true, 0.f)) + 0.001f, 20.f, "%.3f mm", true, 0.f, + m_subdivide_feature ? + _L("No triangle in the painted area stays larger than this, even where the texture " + "is flat. Keep it near the size of the smallest feature you want to come out.") : + _L("Triangles in the painted area are split until none is larger than this. Smaller " + "means finer detail and, fast, a lot more triangles."))) preview_live(); - hover_tip(m_subdivide_feature ? - _u8L("No triangle in the painted area stays larger than this, even where the texture " - "is flat. Keep it near the size of the smallest feature you want to come out.") : - _u8L("Triangles in the painted area are split until none is larger than this. Smaller " - "means finer detail and, fast, a lot more triangles.")); if (m_subdivide_feature) { - if (float_row("##subdiv_detail", _L("Detail"), &m_subdivide_detail_mm, 0.001f, 1.f, "%.3f mm", true, 0.f)) + if (float_row("##subdiv_detail", _L("Detail"), &m_subdivide_detail_mm, 0.001f, 1.f, "%.3f mm", true, 0.f, + _L("How far the mesh may sit from the shape the texture describes, in millimetres. " + "Smaller follows fine detail and costs triangles; larger only chases the big " + "features."))) preview_live(); - hover_tip(_u8L("How far the mesh may sit from the shape the texture describes, in millimetres. " - "Smaller follows fine detail and costs triangles; larger only chases the big " - "features.")); - if (float_row("##subdiv_min", _L("Min edge"), &m_subdivide_min_edge_mm, 0.001f, 20.f, "%.3f mm", true, 0.f)) + if (float_row("##subdiv_min", _L("Min edge"), &m_subdivide_min_edge_mm, 0.001f, 20.f, "%.3f mm", true, 0.f, + _L("A floor on triangle size, so a sharp step in the texture cannot be chased " + "forever. Lower it for finer relief, raise it if the count runs away at hard " + "edges."))) preview_live(); - hover_tip(_u8L("A floor on triangle size, so a sharp step in the texture cannot be chased " - "forever. Lower it for finer relief, raise it if the count runs away at hard " - "edges.")); } // Applies in both adaptive sub-modes: it is about the step the bake puts at the paint's edge, which // exists whether or not "Follow texture detail" is on. 0 turns the band off. - if (float_row("##subdiv_border", _L("Edge detail"), &m_subdivide_border_mm, 0.f, 5.f, "%.3f mm", false, 0.f)) + if (float_row("##subdiv_border", _L("Edge detail"), &m_subdivide_border_mm, 0.f, 5.f, "%.3f mm", false, 0.f, + _L("Triangle size along the outline of the painted area, where the relief drops back to the " + "bare surface. Lower it if that rim looks jagged; it only costs triangles along the " + "outline. 0 turns it off."))) preview_live(); - hover_tip(_u8L("Triangle size along the outline of the painted area, where the relief drops back to the " - "bare surface. Lower it if that rim looks jagged; it only costs triangles along the " - "outline. 0 turns it off.")); // Only worth showing when a layer is actually colouring: with no colour there is no boundary for it // to refine and the control would do nothing whatever it is set to. if (mv != nullptr && any_layer_colors(*mv)) { - if (float_row("##subdiv_color", _L("Color detail"), &m_subdivide_color_mm, 0.f, 5.f, "%.3f mm", false, 0.f)) + if (float_row("##subdiv_color", _L("Color detail"), &m_subdivide_color_mm, 0.f, 5.f, "%.3f mm", false, 0.f, + _L("Triangle size where two colors meet. Each triangle prints in one filament, so a " + "color edge can only be as sharp as the triangles along it - and nothing else " + "refines there, since the surface is flat across a change of color. 0 turns it off."))) preview_live(); - hover_tip(_u8L("Triangle size where two colors meet. Each triangle prints in one filament, so a " - "color edge can only be as sharp as the triangles along it - and nothing else " - "refines there, since the surface is flat across a change of color. 0 turns it off.")); } ImGui::TextDisabled("%s", _u8L("Triangle budget: set with Triangles, below.").c_str()); if (m_subdivide_editing && m_subdivide_preview_tris > 0) m_imgui->text(Slic3r::format(_u8L("Preview: %1% triangles"), m_subdivide_preview_tris)); } else { - if (int_row("##subdiv_steps", _L("Steps"), &m_subdivide_count, 0, 5, "%d", 0.f)) { + if (int_row("##subdiv_steps", _L("Steps"), &m_subdivide_count, 0, 5, "%d", 0.f, + _L("Splits every triangle of the model into four, this many times over. Each step " + "quadruples the count - 3 turns 100 k triangles into 6.4 M - so prefer refining " + "only the painted area unless you need the whole model finer."))) { if (m_subdivide_editing) rebuild_subdivide_preview(); // a count of 0 clears the preview, it doesn't compute one m_parent.set_as_dirty(); } - hover_tip(_u8L("Splits every triangle of the model into four, this many times over. Each step " - "quadruples the count - 3 turns 100 k triangles into 6.4 M - so prefer refining " - "only the painted area unless you need the whole model finer.")); } { @@ -7216,9 +7391,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float } m_remesh_target_edge_mm = cnt > 0 ? std::clamp(float(sum / double(cnt)), 0.1f, 20.f) : 1.f; } - float_row("##remesh_edge", _L("Target edge"), &m_remesh_target_edge_mm, 0.1f, 20.f, "%.2f mm", true, 0.f); - hover_tip(_u8L("Triangle size the whole model is rebuilt with, in millimetres. Displacement works best on an " - "even mesh; this is what makes one out of an uneven import.")); + float_row("##remesh_edge", _L("Target edge"), &m_remesh_target_edge_mm, 0.1f, 20.f, "%.2f mm", true, 0.f, + _L("Triangle size the whole model is rebuilt with, in millimetres. Displacement works best on an " + "even mesh; this is what makes one out of an uneven import.")); m_imgui->bbl_checkbox(_L("Keep sharp edges") + "##remesh_sharp", m_remesh_keep_sharp_edges); hover_tip(_u8L("Holds hard edges and open borders in place while the rest is remeshed. Without it " @@ -7226,11 +7401,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float "a cube comes back with wobbly edges.")); ImGui::SameLine(); m_imgui->disabled_begin(!m_remesh_keep_sharp_edges); - ImGui::SetNextItemWidth(-1.f); - ImGui::SliderFloat("##remesh_sharp_angle", &m_remesh_sharp_angle_deg, 5.f, 90.f, "%.0f°", ImGuiSliderFlags_AlwaysClamp); - m_imgui->disabled_end(); - hover_tip(_u8L("How sharp a fold has to be, in degrees, before the remesher treats it as an edge worth " + typed_slider("##remesh_sharp_angle", -(1.f + edit_icon_room), &m_remesh_sharp_angle_deg, 5.f, 90.f, "%.0f°", false, + _L("How sharp a fold has to be, in degrees, before the remesher treats it as an edge worth " "keeping. Lower protects more of the model's shape; higher rebuilds more of it evenly.")); + m_imgui->disabled_end(); m_imgui->disabled_begin(mv == nullptr || busy); if (ImGui::Button(_u8L("Remesh").c_str())) @@ -7258,16 +7432,16 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float "on a layer is a different thing: it blurs the image before it is used.")); if (opts.smooth_enabled) { float percent = opts.smooth_strength * 100.f; - if (float_row("##dispsmooth", _L_CONTEXT("Strength", "Texture Displacement"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f)) { + if (float_row("##dispsmooth", _L_CONTEXT("Strength", "Texture Displacement"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f, + _L("How far each pass pulls a vertex towards its neighbours. High values round the " + "relief off quickly; low values need more passes but keep more of the detail."))) { opts.smooth_strength = std::clamp(percent / 100.f, 0.01f, 1.f); m_preview_params_dirty = true; } - hover_tip(_u8L("How far each pass pulls a vertex towards its neighbours. High values round the " - "relief off quickly; low values need more passes but keep more of the detail.")); - if (int_row("##dispsmoothit", _L("Passes"), &opts.smooth_iterations, 1, 10, "%d", 0.f)) + if (int_row("##dispsmoothit", _L("Passes"), &opts.smooth_iterations, 1, 10, "%d", 0.f, + _L("How many smoothing passes to run. More passes spread the smoothing further across " + "the surface; Strength decides how much each one moves."))) m_preview_params_dirty = true; - hover_tip(_u8L("How many smoothing passes to run. More passes spread the smoothing further across " - "the surface; Strength decides how much each one moves.")); m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Ignore outer ring"), opts.smooth_skip_border); hover_tip(_u8L("Keeps the outer ring of the painted area out of the smoothing. Its neighbours " @@ -7310,16 +7484,16 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float int shown_k = auto_budget ? v2_recommendation(*mv).budget_k : opts.v2_max_triangles_k; m_imgui->disabled_begin(auto_budget); // TRN Slider value: %d is the triangle budget in thousands - if (int_row("##v2budget", _L("Budget"), &shown_k, 0, 4000, auto_budget ? _u8L("%d k (auto)").c_str() : "%d k", 0.f)) { + if (int_row("##v2budget", _L("Budget"), &shown_k, 0, 4000, auto_budget ? _u8L("%d k (auto)").c_str() : "%d k", 0.f, + _L("How many thousand triangles this bake may spend on the area you painted. " + "Relief already baked into the rest of the model is kept on top of it, so a " + "second bake somewhere else gets the same budget as the first. Raise it if the " + "warning below Resolution says the detail will not fit; 0 keeps every triangle " + "the refinement produced, however many that is."))) { opts.v2_max_triangles_k = shown_k; m_preview_params_dirty = true; } m_imgui->disabled_end(); - hover_tip(_u8L("How many thousand triangles this bake may spend on the area you painted. " - "Relief already baked into the rest of the model is kept on top of it, so a " - "second bake somewhere else gets the same budget as the first. Raise it if the " - "warning below Resolution says the detail will not fit; 0 keeps every triangle " - "the refinement produced, however many that is.")); } if (SHOW_PIPELINE_DEV_CONTROLS && opts.pipeline_v2) { // Keeping the relief above the plate is not a checkbox: it is unconditional, in both pipelines @@ -7426,26 +7600,28 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float "default. Untick to set both yourself - worth doing for a texture much finer or " "much coarser than the part it sits on.")); ImGui::SameLine(); - ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX()); float shown = auto_res ? rec.edge_mm : opts.v2_refine_mm; + // Held in a local: the format outlives the call that reads it, and the slider and the + // range in its tooltip are then formatted the same way. + const std::string v2edge_format = auto_res ? _u8L("%.2f mm (auto)") : std::string("%.2f mm"); + const wxString v2edge_tip = + auto_res && rec.edge_mm > 0.f ? + from_u8(Slic3r::format(_u8L("Chosen from the size of the model: %1$.2f mm triangles, " + "budget %2% k. Untick Auto to set them yourself."), + rec.edge_mm, rec.budget_k)) : + _L("How fine the mesh is made under the paint, in millimetres. It has to be " + "smaller than the detail you want out of the texture - a 0.5 mm groove needs " + "triangles well under 0.5 mm. Smaller costs triangles fast: halving it needs " + "four times as many, and once they no longer fit the budget the bake simplifies " + "back down and the detail goes with it."); m_imgui->disabled_begin(busy || auto_res); - if (ImGui::SliderFloat("##v2edge", &shown, 0.02f, 2.f, auto_res ? _u8L("%.2f mm (auto)").c_str() : "%.2f mm", - ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic)) { + if (typed_slider("##v2edge", x0 + panel_w - ImGui::GetCursorPosX() - edit_icon_room, &shown, 0.02f, 2.f, + v2edge_format.c_str(), true, v2edge_tip)) { opts.v2_refine_mm = shown; m_preview_params_dirty = true; m_parent.set_as_dirty(); } m_imgui->disabled_end(); - if (auto_res && rec.edge_mm > 0.f) - hover_tip(Slic3r::format(_u8L("Chosen from the size of the model: %1$.2f mm triangles, " - "budget %2% k. Untick Auto to set them yourself."), - rec.edge_mm, rec.budget_k)); - else - hover_tip(_u8L("How fine the mesh is made under the paint, in millimetres. It has to be " - "smaller than the detail you want out of the texture - a 0.5 mm groove needs " - "triangles well under 0.5 mm. Smaller costs triangles fast: halving it needs " - "four times as many, and once they no longer fit the budget the bake simplifies " - "back down and the detail goes with it.")); // What this resolution costs over what is painted, against what the budget allows. Refining // past the budget is not an error - the bake simplifies back down to it - but the result @@ -7477,18 +7653,17 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float m_imgui->text(_L("Triangles")); ImGui::SameLine(); ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w)); - ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX()); m_imgui->disabled_begin(busy); - if (ImGui::SliderInt("##subdivbudget", &m_subdivide_budget_k, 10, 2000, "+%d k", - ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic)) { + if (typed_slider_int("##subdivbudget", x0 + panel_w - ImGui::GetCursorPosX() - edit_icon_room, + &m_subdivide_budget_k, 10, 2000, "+%d k", true, + _L("How many thousand triangles the refinement may add to the model. The triangle " + "that fits worst is always split first, so even a run that spends the lot has " + "spent it where it shows most. Raise it if the relief still looks coarse."))) { if (m_subdivide_editing) rebuild_subdivide_preview(); m_parent.set_as_dirty(); } m_imgui->disabled_end(); - hover_tip(_u8L("How many thousand triangles the refinement may add to the model. The triangle " - "that fits worst is always split first, so even a run that spends the lot has " - "spent it where it shows most. Raise it if the relief still looks coarse.")); } const float button_h = std::round(frame_h * 1.25f); @@ -7507,7 +7682,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float : m_tool_type == ToolType::SMART_FILL ? m_shortcuts_bucket_fill : m_tool_type == ToolType::BRUSH ? m_shortcuts_brush : m_tool_type == ToolType::GAP_FILL ? m_shortcuts_gap_fill - : std::vector>{}; + : m_shortcuts_sliders; // the sliders are there whether or not a painting tool is ImGui::SetCursorPosY(row_y + (row_h - icon_h) * .5f); // center vertically ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(int(m_imgui->scaled(.5f)), style.ItemSpacing.y)); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp index 24eaaa1f9b..642481c139 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp @@ -224,6 +224,27 @@ public: }; static PaintedColors painted_colors(const TriangleMesh &mesh, const TriangleSelector::TriangleSplittingData &paint); + // The view while the UV editor's selected islands are highlighted on the model. The highlight lies on the + // undisplaced surface, so the Normal view's relief hides it wherever it rises: Normal gives way to Fast + // while islands are selected and comes back once none are. A view the user clicks in the View row is + // theirs - the pane's background buttons are not a pick, Height only meaning "the texture view". + struct IslandViewFollow + { + bool shown = false; // the highlight, as of the last step() + bool switched = false; // Fast was switched on for the highlight, so Normal comes back once it is gone + bool keep_normal = false; // the user picked Normal in the View row while the highlight was shown + // The view to apply this frame (0 Normal, 1 Fast), or -1 for none. `view` is the one on screen, numbered as + // apply_view_mode() takes it; `debug_open` a debug stage on screen, which holds Normal itself. + int step(bool shown_now, int view, bool debug_open); + // A View row button clicked, the active one included. The row is drawn after step() in the same frame, + // so `shown` is this frame's. + void picked(int view) + { + switched = false; + keep_normal = shown && view == 0; + } + }; + void render_painter_gizmo() override; // Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/ @@ -339,6 +360,9 @@ private: bool m_image_sec_place = true; bool m_image_sec_color = true; bool m_image_sec_bg = true; + // Copies the panel's own state (active layer, Standard/Pro) onto the volume, so the project file + // can carry it. Cheap and idempotent - called wherever either of the two changes. + void store_panel_state(); // Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace. // The order matters and is the whole reason this is one button - a height map can only move // existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop @@ -479,6 +503,12 @@ private: // Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation. // Panel state only, not saved with the project. std::array m_layer_expanded{}; + // The slider whose edit icon was just clicked, as its ImGui id, and the frame it was clicked on. + // The row acts on it on the next frame - ImGui's keyboard-focus request only takes effect a frame + // later - and clears it; the frame number is what retires a request whose row has gone off screen + // in the meantime. Panel state only, not saved with the project. + std::string m_slider_type_request; + int m_slider_type_request_frame = 0; // "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active // layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane @@ -579,6 +609,11 @@ private: // The panel's view modes: 0 Normal, 1 Fast, 2 Checker, 3 Distortion. Shared by the View row and the pane's // background buttons, so both switch views the same way. void apply_view_mode(int mode); + // The view on screen, numbered as apply_view_mode() takes it. + int view_mode() const; + // Run once a frame, after the pane's commands: switches the view as m_island_view says. + void follow_island_highlight(); + IslandViewFollow m_island_view; // The pane header's thumbnail of the active layer's texture, rebuilt only when its image changes. static constexpr int UV_THUMB_PX = 64; const void *m_uv_thumb_source = nullptr; @@ -810,6 +845,8 @@ private: // in place. Rebuilt whenever the pane's selection differs from the one it was built for. GLModel m_island_overlay_glmodel; std::vector m_island_overlay_selection; + // The pane's selection while the overlay applies - the pane open over an unwrapped LSCM layer - else null. + const std::vector *island_highlight_selection(); void rebuild_island_overlay(const std::vector &selection); void render_island_overlay(); // Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept @@ -990,26 +1027,27 @@ private: bool m_preview_job_pending = false; void queue_preview_job(); - // Per-slot GPU thumbnail cache for the layer list panel, keyed by the image_data pointer that - // was current the last time each thumbnail was built (see get_layer_thumbnail()). - std::array, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails; - std::array m_thumbnail_source{}; + // Per-slot GPU thumbnail cache for the layer list panel, each keyed by the image_data it was built from + // (see get_layer_thumbnail()). Held weakly rather than by address, as decode_height_texture()'s cache is: + // an image freed and replaced by another at the same address must not pass for it. + std::array, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails; + std::array>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source; // The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it. - std::array m_thumbnail_smoothing{}; + std::array m_thumbnail_smoothing{}; // Full-resolution height texture for the preview shader, keyed the same way (see // get_layer_height_texture()). A smoothing change re-uploads it, so the fast preview shows the // blur the bake will apply. - std::unique_ptr m_height_tex; - const void *m_height_tex_source = nullptr; - float m_height_tex_smoothing = -1.f; + std::unique_ptr m_height_tex; + std::weak_ptr> m_height_tex_source; + float m_height_tex_smoothing = -1.f; // The same, for the layer's *colour*, which the fast preview quantizes per fragment so it shows // the image at texel resolution rather than at the mesh's. Null for a grayscale texture. - std::unique_ptr m_color_tex; - const void *m_color_tex_source = nullptr; - float m_color_tex_smoothing = -1.f; - ColorAdjustKey m_color_tex_adjust; + std::unique_ptr m_color_tex; + std::weak_ptr> m_color_tex_source; + float m_color_tex_smoothing = -1.f; + ColorAdjustKey m_color_tex_adjust; // Library textures the picker has shown at least once, keyed by file path (see LibraryTexture). std::map m_library_textures; @@ -1047,9 +1085,12 @@ private: PatchUnwrap m_uv_editor_unwrap; // When set, the panel is a free-floating window the user can drag anywhere (with a title bar to - // grab), instead of being pinned to the right of the gizmo toolbar. Persisted across gizmo - // open/close within a session, so the choice sticks while working. + // grab), instead of being pinned to the right of the gizmo toolbar. Kept in the application + // config rather than in the project: it describes the user's window, not the model, so opening + // someone else's project must not move their panel. bool m_undocked = false; + // The config key behind m_undocked. + static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked"; // Smooth scrolling for the panel body (everything between the header and the pinned Bake footer). // ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard @@ -1092,6 +1133,9 @@ private: std::vector> m_shortcuts_bucket_fill; // Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")} std::vector> m_shortcuts_gap_fill; + // The two ways every slider in the panel takes a typed value. They hold whatever tool is selected, + // so they are tacked onto each of the lists above and stand on their own when no tool is active. + std::vector> m_shortcuts_sliders; // Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with // the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index bedc9c97ed..b7994993d5 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -428,7 +428,10 @@ void GLGizmosManager::refresh_on_off_state() void GLGizmosManager::reset_all_states() { - if (! m_enabled || m_serializing) + // The restore/new-project event can run before deferred OpenGL resource + // loading has populated the gizmo list. There is nothing to reset until + // initialization completes. + if (! m_enabled || m_serializing || m_gizmos.empty()) return; const EType current = get_current_type(); @@ -443,6 +446,9 @@ void GLGizmosManager::reset_all_states() bool GLGizmosManager::open_gizmo(EType type) { + if (m_gizmos.empty()) + return false; + int idx = static_cast(type); // re-open same type cause closing @@ -530,7 +536,7 @@ bool GLGizmosManager::is_running() const bool GLGizmosManager::open_gizmo_by_shortcut(Shortcut shortcut) { - if (!m_enabled) + if (!m_enabled || m_gizmos.empty()) return false; // The text tool opens without a selection because it creates its own object. @@ -1438,10 +1444,12 @@ void GLGizmosManager::update_hover_state(const EType &type) bool GLGizmosManager::activate_gizmo(EType type) { - assert(!m_gizmos.empty()); - // already activated - if (m_current == type) return true; + if (m_current == type) + return true; + + if (m_gizmos.empty()) + return false; if (m_current != Undefined) { // clean up previous gizmo diff --git a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp index bb76a171b8..be3e961e09 100644 --- a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp +++ b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp @@ -925,8 +925,9 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper, m_last_active_item = current_active_id; last_move_input_window_width = ImGui::GetWindowWidth(); + ImGui::PopStyleVar(); imgui_wrapper->end(); - ImGui::PopStyleVar(2); + ImGui::PopStyleVar(); ImGuiWrapper::pop_toolbar_style(); } @@ -1131,10 +1132,11 @@ void GizmoObjectManipulation::do_render_rotate_window(ImGuiWrapper *imgui_wrappe m_last_active_item = current_active_id; last_rotate_input_window_width = ImGui::GetWindowWidth(); + ImGui::PopStyleVar(); imgui_wrapper->end(); // BBS - ImGui::PopStyleVar(2); + ImGui::PopStyleVar(); ImGuiWrapper::pop_toolbar_style(); } diff --git a/src/slic3r/GUI/MultiTaskModel.cpp b/src/slic3r/GUI/MultiTaskModel.cpp deleted file mode 100644 index b23468b93e..0000000000 --- a/src/slic3r/GUI/MultiTaskModel.cpp +++ /dev/null @@ -1,6 +0,0 @@ - -namespace Slic3r { -namespace GUI { - -} // namespace GUI -} // namespace Slic3r diff --git a/src/slic3r/GUI/MultiTaskModel.hpp b/src/slic3r/GUI/MultiTaskModel.hpp deleted file mode 100644 index 6b233e9302..0000000000 --- a/src/slic3r/GUI/MultiTaskModel.hpp +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef slic3r_MultiTaskModel_hpp_ -#define slic3r_MultiTaskModel_hpp_ - -namespace Slic3r { -namespace GUI { - - -} // namespace GUI -} // namespace Slic3r - -#endif diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 3bf53ecb73..d5d9fb5b8a 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -1880,13 +1880,6 @@ void PartPlate::render_exclude_area(bool force_default_color) { // draw exclude area glsafe(::glDepthMask(GL_FALSE)); - if (m_selected) { - glsafe(::glColor4fv(select_color.data())); - } - else { - glsafe(::glColor4fv(unselect_color.data())); - } - m_exclude_triangles.set_color(m_selected ? select_color : unselect_color); m_exclude_triangles.render(); glsafe(::glDepthMask(GL_TRUE)); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 8a7277dd94..cf7cce05ac 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -15043,24 +15043,8 @@ bool Plater::priv::can_fix_through_cgal() const std::vector obj_idxs, vol_idxs; sidebar->obj_list()->get_selection_indexes(obj_idxs, vol_idxs); -#if FIX_THROUGH_CGAL_ALWAYS - // Fixing always. + // A closed mesh may still self-intersect, so fixing is always allowed. return ! obj_idxs.empty() || ! vol_idxs.empty(); -#else // FIX_THROUGH_CGAL_ALWAYS - // Fixing only if the model is not manifold. - if (vol_idxs.empty()) { - for (auto obj_idx : obj_idxs) - if (model.objects[obj_idx]->get_repaired_errors_count() > 0) - return true; - return false; - } - - int obj_idx = obj_idxs.front(); - for (auto vol_idx : vol_idxs) - if (model.objects[obj_idx]->get_repaired_errors_count(vol_idx) > 0) - return true; - return false; -#endif // FIX_THROUGH_CGAL_ALWAYS } bool Plater::priv::can_simplify() const @@ -22100,13 +22084,16 @@ void Plater::clear_before_change_mesh(int obj_idx) mv->mmu_segmentation_facets.reset(); mv->fuzzy_skin_facets.reset(); } - if (paint_removed) { - // snapshot_time is captured by copy so the lambda knows where to undo/redo to. - get_notification_manager()->push_notification( - NotificationType::CustomSupportsAndSeamRemovedAfterRepair, - NotificationManager::NotificationLevel::PrintInfoNotificationLevel, - _u8L("Custom supports and color painting were removed before repairing.")); - } + if (paint_removed) + notify_painting_removed(); +} + +void Plater::notify_painting_removed() +{ + get_notification_manager()->push_notification( + NotificationType::CustomSupportsAndSeamRemovedAfterRepair, + NotificationManager::NotificationLevel::PrintInfoNotificationLevel, + _u8L("Custom supports and color painting were removed before repairing.")); } void Plater::changed_mesh(int obj_idx) diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 0770e7e8d9..b3a2954b33 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -599,6 +599,7 @@ public: void reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages = false); void clear_before_change_mesh(int obj_idx); + void notify_painting_removed(); void changed_mesh(int obj_idx); void changed_object(ModelObject &object); diff --git a/src/slic3r/GUI/SysInfoDialog.cpp b/src/slic3r/GUI/SysInfoDialog.cpp deleted file mode 100644 index 585767d318..0000000000 --- a/src/slic3r/GUI/SysInfoDialog.cpp +++ /dev/null @@ -1,232 +0,0 @@ -#include "SysInfoDialog.hpp" -#include "I18N.hpp" -#include "3DScene.hpp" -#include "GUI.hpp" -#include "../Utils/UndoRedo.hpp" -#include "Plater.hpp" - -#include - -#include - -#include -#include -#include "GUI_App.hpp" -#include "MainFrame.hpp" -#include "wxExtensions.hpp" -#include "libslic3r/BlacklistedLibraryCheck.hpp" -#include "libslic3r/Color.hpp" -#include "format.hpp" - -#ifdef _WIN32 - // The standard Windows includes. - #define WIN32_LEAN_AND_MEAN - #ifndef NOMINMAX - #define NOMINMAX - #endif - #include - #include -#endif /* _WIN32 */ - -namespace Slic3r { -namespace GUI { - -std::string get_main_info(bool format_as_html) -{ - std::stringstream out; - - std::string b_start = format_as_html ? "" : ""; - std::string b_end = format_as_html ? "" : ""; - std::string line_end = format_as_html ? "
" : "\n"; - - if (!format_as_html) - out << b_start << (wxGetApp().is_editor() ? SLIC3R_APP_FULL_NAME : GCODEVIEWER_APP_NAME) << b_end << line_end; - out << b_start << "Version: " << b_end << SLIC3R_VERSION << line_end; - out << b_start << "Build: " << b_end << (wxGetApp().is_editor() ? SLIC3R_BUILD_ID : GCODEVIEWER_BUILD_ID) << line_end; - out << line_end; - out << b_start << "Operating System: " << b_end << wxPlatformInfo::Get().GetOperatingSystemFamilyName() << line_end; - out << b_start << "System Architecture: " << b_end << wxPlatformInfo::Get().GetArchName() << line_end; - out << b_start << -#if defined _WIN32 - "Windows Version: " -#else - // Hopefully some kind of unix / linux. - "System Version: " -#endif - << b_end << wxPlatformInfo::Get().GetOperatingSystemDescription() << line_end; - out << b_start << "Total RAM size [MB]: " << b_end << Slic3r::format_memsize_MB(Slic3r::total_physical_memory()); - - return out.str(); -} - -std::string get_mem_info(bool format_as_html) -{ - std::stringstream out; - - std::string b_start = format_as_html ? "" : ""; - std::string b_end = format_as_html ? "" : ""; - std::string line_end = format_as_html ? "
" : "\n"; - - std::string mem_info_str = log_memory_info(true); - std::istringstream mem_info(mem_info_str); - std::string value; - while (std::getline(mem_info, value, ':')) { - out << b_start << (value+": ") << b_end; - std::getline(mem_info, value, ';'); - out << value << line_end; - } - - const Slic3r::UndoRedo::Stack &stack = wxGetApp().plater()->undo_redo_stack_main(); - out << b_start << "RAM size reserved for the Undo / Redo stack: " << b_end << Slic3r::format_memsize_MB(stack.get_memory_limit()) << line_end; - out << b_start << "RAM size occupied by the Undo / Redo stack: " << b_end << Slic3r::format_memsize_MB(stack.memsize()) << line_end << line_end; - - return out.str(); -} - -SysInfoDialog::SysInfoDialog() - : DPIDialog(static_cast(wxGetApp().mainframe), wxID_ANY, (wxGetApp().is_editor() ? wxString(SLIC3R_APP_FULL_NAME) : wxString(GCODEVIEWER_APP_NAME)) + " - " + _L("System Information"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) -{ - wxColour bgr_clr = wxGetApp().get_window_default_clr();//wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); - SetBackgroundColour(bgr_clr); - SetFont(wxGetApp().normal_font()); - - wxBoxSizer* hsizer = new wxBoxSizer(wxHORIZONTAL); - hsizer->SetMinSize(wxSize(50 * wxGetApp().em_unit(), -1)); - - auto main_sizer = new wxBoxSizer(wxVERTICAL); - main_sizer->Add(hsizer, 1, wxEXPAND | wxALL, 10); - - // logo - m_logo_bmp = ScalableBitmap(this, wxGetApp().logo_name(), 192); - m_logo = new wxStaticBitmap(this, wxID_ANY, m_logo_bmp.bmp()); - hsizer->Add(m_logo, 0, wxALIGN_CENTER_VERTICAL); - - wxBoxSizer* vsizer = new wxBoxSizer(wxVERTICAL); - hsizer->Add(vsizer, 1, wxEXPAND|wxLEFT, 20); - - // title - { - wxStaticText* title = new wxStaticText(this, wxID_ANY, wxGetApp().is_editor() ? SLIC3R_APP_FULL_NAME : GCODEVIEWER_APP_NAME, wxDefaultPosition, wxDefaultSize); - wxFont title_font = wxGetApp().bold_font(); - title_font.SetFamily(wxFONTFAMILY_ROMAN); - title_font.SetPointSize(22); - title->SetFont(title_font); - vsizer->Add(title, 0, wxEXPAND | wxTOP, wxGetApp().em_unit()/*50*/); - } - - // main_info_text - wxFont font = get_default_font(this); - const auto text_clr = wxGetApp().get_label_clr_default();//wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); - auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue())); - auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue())); - - const int fs = font.GetPointSize() - 1; - int size[] = { static_cast(fs*1.5), static_cast(fs*1.4), static_cast(fs*1.3), fs, fs, fs, fs }; - - m_html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_NEVER); - { - m_html->SetFonts(font.GetFaceName(), font.GetFaceName(), size); - m_html->SetBorders(2); - const auto text = wxString::Format( - "" - "" - "" - "%s" - "" - "" - "", bgr_clr_str, text_clr_str, text_clr_str, - get_main_info(true)); - m_html->SetPage(text); - vsizer->Add(m_html, 1, wxEXPAND | wxBOTTOM, wxGetApp().em_unit()); - } - - // opengl_info - m_opengl_info_html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_AUTO); - { - m_opengl_info_html->SetMinSize(wxSize(-1, 16 * wxGetApp().em_unit())); - m_opengl_info_html->SetFonts(font.GetFaceName(), font.GetFaceName(), size); - m_opengl_info_html->SetBorders(10); - wxString blacklisted_libraries_message; -#ifdef WIN32 - std::wstring blacklisted_libraries = BlacklistedLibraryCheck::get_instance().get_blacklisted_string().c_str(); - if (! blacklisted_libraries.empty()) - blacklisted_libraries_message = wxString("
") + _L("Blacklisted libraries loaded into OrcaSlicer process:") + "
" + blacklisted_libraries; -#endif // WIN32 - const auto text = GUI::format_wxstr( - "" - "" - "" - "%s
%s
%s
%s" - "
" - "" - "", bgr_clr_str, text_clr_str, text_clr_str, - blacklisted_libraries_message, - get_mem_info(true), wxGetApp().get_gl_info(false), - "" + _L("SIMD is supported:") + " " + Eigen::SimdInstructionSetsInUse()); - - m_opengl_info_html->SetPage(text); - main_sizer->Add(m_opengl_info_html, 1, wxEXPAND | wxBOTTOM, 15); - } - - wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxOK); - m_btn_copy_to_clipboard = new wxButton(this, wxID_ANY, _L("Copy to Clipboard"), wxDefaultPosition, wxDefaultSize); - - buttons->Insert(0, m_btn_copy_to_clipboard, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 5); - m_btn_copy_to_clipboard->Bind(wxEVT_BUTTON, &SysInfoDialog::onCopyToClipboard, this); - - this->SetEscapeId(wxID_OK); - this->Bind(wxEVT_BUTTON, &SysInfoDialog::onCloseDialog, this, wxID_OK); - main_sizer->Add(buttons, 0, wxEXPAND | wxRIGHT | wxBOTTOM, 3); - - wxGetApp().UpdateDlgDarkUI(this, true); - -// this->Bind(wxEVT_LEFT_DOWN, &SysInfoDialog::onCloseDialog, this); -// logo->Bind(wxEVT_LEFT_DOWN, &SysInfoDialog::onCloseDialog, this); - - SetSizer(main_sizer); - main_sizer->SetSizeHints(this); -} - -void SysInfoDialog::on_dpi_changed(const wxRect &suggested_rect) -{ - m_logo_bmp.msw_rescale(); - m_logo->SetBitmap(m_logo_bmp.bmp()); - - wxFont font = get_default_font(this); - const int fs = font.GetPointSize() - 1; - int font_size[] = { static_cast(fs*1.5), static_cast(fs*1.4), static_cast(fs*1.3), fs, fs, fs, fs }; - - m_html->SetFonts(font.GetFaceName(), font.GetFaceName(), font_size); - m_html->Refresh(); - - const int& em = em_unit(); - - msw_buttons_rescale(this, em, { wxID_OK, m_btn_copy_to_clipboard->GetId() }); - - m_opengl_info_html->SetMinSize(wxSize(-1, 16 * em)); - m_opengl_info_html->SetFonts(font.GetFaceName(), font.GetFaceName(), font_size); - m_opengl_info_html->Refresh(); - - const wxSize& size = wxSize(65 * em, 55 * em); - - SetMinSize(size); - Fit(); - - Refresh(); -} - -void SysInfoDialog::onCopyToClipboard(wxEvent &) -{ - wxTheClipboard->Open(); - const auto text = get_main_info(false) + "\n" + wxGetApp().get_gl_info(true); - wxTheClipboard->SetData(new wxTextDataObject(text)); - wxTheClipboard->Close(); -} - -void SysInfoDialog::onCloseDialog(wxEvent &) -{ - this->EndModal(wxID_CLOSE); -} - -} // namespace GUI -} // namespace Slic3r diff --git a/src/slic3r/GUI/SysInfoDialog.hpp b/src/slic3r/GUI/SysInfoDialog.hpp deleted file mode 100644 index 25a59f0fa7..0000000000 --- a/src/slic3r/GUI/SysInfoDialog.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef slic3r_GUI_SysInfoDialog_hpp_ -#define slic3r_GUI_SysInfoDialog_hpp_ - -#include -#include -#include - -#include "GUI_Utils.hpp" -#include "wxExtensions.hpp" - -namespace Slic3r { -namespace GUI { - -class SysInfoDialog : public DPIDialog -{ - ScalableBitmap m_logo_bmp; - wxStaticBitmap* m_logo; - wxHtmlWindow* m_opengl_info_html; - wxHtmlWindow* m_html; - - wxButton* m_btn_copy_to_clipboard; - -public: - SysInfoDialog(); - -protected: - void on_dpi_changed(const wxRect &suggested_rect) override; - -private: - void onCopyToClipboard(wxEvent &); - void onCloseDialog(wxEvent &); -}; -} // namespace GUI -} // namespace Slic3r - -#endif diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 456bfb82fd..772d3efb5d 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3098,6 +3098,7 @@ void TabPrint::build() optgroup = page->new_optgroup(L("Raft"), L"param_raft"); optgroup->append_single_option_line("raft_layers", "support_settings_raft"); optgroup->append_single_option_line("raft_contact_distance", "support_settings_raft"); + optgroup->append_single_option_line("raft_expansion", "support_settings_raft"); optgroup = page->new_optgroup(L("Filament for Supports"), L"param_support_filament"); optgroup->append_single_option_line("support_filament", "support_settings_filament#base"); diff --git a/src/slic3r/GUI/TextureImportDialog.cpp b/src/slic3r/GUI/TextureImportDialog.cpp index bc50cabc8c..a785847670 100644 --- a/src/slic3r/GUI/TextureImportDialog.cpp +++ b/src/slic3r/GUI/TextureImportDialog.cpp @@ -14,7 +14,7 @@ #include "Widgets/StateColor.hpp" #include "Widgets/StaticLine.hpp" #include "libslic3r/ColorDecomposeRecipe.hpp" -#include "libslic3r/MeshBoolean.hpp" +#include "libslic3r/MeshRepair.hpp" #include "libslic3r/TriangleSelector.hpp" #include @@ -2436,25 +2436,25 @@ void TextureImportDialog::start_computation(bool auto_color, bool initial) settings.target_colors_num = auto_color ? 0 : (size_t)m_param_color_count; settings.smooth_weight = m_param_smooth / 10.0; settings.mesh_repair_decision = m_mesh_repair_decision; - // BBS repairs the mesh through the Windows 3D SDK, which is only available on Windows - // builds that ship the SDK. Orca's CGAL-based repair (MeshBoolean::cgal::repair) works - // on all three platforms, so use that instead. settings.mesh_repair_callback = [](const indexed_triangle_set& mesh, indexed_triangle_set& repaired_mesh, std::function progress_callback, std::function cancel_callback, std::string* error_message) -> bool { - if (cancel_callback && cancel_callback()) - return false; if (progress_callback) progress_callback(_u8L("Repairing mesh").c_str(), 0); + struct Canceled {}; TriangleMesh tm(mesh); - if (!MeshBoolean::cgal::repair(tm, nullptr, error_message)) - return false; - - if (cancel_callback && cancel_callback()) + try { + if (!repair_mesh(tm, nullptr, error_message, [&cancel_callback](int) { + if (cancel_callback && cancel_callback()) + throw Canceled(); + })) + return false; + } catch (const Canceled &) { return false; + } repaired_mesh = tm.its; if (progress_callback) progress_callback(_u8L("Repairing mesh").c_str(), 100); @@ -2683,12 +2683,12 @@ void TextureImportDialog::on_mesh_repair_decision_required(wxCommandEvent&) m_progress_dlg = nullptr; } -#ifdef HAS_WIN10SDK Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this, - _L("The mesh has non-manifold geometry or open boundaries. You can import it as-is or repair it with Windows 3D repair service before importing."), - _L("Mesh repair"), wxYES_NO | wxICON_WARNING | wxYES_DEFAULT); + _L("The mesh has non-manifold geometry or open boundaries. You can import it as-is or repair it before importing."), + _L("Mesh repair"), wxYES_NO | wxCANCEL | wxICON_WARNING | wxYES_DEFAULT); dlg.SetButtonLabel(wxID_YES, _L("Import without repair")); dlg.SetButtonLabel(wxID_NO, _L("Repair and import"), true); + dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel")); // "Repair and import" is the recommended action here, so the accent moves off the default YES // button onto NO. MsgDialog::add_button already styled both as ButtonType::Choice, so restyling // with the same type swaps only the palette and leaves the geometry alone. @@ -2704,17 +2704,7 @@ void TextureImportDialog::on_mesh_repair_decision_required(wxCommandEvent&) dlg.Fit(); dlg.CenterOnParent(); int ret = dlg.ShowModal(); - m_mesh_repair_decision = (ret == wxID_NO) - ? Slic3r::TexturePaintingSettings::MeshRepairDecision::RepairAndImport - : Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair; -#else - Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this, - _L("Please note that the mesh has non-manifold geometry or open boundaries."), - _L("Mesh issue"), wxOK | wxCANCEL | wxICON_WARNING | wxOK_DEFAULT); - dlg.SetButtonLabel(wxID_OK, _L("Continue"), true); - dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel")); - int ret = dlg.ShowModal(); - if (ret != wxID_OK) { + if (ret == wxID_CANCEL) { m_cancel_flag = true; if (initial) { m_initial_computation_cancelled = true; @@ -2727,8 +2717,9 @@ void TextureImportDialog::on_mesh_repair_decision_required(wxCommandEvent&) } return; } - m_mesh_repair_decision = Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair; -#endif + m_mesh_repair_decision = (ret == wxID_NO) + ? Slic3r::TexturePaintingSettings::MeshRepairDecision::RepairAndImport + : Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair; start_computation(auto_color, initial); } diff --git a/src/slic3r/GUI/UVEditorCanvas.cpp b/src/slic3r/GUI/UVEditorCanvas.cpp index 2432026689..a4a9901822 100644 --- a/src/slic3r/GUI/UVEditorCanvas.cpp +++ b/src/slic3r/GUI/UVEditorCanvas.cpp @@ -939,7 +939,8 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt) m_active_edge = (hit.first >= 0 && is_edge_selected(hit)) ? hit : std::pair{ -1, -1 }; m_gesture = (m_active_edge.first >= 0) ? Gesture::MoveEdge : Gesture::Pan; } else { - const int hit = island_at(uv); + const std::vector selection_before = m_selection; + const int hit = island_at(uv); if (hit >= 0) { if (evt.ShiftDown()) { // Shift adds to the selection (and makes the clicked one the new primary). @@ -969,6 +970,10 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt) m_selected_island = -1; } m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan; + // The owner shows the selection on the model and would not hear of this click otherwise in time: a + // click that clears the selection starts no gesture, and one that sets it reports only on release. + if (m_selection != selection_before) + run_command(Command::SelectionChanged); } grab_mouse(); Refresh(); diff --git a/src/slic3r/GUI/UVEditorCanvas.hpp b/src/slic3r/GUI/UVEditorCanvas.hpp index 2fc1055cb8..a01d058983 100644 --- a/src/slic3r/GUI/UVEditorCanvas.hpp +++ b/src/slic3r/GUI/UVEditorCanvas.hpp @@ -108,10 +108,12 @@ public: // the command callback - the canvas has the selection and the view, the gizmo has the layer. // `value` carries the new setting for the Set* commands (an angle, a flag as 0/1, or an index into // SelectMode / Background) and is 0 otherwise. + // SelectionChanged is sent by the canvas itself when a click changes the island selection. It carries + // nothing: the owner reads selected_islands() back, and only needs telling that it should look. enum class Command { FrameAll, ToggleSnap, AverageScale, CutSelectedIsland, ProjectFromView, JoinSelected, UnjoinSelected, Unwrap, SetSeamAngle, SetConnectIslands, SetSelectMode, SetMarkSeams, SetSeamPath, ClearSeams, ClearUVEdits, - SetBackground, PickTexture, PaneClosed + SetBackground, PickTexture, PaneClosed, SelectionChanged }; void run_command(Command cmd, float value = 0.f); using CommandFn = std::function; diff --git a/src/slic3r/GUI/WebUpdatePlugin.cpp b/src/slic3r/GUI/WebUpdatePlugin.cpp deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/slic3r/GUI/WebUpdatePlugin.hpp b/src/slic3r/GUI/WebUpdatePlugin.hpp deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/slic3r/GUI/Widgets/ComboBox.cpp b/src/slic3r/GUI/Widgets/ComboBox.cpp index b02e1a8416..55e261d04d 100644 --- a/src/slic3r/GUI/Widgets/ComboBox.cpp +++ b/src/slic3r/GUI/Widgets/ComboBox.cpp @@ -82,6 +82,11 @@ ComboBox::ComboBox(wxWindow *parent, e.SetId(GetId()); GetEventHandler()->ProcessEvent(e); }); + drop.Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) { + e.SetEventObject(this); + e.SetId(GetId()); + GetEventHandler()->ProcessEvent(e); + }); drop.Bind(EVT_DISMISS, [this](auto &) { drop_down = false; wxCommandEvent e(wxEVT_COMBOBOX_CLOSEUP); @@ -251,7 +256,7 @@ int ComboBox::Append(const wxString &text, int style) { auto valid_bit_map = bitmap.IsOk() ? bitmap : wxNullBitmap; - Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, clientData, group_key, group_label}; + Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, wxNullBitmap, clientData, group_key, group_label}; item.style = style; items.push_back(item); SetClientDataType(wxClientData_Void); @@ -297,6 +302,12 @@ void ComboBox::ForceDropdownOpen() } } +void ComboBox::CloseDropdown() +{ + if (drop.IsShown()) + drop.DismissAndNotify(); +} + unsigned int ComboBox::GetCount() const { return items.size(); } void ComboBox::set_replace_text(wxString text, wxString image_name) @@ -350,6 +361,13 @@ void ComboBox::SetItemBitmap(unsigned int n, wxBitmap const &bitmap) drop.Invalidate(); } +void ComboBox::SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap) +{ + if (n >= items.size()) return; + items[n].action_icon = bitmap.IsOk() ? bitmap : wxNullBitmap; + drop.Invalidate(); +} + int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items, unsigned int pos, void ** clientData, @@ -357,7 +375,7 @@ int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items, { if (pos > this->items.size()) return -1; for (int i = 0; i < items.GetCount(); ++i) { - Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL }; + Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL }; this->items.insert(this->items.begin() + pos, item); ++pos; } @@ -419,16 +437,18 @@ void ComboBox::keyDown(wxKeyEvent& event) case WXK_UP: case WXK_DOWN: case WXK_LEFT: - case WXK_RIGHT: - if ((event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) && GetSelection() > 0) { - SetSelection(GetSelection() - 1); - } else if ((event.GetKeyCode() == WXK_DOWN || event.GetKeyCode() == WXK_RIGHT) && GetSelection() + 1 < items.size()) { - SetSelection(GetSelection() + 1); - } else { + case WXK_RIGHT: { + // Step over disabled rows: the mouse cannot select them either. + const int step = (event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) ? -1 : 1; + int n = GetSelection() + step; + while (n >= 0 && n < int(items.size()) && (items[n].style & DD_ITEM_STYLE_DISABLED)) + n += step; + if (n < 0 || n >= int(items.size())) break; - } + SetSelection(n); sendComboBoxEvent(); break; + } case WXK_TAB: HandleAsNavigationKey(event); break; diff --git a/src/slic3r/GUI/Widgets/ComboBox.hpp b/src/slic3r/GUI/Widgets/ComboBox.hpp index 1dae23b85d..1fbc2e257d 100644 --- a/src/slic3r/GUI/Widgets/ComboBox.hpp +++ b/src/slic3r/GUI/Widgets/ComboBox.hpp @@ -96,10 +96,12 @@ public: wxBitmap GetItemBitmap(unsigned int n); void SetItemBitmap(unsigned int n, wxBitmap const &bitmap); + void SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap); bool is_drop_down(){return drop_down;} void DeleteOneItem(unsigned int pos) { DoDeleteOneItem(pos); } void ForceDropdownOpen(); + void CloseDropdown(); protected: virtual int DoInsertItems(const wxArrayStringsAdapter &items, diff --git a/src/slic3r/GUI/Widgets/DropDown.cpp b/src/slic3r/GUI/Widgets/DropDown.cpp index c5a8e280b1..966a090526 100644 --- a/src/slic3r/GUI/Widgets/DropDown.cpp +++ b/src/slic3r/GUI/Widgets/DropDown.cpp @@ -30,6 +30,7 @@ #include "slic3r/GUI/wxExtensions.hpp" wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent); +wxDEFINE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent); BEGIN_EVENT_TABLE(DropDown, PopupWindow) @@ -242,6 +243,19 @@ static wxSize GetBmpSize(wxBitmap & bmp) #endif } +// The x-range [first, second) of the column at the right of the rows that holds the items' action +// icons, empty when no item has one. render() draws the icons in it and mouseReleased() hit-tests +// clicks against it, so the drawn icon and its clickable area always match. +std::pair DropDown::actionColumn() const +{ + if (actionIconWidth <= 0) + return {0, 0}; + int right = rowSize.x; + if (rowSize.y * count > GetSize().y) // position bar + right -= 6; + return {right - actionIconWidth - FromDIP(16), right}; +} + static void _DrawSplitItem(const wxWindow* w, wxDC& dc, wxString split_text, wxPoint start_pt, int item_width, int item_height) { // save dc @@ -371,6 +385,10 @@ void DropDown::render(wxDC &dc) rcContent.width -= szBmp.x + 5; } + const auto action_column = actionColumn(); + if (action_column.second > action_column.first) + rcContent.width = action_column.first - rcContent.x; + std::set groups; // draw texts & icons int index = 0; @@ -434,13 +452,24 @@ void DropDown::render(wxDC &dc) auto text = group.IsEmpty() ? (item.group_key.IsEmpty() ? item.text : item.group_label) : item.text; + if (item.action_icon.IsOk()) { + auto action_size = GetBmpSize(item.action_icon); + wxPoint action_pt(action_column.second - action_size.x - FromDIP(8), + rcContent.y + (rcContent.height - action_size.y) / 2); + dc.DrawBitmap(item.action_icon, action_pt); + } + if (!text_off && !text.IsEmpty()) { wxSize tSize = dc.GetMultiLineTextExtent(text); if (pt.x + tSize.x > rcContent.GetRight()) { - if (is_hover && item.tip.IsEmpty()) - SetToolTip(text); - text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, - rcContent.GetRight() - pt.x); + int max_text_width = rcContent.GetRight() - pt.x; + if (max_text_width <= 0) { + text.clear(); + } else { + if (is_hover && item.tip.IsEmpty()) + SetToolTip(text); + text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, max_text_width); + } } pt.y += (rcContent.height - textSize.y) / 2; dc.SetFont(GetFont()); @@ -529,6 +558,7 @@ void DropDown::messureSize() if (!need_sync) return; textSize = wxSize(); iconSize = wxSize(); + actionIconWidth = 0; count = 0; wxClientDC dc(GetParent() ? GetParent() : this); dc.SetFont(GetFont()); @@ -565,8 +595,13 @@ void DropDown::messureSize() size1.x += size2.x + (text_off ? 0 : 5); } } + if (item.action_icon.IsOk()) + actionIconWidth = std::max(actionIconWidth, GetBmpSize(item.action_icon).x); if (size1.x > textSize.x) textSize = size1; } + // Every row leaves room for the action column, as render() and actionColumn() lay it out. + if (actionIconWidth > 0) + textSize.x += actionIconWidth + FromDIP(16); if (!align_icon) iconSize.x = 0; wxSize szContent = textSize; if (szContent.x < FromDIP(120) && !use_content_width) @@ -636,6 +671,11 @@ void DropDown::messureSize() e.SetId(GetId()); GetEventHandler()->ProcessEvent(e); }); + subDropDown->Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) { + e.SetEventObject(this); + e.SetId(GetId()); + GetEventHandler()->ProcessEvent(e); + }); } need_sync = false; } @@ -714,6 +754,15 @@ void DropDown::mouseReleased(wxMouseEvent& event) if (hover_item < 0) return; if (hover_item >= 0 && (subDropDown == nullptr || subDropDown->group.empty())) { // not moved + int index = hoverIndex(); + if (index >= 0 && items[index].action_icon.IsOk()) { + const auto action_column = actionColumn(); + if (event.GetX() >= action_column.first && event.GetX() < action_column.second) { + sendDropDownActionEvent(index); + paintNow(); + return; + } + } sendDropDownEvent(); if (mainDropDown) mainDropDown->hover_item = -1; // To Dismiss mainDropDown @@ -830,6 +879,17 @@ void DropDown::sendDropDownEvent() GetEventHandler()->ProcessEvent(event); } +void DropDown::sendDropDownActionEvent(int index) +{ + if (index < 0 || index >= int(items.size()) || (items[index].style & DD_ITEM_STYLE_DISABLED)) + return; + wxCommandEvent event(EVT_DROPDOWN_ITEM_ACTION, GetId()); + event.SetEventObject(this); + event.SetInt(index); + event.SetString(items[index].text); + GetEventHandler()->ProcessEvent(event); +} + void DropDown::Dismiss() { if (subDropDown && subDropDown->IsShown()) diff --git a/src/slic3r/GUI/Widgets/DropDown.hpp b/src/slic3r/GUI/Widgets/DropDown.hpp index 8b6ae5d635..a0821e6fff 100644 --- a/src/slic3r/GUI/Widgets/DropDown.hpp +++ b/src/slic3r/GUI/Widgets/DropDown.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "slic3r/GUI/Widgets/StateColor.hpp" #include @@ -27,6 +28,7 @@ class wxWindow; #define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent); +wxDECLARE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent); class DropDown : public PopupWindow { @@ -37,6 +39,7 @@ public: wxString text_static_tips;// display static tips for TextInput.eg.PrinterInfoBox wxBitmap icon; wxBitmap icon_textctrl;// display icon for TextInput.eg.PrinterInfoBox + wxBitmap action_icon; void * data{nullptr}; wxString group_key{}; wxString group_label{}; @@ -65,6 +68,7 @@ private: wxSize textSize; wxSize iconSize; + int actionIconWidth = 0; wxSize rowSize; StateHandler state_handler; @@ -135,6 +139,7 @@ private: int hoverIndex(); int selectedItem(); + std::pair actionColumn() const; friend class ComboBox; void messureSize(); @@ -148,6 +153,7 @@ private: void mouseWheelMoved(wxMouseEvent &event); void sendDropDownEvent(); + void sendDropDownActionEvent(int index); DECLARE_EVENT_TABLE() diff --git a/src/slic3r/GUI/wxinit.h b/src/slic3r/GUI/wxinit.h deleted file mode 100644 index 4c5ab9fb36..0000000000 --- a/src/slic3r/GUI/wxinit.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef slic3r_wxinit_hpp_ -#define slic3r_wxinit_hpp_ - -#include -#include -#include - -// Perl redefines a _ macro, so we undef this one -#undef _ - -// We do want to use translation however, so define it as __ so we can do a find/replace -// later when we no longer need to undef _ -#define __(s) wxGetTranslation((s)) - -#endif diff --git a/src/slic3r/Utils/FixModelByCgal.cpp b/src/slic3r/Utils/FixModelByCgal.cpp index 09dad529fe..081d9dcf0d 100644 --- a/src/slic3r/Utils/FixModelByCgal.cpp +++ b/src/slic3r/Utils/FixModelByCgal.cpp @@ -3,240 +3,115 @@ #include #include #include -#include #include -#include "libslic3r/TriangleMesh.hpp" -#include "libslic3r/BoundingBox.hpp" -#include "libslic3r/Point.hpp" -#include "libslic3r/libslic3r.h" -#include #include -#include "libslic3r/Exception.hpp" -#include +#include #include -#include "slic3r/GUI/Widgets/ProgressDialog.hpp" -#include #include #include -#include #include + #include -#include "libslic3r/MeshBoolean.hpp" -#include "libslic3r/Model.hpp" #include "libslic3r/Format/bbs_3mf.hpp" -#include "libslic3r/format.hpp" +#include "libslic3r/MeshRepair.hpp" +#include "libslic3r/Model.hpp" #include "libslic3r/Thread.hpp" -#include "../GUI/I18N.hpp" -#include "libslic3r/TriangleSelector.hpp" - -// Orca: This file provides utilities for repairing 3D model meshes using the CGAL library, handling mesh splitting, merging, and boolean operations. +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/Widgets/ProgressDialog.hpp" namespace Slic3r { namespace { -// Orca: Helper functions for analyzing mesh properties and transformations. - -bool is_not_3dimensional_part(const TriangleMesh &mesh) +class RepairCanceledException : public std::exception { - // Orca: Determines if a mesh is degenerate or represents a non-3dimensional part by checking volume and bounding box dimensions. - if (mesh.its.indices.empty()) - return true; - - indexed_triangle_set tmp = mesh.its; - its_remove_degenerate_faces(tmp, true); - if (tmp.indices.empty()) - return true; - - const BoundingBoxf3 bbox = mesh.bounding_box(); - const Vec3d size = bbox.size(); - const double min_dim = std::min(size.x(), std::min(size.y(), size.z())); - const double max_dim = std::max(size.x(), std::max(size.y(), size.z())); - if (min_dim <= EPSILON) - return true; - - const double volume = std::abs(its_volume(mesh.its)); - const double bbox_volume = size.x() * size.y() * size.z(); - if (volume <= EPSILON) - return true; - - const double min_relative_thickness = 1e-6; - const double min_volume_ratio = 1e-6; - if (min_dim / max_dim <= min_relative_thickness) - return true; - if (bbox_volume > 0.0 && volume / bbox_volume <= min_volume_ratio) - return true; - - return false; -} +public: + const char *what() const noexcept override { return "Model repair has been canceled"; } +}; } // namespace -// Orca: Exception class for handling user-initiated cancellation of model repair operations. -class RepairCanceledException : public std::exception { -public: - const char* what() const noexcept override { return "Model repair has been canceled"; } -}; - -// Orca: Main function to repair model objects using CGAL, with progress dialog and cancellation support. -// Returns false if fixing was canceled. fix_result contains error message if failed. -bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dialog, const wxString &msg_header, std::string &fix_result, bool keep_painting) +bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dialog, const wxString &msg_header, std::string &fix_result, bool keep_painting, + bool *modified) { - // Hold SaveObjectGaurd to prevent backup manager from racing concurrent mesh mutations (use-after-free). + // Keeps the backup manager from reading a mesh while the worker replaces it. SaveObjectGaurd backup_gaurd(model_object); - // Orca: Synchronization primitives for progress updates between worker thread and GUI. - std::mutex mtx; - std::condition_variable condition; - struct Progress { - std::string message; - int percent = 0; - bool updated = false; - } progress; + std::vector volumes; + if (volume_idx < 0) + volumes = model_object.volumes; + else if (std::size_t(volume_idx) < model_object.volumes.size()) + volumes.push_back(model_object.volumes[volume_idx]); + std::mutex mtx; + std::condition_variable condition; + struct Progress + { + std::string message; + int percent = 0; + bool updated = false; + } progress; std::atomic canceled = false; std::atomic finished = false; + bool any_modified = false; + std::string errors; - bool success = false; - size_t ivolume = 0; - - // Orca: Lambda for updating progress from worker thread. - auto on_progress = [&mtx, &condition, &ivolume, &model_object, &progress](const std::string &msg, unsigned prcnt) { - std::unique_lock lock(mtx); + auto on_progress = [&mtx, &condition, &progress](const std::string &msg, int percent) { + std::lock_guard lock(mtx); progress.message = msg; - const size_t total = std::max(1, model_object.volumes.size()); - progress.percent = int(std::floor((float(prcnt) + float(ivolume) * 100.f) / float(total))); + progress.percent = percent; progress.updated = true; condition.notify_all(); }; - // Orca: Worker thread that performs the actual model repair operations. - auto worker_thread = std::thread([&model_object, volume_idx, &ivolume, on_progress, &success, &canceled, &finished, &fix_result, keep_painting]() { + auto worker_thread = std::thread([&]() { + set_current_thread_name("cgal_fix_model"); try { - set_current_thread_name("cgal_fix_model"); + for (std::size_t i = 0; i < volumes.size(); ++i) { + ModelVolume *volume = volumes[i]; + const std::string name = volumes.size() > 1 ? volume->name : std::string(); + const auto report = [&](int percent) { + if (canceled) + throw RepairCanceledException(); + on_progress(name, int((100 * i + std::size_t(percent)) / volumes.size())); + }; + report(0); - size_t start_volume = volume_idx == -1 ? 0 : size_t(volume_idx); - size_t end_volume = volume_idx == -1 ? std::numeric_limits::max() : size_t(volume_idx); - - for (ivolume = start_volume; ivolume < model_object.volumes.size(); ++ivolume) { - if (volume_idx != -1 && ivolume > end_volume) - break; - if (canceled) - throw RepairCanceledException(); - - on_progress(_u8L("Repairing model object"), 10); - - ModelVolume *volume = model_object.volumes[ivolume]; - - // Orca: Split splittable volumes into parts for individual processing. - size_t parts_count = 1; - if (volume->is_splittable()) { - parts_count = volume->split(1, keep_painting); - if (parts_count > 1) { - const std::string msg = Slic3r::format(L("Split into %1% parts"), parts_count); - on_progress(msg, 10); - } - } - - size_t part_end = std::min(ivolume + parts_count - 1, model_object.volumes.size() - 1); - if (volume_idx != -1) - end_volume = part_end; - - size_t removed_parts = 0; - for (size_t idx = part_end + 1; idx > ivolume; --idx) { - const size_t part_idx = idx - 1; - const ModelVolume *part_volume = model_object.volumes[part_idx]; - if (!is_not_3dimensional_part(part_volume->mesh())) - continue; - - model_object.delete_volume(part_idx); - ++removed_parts; - if (part_end > 0) - --part_end; - else - part_end = 0; - if (volume_idx != -1) - end_volume = part_end; - } - - if (removed_parts >= parts_count) { - ivolume = part_end; - on_progress(_u8L("Repair finished"), 100); - continue; - } - - for (size_t part_idx = ivolume; part_idx <= part_end && part_idx < model_object.volumes.size(); ++part_idx) { - ModelVolume *part_volume = model_object.volumes[part_idx]; - TriangleMesh mesh = part_volume->mesh(); - if (its_num_open_edges(mesh.its) != 0) { - - // Save painting for later remap - const std::optional saved_painting = keep_painting ? - part_volume->save_painting() : - std::optional{}; - - std::string error; - if (!MeshBoolean::cgal::repair(mesh, nullptr, &error)) - throw Slic3r::RuntimeError(error.empty() ? _u8L("Repair failed") : error); - - part_volume->set_mesh(std::move(mesh)); - part_volume->calculate_convex_hull(); - part_volume->invalidate_convex_hull_2d(); - part_volume->set_new_unique_id(); - - // Remap paint back - part_volume->restore_painting(saved_painting); - } - } - - ivolume = part_end; - - on_progress(_u8L("Repair finished"), 100); + MeshRepairStats stats; + std::string error; + if (!volume->repair(keep_painting, &stats, &error, report)) + errors += (errors.empty() ? "" : "\n") + (volumes.size() > 1 ? volume->name + ": " : std::string()) + error; + any_modified = any_modified || stats.modified; } - - model_object.invalidate_bounding_box(); - - if (ivolume > 0) - --ivolume; - on_progress(_u8L("Repair finished"), 100); - success = true; - finished = true; - } catch (RepairCanceledException &) { + } catch (const RepairCanceledException &) { canceled = true; - finished = true; - on_progress(_u8L("Repair canceled"), 100); - } catch (std::exception &ex) { - success = false; - finished = true; - fix_result = ex.what(); - on_progress(ex.what(), 100); + } catch (const std::exception &ex) { + errors += (errors.empty() ? "" : "\n") + std::string(ex.what()); } + // Volumes repaired before a cancel or an error keep their new mesh. + model_object.invalidate_bounding_box(); + finished = true; + on_progress(_u8L("Repair finished"), 100); }); - // Orca: Main GUI loop to update progress dialog and handle cancellation. while (!finished) { std::unique_lock lock(mtx); - condition.wait_for(lock, std::chrono::milliseconds(250), [&progress]{ return progress.updated; }); - - // Decrease progress percent slightly to avoid auto-closing. - if (!progress_dialog.Update(progress.percent - 1, msg_header + _(progress.message))) + condition.wait_for(lock, std::chrono::milliseconds(250), [&progress] { return progress.updated; }); + // One percent short of the maximum, so that the dialog does not close itself. + if (!progress_dialog.Update(std::max(0, progress.percent - 1), msg_header + wxString::FromUTF8(progress.message))) canceled = true; else progress_dialog.Fit(); - progress.updated = false; } - if (canceled) { - // Nothing to show. - } else if (success) { - fix_result.clear(); - } - if (worker_thread.joinable()) worker_thread.join(); + fix_result = canceled ? std::string() : errors; + if (modified) + *modified = any_modified; return !canceled; } diff --git a/src/slic3r/Utils/FixModelByCgal.hpp b/src/slic3r/Utils/FixModelByCgal.hpp index b3f96169f6..e59e767223 100644 --- a/src/slic3r/Utils/FixModelByCgal.hpp +++ b/src/slic3r/Utils/FixModelByCgal.hpp @@ -12,8 +12,9 @@ class Model; class ModelObject; class Print; -// Return false if fixing was canceled. fix_result is empty on success. -extern bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dlg, const wxString &msg_header, std::string &fix_result, bool keep_painting); +// Return false if fixing was canceled. fix_result is empty on success; modified tells whether any mesh changed. +extern bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dlg, const wxString &msg_header, std::string &fix_result, bool keep_painting, + bool *modified = nullptr); } // namespace Slic3r diff --git a/src/slic3r/Utils/Profile.hpp b/src/slic3r/Utils/Profile.hpp deleted file mode 100644 index 5fb1e31167..0000000000 --- a/src/slic3r/Utils/Profile.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef slic3r_GUI_Profile_hpp_ -#define slic3r_GUI_Profile_hpp_ - -// Profiling support using the Shiny intrusive profiler -//#define SLIC3R_PROFILE_GUI -#if defined(SLIC3R_PROFILE) && defined(SLIC3R_PROFILE_GUI) - #include - #define SLIC3R_GUI_PROFILE_FUNC() PROFILE_FUNC() - #define SLIC3R_GUI_PROFILE_BLOCK(name) PROFILE_BLOCK(name) - #define SLIC3R_GUI_PROFILE_UPDATE() PROFILE_UPDATE() - #define SLIC3R_GUI_PROFILE_OUTPUT(x) PROFILE_OUTPUT(x) -#else - #define SLIC3R_GUI_PROFILE_FUNC() - #define SLIC3R_GUI_PROFILE_BLOCK(name) - #define SLIC3R_GUI_PROFILE_UPDATE() - #define SLIC3R_GUI_PROFILE_OUTPUT(x) -#endif - -#endif // slic3r_GUI_Profile_hpp_ diff --git a/tests/cli/CMakeLists.txt b/tests/cli/CMakeLists.txt index dddebee566..3f2982c453 100644 --- a/tests/cli/CMakeLists.txt +++ b/tests/cli/CMakeLists.txt @@ -31,6 +31,22 @@ set_tests_properties(cli_project_missing_keys PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 900) +add_test(NAME cli_arrange_rotations + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_arrange_rotations.sh $ ${ORCA_CLI_TEST_PYTHON} + ${CMAKE_SOURCE_DIR}/resources/profiles/BBL) +set_tests_properties(cli_arrange_rotations PROPERTIES + LABELS "CLI;RequiresApp" + SKIP_RETURN_CODE 77 + TIMEOUT 900) + +add_test(NAME cli_no_suitable_objects + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_no_suitable_objects.sh $ ${ORCA_CLI_TEST_PYTHON} + ${CMAKE_SOURCE_DIR}/resources/profiles/BBL) +set_tests_properties(cli_no_suitable_objects PROPERTIES + LABELS "CLI;RequiresApp" + SKIP_RETURN_CODE 77 + TIMEOUT 900) + add_test(NAME cli_malformed_input COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_malformed_input.sh $ ${ORCA_CLI_TEST_PYTHON}) set_tests_properties(cli_malformed_input PROPERTIES diff --git a/tests/cli/test_cli_arrange_rotations.sh b/tests/cli/test_cli_arrange_rotations.sh new file mode 100755 index 0000000000..e0f9d33adc --- /dev/null +++ b/tests/cli/test_cli_arrange_rotations.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# End-to-end check of the CLI's arrange rotation policy. +# +# Arrange is translate-only unless --allow-rotations is passed, so a bar that fits the bed only +# diagonally is not placed and the plate ends up with nothing fully inside the print volume. The +# run must then say that rotations were off, on stderr as well as in result.json, because +# result.json is written on Linux only. Passing the flag must place the same bar and slice it, and +# an object that fits axis-aligned must be unaffected by the default. +# +# usage: test_cli_arrange_rotations.sh +set -u + +BIN="${1:-}" +PY="${2:-python3}" +PROFILES="${3:-}" +# 77 is the test's SKIP_RETURN_CODE. +[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; } +[ -d "$PROFILES" ] || { echo "FAIL: profiles directory not found: $PROFILES"; exit 1; } + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-arrange-rotations.XXXXXX")" +trap 'rm -rf "$WORK"' EXIT + +# box : an ASCII STL box, so the test needs no fixture. +box() { + "$PY" - "$@" <<'EOF' +import sys + +out, sx, sy, sz = sys.argv[1], float(sys.argv[2]), float(sys.argv[3]), float(sys.argv[4]) +v = [(x, y, z) for z in (0, sz) for y in (0, sy) for x in (0, sx)] +with open(out, "w") as f: + f.write("solid box\n") + for a, b, c, d in ((0, 2, 3, 1), (4, 5, 7, 6), (0, 1, 5, 4), (2, 6, 7, 3), (0, 4, 6, 2), (1, 3, 7, 5)): + for tri in ((v[a], v[b], v[c]), (v[a], v[c], v[d])): + f.write("facet normal 0 0 0\nouter loop\n") + for p in tri: + f.write("vertex %g %g %g\n" % p) + f.write("endloop\nendfacet\n") + f.write("endsolid box\n") +EOF +} + +# Longer than the 256 mm bed edge, shorter than its 362 mm diagonal: it fits only turned. +box "$WORK/bar.stl" 300 10 5 +# Fits axis-aligned, so the default must not change anything for it. +box "$WORK/cube.stl" 10 10 10 + +# slice [option...] -> $WORK//{rc,stdout,stderr,result.json} +slice() { + local out="$WORK/$1" input="$2"; shift 2 + mkdir -p "$out" + timeout 300 "$BIN" --datadir "$out/datadir" \ + --load-settings "$PROFILES/machine/Bambu Lab X1 Carbon 0.4 nozzle.json;$PROFILES/process/0.20mm Standard @BBL X1C.json" \ + --load-defaultfila "$@" --slice 0 --outputdir "$out" "$input" \ + > "$out/stdout" 2> "$out/stderr" + echo "$?" > "$out/rc" +} + +# exit_code : the CLI's negative code as the shell saw it, e.g. -50 arrives as 206. +exit_code() { "$PY" -c "import sys; rc = int(open(sys.argv[1]).read()); print(rc - 256 if rc > 127 else rc)" "$WORK/$1/rc"; } + +fail() { echo "FAIL: $*"; exit 1; } + +# 1. Default: no flag, so no rotations, so the bar does not fit and the reason names rotation. +slice default "$WORK/bar.stl" +[ "$(exit_code default)" = "-50" ] || fail "default: expected -50, got $(exit_code default)" +grep -q "Arrange ran without rotations" "$WORK/default/stderr" \ + || fail "default: stderr does not mention rotations: $(cat "$WORK/default/stderr")" +"$PY" - "$WORK/default/result.json" <<'EOF' || exit 1 +import json, sys + +result = json.load(open(sys.argv[1])) +if result["return_code"] != -50: + sys.exit("FAIL: result.json return_code %s, expected -50" % result["return_code"]) +if "Arrange ran without rotations" not in result["error_string"]: + sys.exit("FAIL: result.json error_string does not mention rotations: %s" % result["error_string"]) +EOF + +# 2. The flag is coBool, so it takes no value: it must place the same bar and slice it. +slice rotations "$WORK/bar.stl" --allow-rotations +[ "$(exit_code rotations)" = "0" ] || fail "--allow-rotations: expected 0, got $(exit_code rotations); $(tail -n 5 "$WORK/rotations/stdout")" +[ -s "$WORK/rotations/plate_1.gcode" ] || fail "--allow-rotations: no g-code written" + +# 3. An object that fits axis-aligned is unaffected by the default, and says nothing about rotation. +slice cube "$WORK/cube.stl" +[ "$(exit_code cube)" = "0" ] || fail "cube: expected 0, got $(exit_code cube); $(tail -n 5 "$WORK/cube/stdout")" +[ -s "$WORK/cube/plate_1.gcode" ] || fail "cube: no g-code written" +grep -q "Arrange ran without rotations" "$WORK/cube/stderr" && fail "cube: unexpected rotation message on a successful run" + +echo "PASS: arrange rotation default, its failure message, and the opt-in flag" diff --git a/tests/cli/test_cli_no_suitable_objects.sh b/tests/cli/test_cli_no_suitable_objects.sh new file mode 100755 index 0000000000..dad9dd4120 --- /dev/null +++ b/tests/cli/test_cli_no_suitable_objects.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# The reason recorded for CLI_NO_SUITABLE_OBJECTS (-50) has to fit the input the CLI was given. +# Any input format is arranged and sliced, so an object larger than the bed empties the plate just +# as a 3mf with an empty plate does, and the recorded reason must not send such a caller off to +# inspect a 3mf or an upload that is not part of their run. +# +# usage: test_cli_no_suitable_objects.sh +set -u + +BIN="${1:-}" +PY="${2:-python3}" +PROFILES="${3:-}" +# 77 is the test's SKIP_RETURN_CODE. +[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; } +[ -d "$PROFILES" ] || { echo "FAIL: profiles directory not found: $PROFILES"; exit 1; } + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-no-suitable-objects.XXXXXX")" +trap 'rm -rf "$WORK"' EXIT + +# box : an ASCII STL box, so the test needs no fixture. +box() { + "$PY" - "$@" <<'EOF' +import sys + +out, sx, sy, sz = sys.argv[1], float(sys.argv[2]), float(sys.argv[3]), float(sys.argv[4]) +v = [(x, y, z) for z in (0, sz) for y in (0, sy) for x in (0, sx)] +with open(out, "w") as f: + f.write("solid box\n") + for a, b, c, d in ((0, 2, 3, 1), (4, 5, 7, 6), (0, 1, 5, 4), (2, 6, 7, 3), (0, 4, 6, 2), (1, 3, 7, 5)): + for tri in ((v[a], v[b], v[c]), (v[a], v[c], v[d])): + f.write("facet normal 0 0 0\nouter loop\n") + for p in tri: + f.write("vertex %g %g %g\n" % p) + f.write("endloop\nendfacet\n") + f.write("endsolid box\n") +EOF +} + +# Wider than the 256 mm bed and wider than its 362 mm diagonal, so no placement can fit it. +box "$WORK/slab.stl" 400 400 5 +# Fits, so the run must succeed: proves the harness itself is sound. +box "$WORK/cube.stl" 10 10 10 + +# slice [option...] -> $WORK//{rc,stdout,stderr,result.json} +slice() { + local out="$WORK/$1" input="$2"; shift 2 + mkdir -p "$out" + timeout 300 "$BIN" --datadir "$out/datadir" \ + --load-settings "$PROFILES/machine/Bambu Lab X1 Carbon 0.4 nozzle.json;$PROFILES/process/0.20mm Standard @BBL X1C.json" \ + "$@" --slice 0 --outputdir "$out" "$input" \ + > "$out/stdout" 2> "$out/stderr" + echo "$?" > "$out/rc" +} + +# exit_code : the CLI's negative code as the shell saw it, e.g. -50 arrives as 206. +exit_code() { "$PY" -c "import sys; rc = int(open(sys.argv[1]).read()); print(rc - 256 if rc > 127 else rc)" "$WORK/$1/rc"; } + +fail() { echo "FAIL: $*"; exit 1; } + +# --allow-rotations keeps this on the generic reason: a translate-only arrange has its own, +# more specific one. The slab is wider than the 362 mm bed diagonal, so rotation cannot place it. +slice slab "$WORK/slab.stl" --allow-rotations +[ "$(exit_code slab)" = "-50" ] || fail "slab: expected -50, got $(exit_code slab); $(tail -n 5 "$WORK/slab/stdout")" +"$PY" - "$WORK/slab/result.json" <<'EOF' || exit 1 +import json, sys + +result = json.load(open(sys.argv[1])) +if result["return_code"] != -50: + sys.exit("FAIL: result.json return_code %s, expected -50" % result["return_code"]) +reason = result["error_string"] +if not reason.strip(): + sys.exit("FAIL: result.json carries no error_string") +# The run was handed an STL and asked for g-code: neither a 3mf nor an upload exists to check. +for word in ("3mf", "3MF", "upload"): + if word in reason: + sys.exit("FAIL: error_string mentions %r for an STL run: %s" % (word, reason)) +EOF + +slice cube "$WORK/cube.stl" +[ "$(exit_code cube)" = "0" ] || fail "cube: expected 0, got $(exit_code cube); $(tail -n 5 "$WORK/cube/stdout")" +[ -s "$WORK/cube/plate_1.gcode" ] || fail "cube: no g-code written" + +echo "PASS: the no-suitable-objects reason fits a non-3mf run" diff --git a/tests/data/surface_model_geometric_set.step b/tests/data/surface_model_geometric_set.step new file mode 100644 index 0000000000..ec11fde62d --- /dev/null +++ b/tests/data/surface_model_geometric_set.step @@ -0,0 +1,35 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION((''),'2;1'); +FILE_NAME('surface_model_geometric_set','2026-10-12T00:00:00',(''),(''),'','',''); +FILE_SCHEMA(('CONFIG_CONTROL_DESIGN')); +ENDSEC; +DATA; +#1=APPLICATION_CONTEXT('configuration controlled 3d designs of mechanical parts and assemblies'); +#2=APPLICATION_PROTOCOL_DEFINITION('international standard','config_control_design',1994,#1); +#3=MECHANICAL_CONTEXT('',#1,'mechanical'); +#4=PRODUCT('surfaces','surfaces','',(#3)); +#5=PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE('','',#4,.NOT_KNOWN.); +#6=DESIGN_CONTEXT('',#1,'design'); +#7=PRODUCT_DEFINITION('','',#5,#6); +#8=PRODUCT_DEFINITION_SHAPE('','',#7); +#9=SHAPE_DEFINITION_REPRESENTATION(#8,#20); +#10=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.)); +#11=(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.)); +#12=(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT()); +#13=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-6),#10,'distance_accuracy_value',''); +#14=(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#13))GLOBAL_UNIT_ASSIGNED_CONTEXT((#10,#11,#12))REPRESENTATION_CONTEXT('','')); +#15=CARTESIAN_POINT('',(0.,0.,0.)); +#16=DIRECTION('',(0.,0.,1.)); +#17=DIRECTION('',(1.,0.,0.)); +#18=AXIS2_PLACEMENT_3D('',#15,#16,#17); +#19=GEOMETRIC_SET('',(#30,#40)); +#20=GEOMETRICALLY_BOUNDED_SURFACE_SHAPE_REPRESENTATION('',(#18,#19),#14); +#30=RECTANGULAR_TRIMMED_SURFACE('',#31,0.,10.,0.,10.,.T.,.T.); +#31=PLANE('',#18); +#40=RECTANGULAR_TRIMMED_SURFACE('',#41,0.,10.,0.,10.,.T.,.T.); +#41=PLANE('',#42); +#42=AXIS2_PLACEMENT_3D('',#43,#16,#17); +#43=CARTESIAN_POINT('',(0.,0.,10.)); +ENDSEC; +END-ISO-10303-21; diff --git a/tests/fff_print/test_gcodeprocessor.cpp b/tests/fff_print/test_gcodeprocessor.cpp index 0be8cc1c12..48cf3e8ae9 100644 --- a/tests/fff_print/test_gcodeprocessor.cpp +++ b/tests/fff_print/test_gcodeprocessor.cpp @@ -178,6 +178,61 @@ bool is_block_move(const GCodeProcessorResult::MoveVertex &move) } // namespace +TEST_CASE("Loaded manual tool change comments switch subsequent moves when enabled", "[GCodeProcessor][Regression]") +{ + const bool manual_filament_change = GENERATE(false, true); + CAPTURE(manual_filament_change); + const DynamicPrintConfig config = Test::multifilament_config(2, { + { "gcode_flavor", "marlin" }, + { "filament_type", "PLA;PETG" }, + { "manual_filament_change", manual_filament_change }, + }); + const bool was_bbl_printer = GCodeProcessor::s_IsBBLPrinter; + const ScopeGuard restore_bbl_printer([was_bbl_printer] { GCodeProcessor::s_IsBBLPrinter = was_bbl_printer; }); + GCodeProcessor::s_IsBBLPrinter = false; + + ScopedTemporaryFile temp(".gcode"); + { + std::ofstream out(temp.string()); + REQUIRE(out.is_open()); + out << "; generated by OrcaSlicer\n" + << "G90\nM83\nT0\n" + << "G1 X10 Y10 Z0.2 F3000\n" + << "G1 X20 E1\n" + << "; MANUAL_TOOL_CHANGE T1\n" + << "G1 X30\nG1 X40 E1\nG1 X50\nG1 X60 E1\n"; + // Let process_file load the embedded DynamicPrintConfig, as the standalone viewer does. + out << "; CONFIG_BLOCK_START\n"; + for (const std::string &key : config.keys()) + out << "; " << key << " = " << config.opt_serialize(key) << '\n'; + out << "; CONFIG_BLOCK_END\n"; + REQUIRE(out.good()); + } + + GCodeProcessor processor; + processor.process_file(temp.string()); + const auto &result = processor.get_result(); + REQUIRE(result.filaments_count == 2); + + constexpr unsigned int tool_change_line = 7; + size_t before = 0; + size_t after = 0; + for (const auto &move : result.moves) { + if (!is_block_move(move)) + continue; + CAPTURE(move.gcode_id, move.type); + if (move.gcode_id < tool_change_line) { + ++before; + CHECK(move.extruder_id == 0); + } else if (move.gcode_id > tool_change_line) { + ++after; + CHECK(move.extruder_id == (manual_filament_change ? 1 : 0)); + } + } + REQUIRE(before > 0); + REQUIRE(after > 0); +} + TEST_CASE("Actual speed moves are inserted on their block's segment just before its move", "[GCodeProcessor]") { // 60 squares take several planner passes, which remap the blocks kept between passes. diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index 170dfabae3..d491527db6 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -1748,3 +1748,29 @@ TEST_CASE("Percent accelerations resolve against the option they are a percentag REQUIRE(accelerations_by_role[role_name] == std::set{ value }); } } + +TEST_CASE("Initial filament start G-code tolerates a list shorter than the filament count", "[GCodeWriter]") +{ + // Two filaments but only one filament_start_gcode entry, and the only object prints on + // filament 2, so the BBL export starts on a filament past the end of the list. + DynamicPrintConfig config = multifilament_config(2); + config.set_key_value("filament_start_gcode", new ConfigOptionStrings({"; filament start"})); + + Model model; + auto *obj = model.add_object(); + obj->add_volume(cube(20)); + obj->add_instance(); + obj->config.set_key_value("extruder", new ConfigOptionInt(2)); + + Print print; + print.is_BBL_printer() = true; + arrange_objects_on_test_bed(model, config); + for (auto *mo : model.objects) { + mo->ensure_on_bed(); + print.auto_assign_extruders(mo); + } + print.apply(model, config); + print.validate(); + + REQUIRE_NOTHROW(Slic3r::Test::gcode(print)); +} diff --git a/tests/fff_print/test_helpers.cpp b/tests/fff_print/test_helpers.cpp index c7302d4c4b..daa8132d5d 100644 --- a/tests/fff_print/test_helpers.cpp +++ b/tests/fff_print/test_helpers.cpp @@ -223,7 +223,10 @@ DynamicPrintConfig multifilament_config(unsigned int filaments, std::initializer // These are read by filament id during export but absent from filament_option_keys(), // so set_num_filaments leaves them size 1; size them too, else out-of-range access. const auto &defaults = FullPrintConfig::defaults(); - for (const char *key : { "filament_type", "filament_vendor", "filament_start_gcode" }) + for (const char *key : { "filament_type", "filament_vendor", "filament_start_gcode", "filament_change_length", "filament_change_length_nc", + "filament_ramming_travel_time", "filament_ramming_travel_time_nc", + "filament_ramming_volumetric_speed", "filament_ramming_volumetric_speed_nc", + "nozzle_temperature", "nozzle_temperature_initial_layer" }) static_cast(config.option(key, true))->resize(filaments, defaults.option(key)); // flush_volumes_matrix must be sized filaments*filaments or export rejects it. diff --git a/tests/fff_print/test_multifilament.cpp b/tests/fff_print/test_multifilament.cpp index e1f70571b8..54a98d2268 100644 --- a/tests/fff_print/test_multifilament.cpp +++ b/tests/fff_print/test_multifilament.cpp @@ -315,6 +315,79 @@ TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]") CHECK(tools_for_role(gcode, "infill") == std::set{ 0 }); // infill not overridden: stays on F1 } +// FanMover's guard against splitting a G1 inside custom gcode keys off the ";_FAN_MOVER_SKIP_START/END" +// markers GCode::set_extruder() must emit around change_filament_gcode's output. Direct +// toolchanges and Type2's wipe-tower toolchange (append_tcr2() calls set_extruder()) both go +// through it; Type1's wipe-tower toolchange (WipeTowerIntegration::append_tcr(), append_tcr2's only +// sibling caller of change_filament_gcode) needs the same bracketing of its own. +TEST_CASE("Toolchange gcode is bracketed against FanMover splitting its moves", "[MultiFilament]") +{ + const std::string custom_gcode = "; fan full\nM106 P1 S255\nM400 S3\nG1 X77 F5000\nG1 X91 F3000\n"; + + // "direct": no prime tower, GCode::set_extruder() runs the toolchange. By-object sequencing + // with a per-object filament override is what makes one happen at all without a tower. + // "type1": a Type1 prime tower routes the toolchange through WipeTowerIntegration::append_tcr() + // instead (append_tcr2, Type2's equivalent, itself calls set_extruder() - so Type2 needs no + // separate case here). + const std::string path = GENERATE(as{}, "direct", "type1"); + CAPTURE(path); + + std::string gcode_str; + if (path == "direct") { + const std::vector> per_object_overrides = { + {}, { { "outer_wall_filament_id", "2" }, { "inner_wall_filament_id", "2" } } + }; + DynamicPrintConfig config = multifilament_config(2, { + { "skirt_loops", 0 }, + { "brim_type", "no_brim" }, + { "print_sequence", "by object" }, + { "change_filament_gcode", custom_gcode }, + { "fan_speedup_time", 0.5 }, + { "fan_kickstart", 0.1 }, + }); + gcode_str = slice_with_object_overrides({ cube(20), cube(20) }, config, per_object_overrides); + } else { + const std::vector> per_object_overrides = { + { { "extruder", "1" } }, { { "extruder", "2" } } + }; + DynamicPrintConfig config = multifilament_config(2, { + { "change_filament_gcode", custom_gcode }, + { "fan_speedup_time", 0.5 }, + { "fan_kickstart", 0.1 }, + { "enable_prime_tower", true }, + { "wipe_tower_type", "type1" }, + { "prime_tower_width", 35 }, + { "wipe_tower_x", 120 }, + { "wipe_tower_y", 120 }, + { "printable_area", "0x0,200x0,200x200,0x200" }, + { "skirt_loops", 0 }, + { "brim_type", "no_brim" }, + }); + gcode_str = slice_with_object_overrides({ cube(20), cube(20) }, config, per_object_overrides); + } + + // The resolved-settings config footer echoes "change_filament_gcode = ..." with its value + // escaped, so drop it before counting or its own copy of "G1 X77 F5000" is miscounted. + const size_t footer_start = gcode_str.find("; CONFIG_BLOCK_START"); + REQUIRE(footer_start != std::string::npos); + gcode_str.erase(footer_start); + + auto count_occurrences = [](const std::string &haystack, const std::string &needle) { + size_t count = 0, pos = 0; + while ((pos = haystack.find(needle, pos)) != std::string::npos) { + ++count; + pos += needle.size(); + } + return count; + }; + const size_t total_waypoints = count_occurrences(gcode_str, "G1 X77 F5000"); + const size_t bracketed = count_occurrences(gcode_str, "M400 S3\nG1 X77 F5000"); + REQUIRE(total_waypoints > 0); + CHECK(bracketed == total_waypoints); + // FanMover drops its own markers; only the start-of-print toolchange, which it never processes, keeps them. + CHECK(count_occurrences(gcode_str, ";_FAN_MOVER_SKIP_START") <= 1); +} + // With wait_for_temp_on_wipe_tower the blocking M109 moves from right after the Tn command to // a stop point parked beside the wipe tower (heat-up drool falls next to the tower, not onto // its top): tagged with _WAIT_FOR_TEMP_ON_WIPE_TOWER, after the toolchange and before the diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 58f8d12e39..4b76de4b55 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -43,6 +43,7 @@ #include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/Exception.hpp" #include "libslic3r/LifecycleEvents.hpp" +#include "libslic3r/Utils.hpp" #include "test_helpers.hpp" #include "test_utils.hpp" @@ -156,6 +157,81 @@ SCENARIO("Changing the number of solid shell layers does not make all surfaces i } } +// Two copies of one object share a single slice. When a copy that has sliced itself becomes identical to an +// earlier object, it keeps its own layers while that object slices and only then takes the shared layers over. +TEST_CASE("An object that starts sharing another object's slices keeps its own layers until it takes them over", "[Print][Regression]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "raft_layers", 1 }, // each object gets support layers of its own + { "sparse_infill_density", "20%" }, // object_a's override below differs from this + }); + // Activates the slicing-pipeline hook installed for the second slice below. + config.set_key_value("slicing_pipeline_plugin", new ConfigOptionStrings({"probe"})); + + Print print; + Model model; + init_print({cube(20)}, print, model, config); + ModelObject *object_a = model.objects.front(); + // add_object() gives the copy extruder 1, and objects only share slices when their configs match. + object_a->config.set_key_value("extruder", new ConfigOptionInt(1)); + ModelObject *object_b = model.add_object(*object_a); // shares object_a's mesh + for (ModelInstance *instance : object_b->instances) + instance->set_offset(instance->get_offset() + Vec3d(40., 0., 0.)); + // A per-object override makes the objects differ, so each slices itself. + object_a->config.set_key_value("sparse_infill_density", new ConfigOptionPercent(40)); + + print.apply(model, config); + print.process(); + REQUIRE(print.objects().size() == 2); + // The non-const accessors hand out the layer vectors themselves, which the checks below compare. + PrintObject *print_b = print.get_object(1); + REQUIRE(print_b->model_object()->id() == object_b->id()); // Print slices its own copy of the model + REQUIRE(print_b->get_shared_object() == nullptr); + const LayerPtrs own_layers = print_b->layers(); + const SupportLayerPtrs own_support_layers = print_b->support_layers(); + REQUIRE(!own_layers.empty()); + REQUIRE(!own_support_layers.empty()); + + // Without the override the objects are identical again: print_a redoes the steps the setting invalidated, + // and print_b shares it. + object_a->config.erase("sparse_infill_density"); + print.apply(model, config); + REQUIRE(print.objects().size() == 2); + PrintObject *print_a = print.get_object(0); + + // Erasing the override invalidates print_a's posPrepareInfill and the steps after it. Its hook is the first one + // print_a fires, and it runs after print_b starts sharing but before print_b takes the shared layers over. + bool hook_fired = false; + bool shared_while_a_runs = false; + bool kept_layers = false; + bool kept_support_layers = false; + { + Print::set_slicing_pipeline_hook_fn([&](Print &, const PrintObject *object, SlicingPipelineStepPlugin) { + if (object != print_a || hook_fired) + return; + hook_fired = true; + shared_while_a_runs = print_b->get_shared_object() == print_a; + kept_layers = print_b->layers() == own_layers; + kept_support_layers = print_b->support_layers() == own_support_layers; + }); + const ScopeGuard reset_hook([] { Print::set_slicing_pipeline_hook_fn(nullptr); }); + print.process(); + } + + REQUIRE(hook_fired); + CHECK(print.objects()[1] == print_b); + CHECK(shared_while_a_runs); + CHECK(kept_layers); + CHECK(kept_support_layers); + CHECK(print_b->get_shared_object() == print_a); + REQUIRE(!print_a->layers().empty()); + CHECK(print_b->layers() == print_a->layers()); + CHECK(print_b->support_layers() == print_a->support_layers()); +} + // --------------------------------------------------------------------------- // Print::validate() warning collection // diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 9e1e7c44f8..1f72f83478 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -53,6 +53,7 @@ add_executable(${_TEST_NAME}_tests test_stl.cpp test_triangle_selector.cpp test_meshboolean.cpp + test_mesh_repair.cpp test_trianglemesh_slicer.cpp test_marchingsquares.cpp test_lay_on_face.cpp diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index f7ac744e39..a59800fafe 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -28,6 +28,7 @@ #include #include "libslic3r/TriangleMesh.hpp" #include "libslic3r/TriangleSelector.hpp" +#include "libslic3r/TextureDisplacement.hpp" #include #include #include @@ -38,6 +39,7 @@ #include #include #include +#include #include #include @@ -349,6 +351,24 @@ static bool replace_in_3mf_entry(const std::string& path, const std::string& suf return replaced; } +// Stores `model` as a one-plate project. +static void store_project(Model& model, const std::string& path, SaveStrategy strategy) +{ + ScopedTemporaryDir backup_dir("orca_paint_src"); + model.set_backup_path(backup_dir.string()); + + DynamicPrintConfig cfg; + PlateData plate; + plate.plate_index = 0; + StoreParams sp; + sp.path = path.c_str(); + sp.model = &model; + sp.config = &cfg; + sp.strategy = strategy; + sp.plate_data_list.push_back(&plate); + REQUIRE(store_bbs_3mf(sp)); +} + // Stores a one-plate project holding a cube whose first two facets are painted Extruder2 and // Extruder3, which the exporter writes as paint_color="8" and paint_color="0C". static void store_painted_cube(const std::string& path) @@ -363,19 +383,7 @@ static void store_painted_cube(const std::string& path) selector.set_facet(1, EnforcerBlockerType::Extruder3); REQUIRE(volume->mmu_segmentation_facets.set(selector)); } - ScopedTemporaryDir backup_dir("orca_paint_src"); - model.set_backup_path(backup_dir.string()); - - DynamicPrintConfig cfg; - PlateData plate; - plate.plate_index = 0; - StoreParams sp; - sp.path = path.c_str(); - sp.model = &model; - sp.config = &cfg; - sp.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence; - sp.plate_data_list.push_back(&plate); - REQUIRE(store_bbs_3mf(sp)); + store_project(model, path, SaveStrategy::Zip64 | SaveStrategy::Silence); } // Loads `path` through the BBS importer into `model`, releasing the plates it returns. The @@ -501,6 +509,163 @@ TEST_CASE("A project with malformed paint data loads without the damaged facet", REQUIRE(selector.num_facets(EnforcerBlockerType::Extruder3) == 1); } +// Paints `triangles` of `volume` on texture displacement layer slot `slot`. +static void paint_texture_slot(ModelVolume& volume, int slot, std::initializer_list triangles) +{ + TriangleSelector selector(volume.mesh()); + for (const int triangle : triangles) + selector.set_facet(triangle, EnforcerBlockerType::ENFORCER); + REQUIRE(volume.texture_displacement_facet(slot).set(selector)); +} + +// Every triangle's texture paint on every slot, in the form the exporter writes it, so two volumes +// compare equal only when the same triangles are painted on the same slots. +static std::vector texture_paint(const ModelVolume& volume) +{ + std::vector paint; + for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) + for (size_t i = 0; i < volume.mesh().its.indices.size(); ++i) + paint.push_back(volume.texture_displacement_facet(slot).get_triangle_as_string(int(i))); + return paint; +} + +// A cube with one texture displacement layer in slot 2 and texture paint on gapped triangles of two +// slots. The layer's image bytes need not decode: the archive stores them as they are. +static ModelVolume& add_texture_displaced_cube(Model& model) +{ + ModelObject* object = model.add_object(); + ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.)); + object->add_instance(); + + TextureDisplacementLayer layer; + layer.slot = 2; + layer.name = "Bark"; + layer.depth_mm = 1.25f; + layer.image_data = std::make_shared>(std::vector{ 0x89, 'P', 'N', 'G', 1, 2, 3 }); + volume->texture_displacement_layers.push_back(layer); + volume->texture_displacement_options.active_slot = 2; + + paint_texture_slot(*volume, 2, { 3, 7 }); + paint_texture_slot(*volume, 5, { 10 }); + return *volume; +} + +TEST_CASE("Texture displacement layers, textures and paint survive a project round trip", "[3mf]") +{ + // A single model file, and one file per object (read by a separate importer). + const SaveStrategy strategy = GENERATE(SaveStrategy::Zip64 | SaveStrategy::Silence, + SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::SplitModel); + INFO("save strategy " << int(strategy)); + + ScopedTemporaryFile temp(".3mf"); + Model source; + const ModelVolume& painted = add_texture_displaced_cube(source); + const std::vector paint = texture_paint(painted); + REQUIRE(std::count_if(paint.begin(), paint.end(), [](const std::string& s) { return !s.empty(); }) == 3); + store_project(source, temp.string(), strategy); + + ScopedTemporaryDir backup_dir("orca_texdisp_dst"); + Model model; + REQUIRE(load_project(temp.string(), model, backup_dir)); + REQUIRE(model.objects.size() == 1); + const ModelVolume& volume = *model.objects.front()->volumes.front(); + + REQUIRE(volume.texture_displacement_layers.size() == 1); + const TextureDisplacementLayer& layer = volume.texture_displacement_layers.front(); + CHECK(layer.slot == 2); + CHECK(layer.name == "Bark"); + CHECK_THAT(layer.depth_mm, Catch::Matchers::WithinAbs(1.25f, 1e-6f)); + REQUIRE(layer.image_data); + CHECK(*layer.image_data == *painted.texture_displacement_layers.front().image_data); + CHECK(volume.texture_displacement_options.active_slot == 2); + CHECK(texture_paint(volume) == paint); +} + +TEST_CASE("Volumes sharing a mesh keep their own texture displacement paint", "[3mf]") +{ + Model source; + ModelObject* first_object = source.add_object(); + ModelVolume* first = first_object->add_volume(make_cube(10., 10., 10.)); + first_object->add_instance(); + ModelVolume* second = source.add_object(*first_object)->volumes.front(); + REQUIRE(second->mesh_ptr() == first->mesh_ptr()); + paint_texture_slot(*first, 0, { 3 }); + paint_texture_slot(*second, 0, { 7 }); + + // What a project save from the app uses: one file per object, an identical mesh written once. + ScopedTemporaryFile temp(".3mf"); + store_project(source, temp.string(), + SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::ShareMesh); + + ScopedTemporaryDir backup_dir("orca_texdisp_dst"); + Model model; + REQUIRE(load_project(temp.string(), model, backup_dir)); + REQUIRE(model.objects.size() == 2); + CHECK(texture_paint(*model.objects[0]->volumes.front()) == texture_paint(*first)); + CHECK(texture_paint(*model.objects[1]->volumes.front()) == texture_paint(*second)); +} + +TEST_CASE("A texture displacement layer never loads its image from outside the project", "[3mf]") +{ + // A file the layer names, on the machine opening the project, that the project did not carry. + ScopedTemporaryFile texture(".png"); + { + boost::nowide::ofstream file(texture.string(), std::ios::binary); + file << "not part of the project"; + } + REQUIRE(boost::filesystem::file_size(texture.path()) > 0); + + Model source; + ModelObject* object = source.add_object(); + ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.)); + object->add_instance(); + TextureDisplacementLayer layer; + layer.slot = 0; + layer.name = "Elsewhere"; + layer.path = texture.string(); + layer.depth_mm = 0.75f; + volume->texture_displacement_layers.push_back(layer); + + ScopedTemporaryFile temp(".3mf"); + store_project(source, temp.string(), SaveStrategy::Zip64 | SaveStrategy::Silence); + + ScopedTemporaryDir backup_dir("orca_texdisp_dst"); + Model model; + REQUIRE(load_project(temp.string(), model, backup_dir)); + REQUIRE(model.objects.size() == 1); + const std::vector& layers = model.objects.front()->volumes.front()->texture_displacement_layers; + REQUIRE(layers.size() == 1); + CHECK(layers.front().name == "Elsewhere"); + CHECK_THAT(layers.front().depth_mm, Catch::Matchers::WithinAbs(0.75f, 1e-6f)); + CHECK_FALSE(layers.front().image_data); +} + +TEST_CASE("A project with a malformed texture displacement value still loads", "[3mf][Regression]") +{ + const auto [from, to] = GENERATE(table({ + { "\"islands\":[]", "\"islands\":[[null,0,0,1]]" }, // a number that is null + { "\"slot\":2", "\"slot\":8" }, // a slot past the last + })); + INFO(from << " -> " << to); + + ScopedTemporaryFile temp(".3mf"); + Model source; + const ModelVolume& painted = add_texture_displaced_cube(source); + store_project(source, temp.string(), SaveStrategy::Zip64 | SaveStrategy::Silence); + REQUIRE(replace_in_3mf_entry(temp.string(), ".json", from, to)); + + ScopedTemporaryDir backup_dir("orca_texdisp_dst"); + Model model; + bool loaded = false; + REQUIRE_NOTHROW(loaded = load_project(temp.string(), model, backup_dir)); + REQUIRE(loaded); + REQUIRE(model.objects.size() == 1); + const ModelVolume& volume = *model.objects.front()->volumes.front(); + // The layer stack is dropped as a whole; the paint, stored apart from it, still loads. + CHECK(volume.texture_displacement_layers.empty()); + CHECK(texture_paint(volume) == texture_paint(painted)); +} + // The recipe lives only in the BBS-native backend, because that is the only one that runs: // store_bbs_3mf is the sole exporter the app calls, and 3mf.cpp's load_3mf is reached only for // files fingerprinted as PrusaSlicer's, which never carry a recipe. This locks in both halves: @@ -2043,3 +2208,40 @@ TEST_CASE("3MF XML entries declaring more than an int can hold fail to load", "[ CHECK_FALSE(parser.check_3mf_from_prusa(path)); } } + +TEST_CASE("Loading a gcode.3mf from a stream keeps the plate's pick and pattern file paths", "[3mf][Regression]") +{ + Model model; + REQUIRE(load_stl((std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl").c_str(), &model)); + model.add_default_instances(); + ScopedTemporaryDir backup_dir("orca_pick"); + model.set_backup_path(backup_dir.string()); + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + + auto plate = std::make_unique(); + plate->plate_index = 0; + plate->pick_file = (backup_dir.path() / "pick.png").string(); + plate->pattern_bbox_file = (backup_dir.path() / "plate.json").string(); + + ScopedTemporaryFile temp(".3mf"); + const std::string test_file = temp.string(); + StoreParams store_params; + store_params.path = test_file.c_str(); + store_params.model = &model; + store_params.config = &config; + store_params.plate_data_list.push_back(plate.get()); + store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence; + REQUIRE(store_bbs_3mf(store_params)); + + boost::nowide::ifstream in(test_file, std::ios::binary); + Model dst_model; + DynamicPrintConfig dst_config; + PlateDataPtrs dst_plates; + Semver file_version; + dst_model.set_backup_path(backup_dir.string()); + REQUIRE(load_gcode_3mf_from_stream(in, &dst_config, &dst_model, &dst_plates, &file_version)); + REQUIRE(dst_plates.size() == 1); + CHECK(dst_plates.front()->pick_file == "Metadata/pick_1.png"); + CHECK(dst_plates.front()->pattern_bbox_file == "Metadata/plate_1.json"); + release_PlateData_list(dst_plates); +} diff --git a/tests/libslic3r/test_mesh_repair.cpp b/tests/libslic3r/test_mesh_repair.cpp new file mode 100644 index 0000000000..3390a65c77 --- /dev/null +++ b/tests/libslic3r/test_mesh_repair.cpp @@ -0,0 +1,255 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace Slic3r; +using Catch::Matchers::WithinRel; + +namespace { + +indexed_triangle_set cube(double size, const Vec3d &offset = Vec3d::Zero()) +{ + TriangleMesh mesh = make_cube(size, size, size); + mesh.translate(offset.cast()); + return mesh.its; +} + +indexed_triangle_set merged(const std::vector &parts) +{ + indexed_triangle_set out; + for (const indexed_triangle_set &part : parts) + its_merge(out, part); + return out; +} + +// Every triangle with its own three vertices, as some exporters write meshes. +indexed_triangle_set soup(const indexed_triangle_set &its) +{ + indexed_triangle_set out; + for (const stl_triangle_vertex_indices &f : its.indices) { + const int first = int(out.vertices.size()); + for (int k = 0; k < 3; ++k) + out.vertices.push_back(its.vertices[f[k]]); + out.indices.emplace_back(first, first + 1, first + 2); + } + return out; +} + +double regular_polygon_area(int sides, double radius) { return 0.5 * sides * radius * radius * std::sin(2. * PI / sides); } + +// Closed by vertex indices, outward oriented and free of self-intersections. +void check_solid(const TriangleMesh &mesh, double volume) +{ + CHECK(its_num_open_edges(mesh.its) == 0); + CHECK_THAT(its_volume(mesh.its), WithinRel(volume, 1e-4)); + CHECK_FALSE(MeshBoolean::cgal::does_self_intersect(mesh)); +} + +} // namespace + +TEST_CASE("A triangle soup repairs into the solid it describes", "[MeshRepair]") +{ + TriangleMesh mesh(soup(cube(20.))); + MeshRepairStats stats; + REQUIRE(repair_mesh(mesh, &stats)); + CHECK(stats.modified); + check_solid(mesh, 20. * 20. * 20.); + CHECK(mesh.its.vertices.size() == 8); +} + +TEST_CASE("Flipped triangles take the orientation of their neighbours", "[MeshRepair]") +{ + indexed_triangle_set its = cube(20.); + std::swap(its.indices[1][1], its.indices[1][2]); + std::swap(its.indices[5][1], its.indices[5][2]); + TriangleMesh mesh(std::move(its)); + REQUIRE(repair_mesh(mesh)); + check_solid(mesh, 20. * 20. * 20.); +} + +TEST_CASE("An inside-out mesh is turned outward", "[MeshRepair]") +{ + indexed_triangle_set its = cube(20.); + its_flip_triangles(its); + TriangleMesh mesh(std::move(its)); + REQUIRE(repair_mesh(mesh)); + check_solid(mesh, 20. * 20. * 20.); +} + +TEST_CASE("Holes are filled", "[MeshRepair]") +{ + indexed_triangle_set its = cube(20.); + its.indices.erase(its.indices.begin(), its.indices.begin() + 3); + TriangleMesh mesh(std::move(its)); + MeshRepairStats stats; + REQUIRE(repair_mesh(mesh, &stats)); + CHECK(stats.filled_holes > 0); + check_solid(mesh, 20. * 20. * 20.); +} + +TEST_CASE("Cracks between triangles that should share corners are closed", "[MeshRepair]") +{ + indexed_triangle_set its = soup(cube(20.)); + // Shift the corners of two triangles by far less than a micron, as float rounding in an exporter would. + for (std::size_t v = 0; v < 6; ++v) + its.vertices[v] += Vec3f(1e-5f, -1e-5f, 1e-5f); + TriangleMesh mesh(std::move(its)); + MeshRepairStats stats; + REQUIRE(repair_mesh(mesh, &stats)); + CHECK(stats.welded_vertices > 0); + check_solid(mesh, 20. * 20. * 20.); +} + +TEST_CASE("A T-junction is closed", "[MeshRepair]") +{ + // The top of the cube is split at the middle of its front edge, which the front side does not share. + indexed_triangle_set its; + its.vertices = {{0, 0, 0}, {20, 0, 0}, {20, 20, 0}, {0, 20, 0}, {0, 0, 20}, {20, 0, 20}, {20, 20, 20}, {0, 20, 20}, {10, 0, 20}}; + its.indices = {{0, 2, 1}, {0, 3, 2}, {4, 8, 7}, {8, 6, 7}, {8, 5, 6}, {0, 1, 5}, {0, 5, 4}, + {1, 2, 6}, {1, 6, 5}, {2, 3, 7}, {2, 7, 6}, {3, 0, 4}, {3, 4, 7}}; + TriangleMesh mesh(std::move(its)); + REQUIRE(repair_mesh(mesh)); + check_solid(mesh, 20. * 20. * 20.); +} + +TEST_CASE("Crossing shells stay separate parts and are made solid", "[MeshRepair]") +{ + indexed_triangle_set inside_out = cube(20., {10., 10., 10.}); + its_flip_triangles(inside_out); + TriangleMesh mesh(merged({cube(20.), inside_out})); + MeshRepairStats stats; + REQUIRE(repair_mesh(mesh, &stats)); + CHECK(stats.reversed_shells == 1); + CHECK(its_num_open_edges(mesh.its) == 0); + CHECK_THAT(its_volume(mesh.its), WithinRel(2 * 8000., 1e-4)); + CHECK(mesh.split().size() == 2); +} + +TEST_CASE("Solids touching face to face in a soup lose the wall between them", "[MeshRepair]") +{ + // The mirrored copy shares the wall at x = 10 with exactly the same triangles, reversed. + TriangleMesh right = make_cube(10., 10., 10.); + right.mirror(X); + right.translate(20.f, 0.f, 0.f); + TriangleMesh mesh(soup(merged({cube(10.), right.its}))); + REQUIRE(repair_mesh(mesh)); + check_solid(mesh, 2 * 1000.); + CHECK(mesh.split().size() == 1); +} + +TEST_CASE("A shell inside a solid follows the nonzero winding rule of slicing", "[MeshRepair]") +{ + SECTION("Oriented like the outer shell, it adds nothing and is removed") + { + TriangleMesh mesh(merged({cube(20.), cube(10., {5., 5., 5.})})); + REQUIRE(repair_mesh(mesh)); + check_solid(mesh, 8000.); + CHECK(mesh.its.indices.size() == 12); + } + SECTION("Oriented inward, it is a cavity and stays") + { + indexed_triangle_set cavity = cube(10., {5., 5., 5.}); + its_flip_triangles(cavity); + TriangleMesh mesh(merged({cube(20.), cavity})); + MeshRepairStats stats; + REQUIRE(repair_mesh(mesh, &stats)); + CHECK_FALSE(stats.modified); + CHECK_THAT(its_volume(mesh.its), WithinRel(8000. - 1000., 1e-4)); + } +} + +TEST_CASE("A pipe missing its end caps gets annular caps", "[MeshRepair]") +{ + const int n = 64; + const double h = 20., r_out = 10., r_in = 7.; + indexed_triangle_set its; + for (const double r : {r_out, r_in}) + for (int k = 0; k < n; ++k) { + const double a = 2. * PI * k / n; + its.vertices.emplace_back(float(r * std::cos(a)), float(r * std::sin(a)), 0.f); + its.vertices.emplace_back(float(r * std::cos(a)), float(r * std::sin(a)), float(h)); + } + for (int k = 0; k < n; ++k) { + const int a0 = 2 * k, b0 = a0 + 1, a1 = 2 * ((k + 1) % n), b1 = a1 + 1; + its.indices.emplace_back(a0, a1, b1); + its.indices.emplace_back(a0, b1, b0); + its.indices.emplace_back(2 * n + a0, 2 * n + b1, 2 * n + a1); + its.indices.emplace_back(2 * n + a0, 2 * n + b0, 2 * n + b1); + } + TriangleMesh mesh(std::move(its)); + REQUIRE(repair_mesh(mesh)); + check_solid(mesh, h * (regular_polygon_area(n, r_out) - regular_polygon_area(n, r_in))); +} + +TEST_CASE("A stray flat triangle is dropped", "[MeshRepair]") +{ + indexed_triangle_set its = cube(20.); + its.vertices.insert(its.vertices.end(), {{30, 0, 0}, {40, 0, 0}, {30, 10, 0}}); + its.indices.emplace_back(8, 9, 10); + TriangleMesh mesh(std::move(its)); + MeshRepairStats stats; + REQUIRE(repair_mesh(mesh, &stats)); + CHECK(stats.removed_shells == 1); + check_solid(mesh, 8000.); +} + +TEST_CASE("A two-sided export keeps one side of each triangle", "[MeshRepair]") +{ + indexed_triangle_set back = cube(20.); + its_flip_triangles(back); + TriangleMesh mesh(soup(merged({cube(20.), back}))); + REQUIRE(repair_mesh(mesh)); + check_solid(mesh, 8000.); +} + +TEST_CASE("A valid mesh is left untouched", "[MeshRepair]") +{ + TriangleMesh mesh = make_sphere(10., 2. * PI / 36.); + const TriangleMesh original = mesh; + MeshRepairStats stats; + REQUIRE(repair_mesh(mesh, &stats)); + CHECK_FALSE(stats.modified); + CHECK(mesh.its.vertices == original.its.vertices); + CHECK(mesh.its.indices == original.its.indices); +} + +TEST_CASE("A mesh enclosing no volume is reported and left untouched", "[MeshRepair]") +{ + indexed_triangle_set its; + its.vertices = {{0, 0, 0}, {10, 0, 0}, {10, 10, 0}, {0, 10, 0}}; + its.indices = {{0, 1, 2}, {0, 2, 3}}; + TriangleMesh mesh(its); + std::string error; + CHECK_FALSE(repair_mesh(mesh, nullptr, &error)); + CHECK_FALSE(error.empty()); + CHECK(mesh.its.indices.size() == 2); +} + +TEST_CASE("Progress is reported in order and throwing from it cancels the repair", "[MeshRepair]") +{ + TriangleMesh mesh(soup(cube(20.))); + std::vector reported; + REQUIRE(repair_mesh(mesh, nullptr, nullptr, [&reported](int percent) { reported.push_back(percent); })); + CHECK_FALSE(reported.empty()); + CHECK(std::is_sorted(reported.begin(), reported.end())); + + struct Canceled {}; + TriangleMesh canceled(soup(cube(20.))); + const TriangleMesh original = canceled; + CHECK_THROWS_AS(repair_mesh(canceled, nullptr, nullptr, [](int) { throw Canceled(); }), Canceled); + CHECK(canceled.its.vertices == original.its.vertices); + CHECK(canceled.its.indices == original.its.indices); +} diff --git a/tests/libslic3r/test_model.cpp b/tests/libslic3r/test_model.cpp index 5fc72a2b9b..abdaabd801 100644 --- a/tests/libslic3r/test_model.cpp +++ b/tests/libslic3r/test_model.cpp @@ -12,7 +12,9 @@ #include #include #include +#include "libslic3r/MeshRepair.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/ObjectID.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/libslic3r.h" @@ -97,3 +99,104 @@ TEST_CASE("An instance added from another's scale, rotation and mirror matches i original->get_rotation(), original->get_mirror()); CHECK(copy->get_matrix().linear().isApprox(original->get_matrix().linear(), 1e-9)); } + +namespace { + +// A flat triangle first, then a cube for each offset, all in one mesh. +TriangleMesh flat_triangle_then_cubes(const std::vector &cube_offsets) +{ + indexed_triangle_set its; + its.vertices = {{-20.f, 0.f, 0.f}, {-10.f, 0.f, 0.f}, {-15.f, 5.f, 0.f}}; + its.indices = {{0, 1, 2}}; + for (double offset : cube_offsets) { + TriangleMesh cube = make_cube(10, 10, 10); + cube.translate(float(offset), 0.f, 0.f); + its_merge(its, cube.its); + } + return TriangleMesh(its); +} + +} // namespace + +TEST_CASE("Splitting a part drops pieces enclosing no volume and keeps the part itself", "[Model]") +{ + Model model; + ModelObject *object = model.add_object(); + ModelVolume *volume = object->add_volume(flat_triangle_then_cubes({0., 30.}), ModelVolumeType::MODEL_PART, false); + REQUIRE(volume->is_splittable()); + + CHECK(volume->split(1, false) == 2); + REQUIRE(object->volumes.size() == 2); + CHECK(std::find(object->volumes.begin(), object->volumes.end(), volume) != object->volumes.end()); + for (const ModelVolume *part : object->volumes) { + CHECK(part->mesh().facets_count() == 12); + CHECK_FALSE(part->is_splittable()); + } +} + +TEST_CASE("Splitting a part with a single solid piece keeps just that piece", "[Model]") +{ + Model model; + ModelObject *object = model.add_object(); + ModelVolume *volume = object->add_volume(flat_triangle_then_cubes({0.}), ModelVolumeType::MODEL_PART, false); + + CHECK(volume->split(1, false) == 1); + REQUIRE(object->volumes.size() == 1); + CHECK(object->volumes.front() == volume); + CHECK(volume->mesh().facets_count() == 12); +} + +TEST_CASE("Splitting a part made only of pieces enclosing no volume leaves it whole", "[Model]") +{ + Model model; + ModelObject *object = model.add_object(); + indexed_triangle_set its; + its.vertices = {{0.f, 0.f, 0.f}, {10.f, 0.f, 0.f}, {5.f, 5.f, 0.f}, {20.f, 0.f, 0.f}, {30.f, 0.f, 0.f}, {25.f, 5.f, 0.f}}; + its.indices = {{0, 1, 2}, {3, 4, 5}}; + ModelVolume *volume = object->add_volume(TriangleMesh(its), ModelVolumeType::MODEL_PART, false); + + CHECK(volume->split(1, false) == 1); + REQUIRE(object->volumes.size() == 1); + CHECK(object->volumes.front() == volume); + CHECK(volume->mesh().facets_count() == 2); +} + +TEST_CASE("Replacing a part's mesh updates whether it can be split", "[Model]") +{ + Model model; + ModelObject *object = model.add_object(); + TriangleMesh two_cubes = make_cube(10, 10, 10); + TriangleMesh second = make_cube(10, 10, 10); + second.translate(30.f, 0.f, 0.f); + two_cubes.merge(second); + ModelVolume *volume = object->add_volume(std::move(two_cubes), ModelVolumeType::MODEL_PART, false); + REQUIRE(volume->is_splittable()); + + volume->set_mesh(make_cube(10, 10, 10)); + CHECK_FALSE(volume->is_splittable()); +} + +TEST_CASE("Repairing a part replaces its mesh only when the repair changes it", "[Model]") +{ + Model model; + ModelObject *object = model.add_object(); + TriangleMesh flipped = make_cube(10, 10, 10); + std::swap(flipped.its.indices.front()[1], flipped.its.indices.front()[2]); + ModelVolume *broken = object->add_volume(std::move(flipped), ModelVolumeType::MODEL_PART, false); + TriangleMesh cube = make_cube(10, 10, 10); + cube.translate(30.f, 0.f, 0.f); + ModelVolume *valid = object->add_volume(std::move(cube), ModelVolumeType::MODEL_PART, false); + const ObjectID broken_id = broken->id(); + const ObjectID valid_id = valid->id(); + + MeshRepairStats stats; + REQUIRE(broken->repair(false, &stats)); + CHECK(stats.modified); + CHECK(broken->id() != broken_id); + CHECK_THAT(its_volume(broken->mesh().its), Catch::Matchers::WithinRel(1000., 1e-6)); + CHECK_FALSE(broken->get_convex_hull().empty()); + + REQUIRE(valid->repair(false, &stats)); + CHECK_FALSE(stats.modified); + CHECK(valid->id() == valid_id); +} diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 02d607650f..342b5343e4 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -37,6 +37,7 @@ #include #include "libslic3r/Config.hpp" #include +#include #include "libslic3r/libslic3r.h" #include "libslic3r/PublishSettings.hpp" #include @@ -333,6 +334,105 @@ TEST_CASE("A directory of user presets loads with each one resolved against its } } +TEST_CASE("A user preset synced to the cloud and back keeps its values on every nozzle variant", "[Preset][Bundle]") +{ + // Two extruders with a Standard and a High Flow variant each, as on a dual-nozzle printer. + auto add_parent = [](PresetCollection &coll) { + Preset &parent = add_inmemory_preset(coll, "Parent Process"); + parent.config.set_key_value("print_extruder_variant", + new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard", "Direct Drive High Flow"})); + parent.config.set_key_value("print_extruder_id", new ConfigOptionInts({1, 1, 2, 2})); + parent.config.set_deserialize_strict("outer_wall_speed", "50,50,50,50"); + parent.is_system = true; + parent.setting_id = "parent-id"; + return parent.config; + }; + + RenameTestCollection local; + const DynamicPrintConfig parent_config = add_parent(local); + Preset &child = add_inmemory_preset(local, "User Process", "Parent Process"); + child.config.apply_only(parent_config, {"print_extruder_variant", "print_extruder_id"}); + child.config.set_deserialize_strict("outer_wall_speed", "11,12,21,22"); + child.version = Semver(1, 0, 0); + + std::map uploaded; + REQUIRE(local.get_differed_values_to_update(child, uploaded) == 0); + uploaded[BBL_JSON_KEY_SETTING_ID] = "child-id"; + uploaded[BBL_JSON_KEY_USER_ID] = "user"; + + // A cloud copy stored before uploads carried the variant layout holds the values alone. + std::map stored_without_layout = uploaded; + stored_without_layout.erase("print_extruder_variant"); + stored_without_layout.erase("print_extruder_id"); + // One saved while the parent had a single variant covers the first extruder only. + std::map stored_single_value = stored_without_layout; + stored_single_value["outer_wall_speed"] = "11"; + + struct Case { const char *label; std::map values; const char *expected; }; + for (const Case &c : {Case{"current upload", uploaded, "11,12,21,22"}, + Case{"stored without layout", stored_without_layout, "11,12,21,22"}, + Case{"stored with a single value", stored_single_value, "11,11,50,50"}}) { + DYNAMIC_SECTION(c.label) { + RenameTestCollection downloaded; + PresetsConfigSubstitutions substitutions; + add_parent(downloaded); + REQUIRE(downloaded.load_user_preset("User Process", c.values, substitutions, ForwardCompatibilitySubstitutionRule::Disable)); + + const Preset *synced = downloaded.find_preset("User Process"); + REQUIRE(synced != nullptr); + CHECK(synced->config.opt_serialize("outer_wall_speed") == c.expected); + } + } +} + +TEST_CASE("A user filament synced to the cloud and back keeps its values and unset overrides on every variant", "[Preset][Bundle]") +{ + auto add_parent = [](PresetCollection &coll) { + Preset &parent = add_inmemory_preset(coll, "Parent Filament"); + parent.config.set_key_value("filament_extruder_variant", + new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"})); + parent.config.set_deserialize_strict("nozzle_temperature", "220,220"); + parent.config.set_deserialize_strict("filament_retraction_length", "nil,nil"); + parent.is_system = true; + parent.setting_id = "parent-id"; + return parent.config; + }; + auto make_collection = [] { + return std::make_unique(Preset::TYPE_FILAMENT, Preset::filament_options(), + static_cast(FullPrintConfig::defaults())); + }; + + auto local = make_collection(); + add_parent(*local); + Preset &child = add_inmemory_preset(*local, "User Filament", "Parent Filament"); + child.config.apply_only(add_parent(*make_collection()), {"filament_extruder_variant"}); + child.config.set_deserialize_strict("nozzle_temperature", "200,210"); + child.config.set_deserialize_strict("filament_retraction_length", "1.2,nil"); + child.version = Semver(1, 0, 0); + + std::map uploaded; + REQUIRE(local->get_differed_values_to_update(child, uploaded) == 0); + uploaded[BBL_JSON_KEY_SETTING_ID] = "child-id"; + uploaded[BBL_JSON_KEY_USER_ID] = "user"; + std::map stored_without_layout = uploaded; + stored_without_layout.erase("filament_extruder_variant"); + + for (const auto &[label, values] : {std::make_pair("current upload", uploaded), + std::make_pair("stored without layout", stored_without_layout)}) { + DYNAMIC_SECTION(label) { + auto downloaded = make_collection(); + PresetsConfigSubstitutions substitutions; + add_parent(*downloaded); + REQUIRE(downloaded->load_user_preset("User Filament", values, substitutions, ForwardCompatibilitySubstitutionRule::Disable)); + + const Preset *synced = downloaded->find_preset("User Filament"); + REQUIRE(synced != nullptr); + CHECK(synced->config.opt_serialize("nozzle_temperature") == "200,210"); + CHECK(synced->config.opt_serialize("filament_retraction_length") == "1.2,nil"); + } + } +} + TEST_CASE("Repeated loads of a user preset directory produce the same presets", "[Preset][Bundle]") { ScopedTemporaryDir temp_dir; diff --git a/tests/libslic3r/test_step.cpp b/tests/libslic3r/test_step.cpp index 5d843499fa..ff8ce1dcba 100644 --- a/tests/libslic3r/test_step.cpp +++ b/tests/libslic3r/test_step.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include "libslic3r/Model.hpp" @@ -91,6 +92,26 @@ TEST_CASE("A cone with a slightly tilted seam imports as a closed mesh", "[Step] CHECK(its_num_open_edges(model.objects.front()->volumes.front()->mesh().its) == 0); } +// The fixture holds two trimmed planes in a geometric set and no shells or solids, as some CAD exporters write +// surface models, so OCCT reads it as a compound of loose faces. +TEST_CASE("A surface model imports whether or not compounds are split", "[Step]") +{ + const std::string path = std::string(TEST_DATA_DIR) + PATH_SEPARATOR "surface_model_geometric_set.step"; + const bool split = GENERATE(false, true); + CAPTURE(split); + + Model model; + bool cancel = false; + Step step(path); + + REQUIRE(step.load() == Step::Step_Status::LOAD_SUCCESS); + REQUIRE(step.mesh(&model, cancel, split) == Step::Step_Status::MESH_SUCCESS); + + REQUIRE(model.objects.size() == 1); + REQUIRE(model.objects.front()->volumes.size() == 1); + CHECK(model.objects.front()->volumes.front()->mesh().facets_count() > 0); +} + TEST_CASE("isUtf8 recognises two, three and four byte sequences", "[Step]") { CHECK(StepPreProcessor::isUtf8("\xC3\xA9")); // U+00E9 diff --git a/tests/libslic3r/test_texture_displacement.cpp b/tests/libslic3r/test_texture_displacement.cpp index 485f4f2a0f..f71c839c42 100644 --- a/tests/libslic3r/test_texture_displacement.cpp +++ b/tests/libslic3r/test_texture_displacement.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include "libslic3r/AABBMesh.hpp" @@ -2157,6 +2158,177 @@ TEST_CASE("A second bake beside a first comes out as fine as a single bake", "[T CHECK(second <= single * 5 / 4); } +// The .3mf stores the layer stack as JSON (see texture_displacement_layers_to_json). Two properties +// matter: everything the UV editor and the panel can set has to survive a round trip, and a document +// written by a build that knew fewer fields has to keep loading, with the missing ones left at their +// defaults rather than zeroed. +TEST_CASE("Texture displacement layers survive a JSON round trip", "[TextureDisplacement]") +{ + std::vector layers(2); + TextureDisplacementLayer &a = layers[0]; + a.slot = 0; + a.name = "Bark"; + a.path = "/textures/bark.png"; + a.depth_mm = 1.25f; + a.tiling_scale = 7.5f; + a.rotation_deg = 30.f; + a.offset = Vec2f(0.25f, -0.5f); + a.invert = true; + a.midlevel = 0.125f; + a.smoothing = 0.75f; + a.edge_smoothing = true; + a.edge_smoothing_amount = 0.25f; + a.auto_connect_islands = false; + a.tile_enabled = false; + a.tile_method = TextureTileMethod::MirroredRepeat; + a.projection_method = TextureProjectionMethod::LSCM; + a.blend_mode = TextureBlendMode::Subtract; + a.color_enabled = true; + a.lscm_seam_angle_deg = 45.f; + a.island_padding_mm = 0.5f; + a.lscm_seam_edges = { { 1, 2 }, { 3, 5 } }; + a.islands = { TextureIsland{ Vec2f(1.f, 2.f), 15.f, 1.5f }, TextureIsland{} }; + a.island_groups = { 0, 0 }; + a.lscm_uv_overrides = { { -4, Vec2f(0.5f, 0.75f) } }; + + TextureDisplacementLayer &b = layers[1]; + b.slot = 1; + b.projection_method = TextureProjectionMethod::ViewProjected; + b.view_project_right = Vec3f(0.f, 1.f, 0.f); + b.view_project_up = Vec3f(0.f, 0.f, 1.f); + b.view_project_projective = true; + for (size_t i = 0; i < b.view_project_matrix.size(); ++i) + b.view_project_matrix[i] = float(i) + 0.5f; + + TextureDisplacementOptions options; + options.displace_border = false; + options.smooth_enabled = true; + options.smooth_strength = 0.6f; + options.smooth_iterations = 5; + options.pipeline_v2 = false; + options.v2_max_triangles_k = 250; + options.color_mix_count = 3; + options.color_despeckle = 4; + options.active_slot = 3; + options.panel_mode = 1; + + const std::string json = texture_displacement_layers_to_json(layers, options); + + std::vector read_layers; + TextureDisplacementOptions read_options; + REQUIRE(texture_displacement_layers_from_json(json, read_layers, read_options)); + REQUIRE(read_layers.size() == layers.size()); + + const TextureDisplacementLayer &ra = read_layers[0]; + CHECK(ra.name == a.name); + CHECK(ra.path == a.path); + CHECK_THAT(ra.depth_mm, WithinAbs(a.depth_mm, 1e-6f)); + CHECK_THAT(ra.tiling_scale, WithinAbs(a.tiling_scale, 1e-6f)); + CHECK_THAT(ra.rotation_deg, WithinAbs(a.rotation_deg, 1e-6f)); + CHECK(ra.offset.isApprox(a.offset)); + CHECK(ra.invert == a.invert); + CHECK_THAT(ra.midlevel, WithinAbs(a.midlevel, 1e-6f)); + CHECK_THAT(ra.smoothing, WithinAbs(a.smoothing, 1e-6f)); + CHECK(ra.edge_smoothing == a.edge_smoothing); + CHECK_THAT(ra.edge_smoothing_amount, WithinAbs(a.edge_smoothing_amount, 1e-6f)); + CHECK(ra.auto_connect_islands == a.auto_connect_islands); + CHECK(ra.tile_enabled == a.tile_enabled); + CHECK(ra.tile_method == a.tile_method); + CHECK(ra.projection_method == a.projection_method); + CHECK(ra.blend_mode == a.blend_mode); + CHECK(ra.color_enabled == a.color_enabled); + CHECK_THAT(ra.lscm_seam_angle_deg, WithinAbs(a.lscm_seam_angle_deg, 1e-6f)); + CHECK_THAT(ra.island_padding_mm, WithinAbs(a.island_padding_mm, 1e-6f)); + CHECK(ra.lscm_seam_edges == a.lscm_seam_edges); + REQUIRE(ra.islands.size() == a.islands.size()); + CHECK(ra.islands[0].offset.isApprox(a.islands[0].offset)); + CHECK_THAT(ra.islands[0].rotation_deg, WithinAbs(a.islands[0].rotation_deg, 1e-6f)); + CHECK_THAT(ra.islands[0].scale, WithinAbs(a.islands[0].scale, 1e-6f)); + CHECK(ra.island_groups == a.island_groups); + REQUIRE(ra.lscm_uv_overrides.size() == 1); + CHECK(ra.lscm_uv_overrides[0].first == -4); + CHECK(ra.lscm_uv_overrides[0].second.isApprox(Vec2f(0.5f, 0.75f))); + + const TextureDisplacementLayer &rb = read_layers[1]; + CHECK(rb.projection_method == TextureProjectionMethod::ViewProjected); + CHECK(rb.view_project_right.isApprox(b.view_project_right)); + CHECK(rb.view_project_up.isApprox(b.view_project_up)); + CHECK(rb.view_project_projective); + CHECK(rb.view_project_matrix == b.view_project_matrix); + + CHECK(read_options.displace_border == options.displace_border); + CHECK(read_options.smooth_enabled == options.smooth_enabled); + CHECK_THAT(read_options.smooth_strength, WithinAbs(options.smooth_strength, 1e-6f)); + CHECK(read_options.smooth_iterations == options.smooth_iterations); + CHECK(read_options.pipeline_v2 == options.pipeline_v2); + CHECK(read_options.v2_max_triangles_k == options.v2_max_triangles_k); + CHECK(read_options.color_mix_count == options.color_mix_count); + CHECK(read_options.color_despeckle == options.color_despeckle); + // Where the panel was left comes back with the rest, so a reopened project is on the same layer. + CHECK(read_options.active_slot == options.active_slot); + CHECK(read_options.panel_mode == options.panel_mode); +} + +TEST_CASE("Texture displacement JSON keeps defaults for keys it does not carry", "[TextureDisplacement]") +{ + // What a build that knew fewer fields would have written: one layer, almost nothing set. + const std::string sparse = R"({"version":1,"layers":[{"slot":2,"name":"Old","depth_mm":0.75}]})"; + + std::vector layers; + TextureDisplacementOptions options; + REQUIRE(texture_displacement_layers_from_json(sparse, layers, options)); + REQUIRE(layers.size() == 1); + + const TextureDisplacementLayer &l = layers[0]; + const TextureDisplacementLayer fresh; + const TextureDisplacementOptions defaults; + CHECK(l.slot == 2); + CHECK(l.name == "Old"); + CHECK_THAT(l.depth_mm, WithinAbs(0.75f, 1e-6f)); + // Everything absent keeps the struct's own default rather than becoming zero. + CHECK_THAT(l.tiling_scale, WithinAbs(fresh.tiling_scale, 1e-6f)); + CHECK(l.auto_connect_islands == fresh.auto_connect_islands); + CHECK(l.tile_enabled == fresh.tile_enabled); + CHECK(l.projection_method == fresh.projection_method); + CHECK_THAT(l.lscm_seam_angle_deg, WithinAbs(fresh.lscm_seam_angle_deg, 1e-6f)); + CHECK(options.color_mix_count == defaults.color_mix_count); + CHECK(options.pipeline_v2 == defaults.pipeline_v2); + CHECK(options.active_slot == defaults.active_slot); + CHECK(options.panel_mode == defaults.panel_mode); + + std::vector unused_layers; + TextureDisplacementOptions unused_options; + CHECK(!texture_displacement_layers_from_json("not json at all", unused_layers, unused_options)); +} + +TEST_CASE("Texture displacement JSON with a wrong-typed element or an out-of-range slot is rejected", "[TextureDisplacement]") +{ + const std::string layer = GENERATE(as{}, + R"({"slot":0,"offset":["x",0]})", + R"({"slot":0,"view_project_right":[null,0,0]})", + R"({"slot":0,"view_project_matrix":[0,0,0,0,0,0,0,0,0,0,0,"x"]})", + R"({"slot":0,"lscm_seam_edges":[1,null]})", + R"({"slot":0,"islands":[[0,0,"x",1]]})", + R"({"slot":0,"lscm_uv_overrides":[[null,0,0]]})", + R"({"slot":8})", + R"({"slot":-1})", + R"({"slot":"x"})", + R"({"name":"No slot"})"); + INFO(layer); + + std::vector layers(1); + layers.front().name = "Kept"; + TextureDisplacementOptions options; + options.color_mix_count = 7; + + bool parsed = true; + REQUIRE_NOTHROW(parsed = texture_displacement_layers_from_json(R"({"version":1,"layers":[)" + layer + "]}", layers, options)); + CHECK_FALSE(parsed); + REQUIRE(layers.size() == 1); + CHECK(layers.front().name == "Kept"); + CHECK(options.color_mix_count == 7); +} + TEST_CASE("whether a layer's texture has colour agrees with its decode", "[TextureDisplacement]") { TextureDisplacementLayer gray; diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index db56a510ac..2fe6512d9e 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -37,6 +37,7 @@ add_executable(${_TEST_NAME}_tests test_plugin_json_depth.cpp test_shortcuts.cpp test_texture_color_palette.cpp + test_texture_island_view.cpp test_file_url.cpp test_user_manager.cpp ../fff_print/test_helpers.cpp diff --git a/tests/slic3rutils/test_texture_island_view.cpp b/tests/slic3rutils/test_texture_island_view.cpp new file mode 100644 index 0000000000..9655c86115 --- /dev/null +++ b/tests/slic3rutils/test_texture_island_view.cpp @@ -0,0 +1,99 @@ +// How the texture displacement gizmo's view follows the UV editor's island highlight. The rules live in +// libslic3r_gui; this is the suite that links it. Same Windows include prologue as +// test_texture_color_palette.cpp (wx pulls in ; keep WIN32_LEAN_AND_MEAN / NOMINMAX ahead of it). +#ifdef WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include +#endif + +#include +#include + +#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp" + +using Follow = Slic3r::GUI::GLGizmoTextureDisplacement::IslandViewFollow; + +namespace { + +// The View row's indices, and step()'s "apply nothing". +constexpr int NORMAL = 0, FAST = 1, CHECKER = 2, DISTORTION = 3, NONE = -1; + +} // namespace + +TEST_CASE("a Normal view gives way to Fast while islands are selected and comes back once none are", "[TextureIslandView][TextureDisplacement]") +{ + Follow f; + CHECK(f.step(false, NORMAL, false) == NONE); + CHECK(f.step(true, NORMAL, false) == FAST); + CHECK(f.step(true, FAST, false) == NONE); + CHECK(f.step(false, FAST, false) == NORMAL); + CHECK(f.step(false, NORMAL, false) == NONE); +} + +TEST_CASE("a Fast, Checker or Distortion view is left alone by the island highlight", "[TextureIslandView][TextureDisplacement]") +{ + const int view = GENERATE(FAST, CHECKER, DISTORTION); + Follow f; + CHECK(f.step(true, view, false) == NONE); + CHECK(f.step(false, view, false) == NONE); +} + +TEST_CASE("arriving in Normal with islands already selected switches to Fast", "[TextureIslandView][TextureDisplacement]") +{ + // The pane's Height button over Checker lands in Normal with the islands still selected. + Follow f; + CHECK(f.step(true, CHECKER, false) == NONE); + CHECK(f.step(true, NORMAL, false) == FAST); + CHECK(f.step(false, FAST, false) == NORMAL); +} + +TEST_CASE("Normal picked while islands are selected stays until they are deselected", "[TextureIslandView][TextureDisplacement]") +{ + Follow f; + REQUIRE(f.step(true, NORMAL, false) == FAST); + f.picked(NORMAL); + CHECK(f.step(true, NORMAL, false) == NONE); + // A debug stage opening and closing over it changes nothing. + CHECK(f.step(true, NORMAL, true) == NONE); + CHECK(f.step(true, NORMAL, false) == NONE); + CHECK(f.step(false, NORMAL, false) == NONE); + CHECK(f.step(true, NORMAL, false) == FAST); +} + +TEST_CASE("Fast picked while it was switched on stays after deselecting", "[TextureIslandView][TextureDisplacement]") +{ + Follow f; + REQUIRE(f.step(true, NORMAL, false) == FAST); + f.picked(FAST); + CHECK(f.step(false, FAST, false) == NONE); +} + +TEST_CASE("Normal picked with nothing selected does not hold off the next switch", "[TextureIslandView][TextureDisplacement]") +{ + Follow f; + CHECK(f.step(false, NORMAL, false) == NONE); + f.picked(NORMAL); + CHECK(f.step(true, NORMAL, false) == FAST); +} + +TEST_CASE("a debug stage holds Normal while it is open, and the switch follows once it closes", "[TextureIslandView][TextureDisplacement]") +{ + Follow f; + REQUIRE(f.step(true, NORMAL, false) == FAST); + // The stage takes Normal itself, with the islands still selected. + CHECK(f.step(true, NORMAL, true) == NONE); + CHECK(f.step(true, NORMAL, false) == FAST); +} + +TEST_CASE("islands deselected while a debug stage is open apply no view over it", "[TextureIslandView][TextureDisplacement]") +{ + Follow f; + REQUIRE(f.step(true, NORMAL, false) == FAST); + CHECK(f.step(false, NORMAL, true) == NONE); + CHECK(f.step(false, NORMAL, false) == NONE); +}