Compare commits

..
Author SHA1 Message Date
Ian Chua b29f06a0ab fix: clang-tidy 2026-10-12 18:49:28 +08:00
Ian Chua 9ec0d39592 Merge branch 'main' into fix/isolate-plugin-browser-session 2026-10-12 18:35:44 +08:00
Ian Chua 0b9b05ed85 fix: Isolate plugin's webview browser sessions 2026-10-12 16:44:24 +08:00
54 changed files with 1625 additions and 2133 deletions
+65
View File
@@ -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
+476
View File
@@ -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 });
}
-8
View File
@@ -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 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M2.5,2.5h11v11h-11z" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><path d="M8,2.5v11M2.5,8h11" style="fill:none;stroke:#949494;stroke-dasharray:1.5,1.5"/><path d="M4.5,11.5l3.5-6l3.5,6z" style="fill:#009688"/></svg>

Before

Width:  |  Height:  |  Size: 340 B

+2 -166
View File
@@ -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
@@ -8373,8 +8211,6 @@ bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archiv
if (const std::optional<EmbossShape> &es = volume->emboss_shape; es.has_value()) { if (const std::optional<EmbossShape> &es = volume->emboss_shape; es.has_value()) {
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())
+349
View File
@@ -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
+39
View File
@@ -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_ */
-22
View File
@@ -353,24 +353,6 @@ Model Model::read_from_file(const std::string&
ObjInfo obj_info; ObjInfo obj_info;
ObjParser::MtlData mtl_data; ObjParser::MtlData mtl_data;
result = load_obj(input_file.c_str(), &model, obj_info, message, nullptr, &mtl_data); result = load_obj(input_file.c_str(), &model, obj_info, message, nullptr, &mtl_data);
// Keep the model's own UV channel on the volume, whatever the colour import then does with
// the mesh: it is what TextureProjectionMethod::ModelUV maps a texture-displacement layer
// through, so a model that arrives with a UV layout is textured on that layout instead of on
// a fresh unwrap. load_obj() fills obj_info.uvs per face of the mesh it built, in the same
// order (and only when every face named a known material, hence the size check). V is flipped
// on the way in, to the convention DecodedHeightTexture samples in - the same flip
// obj_to_textured_mesh() does for the colour importer.
if (result && !obj_info.uvs.empty() && !model.objects.empty() && !model.objects.back()->volumes.empty()) {
ModelVolume &volume = *model.objects.back()->volumes[0];
const indexed_triangle_set &its = volume.mesh().its;
if (obj_info.uvs.size() == its.indices.size()) {
volume.texture_uv.resize(its.indices.size() * 3);
for (size_t fi = 0; fi < obj_info.uvs.size(); ++fi)
for (int k = 0; k < 3; ++k)
volume.texture_uv[fi * 3 + size_t(k)] =
Vec2f(obj_info.uvs[fi][size_t(k)].x(), 1.f - obj_info.uvs[fi][size_t(k)].y());
}
}
if (result && obj_info.has_uv_png && !obj_info.uvs.empty() && !model.objects.empty()) { if (result && obj_info.has_uv_png && !obj_info.uvs.empty() && !model.objects.empty()) {
// Textured OBJ: hand the mesh + materials to the texture-to-color importer // Textured OBJ: hand the mesh + materials to the texture-to-color importer
// instead of the flat per-face colour dialog. // instead of the flat per-face colour dialog.
@@ -2151,10 +2133,6 @@ void ModelVolume::reset_extra_facets()
// than left referring to a mesh that no longer matches it. // than left referring to a mesh that no longer matches it.
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
this->texture_displacement_facet(i).reset(); this->texture_displacement_facet(i).reset();
// Same reasoning for the model's own UV channel: it is indexed by triangle, so against a mesh
// this no longer matches it would map the texture by another mesh's triangle numbering. A caller
// that can carry it across - see transfer_uv_channel() - puts it back after this.
this->reset_texture_uv();
} }
std::optional<TriangleSelector::SavedPainting> ModelVolume::save_painting() const std::optional<TriangleSelector::SavedPainting> ModelVolume::save_painting() const
+2 -23
View File
@@ -974,26 +974,6 @@ public:
// they are not a property of any single layer. // they are not a property of any single layer.
TextureDisplacementOptions texture_displacement_options; TextureDisplacementOptions texture_displacement_options;
// The model's own texture coordinates, as the file it was imported from carried them: three per
// triangle of mesh().its, in its triangle order (corner k of triangle i at 3i+k), V already
// flipped to the sampling convention (V = 0 is the image's top row). Empty when the source had
// none, which is every STL and every 3mf - only a textured OBJ brings one in today.
//
// Per corner rather than per vertex because that is what a UV layout is: the two sides of a seam
// are the same vertex with two different coordinates, and collapsing them would smear the texture
// across every island boundary. TextureProjectionMethod::ModelUV maps through it, which is what
// lets a model that already carries a UV channel be textured on the artist's own layout instead
// of on a fresh unwrap.
//
// Indexed by triangle, so anything that *replaces* the mesh has to bring it across (or drop it):
// see transfer_uv_channel(), and reset_texture_uv() for the cases that cannot.
std::vector<Vec2f> texture_uv;
bool has_texture_uv() const { return !texture_uv.empty() && texture_uv.size() == mesh().its.indices.size() * 3; }
// Forgets the channel. For a mesh change that nothing can carry it across - a split, a boolean -
// where leaving it would map the texture by another mesh's triangle numbering.
void reset_texture_uv() { texture_uv.clear(); }
// Save painting data before reset_extra_facets() discards it. // Save painting data before reset_extra_facets() discards it.
// Used for replacing mesh without losing painting data. // Used for replacing mesh without losing painting data.
// Only for model parts (not modifiers/connectors). // Only for model parts (not modifiers/connectors).
@@ -1256,7 +1236,6 @@ private:
texture_displacement_facets_6(other.texture_displacement_facets_6), texture_displacement_facets_7(other.texture_displacement_facets_7), texture_displacement_facets_6(other.texture_displacement_facets_6), texture_displacement_facets_7(other.texture_displacement_facets_7),
texture_displacement_layers(other.texture_displacement_layers), texture_displacement_layers(other.texture_displacement_layers),
texture_displacement_options(other.texture_displacement_options), texture_displacement_options(other.texture_displacement_options),
texture_uv(other.texture_uv),
cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape) cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape)
{ {
assert(this->id().valid()); assert(this->id().valid());
@@ -1355,7 +1334,7 @@ private:
cereal::load_by_value(ar, f); cereal::load_by_value(ar, f);
mesh_changed |= tf != f.timestamp(); mesh_changed |= tf != f.timestamp();
} }
ar(texture_displacement_layers, texture_displacement_options, texture_uv); ar(texture_displacement_layers, texture_displacement_options);
cereal::load_by_value(ar, config); cereal::load_by_value(ar, config);
cereal::load(ar, text_configuration); cereal::load(ar, text_configuration);
cereal::load(ar, emboss_shape); cereal::load(ar, emboss_shape);
@@ -1379,7 +1358,7 @@ private:
cereal::save_by_value(ar, fuzzy_skin_facets); cereal::save_by_value(ar, fuzzy_skin_facets);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
cereal::save_by_value(ar, texture_displacement_facet(i)); cereal::save_by_value(ar, texture_displacement_facet(i));
ar(texture_displacement_layers, texture_displacement_options, texture_uv); ar(texture_displacement_layers, texture_displacement_options);
cereal::save_by_value(ar, config); cereal::save_by_value(ar, config);
cereal::save(ar, text_configuration); cereal::save(ar, text_configuration);
cereal::save(ar, emboss_shape); cereal::save(ar, emboss_shape);
+59 -522
View File
@@ -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>
@@ -1571,61 +1570,6 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
return islands; return islands;
} }
std::vector<uint8_t> transfer_facet_paint(const indexed_triangle_set &src_mesh,
const TriangleSelector::TriangleSplittingData &src_paint,
const indexed_triangle_set &dst_mesh)
{
if (src_mesh.indices.empty() || dst_mesh.indices.empty() || src_paint.bitstream.empty())
return {};
const TriangleMesh src(src_mesh);
TriangleSelector selector(src);
selector.deserialize(src_paint, false);
// Every sub-triangle of the source, each carrying its own state - NONE included, so the soup
// covers the whole surface and a foot point always lands on something. One soup rather than one
// tree per state: a foot point has one nearest triangle, and that triangle's state is the answer.
std::vector<indexed_triangle_set> per_state;
selector.get_facets(per_state);
indexed_triangle_set soup;
std::vector<uint8_t> label;
for (size_t st = 0; st < per_state.size(); ++st) {
const indexed_triangle_set &part = per_state[st];
if (part.indices.empty())
continue;
const int base = int(soup.vertices.size());
soup.vertices.insert(soup.vertices.end(), part.vertices.begin(), part.vertices.end());
for (const stl_triangle_vertex_indices &t : part.indices) {
soup.indices.emplace_back(t[0] + base, t[1] + base, t[2] + base);
label.push_back(uint8_t(st));
}
}
if (soup.indices.empty())
return {};
const AABBTreeIndirect::Tree3f tree =
AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(soup.vertices, soup.indices);
std::vector<uint8_t> out(dst_mesh.indices.size(), 0);
tbb::parallel_for(tbb::blocked_range<size_t>(0, dst_mesh.indices.size()),
[&](const tbb::blocked_range<size_t> &range) {
for (size_t j = range.begin(); j < range.end(); ++j) {
const stl_triangle_vertex_indices &t = dst_mesh.indices[j];
// The centroid, not a corner: a facet takes one state, and its middle is the point that
// is inside it whatever the triangle looks like.
const Vec3f centroid = (dst_mesh.vertices[size_t(t[0])] + dst_mesh.vertices[size_t(t[1])] +
dst_mesh.vertices[size_t(t[2])]) / 3.f;
size_t hit = 0;
Vec3f foot;
const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(soup.vertices, soup.indices,
tree, centroid, hit, foot);
if (d2 >= 0.f && hit < label.size())
out[j] = label[hit];
}
});
return out;
}
void average_island_scales(std::vector<TextureIsland> &islands) void average_island_scales(std::vector<TextureIsland> &islands)
{ {
if (islands.empty()) if (islands.empty())
@@ -1728,110 +1672,6 @@ std::vector<Vec2f> compute_lscm_corner_uvs(const indexed_triangle_set &patch, co
return corner; return corner;
} }
// Barycentric weights of `p` inside triangle (a, b, c), as the pair that belongs with the edges
// (b - a) and (c - a): p ~= (1 - w1 - w2) * a + w1 * b + w2 * c. `p` is projected onto the triangle's
// plane, so a point a hair off the surface still reports the weights of its foot point. A degenerate
// triangle reports its centroid, the only coordinate that means anything there.
static void barycentric_weights(const Vec3f &a, const Vec3f &b, const Vec3f &c, const Vec3f &p, float &w1, float &w2)
{
const Vec3f e0 = b - a, e1 = c - a, ep = p - a;
const float d00 = e0.dot(e0), d01 = e0.dot(e1), d11 = e1.dot(e1), dp0 = ep.dot(e0), dp1 = ep.dot(e1);
const float den = d00 * d11 - d01 * d01;
if (std::abs(den) <= 1e-20f) {
w1 = w2 = 1.f / 3.f;
return;
}
w1 = (d11 * dp0 - d01 * dp1) / den;
w2 = (d00 * dp1 - d01 * dp0) / den;
}
std::vector<Vec2f> compute_model_corner_uvs(const indexed_triangle_set &base_mesh,
const std::vector<Vec2f> &model_uv,
const indexed_triangle_set &patch,
const std::vector<int> &patch_source)
{
if (patch.indices.empty() || model_uv.size() != base_mesh.indices.size() * 3 ||
patch_source.size() != patch.indices.size())
return {};
std::vector<Vec2f> corner(patch.indices.size() * 3, Vec2f::Zero());
for (size_t j = 0; j < patch.indices.size(); ++j) {
const int S = patch_source[j];
if (S < 0 || size_t(S) >= base_mesh.indices.size())
continue; // no source triangle: leave (0, 0), which the callers read as "no placement"
const stl_triangle_vertex_indices &src = base_mesh.indices[size_t(S)];
const Vec3f &a = base_mesh.vertices[size_t(src[0])];
const Vec3f &b = base_mesh.vertices[size_t(src[1])];
const Vec3f &c = base_mesh.vertices[size_t(src[2])];
const Vec2f *uv = &model_uv[size_t(S) * 3];
const stl_triangle_vertex_indices &tri = patch.indices[j];
for (int k = 0; k < 3; ++k) {
// An unsplit patch triangle is its source triangle, so its corners land exactly on a, b
// and c and this reproduces their uvs unchanged; a sub-triangle the brush cut gets the
// interpolated coordinate, which is the whole point of going through the barycentric.
float w1 = 0.f, w2 = 0.f;
barycentric_weights(a, b, c, patch.vertices[size_t(tri[k])], w1, w2);
corner[j * 3 + size_t(k)] = (1.f - w1 - w2) * uv[0] + w1 * uv[1] + w2 * uv[2];
}
}
return corner;
}
std::vector<Vec2f> collapse_corner_uvs(const indexed_triangle_set &patch, const std::vector<Vec2f> &corner)
{
if (patch.indices.empty() || corner.size() != patch.indices.size() * 3)
return {};
std::vector<Vec2f> per_vertex(patch.vertices.size(), Vec2f::Zero());
std::vector<char> seen(patch.vertices.size(), 0);
for (size_t j = 0; j < patch.indices.size(); ++j)
for (int k = 0; k < 3; ++k) {
const int vi = patch.indices[j][k];
if (vi < 0 || size_t(vi) >= per_vertex.size() || seen[size_t(vi)])
continue;
seen[size_t(vi)] = 1;
per_vertex[size_t(vi)] = corner[j * 3 + size_t(k)];
}
return per_vertex;
}
std::vector<Vec2f> transfer_uv_channel(const indexed_triangle_set &src_mesh,
const std::vector<Vec2f> &src_uv,
const indexed_triangle_set &dst_mesh,
float max_distance)
{
if (src_mesh.indices.empty() || dst_mesh.indices.empty() || src_uv.size() != src_mesh.indices.size() * 3)
return {};
const AABBTreeIndirect::Tree3f tree =
AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(src_mesh.vertices, src_mesh.indices);
const float limit2 = max_distance >= 0.f ? max_distance * max_distance : std::numeric_limits<float>::max();
std::vector<Vec2f> out(dst_mesh.indices.size() * 3, Vec2f::Zero());
tbb::parallel_for(tbb::blocked_range<size_t>(0, dst_mesh.indices.size()),
[&](const tbb::blocked_range<size_t> &range) {
for (size_t j = range.begin(); j < range.end(); ++j) {
const stl_triangle_vertex_indices &tri = dst_mesh.indices[j];
for (int k = 0; k < 3; ++k) {
const Vec3f &p = dst_mesh.vertices[size_t(tri[k])];
size_t hit = 0;
Vec3f foot;
const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
src_mesh.vertices, src_mesh.indices, tree, p, hit, foot);
if (d2 < 0.f || d2 > limit2 || hit >= src_mesh.indices.size())
continue; // nowhere near the source surface: (0, 0), as documented
const stl_triangle_vertex_indices &st = src_mesh.indices[hit];
float w1 = 0.f, w2 = 0.f;
barycentric_weights(src_mesh.vertices[size_t(st[0])], src_mesh.vertices[size_t(st[1])],
src_mesh.vertices[size_t(st[2])], foot, w1, w2);
const Vec2f *uv = &src_uv[hit * 3];
out[j * 3 + size_t(k)] = (1.f - w1 - w2) * uv[0] + w1 * uv[1] + w2 * uv[2];
}
}
});
return out;
}
namespace { namespace {
// apply_uv_transform()'s per-layer constants, worked out once. Triplanar sampling runs the transform // apply_uv_transform()'s per-layer constants, worked out once. Triplanar sampling runs the transform
// three times per point, and recomputing the rotation's cos/sin and the tiling reciprocal on every one // three times per point, and recomputing the rotation's cos/sin and the tiling reciprocal on every one
@@ -1841,16 +1681,13 @@ struct UVTransform
float scale, cs, sn, aspect; float scale, cs, sn, aspect;
Vec2f offset; Vec2f offset;
// `normalized_uv` drops the two steps that only mean something for a coordinate measured on the UVTransform(const TextureDisplacementLayer &layer, float aspect_)
// model - the millimetre tiling scale and the non-square aspect correction - and keeps the
// rotation and offset. See apply_uv_transform(); TextureProjectionMethod::ModelUV is what needs it.
UVTransform(const TextureDisplacementLayer &layer, float aspect_, bool normalized_uv = false)
{ {
scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale); scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
const float rad = layer.rotation_deg * float(M_PI) / 180.f; const float rad = layer.rotation_deg * float(M_PI) / 180.f;
cs = std::cos(rad); cs = std::cos(rad);
sn = std::sin(rad); sn = std::sin(rad);
aspect = normalized_uv ? 1.f : aspect_; aspect = aspect_;
offset = layer.offset; offset = layer.offset;
} }
Vec2f operator()(const Vec2f &planar) const Vec2f operator()(const Vec2f &planar) const
@@ -1875,11 +1712,9 @@ inline Vec3f triplanar_weights(const Vec3f &normal)
} }
} // namespace } // namespace
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect, bool normalized_uv) Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect)
{ {
// A uv that is already normalized has no millimetre scale to divide by and no aspect to correct - const float scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
// see the header. UVTransform does the same, and the two must stay in step.
const float scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
const Vec2f scaled = planar * scale; const Vec2f scaled = planar * scale;
const float rad = layer.rotation_deg * float(M_PI) / 180.f; const float rad = layer.rotation_deg * float(M_PI) / 180.f;
@@ -1897,7 +1732,7 @@ Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &la
// Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a // Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a
// shear, and doing it the other way round would make "Rotation" skew the pattern instead of // shear, and doing it the other way round would make "Rotation" skew the pattern instead of
// turning it. // turning it.
if (!normalized_uv && aspect > 0.f && aspect != 1.f) if (aspect > 0.f && aspect != 1.f)
rotated.y() *= aspect; rotated.y() *= aspect;
return rotated + layer.offset; return rotated + layer.offset;
@@ -1922,9 +1757,6 @@ float blend_displacement(float accumulated, float value, TextureBlendMode mode)
value; value;
return accumulated / divisor; return accumulated / divisor;
} }
// Replaces instead of combining, so `accumulated` is deliberately unused: whatever the layers
// below put here is gone where this one is painted.
case TextureBlendMode::Override: return value;
case TextureBlendMode::Add: case TextureBlendMode::Add:
default: return accumulated + value; default: return accumulated + value;
} }
@@ -1954,21 +1786,14 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
// apply_uv_transform()). Every projection except the projective "from view" one funnels through // apply_uv_transform()). Every projection except the projective "from view" one funnels through
// here, so this one line is what makes them all aspect-correct. // here, so this one line is what makes them all aspect-correct.
const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f; const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f;
// A ModelUV layer's precomputed uv is the model's own, already normalized; every other coordinate const UVTransform xf(layer, aspect);
// reaching the transform - the unwrap's, and every analytic projection's - is in millimetres. The
// `lscm_uv` test is what keeps the two apart: a ModelUV layer with no channel gets none, falls
// through to the millimetre methods below, and must be scaled like them.
const bool normalized_uv = layer.projection_method == TextureProjectionMethod::ModelUV &&
lscm_uv != nullptr;
const UVTransform xf(layer, aspect, normalized_uv);
auto sample_at = [&](const Vec2f &planar) { auto sample_at = [&](const Vec2f &planar) {
return texture.sample(xf(planar), layer.tile_enabled, layer.tile_method); return texture.sample(xf(planar), layer.tile_enabled, layer.tile_method);
}; };
// A precomputed per-patch uv - the LSCM solve, or the model's own channel - wins over the layer's // Precomputed per-patch LSCM solve wins over the layer's own method (see the header): the
// own method (see the header): the caller only passes it when the patch actually has one, so a // caller only passes it when the patch actually parameterized successfully, so a patch that
// patch that failed to unwrap, or a volume with no UV channel, falls through to the analytic // failed to unwrap falls through to the analytic methods below as its documented fallback.
// methods below as its documented fallback.
if (lscm_uv != nullptr) if (lscm_uv != nullptr)
return sample_at(*lscm_uv); return sample_at(*lscm_uv);
@@ -2026,12 +1851,8 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
// the two differ in what "nothing here" means. Height returns 0, which is a perfectly good height // the two differ in what "nothing here" means. Height returns 0, which is a perfectly good height
// (no displacement); colour has no such neutral value - black is a colour - so every path that // (no displacement); colour has no such neutral value - black is a colour - so every path that
// returns 0 there has to report false here instead, and the caller leaves the triangle uncoloured. // returns 0 there has to report false here instead, and the caller leaves the triangle uncoloured.
const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f; const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f;
// Same reasoning as sample_layer_height(), and it has to match it exactly or a layer's colour and const UVTransform xf(layer, aspect);
// its relief would be read from different places in the image.
const bool normalized_uv = layer.projection_method == TextureProjectionMethod::ModelUV &&
lscm_uv != nullptr;
const UVTransform xf(layer, aspect, normalized_uv);
auto sample_at = [&](const Vec2f &planar) { auto sample_at = [&](const Vec2f &planar) {
return texture.sample_color(xf(planar), layer.tile_enabled, layer.tile_method); return texture.sample_color(xf(planar), layer.tile_enabled, layer.tile_method);
}; };
@@ -2045,8 +1866,6 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
}; };
if (lscm_uv != nullptr) { if (lscm_uv != nullptr) {
// The coverage test goes through the same transform, so an untiled ModelUV layer is "there"
// exactly where the model's own uv lands inside the image.
if (!covered(*lscm_uv)) if (!covered(*lscm_uv))
return false; return false;
out = sample_at(*lscm_uv); out = sample_at(*lscm_uv);
@@ -2391,10 +2210,9 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
const TextureColorRequest *color, const TextureColorRequest *color,
bool flip_normals, bool flip_normals,
BakeStageRecorder *debug, BakeStageRecorder *debug,
TextureBakeStats *stats, TextureBakeStats *stats)
const std::vector<Vec2f> *model_uv)
{ {
HeightFieldSampler combined = make_combined_displacement_sampler(mesh, layers, facets_data, model_uv); HeightFieldSampler combined = make_combined_displacement_sampler(mesh, layers, facets_data);
if (!combined) if (!combined)
return mesh; // nothing decodable to displace with return mesh; // nothing decodable to displace with
@@ -2564,8 +2382,7 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
// part of it (half a tile in the neighbour's colour, a tile edge that wanders). // part of it (half a tile in the neighbour's colour, a tile edge that wanders).
const bool want_color = color != nullptr && color->out_triangle != nullptr && bool(color->quantize); const bool want_color = color != nullptr && color->out_triangle != nullptr && bool(color->quantize);
const ColorFieldSampler color_sampler = const ColorFieldSampler color_sampler =
want_color ? make_combined_color_sampler(mesh, layers, facets_data, color->quantize, color->quantize_pure, model_uv) : want_color ? make_combined_color_sampler(mesh, layers, facets_data, color->quantize, color->quantize_pure) : ColorFieldSampler{};
ColorFieldSampler{};
// //
// The *palette* index, not the printed filament. The decimation treats any edge whose two faces // The *palette* index, not the printed filament. The decimation treats any edge whose two faces
// differ as a crease (TextureBakeDecimate.cpp), so it must only ever see where the **perceived** // differ as a crease (TextureBakeDecimate.cpp), so it must only ever see where the **perceived**
@@ -2697,8 +2514,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
const TextureColorRequest *color, const TextureColorRequest *color,
bool flip_normals, bool flip_normals,
BakeStageRecorder *debug, BakeStageRecorder *debug,
TextureBakeStats *stats, TextureBakeStats *stats)
const std::vector<Vec2f> *model_uv)
{ {
// The classic path moves the vertices the mesh already has, so there is no budget to report on. // The classic path moves the vertices the mesh already has, so there is no budget to report on.
(void) stats; (void) stats;
@@ -2735,7 +2551,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
if (options.pipeline_v2) if (options.pipeline_v2)
return build_texture_displacement_v2(mesh, layers, facets_data, options, progress, color, flip_normals, return build_texture_displacement_v2(mesh, layers, facets_data, options, progress, color, flip_normals,
debug, stats, model_uv); debug, stats);
// Layers are combined in slot order, like stacked layers in an image editor: each one folds its // Layers are combined in slot order, like stacked layers in an image editor: each one folds its
// own displacement into the running total via its blend mode (see TextureBlendMode). // own displacement into the running total via its blend mode (see TextureBlendMode).
@@ -2853,19 +2669,13 @@ static indexed_triangle_set build_texture_displacement_in_place(
selector_dirty = true; selector_dirty = true;
const bool color_this_layer = want_color && layer->color_enabled; const bool color_this_layer = want_color && layer->color_enabled;
// The model's own channel is per base-mesh *triangle*, so reading it needs to know which base
// triangle each (possibly brush-split) patch triangle came from - the colour pass's map, asked
// for here as well.
const bool model_uv_layer = layer->projection_method == TextureProjectionMethod::ModelUV &&
model_uv != nullptr && model_uv->size() == mesh.indices.size() * 3;
// A flat-colour image is matched against the filaments alone (see TextureColorRequest). // A flat-colour image is matched against the filaments alone (see TextureColorRequest).
const ColorQuantizeFn &layer_quantize = const ColorQuantizeFn &layer_quantize =
(color_this_layer && color->quantize_pure && analyze_texture_detail(*layer).flat_colors) ? color->quantize_pure (color_this_layer && color->quantize_pure && analyze_texture_detail(*layer).flat_colors) ? color->quantize_pure
: color->quantize; : color->quantize;
std::vector<int> patch_source; // sub-triangle -> base mesh triangle; built when colouring or mapping by ModelUV std::vector<int> patch_source; // sub-triangle -> base mesh triangle, only built when colouring
const indexed_triangle_set patch = const indexed_triangle_set patch =
selector.get_facets_strict(EnforcerBlockerType::ENFORCER, selector.get_facets_strict(EnforcerBlockerType::ENFORCER, color_this_layer ? &patch_source : nullptr);
(color_this_layer || model_uv_layer) ? &patch_source : nullptr);
if (patch.indices.empty()) if (patch.indices.empty())
continue; continue;
// get_facets_strict() returns the same vertex array whichever state is asked for (only the // get_facets_strict() returns the same vertex array whichever state is asked for (only the
@@ -2896,25 +2706,18 @@ static indexed_triangle_set build_texture_displacement_in_place(
// A real unwrap of the whole patch, computed once here rather than per vertex - it is a // A real unwrap of the whole patch, computed once here rather than per vertex - it is a
// per-chart solve over the whole patch, not a per-point formula. Cached, so repeating this // per-chart solve over the whole patch, not a per-point formula. Cached, so repeating this
// for every slider tweak costs a hash rather than a re-solve (see compute_patch_unwrap()). // for every slider tweak costs a hash rather than a re-solve (see compute_patch_unwrap()).
// ModelUV takes its corners from the volume's own channel instead of solving anything, and const std::vector<Vec2f> lscm_uvs = (layer->projection_method == TextureProjectionMethod::LSCM) ?
// collapses them to one per vertex the same way the unwrap does.
const std::vector<Vec2f> model_corner_uvs = model_uv_layer ?
compute_model_corner_uvs(mesh, *model_uv, patch, patch_source) :
std::vector<Vec2f>{};
const std::vector<Vec2f> patch_uvs = model_uv_layer ? collapse_corner_uvs(patch, model_corner_uvs) :
(layer->projection_method == TextureProjectionMethod::LSCM) ?
compute_lscm_uvs(patch, *layer) : compute_lscm_uvs(patch, *layer) :
std::vector<Vec2f>{}; std::vector<Vec2f>{};
// The colour pass below samples per *triangle*, so it takes the per-corner unwrap instead: the // The colour pass below samples per *triangle*, so it takes the per-corner unwrap instead: the
// per-vertex collapse above would hand a triangle at a seam the island layout did not join its // per-vertex collapse above would hand a triangle at a seam the island layout did not join its
// neighbour's placement, painting one triangle per face from the wrong part of the texture. // neighbour's placement, painting one triangle per face from the wrong part of the texture.
// (The displacement itself stays on patch_uvs - a vertex has one position, so one height.) // (The displacement itself stays on lscm_uvs - a vertex has one position, so one height.)
const std::vector<Vec2f> patch_corner_uvs = model_uv_layer ? model_corner_uvs : const std::vector<Vec2f> lscm_corner_uvs = (layer->projection_method == TextureProjectionMethod::LSCM) ?
(layer->projection_method == TextureProjectionMethod::LSCM) ?
compute_lscm_corner_uvs(patch, *layer) : compute_lscm_corner_uvs(patch, *layer) :
std::vector<Vec2f>{}; std::vector<Vec2f>{};
const bool corner_uv_ok = !patch_corner_uvs.empty() && const bool corner_uv_ok = !lscm_corner_uvs.empty() &&
patch_corner_uvs.size() == patch.indices.size() * 3; lscm_corner_uvs.size() == patch.indices.size() * 3;
// Colour, if this layer carries any. Area-weighted over each base triangle's *painted* part, // Colour, if this layer carries any. Area-weighted over each base triangle's *painted* part,
// so a triangle the brush only clipped a corner off takes the colour of that corner rather // so a triangle the brush only clipped a corner off takes the colour of that corner rather
@@ -2944,7 +2747,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
// the per-vertex height sampling uses, evaluated at the centroid instead. // the per-vertex height sampling uses, evaluated at the centroid instead.
Vec3f n = Vec3f::Zero(); Vec3f n = Vec3f::Zero();
Vec2f uv = Vec2f::Zero(); Vec2f uv = Vec2f::Zero();
bool have_uv = !patch_uvs.empty(); bool have_uv = !lscm_uvs.empty();
for (int k = 0; k < 3; ++k) { for (int k = 0; k < 3; ++k) {
const int vi = t[k]; const int vi = t[k];
if (vi < int(vertex_normals.size())) if (vi < int(vertex_normals.size()))
@@ -2952,9 +2755,9 @@ static indexed_triangle_set build_texture_displacement_in_place(
if (!have_uv) if (!have_uv)
continue; continue;
if (corner_uv_ok) if (corner_uv_ok)
uv += patch_corner_uvs[j * 3 + size_t(k)]; uv += lscm_corner_uvs[j * 3 + size_t(k)];
else if (size_t(vi) < patch_uvs.size()) else if (size_t(vi) < lscm_uvs.size())
uv += patch_uvs[size_t(vi)]; uv += lscm_uvs[size_t(vi)];
else else
have_uv = false; have_uv = false;
} }
@@ -3028,7 +2831,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
[&](const tbb::blocked_range<size_t> &range) { [&](const tbb::blocked_range<size_t> &range) {
for (size_t k = range.begin(); k < range.end(); ++k) { for (size_t k = range.begin(); k < range.end(); ++k) {
const size_t vi = size_t(layer_vertices[k]); const size_t vi = size_t(layer_vertices[k]);
const Vec2f *lscm_uv = patch_uvs.empty() ? nullptr : &patch_uvs[vi]; const Vec2f *lscm_uv = lscm_uvs.empty() ? nullptr : &lscm_uvs[vi];
const float h = sample_layer_height(height, *layer, mesh.vertices[vi], vertex_normals[vi], const float h = sample_layer_height(height, *layer, mesh.vertices[vi], vertex_normals[vi],
patch_centroid, patch_axis, lscm_uv); patch_centroid, patch_axis, lscm_uv);
// midlevel is the height that means "stay put", so anything below it displaces // midlevel is the height that means "stay put", so anything below it displaces
@@ -3161,8 +2964,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
const TextureColorRequest *color, const TextureColorRequest *color,
const Transform3d &volume_to_world, const Transform3d &volume_to_world,
BakeStageRecorder *debug, BakeStageRecorder *debug,
TextureBakeStats *stats, TextureBakeStats *stats)
const std::vector<Vec2f> *model_uv)
{ {
// An untransformed volume on an untransformed instance is by far the common case, and the round // An untransformed volume on an untransformed instance is by far the common case, and the round
// trip costs two matrix multiplies per vertex on a mesh that can carry millions of them - so take // trip costs two matrix multiplies per vertex on a mesh that can carry millions of them - so take
@@ -3172,7 +2974,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
const Transform3d frame = texture_displacement_bake_frame(volume_to_world); const Transform3d frame = texture_displacement_bake_frame(volume_to_world);
if (frame.matrix().isApprox(Transform3d::Identity().matrix())) if (frame.matrix().isApprox(Transform3d::Identity().matrix()))
return build_texture_displacement_in_place(base_mesh, layers, facets_data, options, progress, color, return build_texture_displacement_in_place(base_mesh, layers, facets_data, options, progress, color,
false, debug, stats, model_uv); false, debug, stats);
const Transform3d to_local = frame.inverse(); const Transform3d to_local = frame.inverse();
// A mirroring placement leaves the positions correct but every winding-derived normal pointing // A mirroring placement leaves the positions correct but every winding-derived normal pointing
@@ -3186,10 +2988,8 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
v = (frame * v.cast<double>()).cast<float>(); v = (frame * v.cast<double>()).cast<float>();
const size_t debug_mark = (debug != nullptr) ? debug->mark() : 0; const size_t debug_mark = (debug != nullptr) ? debug->mark() : 0;
// The UV channel rides along untouched: a uv is a coordinate in the texture, not on the model, so
// the frame the mesh is measured in does not reach it.
indexed_triangle_set out = build_texture_displacement_in_place(world, layers, facets_data, options, indexed_triangle_set out = build_texture_displacement_in_place(world, layers, facets_data, options,
progress, color, mirrored, debug, stats, model_uv); progress, color, mirrored, debug, stats);
// Everything the bake recorded is in world millimetres, like `out` itself. The debug view draws in // Everything the bake recorded is in world millimetres, like `out` itself. The debug view draws in
// the volume's local frame, so the stages are brought back the same way the result is. // the volume's local frame, so the stages are brought back the same way the result is.
if (debug != nullptr) if (debug != nullptr)
@@ -3261,8 +3061,7 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume)
return build_texture_displacement(volume.mesh().its, volume.texture_displacement_layers, facets_data, return build_texture_displacement(volume.mesh().its, volume.texture_displacement_layers, facets_data,
volume.texture_displacement_options, {}, nullptr, volume.texture_displacement_options, {}, nullptr,
texture_displacement_volume_to_world(volume), nullptr, nullptr, texture_displacement_volume_to_world(volume));
volume.has_texture_uv() ? &volume.texture_uv : nullptr);
} }
void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength, void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength,
@@ -3326,18 +3125,16 @@ void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t>
namespace { namespace {
// One decoded texture + placement per sampleable layer, in blend (slot) order. Held by shared_ptr so // One decoded texture + placement per sampleable layer, in blend (slot) order. Held by shared_ptr so
// the returned closure owns it for as long as the subdivider keeps calling back. // the returned closure owns it for as long as the subdivider keeps calling back.
// A per-triangle uv map turned into something a *point* sampler can use. Neither of the two maps that // An unwrap turned into something a *point* sampler can use. LSCM has no formula from position to
// are not a formula from position to uv - the LSCM unwrap and the model's own UV channel - can be // uv - it is a per-triangle map - so a point is placed on the painted patch (the nearest patch
// evaluated at an arbitrary point directly, so a point is placed on the painted patch (the nearest // triangle, and its barycentric coordinates there) and the uv is interpolated from that triangle's own
// patch triangle, and its barycentric coordinates there) and the uv is interpolated from that // per-corner uvs. Exact for a point on the base surface, which is where both samplers are queried: the
// triangle's own per-corner uvs. Exact for a point on the base surface, which is where both samplers // displacement samples refined positions before moving them, and the colour pass samples the foot
// are queried: the displacement samples refined positions before moving them, and the colour pass // point on the painted pieces.
// samples the foot point on the painted pieces.
struct LscmLookup { struct LscmLookup {
indexed_triangle_set patch; // the layer's painted patch, as the uvs were computed on indexed_triangle_set patch; // the layer's painted patch, as the unwrap was solved on
AABBTreeIndirect::Tree3f tree; AABBTreeIndirect::Tree3f tree;
// compute_lscm_corner_uvs(patch, layer), or compute_model_corner_uvs() for a ModelUV layer std::vector<Vec2f> corner; // compute_lscm_corner_uvs(patch, layer)
std::vector<Vec2f> corner;
// False when `pos` is not on this layer's patch (farther than `tol`): there is no uv there, so the // False when `pos` is not on this layer's patch (farther than `tol`): there is no uv there, so the
// layer contributes nothing - the same as a non-tiled texture outside its placement. // layer contributes nothing - the same as a non-tiled texture outside its placement.
@@ -3350,9 +3147,15 @@ struct LscmLookup {
if (d2 < 0.f || d2 > tol * tol || hit >= patch.indices.size()) if (d2 < 0.f || d2 > tol * tol || hit >= patch.indices.size())
return false; return false;
const stl_triangle_vertex_indices &t = patch.indices[hit]; const stl_triangle_vertex_indices &t = patch.indices[hit];
float w1 = 0.f, w2 = 0.f; // a degenerate triangle takes its centroid's uv const Vec3f &a = patch.vertices[size_t(t[0])], &b = patch.vertices[size_t(t[1])], &c = patch.vertices[size_t(t[2])];
barycentric_weights(patch.vertices[size_t(t[0])], patch.vertices[size_t(t[1])], patch.vertices[size_t(t[2])], const Vec3f e0 = b - a, e1 = c - a, ep = foot - a;
foot, w1, w2); const float d00 = e0.dot(e0), d01 = e0.dot(e1), d11 = e1.dot(e1), dp0 = ep.dot(e0), dp1 = ep.dot(e1);
const float den = d00 * d11 - d01 * d01;
float w1 = 1.f / 3.f, w2 = 1.f / 3.f; // a degenerate triangle takes its centroid's uv
if (std::abs(den) > 1e-20f) {
w1 = (d11 * dp0 - d01 * dp1) / den;
w2 = (d00 * dp1 - d01 * dp0) / den;
}
const Vec2f *c3 = &corner[hit * 3]; const Vec2f *c3 = &corner[hit * 3];
uv = (1.f - w1 - w2) * c3[0] + w1 * c3[1] + w2 * c3[2]; uv = (1.f - w1 - w2) * c3[0] + w1 * c3[1] + w2 * c3[2];
return true; return true;
@@ -3411,7 +3214,7 @@ struct PreparedLayer {
// where a layer is placed. `need_color` additionally drops layers that cannot contribute colour. // where a layer is placed. `need_color` additionally drops layers that cannot contribute colour.
std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers( std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
const indexed_triangle_set &base_mesh, const std::vector<TextureDisplacementLayer> &layers, const indexed_triangle_set &base_mesh, const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data, bool need_color, const std::vector<Vec2f> *model_uv) const TextureDisplacementFacetsData &facets_data, bool need_color)
{ {
auto prepared = std::make_shared<std::vector<PreparedLayer>>(); auto prepared = std::make_shared<std::vector<PreparedLayer>>();
@@ -3442,14 +3245,9 @@ std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
if (tex.empty() || (need_color && !tex.has_color())) if (tex.empty() || (need_color && !tex.has_color()))
continue; continue;
const bool model_uv_layer = layer->projection_method == TextureProjectionMethod::ModelUV &&
model_uv != nullptr && model_uv->size() == base_mesh.indices.size() * 3;
TriangleSelector selector(selector_mesh); TriangleSelector selector(selector_mesh);
selector.deserialize(data, false); selector.deserialize(data, false);
std::vector<int> patch_source; // only the model's own channel needs it; see the classic path const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER);
const indexed_triangle_set patch =
selector.get_facets_strict(EnforcerBlockerType::ENFORCER, model_uv_layer ? &patch_source : nullptr);
if (patch.indices.empty()) if (patch.indices.empty())
continue; continue;
@@ -3459,16 +3257,12 @@ std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
texture_displacement_patch_frame(patch, vertex_normals, centroid, axis, average_normal); texture_displacement_patch_frame(patch, vertex_normals, centroid, axis, average_normal);
std::shared_ptr<const LscmLookup> lscm; std::shared_ptr<const LscmLookup> lscm;
if (model_uv_layer || layer->projection_method == TextureProjectionMethod::LSCM) { if (layer->projection_method == TextureProjectionMethod::LSCM) {
// Solved on the very patch the classic path and the GUI solve it on (same geometry, seam // Solved on the very patch the classic path and the GUI solve it on (same geometry, seam
// angle and edges), so it hits the unwrap cache and lands exactly where the UV editor // angle and edges), so it hits the unwrap cache and lands exactly where the UV editor
// shows it, hand-placed islands and UV edits included. A ModelUV layer needs no solve at // shows it, hand-placed islands and UV edits included.
// all - its corners are read straight out of the volume's channel - but it is looked up
// through the same per-triangle machinery, for the same reason: there is no formula from a
// point to its uv.
auto l = std::make_shared<LscmLookup>(); auto l = std::make_shared<LscmLookup>();
l->corner = model_uv_layer ? compute_model_corner_uvs(base_mesh, *model_uv, patch, patch_source) : l->corner = compute_lscm_corner_uvs(patch, *layer);
compute_lscm_corner_uvs(patch, *layer);
if (l->corner.size() == patch.indices.size() * 3) { if (l->corner.size() == patch.indices.size() * 3) {
l->patch = patch; l->patch = patch;
l->tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(l->patch.vertices, l->patch.indices); l->tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(l->patch.vertices, l->patch.indices);
@@ -3494,12 +3288,11 @@ ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set
const std::vector<TextureDisplacementLayer> &layers, const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data, const TextureDisplacementFacetsData &facets_data,
ColorQuantizeFn quantize, ColorQuantizeFn quantize,
ColorQuantizeFn quantize_pure, ColorQuantizeFn quantize_pure)
const std::vector<Vec2f> *model_uv)
{ {
if (!quantize) if (!quantize)
return nullptr; return nullptr;
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ true, model_uv); auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ true);
if (prepared->empty()) if (prepared->empty())
return nullptr; return nullptr;
// Per layer: a flat-colour image is matched against the filaments alone, when that quantizer // Per layer: a flat-colour image is matched against the filaments alone, when that quantizer
@@ -3530,10 +3323,9 @@ ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh, HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers, const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data, const TextureDisplacementFacetsData &facets_data)
const std::vector<Vec2f> *model_uv)
{ {
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ false, model_uv); auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ false);
if (prepared->empty()) if (prepared->empty())
return nullptr; return nullptr;
@@ -5374,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
+5 -142
View File
@@ -86,17 +86,6 @@ enum class TextureProjectionMethod : int
// that is inherent to view projection and is the user's call, not a bug. The projector's two // that is inherent to view projection and is the user's call, not a bug. The projector's two
// in-plane axes live in TextureDisplacementLayer::view_project_right/up. // in-plane axes live in TextureDisplacementLayer::view_project_right/up.
ViewProjected = 4, ViewProjected = 4,
// The model's own texture coordinates, as the file it was imported from carried them (see
// ModelVolume::texture_uv). Nothing is unwrapped or projected: the artist's layout *is* the
// placement, so a texture authored against that layout - the model's own albedo map, say - lands
// exactly where it was painted. Automatically picked for a new layer on a volume that has a UV
// channel, since re-unwrapping a model that already carries one can only be worse than using it.
//
// The uv is already normalized, so unlike every method above it is not measured in millimetres:
// "Tile size" and the non-square aspect correction do not apply to it (the layer's rotation and
// offset still do). Falls back to Triplanar when the volume has no channel, exactly as LSCM falls
// back when a patch cannot be flattened.
ModelUV = 5,
}; };
// Dihedral angle (degrees) above which an edge between two painted triangles becomes a chart seam // Dihedral angle (degrees) above which an edge between two painted triangles becomes a chart seam
@@ -132,11 +121,6 @@ enum class TextureBlendMode : int
Subtract = 1, // acc - value (carve this layer's relief out of the layers below) Subtract = 1, // acc - value (carve this layer's relief out of the layers below)
Multiply = 2, // acc * (value / 1mm) (mask/modulate the layers below by this layer) Multiply = 2, // acc * (value / 1mm) (mask/modulate the layers below by this layer)
Divide = 3, // acc / (value / 1mm) (inverse mask; guarded against a zero/near-zero divisor) Divide = 3, // acc / (value / 1mm) (inverse mask; guarded against a zero/near-zero divisor)
// acc = value: this layer replaces what is under it instead of combining with it, so where it is
// painted only its own relief remains. The image editor's "Normal" mode, under the name the panel
// uses. Note the stack's *colour* already behaves this way - a higher slot's colour overwrites a
// lower one's where they overlap - so this is what makes the displacement agree with it.
Override = 4,
}; };
// Combines one layer's signed displacement `value` (mm) into `accumulated` (mm) per `mode`. // Combines one layer's signed displacement `value` (mm) into `accumulated` (mm) per `mode`.
@@ -413,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.
@@ -584,13 +541,7 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is // squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is
// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the // `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the
// square case and leaves the coordinate exactly as it always was. // square case and leaves the coordinate exactly as it always was.
// Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f);
// `normalized_uv` says the coordinate is already a uv rather than a millimetre measurement on the
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tiling scale (mm
// per tile is meaningless for it) nor the aspect correction (the artist's layout already accounts for
// the image's proportions) applies, and only the rotation and offset are left.
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f,
bool normalized_uv = false);
// Applies a row-major 3x4 projective matrix (see TextureDisplacementLayer::view_project_matrix) to a // Applies a row-major 3x4 projective matrix (see TextureDisplacementLayer::view_project_matrix) to a
// local-space point, writing the resulting texture uv. Returns false - and leaves `uv` untouched - // local-space point, writing the resulting texture uv. Returns false - and leaves `uv` untouched -
@@ -627,31 +578,6 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(), const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(),
const Vec2f *lscm_uv = nullptr); const Vec2f *lscm_uv = nullptr);
// Carries a per-facet paint annotation - the colour paint above all - from a mesh onto the mesh a
// bake produced from it: every triangle of `dst_mesh` takes the state painted at its own foot point on
// `src_mesh`, i.e. on the nearest point of the surface it was built from. Returned as one
// EnforcerBlockerType per `dst_mesh` triangle, 0 (NONE) where that foot point carried no paint.
//
// ModelVolume::restore_painting() cannot do this. Its remap pairs a source paint triangle with a
// target triangle that *overlaps* it to within 0.01 mm, which is right for a remesh or a
// simplification - they leave the surface where it was - and useless after a bake, which moves the
// surface by the relief depth. Measured on a model displaced by a tenth of its own size, that remap
// kept a quarter of the colour and left the rest unpainted; this keeps essentially all of it, because
// a foot point is exactly as far away as the relief is deep and the lookup does not care how far that
// is.
//
// The source's *sub*-triangles are what is looked up, not its triangles, so the part of a triangle a
// brush stroke only clipped a corner off keeps its own state rather than rounding to the whole
// triangle's. No distance limit is needed or wanted: the nearest point on the source surface is where
// a baked triangle came from however far the relief carried it, and an unpainted origin reports NONE
// on its own.
//
// Returns an empty vector when `src_paint` is empty or either mesh is - the caller then has nothing to
// carry, which is not the same as "carry nothing".
std::vector<uint8_t> transfer_facet_paint(const indexed_triangle_set &src_mesh,
const TriangleSelector::TriangleSplittingData &src_paint,
const indexed_triangle_set &dst_mesh);
// Area-weighted centroid and average normal of a layer's currently painted patch, in mesh-local // Area-weighted centroid and average normal of a layer's currently painted patch, in mesh-local
// coordinates - the same measurements build_texture_displacement() uses to pick its dominant // coordinates - the same measurements build_texture_displacement() uses to pick its dominant
// projection axis. Used by the GUI to anchor the on-canvas "adjust texture placement" gizmo to // projection axis. Used by the GUI to anchor the on-canvas "adjust texture placement" gizmo to
@@ -765,55 +691,6 @@ std::vector<Vec2f> compute_lscm_uvs(const indexed_triangle_set &patch, const Tex
// Returns an empty vector if the patch has no triangles or the unwrap carries no source_face map. // Returns an empty vector if the patch has no triangles or the unwrap carries no source_face map.
std::vector<Vec2f> compute_lscm_corner_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer); std::vector<Vec2f> compute_lscm_corner_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer);
// Three UVs per patch triangle, read out of the model's *own* UV channel - the
// TextureProjectionMethod::ModelUV counterpart of compute_lscm_corner_uvs(), and laid out the same way
// (corner k of triangle i at index 3i+k).
//
// `model_uv` is the volume's channel: three uvs per triangle of `base_mesh`, in its triangle order
// (see ModelVolume::texture_uv). `patch_source` maps each patch triangle back to the base-mesh
// triangle it was cut from - TriangleSelector::get_facets_strict()'s out_source. A patch triangle is
// always a piece of exactly one base triangle, so each of its corners is interpolated barycentrically
// inside that triangle's three uvs. That is what keeps a sub-triangle the brush split (whose corners
// are not base-mesh vertices at all) on the texture, and what keeps the two sides of a UV seam
// reading from their own island instead of from one averaged coordinate.
//
// Returns an empty vector when the channel does not match `base_mesh`, when `patch_source` does not
// match `patch`, or when either is empty - the callers treat that as "no placement" and fall back,
// exactly as they do for an unwrap that failed.
std::vector<Vec2f> compute_model_corner_uvs(const indexed_triangle_set &base_mesh,
const std::vector<Vec2f> &model_uv,
const indexed_triangle_set &patch,
const std::vector<int> &patch_source);
// One UV per patch vertex, collapsed from a per-corner array: the first corner to reach a vertex wins.
// This is the per-corner/per-vertex split compute_lscm_uvs() documents, applied to any corner array -
// displacement is per-vertex, so a vertex on a seam has to settle on one of its copies.
// Returns an empty vector if `corner` is not three per patch triangle.
std::vector<Vec2f> collapse_corner_uvs(const indexed_triangle_set &patch, const std::vector<Vec2f> &corner);
// Resamples a per-corner UV channel from `src_mesh` onto `dst_mesh`, for when something replaces a
// volume's mesh and the model's own UV channel has to come along: the colour import's painted mesh,
// the gizmo's remesh/subdivide, the default bake pipeline's rebuild. Each destination corner takes the
// uv of its closest point on the source surface, interpolated barycentrically inside the source
// triangle it lands on - the same transfer LscmLookup does per point, run over every corner.
//
// Exact wherever the two meshes describe the same surface, which is the case for every caller here
// (they subdivide, repair or displace it, they do not reshape it). A corner that lands nowhere near
// the source - farther than `max_distance` (negative: no limit) - takes (0, 0).
//
// One thing a positional transfer cannot reproduce: a *seam* vertex, where one position carries a
// different uv in each island that meets there. Position alone does not say which island a corner
// belongs to, so corners on a seam all take whichever island the nearest-triangle lookup lands on,
// and the texture can be off by up to one triangle in a band along the island borders. Doing better
// needs a triangle-to-triangle correspondence, which only some of the stages that replace a mesh
// have (a remesh has none at all).
//
// Returns an empty vector when `src_uv` is not three per source triangle, or when either mesh is empty.
std::vector<Vec2f> transfer_uv_channel(const indexed_triangle_set &src_mesh,
const std::vector<Vec2f> &src_uv,
const indexed_triangle_set &dst_mesh,
float max_distance = -1.f);
// The TextureDisplacementLayer::lscm_uv_overrides key for one unwrapped vertex (an index into PatchUnwrap::uvs). // The TextureDisplacementLayer::lscm_uv_overrides key for one unwrapped vertex (an index into PatchUnwrap::uvs).
inline int lscm_uv_override_key(int unwrapped_vertex) { return -(unwrapped_vertex + 1); } inline int lscm_uv_override_key(int unwrapped_vertex) { return -(unwrapped_vertex + 1); }
@@ -872,10 +749,6 @@ using TextureDisplacementFacetsData = std::array<TriangleSelector::TriangleSplit
// //
// `color`, when given, also reports which filament each triangle should print in - see // `color`, when given, also reports which filament each triangle should print in - see
// TextureColorRequest. // TextureColorRequest.
//
// `model_uv`, when given, is the volume's own UV channel (three uvs per base-mesh triangle, see
// ModelVolume::texture_uv) and is what a TextureProjectionMethod::ModelUV layer maps through. A layer
// set to ModelUV without it falls back to Triplanar.
struct TextureColorRequest struct TextureColorRequest
{ {
// RGB -> palette index. Supplied by the GUI, which owns both the perceptual matching and the list // RGB -> palette index. Supplied by the GUI, which owns both the perceptual matching and the list
@@ -950,9 +823,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
// stage, already brought back into `base_mesh`'s frame. // stage, already brought back into `base_mesh`'s frame.
BakeStageRecorder *debug = nullptr, BakeStageRecorder *debug = nullptr,
// When given, receives what the bake spent. // When given, receives what the bake spent.
TextureBakeStats *stats = nullptr, TextureBakeStats *stats = nullptr);
// The model's own UV channel; see above.
const std::vector<Vec2f> *model_uv = nullptr);
// `volume`'s mesh coordinates -> world millimetres: its first instance's transform times its own. // `volume`'s mesh coordinates -> world millimetres: its first instance's transform times its own.
// The mesh is shared by every instance, so a multi-instance object can only be baked for one of // The mesh is shared by every instance, so a multi-instance object can only be baked for one of
@@ -1027,15 +898,11 @@ ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set
const std::vector<TextureDisplacementLayer> &layers, const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data, const TextureDisplacementFacetsData &facets_data,
ColorQuantizeFn quantize, ColorQuantizeFn quantize,
ColorQuantizeFn quantize_pure = nullptr, ColorQuantizeFn quantize_pure = nullptr);
// The model's own UV channel, for ModelUV layers; see
// build_texture_displacement().
const std::vector<Vec2f> *model_uv = nullptr);
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh, HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers, const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data, const TextureDisplacementFacetsData &facets_data);
const std::vector<Vec2f> *model_uv = nullptr);
// Uniformly subdivides `mesh` (every triangle recursively split into 4 via edge midpoints, using a // Uniformly subdivides `mesh` (every triangle recursively split into 4 via edge midpoints, using a
// shared cache so a midpoint is computed once and reused by both triangles on either side of that // shared cache so a midpoint is computed once and reused by both triangles on either side of that
@@ -1240,10 +1107,6 @@ struct TextureDisplacementPrepareResult
{ {
indexed_triangle_set mesh; indexed_triangle_set mesh;
TextureDisplacementFacetsData masks; TextureDisplacementFacetsData masks;
// The model's own UV channel, resampled onto `mesh` (see transfer_uv_channel()). Empty when the
// volume had none. Like the masks it is handed back rather than left to the caller to recompute,
// because only the run itself knows which stages replaced the mesh.
std::vector<Vec2f> uv;
// The remesh landed but no layer's paint survived being carried onto it. Nothing is committed: // The remesh landed but no layer's paint survived being carried onto it. Nothing is committed:
// everything downstream is driven by that paint, so baking on would bake a flat mesh. // everything downstream is driven by that paint, so baking on would bake a flat mesh.
bool paint_lost = false; bool paint_lost = false;
-21
View File
@@ -25,7 +25,6 @@
#include "TextureToColor/ColorUtils.hpp" #include "TextureToColor/ColorUtils.hpp"
#include "Model.hpp" #include "Model.hpp"
#include "TextureDisplacement.hpp"
#include "TextureToColor/TriMesh.hpp" #include "TextureToColor/TriMesh.hpp"
#include "TriangleMesh.hpp" #include "TriangleMesh.hpp"
#include "TriangleSelector.hpp" #include "TriangleSelector.hpp"
@@ -555,29 +554,9 @@ bool apply_painted_mesh_to_volume(
<< ", unit_scale=" << unit_scale << ")"; << ", unit_scale=" << unit_scale << ")";
} }
// The model's own UV channel is indexed by triangle, and this replaces the triangles, so it has
// to be resampled onto them. Kept rather than dropped because it is the layout the model was
// authored against: a texture-displacement layer set to ModelUV maps through it, and the colour
// import is exactly the path a model that carries one arrives by.
const indexed_triangle_set old_its = volume.has_texture_uv() ? volume.mesh().its : indexed_triangle_set{};
const std::vector<Vec2f> old_uv = volume.texture_uv;
volume.set_mesh(std::move(new_mesh)); volume.set_mesh(std::move(new_mesh));
volume.calculate_convex_hull(); volume.calculate_convex_hull();
// Resampled here and not further down, while the two meshes still share a frame: the painted mesh
// was just shifted by the same source.mesh_offset the old one was centred by, so the only thing
// between them is whatever drift the repair introduced. The re-centering below translates the mesh
// again, and a translation is exactly what a closest-point transfer cannot see through - it would
// read every uv off a point that far away. A uv is a coordinate in the texture, so it does not care
// about either shift; it just has to be read before the second one happens.
if (!old_its.indices.empty())
volume.texture_uv = transfer_uv_channel(old_its, old_uv, volume.mesh().its);
else
// No channel to carry, so make sure none is left behind either: a leftover of the right length
// would read as a valid channel against a mesh it was never measured on.
volume.reset_texture_uv();
// Re-center the replaced mesh so its bbox center sits at the origin, // Re-center the replaced mesh so its bbox center sits at the origin,
// matching what center_geometry_after_creation did for the original mesh. // matching what center_geometry_after_creation did for the original mesh.
// CGAL repair / subdivision may shift the bbox center (drift); without // CGAL repair / subdivision may shift the bbox center (drift); without
+4 -1
View File
@@ -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_
+1
View File
@@ -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.
+3 -1
View File
@@ -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;
+3
View File
@@ -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
+4 -1
View File
@@ -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
+19
View File
@@ -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
+3 -2
View File
@@ -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))
+4 -1
View File
@@ -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.
+6
View File
@@ -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);
+1
View File
@@ -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;
@@ -494,17 +494,7 @@ bool GLGizmoTextureDisplacement::on_init()
{ctrl + _L("Mouse wheel"), m_desc["gap_area"]}, {ctrl + _L("Mouse wheel"), m_desc["gap_area"]},
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;
} }
@@ -1231,33 +1221,9 @@ indexed_triangle_set GLGizmoTextureDisplacement::patch_in_world(const indexed_tr
return world; return world;
} }
std::vector<Vec2f> GLGizmoTextureDisplacement::model_uv_corners(const indexed_triangle_set &local_patch,
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source) const
{
const ModelVolume *mv = texture_volume();
if (layer.projection_method != TextureProjectionMethod::ModelUV || patch_source == nullptr || mv == nullptr ||
!mv->has_texture_uv())
return {};
// The channel is indexed against the volume's *local* mesh, and a uv is a coordinate in the
// texture rather than on the model, so unlike every projected method this never goes through
// patch_in_world() - there is no millimetre measurement here for a world scale to change.
std::vector<Vec2f> uv = compute_model_corner_uvs(mv->mesh().its, mv->texture_uv, local_patch, *patch_source);
for (Vec2f &p : uv)
// Already a uv: no tiling scale, no aspect correction - see apply_uv_transform().
p = apply_uv_transform(p, layer, 1.f, /* normalized_uv */ true);
return uv;
}
std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_vertex_uvs(const indexed_triangle_set &local_patch, std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_vertex_uvs(const indexed_triangle_set &local_patch,
const TextureDisplacementLayer &layer, const TextureDisplacementLayer &layer) const
const std::vector<int> *patch_source) const
{ {
if (layer.projection_method == TextureProjectionMethod::ModelUV)
// Collapsed to one per vertex the same way the unwrap is - a vertex has one position, so the
// displacement can only read one height for it.
return collapse_corner_uvs(local_patch, model_uv_corners(local_patch, layer, patch_source));
// The bake maps the texture in world millimetres, so "Tile size (mm)" means the same thing on a // The bake maps the texture in world millimetres, so "Tile size (mm)" means the same thing on a
// scaled instance as it does on an untouched one (see build_texture_displacement()). Everything // scaled instance as it does on an untouched one (see build_texture_displacement()). Everything
// that has to agree with the bake - the fast preview's uvs, the checker/distortion overlays - // that has to agree with the bake - the fast preview's uvs, the checker/distortion overlays -
@@ -1317,11 +1283,8 @@ int GLGizmoTextureDisplacement::layer_projection_frame(const indexed_triangle_se
} }
std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_corner_uvs(const indexed_triangle_set &local_patch, std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_corner_uvs(const indexed_triangle_set &local_patch,
const TextureDisplacementLayer &layer, const TextureDisplacementLayer &layer) const
const std::vector<int> *patch_source) const
{ {
if (layer.projection_method == TextureProjectionMethod::ModelUV)
return model_uv_corners(local_patch, layer, patch_source);
if (layer.projection_method == TextureProjectionMethod::LSCM) { if (layer.projection_method == TextureProjectionMethod::LSCM) {
const indexed_triangle_set patch = patch_in_world(local_patch); const indexed_triangle_set patch = patch_in_world(local_patch);
const float aspect = layer_texture_aspect(layer); const float aspect = layer_texture_aspect(layer);
@@ -1331,7 +1294,7 @@ std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_corner_uvs(const in
return uv; return uv;
} }
// Single-valued per point: fan the per-vertex result out over the corners. // Single-valued per point: fan the per-vertex result out over the corners.
const std::vector<Vec2f> per_vertex = compute_layer_vertex_uvs(local_patch, layer, patch_source); const std::vector<Vec2f> per_vertex = compute_layer_vertex_uvs(local_patch, layer);
if (per_vertex.size() != local_patch.vertices.size()) if (per_vertex.size() != local_patch.vertices.size())
return {}; return {};
std::vector<Vec2f> corner(local_patch.indices.size() * 3); std::vector<Vec2f> corner(local_patch.indices.size() * 3);
@@ -1352,11 +1315,7 @@ void GLGizmoTextureDisplacement::rebuild_shaded_preview_mesh()
// Uses the *live* selector (not the flushed model facet data), so this reflects an in-progress // Uses the *live* selector (not the flushed model facet data), so this reflects an in-progress
// stroke immediately rather than only once it ends - the point of this preview mode is to be // stroke immediately rather than only once it ends - the point of this preview mode is to be
// the fast, no-CPU-meshing one. // the fast, no-CPU-meshing one.
// The source map comes along because a ModelUV layer's uvs are read per base-mesh triangle; every const indexed_triangle_set patch = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER);
// other mapping ignores it.
std::vector<int> patch_source;
const indexed_triangle_set patch =
m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER, &patch_source);
if (patch.indices.empty()) if (patch.indices.empty())
return; return;
@@ -1380,8 +1339,7 @@ void GLGizmoTextureDisplacement::rebuild_shaded_preview_mesh()
// Per *corner*, not per vertex: the mesh below is flat (unshared) anyway, so each triangle can // Per *corner*, not per vertex: the mesh below is flat (unshared) anyway, so each triangle can
// carry its own chart's UVs - see compute_layer_corner_uvs(). // carry its own chart's UVs - see compute_layer_corner_uvs().
const TextureDisplacementLayer *active = active_layer(); const TextureDisplacementLayer *active = active_layer();
std::vector<Vec2f> corner_uv = active != nullptr ? compute_layer_corner_uvs(patch, *active, &patch_source) : std::vector<Vec2f> corner_uv = active != nullptr ? compute_layer_corner_uvs(patch, *active) : std::vector<Vec2f>{};
std::vector<Vec2f>{};
m_shaded_preview_uses_vertex_uv = corner_uv.size() == patch.indices.size() * 3; m_shaded_preview_uses_vertex_uv = corner_uv.size() == patch.indices.size() * 3;
if (!m_shaded_preview_uses_vertex_uv) if (!m_shaded_preview_uses_vertex_uv)
corner_uv.clear(); corner_uv.clear();
@@ -2038,9 +1996,7 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
const ModelVolume *mv = texture_volume(); const ModelVolume *mv = texture_volume();
if (mv == nullptr || m_triangle_selectors.empty()) if (mv == nullptr || m_triangle_selectors.empty())
return; return;
std::vector<int> patch_source; // only ModelUV reads it; see compute_layer_vertex_uvs() const indexed_triangle_set patch = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER);
const indexed_triangle_set patch =
m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER, &patch_source);
if (patch.indices.empty()) if (patch.indices.empty())
return; return;
// Everything below works in the *patch's* vertex space, not the mesh's. Those agree only until a // Everything below works in the *patch's* vertex space, not the mesh's. Those agree only until a
@@ -2054,7 +2010,7 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
// The checker samples wherever the projection puts it; the projections the shader can't // The checker samples wherever the projection puts it; the projections the shader can't
// reconstruct (LSCM, ViewProjected) get a precomputed per-vertex uv, the rest project in-shader. // reconstruct (LSCM, ViewProjected) get a precomputed per-vertex uv, the rest project in-shader.
std::vector<Vec2f> uv = compute_layer_vertex_uvs(patch, *layer, &patch_source); std::vector<Vec2f> uv = compute_layer_vertex_uvs(patch, *layer);
const bool have_uvs = uv.size() == patch.vertices.size(); const bool have_uvs = uv.size() == patch.vertices.size();
m_uvcheck_uses_vertex_uv = have_uvs; m_uvcheck_uses_vertex_uv = have_uvs;
m_uvcheck_projection_mode = have_uvs ? 0 : m_uvcheck_projection_mode = have_uvs ? 0 :
@@ -2063,7 +2019,7 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
// different uv in each island it borders, so the shared-vertex form drew one triangle per face from // different uv in each island it borders, so the shared-vertex form drew one triangle per face from
// a neighbouring island's placement. Only the *drawing* needs this; the distortion metric below is // a neighbouring island's placement. Only the *drawing* needs this; the distortion metric below is
// a per-vertex average by construction and keeps using `uv`. // a per-vertex average by construction and keeps using `uv`.
const std::vector<Vec2f> corner_uv = compute_layer_corner_uvs(patch, *layer, &patch_source); const std::vector<Vec2f> corner_uv = compute_layer_corner_uvs(patch, *layer);
const bool have_corner_uvs = have_uvs && corner_uv.size() == patch.indices.size() * 3; const bool have_corner_uvs = have_uvs && corner_uv.size() == patch.indices.size() * 3;
// Per-vertex area distortion in [0,1] (0.5 == ideal), only when both requested and possible. // Per-vertex area distortion in [0,1] (0.5 == ideal), only when both requested and possible.
@@ -2339,8 +2295,6 @@ void GLGizmoTextureDisplacement::queue_preview_job()
if (input.options.pipeline_v2) if (input.options.pipeline_v2)
input.options.v2_max_triangles_k = 0; input.options.v2_max_triangles_k = 0;
input.volume_to_world = texture_displacement_volume_to_world(*mv); input.volume_to_world = texture_displacement_volume_to_world(*mv);
if (mv->has_texture_uv())
input.model_uv = mv->texture_uv;
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data(); input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data();
// Captured here rather than read in the handler: the palette is main-thread state, and the preview // Captured here rather than read in the handler: the palette is main-thread state, and the preview
@@ -3604,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;
@@ -3640,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
@@ -3706,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)
@@ -3722,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.
@@ -3750,13 +3676,10 @@ void GLGizmoTextureDisplacement::ensure_panel_icons()
// Paint / Erase, brush cursor shape, mapping, tiling, placement and the dock toggle. // Paint / Erase, brush cursor shape, mapping, tiling, placement and the dock toggle.
"texture_displacement_add.svg", "texture_displacement_add_negative.svg", "circle_paint.svg", "texture_displacement_add.svg", "texture_displacement_add_negative.svg", "circle_paint.svg",
"menu_obj_sphere.svg", "menu_obj_cube.svg", "menu_obj_cylinder.svg", "texture_displacement_map_unwrap.svg", "menu_obj_sphere.svg", "menu_obj_cube.svg", "menu_obj_cylinder.svg", "texture_displacement_map_unwrap.svg",
"texture_displacement_map_view.svg", "texture_displacement_map_model_uv.svg", "texture_displacement_map_view.svg", "texture_displacement_tile_repeat.svg", "menu_mirror_x.svg",
"texture_displacement_tile_repeat.svg", "menu_mirror_x.svg",
"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());
@@ -3800,11 +3723,6 @@ void GLGizmoTextureDisplacement::add_texture_layer()
TextureDisplacementLayer layer; TextureDisplacementLayer layer;
layer.slot = free_slot; layer.slot = free_slot;
// A model that came with a UV channel is mapped by it. Re-unwrapping such a model throws away the
// layout it was authored against and can only be worse than using it, so the default follows the
// model rather than the other way round; the Mapping row still offers every other method.
if (mv->has_texture_uv())
layer.projection_method = TextureProjectionMethod::ModelUV;
// Start the layer off on the first library texture rather than on nothing at all: a textureless // Start the layer off on the first library texture rather than on nothing at all: a textureless
// layer looks broken (painting on it appears to do nothing, because there is no height map to // layer looks broken (painting on it appears to do nothing, because there is no height map to
// displace by). The user swaps it for another from the layer's own picker. // displace by). The user swaps it for another from the layer's own picker.
@@ -4950,8 +4868,7 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve
TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh( TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
const indexed_triangle_set &base, const TextureDisplacementFacetsData &masks, const indexed_triangle_set &base, const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers, const std::vector<Vec2f> &model_uv, const std::vector<TextureDisplacementLayer> &layers, const TextureDisplacementPrepareParams &params,
const TextureDisplacementPrepareParams &params,
const TextureColorSettings &color_settings, const DisplacementProgressFn &progress, const TextureColorSettings &color_settings, const DisplacementProgressFn &progress,
BakeStageRecorder *debug) BakeStageRecorder *debug)
{ {
@@ -4978,13 +4895,6 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
bool had_paint = false; bool had_paint = false;
for (const TriangleSelector::TriangleSplittingData &m : masks) for (const TriangleSelector::TriangleSplittingData &m : masks)
had_paint = had_paint || TriangleSelector::has_facets(m, EnforcerBlockerType::ENFORCER); had_paint = had_paint || TriangleSelector::has_facets(m, EnforcerBlockerType::ENFORCER);
// The model's own UV channel for whatever `mesh` currently is. Every stage below keeps the input's
// surface - it evens out, refines or cuts the triangles lying on it - so each corner reads its uv
// straight off `base`'s channel at its own position instead of being carried stage by stage, which
// would compound each stage's approximation. Untouched while nothing has replaced the mesh yet.
const auto uv_now = [&]() {
return (model_uv.empty() || !changed) ? model_uv : transfer_uv_channel(base, model_uv, mesh.its);
};
if (!report(1)) if (!report(1))
return out; return out;
@@ -5031,11 +4941,9 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
// Feature-adaptive: sample the combined displacement so refinement follows texture // Feature-adaptive: sample the combined displacement so refinement follows texture
// curvature. A null sampler (only LSCM layers, or nothing decodable) falls back to the // curvature. A null sampler (only LSCM layers, or nothing decodable) falls back to the
// length baseline alone. // length baseline alone.
const std::vector<Vec2f> uv_stage = uv_now();
const std::vector<Vec2f> *uv_ptr = uv_stage.empty() ? nullptr : &uv_stage;
HeightFieldSampler sampler; HeightFieldSampler sampler;
if (params.subdiv_feature) if (params.subdiv_feature)
sampler = make_combined_displacement_sampler(mesh.its, layers, current, uv_ptr); sampler = make_combined_displacement_sampler(mesh.its, layers, current);
// Colour boundaries need triangles of their own - the chord test cannot see them, since // Colour boundaries need triangles of their own - the chord test cannot see them, since
// the height field is perfectly smooth across a change of filament. // the height field is perfectly smooth across a change of filament.
ColorFieldSampler color; ColorFieldSampler color;
@@ -5043,7 +4951,7 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
// boundaries are refined where the bake will actually change filament. // boundaries are refined where the bake will actually change filament.
if (params.subdiv_color_edge_mm > 0.f && !color_settings.empty()) if (params.subdiv_color_edge_mm > 0.f && !color_settings.empty())
color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(color_settings.palette), color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(color_settings.palette),
make_palette_quantizer(color_settings.palette_pure), uv_ptr); make_palette_quantizer(color_settings.palette_pure));
// The refinement follows perceived colour: a mix is one colour here, however the slicer // The refinement follows perceived colour: a mix is one colour here, however the slicer
// interleaves its filaments layer by layer. // interleaves its filaments layer by layer.
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down // "Min edge" is a feature-mode control (it is the floor the curvature test refines down
@@ -5102,10 +5010,8 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
std::vector<uint8_t> cut_region(mesh.its.indices.size(), 0); std::vector<uint8_t> cut_region(mesh.its.indices.size(), 0);
for (size_t i = 0; i < cut_region.size() && i < source.size(); ++i) for (size_t i = 0; i < cut_region.size() && i < source.size(); ++i)
cut_region[i] = (region[size_t(source[i])] & REFINE_PAINTED) ? 1 : 0; cut_region[i] = (region[size_t(source[i])] & REFINE_PAINTED) ? 1 : 0;
const std::vector<Vec2f> uv_refined = uv_now();
const HeightFieldSampler refined_sampler = const HeightFieldSampler refined_sampler =
make_combined_displacement_sampler(mesh.its, layers, current, make_combined_displacement_sampler(mesh.its, layers, current);
uv_refined.empty() ? nullptr : &uv_refined);
std::vector<int> cut_source; std::vector<int> cut_source;
size_t cuts = 0; size_t cuts = 0;
indexed_triangle_set cut = cut_mesh_at_steps(mesh.its, cut_region, refined_sampler, indexed_triangle_set cut = cut_mesh_at_steps(mesh.its, cut_region, refined_sampler,
@@ -5129,7 +5035,6 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
if (!report(100) || !changed) if (!report(100) || !changed)
return out; // an empty result: nothing to commit, which is not a failure return out; // an empty result: nothing to commit, which is not a failure
out.uv = uv_now();
out.mesh = std::move(mesh.its); out.mesh = std::move(mesh.its);
out.masks = std::move(current); out.masks = std::move(current);
return out; return out;
@@ -5781,8 +5686,6 @@ void GLGizmoTextureDisplacement::queue_prepare(const TextureDisplacementPrepareP
input.layers = mv->texture_displacement_layers; input.layers = mv->texture_displacement_layers;
input.params = params; input.params = params;
input.snapshot_name = snapshot_name; input.snapshot_name = snapshot_name;
if (mv->has_texture_uv())
input.model_uv = mv->texture_uv;
if (params.subdiv_color_edge_mm > 0.f) if (params.subdiv_color_edge_mm > 0.f)
input.color = color_settings_for(*mv); input.color = color_settings_for(*mv);
@@ -6016,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();
@@ -6025,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;
@@ -6173,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.
@@ -6196,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();
@@ -6349,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);
@@ -6365,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();
@@ -6638,25 +6451,16 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// The three controls nearly every layer needs; everything else waits behind "More settings". // 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."));
// Mapped by the model's own UV layout there is no millimetre scale to set: the layout is the m_preview_params_dirty |= float_row("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad);
// placement (see TextureProjectionMethod::ModelUV), so the slider would move nothing. The row hover_tip(_u8L("How wide one copy of the texture is on the model. Smaller repeats the pattern "
// states why rather than hiding: float_row()'s tooltip is shown for a disabled slider too. "more often and makes its detail finer; with Tile off, this is the size of the "
const bool tile_size_na = layer.projection_method == TextureProjectionMethod::ModelUV; "single copy."));
m_imgui->disabled_begin(tile_size_na); 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("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad, hover_tip(_u8L("Turns the texture on the surface, in degrees - for lining a pattern up with an "
tile_size_na ? "edge of the model."));
_L("Mapped by the model's own UV layout, which is already the placement - one "
"copy of the texture covers it exactly, so there is no tile size to set.") :
_L("How wide one copy of the texture is on the model. Smaller repeats the pattern "
"more often and makes its detail finer; with Tile off, this is the size of the "
"single copy."));
m_imgui->disabled_end();
m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad,
_L("Turns the texture on the surface, in degrees - for lining a pattern up with an "
"edge of the model."));
if (m_layer_expanded[size_t(layer.slot)]) { if (m_layer_expanded[size_t(layer.slot)]) {
{ {
@@ -6671,22 +6475,22 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// Midlevel: the height that means "don't move". At 0 the surface only ever bulges outwards; // 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 "
"cut can pass through the other side.")); "cut can pass through the other side."));
if (layer.midlevel > 0.f && layer.depth_mm > 1.f) { if (layer.midlevel > 0.f && layer.depth_mm > 1.f) {
ImGui::PushTextWrapPos(content_rx - card_pad); ImGui::PushTextWrapPos(content_rx - card_pad);
m_imgui->warning_text(_L("Deep inward displacement may self-intersect.")); m_imgui->warning_text(_L("Deep inward displacement may self-intersect."));
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."));
// Edge fade: the relief flattens toward the boundary of the painted area. // Edge fade: the relief flattens toward the boundary of the painted area.
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Edge fade") + "##edge_smoothing", layer.edge_smoothing); m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Edge fade") + "##edge_smoothing", layer.edge_smoothing);
@@ -6694,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.
{ {
@@ -6743,23 +6548,23 @@ 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."));
} }
} }
// Mapping, as one icon per TextureProjectionMethod, in the enum's own order. // Mapping, as five icons in TextureProjectionMethod's own order.
{ {
const float x0 = ImGui::GetCursorPosX(); const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
@@ -6787,26 +6592,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
{ "texture_displacement_map_view.svg", _L("From view"), { "texture_displacement_map_view.svg", _L("From view"),
_L("From view - projects straight onto the painted area from where you are looking, like a slide " _L("From view - projects straight onto the painted area from where you are looking, like a slide "
"projector.") }, "projector.") },
{ "texture_displacement_map_model_uv.svg", _L("Model UV"),
_L("Model UV - uses the texture coordinates the model itself came with, so a texture made for "
"that model lands exactly where it was painted. Nothing is unwrapped, and Tile size does "
"not apply: the model's own layout is the placement.") },
}; };
// Only a volume that actually carries a UV channel can be mapped by it. Shown
// greyed out rather than hidden, so the reason is visible instead of the control
// simply being missing.
const wxString model_uv_na =
(mv != nullptr && mv->has_texture_uv()) ?
wxString() :
_L("This model carries no texture coordinates of its own. Only a model imported with a UV "
"channel - a textured OBJ, say - can be mapped by them.");
for (int mi = 0; mi < int(IM_ARRAYSIZE(mappings)); ++mi) { for (int mi = 0; mi < int(IM_ARRAYSIZE(mappings)); ++mi) {
if (mi > 0) if (mi > 0)
ImGui::SameLine(0.f, gap_s); ImGui::SameLine(0.f, gap_s);
const auto method = static_cast<TextureProjectionMethod>(mi); const auto method = static_cast<TextureProjectionMethod>(mi);
const wxString na = (method == TextureProjectionMethod::ModelUV) ? model_uv_na : wxString();
if (icon_toggle(830 + mi, mappings[mi].file, layer.projection_method == method, icon_sm, mappings[mi].label, if (icon_toggle(830 + mi, mappings[mi].file, layer.projection_method == method, icon_sm, mappings[mi].label,
mappings[mi].tip, na) && mappings[mi].tip) &&
layer.projection_method != method) { layer.projection_method != method) {
// Capture the current view the moment "From view" is chosen, so it does something // Capture the current view the moment "From view" is chosen, so it does something
// sensible immediately rather than projecting from a stale/default direction. // sensible immediately rather than projecting from a stale/default direction.
@@ -6873,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();
@@ -6936,33 +6728,18 @@ 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));
const std::string blend_add = _u8L_CONTEXT("Add", "Texture Displacement"); const std::string blend_add = _u8L_CONTEXT("Add", "Texture Displacement");
const std::string blend_override = _u8L_CONTEXT("Override", "Texture Displacement");
const std::string blend_subtract = _u8L_CONTEXT("Subtract", "Texture Displacement"); const std::string blend_subtract = _u8L_CONTEXT("Subtract", "Texture Displacement");
const std::string blend_multiply = _u8L_CONTEXT("Multiply", "Texture Displacement"); const std::string blend_multiply = _u8L_CONTEXT("Multiply", "Texture Displacement");
const std::string blend_divide = _u8L("Divide"); const std::string blend_divide = _u8L("Divide");
const char *blend_items[] = { blend_add.c_str(), blend_override.c_str(), blend_subtract.c_str(), const char *blend_items[] = { blend_add.c_str(), blend_subtract.c_str(), blend_multiply.c_str(), blend_divide.c_str() };
blend_multiply.c_str(), blend_divide.c_str() };
// The combo's order is the panel's, not the enum's - Override reads as the sibling of Add and
// belongs next to it, while TextureBlendMode's numbering is fixed by what projects already
// store. Hence the explicit mapping rather than a cast of the index.
static const TextureBlendMode blend_modes[] = { TextureBlendMode::Add, TextureBlendMode::Override,
TextureBlendMode::Subtract, TextureBlendMode::Multiply,
TextureBlendMode::Divide };
static_assert(IM_ARRAYSIZE(blend_items) == IM_ARRAYSIZE(blend_modes),
"every Blend entry needs its mode");
// The first layer has nothing before it to combine with - build_texture_displacement() makes it // The first layer has nothing before it to combine with - build_texture_displacement() makes it
// add regardless - so its combo shows Add and cannot be changed. // add regardless - so its combo shows Add and cannot be changed.
const bool base_layer = li == 0; const bool base_layer = li == 0;
int blend_mode = 0; int blend_mode = base_layer ? 0 : static_cast<int>(layer.blend_mode);
if (!base_layer)
for (int bi = 0; bi < int(IM_ARRAYSIZE(blend_modes)); ++bi)
if (blend_modes[bi] == layer.blend_mode)
blend_mode = bi;
ImGui::SetNextItemWidth(-(card_pad + gap_s + icon_sm)); ImGui::SetNextItemWidth(-(card_pad + gap_s + icon_sm));
m_imgui->disabled_begin(base_layer); m_imgui->disabled_begin(base_layer);
if (scoped_combo("##blend_mode", &blend_mode, blend_items, IM_ARRAYSIZE(blend_items)) && !base_layer && if (scoped_combo("##blend_mode", &blend_mode, blend_items, IM_ARRAYSIZE(blend_items)) && !base_layer) {
blend_mode >= 0 && blend_mode < int(IM_ARRAYSIZE(blend_modes))) { layer.blend_mode = static_cast<TextureBlendMode>(blend_mode);
layer.blend_mode = blend_modes[blend_mode];
m_preview_params_dirty = true; m_preview_params_dirty = true;
} }
m_imgui->disabled_end(); m_imgui->disabled_end();
@@ -6970,11 +6747,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
m_imgui->tooltip(base_layer ? m_imgui->tooltip(base_layer ?
_u8L("The base layer has nothing beneath it to combine with, so it always adds.") : _u8L("The base layer has nothing beneath it to combine with, so it always adds.") :
_u8L("What this layer does where it overlaps the ones below. Add and " _u8L("What this layer does where it overlaps the ones below. Add and "
"Subtract pile relief on or carve it away. Override replaces them, " "Subtract pile relief on or carve it away. Multiply and Divide scale "
"so only this layer's relief is left where it is painted. Multiply " "what is underneath, which turns this layer into a mask over it - "
"and Divide scale what is underneath, which turns this layer into a " "there, Depth acts as a strength, and 1 mm leaves white areas "
"mask over it - there, Depth acts as a strength, and 1 mm leaves " "untouched."),
"white areas untouched."),
wrap_w); wrap_w);
ImGui::SameLine(0.f, gap_s); ImGui::SameLine(0.f, gap_s);
@@ -7193,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. "
"Smaller follows fine detail and costs triangles; larger only chases the big "
"features.")))
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("How far the mesh may sit from the shape the texture describes, in millimetres. "
_L("A floor on triangle size, so a sharp step in the texture cannot be chased " "Smaller follows fine detail and costs triangles; larger only chases the big "
"forever. Lower it for finer relief, raise it if the count runs away at hard " "features."));
"edges."))) 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();
hover_tip(_u8L("A floor on triangle size, so a sharp step in the texture cannot be chased "
"forever. Lower it for finer relief, raise it if the count runs away at hard "
"edges."));
} }
// Applies in both adaptive sub-modes: it is about the step the bake puts at the paint's edge, which // 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."));
} }
{ {
@@ -7305,9 +7081,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
} }
m_remesh_target_edge_mm = cnt > 0 ? std::clamp(float(sum / double(cnt)), 0.1f, 20.f) : 1.f; 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);
hover_tip(_u8L("Holds hard edges and open borders in place while the rest is remeshed. Without it " hover_tip(_u8L("Holds hard edges and open borders in place while the rest is remeshed. Without it "
@@ -7315,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()))
@@ -7346,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 "
@@ -7398,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
@@ -7514,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
@@ -7567,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);
@@ -7596,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));
@@ -61,12 +61,6 @@ public:
static TextureDisplacementPrepareResult prepare_mesh(const indexed_triangle_set &base, static TextureDisplacementPrepareResult prepare_mesh(const indexed_triangle_set &base,
const TextureDisplacementFacetsData &masks, const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers, const std::vector<TextureDisplacementLayer> &layers,
// `base`'s own UV channel, when it has one
// (ModelVolume::texture_uv). Resampled onto
// the result and reported back in it, and fed
// to the samplers so a ModelUV layer drives
// the refinement through its own layout.
const std::vector<Vec2f> &model_uv,
const TextureDisplacementPrepareParams &params, const TextureDisplacementPrepareParams &params,
const TextureColorSettings &color_settings, const TextureColorSettings &color_settings,
const DisplacementProgressFn &progress, const DisplacementProgressFn &progress,
@@ -268,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
@@ -413,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
@@ -833,31 +818,18 @@ private:
// Merges chart `b`'s join group into chart `a`'s (materialising `groups` to `chart_count` first). // Merges chart `b`'s join group into chart `a`'s (materialising `groups` to `chart_count` first).
static void join_island_groups(std::vector<int> &groups, int a, int b, int chart_count); static void join_island_groups(std::vector<int> &groups, int a, int b, int chart_count);
// Final per-vertex texture uv for the projections the shader can't reconstruct itself - LSCM (an // Final per-vertex texture uv for the projections the shader can't reconstruct itself - LSCM (an
// unwrap), ModelUV (the model's own channel) and ViewProjected (a projector plane the shader // unwrap) and ViewProjected (a projector plane the shader doesn't know). One entry per patch/base
// doesn't know). One entry per patch/base vertex, already through apply_uv_transform(). Empty for // vertex, already through apply_uv_transform(). Empty for Triplanar/Cylindrical/Spherical, which
// Triplanar/Cylindrical/Spherical, which the shader projects on its own. Shared by the shaded // the shader projects on its own. Shared by the shaded preview and the UV-check overlay.
// preview and the UV-check overlay.
//
// `patch_source` maps each patch triangle back to its base-mesh triangle
// (TriangleSelector::get_facets_strict()'s out_source). Only ModelUV needs it - its channel is per
// base triangle - and without it a ModelUV layer reports no uvs and the preview falls back.
std::vector<Vec2f> compute_layer_vertex_uvs(const indexed_triangle_set &patch, std::vector<Vec2f> compute_layer_vertex_uvs(const indexed_triangle_set &patch,
const TextureDisplacementLayer &layer, const TextureDisplacementLayer &layer) const;
const std::vector<int> *patch_source = nullptr) const;
// The same, but three UVs per patch triangle (corner 0..2 of triangle i at 3i..3i+2). This is what // The same, but three UVs per patch triangle (corner 0..2 of triangle i at 3i..3i+2). This is what
// the flat, unshared-vertex preview meshes actually want: under LSCM a seam vertex has a different // the flat, unshared-vertex preview meshes actually want: under LSCM a seam vertex has a different
// UV in each island it borders, so collapsing to one per vertex handed a triangle at an unjoined // UV in each island it borders, so collapsing to one per vertex handed a triangle at an unjoined
// seam its neighbour's placement - one visibly skewed triangle per face. Every other projection is // seam its neighbour's placement - one visibly skewed triangle per face. Every other projection is
// single-valued per point, so there a corner's UV is just its vertex's. // single-valued per point, so there a corner's UV is just its vertex's.
std::vector<Vec2f> compute_layer_corner_uvs(const indexed_triangle_set &patch, std::vector<Vec2f> compute_layer_corner_uvs(const indexed_triangle_set &patch,
const TextureDisplacementLayer &layer, const TextureDisplacementLayer &layer) const;
const std::vector<int> *patch_source = nullptr) const;
// The model's own channel over `local_patch`, already through apply_uv_transform() - three per
// patch triangle. Empty when the volume has no channel, or when `patch_source` is missing. Shared
// by the two functions above so the per-vertex and per-corner forms can never disagree.
std::vector<Vec2f> model_uv_corners(const indexed_triangle_set &local_patch,
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source) const;
// The in-shader projection for `layer` (0 Triplanar, 1 Cylindrical, 2 Spherical) plus, for the two // The in-shader projection for `layer` (0 Triplanar, 1 Cylindrical, 2 Spherical) plus, for the two
// wrapping ones, the patch centroid and cylinder axis they wrap around - in the texture frame the // wrapping ones, the patch centroid and cylinder axis they wrap around - in the texture frame the
// shaders project in. Taken from the bake's own texture_displacement_patch_frame(), so a preview // shaders project in. Taken from the bake's own texture_displacement_patch_frame(), so a preview
@@ -999,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
@@ -1047,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
@@ -8,7 +8,6 @@
#include "libslic3r/TextureDisplacement.hpp" #include "libslic3r/TextureDisplacement.hpp"
#include <exception> #include <exception>
#include <cstddef> #include <cstddef>
#include <cstdint>
#include "slic3r/GUI/Jobs/Worker.hpp" #include "slic3r/GUI/Jobs/Worker.hpp"
#include <memory> #include <memory>
@@ -27,7 +26,6 @@
#include "slic3r/Utils/UndoRedo.hpp" #include "slic3r/Utils/UndoRedo.hpp"
#include "libslic3r/ObjectID.hpp" #include "libslic3r/ObjectID.hpp"
#include "libslic3r/TriangleMesh.hpp" #include "libslic3r/TriangleMesh.hpp"
#include <array>
namespace Slic3r::GUI { namespace Slic3r::GUI {
@@ -76,8 +74,7 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
} }
return true; return true;
}, },
color, m_input.volume_to_world, nullptr, &m_stats, color, m_input.volume_to_world, nullptr, &m_stats));
m_input.model_uv.empty() ? nullptr : &m_input.model_uv));
// Always finish at 100: this is what closes the notification. Reported even on cancel, where // Always finish at 100: this is what closes the notification. Reported even on cancel, where
// build_texture_displacement() returns an empty mesh and finalize() commits nothing. // build_texture_displacement() returns an empty mesh and finalize() commits nothing.
@@ -129,71 +126,16 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
{ mix.a_percent(), 100 - mix.a_percent() }); { mix.a_percent(), 100 - mix.a_percent() });
}); });
// Whether the bake replaced the triangles or only moved the vertices it was given. The classic
// path is topology-preserving by contract, so every per-triangle annotation on the volume still
// means what it did; the default pipeline refines, cuts and simplifies, so none of them do.
// Both are checked: the flag is what the bake promised, the count is what it delivered. Only
// the triangles matter - the annotations are per facet and reference no vertex index - so a
// vertex count that changed on its own (the bake compacts unreferenced vertices away) is not a
// reason to remap anything.
const bool topology_changed = m_input.options.pipeline_v2 ||
m_result.its.indices.size() != m_input.base_mesh.indices.size();
// Carry the volume's existing paint - the model's own colours above all - across that rebuild.
// Without it a bake left the mmu_segmentation_facets bitstream pointing at a triangle set that
// no longer existed, which is what scrambled the colours of an imported coloured model even
// with colour switched off on every layer and nothing in the bake that was meant to touch them.
//
// transfer_facet_paint() rather than ModelVolume::restore_painting(): the latter's remap wants
// a target triangle overlapping the source paint to within 0.01 mm, which the baked surface is
// nowhere near - it has moved by the relief depth. Nothing to do when the topology is intact:
// triangle i is still triangle i, so the paint is already exactly right and carrying it at all
// could only lose detail.
std::array<TriangleSelector::TriangleSplittingData, 4> before;
if (topology_changed) {
before[0] = volume->supported_facets.get_data();
before[1] = volume->seam_facets.get_data();
before[2] = volume->mmu_segmentation_facets.get_data();
before[3] = volume->fuzzy_skin_facets.get_data();
}
volume->set_mesh(std::move(m_result)); volume->set_mesh(std::move(m_result));
volume->set_new_unique_id(); volume->set_new_unique_id();
volume->calculate_convex_hull(); volume->calculate_convex_hull();
if (topology_changed) {
const std::array<FacetsAnnotation *, 4> target{ &volume->supported_facets, &volume->seam_facets,
&volume->mmu_segmentation_facets,
&volume->fuzzy_skin_facets };
for (size_t ch = 0; ch < target.size(); ++ch) {
const std::vector<uint8_t> state =
transfer_facet_paint(m_input.base_mesh, before[ch], volume->mesh().its);
// Nothing was painted in this channel, or there is nothing to paint it onto: either
// way the annotation must not keep describing the mesh that is gone.
if (state.empty()) {
target[ch]->reset();
continue;
}
TriangleSelector selector(volume->mesh());
bool any = false;
for (size_t i = 0; i < state.size(); ++i)
if (state[i] != 0) {
selector.set_facet(int(i), EnforcerBlockerType(state[i]));
any = true;
}
if (any)
target[ch]->set(selector);
else
target[ch]->reset();
}
}
// Colour lands in mmu_segmentation_facets, merged *over* whatever is already painted there // Colour lands in mmu_segmentation_facets, merged *over* whatever is already painted there
// rather than replacing it: a triangle the texture does not colour keeps its existing filament, // rather than replacing it: a triangle the texture does not colour keeps its existing filament,
// and one the user never painted at all stays at NONE, which already means "the volume's own // and one the user never painted at all stays at NONE, which already means "the volume's own
// filament". That is what confines the effect to the painted area without having to invent a // filament". That is what confines the effect to the painted area without having to invent a
// colour for everything outside it. What was already painted is whatever the transfer above // colour for everything outside it. Safe to index straight onto the new mesh - the bake is
// left in the annotation, so this bake's colour lands on top of it rather than instead of it. // topology-preserving, so triangle i is still triangle i.
if (!m_triangle_color.empty() && m_triangle_color.size() == volume->mesh().its.indices.size()) { if (!m_triangle_color.empty() && m_triangle_color.size() == volume->mesh().its.indices.size()) {
TriangleSelector selector(volume->mesh()); TriangleSelector selector(volume->mesh());
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data(); const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
@@ -217,6 +159,7 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
// pipeline, which rebuilds and then simplifies the mesh. A mask left behind against the old // pipeline, which rebuilds and then simplifies the mesh. A mask left behind against the old
// topology is exactly what makes the gizmo reload an empty selector over a non-empty mask and // topology is exactly what makes the gizmo reload an empty selector over a non-empty mask and
// then erase it on the next flush - see GLGizmoTextureDisplacement::update_from_model_object(). // then erase it on the next flush - see GLGizmoTextureDisplacement::update_from_model_object().
const bool topology_changed = m_input.base_mesh.indices.size() != volume->mesh().its.indices.size();
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) { for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
const auto it = std::find_if(m_input.layers.begin(), m_input.layers.end(), const auto it = std::find_if(m_input.layers.begin(), m_input.layers.end(),
[slot](const TextureDisplacementLayer &l) { return l.slot == slot; }); [slot](const TextureDisplacementLayer &l) { return l.slot == slot; });
@@ -224,14 +167,6 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
volume->texture_displacement_facet(slot).reset(); volume->texture_displacement_facet(slot).reset();
} }
// The model's own UV channel. Resampled onto a rebuilt mesh from the surface the bake started
// from: a displaced vertex came from its foot point on that surface, which is the position its
// uv belongs to - so no distance limit, the relief depth is exactly how far it has moved.
if (!m_input.model_uv.empty())
volume->texture_uv = topology_changed ? transfer_uv_channel(m_input.base_mesh, m_input.model_uv,
volume->mesh().its) :
m_input.model_uv;
ModelObject *object = volume->get_object(); ModelObject *object = volume->get_object();
if (object == nullptr) if (object == nullptr)
return; return;
@@ -284,8 +219,6 @@ void queue_texture_displacement_bake(const ModelVolume &volume, const TextureCol
input.take_snapshot = take_snapshot; input.take_snapshot = take_snapshot;
input.volume_id = volume.id(); input.volume_id = volume.id();
input.base_mesh = volume.mesh().its; input.base_mesh = volume.mesh().its;
if (volume.has_texture_uv())
input.model_uv = volume.texture_uv;
input.layers = volume.texture_displacement_layers; input.layers = volume.texture_displacement_layers;
input.options = volume.texture_displacement_options; input.options = volume.texture_displacement_options;
input.volume_to_world = texture_displacement_volume_to_world(volume); input.volume_to_world = texture_displacement_volume_to_world(volume);
@@ -34,9 +34,6 @@ struct TextureDisplacementBakeInput
// writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU // writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU
// paint gizmo writes - alongside the displaced geometry. // paint gizmo writes - alongside the displaced geometry.
TextureColorSettings color; TextureColorSettings color;
// The volume's own UV channel (ModelVolume::texture_uv), empty when it has none: what a ModelUV
// layer maps through, and what the commit puts back on the baked mesh.
std::vector<Vec2f> model_uv;
// Whether this job pushes its own undo step when it commits. False when the caller has already // Whether this job pushes its own undo step when it commits. False when the caller has already
// taken one that is meant to cover the displacement as well - Standard mode's Bake, which remeshes // taken one that is meant to cover the displacement as well - Standard mode's Bake, which remeshes
// and subdivides first and has to undo as a single action. // and subdivides first and has to undo as a single action.
@@ -17,7 +17,6 @@
#include "slic3r/GUI/Jobs/Worker.hpp" #include "slic3r/GUI/Jobs/Worker.hpp"
#include <memory> #include <memory>
#include "libslic3r/ObjectID.hpp" #include "libslic3r/ObjectID.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/TriangleMesh.hpp" #include "libslic3r/TriangleMesh.hpp"
namespace Slic3r::GUI { namespace Slic3r::GUI {
@@ -53,14 +52,13 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
indexed_triangle_set mesh = m_input.base_mesh; indexed_triangle_set mesh = m_input.base_mesh;
TextureDisplacementFacetsData masks = m_input.facets_data; TextureDisplacementFacetsData masks = m_input.facets_data;
std::vector<Vec2f> uv = m_input.model_uv;
// The default pipeline can only move vertices the mesh already has, so its recipe starts with the // The default pipeline can only move vertices the mesh already has, so its recipe starts with the
// preparation - which is where "first it remeshes, then it refines" actually happens. The // preparation - which is where "first it remeshes, then it refines" actually happens. The
// experimental pipeline refines as part of the bake and has nothing to prepare. // experimental pipeline refines as part of the bake and has nothing to prepare.
if (m_input.run_prepare && !m_input.options.pipeline_v2) { if (m_input.run_prepare && !m_input.options.pipeline_v2) {
const TextureDisplacementPrepareResult prepared = const TextureDisplacementPrepareResult prepared =
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, uv, m_input.prepare_params, GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, m_input.prepare_params,
m_input.color, m_input.color,
// Preparation is roughly half the run; the bake // Preparation is roughly half the run; the bake
// takes the progress bar from there. // takes the progress bar from there.
@@ -79,14 +77,12 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
if (!prepared.mesh.indices.empty()) { if (!prepared.mesh.indices.empty()) {
mesh = prepared.mesh; mesh = prepared.mesh;
masks = prepared.masks; masks = prepared.masks;
uv = prepared.uv; // resampled onto the prepared mesh, as the prepare commit stores it
} }
} }
build_texture_displacement(mesh, m_input.layers, masks, m_input.options, build_texture_displacement(mesh, m_input.layers, masks, m_input.options,
[&report](int pct) { return report(50 + pct / 2); }, [&report](int pct) { return report(50 + pct / 2); },
/* color */ nullptr, m_input.volume_to_world, &recorder, /* stats */ nullptr, /* color */ nullptr, m_input.volume_to_world, &recorder);
uv.empty() ? nullptr : &uv);
m_stages = recorder.take(); m_stages = recorder.take();
ctl.update_status(100, status); // always finish at 100: this is what closes the notification ctl.update_status(100, status); // always finish at 100: this is what closes the notification
@@ -119,8 +115,6 @@ void queue_texture_displacement_debug(const ModelVolume &volume, const TextureCo
input.check_topology = check_topology; input.check_topology = check_topology;
input.color = color; input.color = color;
input.volume_to_world = texture_displacement_volume_to_world(volume); input.volume_to_world = texture_displacement_volume_to_world(volume);
if (volume.has_texture_uv())
input.model_uv = volume.texture_uv;
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data(); input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
@@ -47,9 +47,6 @@ struct TextureDisplacementDebugInput
// Only the quantizer is used, and only by the refinement's colour criterion - the debug run // Only the quantizer is used, and only by the refinement's colour criterion - the debug run
// produces no colour of its own. // produces no colour of its own.
TextureColorSettings color; TextureColorSettings color;
// The volume's own UV channel (ModelVolume::texture_uv), so the recipe captured here is the one a
// ModelUV layer actually bakes through. Empty when the volume has none.
std::vector<Vec2f> model_uv;
// The edge scan behind the open / non-manifold counts is a sort over every half-edge, which on a // The edge scan behind the open / non-manifold counts is a sort over every half-edge, which on a
// multi-million triangle stage costs more than the stage did. On by default because a stage that // multi-million triangle stage costs more than the stage did. On by default because a stage that
@@ -46,7 +46,7 @@ void TextureDisplacementPrepareJob::process(Ctl &ctl)
// idle loop. // idle loop.
int last_reported = 1; int last_reported = 1;
m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers, m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers,
m_input.model_uv, m_input.params, m_input.color, m_input.params, m_input.color,
[&ctl, &status, &last_reported](int percent) { [&ctl, &status, &last_reported](int percent) {
if (ctl.was_canceled()) if (ctl.was_canceled())
return false; return false;
@@ -106,10 +106,6 @@ void TextureDisplacementPrepareJob::finalize(bool canceled, std::exception_ptr &
volume->restore_painting(saved_painting); volume->restore_painting(saved_painting);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
volume->texture_displacement_facet(i).set_data(std::move(m_result.masks[size_t(i)])); volume->texture_displacement_facet(i).set_data(std::move(m_result.masks[size_t(i)]));
// The model's own UV channel, resampled onto the new mesh by the run. Also put back after
// restore_painting(), which resets it along with the extra facets for the same reason: it is
// indexed by triangle and the triangles have changed.
volume->texture_uv = std::move(m_result.uv);
if (ObjectList *obj_list = wxGetApp().obj_list()) { if (ObjectList *obj_list = wxGetApp().obj_list()) {
const ModelObjectPtrs &objs = plater->model().objects; const ModelObjectPtrs &objs = plater->model().objects;
@@ -9,7 +9,6 @@
#include "Job.hpp" #include "Job.hpp"
#include "libslic3r/ObjectID.hpp" #include "libslic3r/ObjectID.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/TextureDisplacement.hpp" #include "libslic3r/TextureDisplacement.hpp"
namespace Slic3r { namespace Slic3r {
@@ -48,10 +47,6 @@ struct TextureDisplacementPrepareInput
// skips the colour criterion entirely. Only the *quantizer* is used here: refinement follows // skips the colour criterion entirely. Only the *quantizer* is used here: refinement follows
// perceived colour, never the interleaving that realises a mix. // perceived colour, never the interleaving that realises a mix.
TextureColorSettings color; TextureColorSettings color;
// The volume's own UV channel (ModelVolume::texture_uv), empty when it has none. prepare_mesh()
// resamples it onto the mesh it produces, and the commit puts it back - like the masks, it is
// indexed by triangle and so cannot survive the stages on its own.
std::vector<Vec2f> model_uv;
// The undo step the commit opens. Standard mode's Bake names it after the bake, because the // The undo step the commit opens. Standard mode's Bake names it after the bake, because the
// displacement job that follows commits into this same step rather than pushing its own. // displacement job that follows commits into this same step rather than pushing its own.
std::string snapshot_name; std::string snapshot_name;
@@ -48,8 +48,7 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
(!m_current_generation || (!m_current_generation ||
m_current_generation->load() == m_generation); m_current_generation->load() == m_generation);
}, },
color, m_input.volume_to_world, nullptr, nullptr, color, m_input.volume_to_world);
m_input.model_uv.empty() ? nullptr : &m_input.model_uv);
} }
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr) void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
@@ -29,9 +29,6 @@ struct TextureDisplacementPreviewInput
// Empty unless a layer is colouring, in which case the preview reports the palette entry per // Empty unless a layer is colouring, in which case the preview reports the palette entry per
// triangle alongside the mesh, so the Normal view shows the colours the bake will produce. // triangle alongside the mesh, so the Normal view shows the colours the bake will produce.
TextureColorSettings color; TextureColorSettings color;
// The volume's own UV channel (ModelVolume::texture_uv), so a ModelUV layer previews through the
// same layout it will bake through. Empty when the volume has none.
std::vector<Vec2f> model_uv;
}; };
// A preview result: the displaced mesh, and - when the input carried a palette - one palette entry per // A preview result: the displaced mesh, and - when the input carried a palette - one palette entry per
+6
View File
@@ -0,0 +1,6 @@
namespace Slic3r {
namespace GUI {
} // namespace GUI
} // namespace Slic3r
+11
View File
@@ -0,0 +1,11 @@
#ifndef slic3r_MultiTaskModel_hpp_
#define slic3r_MultiTaskModel_hpp_
namespace Slic3r {
namespace GUI {
} // namespace GUI
} // namespace Slic3r
#endif
+232
View File
@@ -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
+36
View File
@@ -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
+3 -2
View File
@@ -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.
+2 -1
View File
@@ -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);
+2 -2
View File
@@ -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()));
+2 -1
View File
@@ -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; }
View File
View File
+59 -4
View File
@@ -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));
+5 -1
View File
@@ -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();
+3 -2
View File
@@ -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;
+2 -1
View File
@@ -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);
+15
View File
@@ -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
+1
View File
@@ -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);
+9
View File
@@ -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
+19
View File
@@ -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_
+3 -2
View File
@@ -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(); });
+2 -1
View File
@@ -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))
{ {
+13 -178
View File
@@ -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;
@@ -2346,353 +2174,3 @@ TEST_CASE("whether a layer's texture has colour agrees with its decode", "[Textu
CHECK_FALSE(height_texture_has_color(TextureDisplacementLayer{})); CHECK_FALSE(height_texture_has_color(TextureDisplacementLayer{}));
} }
// ---------------------------------------------------------------------------------------------
// The model's own UV channel, and the Override blend mode
// ---------------------------------------------------------------------------------------------
// A grayscale image that is black in its left half and white in its right half, so where a sample
// lands along u is readable straight off the height it comes back with.
static std::shared_ptr<std::vector<unsigned char>> make_left_right_split_png(size_t w = 16, size_t h = 16)
{
std::vector<uint8_t> pixels(w * h);
for (size_t y = 0; y < h; ++y)
for (size_t x = 0; x < w; ++x)
pixels[y * w + x] = (x < w / 2) ? 0 : 255;
const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path()
/ boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png");
REQUIRE(Slic3r::png::write_gray_to_file(tmp_path.string(), w, h, pixels));
std::vector<unsigned char> bytes;
{
std::ifstream ifs(tmp_path.string(), std::ios::binary);
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
boost::system::error_code ec;
boost::filesystem::remove(tmp_path, ec);
REQUIRE_FALSE(bytes.empty());
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
}
// Where a sheet coordinate lands in uv: across [UV_LO, UV_LO + UV_SPAN] rather than the full [0, 1].
// The inset keeps the tests off the exact edge of an untiled placement, where there is deliberately no
// texture at all rather than its border texel (see DecodedHeightTexture::sample()) - a real model's
// channel does reach 1.0, and a vertex sitting exactly there displaces by nothing on purpose.
static constexpr float UV_LO = 0.1f;
static constexpr float UV_SPAN = 0.8f;
static float sheet_uv_at(float coord, float size, bool mirror = false)
{
const float t = coord / size;
return UV_LO + UV_SPAN * (mirror ? 1.f - t : t);
}
// One UV per triangle corner: u follows x, v follows y. `mirror_u` mirrors u, which is what the tests
// use to show the bake is really reading the channel rather than projecting the position.
static std::vector<Vec2f> sheet_uv_channel(const indexed_triangle_set &sheet, float size, bool mirror_u = false)
{
std::vector<Vec2f> uv(sheet.indices.size() * 3);
for (size_t f = 0; f < sheet.indices.size(); ++f)
for (int k = 0; k < 3; ++k) {
const Vec3f &p = sheet.vertices[size_t(sheet.indices[f][k])];
uv[f * 3 + size_t(k)] = Vec2f(sheet_uv_at(p.x(), size, mirror_u), sheet_uv_at(p.y(), size));
}
return uv;
}
TEST_CASE("Override blend replaces the displacement under it instead of combining with it",
"[TextureDisplacement]")
{
CHECK_THAT(blend_displacement(3.f, 1.f, TextureBlendMode::Override), WithinAbs(1.f, 1e-6f));
// Nothing underneath yet is still just the value, so an Override layer at the bottom of the
// stack behaves exactly like the Add the bake forces there.
CHECK_THAT(blend_displacement(0.f, 1.f, TextureBlendMode::Override), WithinAbs(1.f, 1e-6f));
// The sign carries, and a zero sample really does flatten what is under it - that is the point.
CHECK_THAT(blend_displacement(3.f, -1.f, TextureBlendMode::Override), WithinAbs(-1.f, 1e-6f));
CHECK_THAT(blend_displacement(3.f, 0.f, TextureBlendMode::Override), WithinAbs(0.f, 1e-6f));
}
TEST_CASE("an Override layer leaves only its own relief where two layers overlap", "[TextureDisplacement]")
{
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
TextureDisplacementLayer base;
base.slot = 0;
base.depth_mm = 2.0f;
base.tiling_scale = 5.0f;
base.image_data = make_flat_gray_png(255); // full depth everywhere
TextureDisplacementLayer second = base;
second.slot = 1;
second.depth_mm = 0.5f;
second.blend_mode = TextureBlendMode::Override;
TextureDisplacementFacetsData facets{};
facets[0] = paint_whole_mesh(cube);
facets[1] = facets[0];
const indexed_triangle_set overridden = build_texture_displacement(cube, {base, second}, facets, classic_options());
REQUIRE(overridden.vertices.size() == cube.vertices.size());
// Add would have piled the two up to 2.5 mm; Override keeps the upper layer's 0.5 mm alone.
TextureDisplacementLayer added = second;
added.blend_mode = TextureBlendMode::Add;
const indexed_triangle_set summed = build_texture_displacement(cube, {base, added}, facets, classic_options());
REQUIRE(summed.vertices.size() == cube.vertices.size());
for (size_t i = 0; i < cube.vertices.size(); ++i) {
if (!above_resting_plane(cube, i))
continue;
CHECK_THAT((overridden.vertices[i] - cube.vertices[i]).norm(), WithinAbs(0.5f, 1e-3f));
CHECK_THAT((summed.vertices[i] - cube.vertices[i]).norm(), WithinAbs(2.5f, 1e-3f));
}
}
TEST_CASE("the ModelUV mapping displaces through the model's own UV channel", "[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set sheet = make_flat_sheet(size, 1.f);
TextureDisplacementLayer layer;
layer.slot = 0;
layer.depth_mm = 1.0f;
layer.image_data = make_left_right_split_png();
layer.tile_enabled = false; // the layout is the placement; nothing should repeat
layer.projection_method = TextureProjectionMethod::ModelUV;
TextureDisplacementFacetsData facets{};
facets[0] = paint_whole_mesh(sheet);
const std::vector<Vec2f> uv = sheet_uv_channel(sheet, size);
const std::vector<Vec2f> flipped = sheet_uv_channel(sheet, size, /* mirror_u */ true);
const indexed_triangle_set out = build_texture_displacement(sheet, {layer}, facets, classic_options(), {}, nullptr,
Transform3d::Identity(), nullptr, nullptr, &uv);
REQUIRE(out.vertices.size() == sheet.vertices.size());
// The image is black for u < 0.5 and white above it, and the channel's u rises with x across the
// sheet, so it comes out flat on its left half and raised by the full depth on its right.
size_t low = 0, high = 0;
for (size_t i = 0; i < sheet.vertices.size(); ++i) {
const float x = sheet.vertices[i].x();
const float dz = out.vertices[i].z() - sheet.vertices[i].z();
if (x < 0.4f * size) {
CHECK_THAT(dz, WithinAbs(0.f, 1e-3f));
++low;
} else if (x > 0.6f * size) {
CHECK_THAT(dz, WithinAbs(layer.depth_mm, 1e-3f));
++high;
}
}
REQUIRE(low > 0);
REQUIRE(high > 0);
// Mirroring the channel mirrors the relief: the mapping really is the channel's, not a planar
// projection of the position (which would be unchanged by it).
const indexed_triangle_set mirrored = build_texture_displacement(sheet, {layer}, facets, classic_options(), {},
nullptr, Transform3d::Identity(), nullptr, nullptr,
&flipped);
REQUIRE(mirrored.vertices.size() == sheet.vertices.size());
for (size_t i = 0; i < sheet.vertices.size(); ++i) {
const float x = sheet.vertices[i].x();
if (x < 0.4f * size)
CHECK_THAT(mirrored.vertices[i].z() - sheet.vertices[i].z(), WithinAbs(layer.depth_mm, 1e-3f));
else if (x > 0.6f * size)
CHECK_THAT(mirrored.vertices[i].z() - sheet.vertices[i].z(), WithinAbs(0.f, 1e-3f));
}
}
TEST_CASE("a ModelUV layer falls back to Triplanar when the volume carries no UV channel",
"[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set sheet = make_flat_sheet(size, 2.f);
TextureDisplacementLayer layer;
layer.slot = 0;
layer.depth_mm = 1.0f;
layer.tiling_scale = 5.0f;
layer.image_data = make_flat_gray_png(255);
TextureDisplacementFacetsData facets{};
facets[0] = paint_whole_mesh(sheet);
layer.projection_method = TextureProjectionMethod::Triplanar;
const indexed_triangle_set as_triplanar = build_texture_displacement(sheet, {layer}, facets, classic_options());
layer.projection_method = TextureProjectionMethod::ModelUV;
const indexed_triangle_set no_channel = build_texture_displacement(sheet, {layer}, facets, classic_options());
REQUIRE(no_channel.vertices.size() == as_triplanar.vertices.size());
for (size_t i = 0; i < as_triplanar.vertices.size(); ++i)
CHECK_THAT((no_channel.vertices[i] - as_triplanar.vertices[i]).norm(), WithinAbs(0.f, 1e-5f));
}
TEST_CASE("a bake with colour off on every layer reports no colour at all", "[TextureDisplacement]")
{
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
TextureDisplacementLayer layer;
layer.slot = 0;
layer.depth_mm = 1.0f;
layer.tiling_scale = 5.0f;
layer.image_data = make_flat_rgb_png(200, 40, 10); // has colour to give...
layer.color_enabled = false; // ...but is not asked for it
TextureDisplacementFacetsData facets{};
facets[0] = paint_whole_mesh(cube);
std::vector<uint8_t> triangle_color;
TextureColorRequest request;
request.quantize = [](const Vec3f &) { return 0; }; // would colour everything, if asked
request.out_triangle = &triangle_color;
const indexed_triangle_set out = build_texture_displacement(cube, {layer}, facets, classic_options(), {}, &request);
REQUIRE_FALSE(out.vertices.empty());
// Every entry 0 ("this triangle takes no colour from the texture"), so the commit leaves whatever
// the model was already painted with exactly as it was.
for (const uint8_t entry : triangle_color)
CHECK(entry == 0);
}
TEST_CASE("transfer_uv_channel resamples a UV channel onto a subdivided copy of the same surface",
"[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set coarse = make_flat_sheet(size, 5.f);
const indexed_triangle_set fine = make_flat_sheet(size, 1.f);
const std::vector<Vec2f> uv = sheet_uv_channel(coarse, size);
const std::vector<Vec2f> moved = transfer_uv_channel(coarse, uv, fine);
REQUIRE(moved.size() == fine.indices.size() * 3);
// The channel is affine in position here (u = x/size, v = y/size), so a correct transfer
// reproduces it exactly at every corner of the finer mesh.
for (size_t f = 0; f < fine.indices.size(); ++f)
for (int k = 0; k < 3; ++k) {
const Vec3f &p = fine.vertices[size_t(fine.indices[f][k])];
CHECK_THAT(moved[f * 3 + size_t(k)].x(), WithinAbs(sheet_uv_at(p.x(), size), 1e-4f));
CHECK_THAT(moved[f * 3 + size_t(k)].y(), WithinAbs(sheet_uv_at(p.y(), size), 1e-4f));
}
// A mismatched channel is refused rather than read past its end.
CHECK(transfer_uv_channel(coarse, {Vec2f::Zero()}, fine).empty());
CHECK(transfer_uv_channel(coarse, uv, indexed_triangle_set{}).empty());
}
TEST_CASE("the model's per-corner UVs survive a brush split and collapse to one per vertex",
"[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set sheet = make_flat_sheet(size, 5.f);
const std::vector<Vec2f> uv = sheet_uv_channel(sheet, size);
// The whole sheet as a patch, with the source map the channel is read through.
const TriangleMesh tm(sheet);
TriangleSelector selector(tm);
for (int f = 0; f < int(sheet.indices.size()); ++f)
selector.set_facet(f, EnforcerBlockerType::ENFORCER);
std::vector<int> source;
const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER, &source);
REQUIRE(patch.indices.size() == source.size());
const std::vector<Vec2f> corner = compute_model_corner_uvs(sheet, uv, patch, source);
REQUIRE(corner.size() == patch.indices.size() * 3);
// An unsplit patch triangle *is* its source triangle, so its corners reproduce the channel.
for (size_t f = 0; f < patch.indices.size(); ++f)
for (int k = 0; k < 3; ++k) {
const Vec3f &p = patch.vertices[size_t(patch.indices[f][k])];
CHECK_THAT(corner[f * 3 + size_t(k)].x(), WithinAbs(sheet_uv_at(p.x(), size), 1e-4f));
CHECK_THAT(corner[f * 3 + size_t(k)].y(), WithinAbs(sheet_uv_at(p.y(), size), 1e-4f));
}
const std::vector<Vec2f> per_vertex = collapse_corner_uvs(patch, corner);
REQUIRE(per_vertex.size() == patch.vertices.size());
for (size_t i = 0; i < patch.vertices.size(); ++i) {
CHECK_THAT(per_vertex[i].x(), WithinAbs(sheet_uv_at(patch.vertices[i].x(), size), 1e-4f));
CHECK_THAT(per_vertex[i].y(), WithinAbs(sheet_uv_at(patch.vertices[i].y(), size), 1e-4f));
}
// Mismatched inputs report nothing rather than guessing.
CHECK(compute_model_corner_uvs(sheet, {Vec2f::Zero()}, patch, source).empty());
CHECK(compute_model_corner_uvs(sheet, uv, patch, {}).empty());
CHECK(collapse_corner_uvs(patch, {Vec2f::Zero()}).empty());
}
TEST_CASE("facet paint is carried onto a rebuilt mesh by each facet's foot point", "[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set coarse = make_flat_sheet(size, 2.5f);
// Two filaments, split down the middle - the pattern the transfer has to reproduce.
const TriangleMesh coarse_mesh(coarse);
TriangleSelector painter(coarse_mesh);
for (size_t t = 0; t < coarse.indices.size(); ++t) {
const stl_triangle_vertex_indices &tri = coarse.indices[t];
const float x = (coarse.vertices[size_t(tri[0])].x() + coarse.vertices[size_t(tri[1])].x() +
coarse.vertices[size_t(tri[2])].x()) / 3.f;
painter.set_facet(int(t), x < 0.5f * size ? EnforcerBlockerType::Extruder1 : EnforcerBlockerType::Extruder2);
}
const TriangleSelector::TriangleSplittingData paint = painter.serialize();
// A finer mesh, lifted clear of the original surface the way a bake's relief lifts it. This is
// what ModelVolume::restore_painting()'s 0.01 mm overlap remap cannot follow.
indexed_triangle_set lifted = make_flat_sheet(size, 1.f);
for (Vec3f &v : lifted.vertices)
v.z() += 0.5f;
const std::vector<uint8_t> state = transfer_facet_paint(coarse, paint, lifted);
REQUIRE(state.size() == lifted.indices.size());
size_t left = 0, right = 0;
for (size_t t = 0; t < lifted.indices.size(); ++t) {
const stl_triangle_vertex_indices &tri = lifted.indices[t];
const float x = (lifted.vertices[size_t(tri[0])].x() + lifted.vertices[size_t(tri[1])].x() +
lifted.vertices[size_t(tri[2])].x()) / 3.f;
// Only away from the split: a facet straddling it may legitimately land on either side.
if (x < 0.4f * size) {
CHECK(state[t] == uint8_t(EnforcerBlockerType::Extruder1));
++left;
} else if (x > 0.6f * size) {
CHECK(state[t] == uint8_t(EnforcerBlockerType::Extruder2));
++right;
}
}
REQUIRE(left > 0);
REQUIRE(right > 0);
// An unpainted source has nothing to carry, which is not the same as carrying NONE everywhere:
// the caller has to tell those apart to decide between resetting the annotation and writing one.
CHECK(transfer_facet_paint(coarse, TriangleSelector::TriangleSplittingData{}, lifted).empty());
CHECK(transfer_facet_paint(coarse, paint, indexed_triangle_set{}).empty());
}
TEST_CASE("an unpainted part of the source carries no paint onto the rebuilt mesh", "[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set coarse = make_flat_sheet(size, 2.5f);
// Only the left half is painted at all; the right half must come back NONE rather than borrowing
// the nearest painted colour from across the model.
const TriangleMesh coarse_mesh(coarse);
TriangleSelector painter(coarse_mesh);
for (size_t t = 0; t < coarse.indices.size(); ++t) {
const stl_triangle_vertex_indices &tri = coarse.indices[t];
const float x = (coarse.vertices[size_t(tri[0])].x() + coarse.vertices[size_t(tri[1])].x() +
coarse.vertices[size_t(tri[2])].x()) / 3.f;
if (x < 0.5f * size)
painter.set_facet(int(t), EnforcerBlockerType::Extruder3);
}
const std::vector<uint8_t> state = transfer_facet_paint(coarse, painter.serialize(), coarse);
REQUIRE(state.size() == coarse.indices.size());
for (size_t t = 0; t < coarse.indices.size(); ++t) {
const stl_triangle_vertex_indices &tri = coarse.indices[t];
const float x = (coarse.vertices[size_t(tri[0])].x() + coarse.vertices[size_t(tri[1])].x() +
coarse.vertices[size_t(tri[2])].x()) / 3.f;
if (x < 0.4f * size)
CHECK(state[t] == uint8_t(EnforcerBlockerType::Extruder3));
else if (x > 0.6f * size)
CHECK(state[t] == uint8_t(EnforcerBlockerType::NONE));
}
}