mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-12 18:31:18 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b29f06a0ab | ||
|
|
9ec0d39592 | ||
|
|
0b9b05ed85 |
@@ -16,15 +16,6 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
# Listing caches and builds while waiting for a deps cache.
|
|
||||||
actions: read
|
|
||||||
|
|
||||||
# Read by scripts/ci_deps_cache.sh.
|
|
||||||
env:
|
|
||||||
REPO: ${{ github.repository }}
|
|
||||||
WORKFLOW: build_all.yml
|
|
||||||
BASE_REF: ${{ github.base_ref }}
|
|
||||||
JOB: build_linux (${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04' }})
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||||
@@ -44,7 +35,7 @@ jobs:
|
|||||||
# The PR merge commit plus its first parent, the base it is diffed against.
|
# The PR merge commit plus its first parent, the base it is diffed against.
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
- name: Look for changed files
|
- name: Look for changed C++ files
|
||||||
id: changes
|
id: changes
|
||||||
run: |
|
run: |
|
||||||
if git -c core.quotePath=false diff --name-only HEAD^1 -- src tests | grep -qE '\.(cpp|cc|cxx|hpp|h|hxx)$'; then
|
if git -c core.quotePath=false diff --name-only HEAD^1 -- src tests | grep -qE '\.(cpp|cc|cxx|hpp|h|hxx)$'; then
|
||||||
@@ -52,75 +43,18 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "No C++ changes under src/ or tests/."
|
echo "No C++ changes under src/ or tests/."
|
||||||
fi
|
fi
|
||||||
if ! git diff --quiet HEAD^1 -- deps; then
|
|
||||||
echo "deps=true" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
echo "deps-key=linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Parsing needs the dependency headers, not a build of this PR's deps/.
|
# Parsing needs the dependency headers, not a build of this PR's deps/, so
|
||||||
# Without an exact match the job waits for the build that saves it: this
|
# a PR that changes deps/ falls back to the newest cache main has.
|
||||||
# PR's own when it changes deps/, otherwise a running push build of the
|
|
||||||
# base branch with the same deps. With neither, a PR that keeps deps/
|
|
||||||
# falls back to the newest cache.
|
|
||||||
- name: Restore cached deps
|
- name: Restore cached deps
|
||||||
id: deps_cache
|
|
||||||
if: steps.changes.outputs.cpp == 'true'
|
if: steps.changes.outputs.cpp == 'true'
|
||||||
uses: actions/cache/restore@v6
|
uses: actions/cache/restore@v6
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
|
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
|
||||||
key: ${{ steps.changes.outputs.deps-key }}
|
key: linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}
|
||||||
|
|
||||||
- name: Look for a build to wait for
|
|
||||||
id: wait_check
|
|
||||||
if: steps.changes.outputs.cpp == 'true' && steps.deps_cache.outputs.cache-hit != 'true'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
||||||
run: bash scripts/ci_deps_cache.sh ${{ steps.changes.outputs.deps == 'true' && 'own' || 'check' }}
|
|
||||||
|
|
||||||
# Linux Build Deps takes about 35 minutes on GitHub-hosted runners. The
|
|
||||||
# wait ends early when the deps job fails.
|
|
||||||
- name: Wait for the deps cache
|
|
||||||
if: steps.wait_check.outputs.wait-for != ''
|
|
||||||
timeout-minutes: 100
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
KEY: ${{ steps.changes.outputs.deps-key }}
|
|
||||||
CACHE_REF: ${{ steps.changes.outputs.deps == 'true' && github.ref || format('refs/heads/{0}', github.base_ref) }}
|
|
||||||
KEY_INPUTS: ${{ steps.wait_check.outputs.key-inputs }}
|
|
||||||
WAIT_FOR: ${{ steps.wait_check.outputs.wait-for }}
|
|
||||||
WAIT_SINCE: ${{ steps.wait_check.outputs.wait-since }}
|
|
||||||
WAIT_MINUTES: 90
|
|
||||||
run: bash scripts/ci_deps_cache.sh wait
|
|
||||||
|
|
||||||
- name: Restore the deps cache after waiting
|
|
||||||
id: waited_cache
|
|
||||||
if: steps.wait_check.outputs.wait-for != ''
|
|
||||||
uses: actions/cache/restore@v6
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
|
|
||||||
key: ${{ steps.changes.outputs.deps-key }}
|
|
||||||
|
|
||||||
- name: Fail without this PR's deps cache
|
|
||||||
if: steps.changes.outputs.deps == 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
|
||||||
echo "::error::No deps cache ${{ steps.changes.outputs.deps-key }}. Check this PR's Linux Build Deps job, then rerun clang-tidy."
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: Restore the newest cached deps
|
|
||||||
id: newest_cache
|
|
||||||
if: steps.changes.outputs.deps != 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true'
|
|
||||||
uses: actions/cache/restore@v6
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
|
|
||||||
key: ${{ steps.changes.outputs.deps-key }}
|
|
||||||
restore-keys: linux-clang-cache-orcaslicer_deps-build-
|
restore-keys: linux-clang-cache-orcaslicer_deps-build-
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
|
||||||
- name: Warn about older deps
|
|
||||||
if: steps.newest_cache.outcome == 'success' && steps.newest_cache.outputs.cache-hit != 'true'
|
|
||||||
run: echo "::warning title=Older deps::No cache matches this PR's deps/, so clang-tidy parsed against ${{ steps.newest_cache.outputs.cache-matched-key }}. If it reports errors in code that uses a dependency, rerun clang-tidy once the base branch's Build all has saved the new deps."
|
|
||||||
|
|
||||||
- name: Apt-Install Dependencies
|
- name: Apt-Install Dependencies
|
||||||
if: steps.changes.outputs.cpp == 'true' && !vars.SELF_HOSTED
|
if: steps.changes.outputs.cpp == 'true' && !vars.SELF_HOSTED
|
||||||
uses: ./.github/actions/apt-install-deps
|
uses: ./.github/actions/apt-install-deps
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
name: clang-tidy fix
|
||||||
|
|
||||||
|
# Makes the missing-include fix that /bot fix-tidy (pr-tidy-bot.yml) posts on a
|
||||||
|
# pull request: scripts/run_clang_tidy.sh --fix on the merge commit the
|
||||||
|
# clang-tidy check runs on, uploaded as the clang-tidy-fix artifact.
|
||||||
|
#
|
||||||
|
# Every push to a PR starts this workflow, and its job skips on that first
|
||||||
|
# attempt. The bot re-runs it on request. A re-run keeps the pull_request
|
||||||
|
# context, so the PR's code runs with a read-only token, no secrets and the
|
||||||
|
# PR's own cache, never main's, whoever asked for it.
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- release/*
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
fix:
|
||||||
|
if: github.run_attempt > 1
|
||||||
|
# A hosted runner, so nothing the PR's code leaves behind outlives the job.
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
lfs: 'false'
|
||||||
|
# The PR merge commit plus its first parent, the base it is diffed against.
|
||||||
|
fetch-depth: 2
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
# Parsing needs the dependency headers, not a build of this PR's deps/, so
|
||||||
|
# a PR that changes deps/ falls back to the newest cache main has.
|
||||||
|
- name: Restore cached deps
|
||||||
|
uses: actions/cache/restore@v6
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
|
||||||
|
key: linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}
|
||||||
|
restore-keys: linux-clang-cache-orcaslicer_deps-build-
|
||||||
|
fail-on-cache-miss: true
|
||||||
|
|
||||||
|
- name: Apt-Install Dependencies
|
||||||
|
uses: ./.github/actions/apt-install-deps
|
||||||
|
|
||||||
|
# The status says whether findings remain that --fix cannot fix.
|
||||||
|
- name: Fix the changed lines
|
||||||
|
run: |
|
||||||
|
mkdir -p "$RUNNER_TEMP/clang-tidy-fix"
|
||||||
|
status=0
|
||||||
|
scripts/run_clang_tidy.sh --base HEAD^1 --yes --fix || status=$?
|
||||||
|
echo "$status" > "$RUNNER_TEMP/clang-tidy-fix/status"
|
||||||
|
git -c core.quotePath=false diff --no-color --no-ext-diff --src-prefix=a/ --dst-prefix=b/ \
|
||||||
|
> "$RUNNER_TEMP/clang-tidy-fix/fix.patch"
|
||||||
|
|
||||||
|
- name: Upload the fix
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: clang-tidy-fix
|
||||||
|
path: ${{ runner.temp }}/clang-tidy-fix/
|
||||||
|
retention-days: 7
|
||||||
|
# A later re-run of the same run replaces the earlier fix.
|
||||||
|
overwrite: true
|
||||||
@@ -0,0 +1,476 @@
|
|||||||
|
name: PR clang-tidy Bot
|
||||||
|
|
||||||
|
# Helps a pull request past the clang-tidy check.
|
||||||
|
#
|
||||||
|
# When the check fails on a PR's changes, a comment asks the author to fix it,
|
||||||
|
# either with scripts/run_clang_tidy.sh --fix or by commenting:
|
||||||
|
# /bot fix-tidy
|
||||||
|
#
|
||||||
|
# That command, from the PR's author or an account with write access, re-runs
|
||||||
|
# the PR's clang-tidy fix run (clang_tidy_fix.yml), which makes the fix in the
|
||||||
|
# PR's own context. When it finishes, the fix is posted as one review: each
|
||||||
|
# include whose place falls on a line the PR's diff shows is a suggestion GitHub
|
||||||
|
# can commit in one click, and the review's body holds the whole patch with a
|
||||||
|
# git apply command. Findings --fix cannot fix, such as a using in a header,
|
||||||
|
# are left for the check to report.
|
||||||
|
#
|
||||||
|
# No code from the PR runs here. Nothing is pushed, and the patch is only
|
||||||
|
# posted when every line it adds is an #include in a C++ file the PR changes.
|
||||||
|
#
|
||||||
|
# workflow_run matches the workflows by their name: keep "clang-tidy" and
|
||||||
|
# "clang-tidy fix" in step with clang_tidy.yml and clang_tidy_fix.yml, and
|
||||||
|
# TIDY_STEP below with the clang-tidy step's name in clang_tidy.yml.
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types:
|
||||||
|
- created
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- clang-tidy
|
||||||
|
- clang-tidy fix
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
request:
|
||||||
|
# Skips the job unless a PR comment mentions the command.
|
||||||
|
if: >-
|
||||||
|
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||||
|
&& github.event_name == 'issue_comment'
|
||||||
|
&& github.event.issue.pull_request != null
|
||||||
|
&& contains(github.event.comment.body, '/bot fix-tidy')
|
||||||
|
permissions:
|
||||||
|
actions: write # re-run the fix run
|
||||||
|
pull-requests: write # react to and answer the comment, which is the PR's
|
||||||
|
issues: write # feedback comment + reactions
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Re-run the PR's clang-tidy fix
|
||||||
|
uses: actions/github-script@v9
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const MARKER = '<!-- pr-tidy-bot -->';
|
||||||
|
const FIX_WORKFLOW = 'clang_tidy_fix.yml';
|
||||||
|
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const issue = context.payload.issue;
|
||||||
|
const comment = context.payload.comment;
|
||||||
|
|
||||||
|
// Ignores a comment whose sender is not its author.
|
||||||
|
if (context.payload.sender.login !== comment.user.login || comment.user.type !== 'User') {
|
||||||
|
core.info('Ignoring a comment not written by its sender, or by a bot.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandLine = (comment.body || '')
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.find((line) => /^\/bot\s+fix-tidy\b/i.test(line));
|
||||||
|
if (!commandLine) {
|
||||||
|
core.info('No /bot fix-tidy command found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commenter = comment.user.login;
|
||||||
|
// Answers only the PR's author and maintainers, so the command cannot be used to spam.
|
||||||
|
if (commenter !== issue.user.login) {
|
||||||
|
const { data: access } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: commenter });
|
||||||
|
if (!['admin', 'write'].includes(access.permission)) {
|
||||||
|
core.info(`Ignoring /bot fix-tidy from @${commenter}: not the PR's author and no write access.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const react = (content) =>
|
||||||
|
github.rest.reactions.createForIssueComment({ owner, repo, comment_id: comment.id, content });
|
||||||
|
async function refuse(reason) {
|
||||||
|
await react('-1');
|
||||||
|
await github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: `${MARKER}\n@${commenter} ${reason}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
await react('eyes');
|
||||||
|
|
||||||
|
if (!/^\/bot\s+fix-tidy$/i.test(commandLine)) {
|
||||||
|
return refuse('`/bot fix-tidy` takes no arguments.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: issue.number });
|
||||||
|
if (pr.state !== 'open') {
|
||||||
|
return refuse('the PR is not open.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sha = pr.head.sha.slice(0, 10);
|
||||||
|
const { data: { workflow_runs: runs } } = await github.rest.actions.listWorkflowRuns({
|
||||||
|
owner, repo, workflow_id: FIX_WORKFLOW, event: 'pull_request', head_sha: pr.head.sha, per_page: 1
|
||||||
|
});
|
||||||
|
const run = runs[0];
|
||||||
|
// A PR last pushed before clang_tidy_fix.yml existed has no run to re-run.
|
||||||
|
if (!run) {
|
||||||
|
return refuse(`there is no clang-tidy fix run for ${sha}, the PR's latest commit. If you just pushed it, ` +
|
||||||
|
'try again in a minute. Otherwise, push a commit or merge the base branch to start one, then comment `/bot fix-tidy` again.');
|
||||||
|
}
|
||||||
|
if (run.status !== 'completed') {
|
||||||
|
// The first attempt is the one every push starts, and it makes no fix.
|
||||||
|
return refuse(run.run_attempt > 1
|
||||||
|
? `a clang-tidy fix for ${sha} is already [running](${run.html_url}). I'll post it when it finishes.`
|
||||||
|
: `the PR's checks on ${sha} are still starting. Comment \`/bot fix-tidy\` again in a minute.`);
|
||||||
|
}
|
||||||
|
// GitHub refuses to re-run a run created over 30 days ago.
|
||||||
|
try {
|
||||||
|
await github.rest.actions.reRunWorkflow({ owner, repo, run_id: run.id });
|
||||||
|
} catch (error) {
|
||||||
|
core.warning(`Re-running ${run.html_url} failed: ${error.message}`);
|
||||||
|
return refuse(`GitHub won't re-run the [clang-tidy fix run](${run.html_url}) for ${sha} (${error.message}). ` +
|
||||||
|
'Push a commit or merge the base branch to start a new one, then comment `/bot fix-tidy` again.');
|
||||||
|
}
|
||||||
|
core.info(`Re-ran ${run.html_url}`);
|
||||||
|
|
||||||
|
suggest:
|
||||||
|
# The fix run's re-runs only. Its first attempt makes no fix.
|
||||||
|
if: >-
|
||||||
|
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||||
|
&& github.event_name == 'workflow_run'
|
||||||
|
&& github.event.workflow_run.name == 'clang-tidy fix'
|
||||||
|
&& github.event.workflow_run.event == 'pull_request'
|
||||||
|
&& github.event.workflow_run.run_attempt > 1
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-suggest-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
permissions:
|
||||||
|
actions: read # download the fix
|
||||||
|
contents: read # check out the PR head
|
||||||
|
pull-requests: write # the review
|
||||||
|
issues: write # feedback comment + reactions
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- name: Find the PR and who asked
|
||||||
|
id: find
|
||||||
|
uses: actions/github-script@v9
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const MARKER = '<!-- pr-tidy-bot -->';
|
||||||
|
const CPP = /^(src|tests)\/.+\.(cpp|cc|cxx|hpp|h|hxx)$/;
|
||||||
|
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const run = context.payload.workflow_run;
|
||||||
|
|
||||||
|
// workflow_run lists no PR for a fork's branch, so the PR is found by its branch.
|
||||||
|
const { data: prs } = await github.rest.pulls.list({
|
||||||
|
owner, repo, state: 'open', head: `${run.head_repository.owner.login}:${run.head_branch}`
|
||||||
|
});
|
||||||
|
const pr = prs[0];
|
||||||
|
if (!pr) {
|
||||||
|
core.info('No open PR for this run.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The latest /bot fix-tidy comment is the one this run answers.
|
||||||
|
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number, per_page: 100 });
|
||||||
|
const request = comments.reverse().find((c) => c.user.type === 'User' && /^\s*\/bot\s+fix-tidy\s*$/im.test(c.body || ''));
|
||||||
|
const mention = request ? `@${request.user.login} ` : '';
|
||||||
|
async function refuse(reason) {
|
||||||
|
if (request) {
|
||||||
|
await github.rest.reactions.createForIssueComment({ owner, repo, comment_id: request.id, content: '-1' });
|
||||||
|
}
|
||||||
|
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: `${MARKER}\n${mention}${reason}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pr.head.sha !== run.head_sha) {
|
||||||
|
return refuse('the PR changed while I was making its clang-tidy fix. Comment `/bot fix-tidy` again.');
|
||||||
|
}
|
||||||
|
if (run.conclusion !== 'success') {
|
||||||
|
return refuse(`I couldn't make the clang-tidy fix. See the [run log](${run.html_url}).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The right-side lines of each changed C++ file's diff: the lines a
|
||||||
|
// review comment can be placed on. A file too large for GitHub to
|
||||||
|
// show its diff has none, and gets no suggestions.
|
||||||
|
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: pr.number, per_page: 100 });
|
||||||
|
const ranges = {};
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.status === 'removed' || !CPP.test(file.filename)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ranges[file.filename] = [...(file.patch || '').matchAll(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/gm)]
|
||||||
|
.map((m) => [Number(m[1]), Number(m[1]) + Number(m[2] ?? 1) - 1])
|
||||||
|
.filter(([first, last]) => last >= first);
|
||||||
|
}
|
||||||
|
|
||||||
|
core.setOutput('pr', pr.number);
|
||||||
|
core.setOutput('head-sha', pr.head.sha);
|
||||||
|
core.setOutput('head-repo', run.head_repository.full_name);
|
||||||
|
core.setOutput('ranges', JSON.stringify(ranges));
|
||||||
|
core.setOutput('mention', mention);
|
||||||
|
core.setOutput('request-id', request ? request.id : '');
|
||||||
|
|
||||||
|
- name: Download the fix
|
||||||
|
if: steps.find.outputs.pr != ''
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: clang-tidy-fix
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
path: ${{ runner.temp }}/clang-tidy-fix
|
||||||
|
|
||||||
|
# The PR's own files, only to apply the patch to. Nothing in them is run.
|
||||||
|
- name: Checkout the PR head
|
||||||
|
if: steps.find.outputs.pr != ''
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
repository: ${{ steps.find.outputs.head-repo }}
|
||||||
|
ref: ${{ steps.find.outputs.head-sha }}
|
||||||
|
path: head
|
||||||
|
fetch-depth: 1
|
||||||
|
lfs: 'false'
|
||||||
|
persist-credentials: false
|
||||||
|
sparse-checkout: |
|
||||||
|
src
|
||||||
|
tests
|
||||||
|
|
||||||
|
# The fix was made on the PR's merge commit, so it is applied to the head
|
||||||
|
# and diffed again, giving line numbers in the PR's own files.
|
||||||
|
- name: Check the fix and place its suggestions
|
||||||
|
id: place
|
||||||
|
if: steps.find.outputs.pr != ''
|
||||||
|
env:
|
||||||
|
RANGES: ${{ steps.find.outputs.ranges }}
|
||||||
|
run: |
|
||||||
|
python3 -I - "$RUNNER_TEMP/clang-tidy-fix/fix.patch" head "$RUNNER_TEMP" <<'EOF'
|
||||||
|
import json, os, re, subprocess, sys
|
||||||
|
|
||||||
|
patch_path, head, out = sys.argv[1:]
|
||||||
|
ranges = json.loads(os.environ["RANGES"])
|
||||||
|
|
||||||
|
def output(**values):
|
||||||
|
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
|
||||||
|
for key, value in values.items():
|
||||||
|
f.write(f"{key}={value}\n")
|
||||||
|
|
||||||
|
patch = open(patch_path, encoding="utf-8").read()
|
||||||
|
if not patch.strip():
|
||||||
|
output(result="empty")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
# Every line the patch adds must be an #include or blank, in a C++ file the
|
||||||
|
# PR changes, and it may not remove, create, rename or change the mode of
|
||||||
|
# anything.
|
||||||
|
include = re.compile(r'\s*#\s*include\s*(<[\w./+-]+>|"[\w./+-]+")\s*')
|
||||||
|
header = re.compile(r"(index [0-9a-f]+\.\.[0-9a-f]+( \d+)?|@@ -\d+(,\d+)? \+\d+(,\d+)? @@.*)")
|
||||||
|
problems = []
|
||||||
|
path = None
|
||||||
|
for line in patch.splitlines():
|
||||||
|
if line.startswith("diff --git "):
|
||||||
|
match = re.fullmatch(r"diff --git a/(.+) b/\1", line)
|
||||||
|
path = match.group(1) if match else None
|
||||||
|
if path not in ranges:
|
||||||
|
problems.append(f"{line}: not a C++ file the PR changes")
|
||||||
|
# git appends a tab to the --- and +++ lines of a path that contains a space.
|
||||||
|
elif line.removesuffix("\t") in (f"--- a/{path}", f"+++ b/{path}") or header.fullmatch(line):
|
||||||
|
pass
|
||||||
|
elif line.startswith(" ") or line == "\\ No newline at end of file":
|
||||||
|
pass
|
||||||
|
elif line.startswith("+") and (include.fullmatch(line[1:]) or not line[1:].strip()):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
problems.append(f"{path}: {line}")
|
||||||
|
if problems:
|
||||||
|
for problem in problems:
|
||||||
|
print(f"::error::Refused patch line {problem}")
|
||||||
|
output(result="rejected")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
if subprocess.run(["git", "-C", head, "apply", "--index", os.path.abspath(patch_path)]).returncode != 0:
|
||||||
|
output(result="apply-failed")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
git = ["git", "-C", head, "-c", "core.quotePath=false", "diff", "--cached", "--no-color", "--no-ext-diff",
|
||||||
|
"--src-prefix=a/", "--dst-prefix=b/"]
|
||||||
|
with open(os.path.join(out, "head.patch"), "w", encoding="utf-8") as f:
|
||||||
|
f.write(subprocess.run(git, check=True, capture_output=True, encoding="utf-8").stdout)
|
||||||
|
|
||||||
|
# Each run of added lines becomes one suggestion on the line it goes after
|
||||||
|
# (before, at the top of a file): that line, replaced by itself and the
|
||||||
|
# includes. It can only sit on a line the PR's diff shows.
|
||||||
|
hunks = []
|
||||||
|
path = None
|
||||||
|
for line in subprocess.run(git + ["-U0"], check=True, capture_output=True, encoding="utf-8").stdout.splitlines():
|
||||||
|
if line.startswith("diff --git "):
|
||||||
|
path = re.fullmatch(r"diff --git a/(.+) b/\1", line).group(1)
|
||||||
|
elif line.startswith("@@ "):
|
||||||
|
hunks.append((path, int(re.match(r"@@ -(\d+)", line).group(1)), []))
|
||||||
|
elif hunks and line.startswith("+") and not line.startswith("+++ "):
|
||||||
|
hunks[-1][2].append(line[1:])
|
||||||
|
|
||||||
|
suggestions = []
|
||||||
|
total = suggested = 0
|
||||||
|
for path, after, added in hunks:
|
||||||
|
count = sum(bool(include.fullmatch(line)) for line in added)
|
||||||
|
total += count
|
||||||
|
original = subprocess.run(["git", "-C", head, "show", f"HEAD:{path}"], check=True, capture_output=True,
|
||||||
|
encoding="utf-8").stdout.splitlines()
|
||||||
|
line = max(after, 1)
|
||||||
|
if line > len(original) or not any(first <= line <= last for first, last in ranges[path]):
|
||||||
|
continue
|
||||||
|
body = "\n".join([original[line - 1], *added] if after else [*added, original[0]])
|
||||||
|
# A fence longer than any run of backticks in the line it repeats.
|
||||||
|
fence = "`" * max([3] + [len(run) + 1 for run in re.findall(r"`+", body)])
|
||||||
|
suggestions.append({"path": path, "line": line, "side": "RIGHT",
|
||||||
|
"body": f"{fence}suggestion\n{body}\n{fence}"})
|
||||||
|
suggested += count
|
||||||
|
with open(os.path.join(out, "suggestions.json"), "w", encoding="utf-8") as f:
|
||||||
|
json.dump(suggestions, f)
|
||||||
|
output(result="ok", includes=total, suggested=suggested)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Post the fix
|
||||||
|
if: always() && steps.find.outputs.pr != ''
|
||||||
|
uses: actions/github-script@v9
|
||||||
|
env:
|
||||||
|
PR: ${{ steps.find.outputs.pr }}
|
||||||
|
MENTION: ${{ steps.find.outputs.mention }}
|
||||||
|
REQUEST_ID: ${{ steps.find.outputs.request-id }}
|
||||||
|
HEAD_SHA: ${{ steps.find.outputs.head-sha }}
|
||||||
|
RESULT: ${{ steps.place.outputs.result }}
|
||||||
|
INCLUDES: ${{ steps.place.outputs.includes }}
|
||||||
|
SUGGESTED: ${{ steps.place.outputs.suggested }}
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const MARKER = '<!-- pr-tidy-bot -->';
|
||||||
|
// GitHub's limit on a review body is 65536 characters.
|
||||||
|
const MAX_PATCH = 60000;
|
||||||
|
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const { PR, MENTION, REQUEST_ID, HEAD_SHA, RESULT, INCLUDES, SUGGESTED, RUNNER_TEMP } = process.env;
|
||||||
|
const issue_number = Number(PR);
|
||||||
|
const sha = HEAD_SHA.slice(0, 10);
|
||||||
|
const fixUrl = context.payload.workflow_run.html_url;
|
||||||
|
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
|
||||||
|
|
||||||
|
const react = (content) => REQUEST_ID
|
||||||
|
? github.rest.reactions.createForIssueComment({ owner, repo, comment_id: Number(REQUEST_ID), content })
|
||||||
|
: Promise.resolve();
|
||||||
|
async function reply(body, content) {
|
||||||
|
await react(content);
|
||||||
|
await github.rest.issues.createComment({ owner, repo, issue_number, body: `${MARKER}\n${MENTION}${body}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const read = (...parts) => fs.readFileSync(path.join(RUNNER_TEMP, ...parts), 'utf8');
|
||||||
|
const passes = RESULT && read('clang-tidy-fix', 'status').trim() === '0';
|
||||||
|
|
||||||
|
if (RESULT === 'empty') {
|
||||||
|
return passes
|
||||||
|
? reply(`clang-tidy passes on ${sha}. There is nothing to fix.`, '+1')
|
||||||
|
: reply(`clang-tidy has no automatic fix for what it reports on ${sha}. The findings, in the [fix run's log](${fixUrl}), need a manual edit.`, '-1');
|
||||||
|
}
|
||||||
|
if (RESULT === 'rejected') {
|
||||||
|
return reply(`the clang-tidy fix changes more than includes, so I didn't post it. See the [run log](${runUrl}).`, '-1');
|
||||||
|
}
|
||||||
|
if (RESULT === 'apply-failed') {
|
||||||
|
return reply(`the clang-tidy fix doesn't apply to ${sha}. Merge the base branch into the PR, then comment \`/bot fix-tidy\` again.`, '-1');
|
||||||
|
}
|
||||||
|
if (RESULT !== 'ok') {
|
||||||
|
return reply(`something went wrong. See the [run log](${runUrl}).`, '-1');
|
||||||
|
}
|
||||||
|
|
||||||
|
const patch = read('head.patch');
|
||||||
|
const suggestions = JSON.parse(read('suggestions.json'));
|
||||||
|
|
||||||
|
const one = INCLUDES === '1';
|
||||||
|
const includes = one ? '1 missing include' : `${INCLUDES} missing includes`;
|
||||||
|
const inline = SUGGESTED === '0'
|
||||||
|
? (one ? 'It goes on a line this PR does not change, so I can\'t suggest it inline.'
|
||||||
|
: 'They go on lines this PR does not change, so I can\'t suggest them inline.')
|
||||||
|
: SUGGESTED === INCLUDES
|
||||||
|
? (one ? 'It is suggested inline below.' : 'Each is suggested inline below.')
|
||||||
|
: `${SUGGESTED} of them are suggested inline below; the rest go on lines this PR does not change.`;
|
||||||
|
const remaining = passes
|
||||||
|
? `With ${one ? 'it' : 'them'}, clang-tidy passes.`
|
||||||
|
: `Some findings remain that need a manual edit; see the [fix run's log](${fixUrl}).`;
|
||||||
|
// A fence longer than any run of backticks in the patch's context lines.
|
||||||
|
const fence = '`'.repeat(Math.max(3, ...[...patch.matchAll(/`+/g)].map((m) => m[0].length + 1)));
|
||||||
|
const apply = patch.length > MAX_PATCH
|
||||||
|
? `The full patch is too large to show here. Download \`fix.patch\` from the [fix run](${fixUrl}) and run \`git apply fix.patch\`.`
|
||||||
|
: `To apply ${one ? 'it' : 'all of them'}, run this in your checkout of the PR branch:\n\n${fence}sh\ngit apply <<'EOF'\n${patch}EOF\n${fence}`;
|
||||||
|
const body = (withSuggestions) => `${MARKER}\n${MENTION}clang-tidy's fix for ${sha} adds ${includes}. ` +
|
||||||
|
`${withSuggestions ? `${inline} ` : ''}${remaining}\n\n${apply}`;
|
||||||
|
|
||||||
|
const review = { owner, repo, pull_number: issue_number, commit_id: HEAD_SHA, event: 'COMMENT' };
|
||||||
|
try {
|
||||||
|
await github.rest.pulls.createReview({ ...review, body: body(true), comments: suggestions });
|
||||||
|
} catch (error) {
|
||||||
|
// A suggestion GitHub will not place still leaves the patch.
|
||||||
|
core.warning(`Posting the suggestions failed (${error.message}); posting the patch alone.`);
|
||||||
|
await github.rest.pulls.createReview({ ...review, body: body(false) });
|
||||||
|
}
|
||||||
|
await react('+1');
|
||||||
|
|
||||||
|
nudge:
|
||||||
|
if: >-
|
||||||
|
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||||
|
&& github.event_name == 'workflow_run'
|
||||||
|
&& github.event.workflow_run.name == 'clang-tidy'
|
||||||
|
&& github.event.workflow_run.event == 'pull_request'
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-nudge-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
permissions:
|
||||||
|
actions: read # the check's steps
|
||||||
|
pull-requests: write # comment on the PR
|
||||||
|
issues: write # the comment
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Ask for a fix when the check fails
|
||||||
|
uses: actions/github-script@v9
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const MARKER = '<!-- pr-tidy-bot-nudge -->';
|
||||||
|
// The clang-tidy step's name in clang_tidy.yml. A failure in any other
|
||||||
|
// step is the CI's, not the PR's.
|
||||||
|
const TIDY_STEP = 'Run clang-tidy on the changed lines';
|
||||||
|
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const run = context.payload.workflow_run;
|
||||||
|
|
||||||
|
// workflow_run lists no PR for a fork's branch, so the PR is found by its branch.
|
||||||
|
const { data: prs } = await github.rest.pulls.list({
|
||||||
|
owner, repo, state: 'open', head: `${run.head_repository.owner.login}:${run.head_branch}`
|
||||||
|
});
|
||||||
|
const pr = prs.find((p) => p.head.sha === run.head_sha);
|
||||||
|
if (!pr) {
|
||||||
|
core.info('No open PR at this run\'s commit; a newer run will answer.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: { jobs } } = await github.rest.actions.listJobsForWorkflowRun({ owner, repo, run_id: run.id });
|
||||||
|
const failed = jobs.some((job) => (job.steps || []).some((step) => step.name === TIDY_STEP && step.conclusion === 'failure'));
|
||||||
|
|
||||||
|
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number, per_page: 100 });
|
||||||
|
const previous = comments.find((c) => c.user.type === 'Bot' && (c.body || '').startsWith(MARKER));
|
||||||
|
|
||||||
|
if (!failed) {
|
||||||
|
// Removes the request once the check passes.
|
||||||
|
if (previous && run.conclusion === 'success') {
|
||||||
|
await github.rest.issues.deleteComment({ owner, repo, comment_id: previous.id });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = `${MARKER}\n@${pr.user.login} the clang-tidy check found problems in this PR's changes ` +
|
||||||
|
`at ${run.head_sha.slice(0, 10)} ([log](${run.html_url})). Missing includes can be fixed either way:\n\n` +
|
||||||
|
'- run `scripts/run_clang_tidy.sh --fix` (`scripts\\run_clang_tidy.ps1 -Fix` on Windows) and push the result, or\n' +
|
||||||
|
'- comment `/bot fix-tidy`, and I\'ll suggest the fix here for you to apply.\n\n' +
|
||||||
|
'Other findings, such as a `using` in a header, need a manual edit.';
|
||||||
|
// Edits the earlier request instead of posting another, so each push does not notify again.
|
||||||
|
if (previous) {
|
||||||
|
await github.rest.issues.updateComment({ owner, repo, comment_id: previous.id, body });
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
|
||||||
|
}
|
||||||
@@ -1,18 +1,10 @@
|
|||||||
# ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style
|
# ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style
|
||||||
# depfile, so a cache hit writes the object and no depfile, and Ninja then records
|
# depfile, so a cache hit writes the object and no depfile, and Ninja then records
|
||||||
# no headers for that object. ccache reproduces /showIncludes output on a hit.
|
# no headers for that object. ccache reproduces /showIncludes output on a hit.
|
||||||
#
|
|
||||||
# Clang also records the modification time of every input in a precompiled header
|
|
||||||
# and rejects the header once one differs. ccache caches the compile that creates
|
|
||||||
# the header, so a tree that regenerates cmake_pch.cxx gets back a header stamped
|
|
||||||
# with the old file's time, and every compile that uses it fails. The build system
|
|
||||||
# already rebuilds the header when an input changes. This covers the dependencies,
|
|
||||||
# such as wxWidgets; PrecompiledHeader.cmake does the same for OrcaSlicer's targets.
|
|
||||||
foreach (_lang C CXX)
|
foreach (_lang C CXX)
|
||||||
if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND
|
if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND
|
||||||
CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
|
CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
|
||||||
set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes")
|
set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes")
|
||||||
set(CMAKE_${_lang}_DEPFILE_FORMAT msvc)
|
set(CMAKE_${_lang}_DEPFILE_FORMAT msvc)
|
||||||
list(APPEND CMAKE_${_lang}_COMPILE_OPTIONS_CREATE_PCH -Xclang -fno-pch-timestamp)
|
|
||||||
endif ()
|
endif ()
|
||||||
endforeach ()
|
endforeach ()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Bambulab",
|
"name": "Bambulab",
|
||||||
"url": "http://www.bambulab.com/Parameters/vendor/BBL.json",
|
"url": "http://www.bambulab.com/Parameters/vendor/BBL.json",
|
||||||
"version": "02.08.00.15",
|
"version": "02.08.00.14",
|
||||||
"force_update": "0",
|
"force_update": "0",
|
||||||
"description": "the initial version of BBL configurations",
|
"description": "the initial version of BBL configurations",
|
||||||
"machine_model_list": [
|
"machine_model_list": [
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+17
-43
@@ -1,27 +1,21 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Lets a pull request's macOS deps build, or its clang-tidy job, wait for a
|
# Lets a pull request's macOS deps build wait for a push build of the base
|
||||||
# build that saves the deps cache it needs, from a Linux job. `check` runs after
|
# branch that builds the same deps, from a Linux job. `check` runs after a cache
|
||||||
# a cache miss and names a queued or running push build of the base branch with
|
# miss and names a queued or running base build with the same deps/ and .github/
|
||||||
# the same deps/ and .github/ to wait for, unless the pull request changes either.
|
# to wait for, unless the pull request changes either. `wait` waits until that
|
||||||
# `own` names the pull request's own build instead, for clang-tidy when the pull
|
# build saves KEY. actions/cache then confirms the entry can be restored.
|
||||||
# request changes deps/. `wait` waits until that build saves KEY under CACHE_REF,
|
|
||||||
# or its deps job fails. actions/cache then confirms the entry can be restored.
|
|
||||||
#
|
#
|
||||||
# Environment: GH_TOKEN, REPO, WORKFLOW_REF (or WORKFLOW, the build workflow's
|
# Environment: GH_TOKEN, REPO, WORKFLOW_REF and BASE_REF, plus KEY_INPUTS,
|
||||||
# file name) and BASE_REF, plus HEAD_SHA for `own`, and KEY_INPUTS, WAIT_FOR,
|
# WAIT_FOR, WAIT_SINCE, WAIT_MINUTES, KEY and ARCH for `wait`.
|
||||||
# WAIT_SINCE, WAIT_MINUTES, KEY, CACHE_REF (default the base branch) and ARCH
|
|
||||||
# (or JOB, the matrix job's name) for `wait`.
|
|
||||||
# Writes key-inputs, wait-for and wait-since to $GITHUB_OUTPUT.
|
# Writes key-inputs, wait-for and wait-since to $GITHUB_OUTPUT.
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
workflow=${WORKFLOW_REF:-}
|
workflow=${WORKFLOW_REF%%@*}
|
||||||
workflow=${workflow%%@*}
|
workflow=${workflow##*/}
|
||||||
workflow=${WORKFLOW:-${workflow##*/}}
|
|
||||||
|
|
||||||
# Must match the job names in build_all.yml, build_check_cache.yml and build_deps.yml.
|
# Must match the job names in build_all.yml, build_check_cache.yml and build_deps.yml.
|
||||||
job=${JOB:-build_macos_arch (${ARCH:-})}
|
check_job_name="build_macos_arch (${ARCH:-}) / Check Cache"
|
||||||
check_job_name="$job / Check Cache"
|
deps_job_name="build_macos_arch (${ARCH:-}) / Build Deps / Build Deps"
|
||||||
deps_job_name="$job / Build Deps / Build Deps"
|
|
||||||
|
|
||||||
out() { echo "$1" >> "${GITHUB_OUTPUT:-/dev/stdout}"; }
|
out() { echo "$1" >> "${GITHUB_OUTPUT:-/dev/stdout}"; }
|
||||||
|
|
||||||
@@ -69,27 +63,8 @@ check() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
own() {
|
|
||||||
out "key-inputs=$(local_key_inputs HEAD)"
|
|
||||||
local id
|
|
||||||
# The build can be created a moment after this job.
|
|
||||||
for _ in 1 2 3 4 5; do
|
|
||||||
id=$(gh api -X GET "repos/$REPO/actions/workflows/$workflow/runs" \
|
|
||||||
-f head_sha="$HEAD_SHA" -f event=pull_request -f per_page=1 --jq '.workflow_runs[0].id // empty' < /dev/null)
|
|
||||||
[ -n "$id" ] && break
|
|
||||||
sleep 30
|
|
||||||
done
|
|
||||||
if [ -n "$id" ]; then
|
|
||||||
echo "Waiting for the deps cache of build $id of this pull request."
|
|
||||||
out "wait-for=$id"
|
|
||||||
out "wait-since=$(date +%s)"
|
|
||||||
else
|
|
||||||
echo "No build of this pull request is building its deps."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
key_saved() {
|
key_saved() {
|
||||||
gh cache list -R "$REPO" --ref "${CACHE_REF:-refs/heads/$BASE_REF}" --key "$KEY" --json key < /dev/null \
|
gh cache list -R "$REPO" --ref "refs/heads/$BASE_REF" --key "$KEY" --json key < /dev/null \
|
||||||
| jq -e --arg key "$KEY" 'any(.[]; .key == $key)' > /dev/null
|
| jq -e --arg key "$KEY" 'any(.[]; .key == $key)' > /dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,11 +87,11 @@ wait_for() {
|
|||||||
key_saved && { echo "Found $KEY."; return; }
|
key_saved && { echo "Found $KEY."; return; }
|
||||||
sleep 20
|
sleep 20
|
||||||
done
|
done
|
||||||
echo "Build $id finished its deps without saving $KEY."
|
echo "Build $id of $BASE_REF finished its deps without saving $KEY."
|
||||||
return ;;
|
return ;;
|
||||||
"completed cancelled") ;;
|
"completed cancelled") ;;
|
||||||
completed*)
|
completed*)
|
||||||
echo "The deps job of build $id did not succeed."
|
echo "The deps job of build $id of $BASE_REF did not succeed."
|
||||||
return ;;
|
return ;;
|
||||||
# The deps job exists only once the build's cache check has succeeded.
|
# The deps job exists only once the build's cache check has succeeded.
|
||||||
"")
|
"")
|
||||||
@@ -125,7 +100,7 @@ wait_for() {
|
|||||||
completed*)
|
completed*)
|
||||||
check=$(jq -r --arg name "$check_job_name" 'first(.jobs[] | select(.name == $name) | .conclusion) // ""' <<< "$jobs")
|
check=$(jq -r --arg name "$check_job_name" 'first(.jobs[] | select(.name == $name) | .conclusion) // ""' <<< "$jobs")
|
||||||
if [ -z "$warned" ] && { [ -z "$check" ] || [ "$check" = success ]; }; then
|
if [ -z "$warned" ] && { [ -z "$check" ] || [ "$check" = success ]; }; then
|
||||||
echo "::warning title=Deps wait::Build $id has no job named \"$deps_job_name\". If the build jobs were renamed, update scripts/ci_deps_cache.sh. This does not affect this pull request."
|
echo "::warning title=Deps wait::Build $id of $BASE_REF has no job named \"$deps_job_name\". If the build jobs were renamed, update scripts/ci_deps_cache.sh. This does not affect this pull request."
|
||||||
warned=1
|
warned=1
|
||||||
fi ;;
|
fi ;;
|
||||||
*)
|
*)
|
||||||
@@ -143,12 +118,11 @@ wait_for() {
|
|||||||
seen="$seen $id"
|
seen="$seen $id"
|
||||||
echo "Following build $id of $BASE_REF."
|
echo "Following build $id of $BASE_REF."
|
||||||
done
|
done
|
||||||
echo "No build saved $KEY."
|
echo "No build of $BASE_REF saved $KEY."
|
||||||
}
|
}
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
check) check ;;
|
check) check ;;
|
||||||
own) own ;;
|
|
||||||
wait) wait_for ;;
|
wait) wait_for ;;
|
||||||
*) echo "usage: $0 check|own|wait" >&2; exit 2 ;;
|
*) echo "usage: $0 check|wait" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
#include "libslic3r/BrimEarsPoint.hpp"
|
#include "libslic3r/BrimEarsPoint.hpp"
|
||||||
#include "libslic3r/Config.hpp"
|
#include "libslic3r/Config.hpp"
|
||||||
#include "libslic3r/CustomGCode.hpp"
|
#include "libslic3r/CustomGCode.hpp"
|
||||||
#include "libslic3r/TextureDisplacement.hpp"
|
|
||||||
#include "libslic3r_version.h"
|
#include "libslic3r_version.h"
|
||||||
|
|
||||||
#include "bbs_3mf.hpp"
|
#include "bbs_3mf.hpp"
|
||||||
@@ -355,13 +354,6 @@ static constexpr const char* CUSTOM_SUPPORTS_ATTR = "paint_supports";
|
|||||||
static constexpr const char* CUSTOM_FUZZY_SKIN_ATTR = "paint_fuzzy_skin";
|
static constexpr const char* CUSTOM_FUZZY_SKIN_ATTR = "paint_fuzzy_skin";
|
||||||
static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam";
|
static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam";
|
||||||
static constexpr const char* MMU_SEGMENTATION_ATTR = "paint_color";
|
static constexpr const char* MMU_SEGMENTATION_ATTR = "paint_color";
|
||||||
// Texture displacement. One paint mask per layer slot, mirroring paint_color; the layer stack itself is
|
|
||||||
// a JSON file in the archive, named by the volume metadata key below (see add_texture_displacement()).
|
|
||||||
static constexpr const char* TEXTURE_DISPLACEMENT_ATTRS[Slic3r::TEXTURE_DISPLACEMENT_MAX_LAYERS] = {
|
|
||||||
"paint_texture_0", "paint_texture_1", "paint_texture_2", "paint_texture_3",
|
|
||||||
"paint_texture_4", "paint_texture_5", "paint_texture_6", "paint_texture_7" };
|
|
||||||
static constexpr const char* TEXTURE_DISPLACEMENT_KEY = "texture_displacement";
|
|
||||||
static constexpr const char* TEXTURE_DISPLACEMENT_DIR = "Metadata/texture_displacement/";
|
|
||||||
// BBS
|
// BBS
|
||||||
static constexpr const char* FACE_PROPERTY_ATTR = "face_property";
|
static constexpr const char* FACE_PROPERTY_ATTR = "face_property";
|
||||||
|
|
||||||
@@ -855,9 +847,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
std::vector<std::string> custom_seam;
|
std::vector<std::string> custom_seam;
|
||||||
std::vector<std::string> mmu_segmentation;
|
std::vector<std::string> mmu_segmentation;
|
||||||
std::vector<std::string> fuzzy_skin;
|
std::vector<std::string> fuzzy_skin;
|
||||||
// One per texture displacement layer slot, indexed like `triangles` but stored only as far as the
|
|
||||||
// slot's last painted triangle: empty when the slot has no paint, which is nearly every file.
|
|
||||||
std::vector<std::string> texture_displacement[TEXTURE_DISPLACEMENT_MAX_LAYERS];
|
|
||||||
// BBS
|
// BBS
|
||||||
std::vector<std::string> face_properties;
|
std::vector<std::string> face_properties;
|
||||||
|
|
||||||
@@ -878,8 +867,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
custom_seam.clear();
|
custom_seam.clear();
|
||||||
mmu_segmentation.clear();
|
mmu_segmentation.clear();
|
||||||
fuzzy_skin.clear();
|
fuzzy_skin.clear();
|
||||||
for (std::vector<std::string> &slot : texture_displacement)
|
|
||||||
slot.clear();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1213,11 +1200,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
/*IdToSlaSupportPointsMap m_sla_support_points;
|
/*IdToSlaSupportPointsMap m_sla_support_points;
|
||||||
IdToSlaDrainHolesMap m_sla_drain_holes;*/
|
IdToSlaDrainHolesMap m_sla_drain_holes;*/
|
||||||
PathToEmbossShapeFileMap m_path_to_emboss_shape_files;
|
PathToEmbossShapeFileMap m_path_to_emboss_shape_files;
|
||||||
// Texture displacement: every file under Metadata/texture_displacement/, by archive path. Volumes
|
|
||||||
// are built only after the whole archive has been walked, so by the time a volume names its JSON
|
|
||||||
// every file it could refer to is already in here, whatever order the archive happened to be in.
|
|
||||||
std::map<std::string, std::string> m_texture_displacement_files;
|
|
||||||
void _apply_texture_displacement(ModelVolume &volume, const std::string &json_path);
|
|
||||||
std::string m_curr_metadata_name;
|
std::string m_curr_metadata_name;
|
||||||
std::string m_curr_characters;
|
std::string m_curr_characters;
|
||||||
std::string m_name;
|
std::string m_name;
|
||||||
@@ -2091,13 +2073,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
else if (_is_svg_shape_file(name)) {
|
else if (_is_svg_shape_file(name)) {
|
||||||
_extract_embossed_svg_shape_file(name, archive, stat);
|
_extract_embossed_svg_shape_file(name, archive, stat);
|
||||||
}
|
}
|
||||||
else if (boost::algorithm::istarts_with(name, TEXTURE_DISPLACEMENT_DIR)) {
|
|
||||||
std::string contents(stat.m_uncomp_size, '\0');
|
|
||||||
if (mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, contents.data(), stat.m_uncomp_size, 0))
|
|
||||||
m_texture_displacement_files.emplace(name, std::move(contents));
|
|
||||||
else
|
|
||||||
add_error("Error while reading texture displacement data");
|
|
||||||
}
|
|
||||||
else if (!dont_load_config && boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) {
|
else if (!dont_load_config && boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) {
|
||||||
m_parsing_slice_info = true;
|
m_parsing_slice_info = true;
|
||||||
//extract slice info from archive
|
//extract slice info from archive
|
||||||
@@ -3335,45 +3310,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
// Restores one volume's texture displacement stack from the JSON it named. Called while the volume is
|
|
||||||
// being built, which is after the whole archive has been walked - so every file it can refer to is
|
|
||||||
// already extracted, and no deferral is needed.
|
|
||||||
//
|
|
||||||
// A missing or malformed JSON leaves the volume with no layers but keeps the paint mask it already
|
|
||||||
// loaded, which is the same state as a project saved by a build without the feature: recoverable by
|
|
||||||
// picking the texture again rather than a hard failure.
|
|
||||||
void _BBS_3MF_Importer::_apply_texture_displacement(ModelVolume &volume, const std::string &json_path)
|
|
||||||
{
|
|
||||||
const auto json = m_texture_displacement_files.find(json_path);
|
|
||||||
if (json == m_texture_displacement_files.end()) {
|
|
||||||
add_error("Missing texture displacement data: " + json_path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
std::vector<TextureDisplacementLayer> layers;
|
|
||||||
TextureDisplacementOptions options;
|
|
||||||
if (!texture_displacement_layers_from_json(json->second, layers, options)) {
|
|
||||||
add_error("Malformed texture displacement data: " + json_path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// The archive is the only source of a layer's pixels. `path` comes from the project file, so loading
|
|
||||||
// the project must never read the file it names: that would let whoever made the project pick any
|
|
||||||
// local (or, on Windows, network) file to read, and the next save would embed it in the archive.
|
|
||||||
// `path` stays a label of where the texture came from; a layer the archive carries no image for
|
|
||||||
// loads without one, its settings intact, until the user picks a texture again.
|
|
||||||
for (TextureDisplacementLayer &layer : layers) {
|
|
||||||
if (layer.path_in_3mf.empty())
|
|
||||||
continue;
|
|
||||||
const auto image = m_texture_displacement_files.find(layer.path_in_3mf);
|
|
||||||
if (image == m_texture_displacement_files.end()) {
|
|
||||||
add_error("Missing texture displacement image: " + layer.path_in_3mf);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
layer.image_data = std::make_shared<std::vector<unsigned char>>(image->second.begin(), image->second.end());
|
|
||||||
}
|
|
||||||
volume.texture_displacement_layers = std::move(layers);
|
|
||||||
volume.texture_displacement_options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _BBS_3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat){
|
void _BBS_3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat){
|
||||||
assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end());
|
assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end());
|
||||||
auto file = std::make_unique<std::string>(stat.m_uncomp_size, '\0');
|
auto file = std::make_unique<std::string>(stat.m_uncomp_size, '\0');
|
||||||
@@ -4030,12 +3966,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
m_curr_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
|
m_curr_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
|
||||||
m_curr_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
|
m_curr_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
|
||||||
m_curr_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
|
m_curr_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
|
||||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
|
||||||
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
|
|
||||||
std::vector<std::string> &mask = m_curr_object->geometry.texture_displacement[slot];
|
|
||||||
mask.resize(m_curr_object->geometry.triangles.size());
|
|
||||||
mask.back() = paint;
|
|
||||||
}
|
|
||||||
// BBS
|
// BBS
|
||||||
m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
||||||
}
|
}
|
||||||
@@ -5387,18 +5317,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
volume->mmu_segmentation_facets.touch();
|
volume->mmu_segmentation_facets.touch();
|
||||||
volume->fuzzy_skin_facets.shrink_to_fit();
|
volume->fuzzy_skin_facets.shrink_to_fit();
|
||||||
volume->fuzzy_skin_facets.touch();
|
volume->fuzzy_skin_facets.touch();
|
||||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
|
||||||
const std::vector<std::string> &mask = sub_object->geometry.texture_displacement[slot];
|
|
||||||
if (mask.empty())
|
|
||||||
continue; // no triangle painted on this slot
|
|
||||||
FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
|
|
||||||
facets.reserve(triangles_count);
|
|
||||||
for (size_t i = 0; i < triangles_count && i < mask.size(); ++i)
|
|
||||||
if (!mask[i].empty())
|
|
||||||
facets.set_triangle_from_string(i, mask[i]);
|
|
||||||
facets.shrink_to_fit();
|
|
||||||
facets.touch();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
volume->set_type(volume_data->part_type);
|
volume->set_type(volume_data->part_type);
|
||||||
@@ -5414,8 +5332,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
for (const Metadata& metadata : volume_data->metadata) {
|
for (const Metadata& metadata : volume_data->metadata) {
|
||||||
if (metadata.key == NAME_KEY)
|
if (metadata.key == NAME_KEY)
|
||||||
volume->name = metadata.value;
|
volume->name = metadata.value;
|
||||||
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
|
|
||||||
_apply_texture_displacement(*volume, metadata.value);
|
|
||||||
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
|
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
|
||||||
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
||||||
//for old format
|
//for old format
|
||||||
@@ -5580,28 +5496,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
volume->seam_facets.shrink_to_fit();
|
volume->seam_facets.shrink_to_fit();
|
||||||
volume->mmu_segmentation_facets.shrink_to_fit();
|
volume->mmu_segmentation_facets.shrink_to_fit();
|
||||||
|
|
||||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
|
||||||
const std::vector<std::string> &mask = geometry.texture_displacement[slot];
|
|
||||||
if (mask.empty())
|
|
||||||
continue; // written by a build without the feature
|
|
||||||
FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
|
|
||||||
facets.reserve(triangles_count);
|
|
||||||
for (size_t i = 0; i < triangles_count; ++i) {
|
|
||||||
const size_t index = volume_data.first_triangle_id + i;
|
|
||||||
if (index < mask.size() && !mask[index].empty())
|
|
||||||
facets.set_triangle_from_string(i, mask[index]);
|
|
||||||
}
|
|
||||||
facets.shrink_to_fit();
|
|
||||||
facets.touch();
|
|
||||||
}
|
|
||||||
volume->set_type(volume_data.part_type);
|
volume->set_type(volume_data.part_type);
|
||||||
|
|
||||||
// apply the remaining volume's metadata
|
// apply the remaining volume's metadata
|
||||||
for (const Metadata& metadata : volume_data.metadata) {
|
for (const Metadata& metadata : volume_data.metadata) {
|
||||||
if (metadata.key == NAME_KEY)
|
if (metadata.key == NAME_KEY)
|
||||||
volume->name = metadata.value;
|
volume->name = metadata.value;
|
||||||
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
|
|
||||||
_apply_texture_displacement(*volume, metadata.value);
|
|
||||||
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
|
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
|
||||||
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
||||||
//for old format
|
//for old format
|
||||||
@@ -5905,12 +5805,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
current_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
|
current_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
|
||||||
current_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
|
current_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
|
||||||
current_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
|
current_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
|
||||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
|
||||||
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
|
|
||||||
std::vector<std::string> &mask = current_object->geometry.texture_displacement[slot];
|
|
||||||
mask.resize(current_object->geometry.triangles.size());
|
|
||||||
mask.back() = paint;
|
|
||||||
}
|
|
||||||
// BBS
|
// BBS
|
||||||
current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
||||||
}
|
}
|
||||||
@@ -6914,9 +6808,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
stream << " <Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n";
|
stream << " <Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n";
|
||||||
stream << " <Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\n";
|
stream << " <Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\n";
|
||||||
stream << " <Default Extension=\"png\" ContentType=\"image/png\"/>\n";
|
stream << " <Default Extension=\"png\" ContentType=\"image/png\"/>\n";
|
||||||
stream << " <Default Extension=\"jpg\" ContentType=\"image/jpeg\"/>\n";
|
|
||||||
stream << " <Default Extension=\"jpeg\" ContentType=\"image/jpeg\"/>\n";
|
|
||||||
stream << " <Default Extension=\"json\" ContentType=\"application/json\"/>\n";
|
|
||||||
stream << " <Default Extension=\"gcode\" ContentType=\"text/x.gcode\"/>\n";
|
stream << " <Default Extension=\"gcode\" ContentType=\"text/x.gcode\"/>\n";
|
||||||
stream << "</Types>";
|
stream << "</Types>";
|
||||||
|
|
||||||
@@ -7341,14 +7232,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
if (iter != m_shared_meshes.end())
|
if (iter != m_shared_meshes.end())
|
||||||
{
|
{
|
||||||
const ModelVolume* shared_volume = iter->second.second;
|
const ModelVolume* shared_volume = iter->second.second;
|
||||||
bool same_texture_paint = true;
|
|
||||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS) && same_texture_paint; ++slot)
|
|
||||||
same_texture_paint = shared_volume->texture_displacement_facet(slot).equals(volume->texture_displacement_facet(slot));
|
|
||||||
if ((shared_volume->supported_facets.equals(volume->supported_facets))
|
if ((shared_volume->supported_facets.equals(volume->supported_facets))
|
||||||
&& (shared_volume->seam_facets.equals(volume->seam_facets))
|
&& (shared_volume->seam_facets.equals(volume->seam_facets))
|
||||||
&& (shared_volume->mmu_segmentation_facets.equals(volume->mmu_segmentation_facets))
|
&& (shared_volume->mmu_segmentation_facets.equals(volume->mmu_segmentation_facets))
|
||||||
&& (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets))
|
&& (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets)))
|
||||||
&& same_texture_paint)
|
|
||||||
{
|
{
|
||||||
auto data = iter->second.first;
|
auto data = iter->second.first;
|
||||||
const_cast<_BBS_3MF_Exporter *>(this)->m_volume_paths.insert({volume, {data->sub_path, data->volumes_objectID.find(iter->second.second)->second}});
|
const_cast<_BBS_3MF_Exporter *>(this)->m_volume_paths.insert({volume, {data->sub_path, data->volumes_objectID.find(iter->second.second)->second}});
|
||||||
@@ -7764,20 +7651,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
output_buffer += "\"";
|
output_buffer += "\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
// One attribute per texture displacement layer slot. Older readers ignore attributes they do
|
|
||||||
// not know, so a project written here still opens in a build without the feature - it just
|
|
||||||
// loses the paint, which is also all it could have done with it.
|
|
||||||
for (int slot = 0; slot < TEXTURE_DISPLACEMENT_MAX_LAYERS; ++slot) {
|
|
||||||
const std::string texture_paint = volume->texture_displacement_facet(slot).get_triangle_as_string(i);
|
|
||||||
if (texture_paint.empty())
|
|
||||||
continue;
|
|
||||||
output_buffer += " ";
|
|
||||||
output_buffer += TEXTURE_DISPLACEMENT_ATTRS[slot];
|
|
||||||
output_buffer += "=\"";
|
|
||||||
output_buffer += texture_paint;
|
|
||||||
output_buffer += "\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
// BBS
|
// BBS
|
||||||
if (i < its.properties.size()) {
|
if (i < its.properties.size()) {
|
||||||
std::string prop_str = its.properties[i].to_string();
|
std::string prop_str = its.properties[i].to_string();
|
||||||
@@ -8227,42 +8100,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
return file_path;
|
return file_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes a volume's texture displacement stack into the archive and points the volume's metadata at it.
|
bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
|
||||||
// The layer settings go in as JSON and each layer's texture as the image file it was loaded from, both
|
|
||||||
// referenced by path - the same split EmbossShape makes for its SVG. The image deliberately stays out of
|
|
||||||
// the XML: it is binary and routinely megabytes, and base64 in an attribute would bloat the one file
|
|
||||||
// every reader has to parse just to list the objects.
|
|
||||||
static void add_texture_displacement(std::stringstream &stream, const ModelVolume &volume, mz_zip_archive &archive,
|
|
||||||
const std::string &id)
|
|
||||||
{
|
|
||||||
if (volume.texture_displacement_layers.empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
// A copy, because path_in_3mf is only meaningful inside the archive being written and the volume
|
|
||||||
// being exported is const (and may be saved again, elsewhere, with different paths).
|
|
||||||
std::vector<TextureDisplacementLayer> layers = volume.texture_displacement_layers;
|
|
||||||
for (TextureDisplacementLayer &layer : layers) {
|
|
||||||
layer.path_in_3mf.clear();
|
|
||||||
if (!layer.image_data || layer.image_data->empty())
|
|
||||||
continue;
|
|
||||||
// Always .png: the importer converts whatever the user picked (jpg, bmp, ...) to an 8-bit
|
|
||||||
// greyscale PNG, so image_data is PNG whatever layer.path happens to be named.
|
|
||||||
const std::string path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + "_" + std::to_string(layer.slot) + ".png";
|
|
||||||
// No deflate: a PNG is already compressed, so a second pass only costs time.
|
|
||||||
if (mz_zip_writer_add_mem(&archive, path.c_str(), layer.image_data->data(), layer.image_data->size(),
|
|
||||||
MZ_NO_COMPRESSION))
|
|
||||||
layer.path_in_3mf = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string json = texture_displacement_layers_to_json(layers, volume.texture_displacement_options);
|
|
||||||
const std::string json_path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + ".json";
|
|
||||||
if (!mz_zip_writer_add_mem(&archive, json_path.c_str(), json.data(), json.size(), MZ_DEFAULT_COMPRESSION))
|
|
||||||
return;
|
|
||||||
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << TEXTURE_DISPLACEMENT_KEY << "\" "
|
|
||||||
<< VALUE_ATTR << "=\"" << xml_escape(json_path) << "\"/>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
|
|
||||||
{
|
{
|
||||||
std::stringstream stream;
|
std::stringstream stream;
|
||||||
// Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back
|
// Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back
|
||||||
@@ -8374,8 +8212,6 @@ bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archiv
|
|||||||
to_xml(stream, *es, *volume, archive, m_fullpath_sources);
|
to_xml(stream, *es, *volume, archive, m_fullpath_sources);
|
||||||
}
|
}
|
||||||
|
|
||||||
add_texture_displacement(stream, *volume, archive, std::to_string(volume->id().id));
|
|
||||||
|
|
||||||
if (const std::optional<TextConfiguration> &tc = volume->text_configuration;
|
if (const std::optional<TextConfiguration> &tc = volume->text_configuration;
|
||||||
tc.has_value())
|
tc.has_value())
|
||||||
TextConfigurationSerialization::to_xml(stream, *tc);
|
TextConfigurationSerialization::to_xml(stream, *tc);
|
||||||
|
|||||||
@@ -0,0 +1,349 @@
|
|||||||
|
#include "JumpPointSearch.hpp"
|
||||||
|
#include "BoundingBox.hpp"
|
||||||
|
#include "ExPolygon.hpp"
|
||||||
|
#include "Point.hpp"
|
||||||
|
#include "libslic3r/AStar.hpp"
|
||||||
|
#include "libslic3r/KDTreeIndirect.hpp"
|
||||||
|
#include "libslic3r/Polygon.hpp"
|
||||||
|
#include "libslic3r/Polyline.hpp"
|
||||||
|
#include "libslic3r/libslic3r.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iterator>
|
||||||
|
#include <limits>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <oneapi/tbb/scalable_allocator.h>
|
||||||
|
|
||||||
|
//#define DEBUG_FILES
|
||||||
|
#ifdef DEBUG_FILES
|
||||||
|
#include "libslic3r/SVG.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
// execute fn for each pixel on the line. If fn returns false, terminate the iteration
|
||||||
|
template<typename PointFn> void dda(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
|
||||||
|
{
|
||||||
|
coord_t dx = abs(x1 - x0);
|
||||||
|
coord_t dy = abs(y1 - y0);
|
||||||
|
coord_t x = x0;
|
||||||
|
coord_t y = y0;
|
||||||
|
coord_t n = 1 + dx + dy;
|
||||||
|
coord_t x_inc = (x1 > x0) ? 1 : -1;
|
||||||
|
coord_t y_inc = (y1 > y0) ? 1 : -1;
|
||||||
|
coord_t error = dx - dy;
|
||||||
|
dx *= 2;
|
||||||
|
dy *= 2;
|
||||||
|
|
||||||
|
for (; n > 0; --n) {
|
||||||
|
if (!fn(x, y)) return;
|
||||||
|
|
||||||
|
if (error > 0) {
|
||||||
|
x += x_inc;
|
||||||
|
error -= dy;
|
||||||
|
} else {
|
||||||
|
y += y_inc;
|
||||||
|
error += dx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// will draw the line twice, second time with and offset of 1 in the direction of normal
|
||||||
|
// may call the fn on the same coordiantes multiple times!
|
||||||
|
template<typename PointFn> void double_dda_with_offset(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
|
||||||
|
{
|
||||||
|
Vec2d normal = Point{y1 - y0, x1 - x0}.cast<double>().normalized();
|
||||||
|
normal.x() = ceil(normal.x());
|
||||||
|
normal.y() = ceil(normal.y());
|
||||||
|
Point start_offset = Point(x0, y0) + (normal).cast<coord_t>();
|
||||||
|
Point end_offset = Point(x1, y1) + (normal).cast<coord_t>();
|
||||||
|
|
||||||
|
dda(x0, y0, x1, y1, fn);
|
||||||
|
dda(start_offset.x(), start_offset.y(), end_offset.x(), end_offset.y(), fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename CellPositionType, typename CellQueryFn> class JPSTracer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Use incoming_dir [0,0] for starting points, so that all directions are checked from that point
|
||||||
|
struct Node
|
||||||
|
{
|
||||||
|
CellPositionType position;
|
||||||
|
CellPositionType incoming_dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
JPSTracer(CellPositionType target, CellQueryFn is_passable) : target(target), is_passable(is_passable) {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
CellPositionType target;
|
||||||
|
CellQueryFn is_passable; // should return boolean whether the cell is passable or not
|
||||||
|
|
||||||
|
CellPositionType find_jump_point(CellPositionType start, CellPositionType forward_dir) const
|
||||||
|
{
|
||||||
|
CellPositionType next = start + forward_dir;
|
||||||
|
while (next != target && is_passable(next) && !(is_jump_point(next, forward_dir))) { next = next + forward_dir; }
|
||||||
|
|
||||||
|
if (is_passable(next)) {
|
||||||
|
return next;
|
||||||
|
} else {
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_jump_point(CellPositionType pos, CellPositionType forward_dir) const
|
||||||
|
{
|
||||||
|
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
|
||||||
|
// diagonal
|
||||||
|
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
|
||||||
|
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
|
||||||
|
|
||||||
|
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) { return true; }
|
||||||
|
|
||||||
|
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) { return true; }
|
||||||
|
|
||||||
|
if (find_jump_point(pos, horizontal_check_dir) != pos) { return true; }
|
||||||
|
|
||||||
|
if (find_jump_point(pos, vertical_check_dir) != pos) { return true; }
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} else { // horizontal or vertical
|
||||||
|
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
|
||||||
|
|
||||||
|
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { return true; }
|
||||||
|
|
||||||
|
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { return true; }
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
template<class Fn> void foreach_reachable(const Node &from, Fn &&fn) const
|
||||||
|
{
|
||||||
|
const CellPositionType & pos = from.position;
|
||||||
|
const CellPositionType & forward_dir = from.incoming_dir;
|
||||||
|
std::vector<CellPositionType> dirs_to_check{};
|
||||||
|
|
||||||
|
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 0) { // special case for starting point
|
||||||
|
dirs_to_check = all_directions;
|
||||||
|
} else if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
|
||||||
|
// diagonal
|
||||||
|
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
|
||||||
|
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
|
||||||
|
|
||||||
|
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) {
|
||||||
|
dirs_to_check.push_back(forward_dir - 2 * horizontal_check_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) {
|
||||||
|
dirs_to_check.push_back(forward_dir - 2 * vertical_check_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
dirs_to_check.push_back(horizontal_check_dir);
|
||||||
|
dirs_to_check.push_back(vertical_check_dir);
|
||||||
|
dirs_to_check.push_back(forward_dir);
|
||||||
|
|
||||||
|
} else { // horizontal or vertical
|
||||||
|
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
|
||||||
|
|
||||||
|
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { dirs_to_check.push_back(forward_dir + side_dir); }
|
||||||
|
|
||||||
|
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { dirs_to_check.push_back(forward_dir - side_dir); }
|
||||||
|
dirs_to_check.push_back(forward_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const CellPositionType &dir : dirs_to_check) {
|
||||||
|
CellPositionType jp = find_jump_point(pos, dir);
|
||||||
|
if (jp != pos) fn(Node{jp, dir});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float distance(Node a, Node b) const { return (a.position - b.position).template cast<double>().norm(); }
|
||||||
|
|
||||||
|
float goal_heuristic(Node n) const { return n.position == target ? -1.f : (target - n.position).template cast<double>().norm(); }
|
||||||
|
|
||||||
|
size_t unique_id(Node n) const { return (static_cast<size_t>(uint16_t(n.position.x())) << 16) + static_cast<size_t>(uint16_t(n.position.y())); }
|
||||||
|
|
||||||
|
const std::vector<CellPositionType> all_directions{{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}};
|
||||||
|
};
|
||||||
|
|
||||||
|
void JPSPathFinder::clear()
|
||||||
|
{
|
||||||
|
inpassable.clear();
|
||||||
|
max_search_box.max = Pixel(std::numeric_limits<coord_t>::min(), std::numeric_limits<coord_t>::min());
|
||||||
|
max_search_box.min = Pixel(std::numeric_limits<coord_t>::max(), std::numeric_limits<coord_t>::max());
|
||||||
|
add_obstacles(bed_shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
void JPSPathFinder::add_obstacles(const Lines &obstacles)
|
||||||
|
{
|
||||||
|
auto store_obstacle = [&](coord_t x, coord_t y) {
|
||||||
|
max_search_box.max.x() = std::max(max_search_box.max.x(), x);
|
||||||
|
max_search_box.max.y() = std::max(max_search_box.max.y(), y);
|
||||||
|
max_search_box.min.x() = std::min(max_search_box.min.x(), x);
|
||||||
|
max_search_box.min.y() = std::min(max_search_box.min.y(), y);
|
||||||
|
inpassable.insert(Pixel{x, y});
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const Line &l : obstacles) {
|
||||||
|
Pixel start = pixelize(l.a);
|
||||||
|
Pixel end = pixelize(l.b);
|
||||||
|
double_dda_with_offset(start.x(), start.y(), end.x(), end.y(), store_obstacle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Polyline JPSPathFinder::find_path(const Point &p0, const Point &p1)
|
||||||
|
{
|
||||||
|
Pixel start = pixelize(p0);
|
||||||
|
Pixel end = pixelize(p1);
|
||||||
|
if (inpassable.empty() || (start - end).cast<float>().norm() < 3.0) { return Polyline{p0, p1}; }
|
||||||
|
|
||||||
|
if (inpassable.find(start) != inpassable.end()) {
|
||||||
|
dda(start.x(), start.y(), end.x(), end.y(), [&](coord_t x, coord_t y) {
|
||||||
|
if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
|
||||||
|
start = Pixel(x, y);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inpassable.find(end) != inpassable.end()) {
|
||||||
|
dda(end.x(), end.y(), start.x(), start.y(), [&](coord_t x, coord_t y) {
|
||||||
|
if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
|
||||||
|
end = Pixel(x, y);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
BoundingBox search_box = max_search_box;
|
||||||
|
search_box.max -= Pixel(1, 1);
|
||||||
|
search_box.min += Pixel(1, 1);
|
||||||
|
|
||||||
|
BoundingBox bounding_square(Points{start, end});
|
||||||
|
bounding_square.max += Pixel(5, 5);
|
||||||
|
bounding_square.min -= Pixel(5, 5);
|
||||||
|
coord_t bounding_square_size = 2 * std::max(bounding_square.size().x(), bounding_square.size().y());
|
||||||
|
bounding_square.max.x() += (bounding_square_size - bounding_square.size().x()) / 2;
|
||||||
|
bounding_square.min.x() -= (bounding_square_size - bounding_square.size().x()) / 2;
|
||||||
|
bounding_square.max.y() += (bounding_square_size - bounding_square.size().y()) / 2;
|
||||||
|
bounding_square.min.y() -= (bounding_square_size - bounding_square.size().y()) / 2;
|
||||||
|
|
||||||
|
// Intersection - limit the search box to a square area around the start and end, to fasten the path searching
|
||||||
|
search_box.max = search_box.max.cwiseMin(bounding_square.max);
|
||||||
|
search_box.min = search_box.min.cwiseMax(bounding_square.min);
|
||||||
|
|
||||||
|
auto cell_query = [&](Pixel pixel) { return search_box.contains(pixel) && (pixel == start || pixel == end || inpassable.find(pixel) == inpassable.end()); };
|
||||||
|
|
||||||
|
JPSTracer<Pixel, decltype(cell_query)> tracer(end, cell_query);
|
||||||
|
using QNode = astar::QNode<JPSTracer<Pixel, decltype(cell_query)>>;
|
||||||
|
|
||||||
|
std::unordered_map<size_t, QNode> astar_cache{};
|
||||||
|
std::vector<Pixel, PointsAllocator<Pixel>> out_path;
|
||||||
|
std::vector<decltype(tracer)::Node> out_nodes;
|
||||||
|
|
||||||
|
if (!astar::search_route(tracer, {start, {0, 0}}, std::back_inserter(out_nodes), astar_cache)) {
|
||||||
|
// path not found - just reconstruct the best path from astar cache.
|
||||||
|
// Note that astar_cache is NOT empty - at least the starting point should always be there
|
||||||
|
auto coordiante_func = [&astar_cache](size_t idx, size_t dim) { return float(astar_cache[idx].node.position[dim]); };
|
||||||
|
std::vector<size_t> keys;
|
||||||
|
keys.reserve(astar_cache.size());
|
||||||
|
for (const auto &pair : astar_cache) { keys.push_back(pair.first); }
|
||||||
|
KDTreeIndirect<2, float, decltype(coordiante_func)> kd_tree(coordiante_func, keys);
|
||||||
|
size_t closest_qnode = find_closest_point(kd_tree, end.cast<float>());
|
||||||
|
|
||||||
|
out_path.push_back(end);
|
||||||
|
while (closest_qnode != astar::Unassigned) {
|
||||||
|
out_path.push_back(astar_cache[closest_qnode].node.position);
|
||||||
|
closest_qnode = astar_cache[closest_qnode].parent;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const auto &node : out_nodes) { out_path.push_back(node.position); }
|
||||||
|
out_path.push_back(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef DEBUG_FILES
|
||||||
|
auto scaled_points = [](const Points &ps) {
|
||||||
|
Points r;
|
||||||
|
for (const Point &p : ps) { r.push_back(Point::new_scale(p.x(), p.y())); }
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
auto scaled_point = [](const Point &p) { return Point::new_scale(p.x(), p.y()); };
|
||||||
|
::Slic3r::SVG svg(debug_out_path(("path_jps" + std::to_string(print_z) + "_" + std::to_string(rand() % 1000)).c_str()).c_str(),
|
||||||
|
BoundingBox(scaled_point(search_box.min), scaled_point(search_box.max)));
|
||||||
|
for (const auto &p : inpassable) { svg.draw(scaled_point(p), "black", scale_(0.4)); }
|
||||||
|
for (const auto &qn : astar_cache) { svg.draw(scaled_point(qn.second.node.position), "blue", scale_(0.3)); }
|
||||||
|
svg.draw(Polyline(scaled_points(out_path)), "yellow", scale_(0.25));
|
||||||
|
svg.draw(scaled_point(end), "purple", scale_(0.4));
|
||||||
|
svg.draw(scaled_point(start), "green", scale_(0.4));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::vector<Pixel, PointsAllocator<Pixel>> tmp_path;
|
||||||
|
tmp_path.reserve(out_path.size());
|
||||||
|
// Some path found, reverse and remove points that do not change direction
|
||||||
|
std::reverse(out_path.begin(), out_path.end());
|
||||||
|
{
|
||||||
|
tmp_path.push_back(out_path.front()); // first point
|
||||||
|
for (size_t i = 1; i < out_path.size() - 1; i++) {
|
||||||
|
if ((out_path[i] - out_path[i - 1]).cast<float>().normalized() != (out_path[i + 1] - out_path[i]).cast<float>().normalized()) { tmp_path.push_back(out_path[i]); }
|
||||||
|
}
|
||||||
|
tmp_path.push_back(out_path.back()); // last_point
|
||||||
|
out_path = tmp_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef DEBUG_FILES
|
||||||
|
svg.draw(Polyline(scaled_points(out_path)), "orange", scale_(0.20));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
tmp_path.clear();
|
||||||
|
// remove redundant jump points - there are points that change direction but are not needed - this inefficiency arises from the
|
||||||
|
// usage of grid search The removal alg tries to find the longest Px Px+k path without obstacles. If Px Px+k+1 is blocked, it will
|
||||||
|
// insert the Px+k point to result and continue search from Px+k
|
||||||
|
{
|
||||||
|
tmp_path.push_back(out_path.front()); // first point
|
||||||
|
size_t index_of_last_stored_point = 0;
|
||||||
|
for (size_t i = 1; i < out_path.size(); i++) {
|
||||||
|
if (i - index_of_last_stored_point < 2) continue;
|
||||||
|
bool passable = true;
|
||||||
|
auto store_obstacle = [&](coord_t x, coord_t y) {
|
||||||
|
if (Pixel(x, y) != start && Pixel(x, y) != end && inpassable.find(Pixel(x, y)) != inpassable.end()) {
|
||||||
|
passable = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
dda(tmp_path.back().x(), tmp_path.back().y(), out_path[i].x(), out_path[i].y(), store_obstacle);
|
||||||
|
if (!passable) {
|
||||||
|
tmp_path.push_back(out_path[i - 1]);
|
||||||
|
index_of_last_stored_point = i - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tmp_path.push_back(out_path.back()); // last_point
|
||||||
|
out_path = tmp_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef DEBUG_FILES
|
||||||
|
svg.draw(Polyline(scaled_points(out_path)), "red", scale_(0.15));
|
||||||
|
svg.Close();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// before returing the path, transform it from pixels back to points.
|
||||||
|
// Also replace the first and last pixel by input points so that result path patches input params exactly.
|
||||||
|
for (Pixel &p : out_path) { p = unpixelize(p); }
|
||||||
|
out_path.front() = p0;
|
||||||
|
out_path.back() = p1;
|
||||||
|
|
||||||
|
return Polyline(out_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Slic3r
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "Line.hpp"
|
||||||
|
#ifndef SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
|
||||||
|
#define SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
|
||||||
|
|
||||||
|
#include "BoundingBox.hpp"
|
||||||
|
#include "Polygon.hpp"
|
||||||
|
#include "libslic3r/Layer.hpp"
|
||||||
|
#include "libslic3r/Point.hpp"
|
||||||
|
#include "libslic3r/Polyline.hpp"
|
||||||
|
#include "libslic3r/libslic3r.h"
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
class JPSPathFinder
|
||||||
|
{
|
||||||
|
using Pixel = Point;
|
||||||
|
std::unordered_set<Pixel, PointHash> inpassable;
|
||||||
|
coordf_t print_z;
|
||||||
|
BoundingBox max_search_box;
|
||||||
|
Lines bed_shape;
|
||||||
|
|
||||||
|
const coord_t resolution = scaled(1.5);
|
||||||
|
Pixel pixelize(const Point &p) { return p / resolution; }
|
||||||
|
Point unpixelize(const Pixel &p) { return p * resolution; }
|
||||||
|
|
||||||
|
public:
|
||||||
|
JPSPathFinder() = default;
|
||||||
|
void init_bed_shape(const Points &bed_shape) { this->bed_shape = (to_lines(Polygon{bed_shape})); };
|
||||||
|
void clear();
|
||||||
|
void add_obstacles(const Lines &obstacles);
|
||||||
|
Polyline find_path(const Point &start, const Point &end);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Slic3r
|
||||||
|
|
||||||
|
#endif /* SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_ */
|
||||||
@@ -2991,15 +2991,6 @@ void PrintObject::clear_shared_object()
|
|||||||
void PrintObject::copy_layers_from_shared_object()
|
void PrintObject::copy_layers_from_shared_object()
|
||||||
{
|
{
|
||||||
if (m_shared_object) {
|
if (m_shared_object) {
|
||||||
// Orca: Print::process() calls this once per slice for each sharing object, after clear_shared_object() has
|
|
||||||
// emptied any earlier alias, so the vectors hold only layers this object sliced itself before it started sharing
|
|
||||||
// (or nothing). Free them here, as clear_support_layers() / clear_layers() would; those skip deletion while
|
|
||||||
// m_shared_object is set. The belt-only data they also free is always empty here, because belt printers never
|
|
||||||
// share layers.
|
|
||||||
for (SupportLayer *layer : m_support_layers)
|
|
||||||
delete layer;
|
|
||||||
for (Layer *layer : m_layers)
|
|
||||||
delete layer;
|
|
||||||
m_layers.clear();
|
m_layers.clear();
|
||||||
m_support_layers.clear();
|
m_support_layers.clear();
|
||||||
|
|
||||||
|
|||||||
@@ -982,9 +982,6 @@ void PrintObject::generate_support_material(bool with_belt_brim)
|
|||||||
this->_generate_support_material();
|
this->_generate_support_material();
|
||||||
m_print->throw_if_canceled();
|
m_print->throw_if_canceled();
|
||||||
}
|
}
|
||||||
// Orca: the tree support collision/avoidance caches and support nodes are only used while this step runs
|
|
||||||
// (detect_overhangs() rebuilds them from scratch), so don't keep them resident until the next slice.
|
|
||||||
this->clear_tree_support_preview_cache();
|
|
||||||
// Belt brim rides here rather than in the brim step because its apron
|
// Belt brim rides here rather than in the brim step because its apron
|
||||||
// prologue introduces print_z values below the object's first layer, and
|
// prologue introduces print_z values below the object's first layer, and
|
||||||
// those must exist before ToolOrdering is built at psWipeTower - one step
|
// those must exist before ToolOrdering is built at psWipeTower - one step
|
||||||
|
|||||||
@@ -1532,7 +1532,7 @@ void PrintObject::slice_volumes()
|
|||||||
if (min_growth < 0.f || elfoot > 0.f) {
|
if (min_growth < 0.f || elfoot > 0.f) {
|
||||||
// Apply the negative XY compensation. (the ones that is <0)
|
// Apply the negative XY compensation. (the ones that is <0)
|
||||||
ExPolygons trimming;
|
ExPolygons trimming;
|
||||||
const float eps = float(scale_(m_config.slice_closing_radius.value) * 1.5);
|
static const float eps = float(scale_(m_config.slice_closing_radius.value) * 1.5);
|
||||||
if (elfoot > 0.f) {
|
if (elfoot > 0.f) {
|
||||||
ExPolygons expolygons_to_compensate = offset_ex(layer->merged(eps), -eps);
|
ExPolygons expolygons_to_compensate = offset_ex(layer->merged(eps), -eps);
|
||||||
lslices_elfoot_uncompensated[layer_id] = expolygons_to_compensate;
|
lslices_elfoot_uncompensated[layer_id] = expolygons_to_compensate;
|
||||||
|
|||||||
@@ -1976,15 +1976,37 @@ coordf_t TreeSupport::get_radius(const SupportNode* node)
|
|||||||
return node->radius;
|
return node->radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Orca: drop_nodes() calls these several times per node per layer, so they return a reference into the
|
ExPolygons TreeSupport::get_avoidance(coordf_t radius, size_t obj_layer_nr)
|
||||||
// TreeSupportData cache. The cache only ever inserts, so its entries never move while nodes drop in parallel.
|
|
||||||
const ExPolygons& TreeSupport::get_avoidance(coordf_t radius, size_t obj_layer_nr)
|
|
||||||
{
|
{
|
||||||
|
#if USE_SUPPORT_3D
|
||||||
|
if (m_model_volumes) {
|
||||||
|
bool on_build_plate = m_object_config->support_on_build_plate_only.value;
|
||||||
|
const Polygons& avoid_polys = m_model_volumes->getAvoidance(radius, obj_layer_nr, TreeSupport3D::TreeModelVolumes::AvoidanceType::FastSafe, on_build_plate, true);
|
||||||
|
ExPolygons expolys;
|
||||||
|
for (auto& poly : avoid_polys)
|
||||||
|
expolys.emplace_back(std::move(poly));
|
||||||
|
return expolys;
|
||||||
|
}
|
||||||
|
return ExPolygons();
|
||||||
|
#else
|
||||||
return m_ts_data->get_avoidance(radius, obj_layer_nr);
|
return m_ts_data->get_avoidance(radius, obj_layer_nr);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
const ExPolygons& TreeSupport::get_collision(coordf_t radius, size_t layer_nr)
|
ExPolygons TreeSupport::get_collision(coordf_t radius, size_t layer_nr)
|
||||||
{
|
{
|
||||||
|
#if USE_SUPPORT_3D
|
||||||
|
if (m_model_volumes) {
|
||||||
|
bool on_build_plate = m_object_config->support_on_build_plate_only.value;
|
||||||
|
const Polygons& collision_polys = m_model_volumes->getCollision(radius, layer_nr, true);
|
||||||
|
ExPolygons expolys;
|
||||||
|
for (auto& poly : collision_polys)
|
||||||
|
expolys.emplace_back(std::move(poly));
|
||||||
|
return expolys;
|
||||||
|
}
|
||||||
|
#else
|
||||||
return m_ts_data->get_collision(radius, layer_nr);
|
return m_ts_data->get_collision(radius, layer_nr);
|
||||||
|
#endif
|
||||||
|
return ExPolygons();
|
||||||
}
|
}
|
||||||
Polygons TreeSupport::get_collision_polys(coordf_t radius, size_t layer_nr)
|
Polygons TreeSupport::get_collision_polys(coordf_t radius, size_t layer_nr)
|
||||||
{
|
{
|
||||||
@@ -2805,12 +2827,7 @@ void TreeSupport::draw_circles()
|
|||||||
#endif // SUPPORT_TREE_DEBUG_TO_SVG
|
#endif // SUPPORT_TREE_DEBUG_TO_SVG
|
||||||
|
|
||||||
SupportLayerPtrs& ts_layers = m_object->support_layers();
|
SupportLayerPtrs& ts_layers = m_object->support_layers();
|
||||||
// Orca: the vector owns its layers, so the dropped ones are deleted before they are erased.
|
auto iter = std::remove_if(ts_layers.begin(), ts_layers.end(), [](SupportLayer* ts_layer) { return ts_layer->height < EPSILON; });
|
||||||
// std::stable_partition (unlike std::remove_if) leaves exactly the dropped layers in the tail.
|
|
||||||
// Only heights below EPSILON are dropped; written as !(height < EPSILON), a NaN height is kept.
|
|
||||||
auto iter = std::stable_partition(ts_layers.begin(), ts_layers.end(), [](SupportLayer* ts_layer) { return !(ts_layer->height < EPSILON); });
|
|
||||||
for (auto it = iter; it != ts_layers.end(); ++it)
|
|
||||||
delete *it;
|
|
||||||
ts_layers.erase(iter, ts_layers.end());
|
ts_layers.erase(iter, ts_layers.end());
|
||||||
for (int layer_nr = 0; layer_nr < ts_layers.size(); layer_nr++) {
|
for (int layer_nr = 0; layer_nr < ts_layers.size(); layer_nr++) {
|
||||||
ts_layers[layer_nr]->upper_layer = layer_nr != ts_layers.size() - 1 ? ts_layers[layer_nr + 1] : nullptr;
|
ts_layers[layer_nr]->upper_layer = layer_nr != ts_layers.size() - 1 ? ts_layers[layer_nr + 1] : nullptr;
|
||||||
@@ -3054,7 +3071,7 @@ void TreeSupport::drop_nodes()
|
|||||||
//Insert a completely new node and let both original nodes fade.
|
//Insert a completely new node and let both original nodes fade.
|
||||||
Point next_position = (node.position + neighbours[0]) / 2; //Average position of the two nodes.
|
Point next_position = (node.position + neighbours[0]) / 2; //Average position of the two nodes.
|
||||||
coordf_t next_radius = calc_radius(node.dist_mm_to_top+height_next);
|
coordf_t next_radius = calc_radius(node.dist_mm_to_top+height_next);
|
||||||
const ExPolygons& avoid_layer = get_avoidance(next_radius, obj_layer_nr_next);
|
auto avoid_layer = get_avoidance(next_radius, obj_layer_nr_next);
|
||||||
if (group_index == 0)
|
if (group_index == 0)
|
||||||
{
|
{
|
||||||
//Avoid collisions.
|
//Avoid collisions.
|
||||||
@@ -3258,7 +3275,7 @@ void TreeSupport::drop_nodes()
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
coordf_t next_radius = calc_radius(node.dist_mm_to_top + height_next);
|
coordf_t next_radius = calc_radius(node.dist_mm_to_top + height_next);
|
||||||
const ExPolygons& avoidance_next = get_avoidance(next_radius, obj_layer_nr_next);
|
auto avoidance_next = get_avoidance(next_radius, obj_layer_nr_next);
|
||||||
|
|
||||||
Point to_outside = projection_onto(avoidance_next, node.position);
|
Point to_outside = projection_onto(avoidance_next, node.position);
|
||||||
Point direction_to_outer = to_outside - node.position;
|
Point direction_to_outer = to_outside - node.position;
|
||||||
@@ -3320,7 +3337,7 @@ void TreeSupport::drop_nodes()
|
|||||||
p_node->to_buildplate = false;
|
p_node->to_buildplate = false;
|
||||||
return; // from parallel_for_each lambda
|
return; // from parallel_for_each lambda
|
||||||
}
|
}
|
||||||
const ExPolygons& next_collision = get_collision(0, obj_layer_nr_next);
|
auto next_collision = get_collision(0, obj_layer_nr_next);
|
||||||
const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex);
|
const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex);
|
||||||
// don't increase radius if next node will collide partially with the object (STUDIO-7883)
|
// don't increase radius if next node will collide partially with the object (STUDIO-7883)
|
||||||
to_outside = projection_onto(next_collision, next_layer_vertex);
|
to_outside = projection_onto(next_collision, next_layer_vertex);
|
||||||
|
|||||||
@@ -536,9 +536,9 @@ private:
|
|||||||
coordf_t calc_branch_radius(coordf_t base_radius, coordf_t mm_to_top, double diameter_angle_scale_factor, bool use_min_distance=true);
|
coordf_t calc_branch_radius(coordf_t base_radius, coordf_t mm_to_top, double diameter_angle_scale_factor, bool use_min_distance=true);
|
||||||
coordf_t calc_radius(coordf_t mm_to_top);
|
coordf_t calc_radius(coordf_t mm_to_top);
|
||||||
coordf_t get_radius(const SupportNode* node);
|
coordf_t get_radius(const SupportNode* node);
|
||||||
const ExPolygons& get_avoidance(coordf_t radius, size_t obj_layer_nr);
|
ExPolygons get_avoidance(coordf_t radius, size_t obj_layer_nr);
|
||||||
// layer's expolygon expanded by radius+m_xy_distance
|
// layer's expolygon expanded by radius+m_xy_distance
|
||||||
const ExPolygons& get_collision(coordf_t radius, size_t layer_nr);
|
ExPolygons get_collision(coordf_t radius, size_t layer_nr);
|
||||||
// get Polygons instead of ExPolygons
|
// get Polygons instead of ExPolygons
|
||||||
Polygons get_collision_polys(coordf_t radius, size_t layer_nr);
|
Polygons get_collision_polys(coordf_t radius, size_t layer_nr);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include "nlohmann/json.hpp"
|
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <ratio>
|
#include <ratio>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -5167,260 +5166,5 @@ indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const s
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
|
||||||
// Project persistence. See the declarations in TextureDisplacement.hpp for why this is JSON rather
|
|
||||||
// than the cereal stream next to them.
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
nlohmann::json vec2_to_json(const Vec2f &v) { return nlohmann::json::array({ v.x(), v.y() }); }
|
|
||||||
nlohmann::json vec3_to_json(const Vec3f &v) { return nlohmann::json::array({ v.x(), v.y(), v.z() }); }
|
|
||||||
|
|
||||||
Vec2f vec2_from_json(const nlohmann::json &j, const Vec2f &fallback)
|
|
||||||
{
|
|
||||||
if (!j.is_array() || j.size() != 2)
|
|
||||||
return fallback;
|
|
||||||
return Vec2f(j[0].get<float>(), j[1].get<float>());
|
|
||||||
}
|
|
||||||
Vec3f vec3_from_json(const nlohmann::json &j, const Vec3f &fallback)
|
|
||||||
{
|
|
||||||
if (!j.is_array() || j.size() != 3)
|
|
||||||
return fallback;
|
|
||||||
return Vec3f(j[0].get<float>(), j[1].get<float>(), j[2].get<float>());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reads key `k` into `out` when it is present and of the expected type, leaving `out` alone otherwise.
|
|
||||||
// That "leave it alone" is the whole point: every member starts at its struct default, so a project
|
|
||||||
// written by an older build simply keeps the defaults for whatever it did not know about.
|
|
||||||
template<class T> void read(const nlohmann::json &j, const char *k, T &out)
|
|
||||||
{
|
|
||||||
const auto it = j.find(k);
|
|
||||||
if (it == j.end())
|
|
||||||
return;
|
|
||||||
try {
|
|
||||||
out = it->get<T>();
|
|
||||||
} catch (...) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void read_enum(const nlohmann::json &j, const char *k, int &out)
|
|
||||||
{
|
|
||||||
const auto it = j.find(k);
|
|
||||||
if (it != j.end() && it->is_number_integer())
|
|
||||||
out = it->get<int>();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
std::string texture_displacement_layers_to_json(const std::vector<TextureDisplacementLayer> &layers,
|
|
||||||
const TextureDisplacementOptions &options)
|
|
||||||
{
|
|
||||||
nlohmann::json root;
|
|
||||||
root["version"] = 1;
|
|
||||||
|
|
||||||
nlohmann::json &opt = root["options"];
|
|
||||||
opt["displace_border"] = options.displace_border;
|
|
||||||
opt["smooth_enabled"] = options.smooth_enabled;
|
|
||||||
opt["smooth_strength"] = options.smooth_strength;
|
|
||||||
opt["smooth_iterations"] = options.smooth_iterations;
|
|
||||||
opt["smooth_skip_border"] = options.smooth_skip_border;
|
|
||||||
opt["pipeline_v2"] = options.pipeline_v2;
|
|
||||||
opt["v2_refine_mm"] = options.v2_refine_mm;
|
|
||||||
opt["v2_regularize"] = options.v2_regularize;
|
|
||||||
opt["v2_max_triangles_k"] = options.v2_max_triangles_k;
|
|
||||||
opt["v2_relocate"] = options.v2_relocate;
|
|
||||||
opt["v2_flip_edges"] = options.v2_flip_edges;
|
|
||||||
opt["color_mix_enabled"] = options.color_mix_enabled;
|
|
||||||
opt["color_despeckle"] = options.color_despeckle;
|
|
||||||
opt["color_mix_count"] = options.color_mix_count;
|
|
||||||
opt["active_slot"] = options.active_slot;
|
|
||||||
opt["panel_mode"] = options.panel_mode;
|
|
||||||
|
|
||||||
nlohmann::json &arr = root["layers"];
|
|
||||||
arr = nlohmann::json::array();
|
|
||||||
for (const TextureDisplacementLayer &l : layers) {
|
|
||||||
nlohmann::json j;
|
|
||||||
j["slot"] = l.slot;
|
|
||||||
j["name"] = l.name;
|
|
||||||
j["path"] = l.path;
|
|
||||||
j["path_in_3mf"] = l.path_in_3mf;
|
|
||||||
|
|
||||||
j["depth_mm"] = l.depth_mm;
|
|
||||||
j["tiling_scale"] = l.tiling_scale;
|
|
||||||
j["rotation_deg"] = l.rotation_deg;
|
|
||||||
j["offset"] = vec2_to_json(l.offset);
|
|
||||||
j["invert"] = l.invert;
|
|
||||||
j["midlevel"] = l.midlevel;
|
|
||||||
j["smoothing"] = l.smoothing;
|
|
||||||
|
|
||||||
j["edge_smoothing"] = l.edge_smoothing;
|
|
||||||
j["edge_smoothing_amount"] = l.edge_smoothing_amount;
|
|
||||||
j["auto_connect_islands"] = l.auto_connect_islands;
|
|
||||||
|
|
||||||
j["tile_enabled"] = l.tile_enabled;
|
|
||||||
j["tile_method"] = int(l.tile_method);
|
|
||||||
j["projection_method"] = int(l.projection_method);
|
|
||||||
j["blend_mode"] = int(l.blend_mode);
|
|
||||||
j["color_enabled"] = l.color_enabled;
|
|
||||||
|
|
||||||
j["lscm_seam_angle_deg"] = l.lscm_seam_angle_deg;
|
|
||||||
j["island_padding_mm"] = l.island_padding_mm;
|
|
||||||
|
|
||||||
j["view_project_right"] = vec3_to_json(l.view_project_right);
|
|
||||||
j["view_project_up"] = vec3_to_json(l.view_project_up);
|
|
||||||
j["view_project_projective"] = l.view_project_projective;
|
|
||||||
j["view_project_matrix"] = l.view_project_matrix;
|
|
||||||
|
|
||||||
// Flat pairs rather than nested arrays: shorter, and the reader can simply ignore a trailing
|
|
||||||
// odd element instead of having to validate every sub-array.
|
|
||||||
nlohmann::json &seams = j["lscm_seam_edges"];
|
|
||||||
seams = nlohmann::json::array();
|
|
||||||
for (const auto &[a, b] : l.lscm_seam_edges) {
|
|
||||||
seams.push_back(a);
|
|
||||||
seams.push_back(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
nlohmann::json &islands = j["islands"];
|
|
||||||
islands = nlohmann::json::array();
|
|
||||||
for (const TextureIsland &island : l.islands)
|
|
||||||
islands.push_back(nlohmann::json::array(
|
|
||||||
{ island.offset.x(), island.offset.y(), island.rotation_deg, island.scale }));
|
|
||||||
|
|
||||||
j["island_groups"] = l.island_groups;
|
|
||||||
|
|
||||||
nlohmann::json &overrides = j["lscm_uv_overrides"];
|
|
||||||
overrides = nlohmann::json::array();
|
|
||||||
for (const auto &[key, uv] : l.lscm_uv_overrides)
|
|
||||||
overrides.push_back(nlohmann::json::array({ key, uv.x(), uv.y() }));
|
|
||||||
|
|
||||||
arr.push_back(std::move(j));
|
|
||||||
}
|
|
||||||
return root.dump();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool texture_displacement_layers_from_json(const std::string &text,
|
|
||||||
std::vector<TextureDisplacementLayer> &layers,
|
|
||||||
TextureDisplacementOptions &options)
|
|
||||||
{
|
|
||||||
nlohmann::json root;
|
|
||||||
try {
|
|
||||||
root = nlohmann::json::parse(text);
|
|
||||||
} catch (...) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!root.is_object())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
TextureDisplacementOptions out_options;
|
|
||||||
if (const auto it = root.find("options"); it != root.end() && it->is_object()) {
|
|
||||||
const nlohmann::json &opt = *it;
|
|
||||||
read(opt, "displace_border", out_options.displace_border);
|
|
||||||
read(opt, "smooth_enabled", out_options.smooth_enabled);
|
|
||||||
read(opt, "smooth_strength", out_options.smooth_strength);
|
|
||||||
read(opt, "smooth_iterations", out_options.smooth_iterations);
|
|
||||||
read(opt, "smooth_skip_border", out_options.smooth_skip_border);
|
|
||||||
read(opt, "pipeline_v2", out_options.pipeline_v2);
|
|
||||||
read(opt, "v2_refine_mm", out_options.v2_refine_mm);
|
|
||||||
read(opt, "v2_regularize", out_options.v2_regularize);
|
|
||||||
read(opt, "v2_max_triangles_k", out_options.v2_max_triangles_k);
|
|
||||||
read(opt, "v2_relocate", out_options.v2_relocate);
|
|
||||||
read(opt, "v2_flip_edges", out_options.v2_flip_edges);
|
|
||||||
read(opt, "color_mix_enabled", out_options.color_mix_enabled);
|
|
||||||
read(opt, "color_despeckle", out_options.color_despeckle);
|
|
||||||
read(opt, "color_mix_count", out_options.color_mix_count);
|
|
||||||
read(opt, "active_slot", out_options.active_slot);
|
|
||||||
read(opt, "panel_mode", out_options.panel_mode);
|
|
||||||
out_options.active_slot = std::clamp(out_options.active_slot, 0, int(TEXTURE_DISPLACEMENT_MAX_LAYERS) - 1);
|
|
||||||
out_options.panel_mode = std::clamp(out_options.panel_mode, 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<TextureDisplacementLayer> out_layers;
|
|
||||||
// read() skips a value of the wrong type, but the vector, matrix, seam, island and UV-override arrays
|
|
||||||
// below read their numbers with get<>(), which throws on one; that rejects the document, like text that
|
|
||||||
// does not parse.
|
|
||||||
try {
|
|
||||||
if (const auto it = root.find("layers"); it != root.end() && it->is_array()) {
|
|
||||||
for (const nlohmann::json &j : *it) {
|
|
||||||
if (!j.is_object())
|
|
||||||
continue;
|
|
||||||
TextureDisplacementLayer l;
|
|
||||||
read(j, "slot", l.slot);
|
|
||||||
// The panel and the paint masks are indexed by slot, so a layer without a valid one rejects the
|
|
||||||
// document rather than reaching them.
|
|
||||||
if (l.slot < 0 || l.slot >= int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
|
|
||||||
return false;
|
|
||||||
read(j, "name", l.name);
|
|
||||||
read(j, "path", l.path);
|
|
||||||
read(j, "path_in_3mf", l.path_in_3mf);
|
|
||||||
|
|
||||||
read(j, "depth_mm", l.depth_mm);
|
|
||||||
read(j, "tiling_scale", l.tiling_scale);
|
|
||||||
read(j, "rotation_deg", l.rotation_deg);
|
|
||||||
if (const auto o = j.find("offset"); o != j.end())
|
|
||||||
l.offset = vec2_from_json(*o, l.offset);
|
|
||||||
read(j, "invert", l.invert);
|
|
||||||
read(j, "midlevel", l.midlevel);
|
|
||||||
read(j, "smoothing", l.smoothing);
|
|
||||||
|
|
||||||
read(j, "edge_smoothing", l.edge_smoothing);
|
|
||||||
read(j, "edge_smoothing_amount", l.edge_smoothing_amount);
|
|
||||||
read(j, "auto_connect_islands", l.auto_connect_islands);
|
|
||||||
|
|
||||||
read(j, "tile_enabled", l.tile_enabled);
|
|
||||||
read(j, "color_enabled", l.color_enabled);
|
|
||||||
int tile_method = int(l.tile_method), projection = int(l.projection_method), blend = int(l.blend_mode);
|
|
||||||
read_enum(j, "tile_method", tile_method);
|
|
||||||
read_enum(j, "projection_method", projection);
|
|
||||||
read_enum(j, "blend_mode", blend);
|
|
||||||
l.tile_method = TextureTileMethod(tile_method);
|
|
||||||
l.projection_method = TextureProjectionMethod(std::clamp(projection, 0, 4));
|
|
||||||
l.blend_mode = TextureBlendMode(blend);
|
|
||||||
|
|
||||||
read(j, "lscm_seam_angle_deg", l.lscm_seam_angle_deg);
|
|
||||||
read(j, "island_padding_mm", l.island_padding_mm);
|
|
||||||
|
|
||||||
if (const auto v = j.find("view_project_right"); v != j.end())
|
|
||||||
l.view_project_right = vec3_from_json(*v, l.view_project_right);
|
|
||||||
if (const auto v = j.find("view_project_up"); v != j.end())
|
|
||||||
l.view_project_up = vec3_from_json(*v, l.view_project_up);
|
|
||||||
read(j, "view_project_projective", l.view_project_projective);
|
|
||||||
if (const auto v = j.find("view_project_matrix"); v != j.end() && v->is_array() && v->size() == 12)
|
|
||||||
for (size_t i = 0; i < 12; ++i)
|
|
||||||
l.view_project_matrix[i] = (*v)[i].get<float>();
|
|
||||||
|
|
||||||
if (const auto v = j.find("lscm_seam_edges"); v != j.end() && v->is_array())
|
|
||||||
for (size_t i = 0; i + 1 < v->size(); i += 2)
|
|
||||||
l.lscm_seam_edges.emplace_back((*v)[i].get<int>(), (*v)[i + 1].get<int>());
|
|
||||||
|
|
||||||
if (const auto v = j.find("islands"); v != j.end() && v->is_array())
|
|
||||||
for (const nlohmann::json &e : *v) {
|
|
||||||
if (!e.is_array() || e.size() != 4)
|
|
||||||
continue;
|
|
||||||
TextureIsland island;
|
|
||||||
island.offset = Vec2f(e[0].get<float>(), e[1].get<float>());
|
|
||||||
island.rotation_deg = e[2].get<float>();
|
|
||||||
island.scale = e[3].get<float>();
|
|
||||||
l.islands.push_back(island);
|
|
||||||
}
|
|
||||||
|
|
||||||
read(j, "island_groups", l.island_groups);
|
|
||||||
|
|
||||||
if (const auto v = j.find("lscm_uv_overrides"); v != j.end() && v->is_array())
|
|
||||||
for (const nlohmann::json &e : *v) {
|
|
||||||
if (!e.is_array() || e.size() != 3)
|
|
||||||
continue;
|
|
||||||
l.lscm_uv_overrides.emplace_back(e[0].get<int>(), Vec2f(e[1].get<float>(), e[2].get<float>()));
|
|
||||||
}
|
|
||||||
|
|
||||||
out_layers.push_back(std::move(l));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (const nlohmann::json::exception &) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
layers = std::move(out_layers);
|
|
||||||
options = out_options;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Slic3r
|
} // namespace Slic3r
|
||||||
|
|||||||
@@ -397,41 +397,14 @@ struct TextureDisplacementOptions
|
|||||||
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
|
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
|
||||||
int color_despeckle = 2;
|
int color_despeckle = 2;
|
||||||
|
|
||||||
// Where the panel was left, rather than anything the bake reads. It lives with the volume because
|
|
||||||
// that is what it describes: reopening a project should put the user back on the layer they were
|
|
||||||
// editing, in the mode they were editing it in, not on slot 0 in Standard.
|
|
||||||
int active_slot = 0;
|
|
||||||
int panel_mode = 0; // 0 Standard, 1 Pro
|
|
||||||
|
|
||||||
template<class Archive> void serialize(Archive &ar)
|
template<class Archive> void serialize(Archive &ar)
|
||||||
{
|
{
|
||||||
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
|
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
|
||||||
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
|
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
|
||||||
v2_relocate, v2_flip_edges, color_mix_enabled, color_despeckle, color_mix_count,
|
v2_relocate, color_mix_enabled, color_despeckle, color_mix_count);
|
||||||
active_slot, panel_mode);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Project persistence (see bbs_3mf.cpp). The layer stack and the per-volume options are written to the
|
|
||||||
// .3mf as JSON rather than through the cereal save()/load() above: those two are positional and
|
|
||||||
// unversioned, which is right for the undo/redo stack they serve (one session, one binary) but would
|
|
||||||
// make every future field a project-breaking change. A JSON object tolerates both directions - an
|
|
||||||
// unknown key is ignored, a missing one keeps the member's default - so old projects keep loading and
|
|
||||||
// new ones degrade gracefully in older builds.
|
|
||||||
//
|
|
||||||
// The texture image itself is *not* in here. It is a binary blob that belongs in the archive as a file
|
|
||||||
// of its own, exactly as EmbossShape stores its SVG; `path_in_3mf` names that file, and the caller is
|
|
||||||
// responsible for writing it and for filling `image_data` back in on load.
|
|
||||||
std::string texture_displacement_layers_to_json(const std::vector<TextureDisplacementLayer> &layers,
|
|
||||||
const TextureDisplacementOptions &options);
|
|
||||||
// Returns false and leaves both outputs untouched when the text is not a valid JSON object, a number in a
|
|
||||||
// layer's vector, matrix, seam, island or UV-override array has the wrong type, or a layer's slot is
|
|
||||||
// missing or outside [0, TEXTURE_DISPLACEMENT_MAX_LAYERS). Any other value of the wrong type or shape
|
|
||||||
// keeps its default.
|
|
||||||
bool texture_displacement_layers_from_json(const std::string &text,
|
|
||||||
std::vector<TextureDisplacementLayer> &layers,
|
|
||||||
TextureDisplacementOptions &options);
|
|
||||||
|
|
||||||
// How much detail a height texture carries: central differences of the grey image, the mean gradient
|
// How much detail a height texture carries: central differences of the grey image, the mean gradient
|
||||||
// and the share of texels steeper than 30 grey levels, mapped to how many texels one mesh edge may
|
// and the share of texels steeper than 30 grey levels, mapped to how many texels one mesh edge may
|
||||||
// span (1 for a hard-edged image, 4 for a smooth one). Cached per image, like the decode.
|
// span (1 for a hard-edged image, 4 for a smooth one). Cached per image, like the decode.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma once
|
#ifndef libslic3r_Timer_hpp_
|
||||||
|
#define libslic3r_Timer_hpp_
|
||||||
|
|
||||||
#include "TriangleMesh.hpp"
|
#include "TriangleMesh.hpp"
|
||||||
|
|
||||||
@@ -9,3 +10,5 @@ public:
|
|||||||
static TriangleMesh smooth_triangle_mesh(const TriangleMesh &mesh,bool& ok);
|
static TriangleMesh smooth_triangle_mesh(const TriangleMesh &mesh,bool& ok);
|
||||||
};
|
};
|
||||||
} // namespace Slic3r
|
} // namespace Slic3r
|
||||||
|
|
||||||
|
#endif // libslic3r_Timer_hpp_
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ extern std::string format_memsize_MB(size_t n);
|
|||||||
extern std::string format_memsize(size_t bytes, unsigned int decimals = 1);
|
extern std::string format_memsize(size_t bytes, unsigned int decimals = 1);
|
||||||
// Return string to be added to the boost::log output to inform about the current process memory allocation.
|
// Return string to be added to the boost::log output to inform about the current process memory allocation.
|
||||||
// The string is non-empty if the loglevel >= info (3) or ignore_loglevel==true.
|
// The string is non-empty if the loglevel >= info (3) or ignore_loglevel==true.
|
||||||
|
// Latter is used to get the memory info from SysInfoDialog.
|
||||||
extern std::string log_memory_info(bool ignore_loglevel = false);
|
extern std::string log_memory_info(bool ignore_loglevel = false);
|
||||||
extern void disable_multi_threading();
|
extern void disable_multi_threading();
|
||||||
// Returns the size of physical memory (RAM) in bytes.
|
// Returns the size of physical memory (RAM) in bytes.
|
||||||
|
|||||||
@@ -1744,7 +1744,9 @@ std::string format_diameter_to_str(double diameter, int precision)
|
|||||||
return oss.str();
|
return oss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns platform-specific string to be used as log output.
|
// Returns platform-specific string to be used as log output or parsed in SysInfoDialog.
|
||||||
|
// The latter parses the string with (semi)colons as separators, it should look about as
|
||||||
|
// "desc1: value1; desc2: value2" or similar (spaces should not matter).
|
||||||
std::string log_memory_info(bool ignore_loglevel)
|
std::string log_memory_info(bool ignore_loglevel)
|
||||||
{
|
{
|
||||||
std::string out;
|
std::string out;
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ namespace libvgcode {
|
|||||||
// the triangles that share it: the shader runs 8 times per segment instead of 24.
|
// the triangles that share it: the shader runs 8 times per segment instead of 24.
|
||||||
static constexpr const std::array<uint8_t, 8> VERTEX_DATA = { 0, 1, 2, 3, 4, 5, 6, 7 };
|
static constexpr const std::array<uint8_t, 8> VERTEX_DATA = { 0, 1, 2, 3, 4, 5, 6, 7 };
|
||||||
|
|
||||||
// 16-bit indices: GCN 1/2 GPUs have no 8-bit index support, and some of their drivers
|
static constexpr const std::array<uint8_t, 24> INDEX_DATA = {
|
||||||
// emulate it wrongly in instanced draws.
|
|
||||||
static constexpr const std::array<uint16_t, 24> INDEX_DATA = {
|
|
||||||
0, 1, 2, // front spike
|
0, 1, 2, // front spike
|
||||||
0, 2, 3, // front spike
|
0, 2, 3, // front spike
|
||||||
0, 3, 4, // right/bottom body
|
0, 3, 4, // right/bottom body
|
||||||
@@ -39,7 +37,7 @@ void SegmentTemplate::init()
|
|||||||
if (m_vao_id != 0)
|
if (m_vao_id != 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_size_in_bytes_gpu += VERTEX_DATA.size() * sizeof(uint8_t) + INDEX_DATA.size() * sizeof(uint16_t);
|
m_size_in_bytes_gpu += (VERTEX_DATA.size() + INDEX_DATA.size()) * sizeof(uint8_t);
|
||||||
|
|
||||||
int curr_vertex_array;
|
int curr_vertex_array;
|
||||||
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
|
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
|
||||||
@@ -63,7 +61,7 @@ void SegmentTemplate::init()
|
|||||||
// and restored together with the vao.
|
// and restored together with the vao.
|
||||||
glsafe(glGenBuffers(1, &m_ibo_id));
|
glsafe(glGenBuffers(1, &m_ibo_id));
|
||||||
glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ibo_id));
|
glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ibo_id));
|
||||||
glsafe(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint16_t), INDEX_DATA.data(), GL_STATIC_DRAW));
|
glsafe(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint8_t), INDEX_DATA.data(), GL_STATIC_DRAW));
|
||||||
|
|
||||||
glsafe(glBindBuffer(GL_ARRAY_BUFFER, curr_array_buffer));
|
glsafe(glBindBuffer(GL_ARRAY_BUFFER, curr_array_buffer));
|
||||||
glsafe(glBindVertexArray(curr_vertex_array));
|
glsafe(glBindVertexArray(curr_vertex_array));
|
||||||
@@ -96,7 +94,7 @@ void SegmentTemplate::render(size_t count)
|
|||||||
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
|
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
|
||||||
|
|
||||||
glsafe(glBindVertexArray(m_vao_id));
|
glsafe(glBindVertexArray(m_vao_id));
|
||||||
glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(INDEX_DATA.size()), GL_UNSIGNED_SHORT,
|
glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(INDEX_DATA.size()), GL_UNSIGNED_BYTE,
|
||||||
nullptr, static_cast<GLsizei>(count)));
|
nullptr, static_cast<GLsizei>(count)));
|
||||||
glsafe(glBindVertexArray(curr_vertex_array));
|
glsafe(glBindVertexArray(curr_vertex_array));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -416,6 +416,8 @@ set(SLIC3R_GUI_SOURCES
|
|||||||
GUI/MultiSendMachineModel.hpp
|
GUI/MultiSendMachineModel.hpp
|
||||||
GUI/MultiTaskManagerPage.cpp
|
GUI/MultiTaskManagerPage.cpp
|
||||||
GUI/MultiTaskManagerPage.hpp
|
GUI/MultiTaskManagerPage.hpp
|
||||||
|
GUI/MultiTaskModel.cpp
|
||||||
|
GUI/MultiTaskModel.hpp
|
||||||
GUI/NetworkTestDialog.cpp
|
GUI/NetworkTestDialog.cpp
|
||||||
GUI/NetworkTestDialog.hpp
|
GUI/NetworkTestDialog.hpp
|
||||||
GUI/Notebook.cpp
|
GUI/Notebook.cpp
|
||||||
@@ -800,6 +802,7 @@ set(SLIC3R_GUI_SOURCES
|
|||||||
Utils/PrintHost.hpp
|
Utils/PrintHost.hpp
|
||||||
Utils/Process.cpp
|
Utils/Process.cpp
|
||||||
Utils/Process.hpp
|
Utils/Process.hpp
|
||||||
|
Utils/Profile.hpp
|
||||||
Utils/RaycastManager.cpp
|
Utils/RaycastManager.cpp
|
||||||
Utils/RaycastManager.hpp
|
Utils/RaycastManager.hpp
|
||||||
Utils/Repetier.cpp
|
Utils/Repetier.cpp
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma once
|
#ifndef slic3r_GUI_SingleChoice_hpp_
|
||||||
|
#define slic3r_GUI_SingleChoice_hpp_
|
||||||
|
|
||||||
#include "GUI_Utils.hpp"
|
#include "GUI_Utils.hpp"
|
||||||
#include "Plater.hpp"
|
#include "Plater.hpp"
|
||||||
@@ -32,3 +33,5 @@ private:
|
|||||||
|
|
||||||
};
|
};
|
||||||
}} // namespace Slic3r::GUI
|
}} // namespace Slic3r::GUI
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#include <exception>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include "libslic3r/Exception.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
class ConfigError : public Slic3r::RuntimeError {
|
||||||
|
using Slic3r::RuntimeError::RuntimeError;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace GUI {
|
||||||
|
|
||||||
|
class ConfigGUITypeError : public ConfigError {
|
||||||
|
using ConfigError::ConfigError;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace GUI
|
||||||
|
} // namespace Slic3r
|
||||||
@@ -25,8 +25,9 @@ DockPanel::DockPanel(wxWindow* parent,
|
|||||||
const std::string& html,
|
const std::string& html,
|
||||||
MessageHandler on_message,
|
MessageHandler on_message,
|
||||||
CloseHandler on_close,
|
CloseHandler on_close,
|
||||||
CloseHandler on_destroyed)
|
CloseHandler on_destroyed,
|
||||||
: WebPanel(parent, web_hosting::orca_bridge_script())
|
const wxString& profile)
|
||||||
|
: WebPanel(parent, web_hosting::orca_bridge_script(), profile)
|
||||||
, m_html(html)
|
, m_html(html)
|
||||||
, m_on_message(std::move(on_message))
|
, m_on_message(std::move(on_message))
|
||||||
, m_on_close(std::move(on_close))
|
, m_on_close(std::move(on_close))
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include <wx/string.h>
|
||||||
|
|
||||||
class wxWindow;
|
class wxWindow;
|
||||||
|
|
||||||
namespace Slic3r { namespace GUI {
|
namespace Slic3r { namespace GUI {
|
||||||
@@ -27,7 +29,8 @@ public:
|
|||||||
const std::string& html,
|
const std::string& html,
|
||||||
MessageHandler on_message,
|
MessageHandler on_message,
|
||||||
CloseHandler on_close,
|
CloseHandler on_close,
|
||||||
CloseHandler on_destroyed);
|
CloseHandler on_destroyed,
|
||||||
|
const wxString& profile = wxEmptyString);
|
||||||
~DockPanel() override;
|
~DockPanel() override;
|
||||||
|
|
||||||
// Main thread only.
|
// Main thread only.
|
||||||
|
|||||||
@@ -4862,6 +4862,12 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
|
|||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI exit";
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI exit";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GUI_App::system_info()
|
||||||
|
{
|
||||||
|
//SysInfoDialog dlg;
|
||||||
|
//dlg.ShowModal();
|
||||||
|
}
|
||||||
|
|
||||||
void GUI_App::keyboard_shortcuts(ShortcutContext page, wxWindow* parent)
|
void GUI_App::keyboard_shortcuts(ShortcutContext page, wxWindow* parent)
|
||||||
{
|
{
|
||||||
KBShortcutsDialog dlg(parent != nullptr ? parent : mainframe, page);
|
KBShortcutsDialog dlg(parent != nullptr ? parent : mainframe, page);
|
||||||
|
|||||||
@@ -519,6 +519,7 @@ public:
|
|||||||
void check_printer_presets();
|
void check_printer_presets();
|
||||||
|
|
||||||
void recreate_GUI(const wxString& message);
|
void recreate_GUI(const wxString& message);
|
||||||
|
void system_info();
|
||||||
void keyboard_shortcuts(ShortcutContext page, wxWindow* parent = nullptr); // the main frame when null
|
void keyboard_shortcuts(ShortcutContext page, wxWindow* parent = nullptr); // the main frame when null
|
||||||
void troubleshoot();
|
void troubleshoot();
|
||||||
void load_project(wxWindow *parent, wxString& input_file) const;
|
void load_project(wxWindow *parent, wxString& input_file) const;
|
||||||
|
|||||||
@@ -1036,11 +1036,6 @@ bool GLGizmoPainterBase::on_mouse(const wxMouseEvent &mouse_event)
|
|||||||
Vec2d mouse_pos = mouse_coord.cast<double>();
|
Vec2d mouse_pos = mouse_coord.cast<double>();
|
||||||
|
|
||||||
if (mouse_event.Moving()) {
|
if (mouse_event.Moving()) {
|
||||||
// No button is down, yet a stroke is still open: its release went elsewhere, as a paint drag does not
|
|
||||||
// capture the mouse. End the stroke as that release would have.
|
|
||||||
if (m_button_down != Button::None)
|
|
||||||
gizmo_event(m_button_down == Button::Left ? SLAGizmoEventType::LeftUp : SLAGizmoEventType::RightUp, mouse_pos,
|
|
||||||
mouse_event.ShiftDown(), mouse_event.AltDown(), false);
|
|
||||||
gizmo_event(SLAGizmoEventType::Moving, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false);
|
gizmo_event(SLAGizmoEventType::Moving, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1210,10 +1205,6 @@ void GLGizmoPainterBase::on_set_state()
|
|||||||
m_old_mo_id = -1;
|
m_old_mo_id = -1;
|
||||||
//m_iva.release_geometry();
|
//m_iva.release_geometry();
|
||||||
m_triangle_selectors.clear();
|
m_triangle_selectors.clear();
|
||||||
// A stroke still open goes with the selectors. Its state must not carry into the next session, where the
|
|
||||||
// first move would end it as if released and the first press would paint a line from its last position.
|
|
||||||
m_button_down = Button::None;
|
|
||||||
m_last_mouse_click = Vec2d::Zero();
|
|
||||||
|
|
||||||
//Camera& camera = wxGetApp().plater()->get_camera();
|
//Camera& camera = wxGetApp().plater()->get_camera();
|
||||||
//camera.look_at(camera.get_position(), m_previous_target, Vec3d::UnitZ());
|
//camera.look_at(camera.get_position(), m_previous_target, Vec3d::UnitZ());
|
||||||
|
|||||||
@@ -495,16 +495,6 @@ bool GLGizmoTextureDisplacement::on_init()
|
|||||||
toggle_wireframe_shortcut
|
toggle_wireframe_shortcut
|
||||||
};
|
};
|
||||||
|
|
||||||
// Both ways into the same input box on any slider in the panel: ImGui turns a slider into one when
|
|
||||||
// it is Ctrl+clicked or when it is given the keyboard focus, which is what the edit icon asks for.
|
|
||||||
m_desc["type_value"] = _L("Type an exact value into a slider");
|
|
||||||
m_shortcuts_sliders = {
|
|
||||||
{ctrl + _L("Left mouse button"), m_desc["type_value"]},
|
|
||||||
{_L("Edit icon at a slider's right end"), m_desc["type_value"]},
|
|
||||||
};
|
|
||||||
for (std::vector<std::pair<wxString, wxString>> *list : {&m_shortcuts_brush, &m_shortcuts_bucket_fill, &m_shortcuts_gap_fill})
|
|
||||||
list->insert(list->end(), m_shortcuts_sliders.begin(), m_shortcuts_sliders.end());
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,11 +552,6 @@ void GLGizmoTextureDisplacement::on_shutdown()
|
|||||||
m_shaded_island_delta = Eigen::Matrix<float, 2, 3>::Identity();
|
m_shaded_island_delta = Eigen::Matrix<float, 2, 3>::Identity();
|
||||||
m_island_drag_active = false;
|
m_island_drag_active = false;
|
||||||
m_island_move_set.clear();
|
m_island_move_set.clear();
|
||||||
// The pane closes below, taking the highlight with it, and the view must not open the next session in a
|
|
||||||
// Fast the user never picked. Set directly: nothing is to be queued against a gizmo that is closing.
|
|
||||||
if (m_island_view.switched)
|
|
||||||
m_use_shaded_preview = false;
|
|
||||||
m_island_view = IslandViewFollow{};
|
|
||||||
m_adjust_texture_mode = false;
|
m_adjust_texture_mode = false;
|
||||||
m_seam_edit_mode = false;
|
m_seam_edit_mode = false;
|
||||||
m_adjust_drag_handle = AdjustHandle::None;
|
m_adjust_drag_handle = AdjustHandle::None;
|
||||||
@@ -732,14 +717,19 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
|
|||||||
|
|
||||||
// The UV editor's island selection, shown on the model. Polled here rather than pushed: the pane
|
// The UV editor's island selection, shown on the model. Polled here rather than pushed: the pane
|
||||||
// changes its selection in its own mouse handling, and a compare of a few ints per frame is free.
|
// changes its selection in its own mouse handling, and a compare of a few ints per frame is free.
|
||||||
if (const std::vector<int> *islands = island_highlight_selection()) {
|
{
|
||||||
if (*islands != m_island_overlay_selection)
|
const TextureDisplacementLayer *al = active_layer();
|
||||||
rebuild_island_overlay(*islands);
|
const UVEditorCanvas *uv_canvas = wxGetApp().plater()->get_uv_editor_canvas();
|
||||||
|
if (m_show_uv_editor && al != nullptr && al->projection_method == TextureProjectionMethod::LSCM &&
|
||||||
|
uv_canvas != nullptr && !m_uv_editor_unwrap.empty()) {
|
||||||
|
if (uv_canvas->selected_islands() != m_island_overlay_selection)
|
||||||
|
rebuild_island_overlay(uv_canvas->selected_islands());
|
||||||
render_island_overlay();
|
render_island_overlay();
|
||||||
} else if (m_island_overlay_glmodel.is_initialized()) {
|
} else if (m_island_overlay_glmodel.is_initialized()) {
|
||||||
m_island_overlay_glmodel.reset();
|
m_island_overlay_glmodel.reset();
|
||||||
m_island_overlay_selection.clear();
|
m_island_overlay_selection.clear();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Diagnostic overlays, drawn on top of whatever preview is active (both pull toward the camera
|
// Diagnostic overlays, drawn on top of whatever preview is active (both pull toward the camera
|
||||||
// with a polygon offset so they win the depth test against the coincident surface).
|
// with a polygon offset so they win the depth test against the coincident surface).
|
||||||
@@ -1520,7 +1510,7 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
|
|||||||
// is box-filtered down to 128 px for a ~48 px row, and feeding it to the shader cost the preview
|
// is box-filtered down to 128 px for a ~48 px row, and feeding it to the shader cost the preview
|
||||||
// three quarters of the height map's detail - and, since height_tex_texel is derived from it, also
|
// three quarters of the height map's detail - and, since height_tex_texel is derived from it, also
|
||||||
// flattened the shading gradient and made the parallax march skip itself at angles where it should
|
// flattened the shading gradient and made the parallax march skip itself at angles where it should
|
||||||
// run. Cached per image_data and smoothing, so no PNG is decoded per frame.
|
// run. Cached on the image_data pointer + smoothing, so no PNG is decoded per frame.
|
||||||
GLTexture *tex = get_layer_height_texture(*layer);
|
GLTexture *tex = get_layer_height_texture(*layer);
|
||||||
if (tex == nullptr || tex->get_width() <= 0 || tex->get_height() <= 0)
|
if (tex == nullptr || tex->get_width() <= 0 || tex->get_height() <= 0)
|
||||||
return;
|
return;
|
||||||
@@ -1926,16 +1916,6 @@ void GLGizmoTextureDisplacement::render_paint_overlay(GLModel &overlay)
|
|||||||
shader->stop_using();
|
shader->stop_using();
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<int> *GLGizmoTextureDisplacement::island_highlight_selection()
|
|
||||||
{
|
|
||||||
const TextureDisplacementLayer *al = active_layer();
|
|
||||||
const UVEditorCanvas *uv_canvas = wxGetApp().plater()->get_uv_editor_canvas();
|
|
||||||
if (m_show_uv_editor && al != nullptr && al->projection_method == TextureProjectionMethod::LSCM &&
|
|
||||||
uv_canvas != nullptr && !m_uv_editor_unwrap.empty())
|
|
||||||
return &uv_canvas->selected_islands();
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void GLGizmoTextureDisplacement::rebuild_island_overlay(const std::vector<int> &selection)
|
void GLGizmoTextureDisplacement::rebuild_island_overlay(const std::vector<int> &selection)
|
||||||
{
|
{
|
||||||
m_island_overlay_glmodel.reset();
|
m_island_overlay_glmodel.reset();
|
||||||
@@ -2661,37 +2641,6 @@ void GLGizmoTextureDisplacement::apply_view_mode(int mode)
|
|||||||
m_parent.set_as_dirty();
|
m_parent.set_as_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
int GLGizmoTextureDisplacement::view_mode() const
|
|
||||||
{
|
|
||||||
return m_use_shaded_preview ? 1 :
|
|
||||||
m_uv_check_mode == UVCheckMode::Checker ? 2 :
|
|
||||||
m_uv_check_mode == UVCheckMode::Distortion ? 3 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int GLGizmoTextureDisplacement::IslandViewFollow::step(bool shown_now, int view, bool debug_open)
|
|
||||||
{
|
|
||||||
shown = shown_now;
|
|
||||||
if (!shown) {
|
|
||||||
// The view is back to Normal already if a debug stage took it, and Normal applied over a stage would
|
|
||||||
// queue a preview job that replaces the stage being inspected.
|
|
||||||
const bool restore = switched && view == 1;
|
|
||||||
switched = keep_normal = false;
|
|
||||||
return restore ? 0 : -1;
|
|
||||||
}
|
|
||||||
if (view != 0 || keep_normal || debug_open)
|
|
||||||
return -1;
|
|
||||||
switched = true;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void GLGizmoTextureDisplacement::follow_island_highlight()
|
|
||||||
{
|
|
||||||
const std::vector<int> *islands = island_highlight_selection();
|
|
||||||
const int view = m_island_view.step(islands != nullptr && !islands->empty(), view_mode(), m_debug_stage >= 0);
|
|
||||||
if (view >= 0)
|
|
||||||
apply_view_mode(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GLGizmoTextureDisplacement::push_uv_pane_state()
|
void GLGizmoTextureDisplacement::push_uv_pane_state()
|
||||||
{
|
{
|
||||||
UVEditorCanvas *canvas = wxGetApp().plater()->get_uv_editor_canvas();
|
UVEditorCanvas *canvas = wxGetApp().plater()->get_uv_editor_canvas();
|
||||||
@@ -2932,8 +2881,7 @@ void GLGizmoTextureDisplacement::run_uv_command(int cmd, float value)
|
|||||||
rebuild_preview();
|
rebuild_preview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// FrameAll/ToggleSnap are handled inside the canvas; ProjectFromView is not wired yet. SelectionChanged
|
// FrameAll/ToggleSnap are handled inside the canvas; ProjectFromView is not wired yet.
|
||||||
// only asks for this frame: follow_island_highlight() and the overlay read the selection back themselves.
|
|
||||||
m_parent.set_as_dirty();
|
m_parent.set_as_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3610,7 +3558,6 @@ const ModelVolume* GLGizmoTextureDisplacement::texture_volume() const
|
|||||||
|
|
||||||
void GLGizmoTextureDisplacement::update_model_object()
|
void GLGizmoTextureDisplacement::update_model_object()
|
||||||
{
|
{
|
||||||
store_panel_state();
|
|
||||||
bool updated = false;
|
bool updated = false;
|
||||||
ModelObject *mo = m_c->selection_info()->model_object();
|
ModelObject *mo = m_c->selection_info()->model_object();
|
||||||
int idx = -1;
|
int idx = -1;
|
||||||
@@ -3646,24 +3593,6 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
|
|||||||
{
|
{
|
||||||
wxBusyCursor wait;
|
wxBusyCursor wait;
|
||||||
|
|
||||||
// Opening the gizmo puts the panel back where it was left: the layer and the mode come off the
|
|
||||||
// volume, because they are the user's place in this model's stack, and the dock state off the
|
|
||||||
// application config, because it is about their window. Only on open - this also runs on an
|
|
||||||
// undo/redo and on a layer switch, where overriding what the user is doing now would be wrong.
|
|
||||||
if (first_update) {
|
|
||||||
if (const ModelVolume *mv = texture_volume(); mv != nullptr) {
|
|
||||||
const TextureDisplacementOptions &opts = mv->texture_displacement_options;
|
|
||||||
m_panel_mode = std::clamp(opts.panel_mode, 0, 1);
|
|
||||||
// Only if that slot still holds a layer: the stack can have been edited by anything that
|
|
||||||
// does not go through the panel, and a selection pointing at nothing shows an empty panel.
|
|
||||||
const auto &stack = mv->texture_displacement_layers;
|
|
||||||
if (std::any_of(stack.begin(), stack.end(),
|
|
||||||
[&opts](const TextureDisplacementLayer &l) { return l.slot == opts.active_slot; }))
|
|
||||||
m_active_layer_slot = opts.active_slot;
|
|
||||||
}
|
|
||||||
m_undocked = wxGetApp().app_config->get_bool(UNDOCKED_CONFIG_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModelObject *mo = m_c->selection_info()->model_object();
|
const ModelObject *mo = m_c->selection_info()->model_object();
|
||||||
m_triangle_selectors.clear();
|
m_triangle_selectors.clear();
|
||||||
// The base keeps the last mesh a fill tool hovered, and render_painter_gizmo() reads it as a hover that is
|
// The base keeps the last mesh a fill tool hovered, and render_painter_gizmo() reads it as a hover that is
|
||||||
@@ -3712,14 +3641,6 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
|
|||||||
rebuild_preview();
|
rebuild_preview();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLGizmoTextureDisplacement::store_panel_state()
|
|
||||||
{
|
|
||||||
if (ModelVolume *mv = texture_volume(); mv != nullptr) {
|
|
||||||
mv->texture_displacement_options.active_slot = m_active_layer_slot;
|
|
||||||
mv->texture_displacement_options.panel_mode = m_panel_mode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GLGizmoTextureDisplacement::set_active_layer(int slot)
|
void GLGizmoTextureDisplacement::set_active_layer(int slot)
|
||||||
{
|
{
|
||||||
if (slot == m_active_layer_slot)
|
if (slot == m_active_layer_slot)
|
||||||
@@ -3728,7 +3649,6 @@ void GLGizmoTextureDisplacement::set_active_layer(int slot)
|
|||||||
// reflect - otherwise they would be silently lost.
|
// reflect - otherwise they would be silently lost.
|
||||||
update_model_object();
|
update_model_object();
|
||||||
m_active_layer_slot = slot;
|
m_active_layer_slot = slot;
|
||||||
store_panel_state();
|
|
||||||
update_from_model_object(false);
|
update_from_model_object(false);
|
||||||
// The on-canvas gizmo (if on) is anchored to whichever layer is active - keep it in sync
|
// The on-canvas gizmo (if on) is anchored to whichever layer is active - keep it in sync
|
||||||
// instead of leaving it pointing at the previous layer's (now stale) paint patch.
|
// instead of leaving it pointing at the previous layer's (now stale) paint patch.
|
||||||
@@ -3760,8 +3680,6 @@ void GLGizmoTextureDisplacement::ensure_panel_icons()
|
|||||||
"texture_displacement_adjust.svg", "canvas_drag.svg", "texture_displacement_move_up.svg",
|
"texture_displacement_adjust.svg", "canvas_drag.svg", "texture_displacement_move_up.svg",
|
||||||
"texture_displacement_move_down.svg", "texture_displacement_drag.svg",
|
"texture_displacement_move_down.svg", "texture_displacement_drag.svg",
|
||||||
"texture_displacement_select_all.svg", "texture_displacement_erase_all.svg",
|
"texture_displacement_select_all.svg", "texture_displacement_erase_all.svg",
|
||||||
// The "type a value instead" icon at the right end of every slider.
|
|
||||||
"edit.svg",
|
|
||||||
};
|
};
|
||||||
std::vector<std::string> paths;
|
std::vector<std::string> paths;
|
||||||
paths.reserve(names.size());
|
paths.reserve(names.size());
|
||||||
@@ -4080,7 +3998,7 @@ void GLGizmoTextureDisplacement::remove_texture_layer(int slot)
|
|||||||
if (slot >= 0 && slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS)) {
|
if (slot >= 0 && slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS)) {
|
||||||
mv->texture_displacement_facet(slot).reset();
|
mv->texture_displacement_facet(slot).reset();
|
||||||
m_thumbnails[size_t(slot)].reset();
|
m_thumbnails[size_t(slot)].reset();
|
||||||
m_thumbnail_source[size_t(slot)].reset();
|
m_thumbnail_source[size_t(slot)] = nullptr;
|
||||||
m_layer_expanded[size_t(slot)] = false;
|
m_layer_expanded[size_t(slot)] = false;
|
||||||
}
|
}
|
||||||
if (m_picker_slot == slot)
|
if (m_picker_slot == slot)
|
||||||
@@ -4255,7 +4173,7 @@ int GLGizmoTextureDisplacement::select_visible_faces(const std::array<float, 12>
|
|||||||
selector.reset();
|
selector.reset();
|
||||||
for (unsigned i : unobscured)
|
for (unsigned i : unobscured)
|
||||||
selector.set_facet(int(front_facing[i]), EnforcerBlockerType::ENFORCER);
|
selector.set_facet(int(front_facing[i]), EnforcerBlockerType::ENFORCER);
|
||||||
selector.request_update_render_data(true);
|
selector.request_update_render_data();
|
||||||
|
|
||||||
update_model_object();
|
update_model_object();
|
||||||
m_parent.set_as_dirty();
|
m_parent.set_as_dirty();
|
||||||
@@ -4278,7 +4196,7 @@ void GLGizmoTextureDisplacement::select_whole_model()
|
|||||||
const size_t facet_count = v->mesh().its.indices.size();
|
const size_t facet_count = v->mesh().its.indices.size();
|
||||||
for (size_t i = 0; i < facet_count; ++i)
|
for (size_t i = 0; i < facet_count; ++i)
|
||||||
m_triangle_selectors[idx]->set_facet(int(i), EnforcerBlockerType::ENFORCER);
|
m_triangle_selectors[idx]->set_facet(int(i), EnforcerBlockerType::ENFORCER);
|
||||||
m_triangle_selectors[idx]->request_update_render_data(true);
|
m_triangle_selectors[idx]->request_update_render_data();
|
||||||
}
|
}
|
||||||
update_model_object();
|
update_model_object();
|
||||||
m_parent.set_as_dirty();
|
m_parent.set_as_dirty();
|
||||||
@@ -5384,7 +5302,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_thumbnail(const TextureDisplace
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
const size_t slot = size_t(layer.slot);
|
const size_t slot = size_t(layer.slot);
|
||||||
if (m_thumbnails[slot] && m_thumbnail_source[slot].lock() == layer.image_data &&
|
if (m_thumbnails[slot] && m_thumbnail_source[slot] == layer.image_data.get() &&
|
||||||
m_thumbnail_smoothing[slot] == layer.smoothing)
|
m_thumbnail_smoothing[slot] == layer.smoothing)
|
||||||
return m_thumbnails[slot].get();
|
return m_thumbnails[slot].get();
|
||||||
|
|
||||||
@@ -5397,7 +5315,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_thumbnail(const TextureDisplace
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
m_thumbnails[slot] = std::move(texture);
|
m_thumbnails[slot] = std::move(texture);
|
||||||
m_thumbnail_source[slot] = layer.image_data;
|
m_thumbnail_source[slot] = layer.image_data.get();
|
||||||
m_thumbnail_smoothing[slot] = layer.smoothing;
|
m_thumbnail_smoothing[slot] = layer.smoothing;
|
||||||
return m_thumbnails[slot].get();
|
return m_thumbnails[slot].get();
|
||||||
}
|
}
|
||||||
@@ -5406,17 +5324,17 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_color_texture(const TextureDisp
|
|||||||
{
|
{
|
||||||
if (layer.empty() || !layer.color_enabled)
|
if (layer.empty() || !layer.color_enabled)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
if (m_color_tex && m_color_tex_source.lock() == layer.image_data && m_color_tex_smoothing == layer.smoothing)
|
if (m_color_tex && m_color_tex_source == layer.image_data.get() && m_color_tex_smoothing == layer.smoothing)
|
||||||
return m_color_tex.get();
|
return m_color_tex.get();
|
||||||
|
|
||||||
std::unique_ptr<GLTexture> texture = upload_color_texture(decode_height_texture(layer), HEIGHT_TEX_MAX_PX);
|
std::unique_ptr<GLTexture> texture = upload_color_texture(decode_height_texture(layer), HEIGHT_TEX_MAX_PX);
|
||||||
if (!texture) {
|
if (!texture) {
|
||||||
m_color_tex.reset();
|
m_color_tex.reset();
|
||||||
m_color_tex_source.reset();
|
m_color_tex_source = nullptr;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
m_color_tex = std::move(texture);
|
m_color_tex = std::move(texture);
|
||||||
m_color_tex_source = layer.image_data;
|
m_color_tex_source = layer.image_data.get();
|
||||||
m_color_tex_smoothing = layer.smoothing;
|
m_color_tex_smoothing = layer.smoothing;
|
||||||
return m_color_tex.get();
|
return m_color_tex.get();
|
||||||
}
|
}
|
||||||
@@ -5428,7 +5346,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_height_texture(const TextureDis
|
|||||||
|
|
||||||
// One slot, not one per layer: the preview shader only ever shades the *active* layer, so a single
|
// One slot, not one per layer: the preview shader only ever shades the *active* layer, so a single
|
||||||
// full-resolution upload is enough and the VRAM cost stays at one texture rather than eight.
|
// full-resolution upload is enough and the VRAM cost stays at one texture rather than eight.
|
||||||
if (m_height_tex && m_height_tex_source.lock() == layer.image_data && m_height_tex_smoothing == layer.smoothing)
|
if (m_height_tex && m_height_tex_source == layer.image_data.get() && m_height_tex_smoothing == layer.smoothing)
|
||||||
return m_height_tex.get();
|
return m_height_tex.get();
|
||||||
|
|
||||||
std::unique_ptr<GLTexture> texture = upload_height_thumbnail(decode_height_texture(layer), HEIGHT_TEX_MAX_PX);
|
std::unique_ptr<GLTexture> texture = upload_height_thumbnail(decode_height_texture(layer), HEIGHT_TEX_MAX_PX);
|
||||||
@@ -5436,7 +5354,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_height_texture(const TextureDis
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
m_height_tex = std::move(texture);
|
m_height_tex = std::move(texture);
|
||||||
m_height_tex_source = layer.image_data;
|
m_height_tex_source = layer.image_data.get();
|
||||||
m_height_tex_smoothing = layer.smoothing;
|
m_height_tex_smoothing = layer.smoothing;
|
||||||
return m_height_tex.get();
|
return m_height_tex.get();
|
||||||
}
|
}
|
||||||
@@ -5877,9 +5795,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
GizmoImguiBegin(get_name(), flags);
|
GizmoImguiBegin(get_name(), flags);
|
||||||
ensure_panel_icons();
|
ensure_panel_icons();
|
||||||
process_uv_commands(); // clicks from the UV editor pane, run here where the GL context is current
|
process_uv_commands(); // clicks from the UV editor pane, run here where the GL context is current
|
||||||
// After the pane's commands, which can close it or change the view, and before the View row, so the row
|
|
||||||
// lights the view this frame ends up in and a click on it has the last word.
|
|
||||||
follow_island_highlight();
|
|
||||||
const float previous_body_h = m_panel_body_h;
|
const float previous_body_h = m_panel_body_h;
|
||||||
const float previous_footer_h = m_panel_footer_h;
|
const float previous_footer_h = m_panel_footer_h;
|
||||||
|
|
||||||
@@ -6004,8 +5919,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
}
|
}
|
||||||
return s + "...";
|
return s + "...";
|
||||||
};
|
};
|
||||||
// Label column + a slider filling the rest of the row, value printed inside the track.
|
// Label column + a slider filling the rest of the row, value printed inside the track. Ctrl+click on
|
||||||
// `right_inset` keeps a layer card's inner padding.
|
// any of these types a value in. `right_inset` keeps a layer card's inner padding.
|
||||||
const auto slider_label = [&](const wxString &label) {
|
const auto slider_label = [&](const wxString &label) {
|
||||||
const float x0 = ImGui::GetCursorPosX();
|
const float x0 = ImGui::GetCursorPosX();
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
@@ -6013,107 +5928,17 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- Typed values on sliders ----
|
|
||||||
// Every slider in the panel goes through typed_slider(), so all of them take a number the same
|
|
||||||
// two ways: Ctrl+click on the track, or the edit icon at the right end for the same box without
|
|
||||||
// the modifier. Both end up in ImGui's own "temp input" mode - the icon only asks ImGui to focus
|
|
||||||
// the slider, which is what turns it into an input box (see SliderScalar) - so the parsing,
|
|
||||||
// the clamping and the Enter/click-away behaviour are ImGui's, not a second implementation.
|
|
||||||
|
|
||||||
// Retires a click whose row is no longer being drawn - the panel changed mode in between - so that
|
|
||||||
// it cannot fire later when that row comes back. Before any row is drawn, so a live one still wins.
|
|
||||||
if (!m_slider_type_request.empty() && ImGui::GetFrameCount() > m_slider_type_request_frame + 1)
|
|
||||||
m_slider_type_request.clear();
|
|
||||||
|
|
||||||
// Exactly as tall as the slider it sits beside, so it lines up with the track without the row
|
|
||||||
// having to be re-centred around it; icon_button() pads the glyph itself down to about 0.7 of this.
|
|
||||||
const float edit_icon_w = frame_h;
|
|
||||||
// What the slider's own width has to give up for the icon beside it.
|
|
||||||
const float edit_icon_room = edit_icon_w + gap_s;
|
|
||||||
|
|
||||||
// One end of a range, through the slider's own format, so the bounds in the tooltip read exactly
|
|
||||||
// like the value in the track. A parenthesised note in the format - "(auto)" - is dropped: it
|
|
||||||
// describes the current state, not the bound.
|
|
||||||
const auto fmt_bound = [](const char *format, auto v) {
|
|
||||||
char buf[64];
|
|
||||||
std::snprintf(buf, sizeof(buf), format, v);
|
|
||||||
std::string out(buf);
|
|
||||||
const size_t paren = out.find('(');
|
|
||||||
if (paren != std::string::npos)
|
|
||||||
out.erase(paren);
|
|
||||||
while (!out.empty() && out.back() == ' ')
|
|
||||||
out.pop_back();
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
const auto range_text = [](const std::string &lo, const std::string &hi) {
|
|
||||||
return Slic3r::format(_u8L("Range: %1% to %2%"), lo, hi);
|
|
||||||
};
|
|
||||||
const auto edit_icon_tip = [&](const std::string &range) {
|
|
||||||
return _u8L("Type a value instead of dragging. Ctrl+click on the slider does the same.") + "\n\n" + range;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Draws `slider` at `item_w` (SetNextItemWidth's convention: negative measures back from the right
|
|
||||||
// edge, and the caller has already left edit_icon_room in it), then the edit icon. `tip` and the
|
|
||||||
// range go on both. Returns whatever the slider returned.
|
|
||||||
const auto typed_row = [&](const char *id, float item_w, const wxString &tip, const std::string &range,
|
|
||||||
const std::function<bool()> &slider) -> bool {
|
|
||||||
// Deferred from the click a frame ago: SetKeyboardFocusHere() is a request for the *next*
|
|
||||||
// frame, so it has to be issued before the slider it names, not from the icon beside it.
|
|
||||||
if (m_slider_type_request == id) {
|
|
||||||
ImGui::SetKeyboardFocusHere();
|
|
||||||
m_slider_type_request.clear();
|
|
||||||
m_parent.set_as_dirty(); // the focus only lands on a frame that actually gets drawn
|
|
||||||
}
|
|
||||||
ImGui::SetNextItemWidth(item_w);
|
|
||||||
const bool changed = slider();
|
|
||||||
// AllowWhenDisabled: several of these rows are drawn inside disabled_begin(), and a greyed
|
|
||||||
// slider is exactly when the user needs to be told what it is and why it is not moving.
|
|
||||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
|
||||||
wxString full = tip;
|
|
||||||
if (!full.IsEmpty())
|
|
||||||
full += "\n\n";
|
|
||||||
m_imgui->tooltip(full + from_u8(range), wrap_w);
|
|
||||||
}
|
|
||||||
// SameLine() puts the cursor back at the slider's own top (ItemSize records the Y the item was
|
|
||||||
// drawn at), so an icon of the same height lands on the track wherever the row placed it.
|
|
||||||
ImGui::SameLine(0.f, gap_s);
|
|
||||||
if (icon_button(int(ImGui::GetID(id)), "edit.svg", edit_icon_w, _L("Edit"), from_u8(edit_icon_tip(range)))) {
|
|
||||||
m_slider_type_request = id;
|
|
||||||
m_slider_type_request_frame = ImGui::GetFrameCount();
|
|
||||||
m_parent.set_as_dirty(); // the next frame is where the request above is turned into focus
|
|
||||||
}
|
|
||||||
return changed;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The two forms every slider in the panel is one of. They derive the range from the bounds they are
|
|
||||||
// already given, so no row states its bounds or its format twice and the tooltip cannot drift from
|
|
||||||
// the track. `item_w` is as typed_row() takes it.
|
|
||||||
const auto typed_slider = [&](const char *id, float item_w, float *v, float v_min, float v_max, const char *format,
|
|
||||||
bool log, const wxString &tip) -> bool {
|
|
||||||
return typed_row(id, item_w, tip, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
|
|
||||||
return ImGui::SliderFloat(id, v, v_min, v_max, format,
|
|
||||||
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const auto typed_slider_int = [&](const char *id, float item_w, int *v, int v_min, int v_max, const char *format,
|
|
||||||
bool log, const wxString &tip) -> bool {
|
|
||||||
return typed_row(id, item_w, tip, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
|
|
||||||
return ImGui::SliderInt(id, v, v_min, v_max, format,
|
|
||||||
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Label column + the slider filling the rest of the row.
|
|
||||||
const auto float_row = [&](const char *id, const wxString &label, float *v, float v_min, float v_max, const char *format,
|
const auto float_row = [&](const char *id, const wxString &label, float *v, float v_min, float v_max, const char *format,
|
||||||
bool log, float right_inset, const wxString &tip) -> bool {
|
bool log, float right_inset) -> bool {
|
||||||
slider_label(label);
|
slider_label(label);
|
||||||
return typed_slider(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, log, tip);
|
ImGui::SetNextItemWidth(-std::max(right_inset, 1.f));
|
||||||
|
return ImGui::SliderFloat(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
|
||||||
};
|
};
|
||||||
const auto int_row = [&](const char *id, const wxString &label, int *v, int v_min, int v_max, const char *format,
|
const auto int_row = [&](const char *id, const wxString &label, int *v, int v_min, int v_max, const char *format,
|
||||||
float right_inset, const wxString &tip) -> bool {
|
float right_inset) -> bool {
|
||||||
slider_label(label);
|
slider_label(label);
|
||||||
return typed_slider_int(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, false, tip);
|
ImGui::SetNextItemWidth(-std::max(right_inset, 1.f));
|
||||||
|
return ImGui::SliderInt(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp);
|
||||||
};
|
};
|
||||||
const auto layer_name = [](const TextureDisplacementLayer &l) {
|
const auto layer_name = [](const TextureDisplacementLayer &l) {
|
||||||
return l.name.empty() ? Slic3r::format(_u8L("Layer %1%"), l.slot + 1) : l.name;
|
return l.name.empty() ? Slic3r::format(_u8L("Layer %1%"), l.slot + 1) : l.name;
|
||||||
@@ -6161,7 +5986,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
"not what you wanted and you know why."));
|
"not what you wanted and you know why."));
|
||||||
if (clicked && !on) {
|
if (clicked && !on) {
|
||||||
m_panel_mode = m;
|
m_panel_mode = m;
|
||||||
store_panel_state();
|
|
||||||
if (!pro_mode()) {
|
if (!pro_mode()) {
|
||||||
// Leaving the subdivision preview open would strand a wireframe whose controls just
|
// Leaving the subdivision preview open would strand a wireframe whose controls just
|
||||||
// disappeared, so close it as part of the switch.
|
// disappeared, so close it as part of the switch.
|
||||||
@@ -6184,7 +6008,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
_L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it back beside "
|
_L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it back beside "
|
||||||
"the toolbar.")))
|
"the toolbar.")))
|
||||||
m_undocked = !m_undocked;
|
m_undocked = !m_undocked;
|
||||||
wxGetApp().app_config->set_bool(UNDOCKED_CONFIG_KEY, m_undocked);
|
|
||||||
}
|
}
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
@@ -6326,7 +6149,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
if (v->is_model_part()) {
|
if (v->is_model_part()) {
|
||||||
++idx;
|
++idx;
|
||||||
m_triangle_selectors[idx]->reset();
|
m_triangle_selectors[idx]->reset();
|
||||||
m_triangle_selectors[idx]->request_update_render_data(true);
|
m_triangle_selectors[idx]->request_update_render_data();
|
||||||
}
|
}
|
||||||
update_model_object();
|
update_model_object();
|
||||||
m_parent.set_as_dirty();
|
m_parent.set_as_dirty();
|
||||||
@@ -6337,10 +6160,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
const auto centre_on_row = [&](float h) { ImGui::SetCursorPosY(row_y + std::round((icon_md - h) * 0.5f)); };
|
const auto centre_on_row = [&](float h) { ImGui::SetCursorPosY(row_y + std::round((icon_md - h) * 0.5f)); };
|
||||||
vsep(icon_md);
|
vsep(icon_md);
|
||||||
if (is_brush_mode) {
|
if (is_brush_mode) {
|
||||||
|
ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm)));
|
||||||
centre_on_row(frame_h);
|
centre_on_row(frame_h);
|
||||||
typed_slider("##cursor_radius",
|
ImGui::SliderFloat("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm",
|
||||||
std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm) - edit_icon_room),
|
ImGuiSliderFlags_AlwaysClamp);
|
||||||
&m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm", false, m_desc.at("cursor_size"));
|
hover_tip(m_desc.at("cursor_size"));
|
||||||
vsep(icon_sm);
|
vsep(icon_sm);
|
||||||
const bool is_circle = m_cursor_type == TriangleSelector::CursorType::CIRCLE;
|
const bool is_circle = m_cursor_type == TriangleSelector::CursorType::CIRCLE;
|
||||||
centre_on_row(icon_sm);
|
centre_on_row(icon_sm);
|
||||||
@@ -6353,10 +6177,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
_L("Sphere - paints only within a ball around the point under the cursor")))
|
_L("Sphere - paints only within a ball around the point under the cursor")))
|
||||||
m_cursor_type = TriangleSelector::CursorType::SPHERE;
|
m_cursor_type = TriangleSelector::CursorType::SPHERE;
|
||||||
} else if (is_area_mode) {
|
} else if (is_area_mode) {
|
||||||
|
ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX()));
|
||||||
centre_on_row(frame_h);
|
centre_on_row(frame_h);
|
||||||
typed_slider("##smart_fill_angle", std::max(1.f, row_end - ImGui::GetCursorPosX() - edit_icon_room),
|
ImGui::SliderFloat("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°",
|
||||||
&m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", false,
|
ImGuiSliderFlags_AlwaysClamp);
|
||||||
_L("Angle threshold - the fill stops at edges sharper than this"));
|
hover_tip(_u8L("Angle threshold - the fill stops at edges sharper than this"));
|
||||||
} else {
|
} else {
|
||||||
centre_on_row(frame_h);
|
centre_on_row(frame_h);
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
@@ -6368,9 +6193,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
// ---- View: Normal / Fast / Checker / Distortion as one group, Wireframe on its own ----
|
// ---- View: Normal / Fast / Checker / Distortion as one group, Wireframe on its own ----
|
||||||
// The underlying state stays m_use_shaded_preview + m_uv_check_mode.
|
// The underlying state stays m_use_shaded_preview + m_uv_check_mode.
|
||||||
{
|
{
|
||||||
const int cur_mode = view_mode();
|
const int cur_mode = m_use_shaded_preview ? 1 :
|
||||||
|
m_uv_check_mode == UVCheckMode::Checker ? 2 :
|
||||||
|
m_uv_check_mode == UVCheckMode::Distortion ? 3 : 0;
|
||||||
int new_mode = cur_mode;
|
int new_mode = cur_mode;
|
||||||
int picked = -1; // the button clicked, the active one included
|
|
||||||
bool wf_toggle = false;
|
bool wf_toggle = false;
|
||||||
bool open_uv_editor = false;
|
bool open_uv_editor = false;
|
||||||
// Checker and Distortion both draw *the unwrap* - the first the texture grid laid over it, the second
|
// Checker and Distortion both draw *the unwrap* - the first the texture grid laid over it, the second
|
||||||
@@ -6393,19 +6219,19 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + m_imgui->scaled(2.6f)));
|
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + m_imgui->scaled(2.6f)));
|
||||||
if (icon_toggle(701, "texture_displacement_real_preview.svg", cur_mode == 0, icon_md, _L("Normal"),
|
if (icon_toggle(701, "texture_displacement_real_preview.svg", cur_mode == 0, icon_md, _L("Normal"),
|
||||||
_L("Normal - the real geometry, exactly what Bake will produce. Slower to update")))
|
_L("Normal - the real geometry, exactly what Bake will produce. Slower to update")))
|
||||||
new_mode = picked = 0;
|
new_mode = 0;
|
||||||
ImGui::SameLine(0.f, gap_s);
|
ImGui::SameLine(0.f, gap_s);
|
||||||
if (icon_toggle(702, "texture_displacement_fast_preview.svg", cur_mode == 1, icon_md, _L("Fast"),
|
if (icon_toggle(702, "texture_displacement_fast_preview.svg", cur_mode == 1, icon_md, _L("Fast"),
|
||||||
_L("Fast - the relief is only shaded on, not built, and only for the layer you are "
|
_L("Fast - the relief is only shaded on, not built, and only for the layer you are "
|
||||||
"editing. Updates instantly while you paint")))
|
"editing. Updates instantly while you paint")))
|
||||||
new_mode = picked = 1;
|
new_mode = 1;
|
||||||
ImGui::SameLine(0.f, gap_s);
|
ImGui::SameLine(0.f, gap_s);
|
||||||
if (icon_toggle(703, "texture_displacement_checker.svg", cur_mode == 2, icon_md, _L("Checker"),
|
if (icon_toggle(703, "texture_displacement_checker.svg", cur_mode == 2, icon_md, _L("Checker"),
|
||||||
_L("Checker - a test grid instead of the texture. Where the squares stay square the "
|
_L("Checker - a test grid instead of the texture. Where the squares stay square the "
|
||||||
"texture is undistorted; where they stretch, it will too. Opens the UV editor if "
|
"texture is undistorted; where they stretch, it will too. Opens the UV editor if "
|
||||||
"it is closed"),
|
"it is closed"),
|
||||||
uv_view_na)) {
|
uv_view_na)) {
|
||||||
new_mode = picked = 2;
|
new_mode = 2;
|
||||||
open_uv_editor = true;
|
open_uv_editor = true;
|
||||||
}
|
}
|
||||||
ImGui::SameLine(0.f, gap_s);
|
ImGui::SameLine(0.f, gap_s);
|
||||||
@@ -6413,7 +6239,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
_L("Distortion - blue-to-red stretch heatmap over the unwrap. Opens the UV editor if "
|
_L("Distortion - blue-to-red stretch heatmap over the unwrap. Opens the UV editor if "
|
||||||
"it is closed"),
|
"it is closed"),
|
||||||
uv_view_na)) {
|
uv_view_na)) {
|
||||||
new_mode = picked = 3;
|
new_mode = 3;
|
||||||
open_uv_editor = true;
|
open_uv_editor = true;
|
||||||
}
|
}
|
||||||
vsep(icon_md);
|
vsep(icon_md);
|
||||||
@@ -6437,8 +6263,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
if (new_mode == cur_mode)
|
if (new_mode == cur_mode)
|
||||||
update_uv_editor(); // otherwise apply_view_mode() below does it
|
update_uv_editor(); // otherwise apply_view_mode() below does it
|
||||||
}
|
}
|
||||||
if (picked >= 0)
|
|
||||||
m_island_view.picked(picked);
|
|
||||||
if (new_mode != cur_mode)
|
if (new_mode != cur_mode)
|
||||||
apply_view_mode(new_mode);
|
apply_view_mode(new_mode);
|
||||||
if (wf_toggle) {
|
if (wf_toggle) {
|
||||||
@@ -6627,15 +6451,15 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
|
|
||||||
// The three controls nearly every layer needs; everything else waits behind "More settings".
|
// The three controls nearly every layer needs; everything else waits behind "More settings".
|
||||||
// Depth and tile size are logarithmic, so both ends of their wide ranges stay usable.
|
// Depth and tile size are logarithmic, so both ends of their wide ranges stay usable.
|
||||||
m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad,
|
m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad);
|
||||||
_L("Height of the relief: how far white in the texture lifts the surface, in "
|
hover_tip(_u8L("Height of the relief: how far white in the texture lifts the surface, in "
|
||||||
"millimetres. Black does not move it at all, unless Midlevel says otherwise."));
|
"millimetres. Black does not move it at all, unless Midlevel says otherwise."));
|
||||||
m_preview_params_dirty |= float_row("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad,
|
m_preview_params_dirty |= float_row("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad);
|
||||||
_L("How wide one copy of the texture is on the model. Smaller repeats the pattern "
|
hover_tip(_u8L("How wide one copy of the texture is on the model. Smaller repeats the pattern "
|
||||||
"more often and makes its detail finer; with Tile off, this is the size of the "
|
"more often and makes its detail finer; with Tile off, this is the size of the "
|
||||||
"single copy."));
|
"single copy."));
|
||||||
m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad,
|
m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad);
|
||||||
_L("Turns the texture on the surface, in degrees - for lining a pattern up with an "
|
hover_tip(_u8L("Turns the texture on the surface, in degrees - for lining a pattern up with an "
|
||||||
"edge of the model."));
|
"edge of the model."));
|
||||||
|
|
||||||
if (m_layer_expanded[size_t(layer.slot)]) {
|
if (m_layer_expanded[size_t(layer.slot)]) {
|
||||||
@@ -6651,8 +6475,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
|
|
||||||
// Midlevel: the height that means "don't move". At 0 the surface only ever bulges outwards;
|
// Midlevel: the height that means "don't move". At 0 the surface only ever bulges outwards;
|
||||||
// at 0.5 mid-grey is neutral and darker texels cut inwards.
|
// at 0.5 mid-grey is neutral and darker texels cut inwards.
|
||||||
m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad,
|
m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad);
|
||||||
_L("Which gray stays where the surface already is. At 0 the texture only pushes "
|
hover_tip(_u8L("Which gray stays where the surface already is. At 0 the texture only pushes "
|
||||||
"outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones "
|
"outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones "
|
||||||
"still push out - one image both embosses and engraves.\n\n"
|
"still push out - one image both embosses and engraves.\n\n"
|
||||||
"What cuts in has to fit: inside a sharp corner or through a thin wall, a deep "
|
"What cuts in has to fit: inside a sharp corner or through a thin wall, a deep "
|
||||||
@@ -6663,8 +6487,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
ImGui::PopTextWrapPos();
|
ImGui::PopTextWrapPos();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_preview_params_dirty |= float_row("##smoothing", _L("Smoothing"), &layer.smoothing, 0.f, 1.f, "%.2f", false, card_pad,
|
m_preview_params_dirty |= float_row("##smoothing", _L("Smoothing"), &layer.smoothing, 0.f, 1.f, "%.2f", false, card_pad);
|
||||||
_L("Blurs the image before it is used, which rounds off hard steps and removes "
|
hover_tip(_u8L("Blurs the image before it is used, which rounds off hard steps and removes "
|
||||||
"speckle from a noisy photo. Raise it if the relief looks harsh or grainy; it "
|
"speckle from a noisy photo. Raise it if the relief looks harsh or grainy; it "
|
||||||
"costs fine detail."));
|
"costs fine detail."));
|
||||||
|
|
||||||
@@ -6674,11 +6498,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
"blends into the bare surface instead of stopping at a step."));
|
"blends into the bare surface instead of stopping at a step."));
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
m_imgui->disabled_begin(!layer.edge_smoothing);
|
m_imgui->disabled_begin(!layer.edge_smoothing);
|
||||||
m_preview_params_dirty |= typed_slider("##edge_amount", -(card_pad + edit_icon_room),
|
ImGui::SetNextItemWidth(-card_pad);
|
||||||
&layer.edge_smoothing_amount, 0.02f, 1.f, "%.2f", false,
|
m_preview_params_dirty |= ImGui::SliderFloat("##edge_amount", &layer.edge_smoothing_amount, 0.02f, 1.f, "%.2f",
|
||||||
_L("How far in the fade reaches, as a share of the painted area. Small "
|
ImGuiSliderFlags_AlwaysClamp);
|
||||||
"values soften a narrow band at the edge; 1 flattens almost all of it."));
|
|
||||||
m_imgui->disabled_end();
|
m_imgui->disabled_end();
|
||||||
|
hover_tip(_u8L("How far in the fade reaches, as a share of the painted area. Small values "
|
||||||
|
"soften a narrow band at the edge; 1 flattens almost all of it."));
|
||||||
|
|
||||||
// Invert and Colours share a row.
|
// Invert and Colours share a row.
|
||||||
{
|
{
|
||||||
@@ -6723,19 +6548,19 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
// Every mix can become a filament slot, and there are only so many beside the
|
// Every mix can become a filament slot, and there are only so many beside the
|
||||||
// physical filaments.
|
// physical filaments.
|
||||||
const int max_mixes = std::max(1, int(EnforcerBlockerType::ExtruderMax) - int(m_palette_filaments.size()));
|
const int max_mixes = std::max(1, int(EnforcerBlockerType::ExtruderMax) - int(m_palette_filaments.size()));
|
||||||
if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad,
|
if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad))
|
||||||
_L("The most mixed filaments a bake may add. They are picked from the "
|
|
||||||
"texture's colors, and only the ones the bake actually uses are created.")))
|
|
||||||
m_preview_params_dirty = true;
|
m_preview_params_dirty = true;
|
||||||
|
hover_tip(_u8L("The most mixed filaments a bake may add. They are picked from the "
|
||||||
|
"texture's colors, and only the ones the bake actually uses are created."));
|
||||||
// After the slider, so a change shows in the same frame.
|
// After the slider, so a change shows in the same frame.
|
||||||
ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colors from %2% filaments"),
|
ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colors from %2% filaments"),
|
||||||
int(cached_palette().size()), int(m_palette_filaments.size())).c_str());
|
int(cached_palette().size()), int(m_palette_filaments.size())).c_str());
|
||||||
}
|
}
|
||||||
if (int_row("##color_despeckle", _L("Denoise"), &opts.color_despeckle, 0, 6, "%d", card_pad,
|
if (int_row("##color_despeckle", _L("Denoise"), &opts.color_despeckle, 0, 6, "%d", card_pad))
|
||||||
_L("Cleans up single stray triangles of the wrong color, which detail finer "
|
|
||||||
"than the mesh leaves behind. Raise it if the result looks speckled, "
|
|
||||||
"lower it if small features are being swallowed.")))
|
|
||||||
m_preview_params_dirty = true;
|
m_preview_params_dirty = true;
|
||||||
|
hover_tip(_u8L("Cleans up single stray triangles of the wrong color, which detail finer "
|
||||||
|
"than the mesh leaves behind. Raise it if the result looks speckled, "
|
||||||
|
"lower it if small features are being swallowed."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6840,10 +6665,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
"gets the texture, and its border becomes the edge of the projection."));
|
"gets the texture, and its border becomes the edge of the projection."));
|
||||||
|
|
||||||
if (projector_open) {
|
if (projector_open) {
|
||||||
if (int_row("##projector_opacity", _L("Opacity"), &m_projector_opacity, 20, 255, "%d", card_pad,
|
if (int_row("##projector_opacity", _L("Opacity"), &m_projector_opacity, 20, 255, "%d", card_pad))
|
||||||
_L("How solid the frame window looks while you place it. Lower to see the "
|
|
||||||
"model through it; it does not affect the result.")))
|
|
||||||
m_projector_frame->set_opacity(m_projector_opacity);
|
m_projector_frame->set_opacity(m_projector_opacity);
|
||||||
|
hover_tip(_u8L("How solid the frame window looks while you place it. Lower to see the "
|
||||||
|
"model through it; it does not affect the result."));
|
||||||
|
|
||||||
if (m_imgui->button(_u8L("Apply projection frame"))) {
|
if (m_imgui->button(_u8L("Apply projection frame"))) {
|
||||||
const int painted_count = apply_projection_frame();
|
const int painted_count = apply_projection_frame();
|
||||||
@@ -7144,57 +6969,57 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
// pattern can sample four points that all happen to land at similar heights, report no error,
|
// pattern can sample four points that all happen to land at similar heights, report no error,
|
||||||
// and stall before refinement ever starts.
|
// and stall before refinement ever starts.
|
||||||
if (float_row("##subdiv_target", m_subdivide_feature ? _L("Max edge") : _L("Target edge"), &m_subdivide_target_mm,
|
if (float_row("##subdiv_target", m_subdivide_feature ? _L("Max edge") : _L("Target edge"), &m_subdivide_target_mm,
|
||||||
0.001f, 20.f, "%.3f mm", true, 0.f,
|
0.001f, 20.f, "%.3f mm", true, 0.f))
|
||||||
m_subdivide_feature ?
|
|
||||||
_L("No triangle in the painted area stays larger than this, even where the texture "
|
|
||||||
"is flat. Keep it near the size of the smallest feature you want to come out.") :
|
|
||||||
_L("Triangles in the painted area are split until none is larger than this. Smaller "
|
|
||||||
"means finer detail and, fast, a lot more triangles.")))
|
|
||||||
preview_live();
|
preview_live();
|
||||||
|
hover_tip(m_subdivide_feature ?
|
||||||
|
_u8L("No triangle in the painted area stays larger than this, even where the texture "
|
||||||
|
"is flat. Keep it near the size of the smallest feature you want to come out.") :
|
||||||
|
_u8L("Triangles in the painted area are split until none is larger than this. Smaller "
|
||||||
|
"means finer detail and, fast, a lot more triangles."));
|
||||||
|
|
||||||
if (m_subdivide_feature) {
|
if (m_subdivide_feature) {
|
||||||
if (float_row("##subdiv_detail", _L("Detail"), &m_subdivide_detail_mm, 0.001f, 1.f, "%.3f mm", true, 0.f,
|
if (float_row("##subdiv_detail", _L("Detail"), &m_subdivide_detail_mm, 0.001f, 1.f, "%.3f mm", true, 0.f))
|
||||||
_L("How far the mesh may sit from the shape the texture describes, in millimetres. "
|
preview_live();
|
||||||
|
hover_tip(_u8L("How far the mesh may sit from the shape the texture describes, in millimetres. "
|
||||||
"Smaller follows fine detail and costs triangles; larger only chases the big "
|
"Smaller follows fine detail and costs triangles; larger only chases the big "
|
||||||
"features.")))
|
"features."));
|
||||||
|
if (float_row("##subdiv_min", _L("Min edge"), &m_subdivide_min_edge_mm, 0.001f, 20.f, "%.3f mm", true, 0.f))
|
||||||
preview_live();
|
preview_live();
|
||||||
if (float_row("##subdiv_min", _L("Min edge"), &m_subdivide_min_edge_mm, 0.001f, 20.f, "%.3f mm", true, 0.f,
|
hover_tip(_u8L("A floor on triangle size, so a sharp step in the texture cannot be chased "
|
||||||
_L("A floor on triangle size, so a sharp step in the texture cannot be chased "
|
|
||||||
"forever. Lower it for finer relief, raise it if the count runs away at hard "
|
"forever. Lower it for finer relief, raise it if the count runs away at hard "
|
||||||
"edges.")))
|
"edges."));
|
||||||
preview_live();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Applies in both adaptive sub-modes: it is about the step the bake puts at the paint's edge, which
|
// Applies in both adaptive sub-modes: it is about the step the bake puts at the paint's edge, which
|
||||||
// exists whether or not "Follow texture detail" is on. 0 turns the band off.
|
// exists whether or not "Follow texture detail" is on. 0 turns the band off.
|
||||||
if (float_row("##subdiv_border", _L("Edge detail"), &m_subdivide_border_mm, 0.f, 5.f, "%.3f mm", false, 0.f,
|
if (float_row("##subdiv_border", _L("Edge detail"), &m_subdivide_border_mm, 0.f, 5.f, "%.3f mm", false, 0.f))
|
||||||
_L("Triangle size along the outline of the painted area, where the relief drops back to the "
|
|
||||||
"bare surface. Lower it if that rim looks jagged; it only costs triangles along the "
|
|
||||||
"outline. 0 turns it off.")))
|
|
||||||
preview_live();
|
preview_live();
|
||||||
|
hover_tip(_u8L("Triangle size along the outline of the painted area, where the relief drops back to the "
|
||||||
|
"bare surface. Lower it if that rim looks jagged; it only costs triangles along the "
|
||||||
|
"outline. 0 turns it off."));
|
||||||
|
|
||||||
// Only worth showing when a layer is actually colouring: with no colour there is no boundary for it
|
// Only worth showing when a layer is actually colouring: with no colour there is no boundary for it
|
||||||
// to refine and the control would do nothing whatever it is set to.
|
// to refine and the control would do nothing whatever it is set to.
|
||||||
if (mv != nullptr && any_layer_colors(*mv)) {
|
if (mv != nullptr && any_layer_colors(*mv)) {
|
||||||
if (float_row("##subdiv_color", _L("Color detail"), &m_subdivide_color_mm, 0.f, 5.f, "%.3f mm", false, 0.f,
|
if (float_row("##subdiv_color", _L("Color detail"), &m_subdivide_color_mm, 0.f, 5.f, "%.3f mm", false, 0.f))
|
||||||
_L("Triangle size where two colors meet. Each triangle prints in one filament, so a "
|
|
||||||
"color edge can only be as sharp as the triangles along it - and nothing else "
|
|
||||||
"refines there, since the surface is flat across a change of color. 0 turns it off.")))
|
|
||||||
preview_live();
|
preview_live();
|
||||||
|
hover_tip(_u8L("Triangle size where two colors meet. Each triangle prints in one filament, so a "
|
||||||
|
"color edge can only be as sharp as the triangles along it - and nothing else "
|
||||||
|
"refines there, since the surface is flat across a change of color. 0 turns it off."));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TextDisabled("%s", _u8L("Triangle budget: set with Triangles, below.").c_str());
|
ImGui::TextDisabled("%s", _u8L("Triangle budget: set with Triangles, below.").c_str());
|
||||||
if (m_subdivide_editing && m_subdivide_preview_tris > 0)
|
if (m_subdivide_editing && m_subdivide_preview_tris > 0)
|
||||||
m_imgui->text(Slic3r::format(_u8L("Preview: %1% triangles"), m_subdivide_preview_tris));
|
m_imgui->text(Slic3r::format(_u8L("Preview: %1% triangles"), m_subdivide_preview_tris));
|
||||||
} else {
|
} else {
|
||||||
if (int_row("##subdiv_steps", _L("Steps"), &m_subdivide_count, 0, 5, "%d", 0.f,
|
if (int_row("##subdiv_steps", _L("Steps"), &m_subdivide_count, 0, 5, "%d", 0.f)) {
|
||||||
_L("Splits every triangle of the model into four, this many times over. Each step "
|
|
||||||
"quadruples the count - 3 turns 100 k triangles into 6.4 M - so prefer refining "
|
|
||||||
"only the painted area unless you need the whole model finer."))) {
|
|
||||||
if (m_subdivide_editing)
|
if (m_subdivide_editing)
|
||||||
rebuild_subdivide_preview(); // a count of 0 clears the preview, it doesn't compute one
|
rebuild_subdivide_preview(); // a count of 0 clears the preview, it doesn't compute one
|
||||||
m_parent.set_as_dirty();
|
m_parent.set_as_dirty();
|
||||||
}
|
}
|
||||||
|
hover_tip(_u8L("Splits every triangle of the model into four, this many times over. Each step "
|
||||||
|
"quadruples the count - 3 turns 100 k triangles into 6.4 M - so prefer refining "
|
||||||
|
"only the painted area unless you need the whole model finer."));
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -7256,8 +7081,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
}
|
}
|
||||||
m_remesh_target_edge_mm = cnt > 0 ? std::clamp(float(sum / double(cnt)), 0.1f, 20.f) : 1.f;
|
m_remesh_target_edge_mm = cnt > 0 ? std::clamp(float(sum / double(cnt)), 0.1f, 20.f) : 1.f;
|
||||||
}
|
}
|
||||||
float_row("##remesh_edge", _L("Target edge"), &m_remesh_target_edge_mm, 0.1f, 20.f, "%.2f mm", true, 0.f,
|
float_row("##remesh_edge", _L("Target edge"), &m_remesh_target_edge_mm, 0.1f, 20.f, "%.2f mm", true, 0.f);
|
||||||
_L("Triangle size the whole model is rebuilt with, in millimetres. Displacement works best on an "
|
hover_tip(_u8L("Triangle size the whole model is rebuilt with, in millimetres. Displacement works best on an "
|
||||||
"even mesh; this is what makes one out of an uneven import."));
|
"even mesh; this is what makes one out of an uneven import."));
|
||||||
|
|
||||||
m_imgui->bbl_checkbox(_L("Keep sharp edges") + "##remesh_sharp", m_remesh_keep_sharp_edges);
|
m_imgui->bbl_checkbox(_L("Keep sharp edges") + "##remesh_sharp", m_remesh_keep_sharp_edges);
|
||||||
@@ -7266,10 +7091,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
"a cube comes back with wobbly edges."));
|
"a cube comes back with wobbly edges."));
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
m_imgui->disabled_begin(!m_remesh_keep_sharp_edges);
|
m_imgui->disabled_begin(!m_remesh_keep_sharp_edges);
|
||||||
typed_slider("##remesh_sharp_angle", -(1.f + edit_icon_room), &m_remesh_sharp_angle_deg, 5.f, 90.f, "%.0f°", false,
|
ImGui::SetNextItemWidth(-1.f);
|
||||||
_L("How sharp a fold has to be, in degrees, before the remesher treats it as an edge worth "
|
ImGui::SliderFloat("##remesh_sharp_angle", &m_remesh_sharp_angle_deg, 5.f, 90.f, "%.0f°", ImGuiSliderFlags_AlwaysClamp);
|
||||||
"keeping. Lower protects more of the model's shape; higher rebuilds more of it evenly."));
|
|
||||||
m_imgui->disabled_end();
|
m_imgui->disabled_end();
|
||||||
|
hover_tip(_u8L("How sharp a fold has to be, in degrees, before the remesher treats it as an edge worth "
|
||||||
|
"keeping. Lower protects more of the model's shape; higher rebuilds more of it evenly."));
|
||||||
|
|
||||||
m_imgui->disabled_begin(mv == nullptr || busy);
|
m_imgui->disabled_begin(mv == nullptr || busy);
|
||||||
if (ImGui::Button(_u8L("Remesh").c_str()))
|
if (ImGui::Button(_u8L("Remesh").c_str()))
|
||||||
@@ -7297,16 +7123,16 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
"on a layer is a different thing: it blurs the image before it is used."));
|
"on a layer is a different thing: it blurs the image before it is used."));
|
||||||
if (opts.smooth_enabled) {
|
if (opts.smooth_enabled) {
|
||||||
float percent = opts.smooth_strength * 100.f;
|
float percent = opts.smooth_strength * 100.f;
|
||||||
if (float_row("##dispsmooth", _L_CONTEXT("Strength", "Texture Displacement"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f,
|
if (float_row("##dispsmooth", _L_CONTEXT("Strength", "Texture Displacement"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f)) {
|
||||||
_L("How far each pass pulls a vertex towards its neighbours. High values round the "
|
|
||||||
"relief off quickly; low values need more passes but keep more of the detail."))) {
|
|
||||||
opts.smooth_strength = std::clamp(percent / 100.f, 0.01f, 1.f);
|
opts.smooth_strength = std::clamp(percent / 100.f, 0.01f, 1.f);
|
||||||
m_preview_params_dirty = true;
|
m_preview_params_dirty = true;
|
||||||
}
|
}
|
||||||
if (int_row("##dispsmoothit", _L("Passes"), &opts.smooth_iterations, 1, 10, "%d", 0.f,
|
hover_tip(_u8L("How far each pass pulls a vertex towards its neighbours. High values round the "
|
||||||
_L("How many smoothing passes to run. More passes spread the smoothing further across "
|
"relief off quickly; low values need more passes but keep more of the detail."));
|
||||||
"the surface; Strength decides how much each one moves.")))
|
if (int_row("##dispsmoothit", _L("Passes"), &opts.smooth_iterations, 1, 10, "%d", 0.f))
|
||||||
m_preview_params_dirty = true;
|
m_preview_params_dirty = true;
|
||||||
|
hover_tip(_u8L("How many smoothing passes to run. More passes spread the smoothing further across "
|
||||||
|
"the surface; Strength decides how much each one moves."));
|
||||||
|
|
||||||
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Ignore outer ring"), opts.smooth_skip_border);
|
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Ignore outer ring"), opts.smooth_skip_border);
|
||||||
hover_tip(_u8L("Keeps the outer ring of the painted area out of the smoothing. Its neighbours "
|
hover_tip(_u8L("Keeps the outer ring of the painted area out of the smoothing. Its neighbours "
|
||||||
@@ -7349,16 +7175,16 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
int shown_k = auto_budget ? v2_recommendation(*mv).budget_k : opts.v2_max_triangles_k;
|
int shown_k = auto_budget ? v2_recommendation(*mv).budget_k : opts.v2_max_triangles_k;
|
||||||
m_imgui->disabled_begin(auto_budget);
|
m_imgui->disabled_begin(auto_budget);
|
||||||
// TRN Slider value: %d is the triangle budget in thousands
|
// TRN Slider value: %d is the triangle budget in thousands
|
||||||
if (int_row("##v2budget", _L("Budget"), &shown_k, 0, 4000, auto_budget ? _u8L("%d k (auto)").c_str() : "%d k", 0.f,
|
if (int_row("##v2budget", _L("Budget"), &shown_k, 0, 4000, auto_budget ? _u8L("%d k (auto)").c_str() : "%d k", 0.f)) {
|
||||||
_L("How many thousand triangles this bake may spend on the area you painted. "
|
|
||||||
"Relief already baked into the rest of the model is kept on top of it, so a "
|
|
||||||
"second bake somewhere else gets the same budget as the first. Raise it if the "
|
|
||||||
"warning below Resolution says the detail will not fit; 0 keeps every triangle "
|
|
||||||
"the refinement produced, however many that is."))) {
|
|
||||||
opts.v2_max_triangles_k = shown_k;
|
opts.v2_max_triangles_k = shown_k;
|
||||||
m_preview_params_dirty = true;
|
m_preview_params_dirty = true;
|
||||||
}
|
}
|
||||||
m_imgui->disabled_end();
|
m_imgui->disabled_end();
|
||||||
|
hover_tip(_u8L("How many thousand triangles this bake may spend on the area you painted. "
|
||||||
|
"Relief already baked into the rest of the model is kept on top of it, so a "
|
||||||
|
"second bake somewhere else gets the same budget as the first. Raise it if the "
|
||||||
|
"warning below Resolution says the detail will not fit; 0 keeps every triangle "
|
||||||
|
"the refinement produced, however many that is."));
|
||||||
}
|
}
|
||||||
if (SHOW_PIPELINE_DEV_CONTROLS && opts.pipeline_v2) {
|
if (SHOW_PIPELINE_DEV_CONTROLS && opts.pipeline_v2) {
|
||||||
// Keeping the relief above the plate is not a checkbox: it is unconditional, in both pipelines
|
// Keeping the relief above the plate is not a checkbox: it is unconditional, in both pipelines
|
||||||
@@ -7465,28 +7291,26 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
"default. Untick to set both yourself - worth doing for a texture much finer or "
|
"default. Untick to set both yourself - worth doing for a texture much finer or "
|
||||||
"much coarser than the part it sits on."));
|
"much coarser than the part it sits on."));
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
|
||||||
float shown = auto_res ? rec.edge_mm : opts.v2_refine_mm;
|
float shown = auto_res ? rec.edge_mm : opts.v2_refine_mm;
|
||||||
// Held in a local: the format outlives the call that reads it, and the slider and the
|
|
||||||
// range in its tooltip are then formatted the same way.
|
|
||||||
const std::string v2edge_format = auto_res ? _u8L("%.2f mm (auto)") : std::string("%.2f mm");
|
|
||||||
const wxString v2edge_tip =
|
|
||||||
auto_res && rec.edge_mm > 0.f ?
|
|
||||||
from_u8(Slic3r::format(_u8L("Chosen from the size of the model: %1$.2f mm triangles, "
|
|
||||||
"budget %2% k. Untick Auto to set them yourself."),
|
|
||||||
rec.edge_mm, rec.budget_k)) :
|
|
||||||
_L("How fine the mesh is made under the paint, in millimetres. It has to be "
|
|
||||||
"smaller than the detail you want out of the texture - a 0.5 mm groove needs "
|
|
||||||
"triangles well under 0.5 mm. Smaller costs triangles fast: halving it needs "
|
|
||||||
"four times as many, and once they no longer fit the budget the bake simplifies "
|
|
||||||
"back down and the detail goes with it.");
|
|
||||||
m_imgui->disabled_begin(busy || auto_res);
|
m_imgui->disabled_begin(busy || auto_res);
|
||||||
if (typed_slider("##v2edge", x0 + panel_w - ImGui::GetCursorPosX() - edit_icon_room, &shown, 0.02f, 2.f,
|
if (ImGui::SliderFloat("##v2edge", &shown, 0.02f, 2.f, auto_res ? _u8L("%.2f mm (auto)").c_str() : "%.2f mm",
|
||||||
v2edge_format.c_str(), true, v2edge_tip)) {
|
ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic)) {
|
||||||
opts.v2_refine_mm = shown;
|
opts.v2_refine_mm = shown;
|
||||||
m_preview_params_dirty = true;
|
m_preview_params_dirty = true;
|
||||||
m_parent.set_as_dirty();
|
m_parent.set_as_dirty();
|
||||||
}
|
}
|
||||||
m_imgui->disabled_end();
|
m_imgui->disabled_end();
|
||||||
|
if (auto_res && rec.edge_mm > 0.f)
|
||||||
|
hover_tip(Slic3r::format(_u8L("Chosen from the size of the model: %1$.2f mm triangles, "
|
||||||
|
"budget %2% k. Untick Auto to set them yourself."),
|
||||||
|
rec.edge_mm, rec.budget_k));
|
||||||
|
else
|
||||||
|
hover_tip(_u8L("How fine the mesh is made under the paint, in millimetres. It has to be "
|
||||||
|
"smaller than the detail you want out of the texture - a 0.5 mm groove needs "
|
||||||
|
"triangles well under 0.5 mm. Smaller costs triangles fast: halving it needs "
|
||||||
|
"four times as many, and once they no longer fit the budget the bake simplifies "
|
||||||
|
"back down and the detail goes with it."));
|
||||||
|
|
||||||
// What this resolution costs over what is painted, against what the budget allows. Refining
|
// What this resolution costs over what is painted, against what the budget allows. Refining
|
||||||
// past the budget is not an error - the bake simplifies back down to it - but the result
|
// past the budget is not an error - the bake simplifies back down to it - but the result
|
||||||
@@ -7518,17 +7342,18 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
m_imgui->text(_L("Triangles"));
|
m_imgui->text(_L("Triangles"));
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||||
|
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
|
||||||
m_imgui->disabled_begin(busy);
|
m_imgui->disabled_begin(busy);
|
||||||
if (typed_slider_int("##subdivbudget", x0 + panel_w - ImGui::GetCursorPosX() - edit_icon_room,
|
if (ImGui::SliderInt("##subdivbudget", &m_subdivide_budget_k, 10, 2000, "+%d k",
|
||||||
&m_subdivide_budget_k, 10, 2000, "+%d k", true,
|
ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic)) {
|
||||||
_L("How many thousand triangles the refinement may add to the model. The triangle "
|
|
||||||
"that fits worst is always split first, so even a run that spends the lot has "
|
|
||||||
"spent it where it shows most. Raise it if the relief still looks coarse."))) {
|
|
||||||
if (m_subdivide_editing)
|
if (m_subdivide_editing)
|
||||||
rebuild_subdivide_preview();
|
rebuild_subdivide_preview();
|
||||||
m_parent.set_as_dirty();
|
m_parent.set_as_dirty();
|
||||||
}
|
}
|
||||||
m_imgui->disabled_end();
|
m_imgui->disabled_end();
|
||||||
|
hover_tip(_u8L("How many thousand triangles the refinement may add to the model. The triangle "
|
||||||
|
"that fits worst is always split first, so even a run that spends the lot has "
|
||||||
|
"spent it where it shows most. Raise it if the relief still looks coarse."));
|
||||||
}
|
}
|
||||||
|
|
||||||
const float button_h = std::round(frame_h * 1.25f);
|
const float button_h = std::round(frame_h * 1.25f);
|
||||||
@@ -7547,7 +7372,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
: m_tool_type == ToolType::SMART_FILL ? m_shortcuts_bucket_fill
|
: m_tool_type == ToolType::SMART_FILL ? m_shortcuts_bucket_fill
|
||||||
: m_tool_type == ToolType::BRUSH ? m_shortcuts_brush
|
: m_tool_type == ToolType::BRUSH ? m_shortcuts_brush
|
||||||
: m_tool_type == ToolType::GAP_FILL ? m_shortcuts_gap_fill
|
: m_tool_type == ToolType::GAP_FILL ? m_shortcuts_gap_fill
|
||||||
: m_shortcuts_sliders; // the sliders are there whether or not a painting tool is
|
: std::vector<std::pair<wxString, wxString>>{};
|
||||||
|
|
||||||
ImGui::SetCursorPosY(row_y + (row_h - icon_h) * .5f); // center vertically
|
ImGui::SetCursorPosY(row_y + (row_h - icon_h) * .5f); // center vertically
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(int(m_imgui->scaled(.5f)), style.ItemSpacing.y));
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(int(m_imgui->scaled(.5f)), style.ItemSpacing.y));
|
||||||
|
|||||||
@@ -197,27 +197,6 @@ public:
|
|||||||
};
|
};
|
||||||
static PaintedColors painted_colors(const TriangleMesh &mesh, const TriangleSelector::TriangleSplittingData &paint);
|
static PaintedColors painted_colors(const TriangleMesh &mesh, const TriangleSelector::TriangleSplittingData &paint);
|
||||||
|
|
||||||
// The view while the UV editor's selected islands are highlighted on the model. The highlight lies on the
|
|
||||||
// undisplaced surface, so the Normal view's relief hides it wherever it rises: Normal gives way to Fast
|
|
||||||
// while islands are selected and comes back once none are. A view the user clicks in the View row is
|
|
||||||
// theirs - the pane's background buttons are not a pick, Height only meaning "the texture view".
|
|
||||||
struct IslandViewFollow
|
|
||||||
{
|
|
||||||
bool shown = false; // the highlight, as of the last step()
|
|
||||||
bool switched = false; // Fast was switched on for the highlight, so Normal comes back once it is gone
|
|
||||||
bool keep_normal = false; // the user picked Normal in the View row while the highlight was shown
|
|
||||||
// The view to apply this frame (0 Normal, 1 Fast), or -1 for none. `view` is the one on screen, numbered as
|
|
||||||
// apply_view_mode() takes it; `debug_open` a debug stage on screen, which holds Normal itself.
|
|
||||||
int step(bool shown_now, int view, bool debug_open);
|
|
||||||
// A View row button clicked, the active one included. The row is drawn after step() in the same frame,
|
|
||||||
// so `shown` is this frame's.
|
|
||||||
void picked(int view)
|
|
||||||
{
|
|
||||||
switched = false;
|
|
||||||
keep_normal = shown && view == 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void render_painter_gizmo() override;
|
void render_painter_gizmo() override;
|
||||||
|
|
||||||
// Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/
|
// Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/
|
||||||
@@ -283,9 +262,6 @@ private:
|
|||||||
// Standard is active so what Preview shows is always what Bake will do. Returns true if it actually
|
// Standard is active so what Preview shows is always what Bake will do. Returns true if it actually
|
||||||
// changed something, so the caller can invalidate the preview.
|
// changed something, so the caller can invalidate the preview.
|
||||||
bool apply_standard_mode_presets(ModelVolume *mv);
|
bool apply_standard_mode_presets(ModelVolume *mv);
|
||||||
// Copies the panel's own state (active layer, Standard/Pro) onto the volume, so the project file
|
|
||||||
// can carry it. Cheap and idempotent - called wherever either of the two changes.
|
|
||||||
void store_panel_state();
|
|
||||||
// Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace.
|
// Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace.
|
||||||
// The order matters and is the whole reason this is one button - a height map can only move
|
// The order matters and is the whole reason this is one button - a height map can only move
|
||||||
// existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop
|
// existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop
|
||||||
@@ -428,12 +404,6 @@ private:
|
|||||||
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
|
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
|
||||||
// Panel state only, not saved with the project.
|
// Panel state only, not saved with the project.
|
||||||
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{};
|
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{};
|
||||||
// The slider whose edit icon was just clicked, as its ImGui id, and the frame it was clicked on.
|
|
||||||
// The row acts on it on the next frame - ImGui's keyboard-focus request only takes effect a frame
|
|
||||||
// later - and clears it; the frame number is what retires a request whose row has gone off screen
|
|
||||||
// in the meantime. Panel state only, not saved with the project.
|
|
||||||
std::string m_slider_type_request;
|
|
||||||
int m_slider_type_request_frame = 0;
|
|
||||||
|
|
||||||
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
|
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
|
||||||
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
|
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
|
||||||
@@ -534,11 +504,6 @@ private:
|
|||||||
// The panel's view modes: 0 Normal, 1 Fast, 2 Checker, 3 Distortion. Shared by the View row and the pane's
|
// The panel's view modes: 0 Normal, 1 Fast, 2 Checker, 3 Distortion. Shared by the View row and the pane's
|
||||||
// background buttons, so both switch views the same way.
|
// background buttons, so both switch views the same way.
|
||||||
void apply_view_mode(int mode);
|
void apply_view_mode(int mode);
|
||||||
// The view on screen, numbered as apply_view_mode() takes it.
|
|
||||||
int view_mode() const;
|
|
||||||
// Run once a frame, after the pane's commands: switches the view as m_island_view says.
|
|
||||||
void follow_island_highlight();
|
|
||||||
IslandViewFollow m_island_view;
|
|
||||||
// The pane header's thumbnail of the active layer's texture, rebuilt only when its image changes.
|
// The pane header's thumbnail of the active layer's texture, rebuilt only when its image changes.
|
||||||
static constexpr int UV_THUMB_PX = 64;
|
static constexpr int UV_THUMB_PX = 64;
|
||||||
const void *m_uv_thumb_source = nullptr;
|
const void *m_uv_thumb_source = nullptr;
|
||||||
@@ -770,8 +735,6 @@ private:
|
|||||||
// in place. Rebuilt whenever the pane's selection differs from the one it was built for.
|
// in place. Rebuilt whenever the pane's selection differs from the one it was built for.
|
||||||
GLModel m_island_overlay_glmodel;
|
GLModel m_island_overlay_glmodel;
|
||||||
std::vector<int> m_island_overlay_selection;
|
std::vector<int> m_island_overlay_selection;
|
||||||
// The pane's selection while the overlay applies - the pane open over an unwrapped LSCM layer - else null.
|
|
||||||
const std::vector<int> *island_highlight_selection();
|
|
||||||
void rebuild_island_overlay(const std::vector<int> &selection);
|
void rebuild_island_overlay(const std::vector<int> &selection);
|
||||||
void render_island_overlay();
|
void render_island_overlay();
|
||||||
// Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept
|
// Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept
|
||||||
@@ -952,11 +915,10 @@ private:
|
|||||||
bool m_preview_job_pending = false;
|
bool m_preview_job_pending = false;
|
||||||
void queue_preview_job();
|
void queue_preview_job();
|
||||||
|
|
||||||
// Per-slot GPU thumbnail cache for the layer list panel, each keyed by the image_data it was built from
|
// Per-slot GPU thumbnail cache for the layer list panel, keyed by the image_data pointer that
|
||||||
// (see get_layer_thumbnail()). Held weakly rather than by address, as decode_height_texture()'s cache is:
|
// was current the last time each thumbnail was built (see get_layer_thumbnail()).
|
||||||
// an image freed and replaced by another at the same address must not pass for it.
|
|
||||||
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails;
|
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails;
|
||||||
std::array<std::weak_ptr<const std::vector<unsigned char>>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source;
|
std::array<const void *, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source{};
|
||||||
// The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it.
|
// The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it.
|
||||||
std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{};
|
std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{};
|
||||||
|
|
||||||
@@ -964,13 +926,13 @@ private:
|
|||||||
// get_layer_height_texture()). A smoothing change re-uploads it, so the fast preview shows the
|
// get_layer_height_texture()). A smoothing change re-uploads it, so the fast preview shows the
|
||||||
// blur the bake will apply.
|
// blur the bake will apply.
|
||||||
std::unique_ptr<GLTexture> m_height_tex;
|
std::unique_ptr<GLTexture> m_height_tex;
|
||||||
std::weak_ptr<const std::vector<unsigned char>> m_height_tex_source;
|
const void *m_height_tex_source = nullptr;
|
||||||
float m_height_tex_smoothing = -1.f;
|
float m_height_tex_smoothing = -1.f;
|
||||||
|
|
||||||
// The same, for the layer's *colour*, which the fast preview quantizes per fragment so it shows
|
// The same, for the layer's *colour*, which the fast preview quantizes per fragment so it shows
|
||||||
// the image at texel resolution rather than at the mesh's. Null for a grayscale texture.
|
// the image at texel resolution rather than at the mesh's. Null for a grayscale texture.
|
||||||
std::unique_ptr<GLTexture> m_color_tex;
|
std::unique_ptr<GLTexture> m_color_tex;
|
||||||
std::weak_ptr<const std::vector<unsigned char>> m_color_tex_source;
|
const void *m_color_tex_source = nullptr;
|
||||||
float m_color_tex_smoothing = -1.f;
|
float m_color_tex_smoothing = -1.f;
|
||||||
|
|
||||||
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
|
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
|
||||||
@@ -1009,12 +971,9 @@ private:
|
|||||||
PatchUnwrap m_uv_editor_unwrap;
|
PatchUnwrap m_uv_editor_unwrap;
|
||||||
|
|
||||||
// When set, the panel is a free-floating window the user can drag anywhere (with a title bar to
|
// When set, the panel is a free-floating window the user can drag anywhere (with a title bar to
|
||||||
// grab), instead of being pinned to the right of the gizmo toolbar. Kept in the application
|
// grab), instead of being pinned to the right of the gizmo toolbar. Persisted across gizmo
|
||||||
// config rather than in the project: it describes the user's window, not the model, so opening
|
// open/close within a session, so the choice sticks while working.
|
||||||
// someone else's project must not move their panel.
|
|
||||||
bool m_undocked = false;
|
bool m_undocked = false;
|
||||||
// The config key behind m_undocked.
|
|
||||||
static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked";
|
|
||||||
|
|
||||||
// Smooth scrolling for the panel body (everything between the header and the pinned Bake footer).
|
// Smooth scrolling for the panel body (everything between the header and the pinned Bake footer).
|
||||||
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
|
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
|
||||||
@@ -1057,9 +1016,6 @@ private:
|
|||||||
std::vector<std::pair<wxString, wxString>> m_shortcuts_bucket_fill;
|
std::vector<std::pair<wxString, wxString>> m_shortcuts_bucket_fill;
|
||||||
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
|
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
|
||||||
std::vector<std::pair<wxString, wxString>> m_shortcuts_gap_fill;
|
std::vector<std::pair<wxString, wxString>> m_shortcuts_gap_fill;
|
||||||
// The two ways every slider in the panel takes a typed value. They hold whatever tool is selected,
|
|
||||||
// so they are tacked onto each of the lists above and stand on their own when no tool is active.
|
|
||||||
std::vector<std::pair<wxString, wxString>> m_shortcuts_sliders;
|
|
||||||
|
|
||||||
// Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with
|
// Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with
|
||||||
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
|
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace GUI {
|
||||||
|
|
||||||
|
} // namespace GUI
|
||||||
|
} // namespace Slic3r
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#ifndef slic3r_MultiTaskModel_hpp_
|
||||||
|
#define slic3r_MultiTaskModel_hpp_
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace GUI {
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace GUI
|
||||||
|
} // namespace Slic3r
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
#include "SysInfoDialog.hpp"
|
||||||
|
#include "I18N.hpp"
|
||||||
|
#include "3DScene.hpp"
|
||||||
|
#include "GUI.hpp"
|
||||||
|
#include "../Utils/UndoRedo.hpp"
|
||||||
|
#include "Plater.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <Eigen/Core>
|
||||||
|
|
||||||
|
#include <wx/clipbrd.h>
|
||||||
|
#include <wx/platinfo.h>
|
||||||
|
#include "GUI_App.hpp"
|
||||||
|
#include "MainFrame.hpp"
|
||||||
|
#include "wxExtensions.hpp"
|
||||||
|
#include "libslic3r/BlacklistedLibraryCheck.hpp"
|
||||||
|
#include "libslic3r/Color.hpp"
|
||||||
|
#include "format.hpp"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
// The standard Windows includes.
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <psapi.h>
|
||||||
|
#endif /* _WIN32 */
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace GUI {
|
||||||
|
|
||||||
|
std::string get_main_info(bool format_as_html)
|
||||||
|
{
|
||||||
|
std::stringstream out;
|
||||||
|
|
||||||
|
std::string b_start = format_as_html ? "<b>" : "";
|
||||||
|
std::string b_end = format_as_html ? "</b>" : "";
|
||||||
|
std::string line_end = format_as_html ? "<br>" : "\n";
|
||||||
|
|
||||||
|
if (!format_as_html)
|
||||||
|
out << b_start << (wxGetApp().is_editor() ? SLIC3R_APP_FULL_NAME : GCODEVIEWER_APP_NAME) << b_end << line_end;
|
||||||
|
out << b_start << "Version: " << b_end << SLIC3R_VERSION << line_end;
|
||||||
|
out << b_start << "Build: " << b_end << (wxGetApp().is_editor() ? SLIC3R_BUILD_ID : GCODEVIEWER_BUILD_ID) << line_end;
|
||||||
|
out << line_end;
|
||||||
|
out << b_start << "Operating System: " << b_end << wxPlatformInfo::Get().GetOperatingSystemFamilyName() << line_end;
|
||||||
|
out << b_start << "System Architecture: " << b_end << wxPlatformInfo::Get().GetArchName() << line_end;
|
||||||
|
out << b_start <<
|
||||||
|
#if defined _WIN32
|
||||||
|
"Windows Version: "
|
||||||
|
#else
|
||||||
|
// Hopefully some kind of unix / linux.
|
||||||
|
"System Version: "
|
||||||
|
#endif
|
||||||
|
<< b_end << wxPlatformInfo::Get().GetOperatingSystemDescription() << line_end;
|
||||||
|
out << b_start << "Total RAM size [MB]: " << b_end << Slic3r::format_memsize_MB(Slic3r::total_physical_memory());
|
||||||
|
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string get_mem_info(bool format_as_html)
|
||||||
|
{
|
||||||
|
std::stringstream out;
|
||||||
|
|
||||||
|
std::string b_start = format_as_html ? "<b>" : "";
|
||||||
|
std::string b_end = format_as_html ? "</b>" : "";
|
||||||
|
std::string line_end = format_as_html ? "<br>" : "\n";
|
||||||
|
|
||||||
|
std::string mem_info_str = log_memory_info(true);
|
||||||
|
std::istringstream mem_info(mem_info_str);
|
||||||
|
std::string value;
|
||||||
|
while (std::getline(mem_info, value, ':')) {
|
||||||
|
out << b_start << (value+": ") << b_end;
|
||||||
|
std::getline(mem_info, value, ';');
|
||||||
|
out << value << line_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Slic3r::UndoRedo::Stack &stack = wxGetApp().plater()->undo_redo_stack_main();
|
||||||
|
out << b_start << "RAM size reserved for the Undo / Redo stack: " << b_end << Slic3r::format_memsize_MB(stack.get_memory_limit()) << line_end;
|
||||||
|
out << b_start << "RAM size occupied by the Undo / Redo stack: " << b_end << Slic3r::format_memsize_MB(stack.memsize()) << line_end << line_end;
|
||||||
|
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
SysInfoDialog::SysInfoDialog()
|
||||||
|
: DPIDialog(static_cast<wxWindow*>(wxGetApp().mainframe), wxID_ANY, (wxGetApp().is_editor() ? wxString(SLIC3R_APP_FULL_NAME) : wxString(GCODEVIEWER_APP_NAME)) + " - " + _L("System Information"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
|
||||||
|
{
|
||||||
|
wxColour bgr_clr = wxGetApp().get_window_default_clr();//wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
|
||||||
|
SetBackgroundColour(bgr_clr);
|
||||||
|
SetFont(wxGetApp().normal_font());
|
||||||
|
|
||||||
|
wxBoxSizer* hsizer = new wxBoxSizer(wxHORIZONTAL);
|
||||||
|
hsizer->SetMinSize(wxSize(50 * wxGetApp().em_unit(), -1));
|
||||||
|
|
||||||
|
auto main_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
main_sizer->Add(hsizer, 1, wxEXPAND | wxALL, 10);
|
||||||
|
|
||||||
|
// logo
|
||||||
|
m_logo_bmp = ScalableBitmap(this, wxGetApp().logo_name(), 192);
|
||||||
|
m_logo = new wxStaticBitmap(this, wxID_ANY, m_logo_bmp.bmp());
|
||||||
|
hsizer->Add(m_logo, 0, wxALIGN_CENTER_VERTICAL);
|
||||||
|
|
||||||
|
wxBoxSizer* vsizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
hsizer->Add(vsizer, 1, wxEXPAND|wxLEFT, 20);
|
||||||
|
|
||||||
|
// title
|
||||||
|
{
|
||||||
|
wxStaticText* title = new wxStaticText(this, wxID_ANY, wxGetApp().is_editor() ? SLIC3R_APP_FULL_NAME : GCODEVIEWER_APP_NAME, wxDefaultPosition, wxDefaultSize);
|
||||||
|
wxFont title_font = wxGetApp().bold_font();
|
||||||
|
title_font.SetFamily(wxFONTFAMILY_ROMAN);
|
||||||
|
title_font.SetPointSize(22);
|
||||||
|
title->SetFont(title_font);
|
||||||
|
vsizer->Add(title, 0, wxEXPAND | wxTOP, wxGetApp().em_unit()/*50*/);
|
||||||
|
}
|
||||||
|
|
||||||
|
// main_info_text
|
||||||
|
wxFont font = get_default_font(this);
|
||||||
|
const auto text_clr = wxGetApp().get_label_clr_default();//wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT);
|
||||||
|
auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue()));
|
||||||
|
auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue()));
|
||||||
|
|
||||||
|
const int fs = font.GetPointSize() - 1;
|
||||||
|
int size[] = { static_cast<int>(fs*1.5), static_cast<int>(fs*1.4), static_cast<int>(fs*1.3), fs, fs, fs, fs };
|
||||||
|
|
||||||
|
m_html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_NEVER);
|
||||||
|
{
|
||||||
|
m_html->SetFonts(font.GetFaceName(), font.GetFaceName(), size);
|
||||||
|
m_html->SetBorders(2);
|
||||||
|
const auto text = wxString::Format(
|
||||||
|
"<html>"
|
||||||
|
"<body bgcolor= %s link= %s>"
|
||||||
|
"<font color=%s>"
|
||||||
|
"%s"
|
||||||
|
"</font>"
|
||||||
|
"</body>"
|
||||||
|
"</html>", bgr_clr_str, text_clr_str, text_clr_str,
|
||||||
|
get_main_info(true));
|
||||||
|
m_html->SetPage(text);
|
||||||
|
vsizer->Add(m_html, 1, wxEXPAND | wxBOTTOM, wxGetApp().em_unit());
|
||||||
|
}
|
||||||
|
|
||||||
|
// opengl_info
|
||||||
|
m_opengl_info_html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_AUTO);
|
||||||
|
{
|
||||||
|
m_opengl_info_html->SetMinSize(wxSize(-1, 16 * wxGetApp().em_unit()));
|
||||||
|
m_opengl_info_html->SetFonts(font.GetFaceName(), font.GetFaceName(), size);
|
||||||
|
m_opengl_info_html->SetBorders(10);
|
||||||
|
wxString blacklisted_libraries_message;
|
||||||
|
#ifdef WIN32
|
||||||
|
std::wstring blacklisted_libraries = BlacklistedLibraryCheck::get_instance().get_blacklisted_string().c_str();
|
||||||
|
if (! blacklisted_libraries.empty())
|
||||||
|
blacklisted_libraries_message = wxString("<br><b>") + _L("Blacklisted libraries loaded into OrcaSlicer process:") + "</b><br>" + blacklisted_libraries;
|
||||||
|
#endif // WIN32
|
||||||
|
const auto text = GUI::format_wxstr(
|
||||||
|
"<html>"
|
||||||
|
"<body bgcolor= %s link= %s>"
|
||||||
|
"<font color=%s>"
|
||||||
|
"%s<br>%s<br>%s<br>%s"
|
||||||
|
"</font>"
|
||||||
|
"</body>"
|
||||||
|
"</html>", bgr_clr_str, text_clr_str, text_clr_str,
|
||||||
|
blacklisted_libraries_message,
|
||||||
|
get_mem_info(true), wxGetApp().get_gl_info(false),
|
||||||
|
"<b>" + _L("SIMD is supported:") + "</b> " + Eigen::SimdInstructionSetsInUse());
|
||||||
|
|
||||||
|
m_opengl_info_html->SetPage(text);
|
||||||
|
main_sizer->Add(m_opengl_info_html, 1, wxEXPAND | wxBOTTOM, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxOK);
|
||||||
|
m_btn_copy_to_clipboard = new wxButton(this, wxID_ANY, _L("Copy to Clipboard"), wxDefaultPosition, wxDefaultSize);
|
||||||
|
|
||||||
|
buttons->Insert(0, m_btn_copy_to_clipboard, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 5);
|
||||||
|
m_btn_copy_to_clipboard->Bind(wxEVT_BUTTON, &SysInfoDialog::onCopyToClipboard, this);
|
||||||
|
|
||||||
|
this->SetEscapeId(wxID_OK);
|
||||||
|
this->Bind(wxEVT_BUTTON, &SysInfoDialog::onCloseDialog, this, wxID_OK);
|
||||||
|
main_sizer->Add(buttons, 0, wxEXPAND | wxRIGHT | wxBOTTOM, 3);
|
||||||
|
|
||||||
|
wxGetApp().UpdateDlgDarkUI(this, true);
|
||||||
|
|
||||||
|
// this->Bind(wxEVT_LEFT_DOWN, &SysInfoDialog::onCloseDialog, this);
|
||||||
|
// logo->Bind(wxEVT_LEFT_DOWN, &SysInfoDialog::onCloseDialog, this);
|
||||||
|
|
||||||
|
SetSizer(main_sizer);
|
||||||
|
main_sizer->SetSizeHints(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SysInfoDialog::on_dpi_changed(const wxRect &suggested_rect)
|
||||||
|
{
|
||||||
|
m_logo_bmp.msw_rescale();
|
||||||
|
m_logo->SetBitmap(m_logo_bmp.bmp());
|
||||||
|
|
||||||
|
wxFont font = get_default_font(this);
|
||||||
|
const int fs = font.GetPointSize() - 1;
|
||||||
|
int font_size[] = { static_cast<int>(fs*1.5), static_cast<int>(fs*1.4), static_cast<int>(fs*1.3), fs, fs, fs, fs };
|
||||||
|
|
||||||
|
m_html->SetFonts(font.GetFaceName(), font.GetFaceName(), font_size);
|
||||||
|
m_html->Refresh();
|
||||||
|
|
||||||
|
const int& em = em_unit();
|
||||||
|
|
||||||
|
msw_buttons_rescale(this, em, { wxID_OK, m_btn_copy_to_clipboard->GetId() });
|
||||||
|
|
||||||
|
m_opengl_info_html->SetMinSize(wxSize(-1, 16 * em));
|
||||||
|
m_opengl_info_html->SetFonts(font.GetFaceName(), font.GetFaceName(), font_size);
|
||||||
|
m_opengl_info_html->Refresh();
|
||||||
|
|
||||||
|
const wxSize& size = wxSize(65 * em, 55 * em);
|
||||||
|
|
||||||
|
SetMinSize(size);
|
||||||
|
Fit();
|
||||||
|
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SysInfoDialog::onCopyToClipboard(wxEvent &)
|
||||||
|
{
|
||||||
|
wxTheClipboard->Open();
|
||||||
|
const auto text = get_main_info(false) + "\n" + wxGetApp().get_gl_info(true);
|
||||||
|
wxTheClipboard->SetData(new wxTextDataObject(text));
|
||||||
|
wxTheClipboard->Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SysInfoDialog::onCloseDialog(wxEvent &)
|
||||||
|
{
|
||||||
|
this->EndModal(wxID_CLOSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace GUI
|
||||||
|
} // namespace Slic3r
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#ifndef slic3r_GUI_SysInfoDialog_hpp_
|
||||||
|
#define slic3r_GUI_SysInfoDialog_hpp_
|
||||||
|
|
||||||
|
#include <wx/event.h>
|
||||||
|
#include <wx/wx.h>
|
||||||
|
#include <wx/html/htmlwin.h>
|
||||||
|
|
||||||
|
#include "GUI_Utils.hpp"
|
||||||
|
#include "wxExtensions.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace GUI {
|
||||||
|
|
||||||
|
class SysInfoDialog : public DPIDialog
|
||||||
|
{
|
||||||
|
ScalableBitmap m_logo_bmp;
|
||||||
|
wxStaticBitmap* m_logo;
|
||||||
|
wxHtmlWindow* m_opengl_info_html;
|
||||||
|
wxHtmlWindow* m_html;
|
||||||
|
|
||||||
|
wxButton* m_btn_copy_to_clipboard;
|
||||||
|
|
||||||
|
public:
|
||||||
|
SysInfoDialog();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void on_dpi_changed(const wxRect &suggested_rect) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onCopyToClipboard(wxEvent &);
|
||||||
|
void onCloseDialog(wxEvent &);
|
||||||
|
};
|
||||||
|
} // namespace GUI
|
||||||
|
} // namespace Slic3r
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -939,7 +939,6 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
|
|||||||
m_active_edge = (hit.first >= 0 && is_edge_selected(hit)) ? hit : std::pair<int, int>{ -1, -1 };
|
m_active_edge = (hit.first >= 0 && is_edge_selected(hit)) ? hit : std::pair<int, int>{ -1, -1 };
|
||||||
m_gesture = (m_active_edge.first >= 0) ? Gesture::MoveEdge : Gesture::Pan;
|
m_gesture = (m_active_edge.first >= 0) ? Gesture::MoveEdge : Gesture::Pan;
|
||||||
} else {
|
} else {
|
||||||
const std::vector<int> selection_before = m_selection;
|
|
||||||
const int hit = island_at(uv);
|
const int hit = island_at(uv);
|
||||||
if (hit >= 0) {
|
if (hit >= 0) {
|
||||||
if (evt.ShiftDown()) {
|
if (evt.ShiftDown()) {
|
||||||
@@ -970,10 +969,6 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
|
|||||||
m_selected_island = -1;
|
m_selected_island = -1;
|
||||||
}
|
}
|
||||||
m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan;
|
m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan;
|
||||||
// The owner shows the selection on the model and would not hear of this click otherwise in time: a
|
|
||||||
// click that clears the selection starts no gesture, and one that sets it reports only on release.
|
|
||||||
if (m_selection != selection_before)
|
|
||||||
run_command(Command::SelectionChanged);
|
|
||||||
}
|
}
|
||||||
grab_mouse();
|
grab_mouse();
|
||||||
Refresh();
|
Refresh();
|
||||||
|
|||||||
@@ -108,12 +108,10 @@ public:
|
|||||||
// the command callback - the canvas has the selection and the view, the gizmo has the layer.
|
// the command callback - the canvas has the selection and the view, the gizmo has the layer.
|
||||||
// `value` carries the new setting for the Set* commands (an angle, a flag as 0/1, or an index into
|
// `value` carries the new setting for the Set* commands (an angle, a flag as 0/1, or an index into
|
||||||
// SelectMode / Background) and is 0 otherwise.
|
// SelectMode / Background) and is 0 otherwise.
|
||||||
// SelectionChanged is sent by the canvas itself when a click changes the island selection. It carries
|
|
||||||
// nothing: the owner reads selected_islands() back, and only needs telling that it should look.
|
|
||||||
enum class Command {
|
enum class Command {
|
||||||
FrameAll, ToggleSnap, AverageScale, CutSelectedIsland, ProjectFromView, JoinSelected, UnjoinSelected,
|
FrameAll, ToggleSnap, AverageScale, CutSelectedIsland, ProjectFromView, JoinSelected, UnjoinSelected,
|
||||||
Unwrap, SetSeamAngle, SetConnectIslands, SetSelectMode, SetMarkSeams, SetSeamPath, ClearSeams, ClearUVEdits,
|
Unwrap, SetSeamAngle, SetConnectIslands, SetSelectMode, SetMarkSeams, SetSeamPath, ClearSeams, ClearUVEdits,
|
||||||
SetBackground, PickTexture, PaneClosed, SelectionChanged
|
SetBackground, PickTexture, PaneClosed
|
||||||
};
|
};
|
||||||
void run_command(Command cmd, float value = 0.f);
|
void run_command(Command cmd, float value = 0.f);
|
||||||
using CommandFn = std::function<void(Command, float)>;
|
using CommandFn = std::function<void(Command, float)>;
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ WebDialog::WebDialog(wxWindow* parent,
|
|||||||
SubmitHandler on_submit,
|
SubmitHandler on_submit,
|
||||||
CloseHandler on_close,
|
CloseHandler on_close,
|
||||||
CloseHandler on_destroyed,
|
CloseHandler on_destroyed,
|
||||||
long wx_style)
|
long wx_style,
|
||||||
|
const wxString& profile)
|
||||||
: WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size, wx_style)
|
: WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size, wx_style)
|
||||||
, m_html(html)
|
, m_html(html)
|
||||||
, m_on_message(std::move(on_message))
|
, m_on_message(std::move(on_message))
|
||||||
@@ -37,7 +38,7 @@ WebDialog::WebDialog(wxWindow* parent,
|
|||||||
{
|
{
|
||||||
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
|
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
|
||||||
// is swapped in via SetPage once the bootstrap finishes loading.
|
// is swapped in via SetPage once the bootstrap finishes loading.
|
||||||
create_webview(web_hosting::BOOTSTRAP_PAGE, title, size, wxSize(320, 240));
|
create_webview(web_hosting::BOOTSTRAP_PAGE, title, size, wxSize(320, 240), profile);
|
||||||
|
|
||||||
// Paint the window/webview in the themed background so there is no white
|
// Paint the window/webview in the themed background so there is no white
|
||||||
// flash before the (transparent) bootstrap page and plugin HTML render.
|
// flash before the (transparent) bootstrap page and plugin HTML render.
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ public:
|
|||||||
SubmitHandler on_submit,
|
SubmitHandler on_submit,
|
||||||
CloseHandler on_close,
|
CloseHandler on_close,
|
||||||
CloseHandler on_destroyed,
|
CloseHandler on_destroyed,
|
||||||
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
|
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER,
|
||||||
|
const wxString& profile = wxEmptyString);
|
||||||
~WebDialog() override;
|
~WebDialog() override;
|
||||||
|
|
||||||
static void post_message(WebDialog* dialog, const nlohmann::json& data);
|
static void post_message(WebDialog* dialog, const nlohmann::json& data);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
namespace Slic3r { namespace GUI {
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
WebPanel::WebPanel(wxWindow* parent, const char* bridge_script)
|
WebPanel::WebPanel(wxWindow* parent, const char* bridge_script, const wxString& profile)
|
||||||
: wxPanel(parent, wxID_ANY)
|
: wxPanel(parent, wxID_ANY)
|
||||||
{
|
{
|
||||||
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
||||||
@@ -25,7 +25,7 @@ WebPanel::WebPanel(wxWindow* parent, const char* bridge_script)
|
|||||||
SetSizer(sizer);
|
SetSizer(sizer);
|
||||||
|
|
||||||
// Never null: WebView::CreateWebView substitutes a placeholder view when no backend is available.
|
// Never null: WebView::CreateWebView substitutes a placeholder view when no backend is available.
|
||||||
m_browser = WebView::CreateWebView(this, web_hosting::bootstrap_url());
|
m_browser = WebView::CreateWebView(this, web_hosting::bootstrap_url(), profile);
|
||||||
m_browser->SetBackgroundColour(GetBackgroundColour());
|
m_browser->SetBackgroundColour(GetBackgroundColour());
|
||||||
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script()));
|
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script()));
|
||||||
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script()));
|
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script()));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <wx/event.h>
|
#include <wx/event.h>
|
||||||
#include <wx/panel.h>
|
#include <wx/panel.h>
|
||||||
|
#include <wx/string.h>
|
||||||
#include <wx/webview.h>
|
#include <wx/webview.h>
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
@@ -22,7 +23,7 @@ namespace Slic3r { namespace GUI {
|
|||||||
class WebPanel : public wxPanel
|
class WebPanel : public wxPanel
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
WebPanel(wxWindow* parent, const char* bridge_script);
|
WebPanel(wxWindow* parent, const char* bridge_script, const wxString& profile = wxEmptyString);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
wxWebView* browser() const { return m_browser; }
|
wxWebView* browser() const { return m_browser; }
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include "slic3r/GUI/GUI_App.hpp"
|
#include "slic3r/GUI/GUI_App.hpp"
|
||||||
#include "slic3r/Utils/MacDarkMode.hpp"
|
#include "slic3r/Utils/MacDarkMode.hpp"
|
||||||
|
|
||||||
|
#include <libslic3r/Utils.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@
|
|||||||
#include <exception>
|
#include <exception>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
|
#include <wx/chartype.h>
|
||||||
#include <wx/setup.h>
|
#include <wx/setup.h>
|
||||||
#include <wx/webview.h>
|
#include <wx/webview.h>
|
||||||
#include <wx/string.h>
|
#include <wx/string.h>
|
||||||
@@ -72,6 +75,26 @@ webkit_javascript_result_unref (WebKitJavascriptResult *js_result);
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Private storage root for an isolated (plugin) profile. Plugin pages must not reach the shared
|
||||||
|
// default store's cookies/session (e.g. the OrcaCloud login), so each profile gets its own dir.
|
||||||
|
// macOS isolates with an in-memory store instead (see CreateWebView), so this is unused there.
|
||||||
|
#if !defined(__WXOSX__)
|
||||||
|
static wxString isolated_profile_path(const wxString& profile)
|
||||||
|
{
|
||||||
|
wxString safe;
|
||||||
|
safe.reserve(profile.size());
|
||||||
|
for (wxString::const_iterator it = profile.begin(); it != profile.end(); ++it) {
|
||||||
|
const wxChar c = *it;
|
||||||
|
const bool keep = (c >= wxT('a') && c <= wxT('z')) || (c >= wxT('A') && c <= wxT('Z')) ||
|
||||||
|
(c >= wxT('0') && c <= wxT('9')) || c == wxT('.') || c == wxT('-') || c == wxT('_');
|
||||||
|
safe += keep ? c : wxT('_');
|
||||||
|
}
|
||||||
|
if (safe.empty())
|
||||||
|
safe = wxT("default");
|
||||||
|
return wxString::FromUTF8((boost::filesystem::path(Slic3r::data_dir()) / "web_profiles" / safe.ToStdString()).string());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef __WIN32__
|
#ifdef __WIN32__
|
||||||
// Run Download and Install in another thread so we don't block the UI thread
|
// Run Download and Install in another thread so we don't block the UI thread
|
||||||
DWORD DownloadAndInstallWV2RT() {
|
DWORD DownloadAndInstallWV2RT() {
|
||||||
@@ -135,6 +158,9 @@ DWORD DownloadAndInstallWV2RT() {
|
|||||||
class WebViewEdge : public wxWebViewEdge
|
class WebViewEdge : public wxWebViewEdge
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
WebViewEdge() = default;
|
||||||
|
explicit WebViewEdge(const wxWebViewConfiguration& config) : wxWebViewEdge(config) {}
|
||||||
|
|
||||||
bool SetUserAgent(const wxString &userAgent) override
|
bool SetUserAgent(const wxString &userAgent) override
|
||||||
{
|
{
|
||||||
bool dark = userAgent.Contains("dark");
|
bool dark = userAgent.Contains("dark");
|
||||||
@@ -213,6 +239,11 @@ public:
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
explicit WebViewWebKit(const wxWebViewConfiguration& config)
|
||||||
|
: wxWebViewWebKit(config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
~WebViewWebKit() override
|
~WebViewWebKit() override
|
||||||
{
|
{
|
||||||
RemoveScriptMessageHandler("wx");
|
RemoveScriptMessageHandler("wx");
|
||||||
@@ -284,7 +315,7 @@ static WebViewRef *webview_ref(wxWebView *webView)
|
|||||||
return webView ? static_cast<WebViewRef *>(webView->GetRefData()) : nullptr;
|
return webView ? static_cast<WebViewRef *>(webView->GetRefData()) : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
|
wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url, wxString const & profile)
|
||||||
{
|
{
|
||||||
#if wxUSE_WEBVIEW_EDGE
|
#if wxUSE_WEBVIEW_EDGE
|
||||||
// Check if a fixed version of edge is present in
|
// Check if a fixed version of edge is present in
|
||||||
@@ -304,12 +335,36 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
|
|||||||
if (!url2.empty()) { url2 = wxURI(url2).BuildURI(); }
|
if (!url2.empty()) { url2 = wxURI(url2).BuildURI(); }
|
||||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << url2.ToUTF8();
|
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << url2.ToUTF8();
|
||||||
|
|
||||||
|
const bool isolated = !profile.IsEmpty();
|
||||||
|
|
||||||
#ifdef __WIN32__
|
#ifdef __WIN32__
|
||||||
wxWebView* webView = new WebViewEdge;
|
wxWebView* webView = nullptr;
|
||||||
|
if (isolated) {
|
||||||
|
wxWebViewConfiguration config = wxWebView::NewConfiguration(wxWebViewBackendEdge);
|
||||||
|
config.SetDataPath(isolated_profile_path(profile));
|
||||||
|
webView = new WebViewEdge(config);
|
||||||
|
} else {
|
||||||
|
webView = new WebViewEdge;
|
||||||
|
}
|
||||||
#elif defined(__WXOSX__)
|
#elif defined(__WXOSX__)
|
||||||
wxWebView* webView = new WebViewWebKit;
|
wxWebView* webView = nullptr;
|
||||||
|
if (isolated) {
|
||||||
|
// wx has no SetDataPath on macOS; give the view its own in-memory store instead.
|
||||||
|
wxWebViewConfiguration config = wxWebView::NewConfiguration(wxWebViewBackendWebKit);
|
||||||
|
Slic3r::GUI::WKWebViewConfiguration_setNonPersistentDataStore(config.GetNativeConfiguration());
|
||||||
|
webView = new WebViewWebKit(config);
|
||||||
|
} else {
|
||||||
|
webView = new WebViewWebKit;
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
auto webView = wxWebView::New();
|
wxWebView* webView = nullptr;
|
||||||
|
if (isolated) {
|
||||||
|
wxWebViewConfiguration config = wxWebView::NewConfiguration(wxWebViewBackendWebKit);
|
||||||
|
config.SetDataPath(isolated_profile_path(profile));
|
||||||
|
webView = wxWebView::New(config);
|
||||||
|
} else {
|
||||||
|
webView = wxWebView::New();
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
if (webView) {
|
if (webView) {
|
||||||
webView->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
webView->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ wxDECLARE_EVENT(EVT_WEBVIEW_RECREATED, wxCommandEvent);
|
|||||||
class WebView
|
class WebView
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static wxWebView *CreateWebView(wxWindow *parent, wxString const &url);
|
// `profile` empty -> the shared default backend store used by all trusted UI.
|
||||||
|
// `profile` non-empty -> a private store keyed by `profile`, so this view shares no
|
||||||
|
// cookies/storage/session with the shared store or any other profile. Used for
|
||||||
|
// plugin-supplied (untrusted) content.
|
||||||
|
static wxWebView *CreateWebView(wxWindow *parent, wxString const &url, wxString const &profile = wxEmptyString);
|
||||||
#if wxUSE_WEBVIEW_EDGE
|
#if wxUSE_WEBVIEW_EDGE
|
||||||
static bool CheckWebViewRuntime();
|
static bool CheckWebViewRuntime();
|
||||||
static bool DownloadAndInstallWebViewRuntime();
|
static bool DownloadAndInstallWebViewRuntime();
|
||||||
|
|||||||
@@ -172,7 +172,8 @@ WebViewHostDialog::WebViewHostDialog(wxWindow* parent,
|
|||||||
bool WebViewHostDialog::create_webview(const std::string& resource_path,
|
bool WebViewHostDialog::create_webview(const std::string& resource_path,
|
||||||
const wxString& title,
|
const wxString& title,
|
||||||
const wxSize& dialog_size,
|
const wxSize& dialog_size,
|
||||||
const wxSize& min_size)
|
const wxSize& min_size,
|
||||||
|
const wxString& profile)
|
||||||
{
|
{
|
||||||
SetTitle(title);
|
SetTitle(title);
|
||||||
SetMinSize(FromDIP(min_size));
|
SetMinSize(FromDIP(min_size));
|
||||||
@@ -181,7 +182,7 @@ bool WebViewHostDialog::create_webview(const std::string& resource_path,
|
|||||||
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
|
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
// Created empty and loaded below, after the user scripts are added, so the first load already has them.
|
// Created empty and loaded below, after the user scripts are added, so the first load already has them.
|
||||||
m_browser = WebView::CreateWebView(this, wxEmptyString);
|
m_browser = WebView::CreateWebView(this, wxEmptyString, profile);
|
||||||
if (m_browser == nullptr) {
|
if (m_browser == nullptr) {
|
||||||
wxLogError("Could not init m_browser");
|
wxLogError("Could not init m_browser");
|
||||||
delete topsizer;
|
delete topsizer;
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ public:
|
|||||||
bool create_webview(const std::string& resource_path,
|
bool create_webview(const std::string& resource_path,
|
||||||
const wxString& title,
|
const wxString& title,
|
||||||
const wxSize& dialog_size = wxSize(820, 660),
|
const wxSize& dialog_size = wxSize(820, 660),
|
||||||
const wxSize& min_size = wxSize(640, 640));
|
const wxSize& min_size = wxSize(640, 640),
|
||||||
|
const wxString& profile = wxEmptyString);
|
||||||
|
|
||||||
void load_url(const wxString& url);
|
void load_url(const wxString& url);
|
||||||
bool run_script(const wxString& script);
|
bool run_script(const wxString& script);
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#ifndef slic3r_wxinit_hpp_
|
||||||
|
#define slic3r_wxinit_hpp_
|
||||||
|
|
||||||
|
#include <wx/wx.h>
|
||||||
|
#include <wx/intl.h>
|
||||||
|
#include <wx/html/htmlwin.h>
|
||||||
|
|
||||||
|
// Perl redefines a _ macro, so we undef this one
|
||||||
|
#undef _
|
||||||
|
|
||||||
|
// We do want to use translation however, so define it as __ so we can do a find/replace
|
||||||
|
// later when we no longer need to undef _
|
||||||
|
#define __(s) wxGetTranslation((s))
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -12,6 +12,7 @@ extern double mac_max_scaling_factor();
|
|||||||
extern void set_miniaturizable(void * window);
|
extern void set_miniaturizable(void * window);
|
||||||
void WKWebView_evaluateJavaScript(void * web, wxString const & script, void (*callback)(wxString const &));
|
void WKWebView_evaluateJavaScript(void * web, wxString const & script, void (*callback)(wxString const &));
|
||||||
void WKWebView_setTransparentBackground(void * web);
|
void WKWebView_setTransparentBackground(void * web);
|
||||||
|
void WKWebViewConfiguration_setNonPersistentDataStore(void * configuration);
|
||||||
void WKWebView_force_display(void * web);
|
void WKWebView_force_display(void * web);
|
||||||
void set_tag_when_enter_full_screen(bool isfullscreen);
|
void set_tag_when_enter_full_screen(bool isfullscreen);
|
||||||
void set_title_colour_after_set_title(void * window);
|
void set_title_colour_after_set_title(void * window);
|
||||||
|
|||||||
@@ -102,6 +102,15 @@ void WKWebView_setTransparentBackground(void * web)
|
|||||||
[webView registerForDraggedTypes: @[NSFilenamesPboardType]];
|
[webView registerForDraggedTypes: @[NSFilenamesPboardType]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Give a WKWebViewConfiguration its own in-memory website data store, so the view's cookies and
|
||||||
|
// storage neither come from nor leak into the shared default store. Used to isolate plugin web views.
|
||||||
|
void WKWebViewConfiguration_setNonPersistentDataStore(void * configuration)
|
||||||
|
{
|
||||||
|
WKWebViewConfiguration * config = (WKWebViewConfiguration*)configuration;
|
||||||
|
if (config)
|
||||||
|
config.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
|
||||||
|
}
|
||||||
|
|
||||||
// Force a WKWebView to re-lay-out and repaint. Needed for chrome-less popups: the window is
|
// Force a WKWebView to re-lay-out and repaint. Needed for chrome-less popups: the window is
|
||||||
// transparent, so a WKWebView whose layer has no pending frame leaves the whole window invisible
|
// transparent, so a WKWebView whose layer has no pending frame leaves the whole window invisible
|
||||||
// until the user generates input (scroll/arrow). setNeedsDisplay alone does not always reach the
|
// until the user generates input (scroll/arrow). setNeedsDisplay alone does not always reach the
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#ifndef slic3r_GUI_Profile_hpp_
|
||||||
|
#define slic3r_GUI_Profile_hpp_
|
||||||
|
|
||||||
|
// Profiling support using the Shiny intrusive profiler
|
||||||
|
//#define SLIC3R_PROFILE_GUI
|
||||||
|
#if defined(SLIC3R_PROFILE) && defined(SLIC3R_PROFILE_GUI)
|
||||||
|
#include <Shiny/Shiny.h>
|
||||||
|
#define SLIC3R_GUI_PROFILE_FUNC() PROFILE_FUNC()
|
||||||
|
#define SLIC3R_GUI_PROFILE_BLOCK(name) PROFILE_BLOCK(name)
|
||||||
|
#define SLIC3R_GUI_PROFILE_UPDATE() PROFILE_UPDATE()
|
||||||
|
#define SLIC3R_GUI_PROFILE_OUTPUT(x) PROFILE_OUTPUT(x)
|
||||||
|
#else
|
||||||
|
#define SLIC3R_GUI_PROFILE_FUNC()
|
||||||
|
#define SLIC3R_GUI_PROFILE_BLOCK(name)
|
||||||
|
#define SLIC3R_GUI_PROFILE_UPDATE()
|
||||||
|
#define SLIC3R_GUI_PROFILE_OUTPUT(x)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // slic3r_GUI_Profile_hpp_
|
||||||
@@ -390,7 +390,8 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
|
|||||||
|
|
||||||
auto* dlg = new GUI::WebDialog(ui_parent(), wxString::FromUTF8(title), html,
|
auto* dlg = new GUI::WebDialog(ui_parent(), wxString::FromUTF8(title), html,
|
||||||
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
|
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
|
||||||
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE);
|
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE,
|
||||||
|
wxString::FromUTF8(plugin_key));
|
||||||
UiRegistry::instance().bind(new_id, dlg, plugin_key);
|
UiRegistry::instance().bind(new_id, dlg, plugin_key);
|
||||||
if (modal) {
|
if (modal) {
|
||||||
dlg->ShowModal();
|
dlg->ShowModal();
|
||||||
@@ -487,7 +488,7 @@ py::object ui_create_dock_panel(const std::string& html, const std::string& titl
|
|||||||
|
|
||||||
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
||||||
auto* panel = new GUI::DockPanel(plater, html, std::move(msg_adapter), make_close_adapter(close_holder),
|
auto* panel = new GUI::DockPanel(plater, html, std::move(msg_adapter), make_close_adapter(close_holder),
|
||||||
std::move(on_destroyed));
|
std::move(on_destroyed), wxString::FromUTF8(plugin_key));
|
||||||
UiRegistry::instance().bind(new_id, panel, plugin_key);
|
UiRegistry::instance().bind(new_id, panel, plugin_key);
|
||||||
plater->add_dock_pane(panel, GUI::plugin_pane_name(plugin_key, title), wxString::FromUTF8(title), dock,
|
plater->add_dock_pane(panel, GUI::plugin_pane_name(plugin_key, title), wxString::FromUTF8(title), dock,
|
||||||
wxSize(w, h), [panel]() { panel->fire_close(); });
|
wxSize(w, h), [panel]() { panel->fire_close(); });
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ constexpr char PLUGIN_PAGE_BRIDGE_JS[] = R"JS(
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
|
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
|
||||||
: GUI::WebPanel(parent, PLUGIN_PAGE_BRIDGE_JS)
|
: GUI::WebPanel(parent, PLUGIN_PAGE_BRIDGE_JS,
|
||||||
|
wxString::FromUTF8(capability ? capability->identity().plugin_key : std::string()))
|
||||||
, m_cap(std::move(capability))
|
, m_cap(std::move(capability))
|
||||||
, m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this))
|
, m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||||
#include "libslic3r/Exception.hpp"
|
#include "libslic3r/Exception.hpp"
|
||||||
#include "libslic3r/LifecycleEvents.hpp"
|
#include "libslic3r/LifecycleEvents.hpp"
|
||||||
#include "libslic3r/Utils.hpp"
|
|
||||||
|
|
||||||
#include "test_helpers.hpp"
|
#include "test_helpers.hpp"
|
||||||
#include "test_utils.hpp"
|
#include "test_utils.hpp"
|
||||||
@@ -157,81 +156,6 @@ SCENARIO("Changing the number of solid shell layers does not make all surfaces i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two copies of one object share a single slice. When a copy that has sliced itself becomes identical to an
|
|
||||||
// earlier object, it keeps its own layers while that object slices and only then takes the shared layers over.
|
|
||||||
TEST_CASE("An object that starts sharing another object's slices keeps its own layers until it takes them over", "[Print][Regression]")
|
|
||||||
{
|
|
||||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
|
||||||
config.set_deserialize_strict({
|
|
||||||
{ "layer_height", 0.2 },
|
|
||||||
{ "initial_layer_print_height", 0.2 },
|
|
||||||
{ "raft_layers", 1 }, // each object gets support layers of its own
|
|
||||||
{ "sparse_infill_density", "20%" }, // object_a's override below differs from this
|
|
||||||
});
|
|
||||||
// Activates the slicing-pipeline hook installed for the second slice below.
|
|
||||||
config.set_key_value("slicing_pipeline_plugin", new ConfigOptionStrings({"probe"}));
|
|
||||||
|
|
||||||
Print print;
|
|
||||||
Model model;
|
|
||||||
init_print({cube(20)}, print, model, config);
|
|
||||||
ModelObject *object_a = model.objects.front();
|
|
||||||
// add_object() gives the copy extruder 1, and objects only share slices when their configs match.
|
|
||||||
object_a->config.set_key_value("extruder", new ConfigOptionInt(1));
|
|
||||||
ModelObject *object_b = model.add_object(*object_a); // shares object_a's mesh
|
|
||||||
for (ModelInstance *instance : object_b->instances)
|
|
||||||
instance->set_offset(instance->get_offset() + Vec3d(40., 0., 0.));
|
|
||||||
// A per-object override makes the objects differ, so each slices itself.
|
|
||||||
object_a->config.set_key_value("sparse_infill_density", new ConfigOptionPercent(40));
|
|
||||||
|
|
||||||
print.apply(model, config);
|
|
||||||
print.process();
|
|
||||||
REQUIRE(print.objects().size() == 2);
|
|
||||||
// The non-const accessors hand out the layer vectors themselves, which the checks below compare.
|
|
||||||
PrintObject *print_b = print.get_object(1);
|
|
||||||
REQUIRE(print_b->model_object()->id() == object_b->id()); // Print slices its own copy of the model
|
|
||||||
REQUIRE(print_b->get_shared_object() == nullptr);
|
|
||||||
const LayerPtrs own_layers = print_b->layers();
|
|
||||||
const SupportLayerPtrs own_support_layers = print_b->support_layers();
|
|
||||||
REQUIRE(!own_layers.empty());
|
|
||||||
REQUIRE(!own_support_layers.empty());
|
|
||||||
|
|
||||||
// Without the override the objects are identical again: print_a redoes the steps the setting invalidated,
|
|
||||||
// and print_b shares it.
|
|
||||||
object_a->config.erase("sparse_infill_density");
|
|
||||||
print.apply(model, config);
|
|
||||||
REQUIRE(print.objects().size() == 2);
|
|
||||||
PrintObject *print_a = print.get_object(0);
|
|
||||||
|
|
||||||
// Erasing the override invalidates print_a's posPrepareInfill and the steps after it. Its hook is the first one
|
|
||||||
// print_a fires, and it runs after print_b starts sharing but before print_b takes the shared layers over.
|
|
||||||
bool hook_fired = false;
|
|
||||||
bool shared_while_a_runs = false;
|
|
||||||
bool kept_layers = false;
|
|
||||||
bool kept_support_layers = false;
|
|
||||||
{
|
|
||||||
Print::set_slicing_pipeline_hook_fn([&](Print &, const PrintObject *object, SlicingPipelineStepPlugin) {
|
|
||||||
if (object != print_a || hook_fired)
|
|
||||||
return;
|
|
||||||
hook_fired = true;
|
|
||||||
shared_while_a_runs = print_b->get_shared_object() == print_a;
|
|
||||||
kept_layers = print_b->layers() == own_layers;
|
|
||||||
kept_support_layers = print_b->support_layers() == own_support_layers;
|
|
||||||
});
|
|
||||||
const ScopeGuard reset_hook([] { Print::set_slicing_pipeline_hook_fn(nullptr); });
|
|
||||||
print.process();
|
|
||||||
}
|
|
||||||
|
|
||||||
REQUIRE(hook_fired);
|
|
||||||
CHECK(print.objects()[1] == print_b);
|
|
||||||
CHECK(shared_while_a_runs);
|
|
||||||
CHECK(kept_layers);
|
|
||||||
CHECK(kept_support_layers);
|
|
||||||
CHECK(print_b->get_shared_object() == print_a);
|
|
||||||
REQUIRE(!print_a->layers().empty());
|
|
||||||
CHECK(print_b->layers() == print_a->layers());
|
|
||||||
CHECK(print_b->support_layers() == print_a->support_layers());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Print::validate() warning collection
|
// Print::validate() warning collection
|
||||||
//
|
//
|
||||||
|
|||||||
+13
-178
@@ -28,7 +28,6 @@
|
|||||||
#include <miniz.h>
|
#include <miniz.h>
|
||||||
#include "libslic3r/TriangleMesh.hpp"
|
#include "libslic3r/TriangleMesh.hpp"
|
||||||
#include "libslic3r/TriangleSelector.hpp"
|
#include "libslic3r/TriangleSelector.hpp"
|
||||||
#include "libslic3r/TextureDisplacement.hpp"
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <ios>
|
#include <ios>
|
||||||
@@ -39,7 +38,6 @@
|
|||||||
#include <boost/algorithm/string/predicate.hpp>
|
#include <boost/algorithm/string/predicate.hpp>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <initializer_list>
|
|
||||||
|
|
||||||
#include <catch2/catch_tostring.hpp>
|
#include <catch2/catch_tostring.hpp>
|
||||||
#include <Eigen/Core>
|
#include <Eigen/Core>
|
||||||
@@ -351,24 +349,6 @@ static bool replace_in_3mf_entry(const std::string& path, const std::string& suf
|
|||||||
return replaced;
|
return replaced;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stores `model` as a one-plate project.
|
|
||||||
static void store_project(Model& model, const std::string& path, SaveStrategy strategy)
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir backup_dir("orca_paint_src");
|
|
||||||
model.set_backup_path(backup_dir.string());
|
|
||||||
|
|
||||||
DynamicPrintConfig cfg;
|
|
||||||
PlateData plate;
|
|
||||||
plate.plate_index = 0;
|
|
||||||
StoreParams sp;
|
|
||||||
sp.path = path.c_str();
|
|
||||||
sp.model = &model;
|
|
||||||
sp.config = &cfg;
|
|
||||||
sp.strategy = strategy;
|
|
||||||
sp.plate_data_list.push_back(&plate);
|
|
||||||
REQUIRE(store_bbs_3mf(sp));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stores a one-plate project holding a cube whose first two facets are painted Extruder2 and
|
// Stores a one-plate project holding a cube whose first two facets are painted Extruder2 and
|
||||||
// Extruder3, which the exporter writes as paint_color="8" and paint_color="0C".
|
// Extruder3, which the exporter writes as paint_color="8" and paint_color="0C".
|
||||||
static void store_painted_cube(const std::string& path)
|
static void store_painted_cube(const std::string& path)
|
||||||
@@ -383,7 +363,19 @@ static void store_painted_cube(const std::string& path)
|
|||||||
selector.set_facet(1, EnforcerBlockerType::Extruder3);
|
selector.set_facet(1, EnforcerBlockerType::Extruder3);
|
||||||
REQUIRE(volume->mmu_segmentation_facets.set(selector));
|
REQUIRE(volume->mmu_segmentation_facets.set(selector));
|
||||||
}
|
}
|
||||||
store_project(model, path, SaveStrategy::Zip64 | SaveStrategy::Silence);
|
ScopedTemporaryDir backup_dir("orca_paint_src");
|
||||||
|
model.set_backup_path(backup_dir.string());
|
||||||
|
|
||||||
|
DynamicPrintConfig cfg;
|
||||||
|
PlateData plate;
|
||||||
|
plate.plate_index = 0;
|
||||||
|
StoreParams sp;
|
||||||
|
sp.path = path.c_str();
|
||||||
|
sp.model = &model;
|
||||||
|
sp.config = &cfg;
|
||||||
|
sp.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||||
|
sp.plate_data_list.push_back(&plate);
|
||||||
|
REQUIRE(store_bbs_3mf(sp));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads `path` through the BBS importer into `model`, releasing the plates it returns. The
|
// Loads `path` through the BBS importer into `model`, releasing the plates it returns. The
|
||||||
@@ -509,163 +501,6 @@ TEST_CASE("A project with malformed paint data loads without the damaged facet",
|
|||||||
REQUIRE(selector.num_facets(EnforcerBlockerType::Extruder3) == 1);
|
REQUIRE(selector.num_facets(EnforcerBlockerType::Extruder3) == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paints `triangles` of `volume` on texture displacement layer slot `slot`.
|
|
||||||
static void paint_texture_slot(ModelVolume& volume, int slot, std::initializer_list<int> triangles)
|
|
||||||
{
|
|
||||||
TriangleSelector selector(volume.mesh());
|
|
||||||
for (const int triangle : triangles)
|
|
||||||
selector.set_facet(triangle, EnforcerBlockerType::ENFORCER);
|
|
||||||
REQUIRE(volume.texture_displacement_facet(slot).set(selector));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every triangle's texture paint on every slot, in the form the exporter writes it, so two volumes
|
|
||||||
// compare equal only when the same triangles are painted on the same slots.
|
|
||||||
static std::vector<std::string> texture_paint(const ModelVolume& volume)
|
|
||||||
{
|
|
||||||
std::vector<std::string> paint;
|
|
||||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
|
||||||
for (size_t i = 0; i < volume.mesh().its.indices.size(); ++i)
|
|
||||||
paint.push_back(volume.texture_displacement_facet(slot).get_triangle_as_string(int(i)));
|
|
||||||
return paint;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A cube with one texture displacement layer in slot 2 and texture paint on gapped triangles of two
|
|
||||||
// slots. The layer's image bytes need not decode: the archive stores them as they are.
|
|
||||||
static ModelVolume& add_texture_displaced_cube(Model& model)
|
|
||||||
{
|
|
||||||
ModelObject* object = model.add_object();
|
|
||||||
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
|
|
||||||
object->add_instance();
|
|
||||||
|
|
||||||
TextureDisplacementLayer layer;
|
|
||||||
layer.slot = 2;
|
|
||||||
layer.name = "Bark";
|
|
||||||
layer.depth_mm = 1.25f;
|
|
||||||
layer.image_data = std::make_shared<std::vector<unsigned char>>(std::vector<unsigned char>{ 0x89, 'P', 'N', 'G', 1, 2, 3 });
|
|
||||||
volume->texture_displacement_layers.push_back(layer);
|
|
||||||
volume->texture_displacement_options.active_slot = 2;
|
|
||||||
|
|
||||||
paint_texture_slot(*volume, 2, { 3, 7 });
|
|
||||||
paint_texture_slot(*volume, 5, { 10 });
|
|
||||||
return *volume;
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Texture displacement layers, textures and paint survive a project round trip", "[3mf]")
|
|
||||||
{
|
|
||||||
// A single model file, and one file per object (read by a separate importer).
|
|
||||||
const SaveStrategy strategy = GENERATE(SaveStrategy::Zip64 | SaveStrategy::Silence,
|
|
||||||
SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::SplitModel);
|
|
||||||
INFO("save strategy " << int(strategy));
|
|
||||||
|
|
||||||
ScopedTemporaryFile temp(".3mf");
|
|
||||||
Model source;
|
|
||||||
const ModelVolume& painted = add_texture_displaced_cube(source);
|
|
||||||
const std::vector<std::string> paint = texture_paint(painted);
|
|
||||||
REQUIRE(std::count_if(paint.begin(), paint.end(), [](const std::string& s) { return !s.empty(); }) == 3);
|
|
||||||
store_project(source, temp.string(), strategy);
|
|
||||||
|
|
||||||
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
|
|
||||||
Model model;
|
|
||||||
REQUIRE(load_project(temp.string(), model, backup_dir));
|
|
||||||
REQUIRE(model.objects.size() == 1);
|
|
||||||
const ModelVolume& volume = *model.objects.front()->volumes.front();
|
|
||||||
|
|
||||||
REQUIRE(volume.texture_displacement_layers.size() == 1);
|
|
||||||
const TextureDisplacementLayer& layer = volume.texture_displacement_layers.front();
|
|
||||||
CHECK(layer.slot == 2);
|
|
||||||
CHECK(layer.name == "Bark");
|
|
||||||
CHECK_THAT(layer.depth_mm, Catch::Matchers::WithinAbs(1.25f, 1e-6f));
|
|
||||||
REQUIRE(layer.image_data);
|
|
||||||
CHECK(*layer.image_data == *painted.texture_displacement_layers.front().image_data);
|
|
||||||
CHECK(volume.texture_displacement_options.active_slot == 2);
|
|
||||||
CHECK(texture_paint(volume) == paint);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Volumes sharing a mesh keep their own texture displacement paint", "[3mf]")
|
|
||||||
{
|
|
||||||
Model source;
|
|
||||||
ModelObject* first_object = source.add_object();
|
|
||||||
ModelVolume* first = first_object->add_volume(make_cube(10., 10., 10.));
|
|
||||||
first_object->add_instance();
|
|
||||||
ModelVolume* second = source.add_object(*first_object)->volumes.front();
|
|
||||||
REQUIRE(second->mesh_ptr() == first->mesh_ptr());
|
|
||||||
paint_texture_slot(*first, 0, { 3 });
|
|
||||||
paint_texture_slot(*second, 0, { 7 });
|
|
||||||
|
|
||||||
// What a project save from the app uses: one file per object, an identical mesh written once.
|
|
||||||
ScopedTemporaryFile temp(".3mf");
|
|
||||||
store_project(source, temp.string(),
|
|
||||||
SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::ShareMesh);
|
|
||||||
|
|
||||||
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
|
|
||||||
Model model;
|
|
||||||
REQUIRE(load_project(temp.string(), model, backup_dir));
|
|
||||||
REQUIRE(model.objects.size() == 2);
|
|
||||||
CHECK(texture_paint(*model.objects[0]->volumes.front()) == texture_paint(*first));
|
|
||||||
CHECK(texture_paint(*model.objects[1]->volumes.front()) == texture_paint(*second));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("A texture displacement layer never loads its image from outside the project", "[3mf]")
|
|
||||||
{
|
|
||||||
// A file the layer names, on the machine opening the project, that the project did not carry.
|
|
||||||
ScopedTemporaryFile texture(".png");
|
|
||||||
{
|
|
||||||
boost::nowide::ofstream file(texture.string(), std::ios::binary);
|
|
||||||
file << "not part of the project";
|
|
||||||
}
|
|
||||||
REQUIRE(boost::filesystem::file_size(texture.path()) > 0);
|
|
||||||
|
|
||||||
Model source;
|
|
||||||
ModelObject* object = source.add_object();
|
|
||||||
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
|
|
||||||
object->add_instance();
|
|
||||||
TextureDisplacementLayer layer;
|
|
||||||
layer.slot = 0;
|
|
||||||
layer.name = "Elsewhere";
|
|
||||||
layer.path = texture.string();
|
|
||||||
layer.depth_mm = 0.75f;
|
|
||||||
volume->texture_displacement_layers.push_back(layer);
|
|
||||||
|
|
||||||
ScopedTemporaryFile temp(".3mf");
|
|
||||||
store_project(source, temp.string(), SaveStrategy::Zip64 | SaveStrategy::Silence);
|
|
||||||
|
|
||||||
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
|
|
||||||
Model model;
|
|
||||||
REQUIRE(load_project(temp.string(), model, backup_dir));
|
|
||||||
REQUIRE(model.objects.size() == 1);
|
|
||||||
const std::vector<TextureDisplacementLayer>& layers = model.objects.front()->volumes.front()->texture_displacement_layers;
|
|
||||||
REQUIRE(layers.size() == 1);
|
|
||||||
CHECK(layers.front().name == "Elsewhere");
|
|
||||||
CHECK_THAT(layers.front().depth_mm, Catch::Matchers::WithinAbs(0.75f, 1e-6f));
|
|
||||||
CHECK_FALSE(layers.front().image_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("A project with a malformed texture displacement value still loads", "[3mf][Regression]")
|
|
||||||
{
|
|
||||||
const auto [from, to] = GENERATE(table<std::string, std::string>({
|
|
||||||
{ "\"islands\":[]", "\"islands\":[[null,0,0,1]]" }, // a number that is null
|
|
||||||
{ "\"slot\":2", "\"slot\":8" }, // a slot past the last
|
|
||||||
}));
|
|
||||||
INFO(from << " -> " << to);
|
|
||||||
|
|
||||||
ScopedTemporaryFile temp(".3mf");
|
|
||||||
Model source;
|
|
||||||
const ModelVolume& painted = add_texture_displaced_cube(source);
|
|
||||||
store_project(source, temp.string(), SaveStrategy::Zip64 | SaveStrategy::Silence);
|
|
||||||
REQUIRE(replace_in_3mf_entry(temp.string(), ".json", from, to));
|
|
||||||
|
|
||||||
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
|
|
||||||
Model model;
|
|
||||||
bool loaded = false;
|
|
||||||
REQUIRE_NOTHROW(loaded = load_project(temp.string(), model, backup_dir));
|
|
||||||
REQUIRE(loaded);
|
|
||||||
REQUIRE(model.objects.size() == 1);
|
|
||||||
const ModelVolume& volume = *model.objects.front()->volumes.front();
|
|
||||||
// The layer stack is dropped as a whole; the paint, stored apart from it, still loads.
|
|
||||||
CHECK(volume.texture_displacement_layers.empty());
|
|
||||||
CHECK(texture_paint(volume) == texture_paint(painted));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The recipe lives only in the BBS-native backend, because that is the only one that runs:
|
// The recipe lives only in the BBS-native backend, because that is the only one that runs:
|
||||||
// store_bbs_3mf is the sole exporter the app calls, and 3mf.cpp's load_3mf is reached only for
|
// store_bbs_3mf is the sole exporter the app calls, and 3mf.cpp's load_3mf is reached only for
|
||||||
// files fingerprinted as PrusaSlicer's, which never carry a recipe. This locks in both halves:
|
// files fingerprinted as PrusaSlicer's, which never carry a recipe. This locks in both halves:
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
|
|
||||||
#include "libslic3r/AABBMesh.hpp"
|
#include "libslic3r/AABBMesh.hpp"
|
||||||
@@ -2158,177 +2157,6 @@ TEST_CASE("A second bake beside a first comes out as fine as a single bake", "[T
|
|||||||
CHECK(second <= single * 5 / 4);
|
CHECK(second <= single * 5 / 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The .3mf stores the layer stack as JSON (see texture_displacement_layers_to_json). Two properties
|
|
||||||
// matter: everything the UV editor and the panel can set has to survive a round trip, and a document
|
|
||||||
// written by a build that knew fewer fields has to keep loading, with the missing ones left at their
|
|
||||||
// defaults rather than zeroed.
|
|
||||||
TEST_CASE("Texture displacement layers survive a JSON round trip", "[TextureDisplacement]")
|
|
||||||
{
|
|
||||||
std::vector<TextureDisplacementLayer> layers(2);
|
|
||||||
TextureDisplacementLayer &a = layers[0];
|
|
||||||
a.slot = 0;
|
|
||||||
a.name = "Bark";
|
|
||||||
a.path = "/textures/bark.png";
|
|
||||||
a.depth_mm = 1.25f;
|
|
||||||
a.tiling_scale = 7.5f;
|
|
||||||
a.rotation_deg = 30.f;
|
|
||||||
a.offset = Vec2f(0.25f, -0.5f);
|
|
||||||
a.invert = true;
|
|
||||||
a.midlevel = 0.125f;
|
|
||||||
a.smoothing = 0.75f;
|
|
||||||
a.edge_smoothing = true;
|
|
||||||
a.edge_smoothing_amount = 0.25f;
|
|
||||||
a.auto_connect_islands = false;
|
|
||||||
a.tile_enabled = false;
|
|
||||||
a.tile_method = TextureTileMethod::MirroredRepeat;
|
|
||||||
a.projection_method = TextureProjectionMethod::LSCM;
|
|
||||||
a.blend_mode = TextureBlendMode::Subtract;
|
|
||||||
a.color_enabled = true;
|
|
||||||
a.lscm_seam_angle_deg = 45.f;
|
|
||||||
a.island_padding_mm = 0.5f;
|
|
||||||
a.lscm_seam_edges = { { 1, 2 }, { 3, 5 } };
|
|
||||||
a.islands = { TextureIsland{ Vec2f(1.f, 2.f), 15.f, 1.5f }, TextureIsland{} };
|
|
||||||
a.island_groups = { 0, 0 };
|
|
||||||
a.lscm_uv_overrides = { { -4, Vec2f(0.5f, 0.75f) } };
|
|
||||||
|
|
||||||
TextureDisplacementLayer &b = layers[1];
|
|
||||||
b.slot = 1;
|
|
||||||
b.projection_method = TextureProjectionMethod::ViewProjected;
|
|
||||||
b.view_project_right = Vec3f(0.f, 1.f, 0.f);
|
|
||||||
b.view_project_up = Vec3f(0.f, 0.f, 1.f);
|
|
||||||
b.view_project_projective = true;
|
|
||||||
for (size_t i = 0; i < b.view_project_matrix.size(); ++i)
|
|
||||||
b.view_project_matrix[i] = float(i) + 0.5f;
|
|
||||||
|
|
||||||
TextureDisplacementOptions options;
|
|
||||||
options.displace_border = false;
|
|
||||||
options.smooth_enabled = true;
|
|
||||||
options.smooth_strength = 0.6f;
|
|
||||||
options.smooth_iterations = 5;
|
|
||||||
options.pipeline_v2 = false;
|
|
||||||
options.v2_max_triangles_k = 250;
|
|
||||||
options.color_mix_count = 3;
|
|
||||||
options.color_despeckle = 4;
|
|
||||||
options.active_slot = 3;
|
|
||||||
options.panel_mode = 1;
|
|
||||||
|
|
||||||
const std::string json = texture_displacement_layers_to_json(layers, options);
|
|
||||||
|
|
||||||
std::vector<TextureDisplacementLayer> read_layers;
|
|
||||||
TextureDisplacementOptions read_options;
|
|
||||||
REQUIRE(texture_displacement_layers_from_json(json, read_layers, read_options));
|
|
||||||
REQUIRE(read_layers.size() == layers.size());
|
|
||||||
|
|
||||||
const TextureDisplacementLayer &ra = read_layers[0];
|
|
||||||
CHECK(ra.name == a.name);
|
|
||||||
CHECK(ra.path == a.path);
|
|
||||||
CHECK_THAT(ra.depth_mm, WithinAbs(a.depth_mm, 1e-6f));
|
|
||||||
CHECK_THAT(ra.tiling_scale, WithinAbs(a.tiling_scale, 1e-6f));
|
|
||||||
CHECK_THAT(ra.rotation_deg, WithinAbs(a.rotation_deg, 1e-6f));
|
|
||||||
CHECK(ra.offset.isApprox(a.offset));
|
|
||||||
CHECK(ra.invert == a.invert);
|
|
||||||
CHECK_THAT(ra.midlevel, WithinAbs(a.midlevel, 1e-6f));
|
|
||||||
CHECK_THAT(ra.smoothing, WithinAbs(a.smoothing, 1e-6f));
|
|
||||||
CHECK(ra.edge_smoothing == a.edge_smoothing);
|
|
||||||
CHECK_THAT(ra.edge_smoothing_amount, WithinAbs(a.edge_smoothing_amount, 1e-6f));
|
|
||||||
CHECK(ra.auto_connect_islands == a.auto_connect_islands);
|
|
||||||
CHECK(ra.tile_enabled == a.tile_enabled);
|
|
||||||
CHECK(ra.tile_method == a.tile_method);
|
|
||||||
CHECK(ra.projection_method == a.projection_method);
|
|
||||||
CHECK(ra.blend_mode == a.blend_mode);
|
|
||||||
CHECK(ra.color_enabled == a.color_enabled);
|
|
||||||
CHECK_THAT(ra.lscm_seam_angle_deg, WithinAbs(a.lscm_seam_angle_deg, 1e-6f));
|
|
||||||
CHECK_THAT(ra.island_padding_mm, WithinAbs(a.island_padding_mm, 1e-6f));
|
|
||||||
CHECK(ra.lscm_seam_edges == a.lscm_seam_edges);
|
|
||||||
REQUIRE(ra.islands.size() == a.islands.size());
|
|
||||||
CHECK(ra.islands[0].offset.isApprox(a.islands[0].offset));
|
|
||||||
CHECK_THAT(ra.islands[0].rotation_deg, WithinAbs(a.islands[0].rotation_deg, 1e-6f));
|
|
||||||
CHECK_THAT(ra.islands[0].scale, WithinAbs(a.islands[0].scale, 1e-6f));
|
|
||||||
CHECK(ra.island_groups == a.island_groups);
|
|
||||||
REQUIRE(ra.lscm_uv_overrides.size() == 1);
|
|
||||||
CHECK(ra.lscm_uv_overrides[0].first == -4);
|
|
||||||
CHECK(ra.lscm_uv_overrides[0].second.isApprox(Vec2f(0.5f, 0.75f)));
|
|
||||||
|
|
||||||
const TextureDisplacementLayer &rb = read_layers[1];
|
|
||||||
CHECK(rb.projection_method == TextureProjectionMethod::ViewProjected);
|
|
||||||
CHECK(rb.view_project_right.isApprox(b.view_project_right));
|
|
||||||
CHECK(rb.view_project_up.isApprox(b.view_project_up));
|
|
||||||
CHECK(rb.view_project_projective);
|
|
||||||
CHECK(rb.view_project_matrix == b.view_project_matrix);
|
|
||||||
|
|
||||||
CHECK(read_options.displace_border == options.displace_border);
|
|
||||||
CHECK(read_options.smooth_enabled == options.smooth_enabled);
|
|
||||||
CHECK_THAT(read_options.smooth_strength, WithinAbs(options.smooth_strength, 1e-6f));
|
|
||||||
CHECK(read_options.smooth_iterations == options.smooth_iterations);
|
|
||||||
CHECK(read_options.pipeline_v2 == options.pipeline_v2);
|
|
||||||
CHECK(read_options.v2_max_triangles_k == options.v2_max_triangles_k);
|
|
||||||
CHECK(read_options.color_mix_count == options.color_mix_count);
|
|
||||||
CHECK(read_options.color_despeckle == options.color_despeckle);
|
|
||||||
// Where the panel was left comes back with the rest, so a reopened project is on the same layer.
|
|
||||||
CHECK(read_options.active_slot == options.active_slot);
|
|
||||||
CHECK(read_options.panel_mode == options.panel_mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Texture displacement JSON keeps defaults for keys it does not carry", "[TextureDisplacement]")
|
|
||||||
{
|
|
||||||
// What a build that knew fewer fields would have written: one layer, almost nothing set.
|
|
||||||
const std::string sparse = R"({"version":1,"layers":[{"slot":2,"name":"Old","depth_mm":0.75}]})";
|
|
||||||
|
|
||||||
std::vector<TextureDisplacementLayer> layers;
|
|
||||||
TextureDisplacementOptions options;
|
|
||||||
REQUIRE(texture_displacement_layers_from_json(sparse, layers, options));
|
|
||||||
REQUIRE(layers.size() == 1);
|
|
||||||
|
|
||||||
const TextureDisplacementLayer &l = layers[0];
|
|
||||||
const TextureDisplacementLayer fresh;
|
|
||||||
const TextureDisplacementOptions defaults;
|
|
||||||
CHECK(l.slot == 2);
|
|
||||||
CHECK(l.name == "Old");
|
|
||||||
CHECK_THAT(l.depth_mm, WithinAbs(0.75f, 1e-6f));
|
|
||||||
// Everything absent keeps the struct's own default rather than becoming zero.
|
|
||||||
CHECK_THAT(l.tiling_scale, WithinAbs(fresh.tiling_scale, 1e-6f));
|
|
||||||
CHECK(l.auto_connect_islands == fresh.auto_connect_islands);
|
|
||||||
CHECK(l.tile_enabled == fresh.tile_enabled);
|
|
||||||
CHECK(l.projection_method == fresh.projection_method);
|
|
||||||
CHECK_THAT(l.lscm_seam_angle_deg, WithinAbs(fresh.lscm_seam_angle_deg, 1e-6f));
|
|
||||||
CHECK(options.color_mix_count == defaults.color_mix_count);
|
|
||||||
CHECK(options.pipeline_v2 == defaults.pipeline_v2);
|
|
||||||
CHECK(options.active_slot == defaults.active_slot);
|
|
||||||
CHECK(options.panel_mode == defaults.panel_mode);
|
|
||||||
|
|
||||||
std::vector<TextureDisplacementLayer> unused_layers;
|
|
||||||
TextureDisplacementOptions unused_options;
|
|
||||||
CHECK(!texture_displacement_layers_from_json("not json at all", unused_layers, unused_options));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Texture displacement JSON with a wrong-typed element or an out-of-range slot is rejected", "[TextureDisplacement]")
|
|
||||||
{
|
|
||||||
const std::string layer = GENERATE(as<std::string>{},
|
|
||||||
R"({"slot":0,"offset":["x",0]})",
|
|
||||||
R"({"slot":0,"view_project_right":[null,0,0]})",
|
|
||||||
R"({"slot":0,"view_project_matrix":[0,0,0,0,0,0,0,0,0,0,0,"x"]})",
|
|
||||||
R"({"slot":0,"lscm_seam_edges":[1,null]})",
|
|
||||||
R"({"slot":0,"islands":[[0,0,"x",1]]})",
|
|
||||||
R"({"slot":0,"lscm_uv_overrides":[[null,0,0]]})",
|
|
||||||
R"({"slot":8})",
|
|
||||||
R"({"slot":-1})",
|
|
||||||
R"({"slot":"x"})",
|
|
||||||
R"({"name":"No slot"})");
|
|
||||||
INFO(layer);
|
|
||||||
|
|
||||||
std::vector<TextureDisplacementLayer> layers(1);
|
|
||||||
layers.front().name = "Kept";
|
|
||||||
TextureDisplacementOptions options;
|
|
||||||
options.color_mix_count = 7;
|
|
||||||
|
|
||||||
bool parsed = true;
|
|
||||||
REQUIRE_NOTHROW(parsed = texture_displacement_layers_from_json(R"({"version":1,"layers":[)" + layer + "]}", layers, options));
|
|
||||||
CHECK_FALSE(parsed);
|
|
||||||
REQUIRE(layers.size() == 1);
|
|
||||||
CHECK(layers.front().name == "Kept");
|
|
||||||
CHECK(options.color_mix_count == 7);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("whether a layer's texture has colour agrees with its decode", "[TextureDisplacement]")
|
TEST_CASE("whether a layer's texture has colour agrees with its decode", "[TextureDisplacement]")
|
||||||
{
|
{
|
||||||
TextureDisplacementLayer gray;
|
TextureDisplacementLayer gray;
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_plugin_json_depth.cpp
|
test_plugin_json_depth.cpp
|
||||||
test_shortcuts.cpp
|
test_shortcuts.cpp
|
||||||
test_texture_color_palette.cpp
|
test_texture_color_palette.cpp
|
||||||
test_texture_island_view.cpp
|
|
||||||
test_file_url.cpp
|
test_file_url.cpp
|
||||||
test_user_manager.cpp
|
test_user_manager.cpp
|
||||||
../fff_print/test_helpers.cpp
|
../fff_print/test_helpers.cpp
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
// How the texture displacement gizmo's view follows the UV editor's island highlight. The rules live in
|
|
||||||
// libslic3r_gui; this is the suite that links it. Same Windows include prologue as
|
|
||||||
// test_texture_color_palette.cpp (wx pulls in <windows.h>; keep WIN32_LEAN_AND_MEAN / NOMINMAX ahead of it).
|
|
||||||
#ifdef WIN32
|
|
||||||
#ifndef WIN32_LEAN_AND_MEAN
|
|
||||||
#define WIN32_LEAN_AND_MEAN
|
|
||||||
#endif
|
|
||||||
#ifndef NOMINMAX
|
|
||||||
#define NOMINMAX
|
|
||||||
#endif
|
|
||||||
#include <Windows.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <catch2/catch_test_macros.hpp>
|
|
||||||
#include <catch2/generators/catch_generators.hpp>
|
|
||||||
|
|
||||||
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
|
|
||||||
|
|
||||||
using Follow = Slic3r::GUI::GLGizmoTextureDisplacement::IslandViewFollow;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// The View row's indices, and step()'s "apply nothing".
|
|
||||||
constexpr int NORMAL = 0, FAST = 1, CHECKER = 2, DISTORTION = 3, NONE = -1;
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_CASE("a Normal view gives way to Fast while islands are selected and comes back once none are", "[TextureIslandView][TextureDisplacement]")
|
|
||||||
{
|
|
||||||
Follow f;
|
|
||||||
CHECK(f.step(false, NORMAL, false) == NONE);
|
|
||||||
CHECK(f.step(true, NORMAL, false) == FAST);
|
|
||||||
CHECK(f.step(true, FAST, false) == NONE);
|
|
||||||
CHECK(f.step(false, FAST, false) == NORMAL);
|
|
||||||
CHECK(f.step(false, NORMAL, false) == NONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("a Fast, Checker or Distortion view is left alone by the island highlight", "[TextureIslandView][TextureDisplacement]")
|
|
||||||
{
|
|
||||||
const int view = GENERATE(FAST, CHECKER, DISTORTION);
|
|
||||||
Follow f;
|
|
||||||
CHECK(f.step(true, view, false) == NONE);
|
|
||||||
CHECK(f.step(false, view, false) == NONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("arriving in Normal with islands already selected switches to Fast", "[TextureIslandView][TextureDisplacement]")
|
|
||||||
{
|
|
||||||
// The pane's Height button over Checker lands in Normal with the islands still selected.
|
|
||||||
Follow f;
|
|
||||||
CHECK(f.step(true, CHECKER, false) == NONE);
|
|
||||||
CHECK(f.step(true, NORMAL, false) == FAST);
|
|
||||||
CHECK(f.step(false, FAST, false) == NORMAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Normal picked while islands are selected stays until they are deselected", "[TextureIslandView][TextureDisplacement]")
|
|
||||||
{
|
|
||||||
Follow f;
|
|
||||||
REQUIRE(f.step(true, NORMAL, false) == FAST);
|
|
||||||
f.picked(NORMAL);
|
|
||||||
CHECK(f.step(true, NORMAL, false) == NONE);
|
|
||||||
// A debug stage opening and closing over it changes nothing.
|
|
||||||
CHECK(f.step(true, NORMAL, true) == NONE);
|
|
||||||
CHECK(f.step(true, NORMAL, false) == NONE);
|
|
||||||
CHECK(f.step(false, NORMAL, false) == NONE);
|
|
||||||
CHECK(f.step(true, NORMAL, false) == FAST);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Fast picked while it was switched on stays after deselecting", "[TextureIslandView][TextureDisplacement]")
|
|
||||||
{
|
|
||||||
Follow f;
|
|
||||||
REQUIRE(f.step(true, NORMAL, false) == FAST);
|
|
||||||
f.picked(FAST);
|
|
||||||
CHECK(f.step(false, FAST, false) == NONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Normal picked with nothing selected does not hold off the next switch", "[TextureIslandView][TextureDisplacement]")
|
|
||||||
{
|
|
||||||
Follow f;
|
|
||||||
CHECK(f.step(false, NORMAL, false) == NONE);
|
|
||||||
f.picked(NORMAL);
|
|
||||||
CHECK(f.step(true, NORMAL, false) == FAST);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("a debug stage holds Normal while it is open, and the switch follows once it closes", "[TextureIslandView][TextureDisplacement]")
|
|
||||||
{
|
|
||||||
Follow f;
|
|
||||||
REQUIRE(f.step(true, NORMAL, false) == FAST);
|
|
||||||
// The stage takes Normal itself, with the islands still selected.
|
|
||||||
CHECK(f.step(true, NORMAL, true) == NONE);
|
|
||||||
CHECK(f.step(true, NORMAL, false) == FAST);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("islands deselected while a debug stage is open apply no view over it", "[TextureIslandView][TextureDisplacement]")
|
|
||||||
{
|
|
||||||
Follow f;
|
|
||||||
REQUIRE(f.step(true, NORMAL, false) == FAST);
|
|
||||||
CHECK(f.step(false, NORMAL, true) == NONE);
|
|
||||||
CHECK(f.step(false, NORMAL, false) == NONE);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user