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\nYour 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/build_release_vs.bat b/build_release_vs.bat
index 3288beda4c..78419dadf5 100644
--- a/build_release_vs.bat
+++ b/build_release_vs.bat
@@ -152,7 +152,7 @@ echo on
set CMAKE_POLICY_VERSION_MINIMUM=3.5
if "%USE_NINJA%"=="1" (
cmake .. -G %CMAKE_GENERATOR% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type%
- cmake --build . --config %build_type% --target ALL_BUILD
+ cmake --build . --config %build_type% --target all
) else (
cmake .. -G %CMAKE_GENERATOR% -A %arch% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target ALL_BUILD -- -m
diff --git a/deps/TBB/MSVC.cmake b/deps/TBB/MSVC.cmake
new file mode 100644
index 0000000000..d7984bff80
--- /dev/null
+++ b/deps/TBB/MSVC.cmake
@@ -0,0 +1,98 @@
+# Copyright (c) 2020-2021 Intel Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set(TBB_LINK_DEF_FILE_FLAG ${CMAKE_LINK_DEF_FILE_FLAG})
+set(TBB_DEF_FILE_PREFIX win${TBB_ARCH})
+
+# Workaround for CMake issue https://gitlab.kitware.com/cmake/cmake/issues/18317.
+# TODO: consider use of CMP0092 CMake policy.
+string(REGEX REPLACE "/W[0-4]" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+
+set(TBB_WARNING_LEVEL $<$:/W4> $<$:/WX>)
+
+# Warning suppression C4324: structure was padded due to alignment specifier
+set(TBB_WARNING_SUPPRESS /wd4324)
+set(TBB_TEST_COMPILE_FLAGS /bigobj)
+
+if (MSVC_VERSION LESS_EQUAL 1900)
+ # Warning suppression C4503 for VS2015 and earlier:
+ # decorated name length exceeded, name was truncated.
+ # More info can be found at
+ # https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-1-c4503
+ set(TBB_TEST_COMPILE_FLAGS ${TBB_TEST_COMPILE_FLAGS} /wd4503)
+endif()
+
+set(TBB_LIB_COMPILE_FLAGS -D_CRT_SECURE_NO_WARNINGS /GS)
+set(TBB_COMMON_COMPILE_FLAGS /volatile:iso /FS /EHsc)
+
+# Ignore /WX set through add_compile_options() or added to CMAKE_CXX_FLAGS if TBB_STRICT is disabled.
+if (NOT TBB_STRICT AND COMMAND tbb_remove_compile_flag)
+ tbb_remove_compile_flag(/WX)
+endif()
+
+if (WINDOWS_STORE OR TBB_WINDOWS_DRIVER)
+ set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} /D_WIN32_WINNT=0x0A00)
+ set(TBB_COMMON_LINK_FLAGS -NODEFAULTLIB:kernel32.lib -INCREMENTAL:NO)
+ set(TBB_COMMON_LINK_LIBS OneCore.lib)
+endif()
+
+if (WINDOWS_STORE)
+ if (NOT CMAKE_SYSTEM_VERSION EQUAL 10.0)
+ message(FATAL_ERROR "CMAKE_SYSTEM_VERSION must be equal to 10.0")
+ endif()
+ set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} /ZW /ZW:nostdlib)
+ # CMake define this extra lib, remove it for this build type
+ string(REGEX REPLACE "WindowsApp.lib" "" CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES}")
+
+ if (TBB_NO_APPCONTAINER)
+ set(TBB_LIB_LINK_FLAGS ${TBB_LIB_LINK_FLAGS} -APPCONTAINER:NO)
+ endif()
+endif()
+
+if (TBB_WINDOWS_DRIVER)
+ # Since this is universal driver disable this variable
+ set(CMAKE_SYSTEM_PROCESSOR "")
+ # CMake define list additional libs, remove it for this build type
+ set(CMAKE_CXX_STANDARD_LIBRARIES "")
+ set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} /D _UNICODE /DUNICODE /DWINAPI_FAMILY=WINAPI_FAMILY_APP /D__WRL_NO_DEFAULT_LIB__)
+endif()
+
+if (NOT DEFINED TBB_ENABLE_IPO)
+ if (DEFINED CMAKE_INTERPROCEDURAL_OPTIMIZATION)
+ set(TBB_ENABLE_IPO ${CMAKE_INTERPROCEDURAL_OPTIMIZATION})
+ else()
+ set(TBB_ENABLE_IPO ON)
+ endif()
+endif()
+
+if (TBB_ENABLE_IPO)
+ if (CMAKE_CXX_COMPILER_ID MATCHES "(Clang|IntelLLVM)")
+ if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86|AMD64)")
+ set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} -mrtm -mwaitpkg)
+ endif()
+ set(TBB_OPENMP_NO_LINK_FLAG TRUE)
+ set(TBB_IPO_COMPILE_FLAGS $<$>:-flto>)
+ else()
+ set(TBB_IPO_COMPILE_FLAGS $<$>:/GL>)
+ set(TBB_IPO_LINK_FLAGS $<$>:-LTCG> $<$>:-INCREMENTAL:NO>)
+ endif()
+else()
+ if (CMAKE_CXX_COMPILER_ID MATCHES "(Clang|IntelLLVM)" AND CMAKE_SYSTEM_PROCESSOR MATCHES "(x86|AMD64)")
+ set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} -mrtm -mwaitpkg)
+ endif()
+ set(TBB_IPO_COMPILE_FLAGS "")
+ set(TBB_IPO_LINK_FLAGS "")
+endif()
+
+set(TBB_OPENMP_FLAG /openmp)
diff --git a/deps/TBB/TBB.cmake b/deps/TBB/TBB.cmake
index 9b1452d33e..dac2ed63e6 100644
--- a/deps/TBB/TBB.cmake
+++ b/deps/TBB/TBB.cmake
@@ -1,4 +1,6 @@
-if (FLATPAK AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
+if (MSVC)
+ set(_patch_command ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/MSVC.cmake ./cmake/compilers/MSVC.cmake)
+elseif (FLATPAK AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
set(_patch_command ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/GNU.cmake ./cmake/compilers/GNU.cmake)
else()
set(_patch_command "")
@@ -13,6 +15,8 @@ orcaslicer_add_cmake_project(
-DTBB_BUILD_SHARED=OFF
-DTBB_BUILD_TESTS=OFF
-DTBB_TEST=OFF
+ -DTBB_ENABLE_IPO=OFF
+ -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
-DCMAKE_DEBUG_POSTFIX=_debug
)
diff --git a/deps/wxWidgets/0001-Clang-CL-fix.patch b/deps/wxWidgets/0001-Clang-CL-fix.patch
new file mode 100644
index 0000000000..23bf23b3f4
--- /dev/null
+++ b/deps/wxWidgets/0001-Clang-CL-fix.patch
@@ -0,0 +1,28 @@
+---
+ build/cmake/wxWidgetsConfig.cmake.in | 10 +++++++++-
+ 1 file changed, 10 insertions(+), 1 deletion(-)
+
+diff --git a/build/cmake/wxWidgetsConfig.cmake.in b/build/cmake/wxWidgetsConfig.cmake.in
+index 1a83f36..70ad8a4 100644
+--- a/build/cmake/wxWidgetsConfig.cmake.in
++++ b/build/cmake/wxWidgetsConfig.cmake.in
+@@ -58,7 +58,16 @@ if(WIN32_MSVC_NAMING)
+ endif()
+ endif()
+
+-include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/@PROJECT_NAME@Targets.cmake")
++if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
++ if (CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64" OR CMAKE_VS_PLATFORM_NAME STREQUAL "ARM64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64)$")
++ set(_wx_clang_msvc_lib_dir "vc_arm64_lib")
++ else()
++ set(_wx_clang_msvc_lib_dir "vc_x64_lib")
++ endif()
++ include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/${_wx_clang_msvc_lib_dir}/@PROJECT_NAME@Targets.cmake")
++else()
++ include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/@PROJECT_NAME@Targets.cmake")
++endif()
+
+ macro(wx_inherit_property source dest name)
+ # property name without _
+--
+2.43.0
diff --git a/deps/wxWidgets/wxWidgets.cmake b/deps/wxWidgets/wxWidgets.cmake
index 1e2cc85f78..07bb31d8be 100644
--- a/deps/wxWidgets/wxWidgets.cmake
+++ b/deps/wxWidgets/wxWidgets.cmake
@@ -28,6 +28,7 @@ orcaslicer_add_cmake_project(
GIT_SHALLOW ON
GIT_SUBMODULES 3rdparty/catch 3rdparty/pcre 3rdparty/libwebp
DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG}
+ PATCH_COMMAND git apply --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-Clang-CL-fix.patch
CMAKE_ARGS
-DwxBUILD_PRECOMP=ON
${_wx_toolkit}
diff --git a/deps_src/clipper2/CMakeLists.txt b/deps_src/clipper2/CMakeLists.txt
index c604002da7..86c9a9efab 100644
--- a/deps_src/clipper2/CMakeLists.txt
+++ b/deps_src/clipper2/CMakeLists.txt
@@ -37,7 +37,11 @@ target_include_directories(Clipper2
)
if (WIN32)
- target_compile_options(Clipper2 PRIVATE /W4 /WX)
+ if (MSVC AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
+ target_compile_options(Clipper2 PRIVATE /W4 /WX)
+ elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
+ target_compile_options(Clipper2 PRIVATE /W4)
+ endif()
else()
target_compile_options(Clipper2 PRIVATE -Wall -Wextra -Wpedantic -Werror)
target_link_libraries(Clipper2 PUBLIC -lm)
diff --git a/deps_src/miniz/CMakeLists.txt b/deps_src/miniz/CMakeLists.txt
index e02d8a4885..7e060a180f 100644
--- a/deps_src/miniz/CMakeLists.txt
+++ b/deps_src/miniz/CMakeLists.txt
@@ -11,6 +11,8 @@ add_library(miniz_static STATIC
if(${CMAKE_C_COMPILER_ID} STREQUAL "GNU")
target_compile_definitions(miniz_static PRIVATE _GNU_SOURCE)
+elseif (CMAKE_C_COMPILER_ID STREQUAL "Clang" AND CMAKE_C_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
+ target_compile_options(miniz_static PRIVATE /clang:-Wno-error=incompatible-pointer-types)
endif()
target_link_libraries(miniz INTERFACE miniz_static)
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/CMakeLists.txt b/src/CMakeLists.txt
index 79b49cfd16..0082b29831 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -256,14 +256,6 @@ if (WIN32)
VERBATIM
)
endforeach ()
-
- if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
- orcaslicer_copy_dlls(COPY_DLLS "Debug" "d" output_dlls_Debug)
- elseif("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo")
- orcaslicer_copy_dlls(COPY_DLLS "RelWithDebInfo" "" output_dlls_Release)
- else()
- orcaslicer_copy_dlls(COPY_DLLS "Release" "" output_dlls_Release)
- endif()
else ()
file(TO_NATIVE_PATH "${CMAKE_CURRENT_BINARY_DIR}/resources" WIN_RESOURCES_SYMLINK)
add_custom_command(TARGET OrcaSlicer POST_BUILD
@@ -279,6 +271,27 @@ if (WIN32)
COMMENT "Copying Python runtime into the build tree"
VERBATIM)
+ if (CMAKE_CONFIGURATION_TYPES)
+ # Multi-config generators (Visual Studio, Ninja Multi-Config): copy per config.
+ foreach (cfg ${CMAKE_CONFIGURATION_TYPES})
+ if ("${cfg}" STREQUAL "Debug")
+ orcaslicer_copy_dlls(COPY_DLLS "Debug" "d" output_dlls_Debug)
+ elseif("${cfg}" STREQUAL "RelWithDebInfo")
+ orcaslicer_copy_dlls(COPY_DLLS "RelWithDebInfo" "" output_dlls_RelWithDebInfo)
+ else()
+ orcaslicer_copy_dlls(COPY_DLLS "${cfg}" "" output_dlls_${cfg})
+ endif()
+ endforeach()
+ else()
+ # Single-config generators (Ninja): use CMAKE_BUILD_TYPE.
+ if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
+ orcaslicer_copy_dlls(COPY_DLLS "Debug" "d" output_dlls_Debug)
+ elseif("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo")
+ orcaslicer_copy_dlls(COPY_DLLS "RelWithDebInfo" "" output_dlls_RelWithDebInfo)
+ else()
+ orcaslicer_copy_dlls(COPY_DLLS "Release" "" output_dlls_Release)
+ endif()
+ endif()
else ()
if (APPLE AND NOT CMAKE_MACOSX_BUNDLE)
diff --git a/src/dev-utils/BaseException.cpp b/src/dev-utils/BaseException.cpp
index f33c8f635f..efb7a98245 100644
--- a/src/dev-utils/BaseException.cpp
+++ b/src/dev-utils/BaseException.cpp
@@ -69,7 +69,7 @@ void CBaseException::OutputString(LPCTSTR lpszFormat, ...)
//WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), szBuf, _tcslen(szBuf), NULL, NULL);
//output it to the current directory of binary
- std::string output_str = textconv_helper::T2A_(szBuf);
+ std::string output_str = static_cast(textconv_helper::T2A_(szBuf));
*output_file << output_str;
output_file->flush();
}
diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp
index a5d0e24eac..4dc838172c 100644
--- a/src/libslic3r/AppConfig.cpp
+++ b/src/libslic3r/AppConfig.cpp
@@ -280,6 +280,9 @@ void AppConfig::set_defaults()
set(SETTING_OPENGL_FPS_CAP, std::to_string(fps_cap));
}
+ // The getter already defaults, parses and clamps; write back what it resolves to.
+ set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(get_plugin_pages_visible_count()));
+
if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty())
set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false);
@@ -1630,6 +1633,22 @@ void AppConfig::set_network_plugin_version(const std::string& version)
set(SETTING_NETWORK_PLUGIN_VERSION, version);
}
+int AppConfig::get_plugin_pages_visible_count() const
+{
+ std::string value = get(SETTING_PLUGIN_PAGES_VISIBLE_COUNT);
+ if (value.empty())
+ return PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT;
+
+ int visible_count = PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT;
+ try {
+ visible_count = std::stoi(value);
+ }
+ catch (...) {
+ return PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT;
+ }
+ return std::clamp(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX);
+}
+
std::vector AppConfig::get_skipped_network_versions() const
{
std::vector result;
diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp
index 2c83ebb488..65c57cdb30 100644
--- a/src/libslic3r/AppConfig.hpp
+++ b/src/libslic3r/AppConfig.hpp
@@ -41,6 +41,11 @@ using namespace nlohmann;
#define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao"
#define SETTING_OPENGL_PHONG_SMOOTH_NORMALS "opengl_phong_smooth_normals"
+#define SETTING_PLUGIN_PAGES_VISIBLE_COUNT "plugin_pages_visible_count"
+#define PLUGIN_PAGES_VISIBLE_COUNT_MIN 1
+#define PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT 5
+#define PLUGIN_PAGES_VISIBLE_COUNT_MAX 10
+
#if defined(_WIN32) || defined(_WIN64)
#define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09"
#else
@@ -374,6 +379,10 @@ public:
std::string get_network_plugin_version() const;
void set_network_plugin_version(const std::string& version);
+ // Number of plugin pages shown as fixed tabs before the rest are collapsed into a
+ // dropdown on the last tab.
+ int get_plugin_pages_visible_count() const;
+
std::vector get_skipped_network_versions() const;
void add_skipped_network_version(const std::string& version);
bool is_network_version_skipped(const std::string& version) const;
diff --git a/src/libslic3r/BoundingBox.cpp b/src/libslic3r/BoundingBox.cpp
index a2a510b64c..cf5441dace 100644
--- a/src/libslic3r/BoundingBox.cpp
+++ b/src/libslic3r/BoundingBox.cpp
@@ -8,6 +8,8 @@
namespace Slic3r {
template BoundingBoxBase::BoundingBoxBase(const Points &points);
+template void BoundingBoxBase::construct<0, BoundingBox, Points::const_iterator>(BoundingBox&, Points::const_iterator, Points::const_iterator);
+template void BoundingBoxBase::construct<1, BoundingBox, Points::const_iterator>(BoundingBox&, Points::const_iterator, Points::const_iterator);
template BoundingBoxBase::BoundingBoxBase(const std::vector &points);
template BoundingBox3Base::BoundingBox3Base(const std::vector &points);
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/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp
index 29b3d1e677..60c90155a9 100644
--- a/src/libslic3r/PresetBundle.cpp
+++ b/src/libslic3r/PresetBundle.cpp
@@ -50,7 +50,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/CMakeLists.txt b/src/slic3r/CMakeLists.txt
index b98396f943..9174b044ec 100644
--- a/src/slic3r/CMakeLists.txt
+++ b/src/slic3r/CMakeLists.txt
@@ -620,6 +620,8 @@ set(SLIC3R_GUI_SOURCES
plugin/host/PluginHostSlicing.cpp
plugin/host/PluginHostUi.cpp
plugin/host/PluginHostUi.hpp
+ plugin/host/PluginPages.cpp
+ plugin/host/PluginPages.hpp
plugin/CloudPluginService.cpp
plugin/CloudPluginService.hpp
plugin/PluginFsUtils.cpp
@@ -640,6 +642,9 @@ set(SLIC3R_GUI_SOURCES
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp
+ plugin/pluginTypes/pages/PagesPluginCapability.hpp
+ plugin/pluginTypes/pages/PagesPluginCapability.cpp
+ plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp
plugin/pluginTypes/script/ScriptPluginCapability.hpp
plugin/pluginTypes/script/ScriptPluginCapability.cpp
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp
diff --git a/src/slic3r/GUI/Auxiliary.cpp b/src/slic3r/GUI/Auxiliary.cpp
index 95244436a3..13ca173eb6 100644
--- a/src/slic3r/GUI/Auxiliary.cpp
+++ b/src/slic3r/GUI/Auxiliary.cpp
@@ -869,11 +869,11 @@ void AuxiliaryPanel::init_tabpanel()
m_assembly_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::ASSEMBLY_GUIDE);
m_others_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::OTHERS);
- m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), "", true);
- m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), "", false);
- m_tabpanel->AddPage(m_bill_of_materials_panel, _L("Bill of Materials"), "", false);
- m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), "", false);
- m_tabpanel->AddPage(m_others_panel, _L("Others"), "", false);
+ m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), true);
+ m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), false);
+ m_tabpanel->AddPage(m_bill_of_materials_panel, _L("Bill of Materials"), false);
+ m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), false);
+ m_tabpanel->AddPage(m_others_panel, _L("Others"), false);
}
wxWindow *AuxiliaryPanel::create_side_tools()
diff --git a/src/slic3r/GUI/CalibrationPanel.cpp b/src/slic3r/GUI/CalibrationPanel.cpp
index b006509adf..bdc79c1c8e 100644
--- a/src/slic3r/GUI/CalibrationPanel.cpp
+++ b/src/slic3r/GUI/CalibrationPanel.cpp
@@ -488,7 +488,6 @@ void CalibrationPanel::init_tabpanel() {
selected = true;
m_tabpanel->AddPage(m_cali_panels[i],
get_calibration_type_name(m_cali_panels[i]->get_calibration_mode()),
- "",
selected);
}
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/Downloader.cpp b/src/slic3r/GUI/Downloader.cpp
index c61b2716fc..0d37a0eca6 100644
--- a/src/slic3r/GUI/Downloader.cpp
+++ b/src/slic3r/GUI/Downloader.cpp
@@ -134,7 +134,7 @@ void Downloader::start_download(const std::string& full_url)
Plater* plater = wxGetApp().plater();
mainframe->Freeze();
- mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor);
+ mainframe->select_tab(TAB_ID_PREPARE);
plater->select_view_3D("3D");
plater->select_view("plate");
plater->get_current_canvas3D()->zoom_to_bed();
diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp
index 8d05de13a4..dbbf5ec5e2 100644
--- a/src/slic3r/GUI/Field.cpp
+++ b/src/slic3r/GUI/Field.cpp
@@ -331,8 +331,10 @@ void Field::PostInitialize()
}
default: break;
}
- if (tab_id >= 0)
- wxGetApp().mainframe->select_tab(tab_id);
+ if (tab_id >= 0) {
+ static constexpr const char* kShortcutTabIds[] = {TAB_ID_HOME, TAB_ID_PREPARE, TAB_ID_PREVIEW, TAB_ID_MONITOR};
+ wxGetApp().mainframe->select_tab(kShortcutTabIds[tab_id]);
+ }
if (tab_id > 0)
// tab panel should be focused for correct navigation between tabs
wxGetApp().tab_panel()->SetFocus();
diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp
index a51a10296c..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();
@@ -9196,7 +9196,7 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
view3d_canvas->get_gizmos_manager().reset_all_states(); // close all gizmos
view3d_canvas->reload_scene(true);
}
- app.mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor);
+ app.mainframe->select_tab(TAB_ID_PREPARE);
}
}
});
diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp
index 0d4e3f35cf..ffde09dbca 100644
--- a/src/slic3r/GUI/GUI_App.cpp
+++ b/src/slic3r/GUI/GUI_App.cpp
@@ -813,12 +813,12 @@ void GUI_App::post_init()
m_open_method = "url";
} else {
if (this->init_params->input_gcode) {
- mainframe->select_tab(size_t(MainFrame::tp3DEditor));
+ mainframe->select_tab(TAB_ID_PREPARE);
plater_->select_view_3D("3D");
this->plater()->load_gcode(from_u8(this->init_params->input_files.front()));
m_open_method = "gcode";
} else {
- mainframe->select_tab(size_t(MainFrame::tp3DEditor));
+ mainframe->select_tab(TAB_ID_PREPARE);
plater_->select_view_3D("3D");
wxArrayString input_files;
for (auto& file : this->init_params->input_files) {
@@ -852,7 +852,7 @@ void GUI_App::post_init()
mainframe->Freeze();
#endif
plater_->canvas3D()->enable_render(false);
- mainframe->select_tab(size_t(MainFrame::tp3DEditor));
+ mainframe->select_tab(TAB_ID_PREPARE);
plater_->select_view_3D("3D");
//BBS init the opengl resource here
if (!plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen() ||
@@ -890,9 +890,9 @@ void GUI_App::post_init()
}
}
if (is_editor())
- mainframe->select_tab(size_t(0));
+ mainframe->select_tab(TAB_ID_HOME);
if (app_config->get("default_page") == "1")
- mainframe->select_tab(size_t(1));
+ mainframe->select_tab(TAB_ID_PREPARE);
#ifndef __linux__
mainframe->Thaw();
#endif
@@ -1829,10 +1829,10 @@ bool GUI_App::hot_reload_network_plugin()
wxWindowDisabler disabler;
if (mainframe) {
- int current_tab = mainframe->m_tabpanel->GetSelection();
- if (current_tab == MainFrame::TabPosition::tpMonitor) {
+ wxString current_tab = mainframe->m_tabpanel->GetSelectedPageName();
+ if (current_tab == TAB_ID_MONITOR) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": navigating away from Monitor tab before unload";
- mainframe->m_tabpanel->SetSelection(MainFrame::TabPosition::tp3DEditor);
+ mainframe->m_tabpanel->SelectPageByName(TAB_ID_PREPARE);
}
}
@@ -2851,6 +2851,16 @@ void GUI_App::init_plugin_gui_wiring()
plugin_mgr.subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
plugin_mgr.subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin);
plugin_mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
+ plugin_mgr.subscribe_on_load_callback([](const std::string& plugin_key) {
+ if (wxTheApp == nullptr || wxGetApp().is_closing() || wxGetApp().mainframe == nullptr)
+ return;
+ wxGetApp().mainframe->plugin_pages().on_plugin_register(plugin_key);
+ });
+ plugin_mgr.subscribe_on_unload_callback([](const std::string& plugin_key) {
+ if (wxTheApp == nullptr || wxGetApp().is_closing() || wxGetApp().mainframe == nullptr)
+ return;
+ wxGetApp().mainframe->plugin_pages().on_plugin_deregister(plugin_key);
+ });
plugin_mgr.subscribe_on_load_callback(refresh_printer_agent_dropdown_after_load);
plugin_mgr.subscribe_on_unload_callback(switch_printer_agent_after_unload);
plugin_mgr.subscribe_on_capability_load_callback(
@@ -2866,11 +2876,15 @@ void GUI_App::init_plugin_gui_wiring()
if (Plater* plater = wxGetApp().plater())
plater->revalidate_current_plate_if_plugins_missing();
});
+ if (capability.type == PluginCapabilityType::Pages && wxTheApp && !wxGetApp().is_closing() && wxGetApp().mainframe)
+ wxGetApp().mainframe->plugin_pages().on_cap_register(capability);
});
plugin_mgr.subscribe_on_capability_unload_callback(
[refresh_plugins_dialog, switch_printer_agent_after_unload](const PluginCapabilityId& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection)
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
+ if (capability.type == PluginCapabilityType::Pages && wxTheApp && !wxGetApp().is_closing() && wxGetApp().mainframe)
+ wxGetApp().mainframe->plugin_pages().on_cap_deregister(capability);
refresh_plugins_dialog();
switch_printer_agent_after_unload(capability.plugin_key);
});
@@ -3382,7 +3396,7 @@ bool GUI_App::on_init_inner()
mainframe = new MainFrame();
// hide settings tabs after first Layout
if (is_editor()) {
- mainframe->select_tab(size_t(0));
+ mainframe->select_tab(TAB_ID_HOME);
}
sidebar().obj_list()->init();
@@ -4590,7 +4604,7 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
mainframe = new MainFrame();
if (is_editor())
// hide settings tabs after first Layout
- mainframe->select_tab(size_t(MainFrame::tp3DEditor));
+ mainframe->select_tab(TAB_ID_PREPARE);
// Propagate model objects to object list.
sidebar().obj_list()->init();
//sidebar().aux_list()->init_auxiliary();
@@ -9849,7 +9863,7 @@ bool GUI_App::check_url_association(std::wstring url_prefix, std::wstring& reg_b
{
reg_bin = L"";
#ifdef WIN32
- wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command");
+ wxRegKey key_full(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix + L"\\shell\\open\\command");
if (!key_full.Exists()) {
return false;
}
@@ -9875,8 +9889,8 @@ void GUI_App::associate_url(std::wstring url_prefix)
wxString key_string = "\"" + wbinary + "\" \"%1\"";
- wxRegKey key_first(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix);
- wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command");
+ wxRegKey key_first(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix);
+ wxRegKey key_full(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix + L"\\shell\\open\\command");
if (!key_first.Exists()) {
key_first.Create(false);
}
@@ -9896,7 +9910,7 @@ void GUI_App::disassociate_url(std::wstring url_prefix)
#ifdef WIN32
if (is_running_in_msix())
return;
- wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command");
+ wxRegKey key_full(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix + L"\\shell\\open\\command");
if (!key_full.Exists()) {
return;
}
diff --git a/src/slic3r/GUI/HMS.cpp b/src/slic3r/GUI/HMS.cpp
index 4d67a398f7..5471c16064 100644
--- a/src/slic3r/GUI/HMS.cpp
+++ b/src/slic3r/GUI/HMS.cpp
@@ -1,6 +1,7 @@
#include "HMS.hpp"
#include "GUI.hpp"
+#include "GUI_App.hpp"
#include "DeviceManager.hpp"
#include "DeviceCore/DevManager.h"
#include "DeviceCore/DevUtil.h"
diff --git a/src/slic3r/GUI/HMS.hpp b/src/slic3r/GUI/HMS.hpp
index d2a87ebf42..c494539b36 100644
--- a/src/slic3r/GUI/HMS.hpp
+++ b/src/slic3r/GUI/HMS.hpp
@@ -1,7 +1,6 @@
#ifndef slic3r_HMS_hpp_
#define slic3r_HMS_hpp_
-#include "GUI_App.hpp"
#include "GUI.hpp"
#include "I18N.hpp"
#include "Widgets/Label.hpp"
@@ -11,7 +10,11 @@
#include "slic3r/Utils/Http.hpp"
#include "libslic3r/Thread.hpp"
#include "nlohmann/json.hpp"
+#include
#include
+#include
+#include
+#include
namespace Slic3r {
@@ -26,12 +29,12 @@ namespace GUI {
class HMSQuery {
protected:
- std::unordered_map m_hms_info_jsons; // key-> device id type, the first three digits of SN number
- std::unordered_map m_hms_action_jsons;// key-> device id type
+ std::unordered_map m_hms_info_jsons; // key-> device id type, the first three digits of SN number
+ std::unordered_map m_hms_action_jsons;// key-> device id type
std::unordered_map m_hms_local_images; // key-> image name
mutable std::mutex m_hms_mutex;
- std::unordered_map m_cloud_hms_last_update_time;
+ std::unordered_map m_cloud_hms_last_update_time;
public:
HMSQuery() { }
@@ -61,18 +64,18 @@ private:
// load hms
void init_hms_info(const std::string& dev_type_id);
void copy_from_data_dir_to_local();
- int download_hms_related(const std::string& hms_type, const std::string& dev_id_type, json* receive_json);
- int load_from_local(const std::string& hms_type, const std::string& dev_id_type, json* receive_json, std::string& version_info);
- int save_to_local(std::string lang, std::string hms_type, std::string dev_id_type, json save_json);
+ int download_hms_related(const std::string& hms_type, const std::string& dev_id_type, nlohmann::json* receive_json);
+ int load_from_local(const std::string& hms_type, const std::string& dev_id_type, nlohmann::json* receive_json, std::string& version_info);
+ int save_to_local(std::string lang, std::string hms_type, std::string dev_id_type, nlohmann::json save_json);
std::string get_hms_file(std::string hms_type, std::string lang = std::string("en"), std::string dev_id_type = "");
// internal query
- string get_dev_id_type(const MachineObject* obj) const;
- wxString _query_hms_msg(const string& dev_id_type, const string& long_error_code, const string& lang_code = std::string("en"));
+ std::string get_dev_id_type(const MachineObject* obj) const;
+ wxString _query_hms_msg(const std::string& dev_id_type, const std::string& long_error_code, const std::string& lang_code = std::string("en"));
- bool _is_internal_error(const string &dev_id_type, const string &long_error_code, const string &lang_code = std::string("en"));
- wxString _query_error_msg(const string& dev_id_type, const std::string& long_error_code, const std::string& lang_code = std::string("en"));
- wxString _query_error_image_action(const string& dev_id_type, const std::string& long_error_code, std::vector& button_action);
+ bool _is_internal_error(const std::string &dev_id_type, const std::string &long_error_code, const std::string &lang_code = std::string("en"));
+ wxString _query_error_msg(const std::string& dev_id_type, const std::string& long_error_code, const std::string& lang_code = std::string("en"));
+ wxString _query_error_image_action(const std::string& dev_id_type, const std::string& long_error_code, std::vector& button_action);
};
int get_hms_info_version(std::string &version);
@@ -85,4 +88,4 @@ std::string get_error_message(int error_code);
}
-#endif
\ No newline at end of file
+#endif
diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp
index 5a0e70b74c..3b1cf1dffd 100644
--- a/src/slic3r/GUI/MainFrame.cpp
+++ b/src/slic3r/GUI/MainFrame.cpp
@@ -493,9 +493,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
});
//BBS
- Bind(EVT_SELECT_TAB, [this](wxCommandEvent&evt) {
- TabPosition pos = (TabPosition)evt.GetInt();
- m_tabpanel->SetSelection(pos);
+ Bind(EVT_SELECT_TAB, [this](wxCommandEvent& evt) {
+ m_tabpanel->SelectPageByName(evt.GetString());
});
Bind(EVT_SYNC_CLOUD_PRESET, &MainFrame::on_select_default_preset, this);
@@ -702,7 +701,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
}
return;}
#endif
- if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SetSelection(tpPreview); } return; }
+ if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return; }
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') {
m_plater->apply_background_progress();
m_print_enable = get_enable_print_status();
@@ -723,7 +722,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'S') { if (can_save_as()) m_plater->save_project(true); return;}
else if (evt.CmdDown() && evt.GetKeyCode() == 'S') { if (can_save()) m_plater->save_project(); return;}
if (evt.CmdDown() && evt.GetKeyCode() == 'F') {
- if (m_plater && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview)) {
+ if (m_plater && is_prepare_or_preview_tab()) {
m_plater->sidebar().can_search();
}
}
@@ -1007,8 +1006,8 @@ void MainFrame::update_layout()
m_layout = layout;
// From the very beginning the Print settings should be selected
- //m_last_selected_tab = m_layout == ESettingsLayout::Dlg ? 0 : 1;
- m_last_selected_tab = 1;
+ //m_last_selected_tab = m_layout == ESettingsLayout::Dlg ? TAB_ID_HOME : TAB_ID_PREPARE;
+ m_last_selected_tab = TAB_ID_PREPARE;
// Set new settings
switch (m_layout)
@@ -1016,14 +1015,18 @@ void MainFrame::update_layout()
case ESettingsLayout::Old:
{
m_plater->Reparent(m_tabpanel);
- m_tabpanel->InsertPage(tp3DEditor, m_plater, _L("Prepare"), std::string("tab_3d_active"), std::string("tab_3d_active"), false);
- m_tabpanel->InsertPage(tpPreview, m_plater, _L("Preview"), std::string("tab_preview_active"), std::string("tab_preview_active"), false);
+ // Right after Home — or first, when there is no Home tab (PositionAfter() would
+ // append instead, and by now the other built-in tabs are already in place).
+ const int home_idx = m_tabpanel->FindPageByName(TAB_ID_HOME);
+ const size_t prepare_pos = (home_idx == wxNOT_FOUND) ? 0 : static_cast(home_idx) + 1;
+ m_tabpanel->InsertPage(prepare_pos, TAB_ID_PREPARE, m_plater, _L("Prepare"), "tab_3d_active");
+ m_tabpanel->InsertPage(prepare_pos + 1, TAB_ID_PREVIEW, m_plater, _L("Preview"), "tab_preview_active");
m_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 0);
m_tabpanel->Bind(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, [this](wxCommandEvent& evt)
{
// jump to 3deditor under preview_only mode
- if (evt.GetId() == tp3DEditor){
+ if (evt.GetId() == m_tabpanel->FindPageByName(TAB_ID_PREPARE)) {
Sidebar& sidebar = GUI::wxGetApp().sidebar();
if (sidebar.need_auto_sync_after_connect_printer()) {
sidebar.set_need_auto_sync_after_connect_printer(false);
@@ -1107,6 +1110,9 @@ void MainFrame::update_edge_panels()
void MainFrame::shutdown()
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "MainFrame::shutdown enter";
+ if (m_project != nullptr)
+ m_project->shutdown();
+ m_plugin_pages.shutdown();
#ifdef __WXGTK__
// Edge panels are child windows — wxWidgets destroys them automatically.
m_edge_bottom = nullptr;
@@ -1252,15 +1258,14 @@ void MainFrame::init_tabpanel() {
#endif
//BBS
wxWindow* panel = m_tabpanel->GetCurrentPage();
- int sel = m_tabpanel->GetSelection();
//wxString page_text = m_tabpanel->GetPageText(sel);
- m_last_selected_tab = m_tabpanel->GetSelection();
+ m_last_selected_tab = m_tabpanel->GetSelectedPageName();
if (panel == m_plater) {
- if (sel == tp3DEditor) {
+ if (m_last_selected_tab == TAB_ID_PREPARE) {
wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_3D));
m_param_panel->OnActivate();
}
- else if (sel == tpPreview) {
+ else if (m_last_selected_tab == TAB_ID_PREVIEW) {
m_plater->reset_check_status();
if (!m_plater->check_ams_status(m_slice_select == eSliceAll))
return;
@@ -1275,7 +1280,7 @@ void MainFrame::init_tabpanel() {
//monitor
}
#ifndef __APPLE__
- if (sel == tp3DEditor) {
+ if (m_last_selected_tab == TAB_ID_PREPARE) {
m_topbar->EnableUndoRedoItems();
}
else {
@@ -1285,34 +1290,16 @@ void MainFrame::init_tabpanel() {
if (panel)
panel->SetFocus();
-
- /*switch (sel) {
- case TabPosition::tpHome:
- show_option(false);
- break;
- case TabPosition::tp3DEditor:
- show_option(true);
- break;
- case TabPosition::tpPreview:
- show_option(true);
- break;
- case TabPosition::tpMonitor:
- show_option(false);
- break;
- default:
- show_option(false);
- break;
- }*/
});
if (wxGetApp().is_editor()) {
m_webview = new WebViewPanel(m_tabpanel);
Bind(EVT_LOAD_URL, [this](wxCommandEvent &evt) {
wxString url = evt.GetString();
- select_tab(MainFrame::tpHome);
+ select_tab(TAB_ID_HOME);
m_webview->load_url(url);
});
- m_tabpanel->AddPage(m_webview, "", "tab_home_active", "tab_home_active", false);
+ m_tabpanel->AddPage(TAB_ID_HOME, m_webview, "", "tab_home_active");
m_param_panel = new ParamsPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL);
}
@@ -1327,7 +1314,7 @@ void MainFrame::init_tabpanel() {
//BBS add pages
m_monitor = new MonitorPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_monitor->SetBackgroundColour(*wxWHITE);
- m_tabpanel->AddPage(m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active"), false);
+ m_tabpanel->AddPage(TAB_ID_MONITOR, m_monitor, _L("Device"), "tab_monitor_active");
m_printer_view = new PrinterWebView(m_tabpanel);
Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent &evt) {
@@ -1342,16 +1329,20 @@ void MainFrame::init_tabpanel() {
m_multi_machine = new MultiMachinePage(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_multi_machine->SetBackgroundColour(*wxWHITE);
// TODO: change the bitmap
- m_tabpanel->AddPage(m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), std::string("tab_multi_active"), false);
+ m_tabpanel->AddPage(TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), "tab_multi_active");
}
m_project = new ProjectPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_project->SetBackgroundColour(*wxWHITE);
- m_tabpanel->AddPage(m_project, _L("Project"), std::string("tab_auxiliary_active"), std::string("tab_auxiliary_active"), false);
+ m_tabpanel->AddPage(TAB_ID_PROJECT, m_project, _L("Project"), "tab_auxiliary_active");
m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_calibration->SetBackgroundColour(*wxWHITE);
- m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false);
+ m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), "tab_calibration_active");
+
+ // Plugin pages are appended after the built-in tabs; their ids are namespaced
+ // (plugin..) so they can't collide with the built-in TAB_ID_* constants.
+ m_plugin_pages.initialize(m_tabpanel);
if (m_plater) {
// load initial config
@@ -1373,10 +1364,15 @@ void MainFrame::show_device(bool should_use_native) {
const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents");
- // The web page is appended when printer agents are enabled. Remove that
- // extra page before switching back to the normal native/Web layout.
- if (!use_printer_agents) {
- if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND && idx != tpMonitor) {
+ // The web Device page is the extra tab printer-agents mode shows alongside the native one.
+ // Printers that drive the native Bambu device tab have nothing to put in it, so they don't
+ // get it — otherwise a Bambu user sees two Device tabs, one of them permanently empty.
+ const bool want_web_device_tab = use_printer_agents && wxGetApp().preset_bundle != nullptr &&
+ !wxGetApp().preset_bundle->use_bbl_device_tab();
+
+ // Remove the extra page before switching to any layout that shouldn't have it.
+ if (!want_web_device_tab) {
+ if ((idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR_WEB)) != wxNOT_FOUND) {
m_printer_view->Show(false);
m_tabpanel->RemovePage(idx);
}
@@ -1394,8 +1390,8 @@ void MainFrame::show_device(bool should_use_native) {
m_tabpanel->RemovePage(idx);
}
m_monitor->Show(false);
- m_tabpanel->InsertPage(tpMonitor, m_monitor, _L("Device"), std::string("tab_monitor_active"),
- std::string("tab_monitor_active"));
+ m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor,
+ _L("Device"), "tab_monitor_active");
}
if (m_printer_view == nullptr) {
@@ -1416,28 +1412,31 @@ void MainFrame::show_device(bool should_use_native) {
// TODO: change the bitmap
if (m_tabpanel->FindPage(m_multi_machine) == wxNOT_FOUND) {
m_multi_machine->Show(false);
- m_tabpanel->InsertPage(tpMultiDevice, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"),
- std::string("tab_multi_active"), false);
+ // Past the web Device tab when it is already there, so enabling multi-machine
+ // later can't wedge this page between the two Device tabs.
+ m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR_WEB, TAB_ID_MONITOR}),
+ TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), "tab_multi_active");
}
}
if (!m_calibration) {
m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_calibration->SetBackgroundColour(*wxWHITE);
}
- // Calibration is always the last page, so don't use InsertPage here. Otherwise, if multi_machine page is not enabled,
- // the calibration tab won't be properly added as well, due to the TabPosition::tpCalibration no longer matches the real tab position.
if (m_tabpanel->FindPage(m_calibration) == wxNOT_FOUND) {
m_calibration->Show(false);
- m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"),
- std::string("tab_calibration_active"), false);
+ m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration,
+ _L("Calibration"), "tab_calibration_active");
}
- if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) {
- m_printer_view->Show(false);
- m_tabpanel->AddPage(m_printer_view, _L("Device (Web)"), std::string("tab_monitor_active"),
- std::string("tab_monitor_active"), false);
- } else {
- m_tabpanel->SetPageText(idx, _L("Device (Web)"));
+ if (want_web_device_tab) {
+ if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) {
+ m_printer_view->Show(false);
+ // Immediately right of the native Device tab, not at the end of the tab bar.
+ m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MONITOR_WEB,
+ m_printer_view, _L("Device (Web)"), "tab_monitor_active");
+ } else {
+ m_tabpanel->SetPageText(idx, _L("Device (Web)"));
+ }
}
#ifdef _MSW_DARK_MODE
@@ -1445,6 +1444,7 @@ void MainFrame::show_device(bool should_use_native) {
#endif // _MSW_DARK_MODE
fit_tab_labels(); // ORCA on printer change
+ m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above
return;
}
@@ -1466,7 +1466,8 @@ void MainFrame::show_device(bool should_use_native) {
m_monitor->SetBackgroundColour(*wxWHITE);
}
m_monitor->Show(false);
- m_tabpanel->InsertPage(tpMonitor, m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active"));
+ m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor,
+ _L("Device"), "tab_monitor_active");
if (wxGetApp().is_enable_multi_machine()) {
if (!m_multi_machine) {
@@ -1475,18 +1476,18 @@ void MainFrame::show_device(bool should_use_native) {
}
// TODO: change the bitmap
m_multi_machine->Show(false);
- m_tabpanel->InsertPage(tpMultiDevice, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"),
- std::string("tab_multi_active"), false);
+ m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MULTI_DEVICE, m_multi_machine,
+ _L("Multi-device"), "tab_multi_active");
}
if (!m_calibration) {
m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_calibration->SetBackgroundColour(*wxWHITE);
}
m_calibration->Show(false);
- // Calibration is always the last page, so don't use InsertPage here. Otherwise, if multi_machine page is not enabled,
- // the calibration tab won't be properly added as well, due to the TabPosition::tpCalibration no longer matches the real tab position.
- m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"),
- std::string("tab_calibration_active"), false);
+ // Last of the built-in tabs, but plugin tabs already sit past it — anchor rather than
+ // append, so its position doesn't depend on the relayout() below running afterwards.
+ m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration,
+ _L("Calibration"), "tab_calibration_active");
#ifdef _MSW_DARK_MODE
wxGetApp().UpdateDarkUIWin(this);
@@ -1519,10 +1520,17 @@ void MainFrame::show_device(bool should_use_native) {
});
}
m_printer_view->Show(false);
- m_tabpanel->InsertPage(tpMonitor, m_printer_view, _L("Device"), std::string("tab_monitor_active"),
- std::string("tab_monitor_active"));
+ m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_printer_view,
+ _L("Device"), "tab_monitor_active");
}
fit_tab_labels(); // ORCA on printer change
+ m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above
+}
+
+bool MainFrame::is_prepare_or_preview_tab() const
+{
+ const wxString tab = m_tabpanel->GetSelectedPageName();
+ return tab == TAB_ID_PREPARE || tab == TAB_ID_PREVIEW;
}
void MainFrame::fit_tab_labels()
@@ -1554,7 +1562,7 @@ void MainFrame::fit_tab_labels()
bool MainFrame::preview_only_hint()
{
if (m_plater && (m_plater->only_gcode_mode() || (m_plater->using_exported_file()))) {
- BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2% in preview mode")%m_tabpanel->GetSelection() %tp3DEditor;
+ BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2% in preview mode")%m_tabpanel->GetSelectedPageName() %wxString(TAB_ID_PREPARE);
ConfirmBeforeSendDialog confirm_dlg(this, wxID_ANY, _L("Warning"));
confirm_dlg.Bind(EVT_SECONDARY_CHECK_CONFIRM, [this](wxCommandEvent& e) {
@@ -1872,22 +1880,22 @@ bool MainFrame::can_clone() const {
bool MainFrame::can_select() const
{
- return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->model().objects.empty();
+ return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->model().objects.empty();
}
bool MainFrame::can_deselect() const
{
- return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->is_selection_empty();
+ return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->is_selection_empty();
}
bool MainFrame::can_delete() const
{
- return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->is_selection_empty();
+ return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->is_selection_empty();
}
bool MainFrame::can_delete_all() const
{
- return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->model().objects.empty();
+ return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->model().objects.empty();
}
bool MainFrame::can_reslice() const
@@ -1996,7 +2004,7 @@ wxBoxSizer* MainFrame::create_side_tools()
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL));
else
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE));
- this->m_tabpanel->SetSelection(tpPreview);
+ this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW);
}
});
@@ -3143,7 +3151,7 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().app_config->set_bool("auto_perspective", !wxGetApp().app_config->get_bool("auto_perspective"));
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
},
- this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; },
+ this, [this]() { return is_prepare_or_preview_tab(); },
[this]() { return wxGetApp().app_config->get_bool("auto_perspective"); }, this);
viewMenu->AppendSeparator();
@@ -3152,7 +3160,7 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().toggle_show_gcode_window();
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
},
- this, [this]() { return m_tabpanel->GetSelection() == tpPreview; },
+ this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; },
[this]() { return wxGetApp().show_gcode_window(); }, this);
append_menu_check_item(
@@ -3161,7 +3169,7 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().toggle_show_3d_navigator();
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
},
- this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; },
+ this, [this]() { return is_prepare_or_preview_tab(); },
[this]() { return wxGetApp().show_3d_navigator(); }, this);
append_menu_check_item(viewMenu, wxID_ANY, _L("Show Gridlines"), _L("Show Gridlines on plate"),
@@ -3169,15 +3177,14 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().toggle_show_plate_gridlines();
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
}, this,
- [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; },
+ [this]() { return is_prepare_or_preview_tab(); },
[this]() { return wxGetApp().show_plate_gridlines(); }, this);
append_menu_item(
viewMenu, wxID_ANY, _L("Reset Window Layout"), _L("Reset to default window layout"),
[this](wxCommandEvent&) { m_plater->reset_window_layout(); }, "", this,
[this]() {
- return (m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview) &&
- m_plater->is_sidebar_enabled();
+ return is_prepare_or_preview_tab() && m_plater->is_sidebar_enabled();
},
this);
@@ -3199,7 +3206,7 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().toggle_show_outline();
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
},
- this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor; },
+ this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE; },
[this]() { return wxGetApp().show_outline(); }, this);
/*viewMenu->AppendSeparator();
@@ -4000,13 +4007,16 @@ void MainFrame::select_tab(wxPanel* panel)
wxGetApp().params_dialog()->Popup();
return;
}
+ // Not panel->GetName(): Prepare and Preview share the single m_plater window, so the
+ // window has no one correct name. The slot -> id lookup is the only correct resolution.
int page_idx = m_tabpanel->FindPage(panel);
- if (page_idx == tp3DEditor && m_tabpanel->GetSelection() == tpPreview)
+ wxString page_name = (page_idx == wxNOT_FOUND) ? wxString() : m_tabpanel->GetPageName(static_cast(page_idx));
+ if (page_name == TAB_ID_PREPARE && m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW)
return;
//BBS GUI refactor: remove unused layout new/dlg
/*if (page_idx != wxNOT_FOUND && m_layout == ESettingsLayout::Dlg)
page_idx++;*/
- select_tab(size_t(page_idx));
+ select_tab(page_name);
}
//BBS
@@ -4014,7 +4024,7 @@ void MainFrame::jump_to_monitor(std::string dev_id)
{
if(!m_monitor)
return;
- m_tabpanel->SetSelection(tpMonitor);
+ m_tabpanel->SelectPageByName(TAB_ID_MONITOR);
if (!dev_id.empty()) {
((MonitorPanel*)m_monitor)->select_machine(dev_id);
}
@@ -4024,26 +4034,26 @@ void MainFrame::jump_to_multipage()
{
if(!m_multi_machine)
return;
- m_tabpanel->SetSelection(tpMultiDevice);
+ m_tabpanel->SelectPageByName(TAB_ID_MULTI_DEVICE);
((MultiMachinePage*)m_multi_machine)->jump_to_send_page();
}
//BBS GUI refactor: remove unused layout new/dlg
-void MainFrame::select_tab(size_t tab/* = size_t(-1)*/)
+void MainFrame::select_tab(const wxString& id/* = wxString()*/)
{
//bool tabpanel_was_hidden = false;
// Controls on page are created on active page of active tab now.
// We should select/activate tab before its showing to avoid an UI-flickering
- auto select = [this, tab](bool was_hidden) {
- // when tab == -1, it means we should show the last selected tab
+ auto select = [this, id](bool was_hidden) {
+ // when id is empty, it means we should show the last selected tab
//BBS GUI refactor: remove unused layout new/dlg
//size_t new_selection = tab == (size_t)(-1) ? m_last_selected_tab : (m_layout == ESettingsLayout::Dlg && tab != 0) ? tab - 1 : tab;
- size_t new_selection = tab == (size_t)(-1) ? m_last_selected_tab : tab;
+ wxString new_selection = id.empty() ? m_last_selected_tab : id;
- if (m_tabpanel->GetSelection() != (int)new_selection)
- m_tabpanel->SetSelection(new_selection);
+ if (m_tabpanel->GetSelectedPageName() != new_selection)
+ m_tabpanel->SelectPageByName(new_selection);
#ifdef _MSW_DARK_MODE
/*if (wxGetApp().tabs_as_menu()) {
if (Tab* cur_tab = dynamic_cast(m_tabpanel->GetPage(new_selection)))
@@ -4052,10 +4062,12 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/)
m_plater->get_current_canvas3D()->render();
}*/
#endif
- if (tab == MainFrame::tp3DEditor && m_layout == ESettingsLayout::Old)
+ // Intentionally `id`, not `new_selection`: the fallback-to-last-tab path must not
+ // trigger this render even when the last selected tab was Prepare.
+ if (id == TAB_ID_PREPARE && m_layout == ESettingsLayout::Old)
m_plater->canvas3D()->render();
else if (was_hidden) {
- Tab* cur_tab = dynamic_cast(m_tabpanel->GetPage(new_selection));
+ Tab* cur_tab = dynamic_cast(m_tabpanel->GetPageByName(new_selection));
if (cur_tab)
cur_tab->OnActivate();
}
@@ -4064,10 +4076,10 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/)
select(false);
}
-void MainFrame::request_select_tab(TabPosition pos)
+void MainFrame::request_select_tab(const wxString& id)
{
wxCommandEvent* evt = new wxCommandEvent(EVT_SELECT_TAB);
- evt->SetInt(pos);
+ evt->SetString(id);
wxQueueEvent(this, evt);
}
@@ -4359,7 +4371,7 @@ void MainFrame::load_printer_url()
}
}
-bool MainFrame::is_printer_view() const { return m_tabpanel->GetSelection() == TabPosition::tpMonitor; }
+bool MainFrame::is_printer_view() const { return m_tabpanel->GetSelectedPageName() == TAB_ID_MONITOR; }
void MainFrame::refresh_plugin_tips()
diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp
index a614783c31..2052860d80 100644
--- a/src/slic3r/GUI/MainFrame.hpp
+++ b/src/slic3r/GUI/MainFrame.hpp
@@ -35,6 +35,21 @@
#include "PrinterWebView.hpp"
#include "calib_dlg.hpp"
#include "MultiMachinePage.hpp"
+#include "slic3r/plugin/host/PluginPages.hpp"
+
+// Stable identifiers for MainFrame::m_tabpanel's built-in pages. These are
+// names rather than positional indices so optional pages cannot shift them.
+#define TAB_ID_HOME "home"
+#define TAB_ID_PREPARE "prepare"
+#define TAB_ID_PREVIEW "preview"
+#define TAB_ID_MONITOR "monitor"
+// Printer-agents mode shows the legacy web page alongside the native Device tab, so it needs an
+// id of its own: sharing TAB_ID_MONITOR makes every name lookup resolve to whichever of the two
+// comes first, which silently defeats PluginPages' selection round-trip across a tab relayout.
+#define TAB_ID_MONITOR_WEB "monitor_web"
+#define TAB_ID_MULTI_DEVICE "multi_device"
+#define TAB_ID_PROJECT "project"
+#define TAB_ID_CALIBRATION "calibration"
#define ENABEL_PRINT_ALL 0
@@ -115,7 +130,7 @@ class MainFrame : public DPIFrame
wxMenuItem* m_menu_item_reslice_now { nullptr };
wxSizer* m_main_sizer{ nullptr };
- size_t m_last_selected_tab;
+ wxString m_last_selected_tab;
std::string get_base_name(const wxString &full_name, const char *extension = nullptr) const;
std::string get_dir_name(const wxString &full_name) const;
@@ -214,19 +229,6 @@ public:
#ifdef __APPLE__
bool get_mac_full_screen() { return m_mac_fullscreen; }
#endif
- //BBS GUI refactor
- enum TabPosition
- {
- tpHome = 0,
- tp3DEditor = 1,
- tpPreview = 2,
- tpMonitor = 3,
- tpMultiDevice = 4,
- tpProject = 5,
- tpCalibration = 6,
- tpAuxiliary = 7,
- toDebugTool = 8,
- };
//BBS: add slice&&print status update logic
enum SlicePrintEventType
@@ -326,8 +328,8 @@ public:
// When tab == -1, will be selected last selected tab
//BBS: GUI refactor
void select_tab(wxPanel* panel);
- void select_tab(size_t tab = size_t(-1));
- void request_select_tab(TabPosition pos);
+ void select_tab(const wxString& id = wxString());
+ void request_select_tab(const wxString& id);
int get_calibration_curr_tab();
void select_view(const std::string& direction);
// Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig
@@ -360,6 +362,9 @@ public:
//SoftFever
void show_device(bool should_use_native);
void fit_tab_labels(); // ORCA
+ // True while either of the two tabs backed by m_plater is selected.
+ bool is_prepare_or_preview_tab() const;
+ PluginPages& plugin_pages() { return m_plugin_pages; }
PA_Calibration_Dlg* m_pa_calib_dlg{ nullptr };
FlowRateCalibrationDialog* m_flow_rate_calib_dlg{ nullptr };
@@ -385,7 +390,8 @@ public:
CalibrationPanel* m_calibration{ nullptr };
WebViewPanel* m_webview { nullptr };
PrinterWebView* m_printer_view{nullptr};
- wxLogWindow* m_log_window { nullptr };
+ PluginPages m_plugin_pages;
+ wxLogWindow* m_log_window { nullptr };
// BBS
//wxBookCtrlBase* m_tabpanel { nullptr };
Notebook* m_tabpanel{ nullptr };
diff --git a/src/slic3r/GUI/Monitor.cpp b/src/slic3r/GUI/Monitor.cpp
index 9f074d3f93..1a6d969988 100644
--- a/src/slic3r/GUI/Monitor.cpp
+++ b/src/slic3r/GUI/Monitor.cpp
@@ -186,17 +186,17 @@ void MonitorPanel::init_tabpanel()
//m_status_add_machine_panel = new AddMachinePanel(m_tabpanel);
m_status_info_panel = new StatusPanel(m_tabpanel);
- m_tabpanel->AddPage(m_status_info_panel, _L("Status"), "", true);
+ m_tabpanel->AddPage(m_status_info_panel, _L("Status"), true);
m_media_file_panel = new MediaFilePanel(m_tabpanel);
- m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), "", false);
- //m_tabpanel->AddPage(m_media_file_panel, _L("Internal Storage"), "", false);
+ m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), false);
+ //m_tabpanel->AddPage(m_media_file_panel, _L("Internal Storage"), false);
m_upgrade_panel = new UpgradePanel(m_tabpanel);
- m_tabpanel->AddPage(m_upgrade_panel, _L_CONTEXT(L_CONTEXT("Update", "Firmware"), "Firmware"), "", false);
+ m_tabpanel->AddPage(m_upgrade_panel, _L_CONTEXT(L_CONTEXT("Update", "Firmware"), "Firmware"), false);
m_hms_panel = new HMSPanel(m_tabpanel);
- m_tabpanel->AddPage(m_hms_panel, _L("Assistant(HMS)"), "", false);
+ m_tabpanel->AddPage(m_hms_panel, _L("Assistant(HMS)"), false);
std::string network_ver = Slic3r::NetworkAgent::get_version();
if (!network_ver.empty()) {
@@ -413,7 +413,10 @@ void MonitorPanel::update_hms_tag()
bool MonitorPanel::Show(bool show)
{
#ifdef __APPLE__
- wxGetApp().mainframe->SetMinSize(wxGetApp().plater()->GetMinSize());
+ // Notebook::InsertPage() hides every page it appends, so this also runs while MainFrame is
+ // still constructing, before GUI_App::mainframe is assigned. Same guard as Plater::Show().
+ if (wxGetApp().mainframe)
+ wxGetApp().mainframe->SetMinSize(wxGetApp().plater()->GetMinSize());
#endif
NetworkAgent* m_agent = wxGetApp().getAgent();
diff --git a/src/slic3r/GUI/MultiMachinePage.cpp b/src/slic3r/GUI/MultiMachinePage.cpp
index b9b71ad670..88d03007b9 100644
--- a/src/slic3r/GUI/MultiMachinePage.cpp
+++ b/src/slic3r/GUI/MultiMachinePage.cpp
@@ -86,9 +86,9 @@ void MultiMachinePage::init_tabpanel()
m_cloud_task_manager = new CloudTaskManagerPage(m_tabpanel);
m_machine_manager = new MultiMachineManagerPage(m_tabpanel);
- m_tabpanel->AddPage(m_machine_manager, _L("Device"), "", true);
- m_tabpanel->AddPage(m_local_task_manager, _L("Task Sending"), "", false);
- m_tabpanel->AddPage(m_cloud_task_manager, _L("Task Sent"), "", false);
+ m_tabpanel->AddPage(m_machine_manager, _L("Device"), true);
+ m_tabpanel->AddPage(m_local_task_manager, _L("Task Sending"), false);
+ m_tabpanel->AddPage(m_cloud_task_manager, _L("Task Sent"), false);
}
void MultiMachinePage::init_timer()
diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp
index ceda3fc0d6..673508454a 100644
--- a/src/slic3r/GUI/Notebook.cpp
+++ b/src/slic3r/GUI/Notebook.cpp
@@ -120,11 +120,11 @@ void ButtonsListCtrl::Rescale()
void ButtonsListCtrl::SetSelection(int sel)
{
- if (m_selection == sel)
+ if (m_selection == sel && sel >= 0 && sel < static_cast(m_pageButtons.size()))
return;
// BBS: change button color
wxColour selected_btn_bg("#009688"); // Gradient #009688
- if (m_selection >= 0) {
+ if (m_selection >= 0 && m_selection < static_cast(m_pageButtons.size())) {
StateColor bg_color = StateColor(
std::pair{wxColour(107, 107, 107), (int) StateColor::Hovered},
std::pair{wxColour(59, 68, 70), (int) StateColor::Normal});
@@ -132,9 +132,15 @@ void ButtonsListCtrl::SetSelection(int sel)
StateColor text_color = StateColor(
std::pair{wxColour(254,254, 254), (int) StateColor::Normal}
);
- m_pageButtons[m_selection]->SetSelected(false);
m_pageButtons[m_selection]->SetTextColor(text_color);
}
+
+ if (sel < 0 || sel >= static_cast(m_pageButtons.size())) {
+ m_selection = -1;
+ Refresh();
+ return;
+ }
+
m_selection = sel;
StateColor bg_color = StateColor(
@@ -145,17 +151,19 @@ void ButtonsListCtrl::SetSelection(int sel)
StateColor text_color = StateColor(
std::pair{wxColour(254, 254, 254), (int) StateColor::Normal}
);
- m_pageButtons[m_selection]->SetSelected(true);
m_pageButtons[m_selection]->SetTextColor(text_color);
Refresh();
}
-bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const std::string &inactive_bmp_name)
+bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const wxBitmap &bmp /* = wxNullBitmap */)
{
Button * btn = new Button(this, text.empty() ? text : " " + text, bmp_name, wxNO_BORDER);
btn->SetCornerRadius(0);
+ if (bmp_name.empty() && bmp.IsOk())
+ btn->SetIcon(bmp);
+
int em = em_unit(this);
//BBS set size for button
btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10});
@@ -168,8 +176,6 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /*
StateColor text_color = StateColor(
std::pair{wxColour(254,254, 254), (int) StateColor::Normal});
btn->SetTextColor(text_color);
- btn->SetInactiveIcon(inactive_bmp_name);
- btn->SetSelected(false);
btn->Bind(wxEVT_BUTTON, [this, btn](wxCommandEvent& event) {
if (auto it = std::find(m_pageButtons.begin(), m_pageButtons.end(), btn); it != m_pageButtons.end()) {
auto sel = it - m_pageButtons.begin();
@@ -192,6 +198,14 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /*
void ButtonsListCtrl::RemovePage(size_t n)
{
+ if (n >= m_pageButtons.size())
+ return;
+
+ if (m_selection == static_cast(n))
+ m_selection = -1;
+ else if (m_selection > static_cast(n))
+ --m_selection;
+
Button* btn = m_pageButtons[n];
m_pageButtons.erase(m_pageButtons.begin() + n);
m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA
@@ -240,6 +254,24 @@ wxString ButtonsListCtrl::GetPageText(size_t n) const
return btn->GetLabel();
}
+// ORCA
+void ButtonsListCtrl::SetOverflowButton(wxWindow* button)
+{
+ if (m_overflow_button == button)
+ return;
+
+ if (m_overflow_button != nullptr)
+ m_sizer->Detach(m_overflow_button);
+
+ m_overflow_button = button;
+
+ if (m_overflow_button != nullptr)
+ // Right after the tab buttons (index 0), ahead of any stretch spacer / side_tools.
+ m_sizer->Insert(1, m_overflow_button, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxBOTTOM, m_btn_margin);
+
+ m_sizer->Layout();
+}
+
//#endif // _WIN32
void Notebook::Init()
@@ -253,6 +285,8 @@ void Notebook::Init()
m_showTimeout = m_hideTimeout = 0;
+ m_pageNames.clear();
+
/* On Linux, Gstreamer wxMediaCtrl does not seem to get along well with
* 32-bit X11 visuals (the overlay does not work). Is this a wxWindows
* bug? Is this a Gstreamer bug? No idea, but it is our problem ...
diff --git a/src/slic3r/GUI/Notebook.hpp b/src/slic3r/GUI/Notebook.hpp
index d333956561..4734122b18 100644
--- a/src/slic3r/GUI/Notebook.hpp
+++ b/src/slic3r/GUI/Notebook.hpp
@@ -3,7 +3,11 @@
//#ifdef _WIN32
+#include
+#include
+#include
#include
+#include
#include
class ScalableButton;
@@ -23,13 +27,16 @@ public:
void SetSelection(int sel);
void UpdateMode();
void Rescale();
- bool InsertPage(size_t n, const wxString &text, bool bSelect = false, const std::string &bmp_name = "", const std::string &inactive_bmp_name = "");
+ bool InsertPage(size_t n, const wxString &text, bool bSelect = false, const std::string &bmp_name = "", const wxBitmap &bmp = wxNullBitmap);
void RemovePage(size_t n);
bool SetPageImage(size_t n, const std::string& bmp_name) const;
void SetPageText(size_t n, const wxString& strText);
void SetCompact(size_t n, bool compact); // ORCA
wxString GetPageText(size_t n) const;
wxFlexGridSizer* GetBtnsSizer(){return m_buttons_sizer;}; // ORCA
+ // ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g.
+ // an overflow indicator. Pass nullptr to remove it; ownership stays with the caller.
+ void SetOverflowButton(wxWindow* button);
private:
wxFlexGridSizer* m_buttons_sizer;
@@ -40,9 +47,10 @@ private:
int m_btn_margin;
int m_line_margin;
std::vector m_pageLabels; // ORCA
+ wxWindow* m_overflow_button{nullptr}; // ORCA
};
-class Notebook: public wxBookCtrlBase
+class Notebook : public wxBookCtrlBase
{
public:
Notebook(wxWindow * parent,
@@ -103,7 +111,7 @@ public:
// by this control) and show it immediately.
bool ShowNewPage(wxWindow * page)
{
- return AddPage(page, wxString(), "", "");
+ return AddPage(page, wxString(), false, NO_IMAGE);
}
@@ -135,51 +143,56 @@ public:
// Implement base class pure virtual methods.
- // adds a new page to the control
- bool AddPage(wxWindow* page,
+ // Page management. Every insertion funnels through the InsertPage() below; `id` is the
+ // stable page name FindPageByName() resolves. Built-in tabs name a resource bitmap,
+ // plugin pages hand over a ready wxBitmap; wx's own imageId overloads carry neither.
+ bool AddPage(const wxString& id,
+ wxWindow* page,
const wxString& text,
- const std::string& bmp_name,
- const std::string& inactive_bmp_name,
+ const std::string& bmp_name = "",
bool bSelect = false)
{
DoInvalidateBestSize();
- return InsertPage(GetPageCount(), page, text, bmp_name, inactive_bmp_name, bSelect);
+ return InsertPage(GetPageCount(), id, page, text, bmp_name, bSelect);
}
- // Page management
- virtual bool InsertPage(size_t n,
- wxWindow * page,
- const wxString & text,
- bool bSelect = false,
- int imageId = NO_IMAGE) override
+ bool AddPage(wxWindow* page, const wxString& text, bool bSelect = false, int imageId = NO_IMAGE) override
{
- if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, imageId))
+ DoInvalidateBestSize();
+ return InsertPage(GetPageCount(), page, text, bSelect, imageId);
+ }
+
+ bool InsertPage(size_t n,
+ const wxString& id,
+ wxWindow * page,
+ const wxString & text,
+ const std::string& bmp_name = "",
+ bool bSelect = false,
+ const wxBitmap& bmp = wxNullBitmap)
+ {
+ if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect))
return false;
- GetBtnsListCtrl()->InsertPage(n, text, bSelect);
+ m_pageNames.insert(m_pageNames.begin() + n, id);
+ GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, bmp);
+ // wxBookCtrlBase::InsertPage() only inserts into the page list and sizes the new
+ // page to the current page's rect — it never touches visibility, and a freshly
+ // constructed page defaults to shown. Without this it renders on top of whatever
+ // page is currently selected until the next SetSelection() call hides it.
if (!DoSetSelectionAfterInsertion(n, bSelect))
page->Hide();
return true;
}
- bool InsertPage(size_t n,
- wxWindow * page,
- const wxString & text,
- const std::string& bmp_name = "",
- const std::string& inactive_bmp_name = "",
- bool bSelect = false)
+ virtual bool InsertPage(size_t n,
+ wxWindow * page,
+ const wxString & text,
+ bool bSelect = false,
+ int WXUNUSED(imageId) = NO_IMAGE) override
{
- if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect))
- return false;
-
- GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, inactive_bmp_name);
-
- if (bSelect)
- SetSelection(n);
-
- return true;
+ return InsertPage(n, wxString(), page, text, "", bSelect);
}
virtual int SetSelection(size_t n) override
@@ -211,8 +224,8 @@ public:
return DoSetSelection(n);
}
- // Neither labels nor images are supported but we still store the labels
- // just in case the user code attaches some importance to them.
+ // Labels are stored by the custom button list; wx's image-list API is unused — tab icons
+ // are set directly on the buttons, either from a resource name or a ready wxBitmap.
virtual bool SetPageText(size_t n, const wxString & strText) override
{
wxCHECK_MSG(n < GetPageCount(), false, wxS("Invalid page"));
@@ -251,7 +264,64 @@ public:
page->SetFocus();
}
+ // The base clears its page list directly instead of calling DoRemovePage() per page,
+ // which would leave m_pageNames behind. No caller today; kept in sync regardless.
+ virtual bool DeleteAllPages() override
+ {
+ m_pageNames.clear();
+ return wxBookCtrlBase::DeleteAllPages();
+ }
+
ButtonsListCtrl* GetBtnsListCtrl() const { return static_cast(m_bookctrl); }
+ void SetOverflowButton(wxWindow* button) { GetBtnsListCtrl()->SetOverflowButton(button); }
+
+ // Insertion index just past the first of `ids` that is present, or the end of the bar
+ // if none is — lets call sites state tab order as "after X" instead of re-deriving it.
+ size_t PositionAfter(std::initializer_list ids) const
+ {
+ for (const char* id : ids)
+ if (const int idx = FindPageByName(id); idx != wxNOT_FOUND)
+ return static_cast(idx) + 1;
+ return GetPageCount();
+ }
+
+ int FindPageByName(const wxString& id) const
+ {
+ if (id.empty())
+ return wxNOT_FOUND;
+ for (size_t i = 0; i < m_pageNames.size(); ++i)
+ if (m_pageNames[i] == id)
+ return static_cast(i);
+ return wxNOT_FOUND;
+ }
+
+ wxWindow* GetPageByName(const wxString& id) const
+ {
+ const int idx = FindPageByName(id);
+ return idx == wxNOT_FOUND ? nullptr : GetPage(static_cast(idx));
+ }
+
+ bool SelectPageByName(const wxString& id)
+ {
+ const int idx = FindPageByName(id);
+ if (idx == wxNOT_FOUND)
+ return false;
+ SetSelection(static_cast(idx));
+ return true;
+ }
+
+ // Inverse of FindPageByName: index -> id. Empty string for an out-of-range
+ // index or a page that was never given an id (e.g. settings Tab pages).
+ wxString GetPageName(size_t n) const
+ {
+ return n < m_pageNames.size() ? m_pageNames[n] : wxString();
+ }
+
+ wxString GetSelectedPageName() const
+ {
+ const int sel = GetSelection();
+ return sel < 0 ? wxString() : GetPageName(static_cast(sel));
+ }
void UpdateMode()
{
@@ -369,6 +439,7 @@ protected:
wxWindow* const win = wxBookCtrlBase::DoRemovePage(page);
if (win)
{
+ m_pageNames.erase(m_pageNames.begin() + page);
GetBtnsListCtrl()->RemovePage(page);
DoSetSelectionAfterRemoval(page);
}
@@ -394,6 +465,8 @@ protected:
private:
void Init();
+ std::vector m_pageNames; // index-parallel to wxBookCtrlBase::m_pages
+
wxShowEffect m_showEffect,
m_hideEffect;
diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp
index 8e81f0654c..5e83ad845f 100644
--- a/src/slic3r/GUI/NotificationManager.cpp
+++ b/src/slic3r/GUI/NotificationManager.cpp
@@ -1918,7 +1918,7 @@ void NotificationManager::push_validate_error_notification(StringObjectException
wxGetApp().sidebar().jump_to_option(opt, Preset::TYPE_PRINT, L"");
}
else {
- wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
}
return false;
};
@@ -1985,7 +1985,7 @@ void NotificationManager::push_validate_error_notification(StringObjectException
wxGetApp().sidebar().jump_to_option(opt, opt_type, L"");
}
else {
- wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
}
return false;
};
@@ -2015,7 +2015,7 @@ void NotificationManager::push_slicing_error_notification(const std::string &tex
if (iter != objects.end()) { ovs.push_back({ *iter, nullptr }); }
}
if (!ovs.empty()) {
- wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
wxGetApp().obj_list()->select_items(ovs);
}
return false;
@@ -2046,7 +2046,7 @@ void NotificationManager::push_slicing_warning_notification(const std::string& t
auto& objects = wxGetApp().model().objects;
auto iter = std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; });
if (iter != objects.end()) {
- wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
wxGetApp().obj_list()->select_items({ {*iter, nullptr} });
}
return false;
@@ -2693,7 +2693,7 @@ void NotificationManager::push_slicing_serious_warning_notification(const std::s
if (iter != objects.end()) { ovs.push_back({ *iter, nullptr }); }
}
if (!ovs.empty()) {
- wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
wxGetApp().obj_list()->select_items(ovs);
wxGetApp().obj_list()->update_selections_on_canvas();
}
@@ -2777,7 +2777,7 @@ void NotificationManager::push_slicing_serious_warning_notification(const std::s
}
}
- wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (!sel_items.empty()) {
obj_list->select_items(sel_items);
diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp
index 583f7d1528..b8cc0c738f 100644
--- a/src/slic3r/GUI/Plater.cpp
+++ b/src/slic3r/GUI/Plater.cpp
@@ -5831,6 +5831,8 @@ private:
bool show_warning_dialog { false };
};
+Plater::~Plater() = default;
+
const std::regex Plater::priv::pattern_bundle(".*[.](amf|amf[.]xml|zip[.]amf|3mf)", std::regex::icase);
const std::regex Plater::priv::pattern_3mf(".*3mf", std::regex::icase);
const std::regex Plater::priv::pattern_zip_amf(".*[.]zip[.]amf", std::regex::icase);
@@ -5845,7 +5847,7 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi
#endif // WIN32
m_mainframe.Raise();
- m_mainframe.select_tab(size_t(MainFrame::tp3DEditor));
+ m_mainframe.select_tab(TAB_ID_PREPARE);
if (wxGetApp().is_editor())
m_plater.select_view_3D("3D");
@@ -6627,9 +6629,9 @@ void Plater::priv::select_next_view_3D()
{
if (current_panel == view3D)
- wxGetApp().mainframe->select_tab(size_t(MainFrame::tpPreview));
+ wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW);
else if (current_panel == preview)
- wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
// else if (current_panel == assemble_view)
// set_current_panel(view3D);
}
@@ -7928,7 +7930,7 @@ std::vector Plater::priv::load_files(const std::vector& input_
q->select_plate(first_plate_index);
//set to 3d tab
q->select_view_3D("Preview");
- wxGetApp().mainframe->select_tab(MainFrame::tpPreview);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW);
}
else {
//set to 3d tab
@@ -7947,7 +7949,7 @@ std::vector Plater::priv::load_files(const std::vector& input_
else {
//always set to 3D after loading files
q->select_view_3D("3D");
- wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
}
if (load_model) {
@@ -8855,7 +8857,7 @@ void Plater::priv::process_validation_warning(StringObjectException const &warni
}
}
- wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (inst_idx != -1) {
auto* model = wxGetApp().obj_list()->GetModel();
@@ -8884,7 +8886,7 @@ void Plater::priv::process_validation_warning(StringObjectException const &warni
} else {
auto iter = id.id ? std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }) : objects.end();
if (iter != objects.end()) {
- wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
wxGetApp().obj_list()->select_items({{*iter, nullptr}});
wxGetApp().obj_list()->update_selections_on_canvas();
}
@@ -11268,13 +11270,19 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e)
}
const int new_sel = e.GetSelection();
- sidebar_layout.show = new_sel == MainFrame::tp3DEditor || new_sel == MainFrame::tpPreview;
+ if (new_sel == wxNOT_FOUND) {
+ // GetPage(new_sel) below needs a valid index.
+ e.Skip();
+ return;
+ }
+ const wxString new_name = main_frame->m_tabpanel->GetPageName(new_sel);
+ sidebar_layout.show = new_name == TAB_ID_PREPARE || new_name == TAB_ID_PREVIEW;
update_sidebar();
int old_sel = e.GetOldSelection();
const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents");
const bool use_native_device_tab = wxGetApp().preset_bundle &&
(wxGetApp().preset_bundle->use_bbl_device_tab() || use_printer_agents);
- if (use_native_device_tab && new_sel == MainFrame::tpMonitor) {
+ if (use_native_device_tab && new_name == TAB_ID_MONITOR) {
// BBL network module is only required for BBL-vendor printers.
// Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it.
if (!use_printer_agents && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) {
@@ -11286,12 +11294,15 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e)
}
}
} else {
+ // Pointer test, not a name lookup: in printer-agents mode this page is TAB_ID_MONITOR_WEB
+ // while the native Device tab holds TAB_ID_MONITOR, and in legacy-web mode it holds
+ // TAB_ID_MONITOR itself.
const bool selecting_web_device_tab = main_frame->m_printer_view &&
main_frame->m_tabpanel->GetPage(new_sel) == main_frame->m_printer_view;
if (selecting_web_device_tab) {
// Use the selected discovered machine when the preset has no host.
main_frame->load_printer_url();
- } else if (new_sel == MainFrame::tpMonitor && wxGetApp().preset_bundle != nullptr) {
+ } else if (new_name == TAB_ID_MONITOR && wxGetApp().preset_bundle != nullptr) {
auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config;
wxString url = from_u8(PrintHost::get_print_host_webui(&cfg));
if (main_frame->m_printer_view && url.empty()) {
@@ -12154,7 +12165,7 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all)
wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL));
else
wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE));
- wxGetApp().mainframe->m_tabpanel->SetSelection(MainFrame::TabPosition::tpPreview);
+ wxGetApp().mainframe->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW);
}
return false;
}
@@ -12722,7 +12733,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();
@@ -12832,7 +12843,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;
@@ -13111,7 +13122,7 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_
get_notification_manager()->clear_all();
if (!silent)
- wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
//get_partplate_list().reinit();
//get_partplate_list().update_slice_context_to_current_plate(p->background_process);
@@ -13260,7 +13271,7 @@ void Plater::load_project(wxString const& filename2,
if (!m_exported_file) {
p->select_view("topfront");
p->camera.requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE;
- wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
}
else {
p->partplate_list.select_plate_view();
@@ -13374,7 +13385,7 @@ void Plater::import_model_id(wxString download_info)
const int max_retries = 3;
/* jump to 3D eidtor */
- wxGetApp().mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
/* prepare progress dialog */
bool cont = true;
@@ -13683,7 +13694,7 @@ void Plater::calib_pa(const Calib_Params& params)
{
const auto calib_pa_name = wxString::Format(L"Pressure Advance Test");
new_project(false, false, calib_pa_name);
- wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
print_config->set_key_value("overhang_reverse", new ConfigOptionBool(false));
@@ -14164,7 +14175,7 @@ void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) {
if (new_project(false, false, calib_name) == wxID_CANCEL)
return;
- wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (is_linear) {
if (pass == 1)
@@ -14201,7 +14212,7 @@ void Plater::calib_temp(const Calib_Params& params) {
const auto calib_temp_name = wxString::Format(L"Nozzle temperature test");
new_project(false, false, calib_temp_name);
- wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Temp_Tower) return;
if (!add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc"))
@@ -14281,7 +14292,7 @@ void Plater::calib_max_vol_speed(const Calib_Params& params)
{
const auto calib_vol_speed_name = wxString::Format(L"Max volumetric speed test");
new_project(false, false, calib_vol_speed_name);
- wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Vol_speed_Tower)
return;
if (!add_model(false, Slic3r::resources_dir() + "/calib/volumetric_speed/SpeedTestStructure.drc"))
@@ -14360,7 +14371,7 @@ void Plater::calib_retraction(const Calib_Params& params)
{
const auto calib_retraction_name = wxString::Format(L"Retraction");
new_project(false, false, calib_retraction_name);
- wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Retraction_tower)
return;
@@ -14420,7 +14431,7 @@ void Plater::calib_VFA(const Calib_Params& params)
{
const auto calib_vfa_name = wxString::Format(L"VFA test");
new_project(false, false, calib_vfa_name);
- wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_VFA_Tower)
return;
@@ -14503,7 +14514,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params)
{
const auto calib_input_shaping_name = wxString::Format(L"Input shaping Frequency test");
new_project(false, false, calib_input_shaping_name);
- wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Input_shaping_freq)
return;
@@ -14569,7 +14580,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params)
{
const auto calib_input_shaping_name = wxString::Format(L"Input shaping Damping test");
new_project(false, false, calib_input_shaping_name);
- wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Input_shaping_damp)
return;
@@ -14634,7 +14645,7 @@ void Plater::Calib_Cornering(const Calib_Params& params)
{
const auto Calib_Cornering = wxString::Format(L"Cornering test");
new_project(false, false, Calib_Cornering);
- wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Cornering)
return;
@@ -14767,7 +14778,7 @@ void Plater::load_gcode(const wxString& filename)
//p->gcode_result.reset();
//reset_gcode_toolpaths();
p->preview->reload_print(m_only_gcode);
- wxGetApp().mainframe->select_tab(MainFrame::tpPreview);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW);
p->set_current_panel(p->preview, true);
p->get_current_canvas3D()->render();
//p->notification_manager->bbl_show_plateinfo_notification(into_u8(_L("Preview only mode for gcode file.")));
@@ -15440,7 +15451,7 @@ LoadType determine_load_type(std::string filename, std::string override_setting)
wxGetApp().app_config->set("import_project_action", std::to_string(choice));
// BBS: jump to plater panel
- wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
return load_type;
}
@@ -15669,7 +15680,7 @@ void Plater::reset_with_confirm()
.ShowModal() == wxID_YES) {
reset();
// BBS: jump to plater panel
- wxGetApp().mainframe->select_tab(size_t(0));
+ wxGetApp().mainframe->select_tab(TAB_ID_HOME);
}
}
@@ -17483,7 +17494,7 @@ int Plater::export_config_3mf(int plate_idx, Export3mfProgressFn proFn)
//BBS
void Plater::send_calibration_job_finished(wxCommandEvent & evt)
{
- p->main_frame->request_select_tab(MainFrame::TabPosition::tpCalibration);
+ p->main_frame->request_select_tab(TAB_ID_CALIBRATION);
auto calibration_panel = p->main_frame->m_calibration;
if (calibration_panel) {
auto curr_wizard = static_cast(calibration_panel->get_tabpanel()->GetPage(evt.GetInt()));
@@ -17515,7 +17526,7 @@ void Plater::print_job_finished(wxCommandEvent &evt)
if (!dev) return;
dev->set_selected_machine(evt.GetString().ToStdString());
- p->main_frame->request_select_tab(MainFrame::TabPosition::tpMonitor);
+ p->main_frame->request_select_tab(TAB_ID_MONITOR);
//jump to monitor and select device status panel
MonitorPanel* curr_monitor = p->main_frame->m_monitor;
if(curr_monitor)
@@ -17530,7 +17541,7 @@ void Plater::send_job_finished(wxCommandEvent& evt)
send_gcode_finish(evt.GetString());
p->hide_send_to_printer_dlg();
- //p->main_frame->request_select_tab(MainFrame::TabPosition::tpMonitor);
+ //p->main_frame->request_select_tab(TAB_ID_MONITOR);
////jump to monitor and select device status panel
//MonitorPanel* curr_monitor = p->main_frame->m_monitor;
//if (curr_monitor)
@@ -18424,7 +18435,7 @@ void Plater::pop_warning_and_go_to_device_page(wxString printer_name, PrinterWar
MessageDialog dlg(this, content, title, wxOK | wxFORWARD | wxICON_WARNING, _L("Device Page"));
auto result = dlg.ShowModal();
if (result == wxFORWARD) {
- wxGetApp().mainframe->select_tab(size_t(MainFrame::tpMonitor));
+ wxGetApp().mainframe->select_tab(TAB_ID_MONITOR);
}
}
diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp
index b60c0eb242..6a42f61fd5 100644
--- a/src/slic3r/GUI/Plater.hpp
+++ b/src/slic3r/GUI/Plater.hpp
@@ -290,7 +290,7 @@ public:
Plater(const Plater &) = delete;
Plater &operator=(Plater &&) = delete;
Plater &operator=(const Plater &) = delete;
- ~Plater() = default;
+ ~Plater();
bool Show(bool show = true);
@@ -1020,4 +1020,4 @@ wxArrayString get_all_camera_view_type();
} // namespace GUI
} // namespace Slic3r
-#endif
\ No newline at end of file
+#endif
diff --git a/src/slic3r/GUI/PluginWebDialog.cpp b/src/slic3r/GUI/PluginWebDialog.cpp
index 1808f21ce9..d89aac7270 100644
--- a/src/slic3r/GUI/PluginWebDialog.cpp
+++ b/src/slic3r/GUI/PluginWebDialog.cpp
@@ -15,39 +15,6 @@ namespace Slic3r { namespace GUI {
namespace {
-// Low-specificity element defaults (no !important) for UNSTYLED plugin HTML, so a bare
-// plugin page looks native while any CSS the plugin ships still wins. Built on the
-// --orca-* variables the host injects (see WebViewHostDialog); document-start injected
-// AFTER the host contract so the variables are defined (shares the base injector's
-// WebView2 timing guard).
-std::string plugin_defaults_user_script()
-{
- std::string css;
- css += "";
- return WebViewHostDialog::document_start_injector(css, "orca-plugin-defaults", "beforeend");
-}
-
// Injected into the top-level page at document start (before the plugin's own
// scripts). Defines window.orca as the only host surface the page may use. It
// references window.wx lazily (at call time) so it never races the backend's
@@ -129,7 +96,7 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
void PluginWebDialog::add_user_scripts()
{
if (wxWebView* wv = browser()) {
- wv->AddUserScript(wxString::FromUTF8(plugin_defaults_user_script()));
+ wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script()));
wv->AddUserScript(ORCA_BRIDGE_JS);
}
}
diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp
index f802ba6ecb..ca115b9773 100644
--- a/src/slic3r/GUI/Preferences.cpp
+++ b/src/slic3r/GUI/Preferences.cpp
@@ -1748,11 +1748,26 @@ void PreferencesDialog::create_items()
g_sizer->Add(item_pop_up_filament_map_dialog);
#endif
+ //// GENERAL > Plugins
+ g_sizer->Add(create_item_title(_L("Plugins")), 1, wxEXPAND);
+
+ auto item_plugin_pages_visible_count = create_item_spinctrl(
+ _L("Visible plugin pages"),
+ "",
+ _L("pages"),
+ _L("Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab."),
+ SETTING_PLUGIN_PAGES_VISIBLE_COUNT,
+ PLUGIN_PAGES_VISIBLE_COUNT_MIN,
+ PLUGIN_PAGES_VISIBLE_COUNT_MAX,
+ [](int value) { wxGetApp().mainframe->plugin_pages().set_visible_page_count(value); }
+ );
+ g_sizer->Add(item_plugin_pages_visible_count);
+
g_sizer->AddSpacer(FromDIP(10));
sizer_page->Add(g_sizer, 0, wxEXPAND);
//////////////////////////
- //// CONTROL TAB
+ //// CONTROL TAB
/////////////////////////////////////
m_pref_tabs->AppendItem(_L("Control"));
f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0));
diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp
index 27653790dd..2f82a5630e 100644
--- a/src/slic3r/GUI/PresetComboBoxes.cpp
+++ b/src/slic3r/GUI/PresetComboBoxes.cpp
@@ -1048,7 +1048,7 @@ bool PlaterPresetComboBox::switch_to_tab()
//BBS Select NoteBook Tab params
if (tab->GetParent() == wxGetApp().params_panel())
- wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
+ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
else {
wxGetApp().params_dialog()->Popup();
tab->OnActivate();
diff --git a/src/slic3r/GUI/PresetComboBoxes.hpp b/src/slic3r/GUI/PresetComboBoxes.hpp
index dcd5457dfd..b63f51dd70 100644
--- a/src/slic3r/GUI/PresetComboBoxes.hpp
+++ b/src/slic3r/GUI/PresetComboBoxes.hpp
@@ -39,7 +39,7 @@ public:
PresetComboBox(wxWindow* parent, Preset::Type preset_type, const wxSize& size = wxDefaultSize, PresetBundle* preset_bundle = nullptr);
~PresetComboBox();
- enum LabelItemType {
+ enum LabelItemType : std::size_t {
LABEL_ITEM_PHYSICAL_PRINTER = 0xffffff01,
LABEL_ITEM_PRINTER_MODELS,
LABEL_ITEM_DISABLED,
diff --git a/src/slic3r/GUI/Project.cpp b/src/slic3r/GUI/Project.cpp
index 57410b1202..6d1eb0e180 100644
--- a/src/slic3r/GUI/Project.cpp
+++ b/src/slic3r/GUI/Project.cpp
@@ -74,7 +74,18 @@ ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos,
Fit();
}
-ProjectPanel::~ProjectPanel() {}
+ProjectPanel::~ProjectPanel()
+{
+ shutdown();
+}
+
+void ProjectPanel::shutdown()
+{
+ m_reload_cancel_token->store(true, std::memory_order_release);
+ if (m_reload_task && m_reload_task->joinable())
+ m_reload_task->join();
+ m_reload_task.reset();
+}
// Helper to convert newlines to
static std::string convert_newlines_to_br(const std::string& text) {
@@ -101,7 +112,17 @@ void ProjectPanel::onWebNavigating(wxWebViewEvent& evt)
void ProjectPanel::on_reload(wxCommandEvent& evt)
{
- boost::thread reload = boost::thread([this] {
+ if (wxTheApp == nullptr || wxGetApp().is_closing() ||
+ m_reload_cancel_token->load(std::memory_order_acquire))
+ return;
+
+ if (m_reload_task && m_reload_task->joinable())
+ m_reload_task->join();
+
+ const auto cancel_token = m_reload_cancel_token;
+ m_reload_task = std::make_unique([this, cancel_token] {
+ if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
+ return;
std::string update_type;
std::string license;
std::string model_name;
@@ -115,6 +136,9 @@ void ProjectPanel::on_reload(wxCommandEvent& evt)
std::map> files;
+ if (wxGetApp().plater() == nullptr)
+ return;
+
Model model = wxGetApp().plater()->model();
auto model_info = model.model_info;
@@ -156,7 +180,14 @@ void ProjectPanel::on_reload(wxCommandEvent& evt)
std::string file_path = encode_path(wxGetApp().plater()->model().get_auxiliary_file_temp_path().c_str());
if (!file_path.empty()) {
files = Reload(file_path);
- wxGetApp().CallAfter([this, file_path, files] { m_auxiliary->Reload(file_path, files); });
+ if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
+ return;
+
+ wxGetApp().CallAfter([this, cancel_token, file_path, files] {
+ if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
+ return;
+ m_auxiliary->Reload(file_path, files);
+ });
} else {
clear_model_info();
return;
@@ -215,15 +246,18 @@ void ProjectPanel::on_reload(wxCommandEvent& evt)
json m_Res = json::object();
m_Res["command"] = "show_3mf_info";
- m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id++);
+ m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed));
m_Res["model"] = j;
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore));
- if (m_web_init_completed) {
- wxGetApp().CallAfter([this, strJS] {
+ if (m_web_init_completed.load(std::memory_order_acquire) &&
+ !cancel_token->load(std::memory_order_acquire) && wxTheApp != nullptr && !wxGetApp().is_closing()) {
+ wxGetApp().CallAfter([this, cancel_token, strJS] {
+ if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
+ return;
RunScript(strJS.ToStdString());
- });
+ });
}
});
}
@@ -264,7 +298,7 @@ void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt)
}
}
else if (strCmd == "request_3mf_info") {
- m_web_init_completed = true;
+ m_web_init_completed.store(true, std::memory_order_release);
}
else if (strCmd == "edit_project_info") {
show_info_editor(true);
@@ -307,13 +341,20 @@ void ProjectPanel::update_model_data()
void ProjectPanel::clear_model_info()
{
+ if (wxTheApp == nullptr || wxGetApp().is_closing() ||
+ m_reload_cancel_token->load(std::memory_order_acquire))
+ return;
+
json m_Res = json::object();
m_Res["command"] = "clear_3mf_info";
- m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id++);
+ m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed));
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore));
- wxGetApp().CallAfter([this, strJS] {
+ const auto cancel_token = m_reload_cancel_token;
+ wxGetApp().CallAfter([this, cancel_token, strJS] {
+ if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
+ return;
RunScript(strJS.ToStdString());
});
}
diff --git a/src/slic3r/GUI/Project.hpp b/src/slic3r/GUI/Project.hpp
index 0071685e7d..a41f76ba7e 100644
--- a/src/slic3r/GUI/Project.hpp
+++ b/src/slic3r/GUI/Project.hpp
@@ -26,9 +26,11 @@
#include "nlohmann/json.hpp"
#include "slic3r/Utils/json_diff.hpp"
+#include
#include