diff --git a/.github/workflows/check_profiles.yml b/.github/workflows/check_profiles.yml index 59c92e3ec0..db3ae6c4e8 100644 --- a/.github/workflows/check_profiles.yml +++ b/.github/workflows/check_profiles.yml @@ -1,8 +1,12 @@ name: Check profiles on: pull_request: + # release/* is included because pr-merge-bot.yml lets delegates merge into + # it, and it gates on this workflow's result. Without it a delegated merge + # into a release branch would run no profile validation at all. branches: - main + - release/* paths: - 'resources/profiles/**' - ".github/workflows/check_profiles.yml" @@ -20,6 +24,8 @@ permissions: jobs: check_profiles: + # This job name is the check-run name pr-merge-bot.yml requires before a + # delegated merge. Renaming it silently disables that gate. name: Check profiles runs-on: ubuntu-24.04 steps: diff --git a/.github/workflows/pr-merge-bot.yml b/.github/workflows/pr-merge-bot.yml new file mode 100644 index 0000000000..9b5ff2dfaf --- /dev/null +++ b/.github/workflows/pr-merge-bot.yml @@ -0,0 +1,510 @@ +name: PR Merge Bot + +# Merges a pull request on request from a delegated vendor profile maintainer. +# The merge is performed by this workflow's GITHUB_TOKEN, so a delegate needs no +# repository access. +# +# Commands, posted as a comment on the PR: +# /bot merge squash-merge the PR +# /bot merge --dry-run report the verdict without merging +# +# Merges only when the commenter holds a grant covering every changed path, the +# PR targets main or release/*, and CI is green on the head commit. Otherwise it +# comments naming the files that fell outside the grant. +# +# Grants come from the FOLDER_MERGERS variable in the `merge-delegation` +# environment: one per line, `account: path`, `#` comments and blank lines +# allowed. Paths may contain spaces. A vendor takes two grants, the folder and +# its sibling bundle JSON: +# +# # Acme profiles +# vendor-maintainer: resources/profiles/Acme/ +# vendor-maintainer: resources/profiles/Acme.json +# +# Edit the grant list (environment scope, so admin only): +# gh variable set FOLDER_MERGERS --env merge-delegation --body "$(cat folder-mergers.txt)" +# gh variable get FOLDER_MERGERS --env merge-delegation +# +# Stop all merging without touching this file: +# gh variable set MERGE_BOT_DRY_RUN --body true + +on: + issue_comment: + types: + - created + +# One merge attempt per PR at a time, so two quick comments cannot race. +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: false + +jobs: + merge: + # Skips the job unless a PR comment mentions the command. + if: >- + github.repository == 'OrcaSlicer/OrcaSlicer' + && github.event.issue.pull_request != null + && contains(github.event.comment.body, '/bot merge') + permissions: + contents: write # pulls.merge + pull-requests: write # pulls.merge + issues: write # feedback comment + reactions + actions: write # re-dispatch build_all.yml after the merge + runs-on: ubuntu-latest + timeout-minutes: 10 + # Supplies FOLDER_MERGERS. Must carry no protection rules, or every + # delegated merge would wait for a human reviewer. + environment: merge-delegation + steps: + - name: Merge PR on behalf of a folder delegate + uses: actions/github-script@v9 + env: + # Read as env vars, never interpolated into the script body. + FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }} + MERGE_BOT_DRY_RUN: ${{ vars.MERGE_BOT_DRY_RUN }} + with: + script: | + function isPermissionDenied(error) { + return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || ''); + } + + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + + const MARKER = ''; + // No grant may reach outside this root. + const DELEGATABLE_ROOT = 'resources/profiles/'; + const ALLOWED_BASE_BRANCH = /^(?:main|release\/.+)$/; + const MERGE_METHOD = 'squash'; + const REQUIRED_CHECK = 'Check profiles'; // job name in check_profiles.yml + const MAX_CHANGED_FILES = 500; // policy cap, well under listFiles' 3000 + const LISTFILES_CAP = 3000; + const MAX_REPORTED_FILES = 12; + const MERGEABLE_ATTEMPTS = 5; + const MERGEABLE_DELAY_MS = 2000; + const OK_CONCLUSIONS = new Set(['success', 'neutral', 'skipped']); + const REGULAR_FILE_MODES = new Set(['100644', '100755']); + + // Paths refused whatever the grants say. Checked before grants, so + // delegating a new root means removing it from this list too. + const DENIED_PATTERNS = [ + /^\.github\//, + /(^|\/)\.git(attributes|modules|ignore|config)$/, + /^(?:src|deps|deps_src|tests|tools|cmake|sandboxes|scripts|docs?|localization|bbl)\//, + /(^|\/)cmakelists\.txt$/, + /\.cmake$/, + /^build_[^/]*\.(?:sh|bat)$/, + /^version\.inc$/, + // Executables, including those inside the delegatable root. + /\.(?:sh|bash|bat|cmd|ps1|py|js|mjs|cjs|ts|rb|pl|php)$/ + ]; + + function parseGrants(raw) { + // GitHub login: 1-39 chars, alphanumerics with single interior hyphens. + const loginPattern = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/; + const grantsByLogin = new Map(); + const problems = []; + + (raw || '').split(/\r?\n/).forEach((rawLine, index) => { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) { + return; + } + + // Splits on the first colon only, so paths may contain ':' and spaces. + const separator = line.indexOf(':'); + if (separator === -1) { + problems.push(`line ${index + 1}: expected \`account: path\``); + return; + } + + const login = line.slice(0, separator).trim().replace(/^@/, ''); + const path = line.slice(separator + 1).trim().replace(/\/+$/, ''); + + if (!loginPattern.test(login)) { + problems.push(`line ${index + 1}: \`${login}\` is not a valid GitHub account name`); + return; + } + if (/[\\*?\u0000-\u001f\u007f]/.test(path) || path.split('/').includes('..') || path.includes('//')) { + problems.push(`line ${index + 1}: invalid path (no globs, \`..\`, \`//\`, backslashes or control characters)`); + return; + } + // Rejects anything outside the root, and the bare root itself. + if (!path.startsWith(DELEGATABLE_ROOT) || path.length <= DELEGATABLE_ROOT.length) { + problems.push(`line ${index + 1}: \`${path}\` is not inside \`${DELEGATABLE_ROOT}\``); + return; + } + + const key = login.toLowerCase(); + grantsByLogin.set(key, (grantsByLogin.get(key) || []).concat(path)); + }); + + return { grantsByLogin, problems }; + } + + function isDenied(path) { + if (/[\\\u0000-\u001f\u007f]/.test(path) || path.startsWith('/') || path.split('/').includes('..')) { + return true; + } + + const normalized = path.normalize('NFKC').toLowerCase(); + return DENIED_PATTERNS.some((pattern) => pattern.test(normalized)); + } + + // Byte-exact match on directory boundaries, so a grant of + // `.../Acme` covers neither `.../Acme Labs/x.json` nor `.../Acme.json`. + function isGranted(path, grants) { + return grants.some((grant) => path === grant || path.startsWith(`${grant}/`)); + } + + // Both endpoints of a rename; both must satisfy the grant. + function pathsFor(file) { + return [file.filename, file.previous_filename].filter(Boolean); + } + + function formatList(items) { + const unique = [...new Set(items)]; + const shown = unique.slice(0, MAX_REPORTED_FILES).map((item) => `- \`${item}\``); + if (unique.length > MAX_REPORTED_FILES) { + shown.push(`- …and ${unique.length - MAX_REPORTED_FILES} more`); + } + return shown.join('\n'); + } + + const { owner, repo } = context.repo; + const issue = context.payload.issue; + const comment = context.payload.comment; + + if (!issue.pull_request) { + core.info('Ignoring comment that is not on a pull request.'); + return; + } + // Ignores a comment whose sender is not its author. + if (context.payload.action !== 'created' || context.payload.sender.login !== comment.user.login) { + core.warning('Ignoring comment whose sender does not match its author.'); + return; + } + if (comment.user.type !== 'User') { + core.info('Ignoring bot-authored command.'); + return; + } + + const commandLine = (comment.body || '') + .split('\n') + .map((line) => line.trim()) + .find((line) => /^\/bot\s+merge\b/i.test(line)); + + if (!commandLine) { + core.info('No /bot merge command found.'); + return; + } + + const commenter = comment.user.login; + const { grantsByLogin, problems } = parseGrants(process.env.FOLDER_MERGERS); + const grants = grantsByLogin.get(commenter.toLowerCase()) || []; + + for (const problem of problems) { + core.warning(`FOLDER_MERGERS ${problem}`); + } + + // Says nothing to accounts with no grant, so it cannot be used to spam. + if (!grants.length) { + core.info(`Ignoring /bot merge from @${commenter}: not listed in FOLDER_MERGERS.`); + return; + } + + // Warns instead of failing when the token cannot post feedback. + async function bestEffort(call, warning) { + try { + await call(); + } catch (error) { + if (isPermissionDenied(error)) { + core.warning(warning); + return; + } + + throw error; + } + } + + const react = (content) => bestEffort( + () => github.rest.reactions.createForIssueComment({ owner, repo, comment_id: comment.id, content }), + `Cannot add the "${content}" reaction because the token cannot write.`); + + const say = (body) => bestEffort( + () => github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: `${MARKER}\n${body}` }), + 'Cannot post a comment because the token cannot write comments.'); + + // Declines the command: warns in the log, reacts, explains on the PR. + async function refuse(reason) { + const configNote = problems.length + ? `\n\n\`FOLDER_MERGERS\` also has problems a maintainer needs to fix:\n${problems.map((problem) => `- ${problem}`).join('\n')}` + : ''; + const grantsNote = `\n\n
Your current grants\n\n${formatList(grants)}\n\n
`; + + core.warning(`Refused /bot merge from @${commenter}: ${reason}`); + await react('-1'); + await say(`@${commenter} I can't merge this PR: ${reason}${configNote}${grantsNote}`); + } + + await react('eyes'); + + const args = (commandLine.match(/^\/bot\s+merge\s*(.*)$/i)[1] || '').trim().split(/\s+/).filter(Boolean); + const unknownArgs = args.filter((arg) => arg.toLowerCase() !== '--dry-run'); + const dryRun = String(process.env.MERGE_BOT_DRY_RUN || '').toLowerCase() === 'true' + || unknownArgs.length !== args.length; + + if (unknownArgs.length) { + return refuse( + `I don't understand ${unknownArgs.map((arg) => `\`${arg}\``).join(', ')}. ` + + 'Usage: `/bot merge` or `/bot merge --dry-run`.' + ); + } + + // Refuses everything while the grant list is malformed. + if (problems.length) { + return refuse( + 'the `FOLDER_MERGERS` grant list has malformed lines, so I refuse every merge until it is fixed.' + ); + } + + let { data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: issue.number + }); + + if (pr.merged) { + return refuse('it is already merged.'); + } + if (pr.state !== 'open') { + return refuse(`its state is \`${pr.state}\`, not \`open\`.`); + } + if (pr.draft) { + return refuse('it is still a draft. Mark it ready for review first.'); + } + if (!ALLOWED_BASE_BRANCH.test(pr.base.ref)) { + return refuse(`it targets \`${pr.base.ref}\`. Delegated merges are only allowed into \`main\` and \`release/*\`.`); + } + + // ---- folder scope ---- + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number: pr.number, + per_page: 100 + }); + + if (!files.length) { + return refuse('it changes no files, so there is nothing to verify or merge.'); + } + // Refuses when the file list is truncated or disagrees with the PR. + if (files.length >= LISTFILES_CAP || files.length !== pr.changed_files) { + return refuse( + `it reports ${pr.changed_files} changed files but the API listed ${files.length}, ` + + 'so the file list is truncated and I cannot verify the folder scope. A maintainer must merge this one.' + ); + } + if (pr.changed_files > MAX_CHANGED_FILES) { + return refuse(`it changes ${pr.changed_files} files; delegated merges are capped at ${MAX_CHANGED_FILES}.`); + } + + const deniedFiles = []; + const outsideFiles = []; + + for (const file of files) { + for (const path of pathsFor(file)) { + if (isDenied(path)) { + deniedFiles.push(path); + } else if (!isGranted(path, grants)) { + outsideFiles.push(path); + } + } + } + + if (deniedFiles.length) { + core.error(`@${commenter} attempted a delegated merge touching protected paths: ${deniedFiles.join(', ')}`); + return refuse( + 'it touches paths that are never delegatable, whatever the grants say ' + + `(CI, build, scripts or executable files):\n\n${formatList(deniedFiles)}\n\nA maintainer should look at this before it goes any further.` + ); + } + if (outsideFiles.length) { + return refuse( + `${outsideFiles.length} changed path(s) fall outside your grants:\n\n${formatList(outsideFiles)}\n\n` + + 'A vendor needs both grants: `resources/profiles//` **and** `resources/profiles/.json`.' + ); + } + + // ---- file modes: rejects symlinks and submodules ---- + // Fetches the delegatable subtree only; listFiles does not report modes. + const headSha = pr.head.sha; + const { data: tree } = await github.rest.git.getTree({ + owner, + repo, + tree_sha: `${headSha}:${DELEGATABLE_ROOT.replace(/\/$/, '')}`, + recursive: 'true' + }); + + if (tree.truncated) { + return refuse('the git tree is too large to verify file modes. A maintainer must merge this one.'); + } + + // Entry paths are subtree-relative. + const modesByPath = new Map(tree.tree.map((entry) => [`${DELEGATABLE_ROOT}${entry.path}`, entry.mode])); + const irregularFiles = files + .filter((file) => file.status !== 'removed') + .map((file) => [file.filename, modesByPath.get(file.filename)]) + .filter(([, mode]) => !REGULAR_FILE_MODES.has(mode)) + .map(([path, mode]) => `${path} (mode ${mode || 'missing'})`); + + if (irregularFiles.length) { + core.error(`@${commenter} attempted a delegated merge with non-regular files: ${irregularFiles.join(', ')}`); + return refuse( + `it adds symlinks, submodules or files I cannot verify:\n\n${formatList(irregularFiles)}\n\nA maintainer should look at this before it goes any further.` + ); + } + + // ---- mergeability: waits for GitHub to compute it ---- + for (let attempt = 0; pr.mergeable === null && attempt < MERGEABLE_ATTEMPTS; attempt += 1) { + core.info(`Mergeability not computed yet; retrying in ${MERGEABLE_DELAY_MS}ms.`); + await sleep(MERGEABLE_DELAY_MS); + ({ data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: pr.number + })); + } + + if (pr.mergeable === null) { + return refuse('GitHub is still working out whether it can be merged. Try `/bot merge` again in a minute.'); + } + if (!pr.mergeable) { + return refuse(`it is not mergeable (\`${pr.mergeable_state}\`) - most likely a conflict with \`${pr.base.ref}\`.`); + } + + // ---- CI on the head commit ---- + const checkRuns = await github.paginate(github.rest.checks.listForRef, { + owner, + repo, + ref: headSha, + filter: 'latest', + per_page: 100 + }); + const pendingChecks = checkRuns.filter((run) => run.status !== 'completed'); + const failedChecks = checkRuns.filter((run) => run.status === 'completed' && !OK_CONCLUSIONS.has(run.conclusion)); + + if (pendingChecks.length) { + return refuse( + `${pendingChecks.length} check(s) are still running on \`${headSha.slice(0, 7)}\`:\n\n` + + `${formatList(pendingChecks.map((run) => run.name))}\n\nRe-run \`/bot merge\` once they finish.` + ); + } + if (failedChecks.length) { + return refuse( + `${failedChecks.length} check(s) are not green on \`${headSha.slice(0, 7)}\`:\n\n` + + formatList(failedChecks.map((run) => `${run.name} (${run.conclusion})`)) + ); + } + + const { data: combined } = await github.rest.repos.getCombinedStatusForRef({ + owner, + repo, + ref: headSha + }); + // total_count 0 only means there are no legacy statuses. + if (combined.total_count > 0 && combined.state !== 'success') { + return refuse( + `the combined commit status on \`${headSha.slice(0, 7)}\` is \`${combined.state}\`:\n\n` + + formatList(combined.statuses.filter((status) => status.state !== 'success') + .map((status) => `${status.context} (${status.state})`)) + ); + } + + // Requires the check to have actually run, not merely to have not failed. + const requiredCheck = checkRuns.find((run) => + run.name === REQUIRED_CHECK && + run.app && run.app.slug === 'github-actions' && + run.status === 'completed' && OK_CONCLUSIONS.has(run.conclusion)); + + if (!requiredCheck) { + return refuse( + `the \`${REQUIRED_CHECK}\` check has not succeeded on \`${headSha.slice(0, 7)}\`. ` + + 'If it never ran, a maintainer needs to approve the workflow run first.' + ); + } + + const scopeSummary = `${files.length} file(s), all within:\n${formatList(grants)}`; + + if (dryRun) { + core.info('Dry run: every gate passed, not merging.'); + await react('+1'); + await say( + `@${commenter} **dry run** - this PR passes every gate and I *would* squash-merge it ` + + `at \`${headSha.slice(0, 7)}\`.\n\nVerified scope: ${scopeSummary}` + ); + return; + } + + // ---- re-validate, then merge ---- + // An unchanged head SHA means the verified file list still holds. + const { data: fresh } = await github.rest.pulls.get({ + owner, + repo, + pull_number: pr.number + }); + + if (fresh.head.sha !== headSha || fresh.base.ref !== pr.base.ref || fresh.state !== 'open' || fresh.merged || fresh.draft) { + return refuse('it changed while I was checking it. Nothing was merged - re-run `/bot merge`.'); + } + + let merged; + try { + // Pinned to the verified head: a moved head fails with 409. + ({ data: merged } = await github.rest.pulls.merge({ + owner, + repo, + pull_number: pr.number, + sha: headSha, + merge_method: MERGE_METHOD, + commit_title: `${pr.title} (#${pr.number})`, + commit_message: + `Merged by /bot merge on behalf of @${commenter} (id ${comment.user.id}).\n` + + `Grants: ${grants.join(', ')}\nHead: ${headSha}\n` + })); + } catch (error) { + const hint = { + 403: 'the workflow token cannot write to the repository.', + 405: 'GitHub refused the merge - branch protection, a required review or check, a newly added CODEOWNERS file, or squash merging being disabled.', + 409: `the head commit moved after I verified it (was \`${headSha.slice(0, 7)}\`).`, + 422: 'GitHub rejected the merge as invalid.' + }[error.status]; + + if (!hint) { + throw error; + } + + await refuse(`${hint}\n\n> ${error.message}\n\nNothing was merged.`); + core.setFailed(`Delegated merge failed: ${error.status} ${error.message}`); + return; + } + + core.info(`Merged #${pr.number} as ${merged.sha}.`); + await react('rocket'); + await say( + `@${commenter} squash-merged into \`${pr.base.ref}\` as ${merged.sha}.\n\nVerified scope: ${scopeSummary}` + ); + + // ---- re-kick the build ---- + // A GITHUB_TOKEN merge fires no push event, so build_all.yml would + // otherwise never see these files. + try { + await github.rest.actions.createWorkflowDispatch({ + owner, + repo, + workflow_id: 'build_all.yml', + ref: pr.base.ref + }); + core.info(`Dispatched build_all.yml on ${pr.base.ref}.`); + } catch (error) { + core.warning(`Merged successfully, but dispatching build_all.yml failed: ${error.message}`); + } diff --git a/AGENTS.md b/AGENTS.md index fbc624b958..236aa54c05 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,6 +56,7 @@ ctest --test-dir ./tests/fff_print - Add helper functions or utilities only when existing code cannot reasonably be reused. Avoid duplication. - Keep code concise and clear. Manually simplify AI generated bloated codes before review. - Include targeted tests or documented verification for behavior changes, especially in slicing logic, profiles, formats, and GUI defaults. +- For profile changes (`resources/profiles//**`), check that `version` in the sibling `resources/profiles/.json` was bumped. - For translation changes (`localization/i18n/**/*.po`), check that recurring terms match the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/guides/localization_glossary.md) for that language. ## Localization & translations diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index 0d777ec32e..595e46b8cf 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -217,7 +217,6 @@ msgstr "Os filamentos %s são duros e quebradiços, podendo se romper no AMS. E msgid "%s has a risk of nozzle clogging when using 0.4, 0.6, 0.8mm high-flow nozzles. Use with caution." msgstr "%s apresenta risco de entupimento do bico ao utilizar bicos de alto fluxo de 0,4, 0,6 ou 0,8 mm. Use com cautela." -# AI Translated #, c-format, boost-format msgid "%s may fail to load or unload due to the Filament Track Switch. If you wish to continue." msgstr "%s pode falhar ao carregar ou descarregar devido ao Filament Track Switch. Se você deseja continuar." @@ -347,7 +346,6 @@ msgstr "Leitura " msgid "Please wait" msgstr "Por favor, aguarde" -# AI Translated msgid "Reading" msgstr "Lendo" @@ -700,7 +698,6 @@ msgstr "Redefinir posição" msgid "Reset rotation" msgstr "Redefinir rotação" -# AI Translated msgid "World" msgstr "Mundo" @@ -988,7 +985,6 @@ msgstr "Plano de corte com cavidade é inválido" msgid "Connector" msgstr "Conector" -# AI Translated #, boost-format msgid "" "Objects(%1%) have duplicated connectors. Some connectors may be missing in slicing result.\n" @@ -2032,7 +2028,6 @@ msgstr "" "\n" "Se você não usava o Bambu Cloud para sincronizar perfis, esta mudança não afeta você e você pode ignorar esta mensagem com segurança." -# AI Translated msgid "Profile syncing change" msgstr "Alteração de sincronização de perfil" @@ -3429,7 +3424,7 @@ msgid "AMS has not been initialized. Please initialize it before use." msgstr "O AMS não foi inicializado. Por favor, inicialize-o antes de usar." msgid "Changing fan speed during printing may affect print quality, please choose carefully." -msgstr "Mudar a velocidade do ventilador durante a impressão pode afetar a qualidade da impressão. Escolha com cuidado." +msgstr "Mudar a velocidade da ventoinha durante a impressão pode afetar a qualidade da impressão. Escolha com cuidado." msgid "Change Anyway" msgstr "Mudar Mesmo Assim" @@ -3441,7 +3436,7 @@ msgid "Filter" msgstr "Filtrar" msgid "Enabling filtration redirects the right fan to filter gas, which may reduce cooling performance." -msgstr "Ativar a filtragem redireciona o ventilador direito para filtrar o gás, o que pode reduzir o desempenho de resfriamento." +msgstr "Ativar a filtragem redireciona a ventoinha direito para filtrar o gás, o que pode reduzir o desempenho de resfriamento." msgid "Enabling filtration during printing may reduce cooling and affect print quality. Please choose carefully." msgstr "Habilitar a filtragem durante a impressão pode reduzir o resfriamento e afetar a qualidade da impressão. Escolha com cuidado." @@ -3474,7 +3469,7 @@ msgid "Top" msgstr "Topo" msgid "The fan controls the temperature during printing to improve print quality. The system automatically adjusts the fan's switch and speed according to different printing materials." -msgstr "O ventilador controla a temperatura durante a impressão para melhorar a qualidade da impressão. O sistema ajusta automaticamente a ativação e a velocidade do ventilador de acordo com os diferentes materiais de impressão." +msgstr "A ventoinha controla a temperatura durante a impressão para melhorar a qualidade da impressão. O sistema ajusta automaticamente a ativação e a velocidade da ventoinha de acordo com os diferentes materiais de impressão." msgid "Cooling mode is suitable for printing PLA/PETG/TPU materials and filters the chamber air." msgstr "O modo de resfriamento é adequado para impressão com materiais PLA/PETG/TPU e filtra o ar da câmara." @@ -4798,7 +4793,7 @@ msgid "Pause (AMS offline)" msgstr "Pausa (AMS offline)" msgid "Pause (low speed of the heatbreak fan)" -msgstr "Pausa (baixa velocidade do ventilador do heatbreak)" +msgstr "Pausa (baixa velocidade da ventoinha do heatbreak)" msgid "Pause (chamber temperature control problem)" msgstr "Pausa (problema no controle de temperatura da câmara)" @@ -4922,7 +4917,7 @@ msgstr "Para garantir sua segurança, certas tarefas de processamento (como o la #, c-format, boost-format msgid "The chamber temperature is too high, which may cause the filament to soften. Please wait until the chamber temperature drops below %d℃. You may open the front door or enable fans to cool down." -msgstr "A temperatura da câmara está muito alta, o que pode causar o amolecimento do filamento. Aguarde até que a temperatura da câmara caia abaixo de %d℃. Você pode abrir a porta frontal ou ligar os ventiladores para resfriar." +msgstr "A temperatura da câmara está muito alta, o que pode causar o amolecimento do filamento. Aguarde até que a temperatura da câmara caia abaixo de %d℃. Você pode abrir a porta frontal ou ligar as ventoinhas para resfriar." #, c-format, boost-format msgid "AMS temperature is too high, which may cause the filament to soften. Please wait until the AMS temperature drops below %d℃." @@ -5208,7 +5203,7 @@ msgid "Jerk" msgstr "Jerk" msgid "Fan Speed" -msgstr "Velocidade do Ventilador" +msgstr "Velocidade da Ventoinha" msgid "Flow" msgstr "Fluxo" @@ -5314,7 +5309,7 @@ msgid "Flow: " msgstr "Fluxo: " msgid "Fan: " -msgstr "Ventilador: " +msgstr "Ventoinha: " msgid "Temperature: " msgstr "Temperatura: " @@ -5350,7 +5345,7 @@ msgid "Flow rate" msgstr "Taxa de fluxo" msgid "Fan speed" -msgstr "Velocidade do ventilador" +msgstr "Velocidade da ventoinha" msgid "Time" msgstr "Tempo" @@ -5464,7 +5459,7 @@ msgid "Jerk (mm/s)" msgstr "Jerk (mm/s)" msgid "Fan speed (%)" -msgstr "Velocidade do ventilador (%)" +msgstr "Velocidade da ventoinha (%)" msgid "Temperature (℃)" msgstr "Temperatura (℃)" @@ -7368,12 +7363,11 @@ msgstr "Inferior" msgid "Plugin Selection" msgstr "Seleção de plugins" -# AI Translated msgid "" "No plugins capabilities available for this type.\n" "Enable or install some to use." msgstr "" -"Nenhum recurso de plugins disponível para este tipo.\n" +"Nenhuma capacidade de plugin disponível para este tipo.\n" "Ative ou instale algum para usar." msgid "There is stringing-prone filament in the current print job. Enabling nozzle clumping detection now may degrade print quality. Are you sure you want to enable it?" @@ -9758,7 +9752,7 @@ msgid "Unable to automatically match to suitable filament. Please click to manua msgstr "Não foi possível encontrar automaticamente um filamento adequado. Clique para selecionar manualmente." msgid "Install toolhead enhanced cooling fan to prevent filament softening." -msgstr "Instale um ventilador de resfriamento aprimorado no cabeçote de impressão para evitar o amolecimento do filamento." +msgstr "Instale uma ventoinha de resfriamento aprimorado no cabeçote de impressão para evitar o amolecimento do filamento." msgid "Smooth Cool Plate" msgstr "Placa Fria Lisa" @@ -10382,25 +10376,25 @@ msgid "Cooling for specific layer" msgstr "Resfriamento para camada específica" msgid "Part cooling fan" -msgstr "Ventilador de resfriamento de peças" +msgstr "Ventoinha de resfriamento de peças" msgid "Min fan speed threshold" -msgstr "Limiar de velocidade mínima do ventilador" +msgstr "Limiar de velocidade mínima da ventoinha" msgid "The part cooling fan will run at the minimum fan speed when the estimated layer time is longer than the threshold value. When the layer time is shorter than the threshold, the fan speed will be interpolated between the minimum and maximum fan speed according to layer printing time." -msgstr "O ventilador de resfriamento de peças irá girar na velocidade mínima quando o tempo estimado da camada for mais longo do que o valor de limiar. Quando o tempo da camada for mais curto que o limiar, a velocidade do ventilador é interpolada entre a velocidade mínima e máxima de acordo com o tempo de impressão da camada." +msgstr "A ventoinha de resfriamento de peças irá girar na velocidade mínima quando o tempo estimado da camada for mais longo do que o valor de limiar. Quando o tempo da camada for mais curto que o limiar, a velocidade da ventoinha é interpolada entre a velocidade mínima e máxima de acordo com o tempo de impressão da camada." msgid "Max fan speed threshold" -msgstr "Limiar de velocidade máxima do ventilador" +msgstr "Limiar de velocidade máxima da ventoinha" msgid "The part cooling fan will run at maximum speed when the estimated layer time is shorter than the threshold value." -msgstr "O ventilador de resfriamento de peças irá girar na velocidade máxima quando o tempo estimado da camada for mais curto que o limiar." +msgstr "A ventoinha de resfriamento de peças irá girar na velocidade máxima quando o tempo estimado da camada for mais curto que o limiar." msgid "Auxiliary part cooling fan" -msgstr "Ventilador auxiliar de resfriamento de peças" +msgstr "Ventoinha auxiliar de resfriamento de peças" msgid "Exhaust fan" -msgstr "Ventilador de exaustão" +msgstr "Ventoinha de exaustão" msgid "During print" msgstr "Durante a impressão" @@ -10450,10 +10444,10 @@ msgid "G-code flavor is switched" msgstr "Tipo de G-code está trocado" msgid "Cooling Fan" -msgstr "Ventilador de resfriamento" +msgstr "Ventoinha de resfriamento" msgid "Fan speed-up time" -msgstr "Tempo de aceleração do ventilador" +msgstr "Tempo de aceleração da ventoinha" msgid "Extruder Clearance" msgstr "Folga da extrusora" @@ -11770,7 +11764,6 @@ msgstr "Erro de agrupamento: " msgid " can not be placed in the " msgstr " não pode ser colocado na " -# AI Translated 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." @@ -12096,7 +12089,6 @@ msgstr "A contração de filamento não será usada porque a contração dos fil msgid "Generating skirt & brim" msgstr "Gerando saia e borda" -# AI Translated msgid "" "Per-object skirts cannot fit between the objects in By object print sequence.\n" "\n" @@ -12277,9 +12269,8 @@ msgstr "API Key" msgid "HTTP digest" msgstr "Digest HTTP" -# AI Translated msgid "Configuration for the plugin capabilities this preset uses, overriding the global Capabilities configuration. Stored as a raw JSON array and edited through the dialog behind the button, never typed in directly." -msgstr "Configuração dos recursos de plugin que esta predefinição usa, substituindo a configuração global de Recursos. Armazenada como um array JSON bruto e editada por meio da caixa de diálogo atrás do botão, nunca digitada diretamente." +msgstr "Configuração das capacidades de plugin que esta predefinição usa, substituindo a configuração global de Capacidades. Armazenada como um array JSON bruto e editada por meio da caixa de diálogo atrás do botão, nunca digitada diretamente." msgid "Avoid crossing walls" msgstr "Evitar atravessar paredes" @@ -12420,26 +12411,26 @@ msgid "Force cooling for overhangs and bridges" msgstr "Resfriamento forçado para saliências e pontes" msgid "Enable this option to allow adjustment of the part cooling fan speed for specifically for overhangs, internal and external bridges. Setting the fan speed specifically for these features can improve overall print quality and reduce warping." -msgstr "Habilite esta opção para permitir o ajuste da velocidade do ventilador de resfriamento de peças especificamente para saliências, pontes internas e externas. Definir a velocidade do ventilador especificamente para esses recursos pode melhorar a qualidade geral da impressão e reduzir a deformação." +msgstr "Habilite esta opção para permitir o ajuste da velocidade da ventoinha de resfriamento de peças especificamente para saliências, pontes internas e externas. Definir a velocidade da ventoinha especificamente para esses recursos pode melhorar a qualidade geral da impressão e reduzir a deformação." msgid "Overhangs and external bridges fan speed" -msgstr "Velocidade do ventilador para saliências e pontes externas" +msgstr "Velocidade da ventoinha para saliências e pontes externas" msgid "" "Use this part cooling fan speed when printing bridges or overhang walls with an overhang threshold that exceeds the value set in the 'Overhangs cooling threshold' parameter above. Increasing the cooling specifically for overhangs and bridges can improve the overall print quality of these features.\n" "\n" "Please note, this fan speed is clamped on the lower end by the minimum fan speed threshold set above. It is also adjusted upwards up to the maximum fan speed threshold when the minimum layer time threshold is not met." msgstr "" -"Use esta parte da velocidade do ventilador de resfriamento ao imprimir pontes ou paredes salientes com um limite de saliência que exceda o valor definido no parâmetro 'Limiar de resfriamento de saliências' acima. Aumentar o resfriamento especificamente para saliências e pontes pode melhorar a qualidade geral de impressão desses recursos.\n" +"Use esta parte da velocidade da ventoinha de resfriamento ao imprimir pontes ou paredes salientes com um limite de saliência que exceda o valor definido no parâmetro 'Limiar de resfriamento de saliências' acima. Aumentar o resfriamento especificamente para saliências e pontes pode melhorar a qualidade geral de impressão desses recursos.\n" "\n" -"Observe que esta velocidade do ventilador é fixada na extremidade inferior pelo limiar mínimo de velocidade do ventilador definido acima. Ela também é ajustada para cima até o limiar máximo de velocidade do ventilador quando o limiar mínimo de tempo da camada não é atingido." +"Observe que esta velocidade da ventoinha é fixada na extremidade inferior pelo limiar mínimo de velocidade da ventoinha definido acima. Ela também é ajustada para cima até o limiar máximo de velocidade da ventoinha quando o limiar mínimo de tempo da camada não é atingido." msgid "Overhang cooling activation threshold" msgstr "Limiar de ativação de resfriamento de saliência" #, no-c-format, no-boost-format msgid "When the overhang exceeds this specified threshold, force the cooling fan to run at the 'Overhang Fan Speed' set below. This threshold is expressed as a percentage, indicating the portion of each line's width that is unsupported by the layer beneath it. Setting this value to 0% forces the cooling fan to run for all outer walls, regardless of the overhang degree." -msgstr "Quando a saliência excede esse limiar especificado, força o ventilador de resfriamento a funcionar na 'Velocidade da ventoinha de saliência' definida abaixo. Esse limiar é expresso como uma porcentagem, indicando a parte da largura de cada linha que não é suportada pela camada abaixo dela. Definir esse valor como 0% força o ventilador de resfriamento a funcionar para todas as paredes externas, independentemente do grau de saliência." +msgstr "Quando a saliência excede esse limiar especificado, força a ventoinha de resfriamento a funcionar na 'Velocidade da ventoinha de saliência' definida abaixo. Esse limiar é expresso como uma porcentagem, indicando a parte da largura de cada linha que não é suportada pela camada abaixo dela. Definir esse valor como 0% força a ventoinha de resfriamento a funcionar para todas as paredes externas, independentemente do grau de saliência." msgid "External bridge infill direction" msgstr "Direção de preenchimento de ponte externa" @@ -13026,11 +13017,9 @@ msgstr "" msgid "As object list" msgstr "Como lista de objetos" -# AI Translated msgid "Best of all (shortest path)" msgstr "Melhor de todas (caminho mais curto)" -# AI Translated msgid "Snake" msgstr "Serpentina" @@ -13038,7 +13027,7 @@ msgid "Slow printing down for better layer cooling" msgstr "Diminuir a velocidade de impressão para melhor resfriamento de camada" msgid "Enable this option to slow printing speed down to ensure that the final layer time is not shorter than the layer time threshold in \"Max fan speed threshold\", so that the layer can be cooled for a longer time. This can improve the quality for small details." -msgstr "Ative esta opção para diminuir a velocidade de impressão para que o tempo da camada final não seja menor do que o limiar de tempo da camada em \"Limiar de velocidade máxima do ventilador\", para que a camada possa ser resfriada um tempo mais longo. Isso pode melhorar a qualidade para detalhes pequenos." +msgstr "Ative esta opção para diminuir a velocidade de impressão para que o tempo da camada final não seja menor do que o limiar de tempo da camada em \"Limiar de velocidade máxima da ventoinha\", para que a camada possa ser resfriada um tempo mais longo. Isso pode melhorar a qualidade para detalhes pequenos." msgid "Normal printing" msgstr "Impressão normal" @@ -13093,16 +13082,16 @@ msgid "Enable this to override the fan speed set in custom G-code after print co msgstr "Habilite para substituir a velocidade da ventoinha definida no G-code personalizado após a conclusão da impressão." msgid "Speed of exhaust fan during printing. This speed will override the speed in filament custom G-code." -msgstr "Velocidade do ventilador de exaustão durante a impressão. Esta velocidade substituirá a velocidade no G-code personalizado do filamento." +msgstr "Velocidade da ventoinha de exaustão durante a impressão. Esta velocidade substituirá a velocidade no G-code personalizado do filamento." msgid "Speed of exhaust fan after printing completes." -msgstr "Velocidade do ventilador de exaustão após a conclusão da impressão." +msgstr "Velocidade da ventoinha de exaustão após a conclusão da impressão." msgid "No cooling for the first" msgstr "Sem resfriamento para as primeiras" msgid "Turn off all cooling fans for the first few layers. This can be used to improve build plate adhesion." -msgstr "Desligar todos os ventiladores de resfriamento para as primeiras camadas. Isso pode ser usado para obter uma melhor adesão à placa de impressão." +msgstr "Desligar todos as ventoinhas de resfriamento para as primeiras camadas. Isso pode ser usado para obter uma melhor adesão à placa de impressão." msgid "Don't support bridges" msgstr "Não suportar pontes" @@ -13278,11 +13267,9 @@ msgstr "Densidade da superfície superior" msgid "Density of top surface layer. A value of 100% creates a fully solid, smooth top layer. Reducing this value results in a textured top surface, according to the chosen top surface pattern. A value of 0% will result in only the walls on the top layer being created. Intended for aesthetic or functional purposes, not to fix issues such as over-extrusion." msgstr "Densidade da camada superior. Um valor de 100% cria uma camada superior totalmente sólida e lisa. Reduzir esse valor resulta em uma superfície superior texturizada, de acordo com o padrão de superfície superior escolhido. Um valor de 0% resultará na criação apenas das paredes da camada superior. Destinado a fins estéticos ou funcionais, não para corrigir problemas como extrusão excessiva." -# AI Translated msgid "Top surface expansion" msgstr "Expansão da superfície superior" -# AI Translated msgid "" "Expands the top surfaces by this distance to connect distinct top surfaces and fill gaps.\n" "Useful for cases where the top surface is interrupted by a raised feature, such as text on a plane. Expanding it removes the holes beneath these features and creates a continuous path with a better finish for printing on top. The expansion is applied to the original top surface, before any other processing such as bridging or overhang detection." @@ -13290,11 +13277,9 @@ msgstr "" "Expande as superfícies superiores por esta distância para conectar superfícies superiores distintas e preencher lacunas.\n" "Útil para casos em que a superfície superior é interrompida por um recurso elevado, como um texto sobre um plano. Expandi-la remove os buracos sob esses recursos e cria um caminho contínuo com melhor acabamento para imprimir por cima. A expansão é aplicada à superfície superior original, antes de qualquer outro processamento, como detecção de ponte ou de saliência." -# AI Translated msgid "Top expansion wall margin" msgstr "Margem de parede da expansão superior" -# AI Translated msgid "" "Using “Top surface expansion” may cause a surface that did not previously touch the model's outer walls to now do so.\n" "This can cause contraction marks (such as the hull line) on the outer walls.\n" @@ -13304,11 +13289,9 @@ msgstr "" "Isso pode causar marcas de contração (como a linha do casco) nas paredes externas.\n" "Ao adicionar uma pequena margem, essa contração não ocorrerá diretamente nas paredes, evitando assim uma marca visível." -# AI Translated msgid "Top expansion direction" msgstr "Direção da expansão superior" -# AI Translated msgid "" "Direction in which the top surface expansion grows.\n" " - Inward grows into the holes and gaps left by features rising from the middle of a top surface.\n" @@ -13335,11 +13318,9 @@ msgstr "Padrão de superfície inferior" msgid "This is the line pattern of bottom surface infill, not including bridge infill." msgstr "Este é o padrão de linha do preenchimento da superfície inferior, não incluindo o preenchimento de ponte." -# AI Translated msgid "Bottom surface density" msgstr "Densidade da superfície inferior" -# AI Translated msgid "" "Density of the bottom surface layer. Intended for aesthetic or functional purposes, not to fix issues such as over-extrusion.\n" "WARNING: Lowering this value may negatively affect bed adhesion." @@ -13347,31 +13328,27 @@ msgstr "" "Densidade da camada da superfície inferior. Destinada a fins estéticos ou funcionais, não a corrigir problemas como sobre-extrusão.\n" "AVISO: reduzir este valor pode afetar negativamente a aderência à mesa." -# AI Translated msgid "Top surface fill order" msgstr "Ordem de preenchimento da superfície superior" -# AI Translated msgid "" "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Direção em que as superfícies superiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral Octograma).\n" +"Direção em que as superfícies superiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral de Octograma).\n" "Para fora começa no centro da superfície, de modo que qualquer excesso de material seja empurrado em direção à borda, onde é menos visível. Para dentro começa na borda e termina com as curvas fechadas no centro.\n" "O padrão usa a ordenação de caminho mais curto, que pode seguir em qualquer direção." -# AI Translated msgid "Bottom surface fill order" msgstr "Ordem de preenchimento da superfície inferior" -# AI Translated msgid "" "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Direção em que as superfícies inferiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral Octograma).\n" +"Direção em que as superfícies inferiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral de Octograma).\n" "Para dentro começa cada superfície com as curvas externas mais largas, o que melhora a aderência da primeira camada em mesas onde as curvas fechadas no centro podem não aderir. Para fora começa no centro, empurrando qualquer excesso de material em direção à borda.\n" "O padrão usa a ordenação de caminho mais curto, que pode seguir em qualquer direção." @@ -13399,19 +13376,15 @@ msgstr "Limiar de pequenos perímetros" msgid "This sets the threshold for small perimeter length. Default threshold is 0mm." msgstr "Isso define o limiar para o comprimento do perímetro pequeno. O limiar padrão é 0 mm." -# AI Translated msgid "Small support perimeters" msgstr "Pequenos perímetros de suporte" -# AI Translated msgid "Same as \"Small perimeters\", but for supports. This separate setting will affect the speed of support for areas <= `small_support_perimeter_threshold`. If expressed as a percentage (for example: 80%), it will be calculated on the support or support interface speed setting above. Set to zero for auto." msgstr "Igual a \"Pequenos perímetros\", mas para suportes. Esta configuração separada afetará a velocidade do suporte para áreas <= `small_support_perimeter_threshold`. Se expressa como uma porcentagem (por exemplo: 80%), será calculada com base na configuração de velocidade de suporte ou de interface de suporte acima. Defina como zero para automático." -# AI Translated msgid "Small support perimeters threshold" -msgstr "Limite de pequenos perímetros de suporte" +msgstr "Limiar de pequenos perímetros de suporte" -# AI Translated msgid "This sets the threshold for small support perimeter length. The default threshold is 0mm." msgstr "Isto define o limite para o comprimento de pequenos perímetros de suporte. O limite padrão é 0mm." @@ -13603,7 +13576,6 @@ msgstr "" msgid "Enable adaptive pressure advance within features (beta)" msgstr "Habilitar pressure advance adaptativo nos recursos (beta)" -# AI Translated msgid "" "Enable adaptive PA whenever there are flow changes in a feature, such as line width changes in a corner or overhangs.\n" "\n" @@ -13635,10 +13607,10 @@ msgid "Default line width if other line widths are set to 0. If expressed as a % msgstr "Largura de linha padrão se outras larguras de linha estiverem definidas como 0. Se expresso como %, será calculado sobre o diâmetro do bico." msgid "Keep fan always on" -msgstr "Manter o ventilador sempre ligado" +msgstr "Manter a ventoinha sempre ligado" msgid "Enabling this setting means that part cooling fan will never stop entirely and will instead run at least at minimum speed to reduce the frequency of starting and stopping." -msgstr "Habilitar esta configuração significa que o ventilador de resfriamento da peça nunca será desligado completamente e funcionará pelo menos na velocidade mínima para reduzir a frequência de inícios e paradas." +msgstr "Habilitar esta configuração significa que a ventoinha de resfriamento da peça nunca será desligado completamente e funcionará pelo menos na velocidade mínima para reduzir a frequência de inícios e paradas." msgid "Don't slow down outer walls" msgstr "Não desacelerar as paredes externas" @@ -13658,7 +13630,7 @@ msgid "Layer time" msgstr "Tempo da camada" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." -msgstr "O ventilador de resfriamento de peças será ativado para camadas cujo tempo estimado seja mais curto que esse valor. A velocidade do ventilador é interpolada entre as velocidades mínima e máxima do ventilador de acordo com o tempo de impressão da camada." +msgstr "A ventoinha de resfriamento de peças será ativado para camadas cujo tempo estimado seja mais curto que esse valor. A velocidade da ventoinha é interpolada entre as velocidades mínima e máxima da ventoinha de acordo com o tempo de impressão da camada." msgid "s" msgstr "s" @@ -13706,7 +13678,6 @@ msgstr "Temperatura de purga" msgid "Temperature when flushing filament. 0 indicates the upper bound of the recommended nozzle temperature range." msgstr "Temperatura ao purgar filamento. 0 indica o limite superior da faixa de temperatura recomendada para o bico." -# AI Translated msgid "Flush temperature used in fast purge mode." msgstr "Temperatura de purga usada no modo de purga rápida." @@ -13972,11 +13943,9 @@ msgstr "Filamento imprimível" msgid "The filament is printable in extruder." msgstr "O filamento é imprimível na extrusora." -# AI Translated msgid "Filament-extruder compatibility" msgstr "Compatibilidade filamento-extrusora" -# AI Translated msgid "A single 32-bit int encoding the compatibility level of a filament across all extruders (up to 10). Every 3 bits represent one extruder (bits [3*i, 3*i+2] for extruder i). 0: printable, 1: error, 2: critical warning, 3: warning, 4-7: reserved." msgstr "Um único inteiro de 32 bits que codifica o nível de compatibilidade de um filamento em todas as extrusoras (até 10). Cada 3 bits representam uma extrusora (bits [3*i, 3*i+2] para a extrusora i). 0: imprimível, 1: erro, 2: aviso crítico, 3: aviso, 4-7: reservado." @@ -14016,11 +13985,9 @@ msgstr "Direção do preenchimento sólido" msgid "Angle for solid infill pattern, which controls the start or main direction of line." msgstr "Ângulo para padrão de preenchimento sólido, que controla a direção inicial ou principal da linha." -# AI Translated msgid "Top layer direction" msgstr "Direção da camada superior" -# AI Translated msgid "" "Fixed angle for the top solid infill and ironing lines.\n" "Set to -1 to follow the default solid infill direction." @@ -14028,11 +13995,9 @@ msgstr "" "Ângulo fixo para o preenchimento sólido superior e as linhas de alisamento.\n" "Defina como -1 para seguir a direção padrão do preenchimento sólido." -# AI Translated msgid "Bottom layer direction" msgstr "Direção da camada inferior" -# AI Translated msgid "" "Fixed angle for the bottom solid infill lines.\n" "Set to -1 to follow the default solid infill direction." @@ -14047,11 +14012,9 @@ msgstr "Densidade do preenchimento esparso" msgid "Density of internal sparse infill, 100% turns all sparse infill into solid infill and internal solid infill pattern will be used." msgstr "Densidade do preenchimento esparso interno, 100% transforma todo o preenchimento esparso em preenchimento sólido e será usado o padrão de preenchimento sólido interno." -# AI Translated msgid "Align directions to model" msgstr "Alinhar direções ao modelo" -# AI Translated msgid "" "Aligns infill, bridge, ironing, and top/bottom surface directions to follow the model's orientation on the build plate.\n" "When enabled, these directions rotate together with the model so the printed features keep their intended orientation relative to the part, preserving optimal strength and surface characteristics regardless of how the model is placed." @@ -14071,11 +14034,9 @@ msgstr "Multilinhas de Preenchimento" msgid "Using multiple lines for the infill pattern, if supported by infill pattern." msgstr "Usar múltiplas linhas para o padrão de preenchimento, se suportado pelo padrão de preenchimento." -# AI Translated msgid "Z-buckling bias optimization (experimental)" msgstr "Otimização de tendência à flambagem em Z (experimental)" -# AI Translated #, no-c-format, no-boost-format msgid "Tightens the gyroid wave along the Z (vertical) axis at low infill density to shorten the effective vertical column length and improve Z-axis compression buckling resistance. Filament use is preserved. No effect at ~30% sparse infill density and above. Only applies when Sparse infill pattern is set to Gyroid." msgstr "Aperta a onda giroide ao longo do eixo Z (vertical) em baixa densidade de preenchimento para encurtar o comprimento efetivo da coluna vertical e melhorar a resistência à flambagem por compressão no eixo Z. O uso de filamento é preservado. Sem efeito em densidade de preenchimento esparso de ~30% ou mais. Aplica-se apenas quando o padrão de Preenchimento esparso está definido como Giroide." @@ -14198,13 +14159,12 @@ msgstr "Jerk para primeira camada." msgid "Jerk for travel." msgstr "Jerk para deslocamento." -# AI Translated msgid "" "Travel jerk of first layer.\n" "The percentage value is relative to Travel Jerk." msgstr "" "Jerk de deslocamento da primeira camada.\n" -"O valor percentual é relativo ao Jerk de deslocamento." +"O valor percentual é relativo ao Jerk de Deslocamento." msgid "Line width of the first layer. If expressed as a %, it will be computed over the nozzle diameter." msgstr "Largura da linha da primeira camada. Se expresso como uma %, será calculado sobre o diâmetro do bico." @@ -14243,10 +14203,10 @@ msgid "Nozzle temperature for printing the first layer with this filament" msgstr "Temperatura do bico para imprimir a primeira camada com este filamento" msgid "Full fan speed at layer" -msgstr "Velocidade total do ventilador na camada" +msgstr "Velocidade total da ventoinha na camada" msgid "Fan speed will be ramped up linearly from zero at layer \"close_fan_the_first_x_layers\" to maximum at layer \"full_fan_speed_layer\". \"full_fan_speed_layer\" will be ignored if lower than \"close_fan_the_first_x_layers\", in which case the fan will be running at maximum allowed speed at layer \"close_fan_the_first_x_layers\" + 1." -msgstr "A velocidade do ventilador aumentará linearmente de zero na camada \"close_fan_the_first_x_layers\" para o máximo na camada \"full_fan_speed_layer\". \"full_fan_speed_layer\" será ignorado se for menor que \"close_fan_the_first_x_layers\", caso em que o ventilador funcionará na velocidade máxima permitida na camada \"close_fan_the_first_x_layers\" + 1." +msgstr "A velocidade da ventoinha aumentará linearmente de zero na camada \"close_fan_the_first_x_layers\" para o máximo na camada \"full_fan_speed_layer\". \"full_fan_speed_layer\" será ignorado se for menor que \"close_fan_the_first_x_layers\", caso em que a ventoinha funcionará na velocidade máxima permitida na camada \"close_fan_the_first_x_layers\" + 1." msgid "layer" msgstr "camada" @@ -14254,7 +14214,6 @@ msgstr "camada" msgid "First layer fan speed" msgstr "Velocidade da ventoinha na primeira camada" -# AI Translated msgid "" "Sets an exact fan speed for the first layer, overriding all other cooling settings. Useful for protecting 3D-printed toolhead parts (e.g. Voron-style ABS/ASA ducts) from a hot bed. A small amount of airflow cools the ducts down, without using full cooling that may in certain conditions hurt first-layer adhesion.\n" "From the second layer onwards, normal cooling resumes.\n" @@ -14262,44 +14221,44 @@ msgid "" "Only available when \"No cooling for the first\" is 0.\n" "Set to -1 to disable it." msgstr "" -"Define uma velocidade exata do ventilador para a primeira camada, substituindo todas as outras configurações de resfriamento. Útil para proteger peças impressas em 3D da cabeça da ferramenta (por exemplo, dutos ABS/ASA no estilo Voron) de uma mesa quente. Uma pequena quantidade de fluxo de ar resfria os dutos, sem usar o resfriamento total que pode, em certas condições, prejudicar a aderência da primeira camada.\n" +"Define uma velocidade exata da ventoinha para a primeira camada, substituindo todas as outras configurações de resfriamento. Útil para proteger peças impressas em 3D da cabeça da ferramenta (por exemplo, dutos ABS/ASA no estilo Voron) de uma mesa aquecida. Uma pequena quantidade de fluxo de ar resfria os dutos, sem usar o resfriamento total que pode, em certas condições, prejudicar a aderência da primeira camada.\n" "A partir da segunda camada, o resfriamento normal é retomado.\n" -"Se \"Velocidade total do ventilador na camada\" também estiver definida, o ventilador aumenta suavemente deste valor na primeira camada até o seu alvo na camada escolhida.\n" +"Se \"Velocidade total da ventoinha na camada\" também estiver definida, a ventoinha aumenta suavemente deste valor na primeira camada até o seu alvo na camada escolhida.\n" "Disponível apenas quando \"Sem resfriamento nas primeiras\" é 0.\n" "Defina como -1 para desativá-la." msgid "Support interface fan speed" -msgstr "Velocidade do ventilador para interface de suporte" +msgstr "Velocidade da ventoinha para interface de suporte" msgid "" "This part cooling fan speed is applied when printing support interfaces. Setting this parameter to a higher than regular speed reduces the layer binding strength between supports and the supported part, making them easier to separate.\n" "Set to -1 to disable it.\n" "This setting is overridden by disable_fan_first_layers." msgstr "" -"Esta velocidade do ventilador de resfriamento de peças é aplicada ao imprimir interfaces de suporte. Definir este parâmetro para uma velocidade maior que a normal reduz a força de adesão de camada entre os suportes e a peça suportada, tornando-os mais fáceis de separar.\n" +"Esta velocidade da ventoinha de resfriamento de peças é aplicada ao imprimir interfaces de suporte. Definir este parâmetro para uma velocidade maior que a normal reduz a força de adesão de camada entre os suportes e a peça suportada, tornando-os mais fáceis de separar.\n" "Defina como -1 para desabilitá-lo.\n" "Esta configuração é substituída por disable_fan_first_layers." msgid "Internal bridges fan speed" -msgstr "Velocidade do ventilador para pontes internas" +msgstr "Velocidade da ventoinha para pontes internas" msgid "" "The part cooling fan speed used for all internal bridges. Set to -1 to use the overhang fan speed settings instead.\n" "\n" "Reducing the internal bridges fan speed, compared to your regular fan speed, can help reduce part warping due to excessive cooling applied over a large surface for a prolonged period of time." msgstr "" -"A velocidade do ventilador de resfriamento de peças usada para todas as pontes internas. Defina como -1 para usar as configurações de velocidade do ventilador de sobreposição.\n" +"A velocidade da ventoinha de resfriamento de peças usada para todas as pontes internas. Defina como -1 para usar as configurações de velocidade da ventoinha de sobreposição.\n" "\n" -"Reduzir a velocidade do ventilador das pontes internas, em comparação com a velocidade normal do ventilador, pode ajudar a reduzir a deformação das peças devido ao resfriamento excessivo aplicado sobre uma grande superfície por um período prolongado de tempo." +"Reduzir a velocidade da ventoinha das pontes internas, em comparação com a velocidade normal da ventoinha, pode ajudar a reduzir a deformação das peças devido ao resfriamento excessivo aplicado sobre uma grande superfície por um período prolongado de tempo." msgid "Ironing fan speed" -msgstr "Velocidade do ventilador para alisamento" +msgstr "Velocidade da ventoinha para alisamento" msgid "" "This part cooling fan speed is applied when ironing. Setting this parameter to a lower than regular speed reduces possible nozzle clogging due to the low volumetric flow rate, making the interface smoother.\n" "Set to -1 to disable it." msgstr "" -"Esta velocidade do ventilador de resfriamento de peças é aplicada durante o alisamento. Definir este parâmetro para uma velocidade menor que a normal reduz a possibilidade de entupimento do bico devido ao baixa taxa de fluxo volumétrico, tornando a interface mais suave.\n" +"Esta velocidade da ventoinha de resfriamento de peças é aplicada durante o alisamento. Definir este parâmetro para uma velocidade menor que a normal reduz a possibilidade de entupimento do bico devido ao baixa taxa de fluxo volumétrico, tornando a interface mais suave.\n" "Defina como -1 para desabilitá-lo." msgid "Ironing flow" @@ -14584,7 +14543,7 @@ msgid "Best auto arranging position in range [0,1] w.r.t. bed shape." msgstr "Melhor posição de arranjo automático na faixa [0,1] em relação ao formato da mesa." msgid "Enable this option if machine has auxiliary part cooling fan. G-code command: M106 P2 S(0-255)." -msgstr "Habilitar esta opção se a máquina tiver ventilador auxiliar de resfriamento de peças. Comando G-code: M106 P2 S(0-255)." +msgstr "Habilitar esta opção se a máquina tiver ventoinha auxiliar de resfriamento de peças. Comando G-code: M106 P2 S(0-255)." msgid "Fan direction" msgstr "Direção da ventoinha" @@ -14592,7 +14551,6 @@ msgstr "Direção da ventoinha" msgid "Cooling fan direction of the printer" msgstr "Direção da ventoinha de resfriamento da impressora" -# AI Translated msgid "Both" msgstr "Ambos" @@ -14602,9 +14560,9 @@ msgid "" "It won't move fan commands into the start G-code if the 'only custom start G-code' is activated.\n" "Use 0 to deactivate." msgstr "" -"Ativar o ventilador este número de segundos antes do seu tempo de início alvo (você pode usar frações de segundos). Ele assume aceleração infinita para esta estimativa de tempo e levará em conta apenas os movimentos G1 e G0 (o ajuste de arco não é suportado).\n" -"Não moverá G-code de comandos do ventilador personalizados (eles funcionam como uma espécie de 'barreira').\n" -"Não moverá comandos do ventilador para o início do G-code se 'apenas G-code de início personalizado' estiver ativo.\n" +"Ativar a ventoinha este número de segundos antes do seu tempo de início alvo (você pode usar frações de segundos). Ele assume aceleração infinita para esta estimativa de tempo e levará em conta apenas os movimentos G1 e G0 (o ajuste de arco não é suportado).\n" +"Não moverá G-code de comandos da ventoinha personalizados (eles funcionam como uma espécie de 'barreira').\n" +"Não moverá comandos da ventoinha para o início do G-code se 'apenas G-code de início personalizado' estiver ativo.\n" "Use 0 para desativar." msgid "Only overhangs" @@ -14614,15 +14572,15 @@ msgid "Will only take into account the delay for the cooling of overhangs." msgstr "Levará em conta apenas o atraso para o resfriamento das saliências." msgid "Fan kick-start time" -msgstr "Tempo de inicialização do ventilador" +msgstr "Tempo de inicialização da ventoinha" msgid "" "Emit a max fan speed command for this amount of seconds before reducing to target speed to kick-start the cooling fan.\n" "This is useful for fans where a low PWM/power may be insufficient to get the fan started spinning from a stop, or to get the fan up to speed faster.\n" "Set to 0 to deactivate." msgstr "" -"Emita um comando de velocidade máxima do ventilador por esta quantidade de segundos antes de reduzir para a velocidade alvo para iniciar o ventilador de resfriamento.\n" -"Isto é útil para ventiladores onde um baixo PWM/potência pode ser insuficiente para fazer o ventilador começar a girar a partir de uma parada, ou para fazer o ventilador alcançar a velocidade mais rapidamente.\n" +"Emita um comando de velocidade máxima da ventoinha por esta quantidade de segundos antes de reduzir para a velocidade alvo para iniciar a ventoinha de resfriamento.\n" +"Isto é útil para ventoinhas onde um baixo PWM/potência pode ser insuficiente para fazer a ventoinha começar a girar a partir de uma parada, ou para fazer a ventoinha alcançar a velocidade mais rapidamente.\n" "Defina como 0 para desativar." msgid "Minimum non-zero part cooling fan speed" @@ -14830,19 +14788,15 @@ msgstr "Ângulo de saliência do preenchimento" msgid "The angle of the infill angled lines. 60° will result in a pure honeycomb." msgstr "O ângulo das linhas de preenchimento. 60° resultará em um favo de mel puro." -# AI Translated msgid "Lightning overhang angle" -msgstr "Ângulo de saliência Relâmpago" +msgstr "Ângulo de saliência de Relâmpago" -# AI Translated msgid "Maximum overhang angle for Lightning infill support propagation." msgstr "Ângulo máximo de saliência para a propagação de suporte do preenchimento Relâmpago." -# AI Translated msgid "Prune angle" msgstr "Ângulo de poda" -# AI Translated msgid "" "Controls how aggressively short or unsupported Lightning branches are pruned.\n" "This angle is converted internally to a per-layer distance." @@ -14850,11 +14804,9 @@ msgstr "" "Controla a agressividade com que os ramos Relâmpago curtos ou sem suporte são podados.\n" "Este ângulo é convertido internamente em uma distância por camada." -# AI Translated msgid "Straightening angle" msgstr "Ângulo de retificação" -# AI Translated msgid "Maximum straightening angle used to simplify Lightning branches." msgstr "Ângulo máximo de retificação usado para simplificar os ramos Relâmpago." @@ -15205,7 +15157,7 @@ msgstr "Força máxima do eixo Y" msgid "The allowed maximum output force of Y axis" msgstr "A força máxima de saída permitida do eixo Y" -# AI Translated +#, fuzzy msgid "N" msgstr "N" @@ -15215,6 +15167,7 @@ msgstr "Massa da mesa do eixo Y" msgid "The machine bed mass load of Y axis" msgstr "A carga de massa da mesa do equipamento no eixo Y" +#, fuzzy msgid "g" msgstr "G" @@ -15369,7 +15322,7 @@ msgstr "" "Para desativar o modelador de entrada, use o tipo Desativar." msgid "The part cooling fan speed may be increased when auto cooling is enabled. This is the maximum speed for the part cooling fan." -msgstr "A velocidade do ventilador de resfriamento de peças pode ser aumentada quando o resfriamento automático está habilitado. Este é o limite máximo de velocidade do ventilador de resfriamento de peças." +msgstr "A velocidade da ventoinha de resfriamento de peças pode ser aumentada quando o resfriamento automático está habilitado. Este é o limite máximo de velocidade da ventoinha de resfriamento de peças." msgid "The highest printable layer height for the extruder. Used to limit the maximum layer height when enable adaptive layer height." msgstr "A maior altura de camada imprimível para a extrusora. Usada para limitar a altura máxima da camada quando a altura da camada adaptativa está ativada." @@ -15432,31 +15385,31 @@ msgid "Applies extrusion rate smoothing only on external perimeters and overhang msgstr "Aplica suavização de taxa de extrusão somente em perímetros externos e saliências. Isso pode ajudar a reduzir artefatos devido a transições de velocidade bruscas em saliências visíveis externamente sem impactar a velocidade de impressão de recursos que não serão visíveis ao usuário." msgid "Minimum speed for part cooling fan." -msgstr "Velocidade mínima para o ventilador de resfriamento de peças." +msgstr "Velocidade mínima para a ventoinha de resfriamento de peças." msgid "" "Speed of auxiliary part cooling fan. Auxiliary fan will run at this speed during printing except the first several layers which is defined by no cooling layers.\n" "Please enable auxiliary_fan in printer settings to use this feature. G-code command: M106 P2 S(0-255)" msgstr "" -"Velocidade do ventilador auxiliar de resfriamento de peças. O ventilador auxiliar funcionará nesta velocidade durante a impressão, exceto nas primeiras camadas, que são definidas por camadas sem resfriamento.\n" +"Velocidade da ventoinha auxiliar de resfriamento de peças. A ventoinha auxiliar funcionará nesta velocidade durante a impressão, exceto nas primeiras camadas, que são definidas por camadas sem resfriamento.\n" "\n" -"Por favor, habilite o ventilador auxiliar nas configurações da impressora para usar esta função. Comando G-code: M106 P2 S(0-255)" +"Por favor, habilite a ventoinha auxiliar nas configurações da impressora para usar esta função. Comando G-code: M106 P2 S(0-255)" msgid "For the first" msgstr "Para as primeiras" msgid "Set special auxiliary cooling fan for the first certain layers." -msgstr "Definir um ventilador auxiliar de resfriamento específico para as primeiras camadas." +msgstr "Definir uma ventoinha auxiliar de resfriamento específico para as primeiras camadas." msgid "" "Auxiliary fan speed will be ramped up linearly from layer \"For the first\" to maximum at layer \"Full fan speed at layer\".\n" "\"Full fan speed at layer\" will be ignored if lower than \"For the first\", in which case the fan will run at maximum allowed speed at layer \"For the first\" + 1." msgstr "" -"A velocidade do ventilador auxiliar aumentará linearmente da camada \"Para as primeiras\" até o máximo na camada \"Velocidade total do ventilador na camada\".\n" -"A \"Velocidade total do ventilador na camada\" será ignorada se for menor que \"Para as primeiras\", caso em que o ventilador funcionará na velocidade máxima permitida na camada \"Para as primeiras\" + 1." +"A velocidade da ventoinha auxiliar aumentará linearmente da camada \"Para as primeiras\" até o máximo na camada \"Velocidade total da ventoinha na camada\".\n" +"A \"Velocidade total da ventoinha na camada\" será ignorada se for menor que \"Para as primeiras\", caso em que a ventoinha funcionará na velocidade máxima permitida na camada \"Para as primeiras\" + 1." msgid "Special auxiliary cooling fan speed, effective only for the first x layers." -msgstr "Velocidade especial do ventilador de resfriamento auxiliar, efetiva apenas para as primeiras x camadas." +msgstr "Velocidade especial da ventoinha de resfriamento auxiliar, efetiva apenas para as primeiras x camadas." msgid "The lowest printable layer height for the extruder. Used to limit the minimum layer height when enable adaptive layer height." msgstr "A menor altura de camada imprimível para a extrusora. Usada para limitar a altura mínima da camada ao habilitar a altura de camada adaptativa." @@ -15621,11 +15574,9 @@ msgstr "Este G-code é inserido quando a função de extrusão é trocada. Ele msgid "Plugins Used" msgstr "Plugins Utilizados" -# AI Translated msgid "Plugin capabilities referenced by this preset, stored as name;uuid;capability." -msgstr "Recursos de plugin referenciados por esta predefinição, armazenados como name;uuid;capability." +msgstr "Capacidades de plugin referenciados por esta predefinição, armazenados como name;uuid;capability." -# AI Translated msgid "Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, including a final G-code post-processing step. Research/experimental." msgstr "Plugin(s) Python invocado(s) em cada etapa do pipeline de fatiamento para ler e modificar dados intermediários de fatiamento, incluindo uma etapa final de pós-processamento do G-code. Pesquisa/experimental." @@ -16243,11 +16194,9 @@ msgstr "Preparar todas as extrusoras de impressão" msgid "If enabled, all printing extruders will be primed at the front edge of the print bed at the start of the print." msgstr "Se ativado, todos as extrusoras de impressão serão preparados na borda frontal da mesa de impressão no início da impressão." -# AI Translated msgid "Toolchange ordering" msgstr "Ordenação de troca de ferramenta" -# AI Translated msgid "" "Determines the order of tool changes on each layer.\n" "- Default: Starts with the last used extruder to minimize tool changes.\n" @@ -16257,7 +16206,6 @@ msgstr "" "- Padrão: começa com a última extrusora usada para minimizar as trocas de ferramenta.\n" "- Cíclico: usa uma sequência fixa de ferramentas em cada camada. Isso sacrifica a velocidade em prol de uma melhor qualidade de superfície, pois as trocas de ferramenta extras dão mais tempo para as camadas resfriarem." -# AI Translated msgid "Cyclic" msgstr "Cíclico" @@ -16638,7 +16586,6 @@ msgstr "" "\n" "Se habilitado, este parâmetro também define uma variável G-code chamada chamber_temperature, que pode ser usada para passar a temperatura desejada da câmara para sua macro de início de impressão ou uma macro de absorção de calor como esta: PRINT_START (outras variáveis) CHAMBER_TEMP=[chamber_temperature]. Isso pode ser útil se sua impressora não suportar comandos M141/M191 ou se você desejar lidar com a absorção de calor na macro de início de impressão se nenhum aquecedor de câmara ativo estiver instalado." -# AI Translated msgid "" "This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n" "\n" @@ -16646,11 +16593,11 @@ msgid "" "\n" "Unlike the \"Target\" chamber temperature, this option does not emit any M141/M191 commands; it only exposes the value to your custom G-code. It should not exceed the \"Target\" chamber temperature." msgstr "" -"Esta é a temperatura da câmara na qual a impressão deve começar, enquanto a câmara continua aquecendo em direção à temperatura da câmara \"Alvo\". Por exemplo, defina o Alvo como 60 e o Mínimo como 50 para começar a imprimir assim que a câmara atingir 50℃, sem esperar pelos 60℃ completos.\n" +"Esta é a temperatura da câmara na qual a impressão deve começar, enquanto a câmara continua aquecendo em direção à temperatura \"Alvo\" da câmara. Por exemplo, defina o Alvo como 60 e o Mínimo como 50 para começar a imprimir assim que a câmara atingir 50℃, sem esperar pelos 60℃ completos.\n" "\n" "Isso define uma variável de G-code chamada chamber_minimal_temperature, que pode ser passada para a sua macro de início de impressão ou uma macro de aquecimento prolongado, assim: PRINT_START (outras variáveis) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n" "\n" -"Ao contrário da temperatura da câmara \"Alvo\", esta opção não emite nenhum comando M141/M191; ela apenas expõe o valor ao seu G-code personalizado. Não deve exceder a temperatura da câmara \"Alvo\"." +"Ao contrário da temperatura \"Alvo\" da câmara, esta opção não emite nenhum comando M141/M191; ela apenas expõe o valor ao seu G-code personalizado. Não deve exceder a temperatura \"Alvo\" da câmara." msgid "Chamber minimal temperature" msgstr "Temperatura mínima da câmara" @@ -16694,20 +16641,18 @@ msgstr "Espessura da casca do topo" msgid "The number of top solid layers is increased when slicing if the thickness calculated by top shell layers is thinner than this value. This can avoid having too thin a shell when layer height is small. 0 means that this setting is disabled and thickness of top shell is determined simply by the number of top shell layers." msgstr "O número de camadas sólidas superiores é aumentado ao fatiar se a espessura calculada pelas camadas da casca do topo for menor do que este valor. Isso pode evitar que a casca seja muito fina quando a altura da camada é pequena. 0 significa que esta configuração está desativada e a espessura da casca do topo é determinada apenas pelo número de camadas da casca do topo." -# AI Translated msgid "Separated infills" msgstr "Preenchimentos separados" -# AI Translated msgid "" "Centers the internal infill of each part on itself, as if it were sliced on its own, instead of on the whole assembly. Parts that touch or overlap are treated as one body and share a center; separate parts (or distinct 3D objects) each get their own.\n" "Useful when an assembly groups several objects that should each keep a consistent, self-centered infill.\n" "Affects line and grid patterns and rotation-template infills.\n" "Patterns locked to global coordinates (Gyroid, Honeycomb, TPMS, ...) are unaffected." msgstr "" -"Centraliza o preenchimento interno de cada peça em si mesma, como se fosse fatiada isoladamente, em vez de no conjunto inteiro. Peças que se tocam ou se sobrepõem são tratadas como um único corpo e compartilham um centro; peças separadas (ou objetos 3D distintos) recebem cada uma o seu próprio.\n" +"Centraliza o preenchimento interno de cada peça em si mesmo, como se fosse fatiada isoladamente, em vez de no conjunto inteiro. Peças que se tocam ou se sobrepõem são tratadas como um único corpo e compartilham um centro; peças separadas (ou objetos 3D distintos) recebem cada uma o seu próprio.\n" "Útil quando um conjunto agrupa vários objetos que devem manter, cada um, um preenchimento consistente e autocentrado.\n" -"Afeta os padrões de linha e grade e os preenchimentos com modelo de rotação.\n" +"Afeta os padrões de linha e grade e os preenchimentos com gabarito de rotação.\n" "Os padrões fixados em coordenadas globais (Giroide, Favo de mel, TPMS, ...) não são afetados." msgid "Center surface pattern on" @@ -16776,11 +16721,9 @@ msgstr "Multiplicador de purga" msgid "The actual flushing volumes is equal to the flush multiplier value multiplied by the flushing volumes in the table." msgstr "Os volumes de purga reais são iguais ao multiplicador de purga multiplicado pelos volumes de purga na tabela." -# AI Translated msgid "Flush multiplier (Fast mode)" msgstr "Multiplicador de purga (Modo rápido)" -# AI Translated msgid "The flush multiplier used in fast purge mode." msgstr "O multiplicador de purga usado no modo de purga rápida." @@ -16790,13 +16733,12 @@ msgstr "Volume de preparo" msgid "This is the volume of material to prime the extruder with on the tower." msgstr "Este é o volume de material para preparar a extrusora na torre." -# AI Translated +#,fuzzy msgid "Prime volume mode" msgstr "Modo de volume de preparação" -# AI Translated msgid "Selects how the wipe-tower prime and flush volumes are computed on multi-extruder printers." -msgstr "Seleciona como os volumes de preparação e de purga da torre de purga são calculados em impressoras com várias extrusoras." +msgstr "Seleciona como os volumes de preparação e de purga da torre de purga são computados em impressoras com múltiplas extrusoras." msgid "Saving" msgstr "Salvando" @@ -17116,7 +17058,7 @@ msgid "The maximum volumetric speed for ramming before extruder change, where -1 msgstr "A velocidade volumétrica máxima para compactação antes da troca de extrusor, onde -1 significa usar a velocidade volumétrica máxima." msgid "To prevent oozing, the nozzle temperature will be cooled during ramming. Note: only a cooldown command and fan activation are triggered, reaching the target temperature is not guaranteed. 0 means disabled." -msgstr "Para evitar o gotejamento, a temperatura do bico será reduzida durante a compactação. Nota: somente um comando de resfriamento e ativação do ventilador são acionados, não sendo garantido o alcance da temperatura alvo. 0 significa desativado." +msgstr "Para evitar o gotejamento, a temperatura do bico será reduzida durante a compactação. Nota: somente um comando de resfriamento e ativação da ventoinha são acionados, não sendo garantido o alcance da temperatura alvo. 0 significa desativado." msgid "The maximum volumetric speed for ramming before a hotend change, where -1 means using the maximum volumetric speed." msgstr "A velocidade volumétrica máxima para compactação antes de uma troca de hotend, em que -1 significa usar a velocidade volumétrica máxima." @@ -20744,8 +20686,8 @@ msgid "" "Auxiliary fan\n" "Did you know that OrcaSlicer supports Auxiliary part cooling fan?" msgstr "" -"Ventilador auxiliar\n" -"Você sabia que o OrcaSlicer suporta ventilador auxiliar de resfriamento de peças?" +"Ventoinha auxiliar\n" +"Você sabia que o OrcaSlicer suporta ventoinha auxiliar de resfriamento de peças?" #: resources/data/hints.ini: [hint:Air filtration] msgid "" @@ -21939,7 +21881,7 @@ msgstr "" #~ msgstr "Pausado devido à perda do AMS" #~ msgid "Paused due to low speed of the heat break fan" -#~ msgstr "Pausado devido a baixa velocidade do ventilador do bloco de aquecimento" +#~ msgstr "Pausado devido a baixa velocidade da ventoinha do bloco de aquecimento" #~ msgid "Paused due to chamber temperature control error" #~ msgstr "Pausado devido a erro no controle de temperatura da câmara" @@ -22468,20 +22410,20 @@ msgstr "" #~ msgstr "Forçar resfriamento para saliências e pontes" #~ msgid "Enable this option to optimize part cooling fan speed for overhang and bridge to get better cooling" -#~ msgstr "Ative esta opção para otimizar a velocidade do ventilador de resfriamento de peças para saliência e ponte para obter melhor resfriamento" +#~ msgstr "Ative esta opção para otimizar a velocidade da ventoinha de resfriamento de peças para saliência e ponte para obter melhor resfriamento" #~ msgid "Fan speed for overhang" -#~ msgstr "Velocidade do ventilador para saliência" +#~ msgstr "Velocidade da ventoinha para saliência" #~ msgid "Force part cooling fan to be this speed when printing bridge or overhang wall which has large overhang degree. Forcing cooling for overhang and bridge can get better quality for these part" -#~ msgstr "Forçar o ventilador de resfriamento de peças a ser nesta velocidade ao imprimir ponte ou parede saliente que tenha um grande grau de saliência. Forçar o resfriamento para saliência e ponte pode obter melhor qualidade para estas partes" +#~ msgstr "Forçar a ventoinha de resfriamento de peças a ser nesta velocidade ao imprimir ponte ou parede saliente que tenha um grande grau de saliência. Forçar o resfriamento para saliência e ponte pode obter melhor qualidade para estas partes" #~ msgid "Cooling overhang threshold" #~ msgstr "Limiar de resfriamento de saliência" #, c-format #~ msgid "Force cooling fan to be specific speed when overhang degree of printed part exceeds this value. Expressed as percentage which indicates how much width of the line without support from lower layer. 0% means forcing cooling for all outer wall no matter how much overhang degree" -#~ msgstr "Forçar o ventilador de resfriamento a ser uma velocidade específica quando o grau de saliência das peças impressa excede este valor. Expresso como porcentagem, que indica quanto da largura da linha sem suporte da camada inferior. Zero significa forçar o resfriamento para toda a parede externa, não importa quanto seja o grau de saliência" +#~ msgstr "Forçar a ventoinha de resfriamento a ser uma velocidade específica quando o grau de saliência das peças impressa excede este valor. Expresso como porcentagem, que indica quanto da largura da linha sem suporte da camada inferior. Zero significa forçar o resfriamento para toda a parede externa, não importa quanto seja o grau de saliência" #~ msgid "Density of external bridges. 100% means solid bridge. Default is 100%." #~ msgstr "Densidade de pontes externas. 100% significa ponte sólida. O padrão é 100%." diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index 467b3c355b..63d1e5bc75 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -4,7 +4,7 @@ msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-07-29 17:40-0300\n" -"PO-Revision-Date: 2026-08-01 20:32+0300\n" +"PO-Revision-Date: 2026-08-04 19:36+0300\n" "Last-Translator: GlauTech\n" "Language-Team: \n" "Language: tr\n" @@ -738,9 +738,8 @@ msgstr "Sabit adım sürükleme" msgid "Context Menu" msgstr "Bağlam Menüsü" -# AI Translated msgid "Toggle Auto-Drop" -msgstr "Otomatik Bırakmayı Aç/Kapat" +msgstr "Otomatik düşürmeyi aç / kapat" msgid "Single sided scaling" msgstr "Tek taraflı ölçekleme" @@ -791,9 +790,8 @@ msgstr "Nesne" msgid "Part" msgstr "Parça" -# AI Translated msgid "Relative" -msgstr "Göreli" +msgstr "Göreceli" # AI Translated msgid "Coordinate system used for transform actions." @@ -2306,7 +2304,7 @@ msgid "new or open project file is not allowed during the slicing process!" msgstr "dilimleme işlemi sırasında yeni veya açık proje dosyasına izin verilmez!" msgid "Open Project" -msgstr "Projeyi Aç" +msgstr "Projeyi aç" msgid "The version of Orca Slicer is too low and needs to be updated to the latest version before it can be used normally." msgstr "Orca Slicer'ın sürümü çok düşük ve normal şekilde kullanılabilmesi için en son sürüme güncellenmesi gerekiyor." @@ -2734,16 +2732,15 @@ msgstr "Simit" msgid "Orca Cube" msgstr "Orca Küpü" -# AI Translated msgid "OrcaSliced Combo" -msgstr "OrcaSliced Combo" +msgstr "Orca Dilimleme Paketi" # AI Translated msgid "Orca Badge" msgstr "Orca Rozeti" msgid "Orca Tolerance Test" -msgstr "Orca tolerans testi" +msgstr "Orca Tolerans Testi" msgid "3DBenchy" msgstr "3DBenchy" @@ -2807,7 +2804,7 @@ msgid "Set as Individual Objects" msgstr "Bireysel nesneler olarak ayarla" msgid "Fill bed with copies" -msgstr "Yatağı kopyalarla doldurun" +msgstr "Tablayı kopyalarla doldur" msgid "Fill the remaining area of bed with copies of the selected object" msgstr "Yatağın kalan alanını seçilen nesnenin kopyalarıyla doldurun" @@ -2815,9 +2812,8 @@ msgstr "Yatağın kalan alanını seçilen nesnenin kopyalarıyla doldurun" msgid "Printable" msgstr "Yazdırılabilir" -# AI Translated msgid "Auto Drop" -msgstr "Otomatik Bırakma" +msgstr "Otomatik düşür" # AI Translated msgid "Automatically drops the selected object to the build plate." @@ -2967,7 +2963,7 @@ msgid "Add Models" msgstr "Model ekle" msgid "Show Labels" -msgstr "Etiketleri Göster" +msgstr "Etiketleri göster" msgid "To Objects" msgstr "Nesnelere" @@ -3009,7 +3005,7 @@ msgid "Select all objects on the current plate" msgstr "Mevcut plakadaki tüm nesneleri seç" msgid "Select All Plates" -msgstr "Tüm Plakaları Seç" +msgstr "Tüm plakaları seç" msgid "Select all objects on all plates" msgstr "Tüm plakalardaki tüm nesneleri seç" @@ -3045,13 +3041,13 @@ msgid "Remove the selected plate" msgstr "Seçilen plakayı kaldır" msgid "Add instance" -msgstr "Örnek ekle" +msgstr "Kopya ekle" msgid "Add one more instance of the selected object" msgstr "Seçilen nesnenin bir örneğini daha ekle" msgid "Remove instance" -msgstr "Örneği kaldır" +msgstr "Kopyayı kaldır" msgid "Remove one instance of the selected object" msgstr "Seçilen nesnenin bir örneğini kaldır" @@ -3060,10 +3056,10 @@ msgid "Set number of instances" msgstr "Örnek sayısını ayarlayın" msgid "Change the number of instances of the selected object" -msgstr "Seçilen nesnenin örnek sayısını değiştirme" +msgstr "Seçilen nesnenin kopya sayısını değiştirme" msgid "Fill bed with instances" -msgstr "Yatağı örneklerle doldurun" +msgstr "Tablayı kopyalarla doldur" msgid "Fill the remaining area of bed with instances of the selected object" msgstr "Yatağın kalan alanını seçilen nesnenin örnekleriyle doldurun" @@ -3075,7 +3071,7 @@ msgid "Simplify Model" msgstr "Modeli basitleştir" msgid "Subdivision mesh" -msgstr "Alt bölüm ağı" +msgstr "Poligon artırma" msgid "(Lost color)" msgstr "(Renk kaybı)" @@ -3090,10 +3086,10 @@ msgid "Edit Process Settings" msgstr "İşlem ayarlarını düzenle" msgid "Copy Process Settings" -msgstr "İşlem Ayarlarını Kopyala" +msgstr "İşlem ayarlarını kopyala" msgid "Paste Process Settings" -msgstr "İşlem Ayarlarını Yapıştır" +msgstr "İşlem ayarlarını yapıştır" msgid "Edit print parameters for a single object" msgstr "Tek bir nesne için yazdırma parametrelerini düzenleme" @@ -3478,7 +3474,7 @@ msgid "More" msgstr "Daha" msgid "Open Preferences" -msgstr "Tercihleri Aç" +msgstr "Tercihleri aç" msgid "Open next tip" msgstr "Sonraki ipucunu aç" @@ -5450,10 +5446,10 @@ msgid "Acceleration" msgstr "Hızlanma" msgid "Jerk" -msgstr "Jerk" +msgstr "Sarsıntı" msgid "Fan Speed" -msgstr "Fan hızı" +msgstr "Fan Hızı" msgid "Flow" msgstr "Akış" @@ -5468,7 +5464,7 @@ msgid "Layer Time" msgstr "Katman Süresi" msgid "Layer Time (log)" -msgstr "Katman Süresi (günlük)" +msgstr "Katman Süresi (log)" msgid "Pressure Advance" msgstr "Basınç İlerlemesi" @@ -5477,10 +5473,10 @@ msgid "Noop" msgstr "Hayır" msgid "Retract" -msgstr "Geri Çekme" +msgstr "Geri çekme" msgid "Unretract" -msgstr "İleri İtme" +msgstr "İleri itme" msgid "Seam" msgstr "Dikiş" @@ -5578,7 +5574,7 @@ msgid "Acceleration: " msgstr "İvme: " msgid "Jerk: " -msgstr "Jerk: " +msgstr "Sarsıntı: " msgid "PA: " msgstr "PA: " @@ -5608,7 +5604,7 @@ msgid "Actual speed profile" msgstr "Gerçek hız profili" msgid "Statistics of All Plates" -msgstr "Tüm Plakaların İstatistikleri" +msgstr "Tüm plakaların istatistikleri" msgid "Display" msgstr "Ekran" @@ -5708,7 +5704,7 @@ msgid "Acceleration (mm/s²)" msgstr "İvme (mm/s²)" msgid "Jerk (mm/s)" -msgstr "Jerk (mm/s)" +msgstr "Sarsıntı (mm/s)" msgid "Fan speed (%)" msgstr "Fan hızı (%)" @@ -5759,9 +5755,8 @@ msgstr "Normal mod" msgid "Total Filament" msgstr "Toplam filament" -# AI Translated msgid "Model Filament" -msgstr "Model Filamenti" +msgstr "Model filamenti" msgid "Prepare time" msgstr "Hazırlık süresi" @@ -6282,20 +6277,19 @@ msgid "Setup Wizard" msgstr "Kurulum sihirbazı" msgid "Show Configuration Folder" -msgstr "Yapılandırma Klasörünü Göster" +msgstr "Yapılandırma klasörünü göster" -# AI Translated msgid "Troubleshoot Center" -msgstr "Sorun Giderme Merkezi" +msgstr "Sorun giderme merkezi" msgid "Open Network Test" -msgstr "Ağ Testini Aç" +msgstr "Ağ testini aç" msgid "Show Tip of the Day" -msgstr "Günün İpucunu Göster" +msgstr "Günün ipucunu göster" msgid "Check for Updates" -msgstr "Güncellemeleri Kontrol Et" +msgstr "Güncellemeleri kontrol et" #, c-format, boost-format msgid "&About %s" @@ -6349,7 +6343,7 @@ msgid "Recent files" msgstr "Son dosyalar" msgid "Save Project" -msgstr "Projeyi Kaydet" +msgstr "Projeyi kaydet" msgid "Save current project to file" msgstr "Mevcut projeyi dosyaya kaydet" @@ -6415,13 +6409,13 @@ msgid "Export toolpaths as OBJ" msgstr "Takımyollarını OBJ olarak dışa aktar" msgid "Export Preset Bundle" -msgstr "Ön Ayar Paketini Dışa Aktar" +msgstr "Ön ayar paketini dışa aktar" msgid "Export current configuration to files" msgstr "Geçerli yapılandırmayı dosyalara aktar" msgid "Export" -msgstr "Dışa Aktar" +msgstr "Dışa aktar" msgid "Quit" msgstr "Çıkış" @@ -6478,13 +6472,13 @@ msgid "Deselects all objects" msgstr "Tüm nesnelerin seçimini kaldırır" msgid "Use Perspective View" -msgstr "Perspektif Görünüm" +msgstr "Perspektif görünüm" msgid "Use Orthogonal View" -msgstr "Ortogonal Görünüm" +msgstr "Ortogonal görünüm" msgid "Auto Perspective" -msgstr "Otomatik Perspektif" +msgstr "Otomatik perspektif" msgid "Automatically switch between orthographic and perspective when changing from top/bottom/side views." msgstr "Üst/Alt/Yan görünümler arasında geçiş yaparken ortografik ve perspektif arasında otomatik olarak geçiş yapın." @@ -6496,37 +6490,37 @@ msgid "Show G-code window in Preview scene." msgstr "Previce sahnesinde G-kodu penceresini göster." msgid "Show 3D Navigator" -msgstr "3D Gezgini Göster" +msgstr "3D gezgini göster" msgid "Show 3D navigator in Prepare and Preview scene." msgstr "Hazırlama ve Önizleme sahnesinde 3D gezgini göster." msgid "Show Gridlines" -msgstr "Kılavuz Çizgilerini Göster" +msgstr "Kılavuz çizgilerini göster" msgid "Show Gridlines on plate" msgstr "Kılavuz Çizgilerini plaka üzerinde göster" msgid "Reset Window Layout" -msgstr "Pencere Düzenini Sıfırla" +msgstr "Pencere düzenini sıfırla" msgid "Reset to default window layout" msgstr "Varsayılan pencere düzenine sıfırla" msgid "Show &Labels" -msgstr "Etiketleri Göster" +msgstr "Etiketleri göster" msgid "Show object labels in 3D scene." msgstr "3B sahnede nesne etiketlerini göster." msgid "Show &Overhang" -msgstr "Çıkıntıyı Göster" +msgstr "Çıkıntıyı göster" msgid "Show object overhang highlight in 3D scene." msgstr "3B sahnede nesne çıkıntısı vurgusunu göster." msgid "Show Selected Outline (beta)" -msgstr "Seçilen Taslağı Göster (Deneysel)" +msgstr "Seçilen taslağı göster (deneysel)" msgid "Show outline around selected object in 3D scene." msgstr "3D sahnede seçilen nesnenin etrafındaki ana hatları göster." @@ -6542,13 +6536,11 @@ msgstr "Düzen" msgid "View" msgstr "Görünüm" -# AI Translated msgid "Preset Bundle" -msgstr "Ön Ayar Paketi" +msgstr "Ön ayar paketi" -# AI Translated msgid "Sync Presets" -msgstr "Ön Ayarları Eşitle" +msgstr "Ön ayarları eşitle" # AI Translated msgid "Pull and apply the latest presets from OrcaCloud" @@ -6590,10 +6582,10 @@ msgid "Cornering calibration" msgstr "Viraj kalibrasyonu" msgid "Input Shaping Frequency" -msgstr "Input shaping Frekansı" +msgstr "Input shaping frekansı" msgid "Input Shaping Damping/zeta factor" -msgstr "Input shaping Sönümleme/zeta faktörü" +msgstr "Input shaping sönümleme/zeta faktörü" msgid "Input Shaping" msgstr "Input shaping" @@ -6601,9 +6593,8 @@ msgstr "Input shaping" msgid "VFA" msgstr "VFA" -# AI Translated msgid "Calibration Guide" -msgstr "Kalibrasyon Kılavuzu" +msgstr "Kalibrasyon kılavuzu" msgid "&Open G-code" msgstr "&G kodunu aç" @@ -8219,9 +8210,8 @@ msgstr "Bu dosyalar birden fazla parçadan oluşan tek bir nesne olarak mı yük msgid "An object with multiple parts was detected" msgstr "Birden fazla parçaya sahip nesne algılandı" -# AI Translated msgid "Auto-Drop" -msgstr "Otomatik Bırakma" +msgstr "Otomatik düşür" #, c-format, boost-format msgid "Connected printer is %s. It must match the project preset for printing.\n" @@ -8296,9 +8286,8 @@ msgstr "Seçilen nesne bölünemedi." msgid "Split to Objects" msgstr "Nesnelere Ayır" -# AI Translated msgid "Disable Auto-Drop to preserve Z positioning?\n" -msgstr "Z konumunu korumak için Otomatik Bırakma devre dışı bırakılsın mı?\n" +msgstr "Z konumunu korumak için Otomatik düşürme devre dışı bırakılsın mı?\n" # AI Translated msgid "Object with floating parts was detected" @@ -8433,7 +8422,7 @@ msgid "Creating a new project" msgstr "Yeni bir proje oluşturma" msgid "Load project" -msgstr "Projeyi Aç" +msgstr "Projeyi aç" msgid "" "Failed to save the project.\n" @@ -8869,10 +8858,10 @@ msgid "Current Association: " msgstr "Mevcut Bağlantı: " msgid "Current Instance" -msgstr "Mevcut Örnek" +msgstr "Mevcut Kopya" msgid "Current Instance Path: " -msgstr "Mevcut Örnek Yolu: " +msgstr "Mevcut Kopya Yolu: " msgid "General" msgstr "Genel" @@ -10708,7 +10697,7 @@ msgid "Reserved keywords found" msgstr "Ayrılmış anahtar kelimeler bulundu" msgid "Setting Overrides" -msgstr "Ayarların Üzerine Yazma" +msgstr "Ayarların Üzerine Yaz" msgid "Basic information" msgstr "Temel Bilgiler" @@ -12227,7 +12216,7 @@ msgid "Group error in manual mode. Please check nozzle count or regroup." msgstr "Elle modda gruplama hatası. Lütfen nozul sayısını denetleyin veya yeniden gruplayın." msgid "Internal Bridge" -msgstr "İç Köprü" +msgstr "İç köprü" msgid "undefined error" msgstr "bilinmeyen hata" @@ -12920,7 +12909,6 @@ msgstr "Çıkıntı bu belirtilen eşiği aştığında, soğutma fanını aşa msgid "External bridge infill direction" msgstr "Dış köprü dolgu yönü" -# AI Translated #, no-c-format, no-boost-format msgid "" "External Bridging angle override.\n" @@ -12937,14 +12925,13 @@ msgstr "" "Aksi hâlde verilen açı şuna göre kullanılır:\n" " - Mutlak koordinatlar\n" " - Mutlak koordinatlar + Model dönüşü: Yönleri modele hizala etkinse\n" -" - En uygun otomatik açı + bu değer: 'Göreli Köprü Açısı' etkinse\n" +" - En uygun otomatik açı + bu değer: ‘Göreceli Köprü Açısı' etkinse\n" "\n" "Sıfır mutlak açı için 180° kullanın." msgid "Internal bridge infill direction" msgstr "İç köprü dolgu yönü" -# AI Translated msgid "" "Internal Bridging angle override.\n" "If left to zero, the bridging angle will be calculated automatically for each specific bridge.\n" @@ -12960,13 +12947,12 @@ msgstr "" "Aksi hâlde verilen açı şuna göre kullanılır:\n" " - Mutlak koordinatlar\n" " - Mutlak koordinatlar + Model dönüşü: Yönleri modele hizala etkinse\n" -" - En uygun otomatik açı + bu değer: 'Göreli Köprü Açısı' etkinse\n" +" - En uygun otomatik açı + bu değer: 'Göreceli Köprü Açısı' etkinse\n" "\n" "Sıfır mutlak açı için 180° kullanın." -# AI Translated msgid "Relative bridge angle" -msgstr "Göreli köprü açısı" +msgstr "Göreceli köprü açısı" # AI Translated msgid "When enabled, the bridge angle values are added to the automatically calculated bridge direction instead of overriding it." @@ -13413,7 +13399,7 @@ msgstr "" "Not: Elde edilen değer ilk katman akış oranından etkilenmez." msgid "Brim follows compensated outline" -msgstr "Kenar telafi edilen taslağı takip ediyor" +msgstr "Kenar toleranslı dış sınırı takip etsin" # AI Translated msgid "" @@ -13724,13 +13710,13 @@ msgid "" msgstr "" "Dikey kabuk kalınlığını garanti etmek için eğimli yüzeylerin yakınına katı dolgu ekleyin (üst + alt katı katmanlar)\n" "Yok: Hiçbir yere katı dolgu eklenmez. Dikkat: Modelinizin eğimli yüzeyleri varsa bu seçeneği dikkatli kullanın.\n" -"Yalnızca kritik: Duvarlar için katı dolgu eklemekten kaçının\n" +"Kritik: Duvarlar için katı dolgu eklemekten kaçının\n" "Orta: Yalnızca çok eğimli yüzeyler için katı dolgu ekleyin\n" "Hepsi: Tüm uygun eğimli yüzeyler için katı dolgu ekleyin\n" "Varsayılan değer Tümü'dür." msgid "Critical Only" -msgstr "Yalnızca kritik" +msgstr "Kritik" msgid "Moderate" msgstr "Orta" @@ -14247,7 +14233,7 @@ msgid "By First filament" msgstr "İlk filamente göre" msgid "By Highest Temp" -msgstr "En Yüksek Sıcaklığa Göre" +msgstr "En yüksek sıcaklığa göre" msgid "Filament diameter is used to calculate extrusion variables in G-code, so it is important that this is accurate and precise." msgstr "Filament çapı, gcode'da ekstrüzyonu hesaplamak için kullanılır; bu nedenle önemlidir ve doğru olmalıdır." @@ -14685,10 +14671,10 @@ msgid "Marlin Firmware Junction Deviation (replaces the traditional XY Jerk sett msgstr "Marlin Firmware Köşe Sapması (geleneksel XY Sarsıntı ayarının yerini alır)" msgid "Jerk of outer walls." -msgstr "Dış duvar JERK değeri." +msgstr "Dış duvar sarsıntı değeri." msgid "Jerk of inner walls." -msgstr "İç duvarlar JERK değeri." +msgstr "İç duvarlar sarsıntı değeri." msgid "Jerk for top surface." msgstr "Üst yüzey için JERK değeri." @@ -15721,7 +15707,7 @@ msgid "" "If your Marlin 2 printer uses Classic Jerk set this value to 0.)" msgstr "" "Maksimum bağlantı sapması (M205 J, yalnızca Marlin Aygıt Yazılımı için JD > 0 ise geçerlidir)\n" -"Marlin 2 yazıcınız Classic Jerk kullanıyorsa bu değeri 0 olarak ayarlayın.)" +"Marlin 2 yazıcınız Classic sarsıntı kullanıyorsa bu değeri 0 olarak ayarlayın.)" msgid "Minimum speed for extruding" msgstr "Ekstrüzyon için minimum hız" @@ -16914,7 +16900,7 @@ msgid "This setting only generates supports that begin on the build plate." msgstr "Model yüzeyinde destek oluşturmayın, yalnızca baskı plakasında." msgid "Support critical regions only" -msgstr "Yalnızca kritik bölgeleri destekleyin" +msgstr "Kritik bölgeleri destekleyin" msgid "Only create support for critical regions including sharp tail, cantilever, etc." msgstr "Yalnızca keskin kuyruk, konsol vb. gibi kritik bölgeler için destek oluşturun." @@ -19288,13 +19274,13 @@ msgid "" "To test Classic Jerk, set 'Maximum Junction Deviation' in Motion ability to 0." msgstr "" "Marlin 2 Kavşak Sapması tespit edildi:\n" -"Classic Jerk'i test etmek için Hareket yeteneğinde 'Maksimum Kavşak Sapması'nı 0'a ayarlayın." +"Classic sarsıntıyı test etmek için Hareket yeteneğinde 'Maksimum Kavşak Sapması'nı 0'a ayarlayın." msgid "" "Marlin 2 Classic Jerk detected:\n" "To test Junction Deviation, set 'Maximum Junction Deviation' in Motion ability to a value > 0." msgstr "" -"Marlin 2 Classic Jerk tespit edildi:\n" +"Marlin 2 Classic sarsıntı tespit edildi:\n" "Kavşak Sapmasını test etmek için Hareket yeteneğinde 'Maksimum Kavşak Sapması'nı > 0 değerine ayarlayın." msgid "" @@ -19538,7 +19524,7 @@ msgid "Start Test Single-Thread" msgstr "Tek İş Parçacığı Testini Başlat" msgid "Export Log" -msgstr "Logu Dışa Aktar" +msgstr "Logu dışa aktar" msgid "OrcaSlicer Version:" msgstr "OrcaSlicer Sürümü:" @@ -20281,9 +20267,8 @@ msgstr "Sistem klasörü silinemedi..." msgid "Failed to determine executable path." msgstr "Yürütülebilir dosya yolu belirlenemedi." -# AI Translated msgid "Failed to launch a new instance." -msgstr "Yeni bir örnek başlatılamadı." +msgstr "Yeni bir kopya başlatılamadı." # AI Translated msgid "log(s)" diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 812d28e088..f7b4de6e25 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -149,6 +149,8 @@ set(lisbslic3r_sources Fill/FillConcentric.hpp Fill/FillConcentricInternal.cpp Fill/FillConcentricInternal.hpp + Fill/FillCornerSmoothing.cpp + Fill/FillCornerSmoothing.hpp Fill/Fill.cpp Fill/FillCrossHatch.cpp Fill/FillCrossHatch.hpp diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 88d87ddb26..0888e1bb55 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -970,9 +970,9 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p region_config.sparse_infill_rotate_template.value); params.fixed_angle = !region_config.sparse_infill_rotate_template.value.empty(); - // Orca: special case; apply smoothing factor only for Hilbert Curve sparse infill. - // FillHilbertCurve::generate clamps and validates the value itself. - if (params.pattern == ipHilbertCurve) + // Orca: the smoothing factor only applies to the sparse infill patterns that + // implement it. The fills clamp and validate the value themselves. + if (is_smoothable_infill_pattern(params.pattern, params.multiline)) params.smooth_factor = 0.01 * region_config.sparse_infill_smooth_factor.value; } else { const bool top_layer_direction_set = surface.is_top() && region_config.top_layer_direction.value >= 0.; diff --git a/src/libslic3r/Fill/Fill3DHoneycomb.cpp b/src/libslic3r/Fill/Fill3DHoneycomb.cpp index 5908f854de..ad5f8918fd 100644 --- a/src/libslic3r/Fill/Fill3DHoneycomb.cpp +++ b/src/libslic3r/Fill/Fill3DHoneycomb.cpp @@ -2,6 +2,7 @@ #include "../ShortestPath.hpp" #include "../Surface.hpp" #include "FillBase.hpp" +#include "FillCornerSmoothing.hpp" #include "Fill3DHoneycomb.hpp" namespace Slic3r { @@ -271,6 +272,9 @@ void Fill3DHoneycomb::_fill_surface_single( for (Polyline &pl : polylines){ pl.translate(bb.min); pl.simplify(5 * spacing); // simplify to 5x line width + // Orca: round the corners of the octahedral wave. The layers where the wave degenerates to a + // straight line have no corner to round. + smooth_polyline_corners(pl, params.smooth_factor, scaled(params.resolution)); } // Apply multiline offset if needed diff --git a/src/libslic3r/Fill/FillConcentric.cpp b/src/libslic3r/Fill/FillConcentric.cpp index a75d2ed7d3..1882f7a656 100644 --- a/src/libslic3r/Fill/FillConcentric.cpp +++ b/src/libslic3r/Fill/FillConcentric.cpp @@ -5,6 +5,7 @@ #include "Arachne/WallToolPaths.hpp" #include "FillConcentric.hpp" +#include "FillCornerSmoothing.hpp" #include namespace Slic3r { @@ -32,12 +33,32 @@ void FillConcentric::_fill_surface_single( Polygons loops = to_polygons(contracted); - ExPolygons last { std::move(contracted) }; + ExPolygons last { contracted }; while (! last.empty()) { last = offset2_ex(last, -(distance + min_spacing/2), +min_spacing/2); append(loops, to_polygons(last)); } + // Orca: round the corners of the loops. Unlike the other patterns these are never clipped to the + // fill region - they are its offsets - so a corner may only be rounded where the curve replacing it + // stays inside. Rounding cuts toward the inside of the turn, which around a hole, at a concave + // feature or across a thin region is outside the fill and would put the extrusion over a wall. + // The reach is capped at half the distance between two loops as well: a loop is as long as the + // object, and a corner cut by half of its side would swallow the neighbouring loops. + auto corner_stays_inside = [&contracted](const Vec2d &from, const Vec2d &to) { + // The straight chord between the ends of the curve is the deepest the curve can cut. + for (const double t : { 0.25, 0.5, 0.75 }) { + const Vec2d sample = from + t * (to - from); + const Point point(coord_t(sample.x()), coord_t(sample.y())); + if (std::none_of(contracted.begin(), contracted.end(), + [&point](const ExPolygon ®ion) { return region.contains(point); })) + return false; + } + return true; + }; + smooth_polygons_corners(loops, params.smooth_factor, scaled(params.resolution), 0.5 * distance, + corner_stays_inside); + // generate paths from the outermost to the innermost, to avoid // adhesion problems of the first central tiny loops loops = union_pt_chained_outside_in(loops); diff --git a/src/libslic3r/Fill/FillCornerSmoothing.cpp b/src/libslic3r/Fill/FillCornerSmoothing.cpp new file mode 100644 index 0000000000..2af9f6bb9c --- /dev/null +++ b/src/libslic3r/Fill/FillCornerSmoothing.cpp @@ -0,0 +1,226 @@ +#include + +#include "FillCornerSmoothing.hpp" + +namespace Slic3r { + +// Turns sharper than this are left untouched: both ends of the curve replacing such a corner nearly +// coincide, so the corner would be rounded into a degenerate loop instead of a hairpin. +static constexpr const double min_smoothed_turn_cosine = -0.9; + +// The control points are expressed in the (incoming, outgoing) basis of the corner, which is not +// orthonormal for turns other than a right angle. +using QuinticBezier = std::array; + +static bool is_bezier_flat(const QuinticBezier &curve, const Vec2d &incoming, const Vec2d &outgoing, const double deviation) +{ + // A Bezier curve stays inside the convex hull of its control points. Therefore, keeping every + // control point within a deviation-wide strip around the endpoint chord conservatively bounds the + // flattening error. The cross product is the perpendicular distance scaled by the chord length; + // comparing squared values avoids a square root. + auto in_plane = [&incoming, &outgoing](const Vec2d &c) { return c.x() * incoming + c.y() * outgoing; }; + const Vec2d chord = in_plane(curve.back() - curve.front()); + const double chord_length_sq = chord.squaredNorm(); + const double max_cross_sq = deviation * deviation * chord_length_sq; + + for (size_t i = 1; i + 1 < curve.size(); ++i) { + const Vec2d offset = in_plane(curve[i] - curve.front()); + const double cross = chord.x() * offset.y() - chord.y() * offset.x(); + if (cross * cross > max_cross_sq) + return false; + } + return true; +} + +static void subdivide_bezier(const QuinticBezier &curve, QuinticBezier &left, QuinticBezier &right) +{ + // Split the curve at t = 0.5 using de Casteljau's algorithm. Each averaging level contributes one + // control point to the left half and one to the right half; the latter is filled backwards to keep + // both resulting control polygons in their original parameter direction. + QuinticBezier subdivision = curve; + left.front() = subdivision.front(); + right.back() = subdivision.back(); + for (size_t level = 1; level < curve.size(); ++level) { + for (size_t i = 0; i + level < curve.size(); ++i) + subdivision[i] = 0.5 * (subdivision[i] + subdivision[i + 1]); + left[level] = subdivision.front(); + right[curve.size() - level - 1] = subdivision[curve.size() - level - 1]; + } +} + +static void flatten_bezier( + const QuinticBezier &curve, const Vec2d &incoming, const Vec2d &outgoing, const double deviation, std::vector &output) +{ + // Subdivide to at least depth 1 so a rounded corner cannot collapse to a single diagonal chord. + // A uniform subdivision depth keeps samples at equal parameter intervals t = k / 2^depth, + // avoiding abrupt segment-length jumps at adaptive-depth boundaries. + static constexpr size_t max_depth = 16; + + std::vector subcurves(2); + subdivide_bezier(curve, subcurves[0], subcurves[1]); + + for (size_t depth = 1; depth < max_depth; ++depth) { + bool all_flat = true; + for (const QuinticBezier &c : subcurves) + if (!is_bezier_flat(c, incoming, outgoing, deviation)) { + all_flat = false; + break; + } + if (all_flat) + break; + std::vector finer(subcurves.size() * 2); + for (size_t i = 0; i < subcurves.size(); ++i) + subdivide_bezier(subcurves[i], finer[i * 2], finer[i * 2 + 1]); + subcurves = std::move(finer); + } + + // The curve start is deliberately omitted so it can be shared with the straight leg feeding into it. + output.clear(); + output.reserve(subcurves.size()); + for (const QuinticBezier &c : subcurves) + output.emplace_back(c.back()); +} + +const std::vector& CornerSmoother::curve_coefficients( + const double corner_distance, const Vec2d &incoming, const Vec2d &outgoing) +{ + const double cosine = incoming.dot(outgoing); + // Corners of the same size and turn angle are congruent, so they flatten identically. An infill + // path walks over the very same corner over and over again, the Hilbert curve over a single one. + if (m_has_cached_coefficients && corner_distance == m_cached_distance && cosine == m_cached_cosine) + return m_cached_coefficients; + + // One canonical corner running from -corner_distance along the incoming leg to corner_distance + // along the outgoing one. At each end, the first three control points are collinear and equally + // spaced: the tangent follows the adjoining straight leg and the second derivative is zero. The + // endpoint curvature is therefore zero, giving G2 joins to both legs. + const double d = corner_distance; + const QuinticBezier corner_curve {{ + {-d, 0.}, {-0.7 * d, 0.}, {-0.4 * d, 0.}, {0., 0.4 * d}, {0., 0.7 * d}, {0., d} + }}; + // Retain a finite positive tolerance if the smoother was set up with an invalid one. + const double deviation = m_tolerance > 0. && std::isfinite(m_tolerance) ? m_tolerance : EPSILON; + flatten_bezier(corner_curve, incoming, outgoing, deviation, m_cached_coefficients); + + m_cached_distance = corner_distance; + m_cached_cosine = cosine; + m_has_cached_coefficients = true; + return m_cached_coefficients; +} + +void CornerSmoother::round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next) +{ + m_corner_points.clear(); + + const Vec2d incoming_leg = corner - previous; + const Vec2d outgoing_leg = next - corner; + const double incoming_length = incoming_leg.norm(); + const double outgoing_length = outgoing_leg.norm(); + if (incoming_length < EPSILON || outgoing_length < EPSILON) { + m_corner_points.emplace_back(corner); + return; + } + + const Vec2d incoming = incoming_leg / incoming_length; + const Vec2d outgoing = outgoing_leg / outgoing_length; + const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x(); + // A collinear vertex is no corner at all, and a hairpin cannot be rounded, see above. + if (std::abs(cross) < EPSILON || incoming.dot(outgoing) < min_smoothed_turn_cosine) { + m_corner_points.emplace_back(corner); + return; + } + + // Consuming at most half of the shorter leg keeps the curves of two adjacent corners apart. + double corner_distance = m_corner_distance_ratio * std::min(incoming_length, outgoing_length); + if (m_max_corner_distance > 0.) + corner_distance = std::min(corner_distance, m_max_corner_distance); + + const Vec2d curve_start = corner - corner_distance * incoming; + const Vec2d curve_end = corner + corner_distance * outgoing; + if (m_corner_filter && !m_corner_filter(curve_start, curve_end)) { + m_corner_points.emplace_back(corner); + return; + } + + const std::vector &coefficients = curve_coefficients(corner_distance, incoming, outgoing); + m_corner_points.reserve(coefficients.size() + 1); + m_corner_points.emplace_back(curve_start); + for (const Vec2d &coefficient : coefficients) + m_corner_points.emplace_back(corner + coefficient.x() * incoming + coefficient.y() * outgoing); +} + +// Rounds the corners of a scaled point sequence. A polygon closes implicitly, so all of its vertices +// are corners; a polyline is an open path that keeps both of its ends, even where they coincide - a +// path returning to where it started retraces its way back and is not a loop. +static Points smooth_corners(const Points &points, const bool polygon, CornerSmoother &smoother) +{ + // A polygon has no free ends, so its first vertex is a corner like any other. Rounding it takes + // feeding the smoother the last vertex first, whose own output point is then dropped again. + size_t skip = polygon ? 1 : 0; + + Points smoothed; + smoothed.reserve(2 * points.size()); + auto emit = [&smoothed, &skip](const Vec2d &point) { + if (skip > 0) { + --skip; + return; + } + smoothed.emplace_back(coord_t(std::floor(point.x() + 0.5)), coord_t(std::floor(point.y() + 0.5))); + }; + + if (polygon) + smoother.push(points.back().cast(), emit); + for (const Point &point : points) + smoother.push(point.cast(), emit); + if (polygon) + // Wrap the first vertex around, so that the last one is a corner as well. + smoother.push(points.front().cast(), emit); + smoother.flush(emit); + + if (polygon) + // The flushed point is the wrapped first vertex, which a polygon does not store. + smoothed.pop_back(); + return smoothed; +} + +void smooth_polyline_corners(Polyline &polyline, const double smooth_factor, const double tolerance, + const double max_corner_distance, const CornerFilter &corner_filter) +{ + CornerSmoother smoother(smooth_factor, tolerance, max_corner_distance, corner_filter); + if (!smoother.enabled() || polyline.size() < 3) + return; + + polyline.points = smooth_corners(polyline.points, false, smoother); + // Rounding back to the integer grid may collapse neighbouring samples of a curve. + polyline.remove_duplicate_points(); +} + +void smooth_polylines_corners(Polylines &polylines, const double smooth_factor, const double tolerance, + const double max_corner_distance, const CornerFilter &corner_filter) +{ + if (sanitize_smooth_factor(smooth_factor) == 0.) + return; + for (Polyline &polyline : polylines) + smooth_polyline_corners(polyline, smooth_factor, tolerance, max_corner_distance, corner_filter); +} + +void smooth_polygons_corners(Polygons &polygons, const double smooth_factor, const double tolerance, + const double max_corner_distance, const CornerFilter &corner_filter) +{ + CornerSmoother smoother(smooth_factor, tolerance, max_corner_distance, corner_filter); + if (!smoother.enabled()) + return; + + for (Polygon &polygon : polygons) { + if (polygon.size() < 3) + continue; + polygon.points = smooth_corners(polygon.points, true, smoother); + polygon.remove_duplicate_points(); + // The curves of the first and of the last corner may have met on the segment they share. A + // polygon closes implicitly, so it must not repeat its first vertex at the end. + if (polygon.points.size() > 1 && polygon.points.front() == polygon.points.back()) + polygon.points.pop_back(); + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/Fill/FillCornerSmoothing.hpp b/src/libslic3r/Fill/FillCornerSmoothing.hpp new file mode 100644 index 0000000000..1852fc4c67 --- /dev/null +++ b/src/libslic3r/Fill/FillCornerSmoothing.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include +#include + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../Polygon.hpp" +#include "../Polyline.hpp" + +namespace Slic3r { + +// Orca: NaN or infinite factors disable the smoothing, everything else is clamped to <0, 1>. +inline double sanitize_smooth_factor(double smooth_factor) +{ + return std::isfinite(smooth_factor) ? std::clamp(smooth_factor, 0., 1.) : 0.; +} + +// Decides whether a corner may be replaced by the curve that leaves the path at `from` and rejoins it +// at `to`, both in the coordinate system of the pushed points. Rounding cuts toward the inside of the +// turn, so a path that is not clipped to the fill region afterwards needs this to stay inside it. +using CornerFilter = std::function; + +// Orca: Replaces the sharp vertices of an infill path with curves that join the adjoining straight +// legs with a continuous curvature, so the toolhead does not have to stop in every corner. +// Points are pushed one by one, because the plane path fills produce their path on the fly, and +// every point of the smoothed path is handed over to the caller supplied emit callback. +// Fully smoothed adjacent corners meet at the midpoint of the segment they share, so the emitted +// points may collapse onto each other once rounded to the integer grid of the caller. Dropping such +// duplicates is left to the caller, which is the only one knowing that grid. +class CornerSmoother +{ +public: + // tolerance is the maximum chordal deviation of the flattened curves, in the units of the pushed + // points. max_corner_distance caps how far a curve may reach along a leg, in the same units; it + // bounds how far a rounded corner moves away from the original path, which matters where the legs + // are much longer than the spacing of the pattern. Zero leaves the reach uncapped. + CornerSmoother(double smooth_factor, double tolerance, double max_corner_distance = 0., + CornerFilter corner_filter = {}) + : m_corner_distance_ratio(0.5 * sanitize_smooth_factor(smooth_factor)), m_tolerance(tolerance), + m_max_corner_distance(max_corner_distance), m_corner_filter(std::move(corner_filter)) + {} + + bool enabled() const { return m_corner_distance_ratio > 0.; } + + template void push(const Vec2d &point, Emit &emit) + { + if (m_pending == 0) { + emit(point); + m_previous = point; + } else if (m_pending > 1) { + round_corner(m_previous, m_corner, point); + for (const Vec2d &corner_point : m_corner_points) + emit(corner_point); + m_previous = m_corner; + } + m_corner = point; + m_pending = std::min(m_pending + 1, 2); + } + + // Emits the last point of the path and prepares the smoother for a new one. + template void flush(Emit &emit) + { + if (m_pending > 1) + emit(m_corner); + m_pending = 0; + } + +private: + // Fills m_corner_points with the points replacing the corner vertex. + void round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next); + // Flattens the canonical corner curve of the given size and turn into coordinates of the + // (incoming, outgoing) basis of the corner. Cached, as an infill path repeats the same corner. + const std::vector& curve_coefficients(double corner_distance, const Vec2d &incoming, const Vec2d &outgoing); + + // Fraction of the shorter adjoining segment consumed on each side of a corner. Half of a segment + // is the maximum, otherwise the curves of two adjacent corners would overlap. + const double m_corner_distance_ratio; + const double m_tolerance; + const double m_max_corner_distance; + const CornerFilter m_corner_filter; + std::vector m_corner_points; + // Cached flattening of the last corner, valid for corners of the same size and turn angle. + std::vector m_cached_coefficients; + double m_cached_distance { 0. }; + double m_cached_cosine { 0. }; + bool m_has_cached_coefficients { false }; + + Vec2d m_previous { Vec2d::Zero() }; + Vec2d m_corner { Vec2d::Zero() }; + // Number of points held back: none, the first point of a path, or a corner candidate. + int m_pending { 0 }; +}; + +// Rounds the corners of already scaled paths in place. Paths of less than three points are left alone. +// Both ends of a polyline are kept where they are, even when they coincide: such a path retraces its +// way back and joining its ends would turn it into a loop. See CornerSmoother for max_corner_distance. +void smooth_polyline_corners(Polyline &polyline, double smooth_factor, double tolerance, + double max_corner_distance = 0., const CornerFilter &corner_filter = {}); +void smooth_polylines_corners(Polylines &polylines, double smooth_factor, double tolerance, + double max_corner_distance = 0., const CornerFilter &corner_filter = {}); +// Polygons close implicitly, so every one of their vertices is a corner. +void smooth_polygons_corners(Polygons &polygons, double smooth_factor, double tolerance, + double max_corner_distance = 0., const CornerFilter &corner_filter = {}); + +} // namespace Slic3r diff --git a/src/libslic3r/Fill/FillCrossHatch.cpp b/src/libslic3r/Fill/FillCrossHatch.cpp index 571095eca4..98be5ef46b 100644 --- a/src/libslic3r/Fill/FillCrossHatch.cpp +++ b/src/libslic3r/Fill/FillCrossHatch.cpp @@ -3,6 +3,7 @@ #include "../Surface.hpp" #include #include "FillBase.hpp" +#include "FillCornerSmoothing.hpp" #include "FillCrossHatch.hpp" namespace Slic3r { @@ -205,6 +206,9 @@ void FillCrossHatch ::_fill_surface_single( // shift the pattern to the actual space for (Polyline &pl : polylines) { pl.translate(bb.min); } + // Orca: round the corners of the transition layers. The repeat layers are straight lines and stay as they are. + smooth_polylines_corners(polylines, params.smooth_factor, scaled(params.resolution)); + // Apply multiline offset if needed multiline_fill(polylines, params, spacing); diff --git a/src/libslic3r/Fill/FillHoneycomb.cpp b/src/libslic3r/Fill/FillHoneycomb.cpp index a595cdb664..82679541da 100644 --- a/src/libslic3r/Fill/FillHoneycomb.cpp +++ b/src/libslic3r/Fill/FillHoneycomb.cpp @@ -2,6 +2,7 @@ #include "../ShortestPath.hpp" #include "../Surface.hpp" +#include "FillCornerSmoothing.hpp" #include "FillHoneycomb.hpp" namespace Slic3r { @@ -70,6 +71,9 @@ void FillHoneycomb::_fill_surface_single( } p.rotate(-direction.first, m.hex_center); p.simplify(5 * spacing); // simplify to 5x line width + // Orca: round the corners of the honeycomb cells. Done before the clipping, so that the + // curves are cut by the region boundary just like the sharp path would be. + smooth_polyline_corners(p, params.smooth_factor, scaled(params.resolution)); all_polylines.push_back(p); } } diff --git a/src/libslic3r/Fill/FillLightning.cpp b/src/libslic3r/Fill/FillLightning.cpp index 7937b9d129..77031b42e0 100644 --- a/src/libslic3r/Fill/FillLightning.cpp +++ b/src/libslic3r/Fill/FillLightning.cpp @@ -2,6 +2,7 @@ #include "../Print.hpp" #include "../ShortestPath.hpp" #include "FillBase.hpp" +#include "FillCornerSmoothing.hpp" #include "FillLightning.hpp" #include "Lightning/Generator.hpp" @@ -17,6 +18,19 @@ void Filler::_fill_surface_single( const Layer &layer = generator->getTreesForLayer(this->layer_id); Polylines fill_lines = layer.convertToLines(to_polygons(expolygon), scaled(0.5 * this->spacing - this->overlap)); + // Orca: round the turns of the branches. Hairpins are left sharp, as they cannot be rounded, and + // the reach is capped: cutting a corner moves the branch, and a branch is as long as the object + // rather than as long as one cell of a pattern, so half of a leg would merge it with its neighbour + // instead of rounding the turn between them. Half the distance between two branches keeps them + // apart. With more than one line per infill wall the branches are printed as outlines drawn around + // them, and the outlines of branches that run into each other merge into a single one; moving a + // branch by more than a fraction of its printed width breaks such an outline up into separate + // loops, so that width bounds the reach as well. + const double branch_width = scaled(this->spacing) * params.multiline; + const double branch_spacing = branch_width / std::max(double(params.density), EPSILON); + const double max_reach = 0.5 * (params.multiline > 1 ? branch_width : branch_spacing); + smooth_polylines_corners(fill_lines, params.smooth_factor, scaled(params.resolution), max_reach); + // Apply multiline offset if needed multiline_fill(fill_lines, params, spacing); diff --git a/src/libslic3r/Fill/FillPlanePath.cpp b/src/libslic3r/Fill/FillPlanePath.cpp index 7c4f285ac6..577aef0600 100644 --- a/src/libslic3r/Fill/FillPlanePath.cpp +++ b/src/libslic3r/Fill/FillPlanePath.cpp @@ -2,6 +2,7 @@ #include "../ShortestPath.hpp" #include "../Surface.hpp" +#include "FillCornerSmoothing.hpp" #include "FillPlanePath.hpp" namespace Slic3r { @@ -288,145 +289,60 @@ static void generate_hilbert_curve(coord_t min_x, coord_t min_y, coord_t max_x, } } -using QuinticBezier = std::array; - -static bool is_bezier_flat(const QuinticBezier &curve, const double deviation) -{ - // A Bezier curve stays inside the convex hull of its control points. Therefore, keeping every - // control point within a deviation-wide strip around the endpoint chord conservatively bounds the - // flattening error. The cross product is the perpendicular distance scaled by the chord length; - // comparing squared values avoids a square root. - const Vec2d chord = curve.back() - curve.front(); - const double chord_length_sq = chord.squaredNorm(); - const double max_cross_sq = deviation * deviation * chord_length_sq; - - for (size_t i = 1; i + 1 < curve.size(); ++i) { - const Vec2d offset = curve[i] - curve.front(); - const double cross = chord.x() * offset.y() - chord.y() * offset.x(); - if (cross * cross > max_cross_sq) - return false; - } - return true; -} - -static void subdivide_bezier(const QuinticBezier &curve, QuinticBezier &left, QuinticBezier &right) -{ - // Split the curve at t = 0.5 using de Casteljau's algorithm. Each averaging level contributes one - // control point to the left half and one to the right half; the latter is filled backwards to keep - // both resulting control polygons in their original parameter direction. - QuinticBezier subdivision = curve; - left.front() = subdivision.front(); - right.back() = subdivision.back(); - for (size_t level = 1; level < curve.size(); ++level) { - for (size_t i = 0; i + level < curve.size(); ++i) - subdivision[i] = 0.5 * (subdivision[i] + subdivision[i + 1]); - left[level] = subdivision.front(); - right[curve.size() - level - 1] = subdivision[curve.size() - level - 1]; - } -} - -static void flatten_bezier(const QuinticBezier &curve, const double deviation, std::vector &output) -{ - // Subdivide to at least depth 1 so a rounded corner cannot collapse to a single diagonal chord. - // A uniform subdivision depth keeps samples at equal parameter intervals t = k / 2^depth, - // avoiding abrupt segment-length jumps at adaptive-depth boundaries. - static constexpr size_t max_depth = 16; - - std::vector subcurves(2); - subdivide_bezier(curve, subcurves[0], subcurves[1]); - - for (size_t depth = 1; depth < max_depth; ++depth) { - bool all_flat = true; - for (const QuinticBezier &c : subcurves) - if (!is_bezier_flat(c, deviation)) { - all_flat = false; - break; - } - if (all_flat) - break; - std::vector finer(subcurves.size() * 2); - for (size_t i = 0; i < subcurves.size(); ++i) - subdivide_bezier(subcurves[i], finer[i * 2], finer[i * 2 + 1]); - subcurves = std::move(finer); - } - - // The curve start is deliberately omitted so consecutive curve pieces can share it without duplication. - output.reserve(output.size() + subcurves.size()); - for (const QuinticBezier &c : subcurves) - output.emplace_back(c.back()); -} - +// Rounds the corners of the generated path on its way to the infill output. template -static void generate_smooth_hilbert_curve( - coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, - const double corner_distance, Output &output) +class SmoothingPolylineOutput { - // A Hilbert curve is defined on a square grid whose side is a power of two. As in the unsmoothed - // generator, expand the larger requested dimension to the next valid Hilbert grid size. The output - // clipper or the later region intersection removes the padded part of the traversal. - size_t sz = 2; - const size_t sz0 = std::max(max_x + 1 - min_x, max_y + 1 - min_y); - while (sz < sz0) - sz <<= 1; +public: + SmoothingPolylineOutput(Output &output, const double smooth_factor, const double tolerance) + : m_output(output), m_smoother(smooth_factor, tolerance) {} - const size_t point_count = sz * sz; - output.reserve(point_count); + void reserve(size_t n) { m_output.reserve(n); } + void add_point(const Vec2d &pt) { auto emit = emitter(); m_smoother.push(pt, emit); } + // The smoother holds back the last point of the path until it knows there is no corner left to round. + void finish() { auto emit = emitter(); m_smoother.flush(emit); } - // The caller normalizes resolution to the unit Hilbert grid; retain a finite positive tolerance - // if this helper is invoked with an invalid resolution. - const double deviation = resolution > 0. && std::isfinite(resolution) ? resolution : EPSILON; - // Construct one canonical 90-degree corner from (-corner_distance, 0) to (0, corner_distance). - // At each end, the first three control points are collinear and equally spaced: the tangent follows - // the adjoining straight leg and the second derivative is zero. The endpoint curvature is therefore - // zero, giving G2 joins to both legs. Every Hilbert turn is an oriented copy of this curve, so flatten - // it only once to the requested chordal-deviation tolerance. - const QuinticBezier corner_curve {{ - {-corner_distance, 0.}, {-0.7 * corner_distance, 0.}, {-0.4 * corner_distance, 0.}, - {0., 0.4 * corner_distance}, {0., 0.7 * corner_distance}, {0., corner_distance} - }}; - std::vector curve_coefficients; - flatten_bezier(corner_curve, deviation, curve_coefficients); - - auto translated_point = [min_x, min_y](size_t idx) { - Point p = hilbert_n_to_xy(idx); - return Point(p.x() + min_x, p.y() + min_y); - }; - auto to_vec2d = [](const Point &p) { return Vec2d(double(p.x()), double(p.y())); }; - bool has_last_output = false; - Vec2d last_output; - // Fully smoothed adjacent corners may meet at the same segment midpoint. Suppress such duplicates - // to avoid emitting zero-length extrusion segments. - auto add_point = [&output, &has_last_output, &last_output](const Vec2d &point) { - if (!has_last_output || point.x() != last_output.x() || point.y() != last_output.y()) { - output.add_point(point); - last_output = point; - has_last_output = true; - } - }; - - Vec2d previous = to_vec2d(translated_point(0)); - Vec2d corner = to_vec2d(translated_point(1)); - add_point(previous); - // Replace each non-collinear Hilbert vertex by the canonical curve expressed in the local basis of - // its incoming and outgoing unit vectors. Collinear vertices remain part of the straight polyline. - for (size_t i = 1; i + 1 < point_count; ++i) { - const Vec2d next = to_vec2d(translated_point(i + 1)); - const Vec2d incoming = (corner - previous).normalized(); - const Vec2d outgoing = (next - corner).normalized(); - const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x(); - - if (std::abs(cross) < EPSILON) { - add_point(corner); - } else { - add_point(corner - corner_distance * incoming); - for (const Vec2d &coefficient : curve_coefficients) - add_point(corner + coefficient.x() * incoming + coefficient.y() * outgoing); - } - - previous = corner; - corner = next; +private: + // The curves of two adjacent corners meet at the midpoint of the segment they share, where they + // may round to the very same output point. Drop those, they would be zero length extrusions. + auto emitter() + { + return [this](const Vec2d &pt) { + const Point snapped = m_output.scaled(pt); + if (m_has_last_snapped && snapped == m_last_snapped) + return; + m_last_snapped = snapped; + m_has_last_snapped = true; + m_output.add_point(pt); + }; } - add_point(corner); + + Output &m_output; + CornerSmoother m_smoother; + Point m_last_snapped { Point::Zero() }; + bool m_has_last_snapped { false }; +}; + +// Runs the path generator against the concrete output type, optionally through the corner smoother. +// The outputs do not share a virtual add_point(), so the type has to be resolved here. +template +static void generate_path(InfillPolylineOutput &output, const FillParams ¶ms, const double resolution, GenerateFn generate) +{ + const double smooth_factor = sanitize_smooth_factor(params.smooth_factor); + auto run = [smooth_factor, resolution, &generate](auto &out) { + if (smooth_factor == 0.) { + generate(out); + } else { + SmoothingPolylineOutput> smoothing(out, smooth_factor, resolution); + generate(smoothing); + smoothing.finish(); + } + }; + + if (output.clips()) + run(static_cast(output)); + else + run(output); } void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double /* resolution */, InfillPolylineOutput &output) @@ -440,19 +356,8 @@ void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coo void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, const FillParams ¶ms, InfillPolylineOutput &output) { - const double smooth_factor = std::isfinite(params.smooth_factor) ? - std::clamp(params.smooth_factor, 0., 1.) : 0.; - if (smooth_factor == 0.) { - this->generate(min_x, min_y, max_x, max_y, resolution, output); - return; - } - - const double corner_distance = 0.5 * smooth_factor; - if (output.clips()) - generate_smooth_hilbert_curve( - min_x, min_y, max_x, max_y, resolution, corner_distance, static_cast(output)); - else - generate_smooth_hilbert_curve(min_x, min_y, max_x, max_y, resolution, corner_distance, output); + generate_path(output, params, resolution, + [min_x, min_y, max_x, max_y](auto &out) { generate_hilbert_curve(min_x, min_y, max_x, max_y, out); }); } template @@ -495,4 +400,11 @@ void FillOctagramSpiral::generate(coord_t min_x, coord_t min_y, coord_t max_x, c generate_octagram_spiral(min_x, min_y, max_x, max_y, output); } +void FillOctagramSpiral::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, + const FillParams ¶ms, InfillPolylineOutput &output) +{ + generate_path(output, params, resolution, + [min_x, min_y, max_x, max_y](auto &out) { generate_octagram_spiral(min_x, min_y, max_x, max_y, out); }); +} + } // namespace Slic3r diff --git a/src/libslic3r/Fill/FillPlanePath.hpp b/src/libslic3r/Fill/FillPlanePath.hpp index b4b25b73ae..a1e9068ca9 100644 --- a/src/libslic3r/Fill/FillPlanePath.hpp +++ b/src/libslic3r/Fill/FillPlanePath.hpp @@ -21,10 +21,10 @@ public: void add_point(const Vec2d& pt) { m_out.emplace_back(this->scaled(pt)); } Points&& result() { return std::move(m_out); } virtual bool clips() const { return false; } - -protected: + // The output grid the generated points are snapped to. const Point scaled(const Vec2d& fpt) const { return { coord_t(floor(fpt.x() * m_scale_out + 0.5)), coord_t(floor(fpt.y() * m_scale_out + 0.5)) }; } +protected: // Output polyline. Points m_out; @@ -93,6 +93,8 @@ public: protected: bool centered() const override { return true; } void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) override; + void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, + const FillParams ¶ms, InfillPolylineOutput &output) override; }; } // namespace Slic3r diff --git a/src/libslic3r/Fill/FillRectilinear.cpp b/src/libslic3r/Fill/FillRectilinear.cpp index 8b40b8753c..0c82354b38 100644 --- a/src/libslic3r/Fill/FillRectilinear.cpp +++ b/src/libslic3r/Fill/FillRectilinear.cpp @@ -18,6 +18,7 @@ #include "../ShortestPath.hpp" #include "../VariableWidth.hpp" +#include "FillCornerSmoothing.hpp" #include "FillRectilinear.hpp" // #define SLIC3R_DEBUG @@ -3364,6 +3365,10 @@ bool FillRectilinear::fill_surface_trapezoidal( for (Polyline &pl : polylines) pl.translate(rotate_vector.second); + // Orca: round the corners of the trapezoids. The straight base lines of the triangular family + // have no corner to round. + smooth_polylines_corners(polylines, params.smooth_factor, scaled(params.resolution)); + // Apply multiline fill multiline_fill(polylines, params, spacing); diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index b282af8f4e..1d65e83f3e 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -39,7 +40,11 @@ std::vector> estimate_points_properties(const POINTS& const AABBTreeLines::LinesDistancer& unscaled_prev_layer, float flow_width, float max_line_length = -1.0f, - float min_distance = -1.0f) + float min_distance = -1.0f, + // Maps an overhang distance onto the speed it will be printed at. Interior sampling + // needs it to tell which of the points it could add would change the G-code, and is + // skipped without it. + const std::function& distance_to_speed = {}) { bool looped = input_points.front() == input_points.back(); std::function get_prev_index = [](size_t idx, size_t count) { @@ -120,6 +125,107 @@ std::vector> estimate_points_properties(const POINTS& points.push_back(next_point); } + // ORCA: Interior sampling + // The passes below infer the support under a span from its endpoints alone, so an interior that is supported + // differently from both ends is invisible to them: the outer perimeter of an overhang whose ends are caged by + // full height walls reads as supported along its whole length. Probe the interior, keep the samples the + // endpoint interpolation fails to predict, and bisect either side of each one, so a span that is only partly + // unsupported gets points where its support actually changes instead of one reading spread across all of it. + if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS && min_distance > 0 && distance_to_speed) { + // Probe at least this densely before treating matching samples as evidence that a span is uniform. The + // segmentation pass below only splits lines of 2mm or more, and every pass here drops points closer + // together than min_spacing, so finer discovery would not produce a more precise speed transition. + const double max_probe_spacing = std::max(2., 4. * min_spacing); + // A backstop for that length test, which on a non-finite length would never be met. + constexpr int max_bisection_depth = 10; + // Whether two readings are interchangeable. A segment is printed at the lower of the speeds its ends + // read, so a sample that agrees on speed with what is already known cannot change the G-code, whatever + // its distance says. The distances themselves are far too coarse a stand-in for this: the speed sections + // interpolate, so readings a small fraction of min_distance apart can still be tens of mm/s apart. + // The tolerance matches the one GCode.cpp applies when it decides a path has a variable speed at all. + auto same_speed = [&distance_to_speed](float a, float b) { + return std::abs(distance_to_speed(a) - distance_to_speed(b)) <= 1.f; + }; + // Whether the first reading is printed slower than the second, once they are known to differ. + auto prints_slower = [&distance_to_speed](float a, float b) { return distance_to_speed(a) < distance_to_speed(b); }; + + // Part of a segment still to bisect: its positions along the segment and bisections left. + struct Subspan { double t0, t1; int depth; }; + + std::vector> sampled_points; // Populated lazily, on the first insertion + std::vector> interior; // Samples of one segment, keyed by position along it + std::vector pending; + + for (size_t point_idx = 0; point_idx + 1 < points.size(); ++point_idx) { + const ExtendedPoint& curr = points[point_idx]; + const ExtendedPoint& next = points[point_idx + 1]; + const Vec step = next.position - curr.position; + const double line_len = step.norm(); + + interior.clear(); + if (line_len >= max_probe_spacing) + pending.push_back({0., 1., max_bisection_depth}); + + while (!pending.empty()) { + const Subspan subspan = pending.back(); + pending.pop_back(); + if (subspan.depth <= 0 || (subspan.t1 - subspan.t0) * line_len < max_probe_spacing) + continue; + + const double t = 0.5 * (subspan.t0 + subspan.t1); + auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra( + (curr.position + t * step).template cast()); + const float sampled = float(distance + boundary_offset); + + interior.emplace_back(t, sampled); + pending.push_back({subspan.t0, t, subspan.depth - 1}); + pending.push_back({t, subspan.t1, subspan.depth - 1}); + } + + if (!interior.empty()) { + std::sort(interior.begin(), interior.end(), + [](const std::pair& l, const std::pair& r) { return l.first < r.first; }); + // Coarse probing keeps every sample it took until this pass can see which ones bracket a speed + // transition. Matching samples cannot be discarded during discovery: one may be the last + // supported point before a narrow unsupported pocket found by a later probe. + size_t kept = 0; + for (size_t i = 0; i < interior.size(); ++i) { + const float sample = interior[i].second; + const bool at_start = kept == 0; // Nothing kept yet, so the segment's own start precedes it + const bool at_end = i + 1 == interior.size(); // And nothing follows the last sample but the segment's end + const float before = at_start ? curr.distance : interior[kept - 1].second; + const float after = at_end ? next.distance : interior[i + 1].second; + // A sample is worth a point in the path only where it prints at a different speed from the + // readings either side of it. Differing from one of the segment's own ends is not enough on + // its own where the sample is the faster of the two: the segmentation pass below already + // ends the slowdown an end reads, at a distance taken from how far out that end is rather + // than from wherever bisection happened to stop, and a point here would leave the span + // beside the end too short for that pass to run at all. Support an end cannot account for, + // where the interior is the slower reading, is exactly what this pass is here to find. + const bool worth_before = !same_speed(sample, before) && (!at_start || prints_slower(sample, before)); + const bool worth_after = !same_speed(sample, after) && (!at_end || prints_slower(sample, after)); + if (worth_before || worth_after) + interior[kept++] = interior[i]; + } + interior.resize(kept); + } + + if (!interior.empty() && sampled_points.empty()) { + sampled_points.reserve(points.size() + 8); + sampled_points.assign(points.begin(), points.begin() + point_idx + 1); + } + if (!sampled_points.empty()) { + // Only a sub-span of max_probe_spacing or more is ever bisected, so these sit at least + // 2 * min_spacing apart, and need none of the filtering the passes either side of this one do. + for (const auto& [t, distance] : interior) + sampled_points.push_back({curr.position + t * step, distance}); + sampled_points.push_back(next); + } + } + if (!sampled_points.empty()) + points = std::move(sampled_points); + } + // Segmentation handling if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS) { std::vector> new_points; @@ -362,9 +468,28 @@ public: smallest_distance_with_lower_speed=-1.f; // Orca: Pass to the point properties estimator the smallest ovehang distance that triggers a slowdown (smallest_distance_with_lower_speed) + auto calculate_speed = [&speed_sections, &original_speed](float distance) { + float final_speed; + if (distance <= speed_sections.front().first) { + final_speed = original_speed; + } else if (distance >= speed_sections.back().first) { + final_speed = speed_sections.back().second; + } else { + size_t section_idx = 0; + while (distance > speed_sections[section_idx + 1].first) { + section_idx++; + } + float t = (distance - speed_sections[section_idx].first) / + (speed_sections[section_idx + 1].first - speed_sections[section_idx].first); + t = std::clamp(t, 0.0f, 1.0f); + final_speed = (1.0f - t) * speed_sections[section_idx].second + t * speed_sections[section_idx + 1].second; + } + return round(final_speed); + }; + std::vector> extended_points = estimate_points_properties(path.polyline.points, prev_layer_boundaries[current_object], path.width, -1, - smallest_distance_with_lower_speed); + smallest_distance_with_lower_speed, calculate_speed); const auto width_inv = 1.0f / path.width; std::vector processed_points; processed_points.reserve(extended_points.size()); @@ -423,25 +548,6 @@ public: } } - auto calculate_speed = [&speed_sections, &original_speed](float distance) { - float final_speed; - if (distance <= speed_sections.front().first) { - final_speed = original_speed; - } else if (distance >= speed_sections.back().first) { - final_speed = speed_sections.back().second; - } else { - size_t section_idx = 0; - while (distance > speed_sections[section_idx + 1].first) { - section_idx++; - } - float t = (distance - speed_sections[section_idx].first) / - (speed_sections[section_idx + 1].first - speed_sections[section_idx].first); - t = std::clamp(t, 0.0f, 1.0f); - final_speed = (1.0f - t) * speed_sections[section_idx].second + t * speed_sections[section_idx + 1].second; - } - return round(final_speed); - }; - float extrusion_speed = std::min(calculate_speed(curr.distance), calculate_speed(next.distance)); // ORCA: Clamp resulting speed to lowest of calculated speed based on the overhang values and the current speed // Fixes bug where resulting overhang speed is higher than the current speed due to (for example) volumetric flow limits. diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 5557d36891..e66ae064f0 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -49,7 +49,6 @@ static std::vector s_project_options { "filament_multi_colour", "wipe_tower_x", "wipe_tower_y", - "wipe_tower_rotation_angle", "curr_bed_type", "flush_multiplier", // Fast-purge mode: project-level purge control, inert at Default. diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index f9d895332a..8083da954e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -3469,9 +3469,8 @@ void PrintConfigDef::init_fff_params() def = this->add("sparse_infill_smooth_factor", coPercent); def->label = L("Sparse infill smooth factor"); def->category = L("Strength"); - def->tooltip = L("Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, " - "while 100% produces the largest possible curves between adjacent infill lines. " - "Currently applies only to the Hilbert Curve."); + def->tooltip = L("Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, " + "while 100% produces the largest possible curves between adjacent infill lines."); def->sidetext = "%"; def->min = 0; def->max = 100; diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index f51c1c6411..26a708b78d 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -146,6 +146,29 @@ inline bool is_separable_infill_pattern(InfillPattern pattern) } } +// Orca: Infill patterns that round their corners by the "sparse_infill_smooth_factor" option. +// Grid, Triangles and Tri-hexagon only do so in their trapezoidal form, which is generated with more +// than one line per infill wall; a single line makes them plain crossing lines with nothing to round. +inline bool is_smoothable_infill_pattern(InfillPattern pattern, int multiline = 1) +{ + switch (pattern) { + case ipHilbertCurve: + case ipOctagramSpiral: + case ipLightning: + case ipHoneycomb: + case ip3DHoneycomb: + case ipConcentric: + case ipCrossHatch: + return true; + case ipGrid: + case ipTriangles: + case ipStars: + return multiline > 1; + default: + return false; + } +} + enum class IroningType { NoIroning, TopSurfaces, diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 3885a391b8..de94bb6b4b 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -752,7 +752,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in bool has_top_shell = has_top_shell_layers && config->option("top_surface_density")->value > 0; bool has_bottom_shell = config->opt_int("bottom_shell_layers") > 0; bool has_solid_infill = has_top_shell_layers || has_bottom_shell; - toggle_line("sparse_infill_smooth_factor", pattern == ipHilbertCurve); + toggle_line("sparse_infill_smooth_factor", is_smoothable_infill_pattern(pattern, config->opt_int("fill_multiline"))); toggle_field("top_surface_pattern", has_top_shell); toggle_field("bottom_surface_pattern", has_bottom_shell); toggle_field("top_surface_density", has_top_shell_layers); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index c01c3e607f..fa6902de8d 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2887,7 +2887,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re float x = dynamic_cast(proj_cfg.option("wipe_tower_x"))->get_at(plate_id); float y = dynamic_cast(proj_cfg.option("wipe_tower_y"))->get_at(plate_id); float w = dynamic_cast(m_config->option("prime_tower_width"))->value; - float a = dynamic_cast(proj_cfg.option("wipe_tower_rotation_angle"))->value; + float a = dynamic_cast(m_config->option("wipe_tower_rotation_angle"))->value; // BBS float v = dynamic_cast(m_config->option("prime_volume"))->value; Vec3d plate_origin = ppl.get_plate(plate_id)->get_origin(); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 36a2dd5de1..8ab733696e 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3287,15 +3287,12 @@ bool GUI_App::on_init_inner() } } */ - copy_network_if_available(); if (scrn) { scrn->SetText(_L("Loading Plugins") + dots, 20); wxYield(); } - on_init_network(); - // Initialize plugins after network then register on_load callbacks so once the plugin loads finish, it gets registered automatically. // initialize() also installs the libslic3r hooks (capability resolver, // slicing-pipeline dispatcher) via plugin_hooks::install() -- no @@ -3324,6 +3321,9 @@ bool GUI_App::on_init_inner() } } + copy_network_if_available(); + on_init_network(); + if (m_agent) plugin_mgr.set_cloud_agent(std::dynamic_pointer_cast(m_agent->get_cloud_agent())); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index bf9d697a7a..5dd77f4389 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -12684,7 +12684,7 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed ModelWipeTower& tower = model.wipe_tower; tower.positions[plate_idx] = Vec2d(tower_x_opt->get_at(plate_idx), tower_y_opt->get_at(plate_idx)); - tower.rotation = proj_cfg.opt_float("wipe_tower_rotation_angle"); + tower.rotation = config.opt_float("wipe_tower_rotation_angle"); } } const GLGizmosManager& gizmos = get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager() : view3D->get_canvas3d()->get_gizmos_manager(); @@ -12794,7 +12794,7 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator ModelWipeTower& tower = model.wipe_tower; tower.positions[plate_idx] = Vec2d(tower_x_opt->get_at(plate_idx), tower_y_opt->get_at(plate_idx)); - tower.rotation = proj_cfg.opt_float("wipe_tower_rotation_angle"); + tower.rotation = config.opt_float("wipe_tower_rotation_angle"); } } const int layer_range_idx = it_snapshot->snapshot_data.layer_range_idx; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 09987ad021..bec3bed20b 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2790,7 +2790,7 @@ void TabPrint::build() optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline"); optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern"); optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized"); - optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_patterns#sparse-infill-smooth-factor"); + optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_infill#sparse-infill-smooth-factor"); optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction"); optgroup->append_single_option_line("sparse_infill_rotate_template", "strength_settings_infill_rotation_template_metalanguage"); optgroup->append_single_option_line("skin_infill_density", "strength_settings_patterns#locked-zag"); diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index 08f86de8a7..43afd4281d 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(${_TEST_NAME}_tests test_helpers.hpp test_cooling.cpp test_extrusion_entity.cpp + test_extrusion_processor.cpp test_fill.cpp test_flow.cpp test_gcode_timing.cpp diff --git a/tests/fff_print/test_extrusion_processor.cpp b/tests/fff_print/test_extrusion_processor.cpp new file mode 100644 index 0000000000..76e331d66a --- /dev/null +++ b/tests/fff_print/test_extrusion_processor.cpp @@ -0,0 +1,441 @@ +#include + +#include "libslic3r/AABBTreeLines.hpp" +#include "libslic3r/GCode/ExtrusionProcessor.hpp" +#include "libslic3r/GCodeReader.hpp" +#include "libslic3r/TriangleMesh.hpp" + +#include "test_helpers.hpp" + +#include +#include +#include +#include + +using namespace Slic3r; +using namespace Slic3r::Test; + +namespace { + +// Print settings the assertions below are derived from. +constexpr double caged_layer_height = 0.2; // mm +constexpr double caged_wall_width = 0.42; // mm, outer wall line width +constexpr double caged_outer_wall_speed = 200.; // mm/s +constexpr double caged_slow_speed = 100.; // mm/s, between every configured overhang speed (<= 50) and the wall speed + +// A wall running 0.2mm out over a previous layer whose edge dishes 0.03mm away from it in the middle, +// standing in for the endpoint readings a caged overhang perimeter takes: enough of a difference to +// print at another speed, but only a fraction of the distance at which slowdown begins. +constexpr double dished_wall_gap = 0.2; // mm, how far the wall runs out past the previous layer's edge +constexpr double dished_layer_depth = 0.03; // mm, how much further out the middle of it reads +constexpr double dished_min_distance = 0.042; // mm, the reading at which the configured speeds begin to slow down +// Every reading here is past that, so the whole wall is slowed and only the amount is in question. +constexpr float dished_end_reading = float(dished_wall_gap + 0.5 * caged_wall_width); +constexpr float dished_mid_reading = float(dished_end_reading + dished_layer_depth); +// The two readings are dished_layer_depth apart, so half of that tells them apart while still allowing +// for the points the passes after sampling add, which read a little further out than the ends do. +constexpr double dished_reading_tolerance = 0.5 * dished_layer_depth; + +// A 40 x 20 x 20 mm box with a 45 degree overhang cut into the y = 0 side. The sloped face spans +// x = 5.086 .. 34.914 only, so the full-height walls of the box cage both ends of every overhang +// perimeter: the endpoints look supported even though the span between them is not. +TriangleMesh caged_overhang_mesh() +{ + return TriangleMesh( + { + {5.0859987f, 10.167065f, 5.711731f}, {34.914257f, 10.167065f, 5.711731f}, + {34.914257f, 0.f, 15.878796f}, {5.0859995f, 0.f, 15.878796f}, + {0.f, 0.f, 0.f}, {0.f, 0.f, 20.f}, + {0.f, 20.f, 20.f}, {0.f, 20.f, 0.f}, + {40.f, 20.f, 20.f}, {40.f, 20.f, 0.f}, + {40.f, 0.f, 20.f}, {40.f, 0.f, 0.f}, + {34.914257f, 0.f, 0.f}, {5.0859995f, 0.f, 0.f}, + {34.914257f, 10.167065f, 0.f}, {5.0859995f, 10.167065f, 0.f}, + }, + { + {0, 1, 2}, {0, 2, 3}, {4, 5, 6}, {4, 6, 7}, {7, 6, 8}, {7, 8, 9}, + {9, 8, 10}, {9, 10, 11}, {12, 11, 10}, {5, 4, 13}, {5, 13, 3}, {2, 12, 10}, + {5, 3, 2}, {10, 5, 2}, {9, 11, 12}, {9, 12, 14}, {13, 4, 7}, {9, 14, 15}, + {15, 13, 7}, {7, 9, 15}, {8, 6, 5}, {8, 5, 10}, {14, 1, 0}, {14, 0, 15}, + {2, 1, 14}, {2, 14, 12}, {15, 0, 3}, {15, 3, 13}, + }); +} + +// Mesh geometry the wall filters below are derived from. +constexpr double caged_box_depth = 20.; // mm, the box spans y = 0 .. 20 +constexpr double caged_slope_face_sum = 15.878796; // mm, y + z of the sloped face, from its corners +// The sloped face spans this x range; outside it the box walls run full height. +constexpr double caged_slope_x_min = 5.0859995; +constexpr double caged_slope_x_max = 34.914257; +constexpr double caged_slope_span = caged_slope_x_max - caged_slope_x_min; // ~29.8 mm +// The z range the sloped face occupies, from the same fixture vertices. +constexpr double caged_slope_z_min = 5.711731; +constexpr double caged_slope_z_max = 15.878796; +// The lowest slope layer still sits on the solid body below the notch, so it is fully supported and +// runs at the outer wall speed by design. The caged span proper begins one layer above it. +constexpr double caged_span_z_min = caged_slope_z_min + caged_layer_height; + +// A layer printed at z is sliced at z - layer_height / 2, and the outer wall centreline sits half a +// line width inside the contour, so the wall on the slope satisfies y + z = 16.189. +constexpr double caged_slope_wall_sum = caged_slope_face_sum + 0.5 * caged_layer_height + 0.5 * caged_wall_width; +// Same inset on the fully supported y = 20 face, vertical over the whole height. +constexpr double caged_back_wall_y = caged_box_depth - 0.5 * caged_wall_width; +// And on the y = 0 face, which runs full height only outside the slope's x range. +constexpr double caged_front_wall_y = 0.5 * caged_wall_width; +// Arachne varies the wall width along a face, and the centreline inset is half that width, so a +// wall sits within about half a line width of where the nominal inset alone would put it. The +// faces being selected are millimetres apart, so this stays far from ambiguous. +constexpr double caged_wall_tolerance = 0.5 * caged_wall_width; + +// Feed rates in mm/min of the long outer wall extrusions `keep_line` selects. +template std::vector outer_wall_feed_rates(const std::string& gcode, KeepLine keep_line) +{ + std::vector feed_rates; + bool outer_wall = false; + GCodeReader parser; + parser.parse_buffer(gcode, [&feed_rates, &outer_wall, &keep_line](GCodeReader& self, const GCodeReader::GCodeLine& line) { + const std::string_view comment = line.comment(); + if (comment.find("FEATURE:") != std::string_view::npos || comment.find("TYPE:") != std::string_view::npos) + outer_wall = comment.find("Outer wall") != std::string_view::npos || + comment.find("External perimeter") != std::string_view::npos; + + if (outer_wall && line.extruding(self) && line.dist_XY(self) > 1.0 && keep_line(self, line)) + feed_rates.push_back(line.new_F(self)); + }); + + return feed_rates; +} + +// The caged 45 degree overhang: outer walls crossing the sloped face for most of its width, on the +// layers where the face genuinely overhangs. +// Both ends are tested against the slope plane rather than requiring a constant Y. Arachne's +// variable-width walls drift slightly in Y along the same slope (Y6.186 -> Y6.189 on one move), so +// a constant-Y filter matches almost nothing under Arachne and silently reduces its coverage. +// The length test excludes the cage walls: they are only as wide as the box is either side of the +// slope, but being vertical their y + z sweeps through the slope plane as z rises, so a couple of +// their fully supported moves would otherwise be counted as part of the span. +std::vector caged_slope_feed_rates(const std::string& gcode) +{ + return outer_wall_feed_rates(gcode, [](const GCodeReader& self, const GCodeReader::GCodeLine& line) { + const double z = line.new_Z(self); + return z > caged_span_z_min && z < caged_slope_z_max && + line.dist_XY(self) > 0.5 * caged_slope_span && + std::abs(self.y() + z - caged_slope_wall_sum) < caged_wall_tolerance && + std::abs(line.new_Y(self) + z - caged_slope_wall_sum) < caged_wall_tolerance; + }); +} + +// The opposite, fully supported face, skipping the initial layer and its own speed settings. +std::vector back_wall_feed_rates(const std::string& gcode) +{ + return outer_wall_feed_rates(gcode, [](const GCodeReader& self, const GCodeReader::GCodeLine& line) { + return line.new_Z(self) > 1.5 * caged_layer_height && + std::abs(self.y() - caged_back_wall_y) < caged_wall_tolerance && + std::abs(line.new_Y(self) - caged_back_wall_y) < caged_wall_tolerance; + }); +} + +// The first layer printed entirely above the slope. Its y = 0 wall runs the full width of the box. +const double caged_layer_above_slope_z = std::ceil(caged_slope_z_max / caged_layer_height) * caged_layer_height; + +// The parts of that wall standing on the cage rather than the slope, so on a contour identical to their own. +// Where the support changes is found by bisection, which stops at spans of 2mm, so the move spanning each end of +// the slope reaches a little way into the cage. Taking only the moves lying wholly outside the slope's x range +// leaves the wall that is unambiguously supported, without asserting how closely the bisection converged. +std::vector cage_shoulder_feed_rates(const std::string& gcode) +{ + return outer_wall_feed_rates(gcode, [](const GCodeReader& self, const GCodeReader::GCodeLine& line) { + return std::abs(line.new_Z(self) - caged_layer_above_slope_z) < 0.5 * caged_layer_height && + std::abs(self.y() - caged_front_wall_y) < caged_wall_tolerance && + std::abs(line.new_Y(self) - caged_front_wall_y) < caged_wall_tolerance && + (std::max(self.x(), line.new_X(self)) <= caged_slope_x_min || + std::min(self.x(), line.new_X(self)) >= caged_slope_x_max); + }); +} + +// The readings a 40mm wall takes over a previous layer whose edge falls away by 0.03mm towards the +// middle: both ends read the same, and the middle reads slightly further out over air. Whether that +// middle reading survives is what decides the speed the wall is printed at. +std::vector> sampled_wall_over_dished_layer(const std::function& distance_to_speed) +{ + const AABBTreeLines::LinesDistancer prev_layer(std::vector{ + {{0., 0.}, {20., -dished_layer_depth}}, + {{20., -dished_layer_depth}, {40., 0.}}, + {{40., 0.}, {40., -10.}}, + {{40., -10.}, {0., -10.}}, + {{0., -10.}, {0., 0.}}, + }); + const Points wall{Point::new_scale(0., dished_wall_gap), Point::new_scale(40., dished_wall_gap)}; + + return estimate_points_properties(wall, prev_layer, caged_wall_width, -1.f, + dished_min_distance, distance_to_speed); +} + +// A straight, otherwise supported wall over a previous-layer boundary with a 2mm-wide pocket. Moving the +// pocket between x = 10 and x = 20 covers both discovery away from the wall's midpoint and refinement around +// a midpoint that has already been discovered. The current wall is inset half its width from the flat boundary, +// so its supported readings are zero after the estimator applies its boundary offset. +constexpr double narrow_pocket_wall_length = 40.; +constexpr double narrow_pocket_width = 2.; +constexpr double narrow_pocket_depth = 0.3; + +std::vector> sampled_wall_over_narrow_pocket( + double pocket_center, const std::function& distance_to_speed) +{ + const double pocket_left = pocket_center - 0.5 * narrow_pocket_width; + const double pocket_right = pocket_center + 0.5 * narrow_pocket_width; + const AABBTreeLines::LinesDistancer prev_layer(std::vector{ + {{0., 0.}, {pocket_left, 0.}}, + {{pocket_left, 0.}, {pocket_left, -narrow_pocket_depth}}, + {{pocket_left, -narrow_pocket_depth}, {pocket_right, -narrow_pocket_depth}}, + {{pocket_right, -narrow_pocket_depth}, {pocket_right, 0.}}, + {{pocket_right, 0.}, {narrow_pocket_wall_length, 0.}}, + {{narrow_pocket_wall_length, 0.}, {narrow_pocket_wall_length, -10.}}, + {{narrow_pocket_wall_length, -10.}, {0., -10.}}, + {{0., -10.}, {0., 0.}}, + }); + const double wall_y = -0.5 * caged_wall_width; + const Points wall{Point::new_scale(0., wall_y), Point::new_scale(narrow_pocket_wall_length, wall_y)}; + + return estimate_points_properties(wall, prev_layer, caged_wall_width, -1.f, + dished_min_distance, distance_to_speed); +} + +// A cross section that grows a layer's worth on the two faces meeting at either end of a wall, as any +// 45 degree overhang does. The wall itself stands on a contour identical to its own, but its ends sit +// where the growing faces cut the corners off, and the previous layer's edge there is nearer than the +// half line width the centreline is inset by. Both ends therefore read an overhang while everything +// between them reads supported: the reverse of the caged span, and the case the sampling above must +// leave to the passes after it. +constexpr double stepped_wall_inset = 0.5 * caged_wall_width; // mm, centreline inset from the contour +constexpr double stepped_end_gap = stepped_wall_inset - caged_layer_height; // mm, how far inside the corner ends up +constexpr double stepped_wall_span = 30.; // mm, the length of the wall + +std::vector> sampled_wall_between_growing_corners(const std::function& distance_to_speed) +{ + const AABBTreeLines::LinesDistancer prev_layer(std::vector{ + {{0., 0.}, {32., 0.}}, + {{32., 0.}, {32., -stepped_wall_span}}, + {{32., -stepped_wall_span}, {0., -stepped_wall_span}}, + {{0., -stepped_wall_span}, {0., 0.}}, + }); + const Points wall{Point::new_scale(stepped_wall_inset, -stepped_end_gap), + Point::new_scale(stepped_wall_inset, stepped_end_gap - stepped_wall_span)}; + + return estimate_points_properties(wall, prev_layer, caged_wall_width, -1.f, + dished_min_distance, distance_to_speed); +} + +// How much of a path is printed below the speed a fully supported reading gives. A segment is printed +// at the lower of the speeds its ends read. +double slowed_length(const std::vector>& points, const std::function& distance_to_speed) +{ + double length = 0.; + for (size_t i = 0; i + 1 < points.size(); ++i) + if (std::min(distance_to_speed(points[i].distance), distance_to_speed(points[i + 1].distance)) < distance_to_speed(0.f)) + length += (points[i + 1].position - points[i].position).norm(); + return length; +} + +float furthest_reading(const std::vector>& points) +{ + return std::max_element(points.begin(), points.end(), [](const ExtendedPoint<2>& l, const ExtendedPoint<2>& r) { + return l.distance < r.distance; + })->distance; +} + +DynamicPrintConfig caged_overhang_config(const char* wall_generator){ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + {"nozzle_diameter", "0.4"}, + {"initial_layer_print_height", caged_layer_height}, + {"layer_height", caged_layer_height}, + {"line_width", caged_wall_width}, + {"outer_wall_line_width", caged_wall_width}, + {"inner_wall_line_width", "0.45"}, + {"wall_loops", "2"}, + {"wall_generator", wall_generator}, + {"wall_sequence", "inner wall/outer wall"}, + {"sparse_infill_density", "15%"}, + {"detect_overhang_wall", "1"}, + {"enable_overhang_speed", "1"}, + {"slowdown_for_curled_perimeters", "0"}, + {"zaa_enabled", "0"}, + {"outer_wall_speed", caged_outer_wall_speed}, + {"inner_wall_speed", "300"}, + {"overhang_1_4_speed", "0"}, + {"overhang_2_4_speed", "50"}, + {"overhang_3_4_speed", "30"}, + {"overhang_4_4_speed", "10"}, + {"bridge_speed", "50"}, + {"filament_max_volumetric_speed", "22"}, + {"slow_down_for_layer_cooling", "0"}, + {"slow_down_layers", "0"}, // Nothing but the overhang settings may lower a wall speed + }); + return config; +} + +std::string caged_overhang_gcode(const char* wall_generator) +{ + Print print; + Model model; + init_print(std::vector{caged_overhang_mesh()}, print, model, caged_overhang_config(wall_generator), nullptr, + false); + return gcode(print); +} + +// Reports the matched move count alongside the extremes, so a filter that selected nothing is +// distinguishable from a span that simply was not slowed. +void info_feed_rates(const char* span, const std::vector& feed_rates) +{ + UNSCOPED_INFO("matched " << feed_rates.size() << " " << span << " moves"); + if (!feed_rates.empty()) { + const auto extremes = std::minmax_element(feed_rates.begin(), feed_rates.end()); + UNSCOPED_INFO("slowest " << *extremes.first / MM_PER_MIN << " mm/s, fastest " << *extremes.second / MM_PER_MIN << " mm/s"); + } +} + +} // namespace + +// Classic reproduces the endpoint-sampling bug: it emits the span as one long move whose endpoints +// both read as supported, so endpoint-only sampling never slows it. Arachne's endpoints already read +// as overhanging, but their placement near the cage makes the inferred support vary by layer. Arachne +// parity is therefore part of this regression's scope: both generators must classify the unsupported +// interior of the same 45-degree span consistently. +TEST_CASE("Caged external overhangs are slowed along their span", "[ExtrusionProcessor][Regression]") +{ + const char* wall_generator = GENERATE("classic", "arachne"); + INFO("wall generator: " << wall_generator); + + const std::vector feed_rates = caged_slope_feed_rates(caged_overhang_gcode(wall_generator)); + info_feed_rates("caged slope", feed_rates); + + REQUIRE_FALSE(feed_rates.empty()); + + // The endpoint bug left Classic at the full wall speed, while Arachne's cage-adjacent endpoint + // samples selected much faster bands on some layers. The whole span must stay in the slowed range + // for both generators, without requiring their different path segmentations to match. + const double fastest = *std::max_element(feed_rates.begin(), feed_rates.end()); + REQUIRE(fastest < caged_slow_speed * MM_PER_MIN); +} + +// The other side of the fix: the midpoint probe fires on every long external perimeter, so a +// regression that over-slows would leave the test above green. A fully supported wall must keep the +// speed it was configured with. +TEST_CASE("Supported vertical walls keep their normal speed", "[ExtrusionProcessor][Regression]") +{ + const char* wall_generator = GENERATE("classic", "arachne"); + INFO("wall generator: " << wall_generator); + + const std::vector feed_rates = back_wall_feed_rates(caged_overhang_gcode(wall_generator)); + info_feed_rates("back wall", feed_rates); + + REQUIRE_FALSE(feed_rates.empty()); + + const double slowest = *std::min_element(feed_rates.begin(), feed_rates.end()); + REQUIRE(slowest >= caged_slow_speed * MM_PER_MIN); +} + +// The slope's top edge falls mid layer, so the first layer above it still stands 0.179mm proud of the layer +// below wherever that layer was still on the slope. That is a real overhang and is slowed, but it ends with the +// slope: outside the slope's x range the box runs full height, so the same wall stands on a contour identical to +// its own. Sampling the interior of that wall at a single point reported one support reading for all of it and +// slowed these fully supported ends along with the rest. +TEST_CASE("Wall sections beside a caged overhang keep their normal speed", "[ExtrusionProcessor][Regression]") +{ + const char* wall_generator = GENERATE("classic", "arachne"); + INFO("wall generator: " << wall_generator); + + const std::vector feed_rates = cage_shoulder_feed_rates(caged_overhang_gcode(wall_generator)); + info_feed_rates("cage shoulder", feed_rates); + + REQUIRE_FALSE(feed_rates.empty()); + + const double slowest = *std::min_element(feed_rates.begin(), feed_rates.end()); + REQUIRE_THAT(slowest / MM_PER_MIN, Catch::Matchers::WithinRel(caged_outer_wall_speed, 0.01)); +} + +// A wall is printed at the lower of the speeds its ends read, so a reading only earns a point in the +// path where it prints at a different speed from the readings around it. Judging that on the readings +// themselves rather than the speeds they produce was too coarse: the configured speeds interpolate +// between their sections, so readings a fraction of the slowdown threshold apart still print more than +// 10% apart, and a real 45 degree overhang had its true reading dropped as if it agreed with its ends. +// The ends then chose the speed on their own, and being next to the walls either side of the overhang +// they read differently from layer to layer, banding an overhang that should have been uniform. +TEST_CASE("An overhang reading is kept whenever it changes the speed", "[ExtrusionProcessor][Regression]") +{ + // A steep speed curve, of the kind the configured overhang speeds interpolate across. + const std::vector> points = + sampled_wall_over_dished_layer([](float distance) { return std::round(200.f - 400.f * distance); }); + + REQUIRE_THAT(furthest_reading(points), Catch::Matchers::WithinAbs(dished_mid_reading, dished_reading_tolerance)); +} + +// The complement, and why the readings alone were tempting: a reading that prints at the same speed as +// its neighbours cannot change the G-code, so sampling must leave the path alone however far out it is. +TEST_CASE("An overhang reading is dropped when the speed is unchanged", "[ExtrusionProcessor]") +{ + // A flat speed curve, of the kind a single configured overhang speed produces. + const std::vector> points = sampled_wall_over_dished_layer([](float) { return 50.f; }); + + REQUIRE_THAT(furthest_reading(points), Catch::Matchers::WithinAbs(dished_end_reading, dished_reading_tolerance)); +} + +TEST_CASE("Coarse probing detects an unsupported pocket away from the wall midpoint", + "[ExtrusionProcessor][Regression]") +{ + const std::function distance_to_speed = [](float distance) { return distance <= 0.2f ? 100.f : 50.f; }; + const std::vector> points = + sampled_wall_over_narrow_pocket(0.25 * narrow_pocket_wall_length, distance_to_speed); + const double slowed = slowed_length(points, distance_to_speed); + + REQUIRE(slowed > 0.); + REQUIRE(slowed < 5.); +} + +TEST_CASE("Coarse probing brackets a narrow slowdown at the wall midpoint", + "[ExtrusionProcessor][Regression]") +{ + // Half of the pocket reading still maps to full speed. A matching probe in either half therefore must not + // prune that half before a supported point has been found close enough to bracket the slow midpoint. + const std::function distance_to_speed = [](float distance) { return distance <= 0.2f ? 100.f : 50.f; }; + const std::vector> points = + sampled_wall_over_narrow_pocket(0.5 * narrow_pocket_wall_length, distance_to_speed); + const double slowed = slowed_length(points, distance_to_speed); + + REQUIRE(slowed > 0.); + REQUIRE(slowed < 5.); +} + +// Sampling probes the interior, so it must not answer for the ends. On a supported wall between two +// corners that read an overhang, the reading that differs is the end's own, and the pass that ends a +// slowdown an end reads places its point from how far out that end is. Sampling took the difference as +// its own to report and put a point at the nearest position bisection had reached instead, which both +// sits further along the wall and leaves too little of it for that pass to run on, so the corner +// slowdown ran millimetres up an otherwise supported wall. Its length grows with the wall, so on a +// model whose cross section keeps growing it reads as a stair stepped band up the corner. +TEST_CASE("A supported wall between overhanging corners is slowed no further than its ends require", + "[ExtrusionProcessor][Regression]") +{ + // A steep speed curve, so the ends and the interior between them print at clearly different speeds. + const std::function distance_to_speed = [](float distance) { + return std::round(float(caged_outer_wall_speed) - 400.f * distance); + }; + + const double sampled = slowed_length(sampled_wall_between_growing_corners(distance_to_speed), distance_to_speed); + // The same wall with sampling switched off: what the endpoint driven passes alone make of the corners. + const double unsampled = slowed_length(sampled_wall_between_growing_corners({}), distance_to_speed); + + // The corners do read an overhang, so there is a slowdown for sampling to have lengthened. + REQUIRE(unsampled > 0.); + REQUIRE(sampled <= unsampled); +} + +TEST_CASE("Benchmark caged overhang interior sampling", "[ExtrusionProcessor][!benchmark]"){ + const char* wall_generator = GENERATE("classic", "arachne"); + + BENCHMARK(wall_generator) + { + return caged_overhang_gcode(wall_generator); + }; +} diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index 5fbce5a342..07460d3990 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -698,3 +698,290 @@ TEST_CASE("Solid infill direction offsets every layer when no template is set", CHECK(delta == 30); } } + +TEST_CASE("Honeycomb infill rounds its cell corners with the smooth factor", "[Fill]") +{ + // A cell whose sides are several times the line width, so that the corners have room to be rounded. + const double spacing = 0.45; + const double density = 0.1; + auto fill = [spacing, density](double smooth_factor) { + std::unique_ptr filler(Slic3r::Fill::new_from_type("honeycomb")); + filler->spacing = spacing; + + FillParams params; + params.density = float(density); + params.dont_adjust = true; + // Keep the fragments apart, so that only the turns of the pattern itself are measured. + params.anchor_length_max = 0.f; + params.smooth_factor = smooth_factor; + + Slic3r::ExPolygon square{ Slic3r::Points{ + Point::new_scale(0., 0.), Point::new_scale(50., 0.), Point::new_scale(50., 50.), Point::new_scale(0., 50.) } }; + Slic3r::Surface surface(stInternal, square); + return filler->fill_surface(&surface, params); + }; + + // Cosine of the sharpest turn of any of the paths, 1 meaning none of them turns at all. + auto sharpest_turn_cosine = [](const Slic3r::Polylines &polylines) { + double sharpest = 1.; + for (const Polyline &polyline : polylines) + for (size_t i = 1; i + 1 < polyline.size(); ++i) { + const Vec2d incoming = (polyline[i] - polyline[i - 1]).cast().normalized(); + const Vec2d outgoing = (polyline[i + 1] - polyline[i]).cast().normalized(); + sharpest = std::min(sharpest, incoming.dot(outgoing)); + } + return sharpest; + }; + auto point_count = [](const Slic3r::Polylines &polylines) { + return std::accumulate(polylines.begin(), polylines.end(), size_t(0), + [](size_t count, const Polyline &polyline) { return count + polyline.size(); }); + }; + + const Slic3r::Polylines sharp = fill(0.); + const Slic3r::Polylines smooth = fill(1.); + + REQUIRE(!sharp.empty()); + REQUIRE(smooth.size() == sharp.size()); + REQUIRE(point_count(smooth) > point_count(sharp)); + // The cell corners turn by 60 degrees; smoothing replaces them by gentle curves. + REQUIRE(sharpest_turn_cosine(sharp) < 0.6); + REQUIRE(sharpest_turn_cosine(smooth) > 0.9); +} + +// Point count, number of turns sharper than 25 degrees and length of the sparse infill of a print. +// A rounded corner is a run of much gentler turns, so smoothing shows up as fewer sharp ones. +struct SparseInfillShape { + size_t point_count { 0 }; + size_t sharp_turns { 0 }; + size_t path_count { 0 }; + double length { 0. }; +}; + +static SparseInfillShape sparse_infill_shape(const Print &print) +{ + SparseInfillShape shape; + + auto account = [&shape](const ExtrusionPath &path) { + if (!sparse_role(path.role())) + return; + const Points3 &pts = path.polyline.points; + ++shape.path_count; + shape.point_count += pts.size(); + for (size_t i = 1; i < pts.size(); ++i) + shape.length += (pts[i] - pts[i - 1]).head<2>().cast().norm(); + for (size_t i = 1; i + 1 < pts.size(); ++i) { + const Vec2d incoming = (pts[i] - pts[i - 1]).head<2>().cast(); + const Vec2d outgoing = (pts[i + 1] - pts[i]).head<2>().cast(); + if (incoming.squaredNorm() > 0. && outgoing.squaredNorm() > 0. && + incoming.normalized().dot(outgoing.normalized()) < 0.9) + ++shape.sharp_turns; + } + }; + + for (const Layer *layer : print.objects().front()->layers()) + for (const LayerRegion *region : layer->regions()) + for (const ExtrusionEntity *entity : region->fills.flatten().entities) { + if (auto *path = dynamic_cast(entity)) + account(*path); + else if (auto *multi = dynamic_cast(entity)) + for (const ExtrusionPath &p : multi->paths) + account(p); + else if (auto *loop = dynamic_cast(entity)) + for (const ExtrusionPath &p : loop->paths) + account(p); + } + return shape; +} + +TEST_CASE("Lightning infill rounds the turns of its branches with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "lightning"}, + {"sparse_infill_density", "15%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + // The branch turns are replaced by curves, which cut the corners off and take more points to + // describe. The turns where two branches are joined into one path stay sharp. + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Concentric infill rounds its loops with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "concentric"}, + {"sparse_infill_density", "20%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Cross hatch infill rounds its transition layers with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "crosshatch"}, + {"sparse_infill_density", "20%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Trapezoidal grid infill rounds its corners only with more than one line", "[Fill]") +{ + auto shape_for = [](int multiline, const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "grid"}, + {"sparse_infill_density", "20%"}, + {"fill_multiline", multiline}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for(2, "0%"); + const SparseInfillShape smooth = shape_for(2, "100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); + + // A single line per infill wall is the plain crossing line grid, which has no corner of its own. + const SparseInfillShape single_sharp = shape_for(1, "0%"); + const SparseInfillShape single_smooth = shape_for(1, "100%"); + REQUIRE(single_sharp.point_count > 0); + REQUIRE(single_smooth.point_count == single_sharp.point_count); + REQUIRE(single_smooth.length == single_sharp.length); +} + +TEST_CASE("3D honeycomb infill rounds its octahedral waves with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "3dhoneycomb"}, + {"sparse_infill_density", "20%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Smoothed concentric infill stays inside the fill region", "[Fill][Regression]") +{ + // The concentric loops are offsets of the fill region and are never clipped to it, so a corner + // rounded across its boundary ends up in a hole or over a wall. Rounding cuts toward the inside of + // the turn, which leaves the region at every corner of a hole, and in a region thinner than the + // curve even at a corner turning inwards. + const bool thin_region = GENERATE(false, true); + ExPolygon region; + if (thin_region) { + // An L of two 1.2mm wide arms: cutting the corner they meet at crosses both of them. + region = ExPolygon{ Slic3r::Points{ + Point::new_scale(0., 0.), Point::new_scale(20., 0.), Point::new_scale(20., 1.2), + Point::new_scale(1.2, 1.2), Point::new_scale(1.2, 20.), Point::new_scale(0., 20.) } }; + } else { + region = ExPolygon{ Slic3r::Points{ Point::new_scale(0., 0.), Point::new_scale(50., 0.), + Point::new_scale(50., 50.), Point::new_scale(0., 50.) }, + Slic3r::Points{ Point::new_scale(30., 20.), Point::new_scale(30., 30.), + Point::new_scale(20., 30.), Point::new_scale(20., 20.) } }; + } + CAPTURE(thin_region); + + auto fill = [®ion](double smooth_factor) { + std::unique_ptr filler(Slic3r::Fill::new_from_type("concentric")); + filler->spacing = 0.45; + + FillParams params; + params.density = 0.1f; + params.dont_adjust = true; + params.smooth_factor = smooth_factor; + + Slic3r::Surface surface(stInternal, region); + return filler->fill_surface(&surface, params); + }; + auto point_count = [](const Slic3r::Polylines &polylines) { + return std::accumulate(polylines.begin(), polylines.end(), size_t(0), + [](size_t count, const Polyline &polyline) { return count + polyline.size(); }); + }; + + const Slic3r::Polylines sharp = fill(0.); + const Slic3r::Polylines smooth = fill(1.); + REQUIRE(!sharp.empty()); + + // Nothing leaves the fill region, which the unrounded loops already touch from the inside. + const ExPolygons bounds = offset_ex(region, float(SCALED_EPSILON)); + REQUIRE(diff_pl(sharp, bounds).empty()); + REQUIRE(diff_pl(smooth, bounds).empty()); + // The corners that the region has room for are still rounded. + if (!thin_region) + REQUIRE(point_count(smooth) > point_count(sharp)); +} + +TEST_CASE("Smoothing multiline lightning infill keeps its outlines connected", "[Fill][Regression]") +{ + // With more than one line per infill wall, the branches are printed as outlines drawn around them, + // and the outlines of branches that run close to each other merge into one. Rounding the branches + // before those outlines are built moves them apart, which breaks the merged outlines up into + // separate loops - many more of them, each needing its own travel move. + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "lightning"}, + {"sparse_infill_density", "50%"}, + {"fill_multiline", 2}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.path_count > 0); + REQUIRE(smooth.path_count <= sharp.path_count); + // The outlines are still rounded. + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); +} diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 1ad299473c..bc10bb4f73 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(${_TEST_NAME}_tests test_preset_setting_id.cpp test_preset_diff.cpp test_elephant_foot_compensation.cpp + test_fill_corner_smoothing.cpp test_fill_plane_path.cpp test_geometry.cpp test_multimaterial_segmentation.cpp diff --git a/tests/libslic3r/test_fill_corner_smoothing.cpp b/tests/libslic3r/test_fill_corner_smoothing.cpp new file mode 100644 index 0000000000..f2c25e816d --- /dev/null +++ b/tests/libslic3r/test_fill_corner_smoothing.cpp @@ -0,0 +1,173 @@ +#include + +#include +#include +#include + +#include "libslic3r/Fill/FillCornerSmoothing.hpp" +#include "libslic3r/Polyline.hpp" +#include "libslic3r/libslic3r.h" + +using namespace Slic3r; + +namespace { + +// A right angle turn, with the outgoing leg ten times longer than the incoming one. +Polyline asymmetric_corner() +{ + return Polyline{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 100.) }; +} + +double max_turn_cosine(const Polyline &polyline) +{ + double sharpest = 1.; + for (size_t i = 1; i + 1 < polyline.size(); ++i) { + const Vec2d incoming = (polyline[i] - polyline[i - 1]).cast().normalized(); + const Vec2d outgoing = (polyline[i + 1] - polyline[i]).cast().normalized(); + sharpest = std::min(sharpest, incoming.dot(outgoing)); + } + return sharpest; +} + +bool contains(const Polyline &polyline, const Point &point) +{ + return std::find(polyline.points.begin(), polyline.points.end(), point) != polyline.points.end(); +} + +const double tolerance = scaled(0.0125); + +} // namespace + +TEST_CASE("Corner smoothing replaces a sharp vertex by a curve", "[FillCornerSmoothing]") +{ + const Polyline sharp = asymmetric_corner(); + Polyline smooth = sharp; + smooth_polyline_corners(smooth, 1., tolerance); + + REQUIRE(smooth.size() > sharp.size()); + REQUIRE(smooth.front() == sharp.front()); + REQUIRE(smooth.back() == sharp.back()); + // The right angle is gone, every remaining turn is a gentle one. + REQUIRE(max_turn_cosine(sharp) < 0.1); + REQUIRE(max_turn_cosine(smooth) > 0.9); + REQUIRE(smooth.length() < sharp.length()); +} + +TEST_CASE("Corner smoothing keeps the path untouched at a zero factor", "[FillCornerSmoothing]") +{ + const Polyline sharp = asymmetric_corner(); + + Polyline none = sharp; + smooth_polyline_corners(none, 0., tolerance); + REQUIRE(none.points == sharp.points); + + Polyline invalid = sharp; + smooth_polyline_corners(invalid, std::numeric_limits::quiet_NaN(), tolerance); + REQUIRE(invalid.points == sharp.points); +} + +TEST_CASE("Corner smoothing consumes at most half of the shorter leg", "[FillCornerSmoothing]") +{ + // The curve must not reach beyond the middle of either adjoining segment, otherwise the curves of + // two adjacent corners would overlap. The shorter leg is 10mm long, so the corner at (10, 0) is + // left 5mm before it and rejoined 5mm past it, even though the other leg is 100mm long. + Polyline smooth = asymmetric_corner(); + smooth_polyline_corners(smooth, 1., tolerance); + + REQUIRE(contains(smooth, Point::new_scale(5., 0.))); + REQUIRE(contains(smooth, Point::new_scale(10., 5.))); + // A Bezier curve stays within the convex hull of its control points, so the rounded path stays + // inside the box spanned by the two legs. + for (const Point &point : smooth.points) { + REQUIRE(point.x() >= 0); + REQUIRE(point.y() >= 0); + REQUIRE(point.x() <= Point::new_scale(10., 0.).x()); + REQUIRE(point.y() <= Point::new_scale(0., 100.).y()); + } +} + +TEST_CASE("Corner smoothing scales the curve with the factor", "[FillCornerSmoothing]") +{ + Polyline half = asymmetric_corner(); + smooth_polyline_corners(half, 0.5, tolerance); + Polyline full = asymmetric_corner(); + smooth_polyline_corners(full, 1., tolerance); + + // Half of the factor leaves the 10mm leg half as far from the corner. + REQUIRE(contains(half, Point::new_scale(7.5, 0.))); + REQUIRE(contains(full, Point::new_scale(5., 0.))); + // A larger factor rounds a wider portion of the legs, cutting more of the corner off. + REQUIRE(full.length() < half.length()); +} + +TEST_CASE("Corner smoothing leaves hairpins sharp", "[FillCornerSmoothing]") +{ + // Both ends of a curve replacing a nearly reversing turn coincide, which would round the hairpin + // into a degenerate loop instead of a tip. + Polyline hairpin{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(0., 0.5) }; + const Polyline sharp = hairpin; + smooth_polyline_corners(hairpin, 1., tolerance); + REQUIRE(hairpin == sharp); +} + +TEST_CASE("Corner smoothing follows the flattening tolerance", "[FillCornerSmoothing]") +{ + Polyline coarse = asymmetric_corner(); + smooth_polyline_corners(coarse, 1., scaled(0.2)); + Polyline fine = asymmetric_corner(); + smooth_polyline_corners(fine, 1., scaled(0.001)); + + REQUIRE(fine.size() > coarse.size()); + REQUIRE(fine.front() == coarse.front()); + REQUIRE(fine.back() == coarse.back()); +} + +TEST_CASE("Corner smoothing emits no zero length segments", "[FillCornerSmoothing]") +{ + // Fully smoothed adjacent corners meet at the midpoint of the segment they share. + Polyline zigzag; + for (int i = 0; i < 8; ++i) + zigzag.points.emplace_back(Point::new_scale(i, i % 2 ? 1. : 0.)); + smooth_polyline_corners(zigzag, 1., tolerance); + + for (size_t i = 1; i < zigzag.size(); ++i) + REQUIRE((zigzag[i] - zigzag[i - 1]).cast().squaredNorm() > 0.); +} + +TEST_CASE("Corner smoothing rounds every vertex of a polygon", "[FillCornerSmoothing]") +{ + // A polygon closes implicitly, so none of its corners may stay sharp, not even the first one. + const Polygon square{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 10.), + Point::new_scale(0., 10.) }; + Polygons smooth{ square }; + smooth_polygons_corners(smooth, 1., tolerance); + const Polyline rounded = smooth.front().split_at_first_point(); + + REQUIRE(smooth.front().size() > square.size()); + REQUIRE(max_turn_cosine(rounded) > 0.9); + // The turn from the closing segment back into the first one must be gentle as well. + const Vec2d incoming = (rounded[rounded.size() - 1] - rounded[rounded.size() - 2]).cast().normalized(); + const Vec2d outgoing = (rounded[1] - rounded[0]).cast().normalized(); + REQUIRE(incoming.dot(outgoing) > 0.9); + // None of the corners is cut by more than half of a 10mm side. + for (const Point &point : smooth.front().points) { + REQUIRE(point.x() >= 0); + REQUIRE(point.y() >= 0); + REQUIRE(point.x() <= Point::new_scale(10., 0.).x()); + REQUIRE(point.y() <= Point::new_scale(0., 10.).y()); + } +} + +TEST_CASE("Corner smoothing keeps the ends of a path that returns to its start", "[FillCornerSmoothing][Regression]") +{ + // A branch of a lightning tree walks out and retraces its way back, ending where it started. Its + // ends are two free ends that happen to coincide, and joining them would close it into a loop. + Polyline retrace{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 10.), + Point::new_scale(5., 10.), Point::new_scale(0., 0.) }; + const Polyline sharp = retrace; + smooth_polyline_corners(retrace, 1., tolerance); + + REQUIRE(retrace.size() > sharp.size()); + REQUIRE(retrace.front() == sharp.front()); + REQUIRE(retrace.back() == sharp.back()); +} diff --git a/tests/libslic3r/test_fill_plane_path.cpp b/tests/libslic3r/test_fill_plane_path.cpp index bbb75dce58..7fc7f4a6b0 100644 --- a/tests/libslic3r/test_fill_plane_path.cpp +++ b/tests/libslic3r/test_fill_plane_path.cpp @@ -27,6 +27,31 @@ public: } }; +class TestableOctagramSpiral : public FillOctagramSpiral +{ +public: + Points generate_points(double resolution, double smooth_factor = 0., coord_t max_coordinate = 7) + { + InfillPolylineOutput output(output_scale); + FillParams params; + params.smooth_factor = smooth_factor; + FillOctagramSpiral::generate(-max_coordinate, -max_coordinate, max_coordinate, max_coordinate, resolution, params, output); + return std::move(output.result()); + } +}; + +// Cosine of the sharpest turn of a path, 1 meaning it has no turn at all. +double sharpest_turn_cosine(const Points &points) +{ + double sharpest = 1.; + for (size_t i = 1; i + 1 < points.size(); ++i) { + const Vec2d incoming = (points[i] - points[i - 1]).cast().normalized(); + const Vec2d outgoing = (points[i + 1] - points[i]).cast().normalized(); + sharpest = std::min(sharpest, incoming.dot(outgoing)); + } + return sharpest; +} + double path_length(const Points &points) { double length = 0.; @@ -146,6 +171,35 @@ TEST_CASE("Hilbert smoothing joins straight segments with continuous curvature", REQUIRE(fine_entry_curvature < 0.25 * coarse_entry_curvature); } +TEST_CASE("Octagram spiral smoothing rounds the turns of the spiral", "[FillPlanePath]") +{ + const Points sharp = TestableOctagramSpiral().generate_points(0.005); + const Points smooth = TestableOctagramSpiral().generate_points(0.005, 1.); + + REQUIRE(smooth.size() > sharp.size()); + REQUIRE(smooth.front() == sharp.front()); + REQUIRE(smooth.back() == sharp.back()); + // The spiral alternates between 90 and 135 degree turns; both are rounded into gentle ones. + REQUIRE(sharpest_turn_cosine(sharp) < -0.7); + REQUIRE(sharpest_turn_cosine(smooth) > 0.9); + + for (size_t i = 1; i < smooth.size(); ++i) + REQUIRE((smooth[i] - smooth[i - 1]).cast().squaredNorm() > 0.); +} + +TEST_CASE("Octagram spiral smooth factor controls corner curvature", "[FillPlanePath]") +{ + const Points sharp = TestableOctagramSpiral().generate_points(0.005); + const Points half_smooth = TestableOctagramSpiral().generate_points(0.005, 0.5); + const Points full_smooth = TestableOctagramSpiral().generate_points(0.005, 1.); + const Points invalid_factor = TestableOctagramSpiral().generate_points( + 0.005, std::numeric_limits::quiet_NaN()); + + REQUIRE(path_length(full_smooth) < path_length(half_smooth)); + REQUIRE(path_length(half_smooth) < path_length(sharp)); + REQUIRE(invalid_factor == sharp); +} + TEST_CASE("Hilbert curve smooth factor controls corner curvature", "[FillPlanePath]") { const Points sharp = TestableHilbertCurve().generate_points(0.005);