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/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/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/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/GCode.cpp b/src/libslic3r/GCode.cpp index ec7ef83a3d..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 { @@ -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/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 58df91ce77..2a64bed1db 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -2165,6 +2165,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); @@ -2629,15 +2639,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/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/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/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/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index c509e5d6f8..2e429dda20 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1514,6 +1514,8 @@ wxDEFINE_EVENT(EVT_GLCANVAS_PRINTABLE, SimpleEvent); const double GLCanvas3D::DefaultCameraZoomToBoxMarginFactor = 1.25; const double GLCanvas3D::DefaultCameraZoomToBedMarginFactor = 2.00; const double GLCanvas3D::DefaultCameraZoomToPlateMarginFactor = 1.25; +// Main-thread only: _render_frame never runs off it, so no atomic is needed. +int GLCanvas3D::s_frames_in_progress = 0; void GLCanvas3D::load_arrange_settings() { @@ -2468,7 +2470,8 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init) } m_in_render = true; - Slic3r::ScopeGuard in_render_guard([this]() { m_in_render = false; }); + ++s_frames_in_progress; + Slic3r::ScopeGuard in_render_guard([this]() { m_in_render = false; --s_frames_in_progress; }); (void)in_render_guard; if (m_canvas == nullptr) @@ -2713,15 +2716,31 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init) // On Wayland, eglSwapBuffers blocks when the canvas is hidden or // occluded. Skip the swap to avoid stalling the render loop. - if (m_canvas->IsShownOnScreen()) { + // Code run from inside this frame - a modal dialog a gizmo opened from its ImGui panel, a + // synchronous paint of another canvas - may have made a different surface current. Bind this one + // again before presenting, so the swap is not issued against a window the context no longer + // belongs to: on Mesa/X11 that is a fatal BadMatch, not the no-op it is on other platforms. Draws + // already issued before this point cannot be redirected - only this swap is protected. + if (m_canvas->IsShownOnScreen() && _set_current()) { m_canvas->SwapBuffers(); m_frame_profiler.mark("swap"); if (!overlay_tick) m_render_stats.increment_fps_counter(); m_presented_signature = signature; } - else + else { + if (m_canvas->IsShownOnScreen()) { + // Logged once: if the rebind fails it does so every frame, and a warning per frame would + // bury everything else. + static bool rebind_failure_logged = false; + if (!rebind_failure_logged) { + rebind_failure_logged = true; + BOOST_LOG_TRIVIAL(warning) << "GLCanvas3D: could not rebind the GL context to this canvas before " + "SwapBuffers(); the frame is not presented"; + } + } m_presented_signature.reset(); + } } // Everything drawn into the 3D scene, from the clear to the post processing passes, ending in the diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index bb148234d1..2cb6bcea4f 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -643,6 +643,8 @@ private: #endif unsigned int m_last_w, m_last_h; bool m_in_render; + // How many canvases are inside _render_frame() right now - see is_frame_in_progress(). + static int s_frames_in_progress; wxTimer m_timer; wxTimer m_timer_set_color; // Armed by each frame that draws the FPS overlay; its tick requests an overlay-only frame. @@ -974,6 +976,11 @@ public: void on_change_color_mode(bool is_dark, bool reinit = true); const bool get_dark_mode_status() { return m_is_dark; } void set_as_dirty() { m_dirty = true; } + // True while any GLCanvas3D is inside _render_frame(). The other surfaces that share the app's one + // GL context (the UV editor canvas) check this before making themselves current: a frame interrupted + // by another surface finishes drawing into that surface and then presents a window the context no + // longer belongs to, which on Mesa/X11 is a fatal X error (BadMatch) rather than a no-op. + static bool is_frame_in_progress() { return s_frames_in_progress > 0; } void requires_check_outside_state() { m_requires_check_outside_state = true; } unsigned int get_volumes_count() const { return (unsigned int)m_volumes.volumes.size(); } 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_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 4a733d3342..40ba495349 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -6297,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); @@ -6328,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) @@ -6360,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; }; @@ -6399,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++; } @@ -6411,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++; } @@ -6419,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)); 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 45ff19480f..768f16f567 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -562,6 +562,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; @@ -727,18 +732,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 @@ -1520,7 +1520,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; @@ -1926,6 +1926,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(); @@ -2651,6 +2661,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(); @@ -2891,7 +2932,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(); } @@ -4038,8 +4080,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; @@ -4213,7 +4255,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(); @@ -4236,7 +4278,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(); @@ -5342,7 +5384,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(); @@ -5355,7 +5397,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(); } @@ -5364,17 +5406,17 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_color_texture(const TextureDisp { if (layer.empty() || !layer.color_enabled) return nullptr; - 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) return m_color_tex.get(); std::unique_ptr texture = upload_color_texture(decode_height_texture(layer), 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; return m_color_tex.get(); } @@ -5386,7 +5428,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); @@ -5394,7 +5436,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(); } @@ -5835,6 +5877,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; @@ -6281,7 +6326,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(); @@ -6323,10 +6368,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 @@ -6349,19 +6393,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); @@ -6369,7 +6413,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); @@ -6393,6 +6437,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) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp index a520fba426..5b754edfe9 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp @@ -197,6 +197,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/ @@ -513,6 +534,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; @@ -744,6 +770,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 @@ -924,25 +952,26 @@ 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; + std::unique_ptr m_color_tex; + std::weak_ptr> m_color_tex_source; + float m_color_tex_smoothing = -1.f; // Library textures the picker has shown at least once, keyed by file path (see LibraryTexture). std::map m_library_textures; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index d840754ac2..41d4811664 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -420,7 +420,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(); @@ -435,6 +438,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 @@ -522,7 +528,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. @@ -1427,10 +1433,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/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 8339e4d9d9..dd2d0e60d3 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -14984,24 +14984,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 @@ -22041,13 +22025,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/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..87f629816f 100644 --- a/src/slic3r/GUI/UVEditorCanvas.cpp +++ b/src/slic3r/GUI/UVEditorCanvas.cpp @@ -36,6 +36,7 @@ #include "3DScene.hpp" #include "BitmapCache.hpp" +#include "GLCanvas3D.hpp" #include "GLShader.hpp" #include "GUI_App.hpp" #include "I18N.hpp" @@ -939,7 +940,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 +971,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(); @@ -1319,8 +1325,13 @@ void UVEditorCanvas::on_size(wxSizeEvent &evt) // Update() flushes it immediately - still through the normal paint path (unlike calling // render() directly), which matters because this canvas shares the app's one GL context with // the 3D view and must not hijack it outside its own paint. + // Not while a 3D canvas frame is in progress, though: wxGTK delivers this size event synchronously + // when the texture displacement gizmo relays out the pane from its ImGui panel, i.e. from inside + // that frame, and flushing the paint there would take the shared context away from it. The Refresh() + // then runs the paint once the frame is over. Refresh(); - Update(); + if (!GLCanvas3D::is_frame_in_progress()) + Update(); } void UVEditorCanvas::on_leave(wxMouseEvent &evt) @@ -1341,6 +1352,16 @@ void UVEditorCanvas::render() { if (m_context == nullptr || !IsShownOnScreen()) return; + // Painted from inside a 3D canvas frame - a modal dialog the gizmo opened from its ImGui panel runs + // this paint, as does any other synchronous event raised there. Making this surface current now + // would take the shared context away from that canvas mid-frame: its remaining draws would land + // here and, on Mesa/X11, its SwapBuffers() would be a fatal BadMatch. Skip, but ask for another + // paint so the skipped one is not simply lost. The pane stays stale for as long as a nested modal + // loop keeps a frame on the stack, which is behind the dialog anyway. + if (GLCanvas3D::is_frame_in_progress()) { + Refresh(); + return; + } // wxGLCanvas::SetCurrent() genuinely fails (returns false) on a canvas that isn't shown on // screen, and can also fail on a pixel-format mismatch with the context - and every GL call 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/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/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_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 e667f7e79f..64fde37396 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 #include "libslic3r/libslic3r.h" #include "libslic3r/PublishSettings.hpp" @@ -334,6 +335,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/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); +}