mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-19 21:51:06 +00:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f64beb4379 | ||
|
|
a2b1a2f3e7 | ||
|
|
0585836550 | ||
|
|
5a36120c58 | ||
|
|
69193d5681 | ||
|
|
cdb9bb57ae | ||
|
|
f0c59208e7 | ||
|
|
9f6a344162 | ||
|
|
c5cfe40a1f | ||
|
|
e5b6cbf091 | ||
|
|
3b9b871ba6 | ||
|
|
cb2260f057 | ||
|
|
3fcbb2adf9 | ||
|
|
252bbbf9f5 | ||
|
|
4c2ae6d202 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: orca-translations
|
||||
description: Use when updating, filling, reviewing or fixing OrcaSlicer translations in localization/i18n — running scripts/run_gettext --full, translating empty or new msgids in the .po catalogs, AI translation passes and their "# AI Translated" / "# Needs human review" comments, plural forms, msgctxt homonyms, and keeping catalogs consistent with the localization glossary (OrcaSlicer_WIKI localization_glossary.tsv), including adding new glossary terms. Also use for msgfmt or run_gettext failures, placeholder or line-break mismatches, and reviewing a translation PR against the glossary.
|
||||
description: Use when updating, filling, reviewing or fixing OrcaSlicer translations in localization/i18n — running scripts/run_gettext --full, translating empty or new msgids in the .po catalogs, AI translation passes and their "# AI Translated" / "# Needs human review" comments, plural forms, msgctxt homonyms, and keeping catalogs consistent with the localization glossary (OrcaSlicer_WIKI developer_reference/glossary), including adding new glossary terms. Also use for msgfmt or run_gettext failures, placeholder or line-break mismatches, and reviewing a translation PR against the glossary.
|
||||
---
|
||||
|
||||
# OrcaSlicer translations
|
||||
@@ -9,7 +9,7 @@ The UI strings live in gettext catalogs: `localization/i18n/OrcaSlicer.pot` is t
|
||||
`localization/i18n/<lang>/OrcaSlicer_<lang>.po` holds each language. The `en` catalog is empty on
|
||||
purpose, so skip it. Recurring terms are fixed by the
|
||||
[localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md),
|
||||
whose source is `developer_reference/localization_glossary.tsv` in the **OrcaSlicer_WIKI** repository.
|
||||
whose source is the per-language files in `developer_reference/glossary/` (`terms.tsv`, `languages.txt` and one `<lang>.tsv`) in the **OrcaSlicer_WIKI** repository.
|
||||
A translation pass therefore touches two repositories: the catalogs here, and the glossary there.
|
||||
|
||||
All catalog editing goes through [`scripts/po_tool.py`](scripts/po_tool.py) (standard library only).
|
||||
@@ -153,7 +153,7 @@ Report:
|
||||
- anything left empty, and why;
|
||||
- the glossary rows added or filled.
|
||||
|
||||
The catalogs (and `OrcaSlicer.pot`) and the glossary (`localization_glossary.tsv` and `.md`) are
|
||||
The catalogs (and `OrcaSlicer.pot`) and the glossary (`developer_reference/glossary/*.tsv` and `localization_glossary.md`) are
|
||||
separate changes in separate repositories. The wiki change should land with, or before, the catalog
|
||||
change. Commit or open PRs only when asked.
|
||||
|
||||
@@ -210,7 +210,7 @@ with the translators, followed by the catalogs.
|
||||
When this skill and the checkouts disagree, the checkouts win:
|
||||
- `scripts/run_gettext.sh` / `.bat` for extraction and merging;
|
||||
- `localization/i18n/list.txt` for the scanned sources;
|
||||
- the wiki's `localization_guide.md` and `localization_glossary.md` / `.tsv` for the rules and terms;
|
||||
- the wiki's `localization_guide.md` and `localization_glossary.md` and `glossary/*.tsv` for the rules and terms;
|
||||
- `AGENTS.md` (Localization & translations) for the repository's conventions.
|
||||
|
||||
## Editing this skill
|
||||
|
||||
@@ -28,13 +28,14 @@ import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unicodedata
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[4]
|
||||
I18N = REPO / "localization" / "i18n"
|
||||
SKIP_LANGS = {"en"} # the English catalog is intentionally empty
|
||||
GLOSSARY_TSV = Path("developer_reference") / "localization_glossary.tsv"
|
||||
GLOSSARY_DIR = Path("developer_reference") / "glossary" # terms.tsv, languages.txt and one <lang>.tsv per language
|
||||
GLOSSARY_MD = Path("developer_reference") / "localization_glossary.md"
|
||||
MISSING = {"", "—"}
|
||||
RAW_URL = "https://raw.githubusercontent.com/OrcaSlicer/OrcaSlicer_WIKI/main/"
|
||||
@@ -252,30 +253,40 @@ def all_langs():
|
||||
|
||||
def find_wiki(arg):
|
||||
for cand in [arg, os.environ.get("ORCA_WIKI_DIR"), REPO.parent / "OrcaSlicer_WIKI", REPO.parent / "OrcaSlicer.wiki"]:
|
||||
if cand and (Path(cand) / GLOSSARY_TSV).exists():
|
||||
if cand and (Path(cand) / GLOSSARY_DIR / "terms.tsv").exists():
|
||||
return Path(cand)
|
||||
return None
|
||||
|
||||
|
||||
def _download(rel):
|
||||
cache = Path(tempfile.gettempdir()) / ("orca_glossary_" + rel.name)
|
||||
def _download(rel, required=True):
|
||||
cache = Path(tempfile.gettempdir()) / "orca_glossary" / rel
|
||||
if not cache.exists() or time.time() - cache.stat().st_mtime > 3600:
|
||||
with urllib.request.urlopen(RAW_URL + rel.as_posix(), timeout=30) as r:
|
||||
cache.write_bytes(r.read())
|
||||
try:
|
||||
with urllib.request.urlopen(RAW_URL + rel.as_posix(), timeout=30) as r:
|
||||
data = r.read()
|
||||
except urllib.error.HTTPError:
|
||||
if required:
|
||||
raise
|
||||
return None
|
||||
cache.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache.write_bytes(data)
|
||||
return cache
|
||||
|
||||
|
||||
class Glossary:
|
||||
def __init__(self, wiki_arg=None, quiet=False):
|
||||
"""The glossary: rows of [English, Description, <one translation per language loaded>]."""
|
||||
|
||||
def __init__(self, wiki_arg=None, quiet=False, langs=None):
|
||||
self.wiki = find_wiki(wiki_arg)
|
||||
self.langs = langs # only these languages' files are read (and downloaded); None means all
|
||||
if self.wiki:
|
||||
self.tsv, self.md = self.wiki / GLOSSARY_TSV, self.wiki / GLOSSARY_MD
|
||||
self.dir, self.md = self.wiki / GLOSSARY_DIR, self.wiki / GLOSSARY_MD
|
||||
if not quiet:
|
||||
self.report_clone_state()
|
||||
else:
|
||||
if not quiet:
|
||||
print("note: no OrcaSlicer_WIKI clone found; using the published glossary read-only", file=sys.stderr)
|
||||
self.tsv, self.md = _download(GLOSSARY_TSV), _download(GLOSSARY_MD)
|
||||
self.dir, self.md = _download(GLOSSARY_DIR / "terms.tsv").parent, _download(GLOSSARY_MD)
|
||||
self.load()
|
||||
|
||||
def report_clone_state(self):
|
||||
@@ -290,11 +301,23 @@ class Glossary:
|
||||
if behind and behind != "0":
|
||||
print(f"note: the wiki clone is {behind} commits behind origin/main (as of its last fetch)", file=sys.stderr)
|
||||
|
||||
def _read(self, name, required=True):
|
||||
path = self.dir / name if self.wiki else _download(GLOSSARY_DIR / name, required)
|
||||
return path.read_text(encoding="utf-8-sig").splitlines() if path and path.exists() else None
|
||||
|
||||
def load(self):
|
||||
rows = [l.split("\t") for l in self.tsv.read_text(encoding="utf-8-sig").splitlines() if l.strip()]
|
||||
self.header = rows[0]
|
||||
# "—" marks a language with no established rendering yet
|
||||
self.rows = [[c if c.strip() not in MISSING else "" for c in r] + [""] * (len(rows[0]) - len(r)) for r in rows[1:]]
|
||||
terms = [l.split(" ") for l in self._read("terms.tsv")[1:] if l.strip()]
|
||||
langs = [l.strip() for l in self._read("languages.txt") if l.strip()]
|
||||
langs = [l for l in langs if self.langs is None or l in self.langs]
|
||||
self.header = ["English", "Description"] + langs
|
||||
self.rows = [[r[0], r[1] if len(r) > 1 else ""] + [""] * len(langs) for r in terms]
|
||||
index = {r[0]: r for r in self.rows}
|
||||
for col, lang in enumerate(langs, start=2):
|
||||
for line in (self._read(f"{lang}.tsv", required=False) or [])[1:]:
|
||||
cells = line.split(" ")
|
||||
# "—" marks a language with no established rendering yet
|
||||
if len(cells) > 2 and cells[0] in index and cells[2].strip() not in MISSING:
|
||||
index[cells[0]][col] = cells[2].strip()
|
||||
self.matchers = []
|
||||
for row in self.rows:
|
||||
base = re.sub(r"\s*\([^)]*\)\s*$", "", row[0]).strip()
|
||||
@@ -494,7 +517,7 @@ def similar_entries(c, e, pool, cutoff=0.72):
|
||||
|
||||
def cmd_pending(a):
|
||||
c = Catalog(catalog_path(a.lang))
|
||||
g = Glossary(a.wiki)
|
||||
g = Glossary(a.wiki, langs=[a.lang])
|
||||
todo = [e for e in c.active() if needs_translation(e, c.nplurals, a.include_fuzzy)]
|
||||
batch = todo[a.offset:a.offset + a.limit] if a.limit else todo[a.offset:]
|
||||
pool = [(x.msgid, x.msgstrs()[0], x.obsolete) for x in c.entries
|
||||
@@ -533,7 +556,7 @@ def cmd_pending(a):
|
||||
|
||||
def cmd_apply(a):
|
||||
c = Catalog(catalog_path(a.lang))
|
||||
g = None if a.no_glossary else Glossary(a.wiki, quiet=True)
|
||||
g = None if a.no_glossary else Glossary(a.wiki, quiet=True, langs=[a.lang])
|
||||
data = json.loads(Path(a.input).read_text(encoding="utf-8"))
|
||||
items = data["entries"] if isinstance(data, dict) else data
|
||||
idx = c.index()
|
||||
@@ -588,7 +611,7 @@ def find_msgfmt():
|
||||
|
||||
|
||||
def cmd_check(a):
|
||||
g = None if a.no_glossary else Glossary(a.wiki, quiet=True)
|
||||
g = None if a.no_glossary else Glossary(a.wiki, quiet=True, langs=a.lang or None)
|
||||
msgfmt = find_msgfmt()
|
||||
status = 0
|
||||
for lang in a.lang or all_langs():
|
||||
@@ -646,7 +669,7 @@ def cmd_lookup(a):
|
||||
|
||||
|
||||
def cmd_glossary(a):
|
||||
g = Glossary(a.wiki)
|
||||
g = Glossary(a.wiki, langs=[a.lang])
|
||||
col = g.column(a.lang)
|
||||
if col is None:
|
||||
print(f"'{a.lang}' has no glossary column yet; add one with glossary-set --set {a.lang}=...")
|
||||
@@ -733,6 +756,17 @@ def cmd_candidates(a):
|
||||
print(f"{cnt:7} {langs:6} {text}{' [label]' if k in labels else ''} e.g. {ex[:70]}{'…' if len(ex) > 70 else ''}")
|
||||
|
||||
|
||||
def _edit_tsv(path, edit):
|
||||
"""Apply edit(lines) to a glossary TSV, keeping its BOM and line endings."""
|
||||
data = path.read_bytes()
|
||||
bom = data.startswith(b"\xef\xbb\xbf")
|
||||
text = data.decode("utf-8-sig")
|
||||
nl = "\r\n" if "\r\n" in text else "\n"
|
||||
lines = text.splitlines()
|
||||
edit(lines)
|
||||
path.write_bytes((b"\xef\xbb\xbf" if bom else b"") + (nl.join(lines) + nl).encode("utf-8"))
|
||||
|
||||
|
||||
def cmd_glossary_set(a):
|
||||
wiki = find_wiki(a.wiki)
|
||||
if not wiki:
|
||||
@@ -741,8 +775,21 @@ def cmd_glossary_set(a):
|
||||
f" {a.term}: " + ", ".join(a.set) + "\n"
|
||||
"Clone https://github.com/OrcaSlicer/OrcaSlicer_WIKI next to this repository, or pass --wiki.", file=sys.stderr)
|
||||
return 2
|
||||
tsv = wiki / GLOSSARY_TSV
|
||||
lock = tsv.with_suffix(".lock")
|
||||
gdir = wiki / GLOSSARY_DIR
|
||||
gen = wiki / "generate_glossary.py"
|
||||
if not gen.exists():
|
||||
sys.exit(f"{gen} not found")
|
||||
run_gen = lambda *args: subprocess.run([sys.executable, str(gen), *args], cwd=wiki, capture_output=True, text=True)
|
||||
langs = [l.strip() for l in (gdir / "languages.txt").read_text(encoding="utf-8-sig").splitlines() if l.strip()]
|
||||
values = {}
|
||||
for s in a.set:
|
||||
lang, _, val = s.partition("=")
|
||||
if lang not in langs:
|
||||
sys.exit(f"no glossary language '{lang}' (add it to {GLOSSARY_DIR.as_posix()}/languages.txt)")
|
||||
if "\t" in val or "\n" in val:
|
||||
sys.exit("values cannot contain tabs or line breaks")
|
||||
values[lang] = val.strip()
|
||||
lock = gdir / ".lock"
|
||||
for _ in range(100):
|
||||
try:
|
||||
fd = os.open(lock, os.O_CREAT | os.O_EXCL)
|
||||
@@ -752,50 +799,51 @@ def cmd_glossary_set(a):
|
||||
else:
|
||||
sys.exit(f"could not lock {lock}; remove it if no other run is active")
|
||||
try:
|
||||
data = tsv.read_bytes()
|
||||
bom = data.startswith(b"\xef\xbb\xbf")
|
||||
text = data.decode("utf-8-sig")
|
||||
nl = "\r\n" if "\r\n" in text else "\n"
|
||||
lines = text.splitlines()
|
||||
header = lines[0].split("\t")
|
||||
values = {}
|
||||
for s in a.set:
|
||||
lang, _, val = s.partition("=")
|
||||
if lang not in header:
|
||||
sys.exit(f"no glossary column '{lang}'")
|
||||
if "\t" in val or "\n" in val:
|
||||
sys.exit("values cannot contain tabs or line breaks")
|
||||
values[header.index(lang)] = val.strip()
|
||||
row_i = next((i for i, l in enumerate(lines[1:], 1) if l.split("\t")[0] == a.term), None)
|
||||
new = row_i is None
|
||||
if new:
|
||||
if not a.description:
|
||||
sys.exit("a new term needs --description (one sentence saying what it is)")
|
||||
row = [a.term, a.description] + [""] * (len(header) - 2)
|
||||
lines.append("")
|
||||
row_i = len(lines) - 1
|
||||
else:
|
||||
row = lines[row_i].split("\t")
|
||||
row += [""] * (len(header) - len(row))
|
||||
if a.description:
|
||||
row[1] = a.description
|
||||
conflicts = [f"{header[i]}: '{row[i]}' -> '{v}'" for i, v in values.items() if row[i].strip() not in MISSING and row[i] != v]
|
||||
terms = (gdir / "terms.tsv").read_text(encoding="utf-8-sig").splitlines()
|
||||
new = not any(l.split("\t")[0] == a.term for l in terms[1:])
|
||||
if new and not a.description:
|
||||
sys.exit("a new term needs --description (one sentence saying what it is)")
|
||||
# an established translation is only replaced on request
|
||||
conflicts = []
|
||||
for lang, v in values.items():
|
||||
for line in (gdir / f"{lang}.tsv").read_text(encoding="utf-8-sig").splitlines()[1:]:
|
||||
cells = line.split("\t")
|
||||
if cells[0] == a.term and len(cells) > 2 and cells[2].strip() not in MISSING and cells[2] != v:
|
||||
conflicts.append(f"{lang}: '{cells[2]}' -> '{v}'")
|
||||
if conflicts and not a.replace:
|
||||
sys.exit("established translations differ (pass --replace, and update the catalogs to match):\n " + "\n ".join(conflicts))
|
||||
for i, v in values.items():
|
||||
row[i] = v
|
||||
lines[row_i] = "\t".join(row)
|
||||
tsv.write_bytes((b"\xef\xbb\xbf" if bom else b"") + (nl.join(lines) + nl).encode("utf-8"))
|
||||
|
||||
def edit_terms(lines):
|
||||
for i, l in enumerate(lines):
|
||||
if i and l.split("\t")[0] == a.term:
|
||||
if a.description:
|
||||
lines[i] = f"{a.term}\t{a.description}"
|
||||
return
|
||||
lines.append(f"{a.term}\t{a.description}")
|
||||
if new or a.description:
|
||||
_edit_tsv(gdir / "terms.tsv", edit_terms)
|
||||
r = run_gen("--sync") # copies the term and description into every language file
|
||||
if r.returncode:
|
||||
sys.exit((r.stdout + r.stderr).strip())
|
||||
|
||||
def setter(lang, v):
|
||||
def edit(lines):
|
||||
for i, l in enumerate(lines):
|
||||
cells = l.split("\t")
|
||||
if i and cells[0] == a.term:
|
||||
lines[i] = "\t".join(cells[:2] + [v])
|
||||
return
|
||||
sys.exit(f"{lang}.tsv has no row for '{a.term}'")
|
||||
return edit
|
||||
for lang, v in values.items():
|
||||
_edit_tsv(gdir / f"{lang}.tsv", setter(lang, v))
|
||||
finally:
|
||||
os.close(fd)
|
||||
os.unlink(lock)
|
||||
print(f"glossary: {'added' if new else 'updated'} '{a.term}'")
|
||||
gen = wiki / "generate_glossary.py"
|
||||
if gen.exists():
|
||||
r = subprocess.run([sys.executable, str(gen), "--update"], cwd=wiki, capture_output=True, text=True)
|
||||
print((r.stdout + r.stderr).strip())
|
||||
return r.returncode
|
||||
return 0
|
||||
r = run_gen("--update")
|
||||
print((r.stdout + r.stderr).strip())
|
||||
return r.returncode
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -19,6 +19,8 @@ on:
|
||||
- 'build_linux.sh'
|
||||
- 'build_win.bat'
|
||||
- 'scripts/test_build_win.ps1'
|
||||
- 'scripts/check_exception_throw_info.ps1'
|
||||
- 'scripts/test_check_exception_throw_info.ps1'
|
||||
- 'build_release_macos.sh'
|
||||
- 'scripts/build_preset_cache.*'
|
||||
- 'scripts/flatpak/**'
|
||||
@@ -41,6 +43,8 @@ on:
|
||||
- 'build_linux.sh'
|
||||
- 'build_win.bat'
|
||||
- 'scripts/test_build_win.ps1'
|
||||
- 'scripts/check_exception_throw_info.ps1'
|
||||
- 'scripts/test_check_exception_throw_info.ps1'
|
||||
- 'build_release_macos.sh'
|
||||
- 'scripts/build_preset_cache.*'
|
||||
- 'scripts/flatpak/**'
|
||||
@@ -84,6 +88,10 @@ jobs:
|
||||
shell: powershell
|
||||
run: .\scripts\test_build_win.ps1
|
||||
|
||||
- name: Run the exception throw info check tests
|
||||
shell: powershell
|
||||
run: .\scripts\test_check_exception_throw_info.ps1
|
||||
|
||||
build_linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
@@ -65,15 +65,6 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}"
|
||||
# clang-cl refuses a precompiled header from another cl.exe build and ccache
|
||||
# does not hash that build, so each one gets its own cache. The build number
|
||||
# is read from cl.exe itself; the toolset directory keeps its name across patches.
|
||||
if [ "${{ runner.os }}" = Windows ]; then
|
||||
vswhere='/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe'
|
||||
toolset=$(tr -d '\r\n' < "$("$vswhere" -latest -products '*' -find 'VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt' | tr -d '\r')")
|
||||
cl=$("$vswhere" -latest -products '*' -find 'VC\Tools\MSVC\'"$toolset"'\**\cl.exe' | tr -d '\r' | head -1)
|
||||
leg="$leg-vc$("$cl" 2>&1 | grep -o -E 'Version [0-9.]+' | cut -d' ' -f2)"
|
||||
fi
|
||||
echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV"
|
||||
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
|
||||
|
||||
@@ -552,6 +543,13 @@ jobs:
|
||||
name: OrcaSlicer_profile_validator_Windows_${{ env.ver }}
|
||||
path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
|
||||
|
||||
- name: Check exception throw info Win
|
||||
if: runner.os == 'Windows' && inputs.compiler == 'clang'
|
||||
working-directory: ${{ github.workspace }}
|
||||
# After the uploads, so a failure still leaves the build and its PDB to inspect, and before any deploy.
|
||||
shell: pwsh
|
||||
run: scripts\check_exception_throw_info.ps1 -BuildDir $env:BUILD_DIR
|
||||
|
||||
- name: Deploy Windows release portable
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
|
||||
@@ -91,7 +91,7 @@ See `.claude/skills/orca-translations/references/source-audit.md` for the full c
|
||||
### Terminology
|
||||
|
||||
- Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated.
|
||||
- If a term's established translation changes, update both the affected `.po` files and the glossary (`localization_glossary.tsv`, then regenerate) so they stay in sync.
|
||||
- If a term's established translation changes, update both the affected `.po` files and the glossary (`developer_reference/glossary/<lang>.tsv` in the wiki, then regenerate) so they stay in sync.
|
||||
- Translate the *meaning*, not the words. Check what the string actually controls before translating it — English reuses one word for different things. `Flow ratio` (multiplier), `Flow Rate` (throughput) and `Flow Dynamics` (pressure compensation) are three different terms; `extruder` may mean the toolhead, the feeder motor, or the nozzle depending on the string.
|
||||
- Reuse one template per recurring message shape (`Failed to connect to …`, `Are you sure you want to …?`), even where the English wording varies.
|
||||
|
||||
|
||||
@@ -321,6 +321,14 @@ if (IS_CROSS_COMPILE)
|
||||
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
endif ()
|
||||
|
||||
# With a precompiled header, clang-cl can omit an exception type's copy constructor from its throw info,
|
||||
# so copies of the exception share and double-free their data: https://github.com/llvm/llvm-project/issues/53486
|
||||
if (MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
message(STATUS "clang-cl: building without the precompiled header (llvm/llvm-project#53486)")
|
||||
set_property(CACHE SLIC3R_PCH PROPERTY HELPSTRING "Use precompiled headers (ignored with clang-cl, llvm/llvm-project#53486)")
|
||||
set(SLIC3R_PCH OFF)
|
||||
endif ()
|
||||
|
||||
# Print out the SLIC3R_* cache options
|
||||
get_cmake_property(_cache_vars CACHE_VARIABLES)
|
||||
list (SORT _cache_vars)
|
||||
|
||||
+7
-4
@@ -53,7 +53,7 @@ call :add_section "Build configuration"
|
||||
call :add_arg config string "" config "release, debug, relwithdebinfo or minsizerel (default: release)"
|
||||
call :add_arg target_arch string "" arch "x64 or arm64 (default: the host architecture)"
|
||||
call :add_arg slicer_asan bool a asan "Build the slicer with ASAN enabled"
|
||||
call :add_arg no_pch bool "" no-pch "Build without the precompiled header, implied by --cache"
|
||||
call :add_arg no_pch bool "" no-pch "Build without the precompiled header, implied by -l and --cache"
|
||||
|
||||
call :add_section "Toolchain"
|
||||
call :add_arg use_clang_cl bool l clang-cl "Use clang-cl as the compiler"
|
||||
@@ -388,6 +388,9 @@ if "%use_ninja%" == "ON" if "%use_msbuild%" == "ON" (
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM CMakeLists turns the precompiled header off for clang-cl (llvm/llvm-project#53486).
|
||||
if "%use_clang_cl%" == "ON" set "no_pch=ON"
|
||||
|
||||
set "generator=!vs_gen_%vs_version%!"
|
||||
if "%use_ninja%" == "ON" (
|
||||
set "generator=Ninja Multi-Config"
|
||||
@@ -543,7 +546,7 @@ REM real path rather than one glued onto the repository root.
|
||||
for %%p in ("!build_dir!") do set "build_full=%%~fp"
|
||||
echo Configuration: %build_type%, %arch%
|
||||
if not "%clang_exe%" == "" echo Compiler: %clang_exe%
|
||||
if "%no_pch%" == "ON" echo Precompiled header: off
|
||||
if "%build_slicer%" == "ON" if "%no_pch%" == "ON" echo Precompiled header: off
|
||||
if not "%cache_args%" == "" echo Compiler cache: %cache_exe%
|
||||
|
||||
set "SIG_FLAG="
|
||||
@@ -716,8 +719,8 @@ if "%build_slicer%" == "ON" (
|
||||
set "slicer_args=!slicer_args! -DSLIC3R_ASAN=ON"
|
||||
)
|
||||
|
||||
REM A later -DSLIC3R_PCH=ON in ORCA_SLICER_CMAKE_ARGS still wins, because
|
||||
REM CMake takes the last definition on the command line.
|
||||
REM Under cl, a later -DSLIC3R_PCH=ON in ORCA_SLICER_CMAKE_ARGS still wins,
|
||||
REM because CMake takes the last definition on the command line.
|
||||
if "%no_pch%" == "ON" (
|
||||
set "slicer_args=!slicer_args! -DSLIC3R_PCH=OFF"
|
||||
)
|
||||
|
||||
Vendored
+17
-7
@@ -16,6 +16,8 @@ else()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(_openssl_dir ${DESTDIR})
|
||||
|
||||
if(WIN32)
|
||||
set(_openssl_msvc_env CC=cl CXX=cl RC=rc CL=/FS)
|
||||
# OpenSSL's perl Configure honors the CC environment variable, but the
|
||||
@@ -43,6 +45,14 @@ if(WIN32)
|
||||
else()
|
||||
if(APPLE)
|
||||
set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})
|
||||
# OPENSSLDIR is only where OpenSSL looks at runtime for cert.pem, certs/
|
||||
# and openssl.cnf; install_sw installs nothing there. Point it at the
|
||||
# system CA bundle, the one libcurl already uses here, so the bundled
|
||||
# Python's ssl module can verify HTTPS. Elsewhere it stays the prefix:
|
||||
# Windows Python reads the system certificate store, and on Linux, where
|
||||
# the bundle's location differs by distro, CurlGlobalInit points
|
||||
# SSL_CERT_FILE at it at startup.
|
||||
set(_openssl_dir /etc/ssl)
|
||||
else()
|
||||
# A static library that is embedded into a shared object must not export
|
||||
# its symbols. On Linux the running process also loads the system OpenSSL
|
||||
@@ -81,7 +91,7 @@ ExternalProject_Add(dep_OpenSSL
|
||||
# URL_HASH SHA256=8c776993154652d0bb393f506d850b811517c8bd8d24b1008aef57fbe55d3f31
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/OpenSSL
|
||||
CONFIGURE_COMMAND ${_conf_cmd} ${_cross_arch}
|
||||
"--openssldir=${DESTDIR}"
|
||||
"--openssldir=${_openssl_dir}"
|
||||
"--prefix=${DESTDIR}"
|
||||
# OpenSSL's linux-x86_64 target sets multilib=64, so it installs to
|
||||
# <prefix>/lib64 while every other dep uses <prefix>/lib. CPython's
|
||||
@@ -115,12 +125,12 @@ ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
|
||||
)
|
||||
|
||||
if (NOT WIN32 AND NOT APPLE)
|
||||
# OpenSSL's object rules do not depend on CFLAGS, so reconfiguring it (for
|
||||
# example to add -fvisibility=hidden) relinks the archives from stale
|
||||
# objects instead of recompiling them, and the change silently has no
|
||||
# effect. Drop the objects whenever this recipe changes so the next build
|
||||
# actually recompiles them.
|
||||
if (NOT WIN32)
|
||||
# OpenSSL's object rules do not depend on the configured flags or paths, so
|
||||
# reconfiguring it (a new compiler flag, a different --openssldir) relinks
|
||||
# the archives from stale objects instead of recompiling them, and the
|
||||
# change silently has no effect. Drop the objects whenever this recipe
|
||||
# changes so the next build actually recompiles them.
|
||||
ExternalProject_Get_Property(dep_OpenSSL SOURCE_DIR)
|
||||
ExternalProject_Add_Step(dep_OpenSSL clean_objects
|
||||
DEPENDEES configure
|
||||
|
||||
Vendored
+5
-4
@@ -365,10 +365,11 @@ if(TARGET dep_ZLIB)
|
||||
add_dependencies(dep_python3 dep_ZLIB)
|
||||
endif()
|
||||
|
||||
if (NOT WIN32 AND NOT APPLE)
|
||||
# CPython's Makefile rules for _ssl and _hashlib depend only on their own
|
||||
# sources, not on the OpenSSL archives, so a rebuilt OpenSSL does not make
|
||||
# them relink and they keep the previous symbols. On an incremental tree,
|
||||
if (NOT WIN32)
|
||||
# _ssl and _hashlib embed the static OpenSSL built in deps/ (Windows uses
|
||||
# CPython's prebuilt OpenSSL instead). Their Makefile rules depend only on
|
||||
# their own sources, not on the OpenSSL archives, so a rebuilt OpenSSL does
|
||||
# not make them relink and they keep the previous one. On an incremental tree,
|
||||
# drop the built modules and relink them against the current OpenSSL; a
|
||||
# fresh build is left alone (its PGO target builds them). "make" alone is a
|
||||
# no-op once PGO has run, so sharedmods is invoked explicitly.
|
||||
|
||||
Vendored
+9
-1
@@ -21,6 +21,14 @@ else ()
|
||||
set(_wx_edge "-DwxUSE_WEBVIEW_EDGE=OFF")
|
||||
endif ()
|
||||
|
||||
# With a precompiled header, clang-cl can omit an exception type's copy constructor from its throw info,
|
||||
# so copies of the exception share and double-free their data: https://github.com/llvm/llvm-project/issues/53486
|
||||
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
|
||||
set(_wx_precomp OFF)
|
||||
else ()
|
||||
set(_wx_precomp ON)
|
||||
endif ()
|
||||
|
||||
set(_wx_patch_command "")
|
||||
if (APPLE)
|
||||
set(_wx_patch_command
|
||||
@@ -39,7 +47,7 @@ orcaslicer_add_cmake_project(
|
||||
PATCH_COMMAND ${_wx_patch_command}
|
||||
DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG}
|
||||
CMAKE_ARGS
|
||||
-DwxBUILD_PRECOMP=ON
|
||||
-DwxBUILD_PRECOMP=${_wx_precomp}
|
||||
${_wx_toolkit}
|
||||
"-DCMAKE_DEBUG_POSTFIX:STRING=${_wx_debug_postfix}"
|
||||
-DwxBUILD_DEBUG_LEVEL=0
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Adaptive TPMS infill — High Level Design
|
||||
# Adaptive infill — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
`tpms_adaptive` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
|
||||
`adaptive_infill_mode` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
|
||||
patterns inside the object: the cells grow continuously from the surface
|
||||
towards the center of the object. `distance_warp`, `smooth_blend` and
|
||||
`stepped_shells` follow the distance to the nearest surface, including the top
|
||||
@@ -10,8 +10,8 @@ and bottom, like concentric shells; `lobes` follows the whole 3D shape towards
|
||||
the center of each lobe of the object; `normal_z`, `normal_y` and `normal_x`
|
||||
follow each section of the object normal to that axis, so the grading does not
|
||||
change along the axis, as suits a profile extruded along it.
|
||||
`sparse_infill_density` is the density at the surface, `tpms_interior_density`
|
||||
the density at the center, and `tpms_adaptive_gradient` picks how the density
|
||||
`sparse_infill_density` is the density at the surface, `adaptive_infill_interior_density`
|
||||
the density at the center, and `adaptive_infill_gradient` picks how the density
|
||||
goes from one to the other. Only internal sparse infill is graded; the Gyroid
|
||||
Z-buckling optimization does not apply to it.
|
||||
|
||||
@@ -22,9 +22,9 @@ split into shells or blended between densities.
|
||||
|
||||
## Radial field
|
||||
|
||||
`TpmsRadialField` gives every point of an object the center of its lobe and a
|
||||
`AdaptiveInfillField` gives every point of an object the center of its lobe and a
|
||||
radial coordinate: 0 at the center, 1 at the surface along the ray from the
|
||||
center. `PrintObject::prepare_tpms_radial_fields()` builds it in
|
||||
center. `PrintObject::prepare_adaptive_infill_fields()` builds it in
|
||||
`bridge_over_infill()`, next to the adaptive cubic octree, because the anchoring
|
||||
infill generated there has to match the printed infill. A field is built for
|
||||
every mode a region uses, and is shared by the regions using that mode: the
|
||||
@@ -202,7 +202,7 @@ built for them.
|
||||
|
||||
## Constraints
|
||||
|
||||
- With `tpms_adaptive` disabled, or for other patterns, the fill parameters are reset
|
||||
- With `adaptive_infill_mode` disabled, or for other patterns, the fill parameters are reset
|
||||
to their defaults, so they neither change the infill nor split fill batches.
|
||||
- `Layer::get_sparse_infill_max_void_area()` uses the sparser of the two
|
||||
densities, as the voids at the center are that large.
|
||||
@@ -0,0 +1,198 @@
|
||||
# Travel optimization — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
Inside an island, the infill paths of each region are chained into one print
|
||||
order by a greedy pass that joins the closest free path ends first. That pass
|
||||
leaves its longest joins for last, and they often cross the whole island. Two
|
||||
things shorten the travel between infill paths:
|
||||
|
||||
- the fill generators join their lines into as few paths as they can, since
|
||||
every separate path needs a travel to it;
|
||||
- the G-code export refines the greedy chain with a local search that shortens
|
||||
the straight-line travel between the paths.
|
||||
|
||||
The refinement covers every chain that `chain_and_reorder_extrusion_entities()`
|
||||
builds from a start point: the infill and ironing of `GCode::extrude_infill()`
|
||||
and the support of `GCode::extrude_support()`, as well as the order of objects
|
||||
and islands under `print_order` Default. It changes only the order and the
|
||||
direction of the paths, not the paths or the walls. Neither part has a setting.
|
||||
|
||||
## The chain
|
||||
|
||||
`ObjectByExtruder::Island::Region::append()` flattens every sortable fill
|
||||
collection into its paths, so the paths of all the fills of a region in one
|
||||
island are chained together. A collection that must keep its order (`no_sort`:
|
||||
monotonic, concentric, a forced fill order, the flow calibration) stays a single
|
||||
entry that cannot be reversed. Paths that must keep their direction cannot be
|
||||
reversed either: Grid's, since it alternates their direction between layers for
|
||||
strength, and those of a fill with a forced order. A loop starts and ends at the
|
||||
same point, so reversing it changes nothing.
|
||||
|
||||
`extrude_infill()` chains each region from the current nozzle position with
|
||||
`chain_and_reorder_extrusion_entities()`, and chains the paths inside a nested
|
||||
sortable collection the same way; `extrude_support()` chains the support paths
|
||||
of a layer so too, unless they keep their order. The overload without a start
|
||||
point, used to chain collections while slicing, keeps the greedy chain. The
|
||||
order the fill generators give their paths does not reach the G-code for a
|
||||
sortable fill, which is chained again.
|
||||
|
||||
## Connecting the lines
|
||||
|
||||
Most patterns cut their lines at the infill boundary, and `Fill::connect_infill()`
|
||||
joins pairs of line ends that are neighbors on a boundary contour by the arc of
|
||||
wall between them, shortest arcs first, as long as neither end is joined yet and
|
||||
the join closes no loop. An arc is joined when it is shorter than
|
||||
`infill_anchor_max`, or than two anchors and a line width if that is longer:
|
||||
two ends further apart each get an anchor toward the other, and where those
|
||||
would meet, the join prints about as much wall as one path instead of two.
|
||||
|
||||
Regular patterns leave many arcs of the same length, and the order among them
|
||||
decides the pairing. In an arbitrary order the pairs along one wall can fall out
|
||||
of step, such as lines 1–2 and 3–4, then 6–7 and 8–9, which leaves the end of
|
||||
line 5 loose, and every loose end starts or ends a separate path. So the arcs
|
||||
are also sorted in steps of half the line spacing and, within a step, along each
|
||||
contour forwards and backwards, which pairs the ends of a wall in one phase.
|
||||
Each order is replayed on the flags and a union-find of the lines, without
|
||||
building any path, and the one that joins the most lines is used; a tie keeps
|
||||
the shortest-first order.
|
||||
|
||||
The replay includes the last pass, which joins any two open ends that are
|
||||
neighbors on a contour. With multiline infill the main pass leaves the copies of
|
||||
one line to that pass, so that each copy reaches a wall of its own. Two
|
||||
neighboring groups cut at both walls can then be joined at either wall by arcs
|
||||
of the same length: at one, their open ends stay next to their own copies, which
|
||||
the last pass joins, so each line prints out and back; at the other, the copies
|
||||
end up in separate paths printed apart.
|
||||
|
||||
A line end left unpaired gets an anchor, a piece of the wall at most
|
||||
`infill_anchor` long. It runs toward the nearest open end of another path,
|
||||
looking up to 64 ends along the contour, since the next path usually starts
|
||||
there; without one it takes the side with more free wall. With multiline infill
|
||||
an anchor toward a copy of the same line stops short of the copy rather than
|
||||
printing over it. A copy is a neighboring end less than the group width
|
||||
(`multiline` spacings) across the line, at any wall angle, while the next group
|
||||
lies a whole line spacing away. Meeting the wall at an angle θ, the copy covers
|
||||
half a width over sin θ of the wall on each side of its end, so the anchor stops
|
||||
that far and half its own width before it, or is left out; the end still counts
|
||||
as anchored, so the last pass pairs the other ends as before.
|
||||
|
||||
Last, `Fill::fill_surface_extrusion()` joins the sparse infill lines whose ends
|
||||
are at most two line spacings apart with a straight piece. With the anchors off
|
||||
(`infill_anchor_max` 0) nothing above runs and this is the only joining; with
|
||||
them on, it takes what the steps above leave next to each other, such as two
|
||||
anchors that stop just short of each other or a short piece cut off by a corner.
|
||||
A gap that short leaves no room to print anything in between, and as a travel it
|
||||
is the one hop a reordering can trade for a longer travel that retracts.
|
||||
Multiline copies leave the most such ends: one spacing apart across the line,
|
||||
their ends where it meets a wall at an angle θ are one spacing over sin θ apart
|
||||
along it, so two spacings reach walls met at 30° and steeper.
|
||||
|
||||
Ends that meet are joined first, then the gaps between the leading ends of their
|
||||
lines, along each line's overall direction, then the rest, shortest first within
|
||||
each, without closing a loop or crossing a line or another join. Taking the
|
||||
leading ends first joins every pair of copies at the same side, which leaves
|
||||
their open ends next to each other; by length alone, neighboring pairs were
|
||||
joined at different walls and the travel between them crossed the island. Lines
|
||||
that cannot reverse, such as Grid's, only join the end of one to the start of the
|
||||
next, and fills that keep their order are left alone.
|
||||
|
||||
## The search
|
||||
|
||||
A chain of up to eight paths is found exactly, by dynamic programming over the
|
||||
subsets of paths: a state is the paths visited so far, the last one and its
|
||||
direction, about four thousand states for eight paths. The shared greedy
|
||||
chaining, `chain_segments_greedy_constrained_reversals_()`, does the same for two
|
||||
to four segments, which also orders up to four islands of a layer and the loops
|
||||
of a perimeter collection exactly. Either way the first path is no longer
|
||||
always the one nearest the start; with a free end, a path near the start can be
|
||||
worth leaving for last.
|
||||
|
||||
Objects (`print_order` Default) and the tour of islands across objects are
|
||||
visited in the same order on every layer, so `chain_points_with_postprocessing()`
|
||||
orders them as a closed tour rather than an open chain. Up to ten points the
|
||||
shortest tour is found exactly by the same dynamic programming with the first
|
||||
point fixed; with a start point the tour then begins at the point nearest it.
|
||||
Larger sets take the greedy chain and alternate 2-opt, or-opt and crossing
|
||||
removal; or-opt moves runs of up to three points next to one of the eight points
|
||||
nearest an end of the run, found on the same grid as the chain's candidates.
|
||||
|
||||
For longer chains, the search refines the greedy chain. On an island cut into many short pieces,
|
||||
such as infill without anchors, that chain can sweep away from the nozzle and
|
||||
come back for the rest with one long jump, which no local move below removes.
|
||||
So a nearest-next chain, going from the nozzle to the nearest free path end each
|
||||
time, is built too; when it is already shorter than the refined greedy chain, it
|
||||
is refined and used instead. Elsewhere it costs one grid query per path.
|
||||
|
||||
`improve_chain_by_local_search()` sees the chain as a sequence of segments with
|
||||
an entry and an exit point each. Its cost is the straight distance from the
|
||||
nozzle to the first entry plus the distance from every exit to the next entry;
|
||||
the end of the chain is free. It applies three kinds of moves:
|
||||
|
||||
- 2-opt reverses a run of segments, which flips each of them and replaces the
|
||||
two links at the ends of the run.
|
||||
- Or-opt moves a run of up to three segments between two others, flipped or
|
||||
not.
|
||||
- A run of any length that starts or ends next to the nozzle moves to the front.
|
||||
Local search can settle on a chain that starts with a block of paths far from
|
||||
the nozzle while the paths next to it sit in the middle, and moving that block
|
||||
takes more than three segments at once.
|
||||
|
||||
A move that would flip a segment that cannot be reversed is skipped, so such
|
||||
segments are only reordered.
|
||||
|
||||
A move is tried only if every link it creates joins an end point to one of the
|
||||
32 end points nearest to it, which covers every move on a chain of up to 17
|
||||
segments, as most islands are; with 8 candidates the search missed most of the
|
||||
gain on grid-like patterns such as Triangles. The candidates come from a uniform
|
||||
grid of about two end points per cell, searched in rings of cells around the
|
||||
query. `KDTreeIndirect` visits its subtrees in a fixed order rather than the
|
||||
nearer one first, so a query for 32 points walks most of the tree, and on long
|
||||
chains finding the candidates would cost more than the search itself.
|
||||
|
||||
The candidates of an end point are tried nearest first, and only while they are
|
||||
closer to it than the link the move would replace there. A 2-opt move that pays
|
||||
off creates a link shorter than the one it replaces at one of its two ends, so
|
||||
this misses no 2-opt move. An or-opt move is tried only while its new link is
|
||||
shorter than what taking the run out saves, which skips a few or-opt moves that
|
||||
would still pay off but cuts the search on long chains to a fraction.
|
||||
|
||||
The search works through a queue of segments and applies the first move that
|
||||
shortens the chain around the segment it examines. After a move, only the
|
||||
segments next to a changed link are queued again. A new link can also make room
|
||||
for a run elsewhere that no segment next to it would try, so when the queue
|
||||
empties after any move, every segment is queued once more, and the search ends
|
||||
when such a sweep finds no move.
|
||||
|
||||
Every move shortens the chain by more than `SCALED_EPSILON`, so the result is
|
||||
never longer than the greedy or the nearest-next chain and the search ends; a
|
||||
cap of 50 moves per segment only bounds its time. The search has no randomness
|
||||
and visits the segments in a fixed order, so the same input always gives the
|
||||
same G-code.
|
||||
|
||||
## Cost
|
||||
|
||||
Finding the candidates of an end point visits a few cells, and the nearest-next
|
||||
chain makes one such query per path, which only grows where the paths around the
|
||||
nozzle are used up and the chain jumps. Examining a segment tries at most a few
|
||||
hundred candidate links, usually only the nearest few, plus a scan of the chain
|
||||
for the move to the front when it is the first segment, and a move rewrites only
|
||||
the range of the chain it changes, so the search stays close to linear in the
|
||||
number of segments. Most islands hold a few to a few dozen paths; the cost shows
|
||||
on infill cut into thousands of pieces per island, such as dense infill with the
|
||||
anchors off. The straight joins only look at the line segments near a candidate
|
||||
join for crossings.
|
||||
|
||||
## Limitations
|
||||
|
||||
The search minimizes straight-line distance. It does not know which travels
|
||||
leave the infill area and retract under `reduce_infill_retraction`, nor the
|
||||
detours of `reduce_crossing_wall`, and a travel's time is not proportional to
|
||||
its length: a short travel is bound by acceleration, so its time grows about
|
||||
with the square root of its length. A chain that is shorter in total but trades
|
||||
very short travels for longer ones, or for ones past `retraction_minimum_travel`,
|
||||
can occasionally take a little longer to print.
|
||||
|
||||
The end of the chain is free: it is not steered toward the next island or the
|
||||
start of the next layer. The regions of an island are chained one after the
|
||||
other, in region order, each from where the previous one ended.
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-15 17:40+0800\n"
|
||||
"POT-Creation-Date: 2026-10-19 09:48-0300\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -1637,16 +1637,16 @@ msgstr ""
|
||||
msgid "Stroke of shape (%1%) contains unsupported: %2%."
|
||||
msgstr ""
|
||||
|
||||
msgid "Not valid state please report reproduction steps on github"
|
||||
msgid "Not valid state please report reproduction steps on GitHub"
|
||||
msgstr ""
|
||||
|
||||
msgid "No embossed file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing svg file in embossed shape"
|
||||
msgid "Missing SVG file in embossed shape"
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing data of svg file"
|
||||
msgid "Missing data of SVG file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Face the camera"
|
||||
@@ -4579,7 +4579,7 @@ msgstr ""
|
||||
msgid "Unknown C++ exception."
|
||||
msgstr ""
|
||||
|
||||
msgid "Processing G-Code from previous file…"
|
||||
msgid "Processing G-code from previous file…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Slicing complete"
|
||||
@@ -6049,7 +6049,7 @@ msgstr ""
|
||||
msgid "Size:"
|
||||
msgstr ""
|
||||
|
||||
#, possible-boost-format
|
||||
#, possible-c-format, possible-boost-format
|
||||
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
|
||||
msgstr ""
|
||||
|
||||
@@ -12318,7 +12318,7 @@ msgstr ""
|
||||
msgid "Left mouse"
|
||||
msgstr ""
|
||||
|
||||
msgid "Available anywhere in the window, even while typing in a text field."
|
||||
msgid "Available anywhere in the window."
|
||||
msgstr ""
|
||||
|
||||
msgid "Run favorite 1 to 10"
|
||||
@@ -12415,9 +12415,6 @@ msgstr ""
|
||||
msgid "The default %s is assigned to %s. Reassign it to %s?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Global shortcuts also apply while typing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A key that types a character cannot be a global shortcut."
|
||||
msgstr ""
|
||||
|
||||
@@ -15648,6 +15645,12 @@ msgstr ""
|
||||
msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
|
||||
msgstr ""
|
||||
|
||||
msgid "Outline tolerance"
|
||||
msgstr ""
|
||||
|
||||
msgid "Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the original outline. Larger values give fewer points, which helps firmware that stalls while parsing G-code with many objects. 0 keeps every point."
|
||||
msgstr ""
|
||||
|
||||
msgid "Verbose G-code"
|
||||
msgstr ""
|
||||
|
||||
@@ -20543,7 +20546,7 @@ msgid "test %s start..."
|
||||
msgstr ""
|
||||
|
||||
#, possible-c-format, possible-boost-format
|
||||
msgid "test %s ip resolved = %s"
|
||||
msgid "test %s IP resolved = %s"
|
||||
msgstr ""
|
||||
|
||||
#, possible-c-format, possible-boost-format
|
||||
@@ -26495,17 +26498,18 @@ msgstr ""
|
||||
msgid "Basic Colors"
|
||||
msgstr ""
|
||||
|
||||
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
msgid "Orca Palette"
|
||||
msgstr ""
|
||||
|
||||
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
#. TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
msgid "RAL Classic"
|
||||
msgstr ""
|
||||
|
||||
#. TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
msgid "Gradients"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "Color picker"
|
||||
msgid "Solid"
|
||||
msgstr ""
|
||||
|
||||
@@ -26880,7 +26884,7 @@ msgstr ""
|
||||
msgid "Medium violet red"
|
||||
msgstr ""
|
||||
|
||||
msgid "PaleV violet red"
|
||||
msgid "Pale violet red"
|
||||
msgstr ""
|
||||
|
||||
msgid "Deep pink"
|
||||
@@ -27042,7 +27046,7 @@ msgstr ""
|
||||
msgid "Shallow sky"
|
||||
msgstr ""
|
||||
|
||||
msgid "Electric bel"
|
||||
msgid "Electric blue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dolphin"
|
||||
@@ -27111,7 +27115,7 @@ msgstr ""
|
||||
msgid "Maize yellow"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dalmatian yellow"
|
||||
msgid "Daffodil yellow"
|
||||
msgstr ""
|
||||
|
||||
msgid "Brown beige"
|
||||
@@ -27372,7 +27376,7 @@ msgstr ""
|
||||
msgid "Turquoise blue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Capsanthin violet"
|
||||
msgid "Capri blue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ocean blue"
|
||||
@@ -27486,7 +27490,7 @@ msgstr ""
|
||||
msgid "Silver grey"
|
||||
msgstr ""
|
||||
|
||||
msgid "Olivestone"
|
||||
msgid "Olive grey"
|
||||
msgstr ""
|
||||
|
||||
msgid "Moss grey"
|
||||
@@ -28074,10 +28078,19 @@ msgstr ""
|
||||
msgid "Built-in"
|
||||
msgstr ""
|
||||
|
||||
msgid "My textures"
|
||||
msgstr ""
|
||||
|
||||
msgid "No textures found."
|
||||
msgstr ""
|
||||
|
||||
msgid "My textures"
|
||||
msgid "Texture libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Explore more texture libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Opens OrcaCloud in the Home page to browse for plugins."
|
||||
msgstr ""
|
||||
|
||||
msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer."
|
||||
@@ -30130,6 +30143,9 @@ msgstr ""
|
||||
msgid "Slicing Pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource Texture"
|
||||
msgstr ""
|
||||
|
||||
msgid "The plugin's configuration UI was empty. Showing the default editor."
|
||||
msgstr ""
|
||||
|
||||
@@ -30245,6 +30261,24 @@ msgstr ""
|
||||
msgid "Plugin directory does not contain a .py or .whl entry file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource texture directory is not set"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource texture directory could not be resolved"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource texture path is not a directory"
|
||||
msgstr ""
|
||||
|
||||
msgid "Plugin folder could not be resolved"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource texture directory is outside the plugin folder"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource texture directory could not be read"
|
||||
msgstr ""
|
||||
|
||||
#, possible-boost-format
|
||||
msgid "Slicing pipeline plugin '%1%' error: %2%"
|
||||
msgstr ""
|
||||
@@ -30601,7 +30635,7 @@ msgstr ""
|
||||
|
||||
#. TRN %1% is the application name.
|
||||
#, possible-boost-format
|
||||
msgid "Not a gcode file generated by %1%."
|
||||
msgid "Not a G-code file generated by %1%."
|
||||
msgstr ""
|
||||
|
||||
#. TRN %1% is a tag in the G-code file that must not be translated.
|
||||
@@ -30689,7 +30723,7 @@ msgstr ""
|
||||
msgid "Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%"
|
||||
msgstr ""
|
||||
|
||||
msgid "Post-processor can't find exported gcode file"
|
||||
msgid "Post-processor can't find exported G-code file"
|
||||
msgstr ""
|
||||
|
||||
#, possible-boost-format
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-15 17:40+0800\n"
|
||||
"POT-Creation-Date: 2026-10-19 09:48-0300\n"
|
||||
"PO-Revision-Date: 2026-06-17 15:44-0300\n"
|
||||
"Last-Translator: Alexandre Folle de Menezes\n"
|
||||
"Language-Team: \n"
|
||||
@@ -1633,16 +1633,16 @@ msgstr ""
|
||||
msgid "Stroke of shape (%1%) contains unsupported: %2%."
|
||||
msgstr ""
|
||||
|
||||
msgid "Not valid state please report reproduction steps on github"
|
||||
msgid "Not valid state please report reproduction steps on GitHub"
|
||||
msgstr ""
|
||||
|
||||
msgid "No embossed file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing svg file in embossed shape"
|
||||
msgid "Missing SVG file in embossed shape"
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing data of svg file"
|
||||
msgid "Missing data of SVG file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Face the camera"
|
||||
@@ -4575,7 +4575,7 @@ msgstr ""
|
||||
msgid "Unknown C++ exception."
|
||||
msgstr ""
|
||||
|
||||
msgid "Processing G-Code from previous file…"
|
||||
msgid "Processing G-code from previous file…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Slicing complete"
|
||||
@@ -6045,7 +6045,7 @@ msgstr ""
|
||||
msgid "Size:"
|
||||
msgstr ""
|
||||
|
||||
#, boost-format
|
||||
#, c-format, boost-format
|
||||
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
|
||||
msgstr ""
|
||||
|
||||
@@ -10653,6 +10653,9 @@ msgstr ""
|
||||
msgid "Send Failed"
|
||||
msgstr ""
|
||||
|
||||
msgid "No available printers"
|
||||
msgstr ""
|
||||
|
||||
msgid "The name length exceeds the limit."
|
||||
msgstr ""
|
||||
|
||||
@@ -12311,7 +12314,7 @@ msgstr ""
|
||||
msgid "Left mouse"
|
||||
msgstr ""
|
||||
|
||||
msgid "Available anywhere in the window, even while typing in a text field."
|
||||
msgid "Available anywhere in the window."
|
||||
msgstr ""
|
||||
|
||||
msgid "Run favorite 1 to 10"
|
||||
@@ -12408,9 +12411,6 @@ msgstr ""
|
||||
msgid "The default %s is assigned to %s. Reassign it to %s?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Global shortcuts also apply while typing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A key that types a character cannot be a global shortcut."
|
||||
msgstr ""
|
||||
|
||||
@@ -15641,6 +15641,12 @@ msgstr ""
|
||||
msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
|
||||
msgstr ""
|
||||
|
||||
msgid "Outline tolerance"
|
||||
msgstr ""
|
||||
|
||||
msgid "Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the original outline. Larger values give fewer points, which helps firmware that stalls while parsing G-code with many objects. 0 keeps every point."
|
||||
msgstr ""
|
||||
|
||||
msgid "Verbose G-code"
|
||||
msgstr ""
|
||||
|
||||
@@ -20536,7 +20542,7 @@ msgid "test %s start..."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "test %s ip resolved = %s"
|
||||
msgid "test %s IP resolved = %s"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
@@ -26488,17 +26494,18 @@ msgstr ""
|
||||
msgid "Basic Colors"
|
||||
msgstr ""
|
||||
|
||||
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
msgid "Orca Palette"
|
||||
msgstr ""
|
||||
|
||||
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
#. TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
msgid "RAL Classic"
|
||||
msgstr ""
|
||||
|
||||
#. TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
msgid "Gradients"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "Color picker"
|
||||
msgid "Solid"
|
||||
msgstr ""
|
||||
|
||||
@@ -26873,7 +26880,7 @@ msgstr ""
|
||||
msgid "Medium violet red"
|
||||
msgstr ""
|
||||
|
||||
msgid "PaleV violet red"
|
||||
msgid "Pale violet red"
|
||||
msgstr ""
|
||||
|
||||
msgid "Deep pink"
|
||||
@@ -27035,7 +27042,7 @@ msgstr ""
|
||||
msgid "Shallow sky"
|
||||
msgstr ""
|
||||
|
||||
msgid "Electric bel"
|
||||
msgid "Electric blue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dolphin"
|
||||
@@ -27104,7 +27111,7 @@ msgstr ""
|
||||
msgid "Maize yellow"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dalmatian yellow"
|
||||
msgid "Daffodil yellow"
|
||||
msgstr ""
|
||||
|
||||
msgid "Brown beige"
|
||||
@@ -27365,7 +27372,7 @@ msgstr ""
|
||||
msgid "Turquoise blue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Capsanthin violet"
|
||||
msgid "Capri blue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ocean blue"
|
||||
@@ -27479,7 +27486,7 @@ msgstr ""
|
||||
msgid "Silver grey"
|
||||
msgstr ""
|
||||
|
||||
msgid "Olivestone"
|
||||
msgid "Olive grey"
|
||||
msgstr ""
|
||||
|
||||
msgid "Moss grey"
|
||||
@@ -28067,10 +28074,19 @@ msgstr ""
|
||||
msgid "Built-in"
|
||||
msgstr ""
|
||||
|
||||
msgid "My textures"
|
||||
msgstr ""
|
||||
|
||||
msgid "No textures found."
|
||||
msgstr ""
|
||||
|
||||
msgid "My textures"
|
||||
msgid "Texture libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Explore more texture libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Opens OrcaCloud in the Home page to browse for plugins."
|
||||
msgstr ""
|
||||
|
||||
msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer."
|
||||
@@ -30123,6 +30139,9 @@ msgstr ""
|
||||
msgid "Slicing Pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource Texture"
|
||||
msgstr ""
|
||||
|
||||
msgid "The plugin's configuration UI was empty. Showing the default editor."
|
||||
msgstr ""
|
||||
|
||||
@@ -30238,6 +30257,24 @@ msgstr ""
|
||||
msgid "Plugin directory does not contain a .py or .whl entry file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource texture directory is not set"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource texture directory could not be resolved"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource texture path is not a directory"
|
||||
msgstr ""
|
||||
|
||||
msgid "Plugin folder could not be resolved"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource texture directory is outside the plugin folder"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource texture directory could not be read"
|
||||
msgstr ""
|
||||
|
||||
#, boost-format
|
||||
msgid "Slicing pipeline plugin '%1%' error: %2%"
|
||||
msgstr ""
|
||||
@@ -30594,7 +30631,7 @@ msgstr ""
|
||||
|
||||
#. TRN %1% is the application name.
|
||||
#, boost-format
|
||||
msgid "Not a gcode file generated by %1%."
|
||||
msgid "Not a G-code file generated by %1%."
|
||||
msgstr ""
|
||||
|
||||
#. TRN %1% is a tag in the G-code file that must not be translated.
|
||||
@@ -30682,7 +30719,7 @@ msgstr ""
|
||||
msgid "Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%"
|
||||
msgstr ""
|
||||
|
||||
msgid "Post-processor can't find exported gcode file"
|
||||
msgid "Post-processor can't find exported G-code file"
|
||||
msgstr ""
|
||||
|
||||
#, boost-format
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+1046
-549
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Slic3rPE\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-15 17:40+0800\n"
|
||||
"POT-Creation-Date: 2026-10-19 09:48-0300\n"
|
||||
"PO-Revision-Date: 2026-06-11 12:37-0300\n"
|
||||
"Last-Translator: Handle <mail@bysb.net>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -1751,7 +1751,7 @@ msgid "Stroke of shape (%1%) contains unsupported: %2%."
|
||||
msgstr "形状(%1%)的描边包含不受支持的内容:%2%。"
|
||||
|
||||
# AI Translated
|
||||
msgid "Not valid state please report reproduction steps on github"
|
||||
msgid "Not valid state please report reproduction steps on GitHub"
|
||||
msgstr "状态无效,请在 GitHub 上报告复现步骤"
|
||||
|
||||
# AI Translated
|
||||
@@ -1759,11 +1759,11 @@ msgid "No embossed file"
|
||||
msgstr "没有浮雕文件"
|
||||
|
||||
# AI Translated
|
||||
msgid "Missing svg file in embossed shape"
|
||||
msgid "Missing SVG file in embossed shape"
|
||||
msgstr "浮雕形状中缺少 SVG 文件"
|
||||
|
||||
# AI Translated
|
||||
msgid "Missing data of svg file"
|
||||
msgid "Missing data of SVG file"
|
||||
msgstr "缺少 SVG 文件数据"
|
||||
|
||||
# AI Translated
|
||||
@@ -5008,7 +5008,7 @@ msgid "Unknown C++ exception."
|
||||
msgstr "未知的 C++ 异常。"
|
||||
|
||||
# AI Translated
|
||||
msgid "Processing G-Code from previous file…"
|
||||
msgid "Processing G-code from previous file…"
|
||||
msgstr "正在处理之前文件的 G-code…"
|
||||
|
||||
msgid "Slicing complete"
|
||||
@@ -6699,7 +6699,7 @@ msgstr "体积:"
|
||||
msgid "Size:"
|
||||
msgstr "尺寸:"
|
||||
|
||||
#, boost-format
|
||||
#, c-format, boost-format
|
||||
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
|
||||
msgstr "发现G-code路径在层%d,高度为%.2lf mm处有冲突。请将有冲突的对象分离得更远(%s <-> %s)。"
|
||||
|
||||
@@ -12013,6 +12013,10 @@ msgstr "上传文件到打印机存储失败,请重试。"
|
||||
msgid "Send Failed"
|
||||
msgstr "发送失败"
|
||||
|
||||
# AI Translated
|
||||
msgid "No available printers"
|
||||
msgstr "没有可用的打印机"
|
||||
|
||||
msgid "The name length exceeds the limit."
|
||||
msgstr "名称长度超过限制。"
|
||||
|
||||
@@ -13922,8 +13926,8 @@ msgid "Left mouse"
|
||||
msgstr "鼠标左键"
|
||||
|
||||
# AI Translated
|
||||
msgid "Available anywhere in the window, even while typing in a text field."
|
||||
msgstr "在窗口任意位置可用,即使正在文本框中输入。"
|
||||
msgid "Available anywhere in the window."
|
||||
msgstr "在窗口任意位置可用。"
|
||||
|
||||
# AI Translated
|
||||
msgid "Run favorite 1 to 10"
|
||||
@@ -14039,10 +14043,6 @@ msgstr "%s 已分配给 %s。是否将其重新分配给 %s?"
|
||||
msgid "The default %s is assigned to %s. Reassign it to %s?"
|
||||
msgstr "默认的 %s 已分配给 %s。是否将其重新分配给 %s?"
|
||||
|
||||
# AI Translated
|
||||
msgid "Global shortcuts also apply while typing."
|
||||
msgstr "全局快捷键在输入文字时同样生效。"
|
||||
|
||||
# AI Translated
|
||||
msgid "A key that types a character cannot be a global shortcut."
|
||||
msgstr "会输入字符的按键不能用作全局快捷键。"
|
||||
@@ -18086,6 +18086,14 @@ msgstr "对象排除"
|
||||
msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
|
||||
msgstr "启用此选项以在G-code中添加EXCLUDE OBJECT命令。"
|
||||
|
||||
# AI Translated
|
||||
msgid "Outline tolerance"
|
||||
msgstr "轮廓线公差"
|
||||
|
||||
# AI Translated
|
||||
msgid "Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the original outline. Larger values give fewer points, which helps firmware that stalls while parsing G-code with many objects. 0 keeps every point."
|
||||
msgstr "写入 EXCLUDE_OBJECT_DEFINE 的对象轮廓线与原始轮廓线之间的最大偏差。数值越大,点数越少,有助于避免固件在解析包含大量对象的 G-code 时卡顿。0 表示保留所有点。"
|
||||
|
||||
msgid "Verbose G-code"
|
||||
msgstr "注释G-code"
|
||||
|
||||
@@ -24004,7 +24012,7 @@ msgstr "开始测试 %s..."
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
msgid "test %s ip resolved = %s"
|
||||
msgid "test %s IP resolved = %s"
|
||||
msgstr "测试 %s:IP 解析结果 = %s"
|
||||
|
||||
# AI Translated
|
||||
@@ -31498,20 +31506,21 @@ msgid "Basic Colors"
|
||||
msgstr "基本颜色"
|
||||
|
||||
# AI Translated
|
||||
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
msgid "Orca Palette"
|
||||
msgstr "Orca 调色板"
|
||||
|
||||
# AI Translated
|
||||
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
#. TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
msgid "RAL Classic"
|
||||
msgstr "RAL 经典色"
|
||||
|
||||
# AI Translated
|
||||
#. TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
msgid "Gradients"
|
||||
msgstr "渐变预设"
|
||||
|
||||
# AI Translated
|
||||
msgctxt "Color picker"
|
||||
msgid "Solid"
|
||||
msgstr "纯色"
|
||||
|
||||
@@ -32010,7 +32019,7 @@ msgid "Medium violet red"
|
||||
msgstr "中紫罗兰红"
|
||||
|
||||
# AI Translated
|
||||
msgid "PaleV violet red"
|
||||
msgid "Pale violet red"
|
||||
msgstr "浅紫罗兰红"
|
||||
|
||||
# AI Translated
|
||||
@@ -32226,8 +32235,7 @@ msgid "Shallow sky"
|
||||
msgstr "浅空"
|
||||
|
||||
# AI Translated
|
||||
# Needs human review: The English “Electric bel” looks like a typo (Electric blue? Electric eel?); the color is #1E90FF, so 电光蓝 was chosen. Recheck once the source name is fixed.
|
||||
msgid "Electric bel"
|
||||
msgid "Electric blue"
|
||||
msgstr "电光蓝"
|
||||
|
||||
# AI Translated
|
||||
@@ -32319,9 +32327,8 @@ msgid "Maize yellow"
|
||||
msgstr "玉米黄"
|
||||
|
||||
# AI Translated
|
||||
# Needs human review: RAL 1007 is “Daffodil yellow” (水仙黄); the English name in the palette looks wrong. Recheck once the source name is fixed.
|
||||
msgid "Dalmatian yellow"
|
||||
msgstr "达尔马提亚黄"
|
||||
msgid "Daffodil yellow"
|
||||
msgstr "水仙黄"
|
||||
|
||||
# AI Translated
|
||||
msgid "Brown beige"
|
||||
@@ -32668,8 +32675,8 @@ msgid "Turquoise blue"
|
||||
msgstr "绿松石蓝"
|
||||
|
||||
# AI Translated
|
||||
msgid "Capsanthin violet"
|
||||
msgstr "辣椒红素紫"
|
||||
msgid "Capri blue"
|
||||
msgstr "卡普里蓝"
|
||||
|
||||
# AI Translated
|
||||
msgid "Ocean blue"
|
||||
@@ -32820,8 +32827,8 @@ msgid "Silver grey"
|
||||
msgstr "银灰色"
|
||||
|
||||
# AI Translated
|
||||
msgid "Olivestone"
|
||||
msgstr "橄榄石色"
|
||||
msgid "Olive grey"
|
||||
msgstr "橄榄灰"
|
||||
|
||||
# AI Translated
|
||||
msgid "Moss grey"
|
||||
@@ -33597,13 +33604,25 @@ msgstr "从“我的纹理”中移除"
|
||||
msgid "Built-in"
|
||||
msgstr "内置"
|
||||
|
||||
# AI Translated
|
||||
msgid "My textures"
|
||||
msgstr "我的纹理"
|
||||
|
||||
# AI Translated
|
||||
msgid "No textures found."
|
||||
msgstr "未找到纹理。"
|
||||
|
||||
# AI Translated
|
||||
msgid "My textures"
|
||||
msgstr "我的纹理"
|
||||
msgid "Texture libraries"
|
||||
msgstr "纹理库"
|
||||
|
||||
# AI Translated
|
||||
msgid "Explore more texture libraries"
|
||||
msgstr "探索更多纹理库"
|
||||
|
||||
# AI Translated
|
||||
msgid "Opens OrcaCloud in the Home page to browse for plugins."
|
||||
msgstr "在主页中打开 OrcaCloud,浏览插件。"
|
||||
|
||||
# AI Translated
|
||||
msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer."
|
||||
@@ -36285,6 +36304,10 @@ msgstr "脚本"
|
||||
msgid "Slicing Pipeline"
|
||||
msgstr "切片流水线"
|
||||
|
||||
# AI Translated
|
||||
msgid "Resource Texture"
|
||||
msgstr "资源纹理"
|
||||
|
||||
# AI Translated
|
||||
msgid "The plugin's configuration UI was empty. Showing the default editor."
|
||||
msgstr "插件的配置界面为空。正在显示默认编辑器。"
|
||||
@@ -36433,6 +36456,30 @@ msgstr "插件目录同时包含 .py 和 .whl 入口文件"
|
||||
msgid "Plugin directory does not contain a .py or .whl entry file"
|
||||
msgstr "插件目录不包含 .py 或 .whl 入口文件"
|
||||
|
||||
# AI Translated
|
||||
msgid "Resource texture directory is not set"
|
||||
msgstr "未设置资源纹理目录"
|
||||
|
||||
# AI Translated
|
||||
msgid "Resource texture directory could not be resolved"
|
||||
msgstr "无法解析资源纹理目录"
|
||||
|
||||
# AI Translated
|
||||
msgid "Resource texture path is not a directory"
|
||||
msgstr "资源纹理路径不是目录"
|
||||
|
||||
# AI Translated
|
||||
msgid "Plugin folder could not be resolved"
|
||||
msgstr "无法解析插件文件夹"
|
||||
|
||||
# AI Translated
|
||||
msgid "Resource texture directory is outside the plugin folder"
|
||||
msgstr "资源纹理目录位于插件文件夹之外"
|
||||
|
||||
# AI Translated
|
||||
msgid "Resource texture directory could not be read"
|
||||
msgstr "无法读取资源纹理目录"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Slicing pipeline plugin '%1%' error: %2%"
|
||||
@@ -36891,7 +36938,7 @@ msgstr "加载配置文件“%1%”失败:%2%"
|
||||
# AI Translated
|
||||
#. TRN %1% is the application name.
|
||||
#, boost-format
|
||||
msgid "Not a gcode file generated by %1%."
|
||||
msgid "Not a G-code file generated by %1%."
|
||||
msgstr "不是由 %1% 生成的 G-code 文件。"
|
||||
|
||||
# AI Translated
|
||||
@@ -37011,7 +37058,7 @@ msgid "Failed making a temporary copy of G-code file %1% before running a post-p
|
||||
msgstr "运行后处理脚本之前,无法创建 G-code 文件 %1% 的临时副本:%2%"
|
||||
|
||||
# AI Translated
|
||||
msgid "Post-processor can't find exported gcode file"
|
||||
msgid "Post-processor can't find exported G-code file"
|
||||
msgstr "后处理器找不到导出的 G-code 文件"
|
||||
|
||||
# AI Translated
|
||||
@@ -37400,6 +37447,40 @@ msgstr ""
|
||||
"避免翘曲\n"
|
||||
"您知道吗?打印ABS这类易翘曲材料时,适当提高热床温度可以降低翘曲的概率。"
|
||||
|
||||
# AI Translated
|
||||
# Needs human review: RAL 1007 is “Daffodil yellow” (水仙黄); the English name in the palette looks wrong. Recheck once the source name is fixed.
|
||||
#~ msgid "Dalmatian yellow"
|
||||
#~ msgstr "达尔马提亚黄"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "Capsanthin violet"
|
||||
#~ msgstr "辣椒红素紫"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "Olivestone"
|
||||
#~ msgstr "橄榄石色"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "Available anywhere in the window, even while typing in a text field."
|
||||
#~ msgstr "在窗口任意位置可用,即使正在文本框中输入。"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "Global shortcuts also apply while typing."
|
||||
#~ msgstr "全局快捷键在输入文字时同样生效。"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "Solid"
|
||||
#~ msgstr "纯色"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "PaleV violet red"
|
||||
#~ msgstr "浅紫罗兰红"
|
||||
|
||||
# AI Translated
|
||||
# Needs human review: The English “Electric bel” looks like a typo (Electric blue? Electric eel?); the color is #1E90FF, so 电光蓝 was chosen. Recheck once the source name is fixed.
|
||||
#~ msgid "Electric bel"
|
||||
#~ msgstr "电光蓝"
|
||||
|
||||
#~ msgid "Please choose the filament color"
|
||||
#~ msgstr "请选择耗材丝颜色"
|
||||
|
||||
@@ -37865,9 +37946,6 @@ msgstr ""
|
||||
#~ msgid "no configured tool"
|
||||
#~ msgstr "未配置的工具头"
|
||||
|
||||
#~ msgid "Add end G-Code when finishing the entire print."
|
||||
#~ msgstr "所有打印结束时的结尾G-code"
|
||||
|
||||
#~ msgid "Maximum X speed"
|
||||
#~ msgstr "X最大速度"
|
||||
|
||||
@@ -37884,26 +37962,11 @@ msgstr ""
|
||||
#~ msgid "Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, including a final G-code post-processing step. Research/experimental."
|
||||
#~ msgstr "在每个切片流水线步骤中调用的 Python 插件,用于读取和修改中间切片数据,包括最终的 G-code 后处理步骤。研究/实验性。"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "When enabled, the slicer emits g-code with the primary zone's contents centered on bed origin (0,0). The printer firmware is responsible for placing copies/mirrors at each physical zone position. Enable only on printers whose firmware applies its own zone offsets in copy/mirror mode (e.g. RepRapFirmware IDEX duplication mode). Has no effect in Primary mode."
|
||||
#~ msgstr "启用后,切片软件输出的 G-code 会以热床原点 (0,0) 为中心放置主区域的内容,由打印机固件负责把复制件/镜像件放到各个物理区域。仅当打印机固件在复制/镜像模式下会自行应用区域偏移时才启用(例如 RepRapFirmware 的 IDEX 复制模式)。主模式下无效。"
|
||||
|
||||
# AI Translated
|
||||
# Needs human review: "pem-inversion defaults": confirm pem is the physical extruder map and that the default is its inverse.
|
||||
#~ msgid "Per-plate override mapping physical heads to filament slots (1-based). Empty means use pem-inversion defaults."
|
||||
#~ msgstr "按盘单独设置物理工具头到耗材丝槽位的映射(从 1 开始编号),覆盖默认映射。留空则使用由物理挤出机映射(pem)反推的默认值。"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "Axis the mesh is rotated about before slicing. This is the belt printer's tilt: an isometric (no distortion) rotation that also drives bed rendering and support gravity tilt, and that the g-code back-transform inverts before the machine-frame shear/scale and remap. X is the typical gantry tilt (belt travels along Y)."
|
||||
#~ msgstr "切片前模型网格的旋转轴。这一旋转即传送带打印机的倾斜:它是保距(无畸变)旋转,同时用于热床渲染和支撑的重力倾斜;生成 G-code 时,逆变换会先还原这一旋转,再进行机器坐标系剪切/缩放和重映射。X 是典型的龙门倾斜轴(传送带沿 Y 方向移动)。"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "Expert override: set the machine-frame (g-code shear/scale) tilt angle independently of the pre-slice rotation angle. When disabled, the machine-frame transform is derived from the belt tilt angle, so a single angle drives both stages. Enable only to compensate for a machine whose physical gantry tilt differs from the slicing rotation."
|
||||
#~ msgstr "专家选项:单独设置机器坐标系(G-code 剪切/缩放)的倾斜角度,与切片前的旋转角度互不影响。禁用时,机器坐标系变换由传送带倾斜角度推导,两个阶段使用同一个角度。仅当机器实际的龙门倾角与切片旋转角度不一致时,才需启用此项进行补偿。"
|
||||
|
||||
#~ msgid "The maximum deviation allowed when reducing the resolution for the 'Maximum wall resolution' setting. If you increase this, the print will be less accurate, but the G-Code will be smaller. 'Maximum wall deviation' limits 'Maximum wall resolution', so if the two conflict, 'Maximum wall deviation' takes precedence."
|
||||
#~ msgstr "在为“最大墙体分辨率”设置降低分辨率时所允许的最大偏差。如果增大此值,打印精度会降低,但 G-code 会更小。“最大墙体偏差”会限制“最大墙体分辨率”,因此当两者冲突时,以“最大墙体偏差”为准。"
|
||||
|
||||
#~ msgid " not in range "
|
||||
#~ msgstr " 不在合理范围内"
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-15 17:40+0800\n"
|
||||
"POT-Creation-Date: 2026-10-19 09:48-0300\n"
|
||||
"PO-Revision-Date: 2025-11-28 13:48-0600\n"
|
||||
"Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -1826,7 +1826,7 @@ msgid "Stroke of shape (%1%) contains unsupported: %2%."
|
||||
msgstr "形狀(%1%)的筆劃包含不支援的內容:%2%。"
|
||||
|
||||
# AI Translated
|
||||
msgid "Not valid state please report reproduction steps on github"
|
||||
msgid "Not valid state please report reproduction steps on GitHub"
|
||||
msgstr "狀態無效,請在 GitHub 上回報重現步驟"
|
||||
|
||||
# AI Translated
|
||||
@@ -1834,11 +1834,11 @@ msgid "No embossed file"
|
||||
msgstr "沒有浮雕檔案"
|
||||
|
||||
# AI Translated
|
||||
msgid "Missing svg file in embossed shape"
|
||||
msgid "Missing SVG file in embossed shape"
|
||||
msgstr "浮雕形狀中缺少 SVG 檔案"
|
||||
|
||||
# AI Translated
|
||||
msgid "Missing data of svg file"
|
||||
msgid "Missing data of SVG file"
|
||||
msgstr "缺少 SVG 檔案的資料"
|
||||
|
||||
msgid "Face the camera"
|
||||
@@ -5219,7 +5219,7 @@ msgid "Unknown C++ exception."
|
||||
msgstr "未知的 C++ 例外。"
|
||||
|
||||
# AI Translated
|
||||
msgid "Processing G-Code from previous file…"
|
||||
msgid "Processing G-code from previous file…"
|
||||
msgstr "正在處理先前檔案的 G-code…"
|
||||
|
||||
msgid "Slicing complete"
|
||||
@@ -6912,7 +6912,7 @@ msgid "Size:"
|
||||
msgstr "尺寸:"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
#, c-format, boost-format
|
||||
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
|
||||
msgstr "在第 %d 層(Z = %.2lf mm)發現 G-code 路徑衝突。請將有衝突的物件分開得更遠(%s <-> %s)。"
|
||||
|
||||
@@ -12298,6 +12298,10 @@ msgstr "無法將檔案上傳至列印裝置的儲存空間,請重試。"
|
||||
msgid "Send Failed"
|
||||
msgstr "傳送失敗"
|
||||
|
||||
# AI Translated
|
||||
msgid "No available printers"
|
||||
msgstr "沒有可用的列印裝置"
|
||||
|
||||
msgid "The name length exceeds the limit."
|
||||
msgstr "名稱長度超過限制。"
|
||||
|
||||
@@ -14251,8 +14255,8 @@ msgid "Left mouse"
|
||||
msgstr "滑鼠左鍵"
|
||||
|
||||
# AI Translated
|
||||
msgid "Available anywhere in the window, even while typing in a text field."
|
||||
msgstr "在視窗中任何位置皆可使用,即使正在文字欄位中輸入也可以。"
|
||||
msgid "Available anywhere in the window."
|
||||
msgstr "在視窗中任何位置皆可使用。"
|
||||
|
||||
# AI Translated
|
||||
msgid "Run favorite 1 to 10"
|
||||
@@ -14372,10 +14376,6 @@ msgstr "%s 已指派給 %s。要改為指派給 %s 嗎?"
|
||||
msgid "The default %s is assigned to %s. Reassign it to %s?"
|
||||
msgstr "預設的 %s 已指派給 %s。要改為指派給 %s 嗎?"
|
||||
|
||||
# AI Translated
|
||||
msgid "Global shortcuts also apply while typing."
|
||||
msgstr "全域快速鍵在輸入文字時也會生效。"
|
||||
|
||||
# AI Translated
|
||||
msgid "A key that types a character cannot be a global shortcut."
|
||||
msgstr "會輸入字元的按鍵不能作為全域快速鍵。"
|
||||
@@ -18513,6 +18513,14 @@ msgstr "物件排除"
|
||||
msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
|
||||
msgstr "啟用此選項以在 G-code 中加入 EXCLUDE OBJECT 指令。"
|
||||
|
||||
# AI Translated
|
||||
msgid "Outline tolerance"
|
||||
msgstr "輪廓線公差"
|
||||
|
||||
# AI Translated
|
||||
msgid "Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the original outline. Larger values give fewer points, which helps firmware that stalls while parsing G-code with many objects. 0 keeps every point."
|
||||
msgstr "寫入 EXCLUDE_OBJECT_DEFINE 的物件輪廓線與原始輪廓線之間的最大偏差。數值越大,點數越少,有助於避免韌體在解析包含大量物件的 G-code 時卡住。0 表示保留所有點。"
|
||||
|
||||
msgid "Verbose G-code"
|
||||
msgstr "詳細 G-code"
|
||||
|
||||
@@ -24597,7 +24605,7 @@ msgstr "測試 %s 開始..."
|
||||
|
||||
# AI Translated
|
||||
#, c-format, boost-format
|
||||
msgid "test %s ip resolved = %s"
|
||||
msgid "test %s IP resolved = %s"
|
||||
msgstr "測試 %s IP 解析結果 = %s"
|
||||
|
||||
# AI Translated
|
||||
@@ -32124,21 +32132,22 @@ msgid "Basic Colors"
|
||||
msgstr "基本顏色"
|
||||
|
||||
# AI Translated
|
||||
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
msgid "Orca Palette"
|
||||
msgstr "Orca 調色盤"
|
||||
|
||||
# AI Translated
|
||||
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
#. TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
msgid "RAL Classic"
|
||||
msgstr "RAL 經典色"
|
||||
|
||||
# AI Translated
|
||||
# Needs human review: Palette dropdown entry for the gradient presets (next to 基本顏色 / Orca 調色盤 / RAL 經典色); 漸層配色 keeps it apart from the Solid/Gradient switch label 漸層. Check it reads right in the dropdown.
|
||||
#. TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
msgid "Gradients"
|
||||
msgstr "漸層配色"
|
||||
|
||||
# AI Translated
|
||||
msgctxt "Color picker"
|
||||
msgid "Solid"
|
||||
msgstr "純色"
|
||||
|
||||
@@ -32637,7 +32646,7 @@ msgid "Medium violet red"
|
||||
msgstr "中紫羅蘭紅"
|
||||
|
||||
# AI Translated
|
||||
msgid "PaleV violet red"
|
||||
msgid "Pale violet red"
|
||||
msgstr "淡紫羅蘭紅"
|
||||
|
||||
# AI Translated
|
||||
@@ -32853,8 +32862,7 @@ msgid "Shallow sky"
|
||||
msgstr "淺色天空"
|
||||
|
||||
# AI Translated
|
||||
# Needs human review: Orca palette: the English is a typo for "Electric blue" (hex #1E90FF); translated as 電光藍. Revisit if the source keeps a different name.
|
||||
msgid "Electric bel"
|
||||
msgid "Electric blue"
|
||||
msgstr "電光藍"
|
||||
|
||||
# AI Translated
|
||||
@@ -32946,9 +32954,8 @@ msgid "Maize yellow"
|
||||
msgstr "玉米黃"
|
||||
|
||||
# AI Translated
|
||||
# Needs human review: RAL 1007 in the RAL Classic list: the English name looks wrong (1007 is Daffodil yellow, 水仙黃); translated literally. Revisit once the source name is fixed.
|
||||
msgid "Dalmatian yellow"
|
||||
msgstr "達爾馬提亞黃"
|
||||
msgid "Daffodil yellow"
|
||||
msgstr "水仙黃"
|
||||
|
||||
# AI Translated
|
||||
msgid "Brown beige"
|
||||
@@ -33296,9 +33303,8 @@ msgid "Turquoise blue"
|
||||
msgstr "綠松石藍"
|
||||
|
||||
# AI Translated
|
||||
# Needs human review: RAL 5019 in the RAL Classic list: the English name is not a RAL Classic name (5019 is Capri blue); translated literally. Revisit once the source name is fixed.
|
||||
msgid "Capsanthin violet"
|
||||
msgstr "辣椒紅素紫"
|
||||
msgid "Capri blue"
|
||||
msgstr "卡布里藍"
|
||||
|
||||
# AI Translated
|
||||
msgid "Ocean blue"
|
||||
@@ -33449,8 +33455,8 @@ msgid "Silver grey"
|
||||
msgstr "銀灰"
|
||||
|
||||
# AI Translated
|
||||
msgid "Olivestone"
|
||||
msgstr "橄欖石色"
|
||||
msgid "Olive grey"
|
||||
msgstr "橄欖灰"
|
||||
|
||||
# AI Translated
|
||||
msgid "Moss grey"
|
||||
@@ -34226,13 +34232,25 @@ msgstr "從「我的紋理」移除"
|
||||
msgid "Built-in"
|
||||
msgstr "內建"
|
||||
|
||||
# AI Translated
|
||||
msgid "My textures"
|
||||
msgstr "我的紋理"
|
||||
|
||||
# AI Translated
|
||||
msgid "No textures found."
|
||||
msgstr "找不到紋理。"
|
||||
|
||||
# AI Translated
|
||||
msgid "My textures"
|
||||
msgstr "我的紋理"
|
||||
msgid "Texture libraries"
|
||||
msgstr "紋理庫"
|
||||
|
||||
# AI Translated
|
||||
msgid "Explore more texture libraries"
|
||||
msgstr "探索更多紋理庫"
|
||||
|
||||
# AI Translated
|
||||
msgid "Opens OrcaCloud in the Home page to browse for plugins."
|
||||
msgstr "在首頁中開啟 OrcaCloud,瀏覽外掛。"
|
||||
|
||||
# AI Translated
|
||||
msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer."
|
||||
@@ -36928,6 +36946,10 @@ msgstr "指令碼"
|
||||
msgid "Slicing Pipeline"
|
||||
msgstr "切片管線"
|
||||
|
||||
# AI Translated
|
||||
msgid "Resource Texture"
|
||||
msgstr "資源紋理"
|
||||
|
||||
# AI Translated
|
||||
msgid "The plugin's configuration UI was empty. Showing the default editor."
|
||||
msgstr "外掛的設定介面為空。將顯示預設編輯器。"
|
||||
@@ -37075,6 +37097,30 @@ msgstr "外掛目錄同時包含 .py 和 .whl 進入點檔案"
|
||||
msgid "Plugin directory does not contain a .py or .whl entry file"
|
||||
msgstr "外掛目錄不包含 .py 或 .whl 進入點檔案"
|
||||
|
||||
# AI Translated
|
||||
msgid "Resource texture directory is not set"
|
||||
msgstr "未設定資源紋理目錄"
|
||||
|
||||
# AI Translated
|
||||
msgid "Resource texture directory could not be resolved"
|
||||
msgstr "無法解析資源紋理目錄"
|
||||
|
||||
# AI Translated
|
||||
msgid "Resource texture path is not a directory"
|
||||
msgstr "資源紋理路徑不是目錄"
|
||||
|
||||
# AI Translated
|
||||
msgid "Plugin folder could not be resolved"
|
||||
msgstr "無法解析外掛資料夾"
|
||||
|
||||
# AI Translated
|
||||
msgid "Resource texture directory is outside the plugin folder"
|
||||
msgstr "資源紋理目錄位於外掛資料夾之外"
|
||||
|
||||
# AI Translated
|
||||
msgid "Resource texture directory could not be read"
|
||||
msgstr "無法讀取資源紋理目錄"
|
||||
|
||||
# AI Translated
|
||||
#, boost-format
|
||||
msgid "Slicing pipeline plugin '%1%' error: %2%"
|
||||
@@ -37533,7 +37579,7 @@ msgstr "載入設定檔「%1%」失敗:%2%"
|
||||
# AI Translated
|
||||
#. TRN %1% is the application name.
|
||||
#, boost-format
|
||||
msgid "Not a gcode file generated by %1%."
|
||||
msgid "Not a G-code file generated by %1%."
|
||||
msgstr "不是由 %1% 產生的 G-code 檔案。"
|
||||
|
||||
# AI Translated
|
||||
@@ -37653,7 +37699,7 @@ msgid "Failed making a temporary copy of G-code file %1% before running a post-p
|
||||
msgstr "執行後處理腳本前,無法建立 G-code 檔案 %1% 的暫存副本:%2%"
|
||||
|
||||
# AI Translated
|
||||
msgid "Post-processor can't find exported gcode file"
|
||||
msgid "Post-processor can't find exported G-code file"
|
||||
msgstr "後處理程式找不到匯出的 G-code 檔案"
|
||||
|
||||
# AI Translated
|
||||
@@ -38049,6 +38095,41 @@ msgstr ""
|
||||
"避免翹曲\n"
|
||||
"您知道嗎?當列印容易翹曲的材料(如 ABS)時,適當提高熱床溫度可以降低翹曲的機率。"
|
||||
|
||||
# AI Translated
|
||||
# Needs human review: RAL 1007 in the RAL Classic list: the English name looks wrong (1007 is Daffodil yellow, 水仙黃); translated literally. Revisit once the source name is fixed.
|
||||
#~ msgid "Dalmatian yellow"
|
||||
#~ msgstr "達爾馬提亞黃"
|
||||
|
||||
# AI Translated
|
||||
# Needs human review: RAL 5019 in the RAL Classic list: the English name is not a RAL Classic name (5019 is Capri blue); translated literally. Revisit once the source name is fixed.
|
||||
#~ msgid "Capsanthin violet"
|
||||
#~ msgstr "辣椒紅素紫"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "Olivestone"
|
||||
#~ msgstr "橄欖石色"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "Available anywhere in the window, even while typing in a text field."
|
||||
#~ msgstr "在視窗中任何位置皆可使用,即使正在文字欄位中輸入也可以。"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "Global shortcuts also apply while typing."
|
||||
#~ msgstr "全域快速鍵在輸入文字時也會生效。"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "Solid"
|
||||
#~ msgstr "純色"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "PaleV violet red"
|
||||
#~ msgstr "淡紫羅蘭紅"
|
||||
|
||||
# AI Translated
|
||||
# Needs human review: Orca palette: the English is a typo for "Electric blue" (hex #1E90FF); translated as 電光藍. Revisit if the source keeps a different name.
|
||||
#~ msgid "Electric bel"
|
||||
#~ msgstr "電光藍"
|
||||
|
||||
#~ msgid "Please choose the filament color"
|
||||
#~ msgstr "請選擇線材顏色"
|
||||
|
||||
@@ -38513,9 +38594,6 @@ msgstr ""
|
||||
#~ msgid "no configured tool"
|
||||
#~ msgstr "未設定的工具頭"
|
||||
|
||||
#~ msgid "Add end G-Code when finishing the entire print."
|
||||
#~ msgstr "所有列印結束時的結尾 G-code"
|
||||
|
||||
#~ msgid "Maximum X speed"
|
||||
#~ msgstr "X 最大速度"
|
||||
|
||||
@@ -38532,25 +38610,10 @@ msgstr ""
|
||||
#~ msgid "Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, including a final G-code post-processing step. Research/experimental."
|
||||
#~ msgstr "在每個切片管線步驟呼叫的 Python 外掛,用於讀取與修改中介切片資料,包括最終的 G-code 後處理步驟。研究/實驗性質。"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "When enabled, the slicer emits g-code with the primary zone's contents centered on bed origin (0,0). The printer firmware is responsible for placing copies/mirrors at each physical zone position. Enable only on printers whose firmware applies its own zone offsets in copy/mirror mode (e.g. RepRapFirmware IDEX duplication mode). Has no effect in Primary mode."
|
||||
#~ msgstr "啟用後,切片軟體輸出的 G-code 會將主區域的內容以列印板原點 (0,0) 為中心放置,並由列印裝置韌體負責將複製件/鏡像件放到各個實體區域位置。僅在韌體會於複製/鏡像模式下自行套用區域偏移的列印裝置上啟用(例如 RepRapFirmware 的 IDEX 複製模式)。在主模式下無效。"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "Per-plate override mapping physical heads to filament slots (1-based). Empty means use pem-inversion defaults."
|
||||
#~ msgstr "依列印板覆寫實體工具頭到線材槽位的映射(從 1 開始編號)。留空表示使用由 pem 反推的預設映射。"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "Axis the mesh is rotated about before slicing. This is the belt printer's tilt: an isometric (no distortion) rotation that also drives bed rendering and support gravity tilt, and that the g-code back-transform inverts before the machine-frame shear/scale and remap. X is the typical gantry tilt (belt travels along Y)."
|
||||
#~ msgstr "切片前網格旋轉所繞的軸。這就是輸送帶列印裝置的傾斜:一種等距(無變形)旋轉,同時用於列印板渲染與支撐的重力傾斜;G-code 反向轉換會先還原此旋轉,再進行機器座標系的剪切/縮放與重新映射。X 是典型的龍門傾斜軸(輸送帶沿 Y 方向移動)。"
|
||||
|
||||
# AI Translated
|
||||
#~ msgid "Expert override: set the machine-frame (g-code shear/scale) tilt angle independently of the pre-slice rotation angle. When disabled, the machine-frame transform is derived from the belt tilt angle, so a single angle drives both stages. Enable only to compensate for a machine whose physical gantry tilt differs from the slicing rotation."
|
||||
#~ msgstr "專家覆寫選項:獨立於切片前的旋轉角度,單獨設定機器座標系(G-code 剪切/縮放)的傾斜角度。停用時,機器座標系變換由輸送帶傾斜角度推導,兩個階段使用同一個角度。僅在機器實際的龍門傾角與切片旋轉角度不同時,才啟用此選項進行補償。"
|
||||
|
||||
#~ msgid "The maximum deviation allowed when reducing the resolution for the 'Maximum wall resolution' setting. If you increase this, the print will be less accurate, but the G-Code will be smaller. 'Maximum wall deviation' limits 'Maximum wall resolution', so if the two conflict, 'Maximum wall deviation' takes precedence."
|
||||
#~ msgstr "「最大牆解析度」設定在降低解析度時所允許的最大偏差。若增加此值,列印會較不精確,但 G-Code 會更小。「最大牆偏差」會限制「最大牆解析度」,因此若兩者衝突,將以「最大牆偏差」為準。"
|
||||
|
||||
#~ msgid " not in range "
|
||||
#~ msgstr " 不在合理的區間"
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ const cssColors = [
|
||||
{hex: "#800080", name: "Purple"},
|
||||
{hex: "#663399", name: "Rebecca purple"},
|
||||
{hex: "#C71585", name: "Medium violet red"},
|
||||
{hex: "#DB7093", name: "PaleV violet red"},
|
||||
{hex: "#DB7093", name: "Pale violet red"},
|
||||
{hex: "#FF1493", name: "Deep pink"},
|
||||
{hex: "#FF69B4", name: "Hot pink"},
|
||||
{hex: "#FFB6C1", name: "Light pink"},
|
||||
@@ -193,7 +193,7 @@ const palettes = {
|
||||
{hex: "#40E0D0", name: "Turquoise bay"},
|
||||
{hex: "#2EBDEF", name: "Sky over reef"},
|
||||
{hex: "#87CEEB", name: "Shallow sky"},
|
||||
{hex: "#1E90FF", name: "Electric bel"},
|
||||
{hex: "#1E90FF", name: "Electric blue"},
|
||||
{hex: "#4169E1", name: "Dolphin"},
|
||||
{hex: "#0000CD", name: "Ocean depths"},
|
||||
{hex: "#191970", name: "Whale"},
|
||||
@@ -219,7 +219,7 @@ const palettes = {
|
||||
{hex: "#EFBF04", name: "1004 Golden yellow"},
|
||||
{hex: "#D7A500", name: "1005 Honey yellow"},
|
||||
{hex: "#F2D185", name: "1006 Maize yellow"},
|
||||
{hex: "#FDD185", name: "1007 Dalmatian yellow"},
|
||||
{hex: "#FDD185", name: "1007 Daffodil yellow"},
|
||||
{hex: "#8B7D6B", name: "1011 Brown beige"},
|
||||
{hex: "#F5DB50", name: "1012 Lemon yellow"},
|
||||
{hex: "#F3E5AB", name: "1013 Oyster white"},
|
||||
@@ -318,7 +318,7 @@ const palettes = {
|
||||
{hex: "#0384A7", name: "5015 Sky blue"},
|
||||
{hex: "#006A9E", name: "5017 Traffic blue"},
|
||||
{hex: "#005F87", name: "5018 Turquoise blue"},
|
||||
{hex: "#5A3D5B", name: "5019 Capsanthin violet"},
|
||||
{hex: "#1B5583", name: "5019 Capri blue"},
|
||||
{hex: "#04749F", name: "5020 Ocean blue"},
|
||||
{hex: "#009AAA", name: "5021 Water blue"},
|
||||
{hex: "#1A3D5F", name: "5022 Night blue"},
|
||||
@@ -366,7 +366,7 @@ const palettes = {
|
||||
{hex: "#00A550", name: "6039 Mint green"},
|
||||
{hex: "#7D7D7D", name: "7000 Squirrel grey"},
|
||||
{hex: "#A8A9AD", name: "7001 Silver grey"},
|
||||
{hex: "#6F7368", name: "7002 Olivestone"},
|
||||
{hex: "#6F7368", name: "7002 Olive grey"},
|
||||
{hex: "#8F9080", name: "7003 Moss grey"},
|
||||
{hex: "#8E9291", name: "7004 Signal grey"},
|
||||
{hex: "#8B8788", name: "7005 Mouse grey"},
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Fails when a Windows build has an exception class with broken throw info.
|
||||
|
||||
.DESCRIPTION
|
||||
Clang on the MSVC ABI can leave the copy constructor out of the CatchableType it emits for
|
||||
an exception class that comes through a precompiled header
|
||||
(https://github.com/llvm/llvm-project/issues/53486). The linker keeps one throw info per
|
||||
class, so when it keeps such a record, a copy of that exception (std::exception_ptr, a catch
|
||||
by value) shares its data with the original and the data is freed twice.
|
||||
|
||||
A class is either trivially copyable or not, so a binary should never have both a CatchableType
|
||||
without a copy constructor (_CT??_R0?AV<class>@@@8<size>) and one with it
|
||||
(_CT??_R0?AV<class>@@@8??0<class>...). The script lists each PDB's public symbols with the
|
||||
llvm-pdbutil next to the build's compiler and reports every class that has both.
|
||||
|
||||
scripts\test_check_exception_throw_info.ps1 tests it.
|
||||
|
||||
.PARAMETER BuildDir
|
||||
The slicer build directory. Its CMakeCache.txt names the compiler, and its src\<Config>
|
||||
holds the PDBs to check, which must include OrcaSlicer.pdb.
|
||||
|
||||
.PARAMETER Config
|
||||
The configuration whose PDBs to check (default: Release).
|
||||
|
||||
.PARAMETER Pdb
|
||||
PDBs to check instead of the ones in <BuildDir>\src\<Config>, as a list or comma-separated.
|
||||
|
||||
.PARAMETER Dump
|
||||
Saved `llvm-pdbutil dump --publics` output to check instead of PDBs. Needs no build.
|
||||
|
||||
.EXAMPLE
|
||||
scripts\check_exception_throw_info.ps1 -BuildDir build
|
||||
#>
|
||||
param(
|
||||
[string] $BuildDir,
|
||||
[string] $Config = 'Release',
|
||||
[string[]] $Pdb,
|
||||
[string[]] $Dump
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# powershell -File passes "a.pdb,b.pdb" as one string. A path that exists as given stays whole.
|
||||
function Split-PathList([string[]] $values) {
|
||||
@($values | Where-Object { $_ } | ForEach-Object {
|
||||
if ((Test-Path -LiteralPath $_) -or $_ -notmatch ',') { $_ } else { $_ -split ',' | Where-Object { $_ } }
|
||||
})
|
||||
}
|
||||
|
||||
$Dump = Split-PathList $Dump
|
||||
if (-not $Dump) {
|
||||
if (-not $BuildDir) {
|
||||
throw 'Name the slicer build directory with -BuildDir.'
|
||||
}
|
||||
$cache = Join-Path $BuildDir 'CMakeCache.txt'
|
||||
if (-not (Test-Path $cache)) {
|
||||
throw "No CMakeCache.txt in $BuildDir. Name the slicer build directory."
|
||||
}
|
||||
$entry = Select-String -Path $cache -Pattern '^CMAKE_CXX_COMPILER:\w+=(.+)$' | Select-Object -First 1
|
||||
if (-not $entry) {
|
||||
throw "No CMAKE_CXX_COMPILER in $cache."
|
||||
}
|
||||
$compiler = $entry.Matches[0].Groups[1].Value
|
||||
# A tree configured with a bare compiler name found it on PATH.
|
||||
if (-not (Split-Path $compiler)) {
|
||||
$found = Get-Command $compiler -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $found) {
|
||||
throw "$compiler, the compiler in $cache, is not on PATH."
|
||||
}
|
||||
$compiler = $found.Source
|
||||
}
|
||||
$pdbutil = Join-Path (Split-Path $compiler) 'llvm-pdbutil.exe'
|
||||
if (-not (Test-Path $pdbutil)) {
|
||||
throw "No llvm-pdbutil.exe next to $compiler. This check needs a build made with Clang."
|
||||
}
|
||||
|
||||
$Pdb = Split-PathList $Pdb
|
||||
if (-not $Pdb) {
|
||||
# OrcaSlicer.dll holds the slicer itself, so a check without it proves nothing.
|
||||
$pdbDir = Join-Path $BuildDir "src\$Config"
|
||||
if (-not (Test-Path (Join-Path $pdbDir 'OrcaSlicer.pdb'))) {
|
||||
throw "No OrcaSlicer.pdb in $pdbDir."
|
||||
}
|
||||
$Pdb = @(Get-ChildItem $pdbDir -Filter *.pdb | ForEach-Object FullName)
|
||||
}
|
||||
}
|
||||
|
||||
$catchable = [regex]'`_CT\?\?_R0\?A[UV](.+?)@@@8([^`]*)`'
|
||||
$tmp = [IO.Path]::GetTempFileName()
|
||||
$failed = $false
|
||||
$classes = 0
|
||||
try {
|
||||
foreach ($file in $(if ($Dump) { $Dump } else { $Pdb })) {
|
||||
$text = $file
|
||||
if (-not $Dump) {
|
||||
$run = Start-Process -FilePath $pdbutil -ArgumentList 'dump', '--publics', "`"$file`"" -NoNewWindow -Wait -PassThru -RedirectStandardOutput $tmp
|
||||
if ($run.ExitCode -ne 0) {
|
||||
throw "llvm-pdbutil failed on $file."
|
||||
}
|
||||
$text = $tmp
|
||||
}
|
||||
|
||||
$kinds = @{}
|
||||
foreach ($match in $catchable.Matches([IO.File]::ReadAllText((Resolve-Path -LiteralPath $text).Path))) {
|
||||
$class = $match.Groups[1].Value
|
||||
if (-not $kinds.ContainsKey($class)) { $kinds[$class] = @{} }
|
||||
$kinds[$class][$(if ($match.Groups[2].Value.StartsWith('??0')) { 'copy' } else { 'bitwise' })] = $true
|
||||
}
|
||||
|
||||
$broken = @($kinds.Keys | Where-Object { $kinds[$_].Count -eq 2 } | Sort-Object)
|
||||
"$(Split-Path -Leaf $file): $($kinds.Count) exception classes, $($broken.Count) with broken throw info"
|
||||
foreach ($class in $broken) { " $class" }
|
||||
if ($broken.Count -gt 0) { $failed = $true }
|
||||
$classes += $kinds.Count
|
||||
}
|
||||
} finally {
|
||||
Remove-Item $tmp -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# The slicer's PDBs always contain exception throw info, so finding none means the dump format or the PDBs changed.
|
||||
if ($classes -eq 0) {
|
||||
'No exception classes found. Check that llvm-pdbutil still prints public symbols in backticks.'
|
||||
$failed = $true
|
||||
}
|
||||
|
||||
exit [int]$failed
|
||||
@@ -301,6 +301,10 @@ $cases = @(
|
||||
Contains = @('Precompiled header: off') }
|
||||
@{ Name = 'the precompiled header is on unless asked'; Args = @('-s')
|
||||
NotContains = @('SLIC3R_PCH') }
|
||||
@{ Name = '-l turns the precompiled header off'; Args = @('-s', '-l')
|
||||
Contains = @('-DSLIC3R_PCH=OFF', 'Precompiled header: off') }
|
||||
@{ Name = 'a deps-only build says nothing about the precompiled header'; Args = @('-d', '-l')
|
||||
NotContains = @('Precompiled header') }
|
||||
@{ Name = '--no-pch works without a cache'; Args = @('-s', '--no-pch')
|
||||
NotContains = @('COMPILER_LAUNCHER') }
|
||||
@{ Name = 'the slicer build runs gettext'; Args = @('-s')
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Tests scripts/check_exception_throw_info.ps1 on canned llvm-pdbutil dumps.
|
||||
|
||||
.DESCRIPTION
|
||||
Each case runs the check in its own powershell -File process, as CI does, and asserts on the
|
||||
exit code and output. Dumps are written to a temporary folder, so no build is needed.
|
||||
|
||||
.EXAMPLE
|
||||
powershell -File scripts/test_check_exception_throw_info.ps1
|
||||
#>
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$check = Join-Path $PSScriptRoot 'check_exception_throw_info.ps1'
|
||||
$work = Join-Path ([IO.Path]::GetTempPath()) 'check_exception_throw_info_tests'
|
||||
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $work | Out-Null
|
||||
|
||||
function Write-Dump([string] $path, [string[]] $symbols) {
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $path) | Out-Null
|
||||
$i = 0
|
||||
$lines = @(' Public Symbols', '============================================================')
|
||||
foreach ($symbol in $symbols) {
|
||||
$i++
|
||||
$lines += " $i | S_PUB32 [size = 48] ``$symbol``", ' flags = none, addr = 0007:0000'
|
||||
}
|
||||
Set-Content -Path $path -Encoding ascii -Value $lines
|
||||
}
|
||||
|
||||
$bitwise = '_CT??_R0?AVfuture_error@std@@@840'
|
||||
$copy = '_CT??_R0?AVfuture_error@std@@@8??0future_error@std@@QEAA@AEBV01@@Z40'
|
||||
$trivial = '_CT??_R0?AUtag@@@81'
|
||||
Write-Dump "$work\broken.txt" @($bitwise, $copy, $trivial)
|
||||
Write-Dump "$work\clean.txt" @($copy, $trivial)
|
||||
Write-Dump "$work\empty.txt" @()
|
||||
Write-Dump "$work\a dir, with a comma\clean.txt" @($copy)
|
||||
|
||||
# A build directory whose compiler has an llvm-pdbutil beside it but no OrcaSlicer.pdb.
|
||||
# Nothing runs the stand-in; the check stops before it gets there.
|
||||
$build = "$work\build"
|
||||
New-Item -ItemType Directory -Force -Path "$build\bin", "$build\src\Release" | Out-Null
|
||||
Copy-Item "$env:SystemRoot\System32\where.exe" "$build\bin\llvm-pdbutil.exe"
|
||||
Set-Content -Path "$build\CMakeCache.txt" -Encoding ascii -Value "CMAKE_CXX_COMPILER:FILEPATH=$build\bin\clang-cl.exe"
|
||||
Set-Content -Path "$build\src\Release\orca-slicer.pdb" -Value ''
|
||||
$bare = "$work\bare"
|
||||
New-Item -ItemType Directory -Force -Path $bare | Out-Null
|
||||
Set-Content -Path "$bare\CMakeCache.txt" -Encoding ascii -Value 'CMAKE_CXX_COMPILER:STRING=no-such-clang-cl.exe'
|
||||
|
||||
$cases = @(
|
||||
@{ Name = 'a class with and without a copy constructor fails'; Args = @('-Dump', "$work\broken.txt")
|
||||
Exit = 1; Contains = @('1 with broken throw info', 'future_error@std') }
|
||||
@{ Name = 'copy constructors and trivially copyable classes pass'; Args = @('-Dump', "$work\clean.txt")
|
||||
Exit = 0; Contains = @('2 exception classes, 0 with broken throw info') }
|
||||
@{ Name = 'no exception classes at all fails'; Args = @('-Dump', "$work\empty.txt")
|
||||
Exit = 1; Contains = @('No exception classes found') }
|
||||
@{ Name = 'a comma-separated list checks each dump'; Args = @('-Dump', "$work\broken.txt,$work\clean.txt")
|
||||
Exit = 1; Contains = @('broken.txt: 2 exception classes', 'clean.txt: 2 exception classes') }
|
||||
@{ Name = 'a path with a comma stays whole'; Args = @('-Dump', "$work\a dir, with a comma\clean.txt")
|
||||
Exit = 0; Contains = @('clean.txt: 1 exception classes') }
|
||||
@{ Name = 'a build without OrcaSlicer.pdb fails'; Args = @('-BuildDir', $build)
|
||||
Exit = 1; Contains = @('No OrcaSlicer.pdb in') }
|
||||
@{ Name = 'a compiler that is not on PATH is named'; Args = @('-BuildDir', $bare)
|
||||
Exit = 1; Contains = @('no-such-clang-cl.exe, the compiler in') }
|
||||
@{ Name = 'a missing build directory is named'; Args = @('-BuildDir', "$work\nowhere")
|
||||
Exit = 1; Contains = @('No CMakeCache.txt in') }
|
||||
@{ Name = 'no build directory and no dump fails'; Args = @()
|
||||
Exit = 1; Contains = @('Name the slicer build directory') }
|
||||
)
|
||||
|
||||
$failed = 0
|
||||
foreach ($case in $cases) {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$output = (& powershell -NoProfile -File $check @($case.Args) 2>&1 | Out-String)
|
||||
$code = $LASTEXITCODE
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$problems = @()
|
||||
if ($code -ne $case.Exit) { $problems += "exit $code, expected $($case.Exit)" }
|
||||
foreach ($text in $case.Contains) {
|
||||
if (-not $output.Contains($text)) { $problems += "no '$text' in the output" }
|
||||
}
|
||||
if ($problems) {
|
||||
$failed++
|
||||
" FAIL $($case.Name)"
|
||||
$problems | ForEach-Object { " $_" }
|
||||
$output.TrimEnd() -split "`r?`n" | ForEach-Object { " | $_" }
|
||||
} else {
|
||||
" ok $($case.Name)"
|
||||
}
|
||||
}
|
||||
|
||||
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
|
||||
"$($cases.Count - $failed) passed, $failed failed"
|
||||
exit [int]($failed -gt 0)
|
||||
@@ -182,8 +182,8 @@ set(lisbslic3r_sources
|
||||
Fill/FillPlanePath.hpp
|
||||
Fill/FillRectilinear.cpp
|
||||
Fill/FillRectilinear.hpp
|
||||
Fill/FillTpmsAdaptive.cpp
|
||||
Fill/FillTpmsAdaptive.hpp
|
||||
Fill/AdaptiveInfillField.cpp
|
||||
Fill/AdaptiveInfillField.hpp
|
||||
Fill/FillTpmsD.cpp
|
||||
Fill/FillTpmsD.hpp
|
||||
Fill/FillTpmsFK.cpp
|
||||
|
||||
@@ -1432,7 +1432,7 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
|
||||
}
|
||||
if (!header_found) {
|
||||
// TRN %1% is the application name.
|
||||
std::string error_message = format(_u8L("Not a gcode file generated by %1%."), SLIC3R_APP_FULL_NAME);
|
||||
std::string error_message = format(_u8L("Not a G-code file generated by %1%."), SLIC3R_APP_FULL_NAME);
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << error_message;
|
||||
throw Slic3r::RuntimeError(error_message.c_str());
|
||||
}
|
||||
|
||||
+37
-37
@@ -1,4 +1,4 @@
|
||||
#include "FillTpmsAdaptive.hpp"
|
||||
#include "AdaptiveInfillField.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -146,11 +146,11 @@ void rasterize(const ExPolygons &expolygons, const Vec2d &origin, double cell, i
|
||||
}
|
||||
|
||||
// Scale of the pattern relative to the surface at a depth from 0 at the surface to 1 at the deepest point.
|
||||
double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth)
|
||||
double target_scale(double ratio, AdaptiveInfillGradient gradient, double depth)
|
||||
{
|
||||
switch (gradient) {
|
||||
case TpmsAdaptiveGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
|
||||
case TpmsAdaptiveGradient::Exponential: return std::pow(ratio, depth);
|
||||
case AdaptiveInfillGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
|
||||
case AdaptiveInfillGradient::Exponential: return std::pow(ratio, depth);
|
||||
default: return 1. + (ratio - 1.) * depth;
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth)
|
||||
// continuous level of the target of a depth.
|
||||
struct DensityLevels
|
||||
{
|
||||
DensityLevels(double ratio, double step, TpmsAdaptiveGradient gradient)
|
||||
DensityLevels(double ratio, double step, AdaptiveInfillGradient gradient)
|
||||
: ratio(ratio), gradient(gradient), count(int(std::ceil(std::abs(std::log(ratio)) / std::log(step) - EPSILON)))
|
||||
{}
|
||||
|
||||
@@ -170,7 +170,7 @@ struct DensityLevels
|
||||
}
|
||||
|
||||
double ratio;
|
||||
TpmsAdaptiveGradient gradient;
|
||||
AdaptiveInfillGradient gradient;
|
||||
int count;
|
||||
};
|
||||
|
||||
@@ -212,12 +212,12 @@ private:
|
||||
|
||||
} // namespace
|
||||
|
||||
TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
|
||||
AdaptiveInfillField::AdaptiveInfillField(const std::vector<Slice> &slices, const BoundingBox &bbox, AdaptiveInfillMode mode,
|
||||
const std::function<void()> &throw_if_canceled)
|
||||
: m_mode(mode)
|
||||
, m_axis(mode == TpmsAdaptiveMode::NormalX ? 0 : mode == TpmsAdaptiveMode::NormalY ? 1 : mode == TpmsAdaptiveMode::NormalZ ? 2 : -1)
|
||||
, m_axis(mode == AdaptiveInfillMode::NormalX ? 0 : mode == AdaptiveInfillMode::NormalY ? 1 : mode == AdaptiveInfillMode::NormalZ ? 2 : -1)
|
||||
{
|
||||
assert(!slices.empty() && mode != TpmsAdaptiveMode::Disabled);
|
||||
assert(!slices.empty() && mode != AdaptiveInfillMode::Disabled);
|
||||
const Vec3d min(unscaled(bbox.min.x()), unscaled(bbox.min.y()), slices.front().bottom_z);
|
||||
const Vec3d extent = Vec3d(unscaled(bbox.max.x()), unscaled(bbox.max.y()), slices.back().top_z) - min;
|
||||
|
||||
@@ -269,7 +269,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
|
||||
// Bodies are the connected inside nodes, none of which is on the border. The deepest nodes of a body are the
|
||||
// centers of its lobes, unless the depth between them stays above NeckRatio; where the depth ties, the center
|
||||
// is the node nearest to the middle of the tied nodes.
|
||||
const bool lobes = m_mode != TpmsAdaptiveMode::SteppedShells && m_mode != TpmsAdaptiveMode::SmoothBlend;
|
||||
const bool lobes = m_mode != AdaptiveInfillMode::SteppedShells && m_mode != AdaptiveInfillMode::SmoothBlend;
|
||||
m_body.assign(depth.size(), -1);
|
||||
std::vector<size_t> body_nodes;
|
||||
for (size_t seed = 0; seed < depth.size(); ++seed) {
|
||||
@@ -341,7 +341,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
|
||||
}
|
||||
throw_if_canceled();
|
||||
|
||||
if (m_mode == TpmsAdaptiveMode::SteppedShells || m_mode == TpmsAdaptiveMode::SmoothBlend || m_mode == TpmsAdaptiveMode::DistanceWarp) {
|
||||
if (m_mode == AdaptiveInfillMode::SteppedShells || m_mode == AdaptiveInfillMode::SmoothBlend || m_mode == AdaptiveInfillMode::DistanceWarp) {
|
||||
m_depth.assign(depth.size(), 0.f);
|
||||
for (size_t i = 0; i < depth.size(); ++i)
|
||||
if (m_body[i] >= 0)
|
||||
@@ -421,7 +421,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
|
||||
lobe.reach.resize(log_reach.size());
|
||||
std::transform(log_reach.begin(), log_reach.end(), lobe.reach.begin(), [](double v) { return float(std::exp(v)); });
|
||||
|
||||
if (m_mode == TpmsAdaptiveMode::DistanceWarp) {
|
||||
if (m_mode == AdaptiveInfillMode::DistanceWarp) {
|
||||
std::vector<double> mean(log_reach.size() * (Samples + 1));
|
||||
for (size_t d = 0; d < log_reach.size(); ++d) {
|
||||
const Vec3d dir = direction(int(d) / Azimuth, int(d) % Azimuth);
|
||||
@@ -488,7 +488,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
|
||||
}
|
||||
}
|
||||
|
||||
double TpmsRadialField::depth(const Vec3d &pt) const
|
||||
double AdaptiveInfillField::depth(const Vec3d &pt) const
|
||||
{
|
||||
assert(!m_depth.empty());
|
||||
const Vec3d f = (pt - m_origin) / m_cell;
|
||||
@@ -508,7 +508,7 @@ double TpmsRadialField::depth(const Vec3d &pt) const
|
||||
return value;
|
||||
}
|
||||
|
||||
Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d ¢er) const
|
||||
Vec3d AdaptiveInfillField::offset(const Vec3d &pt, const Vec3d ¢er) const
|
||||
{
|
||||
Vec3d d = pt - center;
|
||||
if (m_axis >= 0)
|
||||
@@ -516,7 +516,7 @@ Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d ¢er) const
|
||||
return d;
|
||||
}
|
||||
|
||||
double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const
|
||||
double AdaptiveInfillField::radial(const Lobe &lobe, const Vec3d &pt) const
|
||||
{
|
||||
const Vec3d d = this->offset(pt, lobe.center);
|
||||
const double r = d.norm();
|
||||
@@ -562,7 +562,7 @@ double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const
|
||||
return warp(mean);
|
||||
}
|
||||
|
||||
size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const
|
||||
size_t AdaptiveInfillField::radial(const Vec3d &pt, Radials &out) const
|
||||
{
|
||||
assert(!this->empty());
|
||||
Vec3i32 idx;
|
||||
@@ -588,7 +588,7 @@ size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const
|
||||
return count;
|
||||
}
|
||||
|
||||
size_t TpmsRadialField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const
|
||||
size_t AdaptiveInfillField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const
|
||||
{
|
||||
const Body &body = m_bodies[m_body[node]];
|
||||
if (body.lobes == 1) {
|
||||
@@ -641,7 +641,7 @@ struct AdaptiveTpmsField
|
||||
const long gsize = std::lround(gsizef / rsizef);
|
||||
|
||||
const AdaptiveTpms &tpms;
|
||||
const TpmsRadialField &radial_field;
|
||||
const AdaptiveInfillField &radial_field;
|
||||
RadialScale scale;
|
||||
DensityLevels levels;
|
||||
Point size;
|
||||
@@ -650,7 +650,7 @@ struct AdaptiveTpmsField
|
||||
double cos_angle;
|
||||
double sin_angle;
|
||||
|
||||
AdaptiveTpmsField(const AdaptiveTpms &tpms, const TpmsRadialField &radial_field, const BoundingBox &bbox, coordf_t z, float angle)
|
||||
AdaptiveTpmsField(const AdaptiveTpms &tpms, const AdaptiveInfillField &radial_field, const BoundingBox &bbox, coordf_t z, float angle)
|
||||
: tpms(tpms), radial_field(radial_field), scale(tpms, radial_field.axis() < 0 ? 3 : 2)
|
||||
, levels(std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3), BlendRatio, tpms.gradient)
|
||||
, size(bbox.size()), offs(bbox.min), z(z)
|
||||
@@ -666,7 +666,7 @@ struct AdaptiveTpmsField
|
||||
const double x = unscaled(pt.x());
|
||||
const double y = unscaled(pt.y());
|
||||
const Vec3d obj(cos_angle * x - sin_angle * y, sin_angle * x + cos_angle * y, z);
|
||||
if (radial_field.mode() == TpmsAdaptiveMode::SmoothBlend) {
|
||||
if (radial_field.mode() == AdaptiveInfillMode::SmoothBlend) {
|
||||
// The regular patterns of the two levels around the target of the depth, blended by a smoothstep.
|
||||
auto lattice = [this, x, y](int level) {
|
||||
const double frequency = tpms.surface_frequency * levels.scale(level);
|
||||
@@ -681,7 +681,7 @@ struct AdaptiveTpmsField
|
||||
return float((w < 1. ? (1. - w) * lattice(k) : 0.) + (w > 0. ? w * lattice(k + 1) : 0.));
|
||||
}
|
||||
const int axis = radial_field.axis();
|
||||
TpmsRadialField::Radials radials;
|
||||
AdaptiveInfillField::Radials radials;
|
||||
const size_t count = radial_field.radial(obj, radials);
|
||||
float value = 0.f;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
@@ -708,20 +708,20 @@ template<> struct _RasterTraits<AdaptiveTpmsField>
|
||||
};
|
||||
|
||||
// Continuous density level of the depth over a layer, in the object frame.
|
||||
struct TpmsLevelField
|
||||
struct AdaptiveLevelField
|
||||
{
|
||||
static constexpr float gsizef = 0.5f;
|
||||
static constexpr float rsizef = 0.05f;
|
||||
const coord_t rsize = scaled(rsizef);
|
||||
const long gsize = std::lround(gsizef / rsizef);
|
||||
|
||||
const TpmsRadialField &field;
|
||||
const AdaptiveInfillField &field;
|
||||
const DensityLevels &levels;
|
||||
Point size;
|
||||
Point offs;
|
||||
double z;
|
||||
|
||||
TpmsLevelField(const TpmsRadialField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
|
||||
AdaptiveLevelField(const AdaptiveInfillField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
|
||||
: field(field), levels(levels), size(bbox.size()), offs(bbox.min), z(z)
|
||||
{}
|
||||
|
||||
@@ -736,19 +736,19 @@ struct TpmsLevelField
|
||||
inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); }
|
||||
};
|
||||
|
||||
template<> struct _RasterTraits<TpmsLevelField>
|
||||
template<> struct _RasterTraits<AdaptiveLevelField>
|
||||
{
|
||||
using ValueType = float;
|
||||
static float get(const TpmsLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
|
||||
static size_t rows(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.y()); }
|
||||
static size_t cols(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.x()); }
|
||||
static float get(const AdaptiveLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
|
||||
static size_t rows(const AdaptiveLevelField &sf) { return sf.to_coordr(sf.size.y()); }
|
||||
static size_t cols(const AdaptiveLevelField &sf) { return sf.to_coordr(sf.size.x()); }
|
||||
};
|
||||
|
||||
} // namespace marchsq
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
|
||||
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField &field, BoundingBox bbox,
|
||||
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle)
|
||||
{
|
||||
// A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region.
|
||||
@@ -775,21 +775,21 @@ Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &fi
|
||||
return polylines;
|
||||
}
|
||||
|
||||
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
|
||||
float surface_density, float interior_density, TpmsAdaptiveGradient gradient)
|
||||
std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z,
|
||||
float surface_density, float interior_density, AdaptiveInfillGradient gradient)
|
||||
{
|
||||
const DensityLevels levels(interior_density / surface_density, ShellRatio, gradient);
|
||||
if (levels.count == 0)
|
||||
return {{surface_density, {expolygon}}};
|
||||
// A fixed sampling grid, so that every region of a layer gets the same shells.
|
||||
const coord_t cell = scaled(marchsq::TpmsLevelField::gsizef);
|
||||
const coord_t cell = scaled(marchsq::AdaptiveLevelField::gsizef);
|
||||
BoundingBox bbox = get_extents(expolygon);
|
||||
bbox.offset(cell);
|
||||
bbox.merge(align_to_grid(bbox.min, Point(cell, cell)));
|
||||
const marchsq::TpmsLevelField raster(field, levels, bbox, z);
|
||||
const marchsq::AdaptiveLevelField raster(field, levels, bbox, z);
|
||||
|
||||
// Each level takes the part deeper than the middle between it and the previous one.
|
||||
std::vector<TpmsShell> shells;
|
||||
std::vector<AdaptiveInfillShell> shells;
|
||||
ExPolygons remaining{expolygon};
|
||||
for (int level = 0; level < levels.count && !remaining.empty(); ++level) {
|
||||
Polygons deeper;
|
||||
@@ -808,12 +808,12 @@ std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPo
|
||||
return shells;
|
||||
}
|
||||
|
||||
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, coordf_t spacing,
|
||||
void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, coordf_t spacing,
|
||||
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell)
|
||||
{
|
||||
FillParams shell_params = params;
|
||||
shell_params.tpms_adaptive = TpmsAdaptiveMode::Disabled;
|
||||
for (const TpmsShell &shell : make_tpms_shells(field, expolygon, z, params.density, params.tpms_interior_density, params.tpms_adaptive_gradient)) {
|
||||
shell_params.adaptive_infill_mode = AdaptiveInfillMode::Disabled;
|
||||
for (const AdaptiveInfillShell &shell : make_adaptive_infill_shells(field, expolygon, z, params.density, params.adaptive_infill_interior_density, params.adaptive_infill_gradient)) {
|
||||
shell_params.density = shell.density;
|
||||
for (const ExPolygon &part : offset_ex(shell.expolygons, -float(scale_(0.5 * spacing))))
|
||||
fill_shell(shell_params, part);
|
||||
+12
-12
@@ -21,7 +21,7 @@ namespace Slic3r {
|
||||
// lobe, 1 at its surface. Lobes are parts of a body separated by a neck, like two spheres united. In the 2D modes,
|
||||
// every section normal to the axis has its own bodies and lobes, and distances are measured within the section. The
|
||||
// modes following the distance to the surface use the depth of every point instead; Distance warp also the lobes.
|
||||
class TpmsRadialField
|
||||
class AdaptiveInfillField
|
||||
{
|
||||
public:
|
||||
struct Slice
|
||||
@@ -39,7 +39,7 @@ public:
|
||||
};
|
||||
|
||||
// Slices sorted by z, in the XY coordinates of the fill and the print Z.
|
||||
TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
|
||||
AdaptiveInfillField(const std::vector<Slice> &slices, const BoundingBox &bbox, AdaptiveInfillMode mode,
|
||||
const std::function<void()> &throw_if_canceled);
|
||||
|
||||
// Lobes blended near the sides between them, from a body or from each of the two sections around a point.
|
||||
@@ -56,7 +56,7 @@ public:
|
||||
|
||||
// Axis normal to the sections in the 2D modes, -1 in the modes graded in 3D.
|
||||
int axis() const { return m_axis; }
|
||||
TpmsAdaptiveMode mode() const { return m_mode; }
|
||||
AdaptiveInfillMode mode() const { return m_mode; }
|
||||
|
||||
// In the modes following the distance to the surface: depth relative to the deepest point of the body, from 0 at
|
||||
// the surface to 1.
|
||||
@@ -91,7 +91,7 @@ private:
|
||||
static constexpr int Polar = 24;
|
||||
static constexpr int Azimuth = 48;
|
||||
|
||||
TpmsAdaptiveMode m_mode;
|
||||
AdaptiveInfillMode m_mode;
|
||||
int m_axis;
|
||||
Vec3d m_origin;
|
||||
double m_cell;
|
||||
@@ -106,9 +106,9 @@ private:
|
||||
std::vector<float> m_depth;
|
||||
};
|
||||
|
||||
using TpmsRadialFieldPtr = std::unique_ptr<TpmsRadialField>;
|
||||
using AdaptiveInfillFieldPtr = std::unique_ptr<AdaptiveInfillField>;
|
||||
// A field for every adaptive mode in use, indexed by the mode.
|
||||
using TpmsRadialFields = std::array<TpmsRadialFieldPtr, size_t(TpmsAdaptiveMode::Count)>;
|
||||
using AdaptiveInfillFields = std::array<AdaptiveInfillFieldPtr, size_t(AdaptiveInfillMode::Count)>;
|
||||
|
||||
struct AdaptiveTpms
|
||||
{
|
||||
@@ -117,14 +117,14 @@ struct AdaptiveTpms
|
||||
// Pattern frequencies at the surface and at the center, in radians per mm.
|
||||
double surface_frequency;
|
||||
double interior_frequency;
|
||||
TpmsAdaptiveGradient gradient;
|
||||
AdaptiveInfillGradient gradient;
|
||||
};
|
||||
|
||||
// Infill lines in the fill frame, the object frame rotated by -angle; z is the print_z of the layer.
|
||||
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
|
||||
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField &field, BoundingBox bbox,
|
||||
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle);
|
||||
|
||||
struct TpmsShell
|
||||
struct AdaptiveInfillShell
|
||||
{
|
||||
float density;
|
||||
ExPolygons expolygons;
|
||||
@@ -132,12 +132,12 @@ struct TpmsShell
|
||||
|
||||
// Stepped shells: the parts of an expolygon in the object frame at each density, from the surface inwards; z is the
|
||||
// middle of the layer.
|
||||
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
|
||||
float surface_density, float interior_density, TpmsAdaptiveGradient gradient);
|
||||
std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z,
|
||||
float surface_density, float interior_density, AdaptiveInfillGradient gradient);
|
||||
|
||||
// Stepped shells: fills every shell with fill_shell at its density, each shrunk by half a line like a filled region,
|
||||
// so the lines connected along the boundaries of two shells don't overlap.
|
||||
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, coordf_t spacing,
|
||||
void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, coordf_t spacing,
|
||||
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell);
|
||||
|
||||
} // namespace Slic3r
|
||||
+23
-23
@@ -312,9 +312,9 @@ struct SurfaceFillParams
|
||||
bool gyroid_optimized = false;
|
||||
|
||||
// For TPMS: grade the density from the surface to the interior of the object.
|
||||
TpmsAdaptiveMode tpms_adaptive = TpmsAdaptiveMode::Disabled;
|
||||
float tpms_interior_density = 0.f;
|
||||
TpmsAdaptiveGradient tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear;
|
||||
AdaptiveInfillMode adaptive_infill_mode = AdaptiveInfillMode::Disabled;
|
||||
float adaptive_infill_interior_density = 0.f;
|
||||
AdaptiveInfillGradient adaptive_infill_gradient = AdaptiveInfillGradient::Linear;
|
||||
|
||||
// Orca: corner smoothing factor in the range [0, 1].
|
||||
double smooth_factor { 0. };
|
||||
@@ -358,9 +358,9 @@ struct SurfaceFillParams
|
||||
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
|
||||
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
|
||||
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
|
||||
RETURN_COMPARE_NON_EQUAL(tpms_adaptive);
|
||||
RETURN_COMPARE_NON_EQUAL(tpms_interior_density);
|
||||
RETURN_COMPARE_NON_EQUAL(tpms_adaptive_gradient);
|
||||
RETURN_COMPARE_NON_EQUAL(adaptive_infill_mode);
|
||||
RETURN_COMPARE_NON_EQUAL(adaptive_infill_interior_density);
|
||||
RETURN_COMPARE_NON_EQUAL(adaptive_infill_gradient);
|
||||
RETURN_COMPARE_NON_EQUAL(smooth_factor);
|
||||
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
|
||||
RETURN_COMPARE_NON_EQUAL(separated_infills);
|
||||
@@ -394,9 +394,9 @@ struct SurfaceFillParams
|
||||
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
|
||||
this->separated_infills == rhs.separated_infills &&
|
||||
this->gyroid_optimized == rhs.gyroid_optimized &&
|
||||
this->tpms_adaptive == rhs.tpms_adaptive &&
|
||||
this->tpms_interior_density == rhs.tpms_interior_density &&
|
||||
this->tpms_adaptive_gradient == rhs.tpms_adaptive_gradient &&
|
||||
this->adaptive_infill_mode == rhs.adaptive_infill_mode &&
|
||||
this->adaptive_infill_interior_density == rhs.adaptive_infill_interior_density &&
|
||||
this->adaptive_infill_gradient == rhs.adaptive_infill_gradient &&
|
||||
this->smooth_factor == rhs.smooth_factor &&
|
||||
this->fill_order == rhs.fill_order;
|
||||
}
|
||||
@@ -1010,17 +1010,17 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
||||
params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface;
|
||||
|
||||
// Orca: only the TPMS sparse infill is graded; reset otherwise, as params is reused.
|
||||
params.tpms_adaptive = is_tpms_adaptive_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
|
||||
region_config.tpms_adaptive.value : TpmsAdaptiveMode::Disabled;
|
||||
const bool tpms_adaptive = params.tpms_adaptive != TpmsAdaptiveMode::Disabled;
|
||||
params.tpms_interior_density = tpms_adaptive ? std::max(1.f, float(region_config.tpms_interior_density)) : 0.f;
|
||||
params.tpms_adaptive_gradient = tpms_adaptive ? region_config.tpms_adaptive_gradient.value : TpmsAdaptiveGradient::Linear;
|
||||
params.adaptive_infill_mode = is_adaptive_infill_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
|
||||
region_config.adaptive_infill_mode.value : AdaptiveInfillMode::Disabled;
|
||||
const bool adaptive_infill_mode = params.adaptive_infill_mode != AdaptiveInfillMode::Disabled;
|
||||
params.adaptive_infill_interior_density = adaptive_infill_mode ? std::max(1.f, float(region_config.adaptive_infill_interior_density)) : 0.f;
|
||||
params.adaptive_infill_gradient = adaptive_infill_mode ? region_config.adaptive_infill_gradient.value : AdaptiveInfillGradient::Linear;
|
||||
|
||||
// Pass through gyroid_optimized only when the effective pattern is Gyroid,
|
||||
// so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag
|
||||
// (which would unnecessarily split fill batching). Adaptive density replaces it.
|
||||
// Stored on SurfaceFillParams; copied to FillParams during conversion.
|
||||
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !tpms_adaptive;
|
||||
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !adaptive_infill_mode;
|
||||
|
||||
if (params.extrusion_role == erInternalInfill) {
|
||||
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
|
||||
@@ -1373,7 +1373,7 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
|
||||
f->angle = surface_fill.params.angle;
|
||||
f->fixed_angle = surface_fill.params.fixed_angle;
|
||||
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
|
||||
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
|
||||
f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
|
||||
f->print_config = &this->object()->print()->config();
|
||||
f->print_object_config = &this->object()->config();
|
||||
if (surface_fill.params.pattern == ipConcentricInternal) {
|
||||
@@ -1423,9 +1423,9 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
|
||||
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
|
||||
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
|
||||
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
|
||||
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
|
||||
params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
|
||||
params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
|
||||
params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
|
||||
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||
|
||||
// BBS
|
||||
@@ -1598,7 +1598,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
|
||||
f->angle = surface_fill.params.angle;
|
||||
f->fixed_angle = surface_fill.params.fixed_angle;
|
||||
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
|
||||
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
|
||||
f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
|
||||
f->print_config = &this->object()->print()->config();
|
||||
f->print_object_config = &this->object()->config();
|
||||
|
||||
@@ -1638,9 +1638,9 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
|
||||
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
||||
params.multiline = surface_fill.params.multiline;
|
||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
|
||||
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
|
||||
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
|
||||
params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
|
||||
params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
|
||||
params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
|
||||
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||
// Orca: Match make_fills() when choosing the origin of plane-path patterns.
|
||||
// Without the sparse extrusion role, the filler uses each surface's bounds
|
||||
|
||||
+256
-13
@@ -11,6 +11,10 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <tuple>
|
||||
#include "../AABBTreeLines.hpp"
|
||||
#include "../ClipperUtils.hpp"
|
||||
#include "../EdgeGrid.hpp"
|
||||
#include "../Geometry.hpp"
|
||||
@@ -206,6 +210,9 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para
|
||||
thick_polylines.clear();
|
||||
}
|
||||
else {
|
||||
// Orca: sparse infill lines whose ends are next to each other print as one path, without a travel between them.
|
||||
if (params.extrusion_role == erInternalInfill && ! eec->no_sort)
|
||||
join_close_ends(polylines, scale_(2. * this->spacing), params.can_reverse);
|
||||
extrusion_entities_append_paths(
|
||||
eec->entities, std::move(polylines),
|
||||
params.extrusion_role,
|
||||
@@ -1644,6 +1651,8 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
};
|
||||
|
||||
const double line_half_width = 0.5 * scale_(spacing);
|
||||
// Two ends whose anchors would meet are joined instead, with about as much wall.
|
||||
const double join_length = anchor_length > SCALED_EPSILON ? std::max(anchor_length_max, 2. * (anchor_length + line_half_width)) : anchor_length_max;
|
||||
|
||||
#if 0
|
||||
// Connection from end of one infill line to the start of another infill line.
|
||||
@@ -1720,6 +1729,10 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
}
|
||||
#endif
|
||||
|
||||
// Line ends not yet joined to another line along the contour.
|
||||
std::vector<char> open_end(graph.map_infill_end_point_to_boundary.size(), 1);
|
||||
auto end_index = [&graph](const ContourIntersectionPoint *cp) { return size_t(cp - graph.map_infill_end_point_to_boundary.data()); };
|
||||
|
||||
struct Arc {
|
||||
ContourIntersectionPoint *intersection;
|
||||
double arc_length;
|
||||
@@ -1731,6 +1744,81 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (cp.contour_idx != boundary_idx_unconnected && cp.next_on_contour != &cp && cp.could_connect_next())
|
||||
arches.push_back({ &cp, path_length_along_contour_ccw(&cp, cp.next_on_contour, graph.boundary_params[cp.contour_idx].back()) });
|
||||
std::sort(arches.begin(), arches.end(), [](const auto& l, const auto& r) { return l.arc_length < r.arc_length; });
|
||||
|
||||
// Lines the arcs would join in this order.
|
||||
auto count_joins = [&](const std::vector<Arc> &order) {
|
||||
std::vector<char> consumed(graph.map_infill_end_point_to_boundary.size());
|
||||
for (size_t i = 0; i < consumed.size(); ++ i)
|
||||
consumed[i] = graph.map_infill_end_point_to_boundary[i].consumed;
|
||||
std::vector<size_t> parent(infill_ordered.size());
|
||||
std::iota(parent.begin(), parent.end(), 0);
|
||||
auto find = [&parent](size_t i) {
|
||||
while (parent[i] != i)
|
||||
i = parent[i] = parent[parent[i]];
|
||||
return i;
|
||||
};
|
||||
size_t joins = 0;
|
||||
for (const Arc &arc : order) {
|
||||
const size_t end1 = end_index(arc.intersection), end2 = end_index(arc.intersection->next_on_contour);
|
||||
if (consumed[end1] || consumed[end2] || (params.multiline > 1 && arc.arc_length < scale_(spacing) * params.multiline))
|
||||
continue;
|
||||
const size_t polyline1 = find(end1 / 2), polyline2 = find(end2 / 2);
|
||||
if (polyline1 == polyline2)
|
||||
continue;
|
||||
if (arc.arc_length < join_length) {
|
||||
parent[polyline2] = polyline1;
|
||||
++ joins;
|
||||
} else if (anchor_length <= SCALED_EPSILON)
|
||||
continue;
|
||||
consumed[end1] = consumed[end2] = 1;
|
||||
}
|
||||
// The last pass joins the open ends next to each other, also those of a multiline group.
|
||||
for (size_t end = 0; end < consumed.size(); ++ end) {
|
||||
const ContourIntersectionPoint &cp = graph.map_infill_end_point_to_boundary[end];
|
||||
if (consumed[end] || cp.contour_idx == boundary_idx_unconnected)
|
||||
continue;
|
||||
const double length = graph.boundary_params[cp.contour_idx].back();
|
||||
auto open = [&](const ContourIntersectionPoint *other, bool trimmed) {
|
||||
return other != &cp && ! consumed[end_index(other)] && ! trimmed;
|
||||
};
|
||||
const double lprev = open(cp.prev_on_contour, cp.prev_trimmed || cp.prev_on_contour->next_trimmed) ?
|
||||
path_length_along_contour_ccw(cp.prev_on_contour, &cp, length) : std::numeric_limits<double>::max();
|
||||
const double lnext = open(cp.next_on_contour, cp.next_trimmed || cp.next_on_contour->prev_trimmed) ?
|
||||
path_length_along_contour_ccw(&cp, cp.next_on_contour, length) : std::numeric_limits<double>::max();
|
||||
consumed[end] = anchor_length > SCALED_EPSILON;
|
||||
for (const double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
|
||||
if (l > join_length)
|
||||
break;
|
||||
const size_t other = end_index(l == lprev ? cp.prev_on_contour : cp.next_on_contour);
|
||||
const size_t polyline1 = find(end / 2), polyline2 = find(other / 2);
|
||||
if (polyline1 == polyline2)
|
||||
continue;
|
||||
parent[polyline2] = polyline1;
|
||||
consumed[end] = consumed[other] = 1;
|
||||
++ joins;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return joins;
|
||||
};
|
||||
// Equal arcs taken along the contour pair a wall in one phase; keep the order that joins the most lines.
|
||||
size_t best_joins = count_joins(arches);
|
||||
const double bucket = 0.5 * scale_(spacing);
|
||||
for (const bool backwards : { false, true }) {
|
||||
std::vector<Arc> along_contour = arches;
|
||||
std::sort(along_contour.begin(), along_contour.end(), [bucket, backwards](const Arc &l, const Arc &r) {
|
||||
const long long bucket_l = std::llround(l.arc_length / bucket), bucket_r = std::llround(r.arc_length / bucket);
|
||||
if (bucket_l != bucket_r)
|
||||
return bucket_l < bucket_r;
|
||||
if (l.intersection->contour_idx != r.intersection->contour_idx)
|
||||
return l.intersection->contour_idx < r.intersection->contour_idx;
|
||||
return backwards ? l.intersection->param > r.intersection->param : l.intersection->param < r.intersection->param;
|
||||
});
|
||||
if (const size_t joins = count_joins(along_contour); joins > best_joins) {
|
||||
best_joins = joins;
|
||||
arches = std::move(along_contour);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//FIXME improve the Traveling Salesman problem with 2-opt and 3-opt local optimization.
|
||||
@@ -1752,7 +1840,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (polyline_idx1 != polyline_idx2) {
|
||||
Polyline &polyline1 = infill_ordered[polyline_idx1];
|
||||
Polyline &polyline2 = infill_ordered[polyline_idx2];
|
||||
if (arc.arc_length < anchor_length_max) {
|
||||
if (arc.arc_length < join_length) {
|
||||
// Not closing a loop, connecting the lines.
|
||||
assert(contour[cp1->point_idx] == polyline1.points.front() || contour[cp1->point_idx] == polyline1.points.back());
|
||||
if (contour[cp1->point_idx] == polyline1.points.front())
|
||||
@@ -1761,6 +1849,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (contour[cp2->point_idx] == polyline2.points.back())
|
||||
polyline2.reverse();
|
||||
take(polyline1, polyline2, contour, cp1, cp2, false);
|
||||
open_end[end_index(cp1)] = open_end[end_index(cp2)] = 0;
|
||||
// Mark the second polygon as merged with the first one.
|
||||
if (polyline_idx2 < polyline_idx1) {
|
||||
polyline2 = std::move(polyline1);
|
||||
@@ -1778,6 +1867,23 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
}
|
||||
}
|
||||
|
||||
// Distance along the contour to the nearest open end of another polyline, within 64 ends.
|
||||
auto distance_to_open_end = [&](const ContourIntersectionPoint &from, size_t polyline_idx, bool backwards) {
|
||||
const double length = graph.boundary_params[from.contour_idx].back();
|
||||
double distance = 0.;
|
||||
const ContourIntersectionPoint *cp = &from;
|
||||
for (size_t step = 0; step < 64; ++ step) {
|
||||
const ContourIntersectionPoint *other = backwards ? cp->prev_on_contour : cp->next_on_contour;
|
||||
if (other == nullptr || other == &from)
|
||||
break;
|
||||
distance += backwards ? closed_contour_distance_cw(cp->param, other->param, length) : closed_contour_distance_ccw(cp->param, other->param, length);
|
||||
if (open_end[end_index(other)] && get_and_update_merged_with(end_index(other) / 2) != polyline_idx)
|
||||
return distance;
|
||||
cp = other;
|
||||
}
|
||||
return std::numeric_limits<double>::max();
|
||||
};
|
||||
|
||||
// Connect the remaining open infill lines to the perimeter lines if possible.
|
||||
for (ContourIntersectionPoint &contour_point : graph.map_infill_end_point_to_boundary)
|
||||
if (! contour_point.consumed && contour_point.contour_idx != boundary_idx_unconnected) {
|
||||
@@ -1796,7 +1902,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
assert(contour[contour_point.point_idx] == polyline.points.front() || contour[contour_point.point_idx] == polyline.points.back());
|
||||
bool connected = false;
|
||||
for (double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
|
||||
if (l == std::numeric_limits<double>::max() || l > anchor_length_max)
|
||||
if (l == std::numeric_limits<double>::max() || l > join_length)
|
||||
break;
|
||||
// Take the complete contour.
|
||||
bool reversed = l == lprev;
|
||||
@@ -1815,6 +1921,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (contour[cp2->point_idx] == polyline2.points.back())
|
||||
polyline2.reverse();
|
||||
take(polyline, polyline2, contour, &contour_point, cp2, reversed);
|
||||
open_end[end_index(&contour_point)] = open_end[end_index(cp2)] = 0;
|
||||
if (polyline_idx < polyline_idx2) {
|
||||
// Mark the second polyline as merged with the first one.
|
||||
merged_with[polyline_idx2] = polyline_idx;
|
||||
@@ -1829,18 +1936,35 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
break;
|
||||
}
|
||||
if (! connected && anchor_length > SCALED_EPSILON) {
|
||||
// Which to take? One could optimize for:
|
||||
// 1) Shortest path
|
||||
// 2) Hook length
|
||||
// ...
|
||||
// Let's take the longer now, as this improves the chance of another hook to be placed on the other side of this contour point.
|
||||
double l = std::max(contour_point.contour_not_taken_length_prev, contour_point.contour_not_taken_length_next);
|
||||
if (l > SCALED_EPSILON) {
|
||||
if (contour_point.contour_not_taken_length_prev > contour_point.contour_not_taken_length_next)
|
||||
take_limited(polyline, contour, contour_params, &contour_point, contour_point.prev_on_contour, true, anchor_length, line_half_width);
|
||||
else
|
||||
take_limited(polyline, contour, contour_params, &contour_point, contour_point.next_on_contour, false, anchor_length, line_half_width);
|
||||
// Hook toward the nearest open end of another polyline, where the next path likely starts.
|
||||
const double free_prev = contour_point.contour_not_taken_length_prev;
|
||||
const double free_next = contour_point.contour_not_taken_length_next;
|
||||
const double to_prev = distance_to_open_end(contour_point, polyline_idx, true);
|
||||
const double to_next = distance_to_open_end(contour_point, polyline_idx, false);
|
||||
bool backwards = free_prev > free_next;
|
||||
if (to_prev < to_next && free_prev > SCALED_EPSILON)
|
||||
backwards = true;
|
||||
else if (to_next < to_prev && free_next > SCALED_EPSILON)
|
||||
backwards = false;
|
||||
// Orca: with multiline, a hook toward a copy stops short of it; the copy takes half a width over the sine of its wall angle.
|
||||
double length = anchor_length;
|
||||
if (const ContourIntersectionPoint *other = backwards ? contour_point.prev_on_contour : contour_point.next_on_contour;
|
||||
params.multiline > 1 && other != &contour_point) {
|
||||
const Point &end = contour[contour_point.point_idx];
|
||||
Vec2d dir = Vec2d::Zero();
|
||||
for (size_t i = 1; i < polyline.size() && dir.squaredNorm() < sqr(2. * scale_(spacing) * params.multiline); ++ i)
|
||||
dir = (end - polyline.points[polyline.points.front() == end ? i : polyline.size() - 1 - i]).cast<double>();
|
||||
const Vec2d to_copy = (contour[other->point_idx] - end).cast<double>();
|
||||
const double across = dir.squaredNorm() > 0. ? std::abs(cross2(dir.normalized(), to_copy)) : 0.;
|
||||
if (across > SCALED_EPSILON && across < scale_(spacing) * params.multiline) {
|
||||
const double gap = backwards ? path_length_along_contour_ccw(other, &contour_point, contour_params.back()) :
|
||||
path_length_along_contour_ccw(&contour_point, other, contour_params.back());
|
||||
length = std::min(length, gap - line_half_width * (1. + to_copy.norm() / across));
|
||||
}
|
||||
}
|
||||
if ((backwards ? free_prev : free_next) > SCALED_EPSILON)
|
||||
take_limited(polyline, contour, contour_params, &contour_point, backwards ? contour_point.prev_on_contour : contour_point.next_on_contour,
|
||||
backwards, std::max(0., length), line_half_width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2798,4 +2922,123 @@ void multiline_fill(Polylines& polylines, const FillParams& params, float spacin
|
||||
polylines = std::move(all_polylines);
|
||||
}
|
||||
|
||||
void join_close_ends(Polylines &polylines, double max_gap, bool can_reverse)
|
||||
{
|
||||
const size_t n = polylines.size();
|
||||
if (n < 2 || max_gap <= 0.)
|
||||
return;
|
||||
static constexpr size_t npos = size_t(-1);
|
||||
// End 2 * i is the first point of polyline i, 2 * i + 1 its last.
|
||||
auto end_point = [&polylines](size_t e) -> const Point & { return (e & 1) ? polylines[e >> 1].points.back() : polylines[e >> 1].points.front(); };
|
||||
// Ends in cells of max_gap, so the ends within max_gap of one are in its cell or the eight around it.
|
||||
auto cell_of = [max_gap](const Point &p) { return Vec2i64(int64_t(std::floor(p.x() / max_gap)), int64_t(std::floor(p.y() / max_gap))); };
|
||||
auto key = [](const Vec2i64 &c) { return (uint64_t(c.x()) << 32) ^ uint64_t(uint32_t(c.y())); };
|
||||
std::unordered_map<uint64_t, std::vector<size_t>> grid;
|
||||
for (size_t e = 0; e < 2 * n; ++ e)
|
||||
if (polylines[e >> 1].size() >= 2)
|
||||
grid[key(cell_of(end_point(e)))].push_back(e);
|
||||
|
||||
// The end of a polyline that comes last along its overall direction, taken along the dominant axis.
|
||||
std::vector<char> backwards(n, 0);
|
||||
for (size_t i = 0; i < n; ++ i)
|
||||
if (polylines[i].size() >= 2) {
|
||||
const Vec2d dir = (polylines[i].points.back() - polylines[i].points.front()).cast<double>();
|
||||
backwards[i] = std::abs(dir.x()) >= std::abs(dir.y()) ? dir.x() < 0. : dir.y() < 0.;
|
||||
}
|
||||
auto trailing = [&backwards](size_t e) { return bool(e & 1) != bool(backwards[e >> 1]); };
|
||||
// Ends that meet first, then shortest gaps between leading ends, then the others: the copies of neighboring multiline
|
||||
// groups are all joined at the same side, which leaves their open ends next to each other rather than on alternating walls.
|
||||
std::vector<std::tuple<bool, bool, double, size_t, size_t>> gaps;
|
||||
for (const auto &[cell_key, ends] : grid) {
|
||||
const Vec2i64 c = cell_of(end_point(ends.front()));
|
||||
for (int64_t dx = -1; dx <= 1; ++ dx)
|
||||
for (int64_t dy = -1; dy <= 1; ++ dy)
|
||||
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
|
||||
for (size_t a : ends)
|
||||
for (size_t b : it->second)
|
||||
if (a < b && (a >> 1) != (b >> 1) && (can_reverse || (a & 1) != (b & 1)))
|
||||
if (const double d = (end_point(a) - end_point(b)).cast<double>().norm(); d <= max_gap)
|
||||
gaps.emplace_back(d > SCALED_EPSILON, d > SCALED_EPSILON && (trailing(a) || trailing(b)), d, a, b);
|
||||
}
|
||||
if (gaps.empty())
|
||||
return;
|
||||
std::sort(gaps.begin(), gaps.end());
|
||||
|
||||
std::vector<size_t> partner(2 * n, npos), parent(n);
|
||||
std::iota(parent.begin(), parent.end(), 0);
|
||||
auto find = [&parent](size_t i) {
|
||||
while (parent[i] != i)
|
||||
i = parent[i] = parent[parent[i]];
|
||||
return i;
|
||||
};
|
||||
// A join may not cross a line or another join; touching them at its own ends is fine, as where a line ends on another.
|
||||
// Only the segments through a cell next to a candidate join can cross one.
|
||||
std::unordered_set<uint64_t> near_gaps;
|
||||
for (const auto &[apart, trail, d, a, b] : gaps)
|
||||
for (size_t e : { a, b })
|
||||
for (int64_t dx = -1; dx <= 1; ++ dx)
|
||||
for (int64_t dy = -1; dy <= 1; ++ dy)
|
||||
near_gaps.insert(key(cell_of(end_point(e)) + Vec2i64(dx, dy)));
|
||||
Lines near_lines;
|
||||
for (const Polyline &pl : polylines)
|
||||
for (size_t i = 1; i < pl.size(); ++ i) {
|
||||
const Vec2i64 c0 = cell_of(pl.points[i - 1]), c1 = cell_of(pl.points[i]);
|
||||
const Vec2i64 lo = c0.cwiseMin(c1), hi = c0.cwiseMax(c1);
|
||||
bool near = (hi - lo).maxCoeff() > 3;
|
||||
for (int64_t x = lo.x(); x <= hi.x() && ! near; ++ x)
|
||||
for (int64_t y = lo.y(); y <= hi.y() && ! near; ++ y)
|
||||
near = near_gaps.count(key(Vec2i64(x, y))) > 0;
|
||||
if (near)
|
||||
near_lines.emplace_back(pl.points[i - 1], pl.points[i]);
|
||||
}
|
||||
const AABBTreeLines::LinesDistancer<Line> lines(std::move(near_lines));
|
||||
auto crosses = [&](size_t a, size_t b) {
|
||||
const Vec2d p = end_point(a).cast<double>(), q = end_point(b).cast<double>();
|
||||
const double length = (q - p).norm();
|
||||
if (length <= 2. * SCALED_EPSILON)
|
||||
return false;
|
||||
const Vec2d dir = (q - p) / length;
|
||||
const Line inner(Point((p + SCALED_EPSILON * dir).cast<coord_t>()), Point((q - SCALED_EPSILON * dir).cast<coord_t>()));
|
||||
if (! lines.get_lines().empty() && ! lines.intersections_with_line<false>(inner).empty())
|
||||
return true;
|
||||
const Vec2i64 c = cell_of(end_point(a));
|
||||
for (int64_t dx = -3; dx <= 3; ++ dx)
|
||||
for (int64_t dy = -3; dy <= 3; ++ dy)
|
||||
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
|
||||
for (size_t e : it->second)
|
||||
if (partner[e] != npos && Geometry::segments_intersect(inner.a, inner.b, end_point(e), end_point(partner[e])))
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
for (const auto &[apart, trail, d, a, b] : gaps)
|
||||
if (partner[a] == npos && partner[b] == npos && find(a >> 1) != find(b >> 1) && ! crosses(a, b)) {
|
||||
partner[a] = b;
|
||||
partner[b] = a;
|
||||
parent[find(a >> 1)] = find(b >> 1);
|
||||
}
|
||||
|
||||
// Walk each group from a polyline with a free end, entering the next polyline at the end joined to the last one.
|
||||
Polylines out;
|
||||
std::vector<char> done(n, 0);
|
||||
auto walk = [&](size_t first_end) {
|
||||
Polyline joined;
|
||||
for (size_t e = first_end; e != npos;) {
|
||||
Polyline &pl = polylines[e >> 1];
|
||||
done[e >> 1] = 1;
|
||||
if (e & 1)
|
||||
pl.reverse();
|
||||
joined.points.insert(joined.points.end(), pl.points.begin() + (! joined.empty() && joined.points.back() == pl.points.front()), pl.points.end());
|
||||
e = partner[e ^ 1];
|
||||
}
|
||||
out.emplace_back(std::move(joined));
|
||||
};
|
||||
for (size_t i = 0; i < n; ++ i)
|
||||
if (! done[i] && partner[2 * i] == npos)
|
||||
walk(2 * i);
|
||||
for (size_t i = 0; i < n; ++ i)
|
||||
if (! done[i] && partner[2 * i + 1] == npos)
|
||||
walk(2 * i + 1);
|
||||
polylines = std::move(out);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -33,7 +33,7 @@ namespace Slic3r { class ExtrusionEntityCollection; }
|
||||
namespace Slic3r {
|
||||
|
||||
class Surface;
|
||||
class TpmsRadialField;
|
||||
class AdaptiveInfillField;
|
||||
enum InfillPattern : int;
|
||||
|
||||
namespace FillAdaptive {
|
||||
@@ -93,9 +93,9 @@ struct FillParams
|
||||
bool gyroid_optimized { false };
|
||||
|
||||
// For TPMS: grade the density from the surface to the interior of the object. Density fraction.
|
||||
TpmsAdaptiveMode tpms_adaptive { TpmsAdaptiveMode::Disabled };
|
||||
float tpms_interior_density { 0.f };
|
||||
TpmsAdaptiveGradient tpms_adaptive_gradient { TpmsAdaptiveGradient::Linear };
|
||||
AdaptiveInfillMode adaptive_infill_mode { AdaptiveInfillMode::Disabled };
|
||||
float adaptive_infill_interior_density { 0.f };
|
||||
AdaptiveInfillGradient adaptive_infill_gradient { AdaptiveInfillGradient::Linear };
|
||||
|
||||
// Orca: corner smoothing factor in the range [0, 1].
|
||||
double smooth_factor { 0. };
|
||||
@@ -162,7 +162,7 @@ public:
|
||||
FillAdaptive::Octree* adapt_fill_octree = nullptr;
|
||||
|
||||
// Radial coordinate inside the object for the adaptive TPMS infill
|
||||
const TpmsRadialField* tpms_radial_field = nullptr;
|
||||
const AdaptiveInfillField* adaptive_infill_field = nullptr;
|
||||
|
||||
// PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring).
|
||||
// Orca: also used by gap fill function.
|
||||
@@ -258,8 +258,11 @@ public:
|
||||
|
||||
static coord_t _adjust_solid_spacing(const coord_t width, const coord_t distance);
|
||||
};
|
||||
//Fill Multiline
|
||||
//Fill Multiline
|
||||
void multiline_fill(Polylines& polylines, const FillParams& params, float spacing);
|
||||
// Joins polylines whose ends are at most max_gap apart, shortest gaps first and without closing a loop, so each such
|
||||
// group prints as one path. Without can_reverse only the end of one polyline joins the start of another.
|
||||
void join_close_ends(Polylines &polylines, double max_gap, bool can_reverse);
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_FillBase_hpp_
|
||||
@@ -18,7 +18,7 @@
|
||||
#include "FillGyroid.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "FillTpmsAdaptive.hpp"
|
||||
#include "AdaptiveInfillField.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -315,8 +315,8 @@ void FillGyroid::_fill_surface_single(
|
||||
ExPolygon expolygon,
|
||||
Polylines &polylines_out)
|
||||
{
|
||||
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
|
||||
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
|
||||
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
[&](const FillParams &shell_params, const ExPolygon &shell) {
|
||||
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
|
||||
});
|
||||
@@ -342,11 +342,11 @@ void FillGyroid::_fill_surface_single(
|
||||
|
||||
// generate pattern
|
||||
Polylines polylines;
|
||||
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
|
||||
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
|
||||
// Radians per mm of the regular pattern at a density.
|
||||
auto frequency = [¶ms, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
|
||||
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
|
||||
*this->tpms_radial_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
|
||||
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.adaptive_infill_interior_density), params.adaptive_infill_gradient},
|
||||
*this->adaptive_infill_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
|
||||
} else if (params.gyroid_optimized) {
|
||||
// Marching-squares path on the gyroid implicit field. Base period matches
|
||||
// the standard parametric path's wavelength: 2*pi * spacing / density_adj.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "FillTpmsD.hpp"
|
||||
#include "FillTpmsAdaptive.hpp"
|
||||
#include "AdaptiveInfillField.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -117,8 +117,8 @@ void FillTpmsD::_fill_surface_single(
|
||||
ExPolygon expolygon,
|
||||
Polylines &polylines_out)
|
||||
{
|
||||
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
|
||||
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
|
||||
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
[&](const FillParams &shell_params, const ExPolygon &shell) {
|
||||
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
|
||||
});
|
||||
@@ -130,13 +130,13 @@ void FillTpmsD::_fill_surface_single(
|
||||
expolygon.rotate(-infill_angle);
|
||||
|
||||
Polylines polylines;
|
||||
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
|
||||
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
|
||||
// Radians per mm of the regular pattern at a density.
|
||||
auto frequency = [¶ms, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
|
||||
BoundingBox bbox = expolygon.contour.bounding_box();
|
||||
bbox.offset(scale_((params.multiline + 1) * this->spacing));
|
||||
polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
|
||||
*this->tpms_radial_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
|
||||
polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.adaptive_infill_interior_density), params.adaptive_infill_gradient},
|
||||
*this->adaptive_infill_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
|
||||
} else {
|
||||
BoundingBox bb = expolygon.contour.bounding_box();
|
||||
// Density adjusted to have a good %of weight.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include "libslic3r/Fill/FillBase.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "FillTpmsFK.hpp"
|
||||
#include "FillTpmsAdaptive.hpp"
|
||||
#include "AdaptiveInfillField.hpp"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
@@ -132,8 +132,8 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
|
||||
ExPolygon expolygon,
|
||||
Polylines& polylines_out)
|
||||
{
|
||||
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
|
||||
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
|
||||
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
[&](const FillParams &shell_params, const ExPolygon &shell) {
|
||||
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
|
||||
});
|
||||
@@ -151,10 +151,10 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
|
||||
// Enlarge the bounding box by the multi-line width to avoid artifacts at the edges.
|
||||
bbox.offset(scale_((params.multiline + 1) * spacing));
|
||||
Polylines polylines;
|
||||
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
|
||||
polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.tpms_interior_density),
|
||||
params.tpms_adaptive_gradient},
|
||||
*this->tpms_radial_field, bbox, this->z, params.layer_height, spacing, infill_angle);
|
||||
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
|
||||
polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.adaptive_infill_interior_density),
|
||||
params.adaptive_infill_gradient},
|
||||
*this->adaptive_infill_field, bbox, this->z, params.layer_height, spacing, infill_angle);
|
||||
} else {
|
||||
marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, period(params.density));
|
||||
// Get simplified lines using coarse tolerance of 0.1mm (this is infill).
|
||||
|
||||
@@ -988,7 +988,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
MetadataList metadata;
|
||||
VolumeMetadataList volumes;
|
||||
bool auto_drop { true };
|
||||
std::optional<bool> auto_drop;
|
||||
};
|
||||
|
||||
struct CutObjectInfo
|
||||
@@ -1208,6 +1208,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
//IdToGeometryMap m_orig_geometries; // backup & restore
|
||||
CurrentConfig m_curr_config;
|
||||
IdToMetadataMap m_objects_metadata;
|
||||
std::set<int> m_items_auto_drop_off; // object ids with a build item auto_drop="0" (2.4.x layout)
|
||||
IdToCutObjectInfoMap m_cut_object_infos;
|
||||
IdToLayerHeightsProfileMap m_layer_heights_profiles;
|
||||
IdToLayerConfigRangesMap m_layer_config_ranges;
|
||||
@@ -1463,6 +1464,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
m_objects.clear();
|
||||
m_instances.clear();
|
||||
m_objects_metadata.clear();
|
||||
m_items_auto_drop_off.clear();
|
||||
m_curr_metadata_name.clear();
|
||||
m_curr_characters.clear();
|
||||
|
||||
@@ -1502,6 +1504,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
m_curr_config.object_id = -1;
|
||||
m_curr_config.volume_id = -1;
|
||||
m_objects_metadata.clear();
|
||||
m_items_auto_drop_off.clear();
|
||||
m_layer_heights_profiles.clear();
|
||||
m_layer_config_ranges.clear();
|
||||
m_brim_ear_points.clear();
|
||||
@@ -2257,11 +2260,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
ObjectMetadata::VolumeMetadataList volumes;
|
||||
ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr;
|
||||
|
||||
// 2.4.x stored auto_drop on build items, shared by all instances of an object; any "off" wins
|
||||
const bool legacy_auto_drop = !m_items_auto_drop_off.count(object.first.second);
|
||||
|
||||
IdToMetadataMap::iterator obj_metadata = m_objects_metadata.find(object.first.second);
|
||||
if (obj_metadata != m_objects_metadata.end()) {
|
||||
// config data has been found, this model was saved using slic3r pe
|
||||
|
||||
model_object->auto_drop = obj_metadata->second.auto_drop;
|
||||
model_object->auto_drop = obj_metadata->second.auto_drop.value_or(legacy_auto_drop);
|
||||
|
||||
// apply object's name and config data
|
||||
for (const Metadata& metadata : obj_metadata->second.metadata) {
|
||||
@@ -2279,6 +2285,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
}
|
||||
else {
|
||||
// config data not found, this model was not saved using slic3r pe
|
||||
model_object->auto_drop = legacy_auto_drop;
|
||||
|
||||
// add the entire geometry as the single volume to generate
|
||||
//volumes.emplace_back(0, (int)obj_geometry->second.triangles.size() - 1);
|
||||
@@ -4121,6 +4128,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
std::string path = bbs_get_attribute_value_string(attributes, num_attributes, PPATH_ATTR);
|
||||
Transform3d transform = bbs_get_transform_from_3mf_specs_string(bbs_get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR));
|
||||
int printable = bbs_get_attribute_value_bool(attributes, num_attributes, PRINTABLE_ATTR);
|
||||
if (const char* auto_drop = bbs_get_attribute_value_charptr(attributes, num_attributes, AUTO_DROP_ATTR); auto_drop != nullptr && !::atoi(auto_drop))
|
||||
m_items_auto_drop_off.insert(object_id);
|
||||
return !m_load_model || _create_object_instance(path, object_id, transform, printable, 1);
|
||||
}
|
||||
|
||||
@@ -4489,7 +4498,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
// int instances_count_id = bbs_get_attribute_value_int(attributes, num_attributes, INSTANCESCOUNT_ATTR);
|
||||
|
||||
ObjectMetadata metadata;
|
||||
metadata.auto_drop = bbs_get_attribute_value_bool(attributes, num_attributes, AUTO_DROP_ATTR);
|
||||
if (bbs_get_attribute_value_charptr(attributes, num_attributes, AUTO_DROP_ATTR) != nullptr)
|
||||
metadata.auto_drop = bbs_get_attribute_value_bool(attributes, num_attributes, AUTO_DROP_ATTR);
|
||||
m_objects_metadata.insert({ object_id, std::move(metadata) });
|
||||
m_curr_config.object_id = object_id;
|
||||
return true;
|
||||
@@ -10110,7 +10120,7 @@ bool to_xml(std::stringstream &stream, const EmbossShape::SvgFile &svg, const Mo
|
||||
assert(file_data != nullptr);
|
||||
if (file_data == nullptr && !svg.path.empty()) file_data = read_from_disk(svg.path);
|
||||
if (file_data == nullptr) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Can't write svg file no filedata";
|
||||
BOOST_LOG_TRIVIAL(warning) << "Can't write SVG file no filedata";
|
||||
return false;
|
||||
}
|
||||
const std::string &file_data_str = *file_data;
|
||||
@@ -10126,7 +10136,7 @@ void to_xml(std::stringstream &stream, const EmbossShape &es, const ModelVolume
|
||||
stream << " <" << SHAPE_TAG << " ";
|
||||
if (es.svg_file.has_value())
|
||||
if (!to_xml(stream, *es.svg_file, volume, archive, export_full_path)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Can't write svg file defiden embossed shape into 3mf";
|
||||
BOOST_LOG_TRIVIAL(warning) << "Can't write SVG file defiden embossed shape into 3mf";
|
||||
}
|
||||
|
||||
stream << SHAPE_SCALE_ATTR << "=\"" << es.scale << "\" ";
|
||||
|
||||
@@ -6660,10 +6660,6 @@ LayerResult GCode::process_layer(
|
||||
case CalibMode::Calib_Input_shaping_freq: {
|
||||
if (m_layer_index == 1){
|
||||
gcode += writer().set_input_shaping('A', print.calib_params().start, 0.f, print.calib_params().shaper_type);
|
||||
if (m_writer.get_gcode_flavor() == gcfKlipper) {
|
||||
// Disable minimum cruise ratio to ensure consistent motion for calibration
|
||||
gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n";
|
||||
}
|
||||
} else {
|
||||
if (print.calib_params().freqStartX == print.calib_params().freqStartY && print.calib_params().freqEndX == print.calib_params().freqEndY) {
|
||||
gcode += writer().set_input_shaping('A', 0.f, this->interpolate_value_across_layers(print.calib_params().freqStartX, print.calib_params().freqEndX), "");
|
||||
@@ -6676,10 +6672,6 @@ LayerResult GCode::process_layer(
|
||||
}
|
||||
case CalibMode::Calib_Input_shaping_damp: {
|
||||
if (m_layer_index == 1){
|
||||
if (m_writer.get_gcode_flavor() == gcfKlipper) {
|
||||
// Disable minimum cruise ratio to ensure consistent motion for calibration
|
||||
gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n";
|
||||
}
|
||||
gcode += writer().set_input_shaping('X', 0.f, print.calib_params().freqStartX, print.calib_params().shaper_type);
|
||||
gcode += writer().set_input_shaping('Y', 0.f, print.calib_params().freqStartY, print.calib_params().shaper_type);
|
||||
} else {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <boost/algorithm/string/classification.hpp>
|
||||
#include <boost/algorithm/string/constants.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
@@ -378,6 +379,11 @@ void GCodeProcessor::TimeMachine::reset()
|
||||
travel_acceleration = 0.0f;
|
||||
max_travel_acceleration = 0.0f;
|
||||
extrude_factor_override_percentage = 1.0f;
|
||||
klipper = false;
|
||||
minimum_cruise_ratio = 0.5f;
|
||||
requested_accel_to_decel = -1.0f;
|
||||
klipper_junction_flush = 1.0f;
|
||||
klipper_queue_priming = true;
|
||||
time = 0.0f;
|
||||
stop_times = std::vector<StopTime>();
|
||||
curr.reset();
|
||||
@@ -493,16 +499,87 @@ GCodeProcessor::TimeMachine::AdditionalBuffer GCodeProcessor::TimeMachine::merge
|
||||
return merged;
|
||||
}
|
||||
|
||||
// Klipper toolhead.py LookAheadQueue.flush: the pseudo-acceleration pass
|
||||
// reserves cruising distance across a sequence, not separately in each segment.
|
||||
size_t GCodeProcessor::TimeMachine::plan_klipper(bool lazy)
|
||||
{
|
||||
klipper_junction_flush = requested_accel_to_decel >= 0.0f ? 0.250f : 0.150f;
|
||||
klipper_queue_priming = false;
|
||||
size_t flush_count = blocks.size();
|
||||
bool update_flush_count = lazy;
|
||||
float next_start_v2 = 0.0f;
|
||||
float next_mcr_start_v2 = 0.0f;
|
||||
float peak_cruise_v2 = 0.0f;
|
||||
size_t pending = 0;
|
||||
for (size_t i = blocks.size(); i-- > 0;) {
|
||||
TimeBlock& block = blocks[i];
|
||||
const float delta_v2 = 2.0f * block.distance * block.acceleration;
|
||||
const float reachable_start_v2 = next_start_v2 + delta_v2;
|
||||
const float start_v2 = std::min(sqr(block.max_entry_speed), reachable_start_v2);
|
||||
const float reachable_mcr_v2 = next_mcr_start_v2 + block.mcr_delta_v2;
|
||||
const float mcr_start_v2 = std::min(block.max_mcr_entry_speed_sqr, reachable_mcr_v2);
|
||||
float cruise_v2 = -1.0f;
|
||||
++pending;
|
||||
if (mcr_start_v2 < reachable_mcr_v2) {
|
||||
if (mcr_start_v2 + block.mcr_delta_v2 > next_mcr_start_v2 || pending > 1) {
|
||||
// Stop at this block, not past it: the blocks above still have a cruise speed
|
||||
// that depends on moves not yet seen, so only [0, i) may be committed.
|
||||
if (update_flush_count && peak_cruise_v2 > 0.0f) {
|
||||
flush_count = i;
|
||||
update_flush_count = false;
|
||||
}
|
||||
peak_cruise_v2 = std::min(sqr(block.feedrate_profile.cruise),
|
||||
0.5f * (mcr_start_v2 + reachable_mcr_v2));
|
||||
}
|
||||
cruise_v2 = std::min({0.5f * (start_v2 + reachable_start_v2),
|
||||
sqr(block.feedrate_profile.cruise), peak_cruise_v2});
|
||||
pending = 0;
|
||||
}
|
||||
// Until the forward pass, these fields hold v^2, not speeds; -1 marks
|
||||
// an unassigned cruise speed. A lazy return leaves them in this state.
|
||||
block.feedrate_profile.entry = start_v2;
|
||||
block.feedrate_profile.exit = next_start_v2;
|
||||
block.trapezoid.cruise_feedrate = cruise_v2;
|
||||
block.peak_cruise_v2 = peak_cruise_v2;
|
||||
next_start_v2 = start_v2;
|
||||
next_mcr_start_v2 = mcr_start_v2;
|
||||
}
|
||||
if (update_flush_count)
|
||||
return 0;
|
||||
// A run of blocks that cannot accelerate is resolved here, once the peak cruise speed of the
|
||||
// accelerating block that precedes it is known. Each one takes a running minimum with its own
|
||||
// entry speed, seeded from that peak rather than from the setter's own clamped cruise speed.
|
||||
float prev_cruise_v2 = 0.0f;
|
||||
for (size_t i = 0; i < flush_count; ++i) {
|
||||
TimeBlock& block = blocks[i];
|
||||
float cruise_v2 = block.trapezoid.cruise_feedrate;
|
||||
if (cruise_v2 < 0.0f) {
|
||||
cruise_v2 = std::min(prev_cruise_v2, block.feedrate_profile.entry);
|
||||
prev_cruise_v2 = cruise_v2;
|
||||
} else
|
||||
prev_cruise_v2 = block.peak_cruise_v2;
|
||||
const float start_v2 = std::min(block.feedrate_profile.entry, cruise_v2);
|
||||
const float end_v2 = std::min(block.feedrate_profile.exit, cruise_v2);
|
||||
// A zero acceleration would put an infinity in the ramp distances; the Marlin helpers guard
|
||||
// their divides the same way and fall back to a pure cruise.
|
||||
const float inv_two_accel = block.acceleration > 0.0f ? 0.5f / block.acceleration : 0.0f;
|
||||
block.feedrate_profile.entry = std::sqrt(start_v2);
|
||||
block.feedrate_profile.exit = std::sqrt(end_v2);
|
||||
block.trapezoid.cruise_feedrate = std::sqrt(cruise_v2);
|
||||
block.trapezoid.accelerate_until = (cruise_v2 - start_v2) * inv_two_accel;
|
||||
block.trapezoid.decelerate_after = block.distance - (cruise_v2 - end_v2) * inv_two_accel;
|
||||
}
|
||||
return flush_count;
|
||||
}
|
||||
|
||||
void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, PrintEstimatedStatistics::ETimeMode mode, size_t keep_last_n_blocks, float additional_time, EMoveType target_move_type, bool is_final)
|
||||
{
|
||||
if (!enabled)
|
||||
return;
|
||||
// Orca: on the finalization pass, drain extra time that is still buffered (e.g. a
|
||||
// trailing filament change) even with fewer than two blocks queued -- no later pass
|
||||
// exists to attribute it on. Every other pass keeps the original >= 2 requirement,
|
||||
// and an empty buffer keeps today's early-return unchanged (no behavior change).
|
||||
// Drain buffered delays on the final pass, even with no motion left.
|
||||
// A trailing filament change has no later move to receive its time.
|
||||
const bool drain_final = is_final && !m_additional_time_buffer.empty();
|
||||
if (blocks.size() < 2 && !drain_final) {
|
||||
if ((klipper ? blocks.empty() : blocks.size() < 2) && !drain_final) {
|
||||
// Not enough blocks to attribute the extra time to yet; buffer it so it is
|
||||
// applied on a later pass instead of being dropped.
|
||||
if (additional_time > 0.0f)
|
||||
@@ -510,7 +587,7 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
||||
return;
|
||||
}
|
||||
|
||||
assert(keep_last_n_blocks <= blocks.size());
|
||||
assert(klipper || keep_last_n_blocks <= blocks.size());
|
||||
|
||||
// Merge any previously buffered extra time with this call's extra time. Each
|
||||
// entry is applied, in order, to the first block matching its target move type
|
||||
@@ -520,23 +597,36 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
||||
additional_buffer.emplace_back(target_move_type, additional_time);
|
||||
additional_buffer = merge_adjacent_additional_time_blocks(additional_buffer);
|
||||
|
||||
// reverse_pass
|
||||
for (int i = static_cast<int>(blocks.size()) - 1; i > 0; --i) {
|
||||
planner_reverse_pass_kernel(blocks[i - 1], blocks[i]);
|
||||
size_t n_blocks_process;
|
||||
if (klipper) {
|
||||
n_blocks_process = plan_klipper(keep_last_n_blocks != 0);
|
||||
keep_last_n_blocks = blocks.size() - n_blocks_process;
|
||||
} else {
|
||||
for (int i = static_cast<int>(blocks.size()) - 1; i > 0; --i)
|
||||
planner_reverse_pass_kernel(blocks[i - 1], blocks[i]);
|
||||
for (size_t i = 0; i + 1 < blocks.size(); ++i)
|
||||
planner_forward_pass_kernel(blocks[i], blocks[i + 1]);
|
||||
recalculate_trapezoids(blocks);
|
||||
n_blocks_process = blocks.size() - keep_last_n_blocks;
|
||||
}
|
||||
|
||||
// forward_pass
|
||||
for (size_t i = 0; i + 1 < blocks.size(); ++i) {
|
||||
planner_forward_pass_kernel(blocks[i], blocks[i + 1]);
|
||||
}
|
||||
|
||||
recalculate_trapezoids(blocks);
|
||||
|
||||
const size_t n_blocks_process = blocks.size() - keep_last_n_blocks;
|
||||
size_t additional_buffer_idx = 0;
|
||||
for (size_t i = 0; i < n_blocks_process; ++i) {
|
||||
const TimeBlock& block = blocks[i];
|
||||
float block_time = block.time();
|
||||
double motion_time;
|
||||
if (klipper && block.trapezoid.cruise_feedrate > 0.0f) {
|
||||
// Integrate using the planned speeds. Reconstructing the exit speed
|
||||
// from rounded deceleration distance loses precision near a stop.
|
||||
const double cruise = block.trapezoid.cruise_feedrate;
|
||||
motion_time = block.distance / cruise;
|
||||
if (block.acceleration > 0.0f) {
|
||||
const double accel_delta = cruise - block.feedrate_profile.entry;
|
||||
const double decel_delta = cruise - block.feedrate_profile.exit;
|
||||
motion_time += (sqr(accel_delta) + sqr(decel_delta)) / (2.0 * block.acceleration * cruise);
|
||||
}
|
||||
} else {
|
||||
motion_time = block.time();
|
||||
}
|
||||
double block_time = motion_time;
|
||||
if (additional_buffer_idx < additional_buffer.size()) {
|
||||
const EMoveType buf_move_type = additional_buffer[additional_buffer_idx].first;
|
||||
if (buf_move_type == EMoveType::Noop || buf_move_type == block.move_type) {
|
||||
@@ -545,13 +635,13 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
||||
}
|
||||
}
|
||||
|
||||
time += double(block_time);
|
||||
time += block_time;
|
||||
// Orca: accumulate per-SkipType time spent inside SKIPPABLE regions, folded into this single
|
||||
// calculate_time write site. Fires fleet-wide (the shipping time_lapse_gcode template stamps
|
||||
// blocks stTimelapse), but writes only skippable_part_time, which has no g-code-emitting
|
||||
// reader until the pre-heat injector consumes it, so output is byte-identical.
|
||||
if (block.skippable_type != SkipType::stNone)
|
||||
result.skippable_part_time[block.skippable_type] += block.time();
|
||||
result.skippable_part_time[block.skippable_type] += motion_time;
|
||||
result.moves[block.move_id].time[static_cast<size_t>(mode)] = block_time;
|
||||
gcode_time.cache += block_time;
|
||||
//BBS
|
||||
@@ -689,14 +779,12 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
||||
if (keep_last_n_blocks) {
|
||||
blocks.erase(blocks.begin(), blocks.begin() + n_blocks_process);
|
||||
|
||||
// Ensure that the new first block's entry speed will be preserved to prevent discontinuity
|
||||
// between the erased blocks' exit speed and the new first block's entry speed.
|
||||
// Otherwise, the first block's entry speed could be recalculated on the next pass without
|
||||
// considering that there are no more blocks before this first block. This could lead
|
||||
// to discontinuity between the exit speed (of already processed blocks) and the entry
|
||||
// speed of the first block.
|
||||
TimeBlock &first_block = blocks.front();
|
||||
first_block.max_entry_speed = first_block.feedrate_profile.entry;
|
||||
// Preserve the boundary speed in the Marlin planner. Klipper only flushes
|
||||
// moves whose cruise speed is already independent of future moves.
|
||||
if (!klipper) {
|
||||
TimeBlock& first_block = blocks.front();
|
||||
first_block.max_entry_speed = first_block.feedrate_profile.entry;
|
||||
}
|
||||
} else {
|
||||
blocks.clear();
|
||||
}
|
||||
@@ -3872,9 +3960,9 @@ void GCodeProcessor::finalize(bool post_process)
|
||||
update_slice_warnings();
|
||||
}
|
||||
|
||||
float GCodeProcessor::get_time(PrintEstimatedStatistics::ETimeMode mode) const
|
||||
double GCodeProcessor::get_time(PrintEstimatedStatistics::ETimeMode mode) const
|
||||
{
|
||||
return (mode < PrintEstimatedStatistics::ETimeMode::Count) ? float(m_time_processor.machines[static_cast<size_t>(mode)].time) : 0.0f;
|
||||
return (mode < PrintEstimatedStatistics::ETimeMode::Count) ? m_time_processor.machines[static_cast<size_t>(mode)].time : 0.0;
|
||||
}
|
||||
|
||||
float GCodeProcessor::get_prepare_time(PrintEstimatedStatistics::ETimeMode mode) const
|
||||
@@ -5031,6 +5119,14 @@ void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line, const std::o
|
||||
if (line.has_e()) g1_axes[E] = (double)line.e();
|
||||
std::optional<double> g1_feedrate = std::nullopt;
|
||||
if (line.has_f()) g1_feedrate = (double)line.f();
|
||||
if (m_flavor == gcfKlipper) {
|
||||
// Preserve decimal coordinates for short segments without parsing twice.
|
||||
for (unsigned char axis = X; axis <= E; ++axis)
|
||||
if (line.has(static_cast<Axis>(axis)))
|
||||
g1_axes[axis] = line.precise_value(static_cast<Axis>(axis));
|
||||
if (line.has_f())
|
||||
g1_feedrate = line.precise_value(F);
|
||||
}
|
||||
process_G1(g1_axes, g1_feedrate);
|
||||
}
|
||||
|
||||
@@ -5228,7 +5324,7 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
|
||||
//BBS: limite the cruise according to centripetal acceleration
|
||||
//Only need to handle when both prev and curr segment has movement in x-y plane
|
||||
if ((prev.exit_direction(0) != 0.0f || prev.exit_direction(1) != 0.0f) &&
|
||||
if (m_flavor != gcfKlipper && (prev.exit_direction(0) != 0.0f || prev.exit_direction(1) != 0.0f) &&
|
||||
(curr.enter_direction(0) != 0.0f || curr.enter_direction(1) != 0.0f)) {
|
||||
Vec3f v1 = prev.exit_direction;
|
||||
v1(2, 0) = 0.0f;
|
||||
@@ -5252,6 +5348,15 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
}
|
||||
}
|
||||
|
||||
if (m_flavor == gcfKlipper && !is_extrusion_only_move(delta_pos)) {
|
||||
const float max_velocity = get_axis_max_feedrate(static_cast<PrintEstimatedStatistics::ETimeMode>(i), X, m_machine_config_idx);
|
||||
if (max_velocity > 0.0f)
|
||||
curr.feedrate = std::min(curr.feedrate, max_velocity);
|
||||
}
|
||||
// Klipper applies E limits to retractions and moves without XY motion,
|
||||
// not to positive extrusion along a printing path.
|
||||
const bool limit_extruder = m_flavor != gcfKlipper || delta_pos[E] < 0.0 ||
|
||||
(delta_pos[X] == 0.0 && delta_pos[Y] == 0.0);
|
||||
// calculates block cruise feedrate
|
||||
float min_feedrate_factor = 1.0f;
|
||||
for (unsigned char a = X; a <= E; ++a) {
|
||||
@@ -5260,7 +5365,8 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
curr.axis_feedrate[a] *= machine.extrude_factor_override_percentage;
|
||||
|
||||
curr.abs_axis_feedrate[a] = std::abs(curr.axis_feedrate[a]);
|
||||
if (curr.abs_axis_feedrate[a] != 0.0f) {
|
||||
if (curr.abs_axis_feedrate[a] != 0.0f && (a != E || limit_extruder) &&
|
||||
(m_flavor != gcfKlipper || a == Z || a == E)) {
|
||||
float axis_max_feedrate = get_axis_max_feedrate(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a), m_machine_config_idx);
|
||||
if (axis_max_feedrate != 0.0f) min_feedrate_factor = std::min<float>(min_feedrate_factor, axis_max_feedrate / curr.abs_axis_feedrate[a]);
|
||||
}
|
||||
@@ -5277,20 +5383,42 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
}
|
||||
|
||||
// calculates block acceleration
|
||||
float acceleration =
|
||||
(type == EMoveType::Travel) ? get_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
|
||||
(is_extrusion_only_move(delta_pos) ?
|
||||
get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
|
||||
get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)));
|
||||
float acceleration;
|
||||
if (m_flavor == gcfKlipper && is_extrusion_only_move(delta_pos)) {
|
||||
// Klipper's max_extrude_only_accel. machine_max_acceleration_e is optional and some
|
||||
// shipped profiles leave it at 0, so keep the non-zero retract fallback.
|
||||
acceleration = get_axis_max_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), E, m_machine_config_idx);
|
||||
if (acceleration <= 0.0f)
|
||||
acceleration = get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i));
|
||||
} else
|
||||
acceleration =
|
||||
(m_flavor == gcfKlipper) ? get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
|
||||
(type == EMoveType::Travel) ? get_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
|
||||
(is_extrusion_only_move(delta_pos) ?
|
||||
get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
|
||||
get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)));
|
||||
|
||||
//BBS
|
||||
for (unsigned char a = X; a <= E; ++a) {
|
||||
if ((a == E && !limit_extruder) || (m_flavor == gcfKlipper && (a == X || a == Y)))
|
||||
continue;
|
||||
float axis_max_acceleration = get_axis_max_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a), m_machine_config_idx);
|
||||
// An unset (0) limit means "no limit", as in the feedrate loop above. Only the Klipper
|
||||
// planner divides by the block acceleration, so leave the Marlin path as it was.
|
||||
if (m_flavor == gcfKlipper && axis_max_acceleration <= 0.0f)
|
||||
continue;
|
||||
if (acceleration * std::abs(delta_pos[a]) * inv_distance > axis_max_acceleration)
|
||||
acceleration = axis_max_acceleration / (std::abs(delta_pos[a]) * inv_distance);
|
||||
}
|
||||
|
||||
block.acceleration = acceleration;
|
||||
if (m_flavor == gcfKlipper) {
|
||||
block.junction_deviation = get_junction_deviation(static_cast<PrintEstimatedStatistics::ETimeMode>(i), acceleration);
|
||||
const float cruise_acceleration = machine.requested_accel_to_decel >= 0.0f ?
|
||||
std::min(machine.acceleration, machine.requested_accel_to_decel) :
|
||||
machine.acceleration * (1.0f - machine.minimum_cruise_ratio);
|
||||
block.mcr_delta_v2 = 2.0f * distance * std::min(acceleration, cruise_acceleration);
|
||||
}
|
||||
|
||||
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
|
||||
const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
|
||||
@@ -5401,6 +5529,12 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
block.feedrate_profile.entry = std::min(vmax_junction, v_allowable);
|
||||
|
||||
block.max_entry_speed = vmax_junction;
|
||||
if (m_flavor == gcfKlipper && has_prev_move) {
|
||||
const TimeBlock& previous = blocks.back();
|
||||
block.max_entry_speed = std::min(block.max_entry_speed,
|
||||
std::sqrt(sqr(previous.max_entry_speed) + 2.0f * previous.distance * previous.acceleration));
|
||||
block.max_mcr_entry_speed_sqr = std::min(sqr(block.max_entry_speed), previous.max_mcr_entry_speed_sqr + previous.mcr_delta_v2);
|
||||
}
|
||||
block.flags.nominal_length = (block.feedrate_profile.cruise <= v_allowable);
|
||||
block.flags.recalculate = true;
|
||||
block.safe_feedrate = curr.safe_feedrate;
|
||||
@@ -5412,9 +5546,12 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
prev = curr;
|
||||
|
||||
blocks.push_back(block);
|
||||
if (m_flavor == gcfKlipper && blocks.size() > 1)
|
||||
machine.klipper_junction_flush -= distance / curr.feedrate;
|
||||
}
|
||||
|
||||
if (m_time_processor.machines[0].blocks.size() > TimeProcessor::Planner::refresh_threshold)
|
||||
if (m_flavor == gcfKlipper ? m_time_processor.machines[0].klipper_junction_flush <= 0.0f :
|
||||
m_time_processor.machines[0].blocks.size() > TimeProcessor::Planner::refresh_threshold)
|
||||
calculate_time(m_result, TimeProcessor::Planner::queue_size);
|
||||
|
||||
const Vec3f plate_offset = {(float) m_x_offset, (float) m_y_offset, 0.0f};
|
||||
@@ -6341,6 +6478,8 @@ void GCodeProcessor::process_M108(const GCodeReader::GCodeLine& line)
|
||||
|
||||
void GCodeProcessor::process_M109(const GCodeReader::GCodeLine& line)
|
||||
{
|
||||
if (m_flavor == gcfKlipper)
|
||||
simulate_st_synchronize();
|
||||
int filament_id = get_filament_id();
|
||||
float new_temp;
|
||||
if (line.has_value('R', new_temp)) {
|
||||
@@ -6403,6 +6542,8 @@ void GCodeProcessor::process_M140(const GCodeReader::GCodeLine& line)
|
||||
|
||||
void GCodeProcessor::process_M190(const GCodeReader::GCodeLine& line)
|
||||
{
|
||||
if (m_flavor == gcfKlipper)
|
||||
simulate_st_synchronize();
|
||||
float new_temp;
|
||||
if (line.has_value('S', new_temp))
|
||||
m_highest_bed_temp = m_highest_bed_temp < (int)new_temp ? (int)new_temp : m_highest_bed_temp;
|
||||
@@ -6480,6 +6621,21 @@ void GCodeProcessor::process_M203(const GCodeReader::GCodeLine& line)
|
||||
void GCodeProcessor::process_M204(const GCodeReader::GCodeLine& line)
|
||||
{
|
||||
float value;
|
||||
if (m_flavor == gcfKlipper) {
|
||||
if (!line.has_value('S', value)) {
|
||||
float travel;
|
||||
if (!line.has_value('P', value) || !line.has_value('T', travel))
|
||||
return;
|
||||
value = std::min(value, travel);
|
||||
}
|
||||
// Klipper replaces its single live max_accel for print and travel; the profile-clamping
|
||||
// setters would diverge from the firmware when custom G-code raises the limit.
|
||||
if (value > 0.0f) {
|
||||
for (auto& machine : m_time_processor.machines)
|
||||
machine.acceleration = machine.travel_acceleration = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
|
||||
if (static_cast<PrintEstimatedStatistics::ETimeMode>(i) == PrintEstimatedStatistics::ETimeMode::Normal ||
|
||||
m_time_processor.machine_envelope_processing_enabled) {
|
||||
@@ -6541,50 +6697,50 @@ void GCodeProcessor::process_M205(const GCodeReader::GCodeLine& line)
|
||||
|
||||
void GCodeProcessor::process_SET_VELOCITY_LIMIT(const GCodeReader::GCodeLine& line)
|
||||
{
|
||||
// handle SQUARE_CORNER_VELOCITY
|
||||
static const std::regex square_corner_velocity_pattern("\\sSQUARE_CORNER_VELOCITY\\s*=\\s*([0-9]*\\.*[0-9]*)");
|
||||
std::smatch matches;
|
||||
if (std::regex_search(line.raw(), matches, square_corner_velocity_pattern) && matches.size() == 2) {
|
||||
float _jerk = 0;
|
||||
try
|
||||
{
|
||||
_jerk = std::stof(matches[1]);
|
||||
static const std::regex parameter_pattern(
|
||||
R"(\s(VELOCITY|ACCEL|ACCEL_TO_DECEL|SQUARE_CORNER_VELOCITY|MINIMUM_CRUISE_RATIO)\s*=\s*([+]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?)(?=\s|;|$))",
|
||||
std::regex::icase);
|
||||
const std::string command = line.raw().substr(0, line.raw().find(';'));
|
||||
std::vector<std::pair<std::string, float>> parameters;
|
||||
for (std::sregex_iterator it(command.begin(), command.end(), parameter_pattern), end; it != end; ++it) {
|
||||
float value;
|
||||
try {
|
||||
value = std::stof((*it)[2].str());
|
||||
} catch (const std::exception&) {
|
||||
continue;
|
||||
}
|
||||
catch (...){}
|
||||
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_jerk_x, i, _jerk);
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_jerk_y, i, _jerk);
|
||||
if (std::isfinite(value))
|
||||
parameters.emplace_back(boost::to_upper_copy((*it)[1].str()), value);
|
||||
}
|
||||
// Klipper 0.13 converts ACCEL_TO_DECEL only when the line has no MINIMUM_CRUISE_RATIO, and later
|
||||
// releases ignore it, so a line carrying both (as Orca writes) is planned with the cruise ratio.
|
||||
const bool has_cruise_ratio = std::any_of(parameters.begin(), parameters.end(),
|
||||
[](const auto& parameter) { return parameter.first == "MINIMUM_CRUISE_RATIO" && parameter.second >= 0.0f && parameter.second < 1.0f; });
|
||||
for (const auto& [name, value] : parameters) {
|
||||
for (size_t i = 0; i < m_time_processor.machines.size(); ++i) {
|
||||
TimeMachine& machine = m_time_processor.machines[i];
|
||||
// Klipper replaces its single live max_accel for print and travel; the profile-clamping
|
||||
// setters would diverge from the firmware when custom G-code raises the limit.
|
||||
if (name == "ACCEL" && value > 0.0f)
|
||||
machine.acceleration = machine.travel_acceleration = value;
|
||||
else if (name == "MINIMUM_CRUISE_RATIO" && value >= 0.0f && value < 1.0f) {
|
||||
if (machine.klipper_queue_priming && machine.requested_accel_to_decel >= 0.0f)
|
||||
machine.klipper_junction_flush -= 1.0f;
|
||||
machine.minimum_cruise_ratio = value;
|
||||
machine.requested_accel_to_decel = -1.0f;
|
||||
} else if (name == "ACCEL_TO_DECEL" && value > 0.0f && !has_cruise_ratio) {
|
||||
if (machine.klipper_queue_priming && machine.requested_accel_to_decel < 0.0f)
|
||||
machine.klipper_junction_flush += 1.0f;
|
||||
machine.requested_accel_to_decel = value;
|
||||
} else if (name == "SQUARE_CORNER_VELOCITY" && value >= 0.0f) {
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_jerk_x, i, value);
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_jerk_y, i, value);
|
||||
} else if (name == "VELOCITY" && value > 0.0f) {
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_speed_x, i, value);
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_speed_y, i, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const std::regex accel_pattern("\\sACCEL\\s*=\\s*([0-9]*\\.*[0-9]*)");
|
||||
if (std::regex_search(line.raw(), matches, accel_pattern) && matches.size() == 2) {
|
||||
float _accl = 0;
|
||||
try
|
||||
{
|
||||
_accl = std::stof(matches[1]);
|
||||
}
|
||||
catch (...) {}
|
||||
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
|
||||
set_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), _accl);
|
||||
set_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), _accl);
|
||||
}
|
||||
}
|
||||
|
||||
static const std::regex velocity_pattern("\\sVELOCITY\\s*=\\s*([0-9]*\\.*[0-9]*)");
|
||||
if (std::regex_search(line.raw(), matches, velocity_pattern) && matches.size() == 2) {
|
||||
float _speed = 0;
|
||||
try
|
||||
{
|
||||
_speed = std::stof(matches[1]);
|
||||
}
|
||||
catch (...) {}
|
||||
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_speed_x, i, _speed);
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_speed_y, i, _speed);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GCodeProcessor::process_M221(const GCodeReader::GCodeLine& line)
|
||||
@@ -6621,14 +6777,15 @@ void GCodeProcessor::process_M400(const GCodeReader::GCodeLine& line)
|
||||
{
|
||||
float value_s = 0.0;
|
||||
float value_p = 0.0;
|
||||
if (line.has_value('S', value_s) || line.has_value('P', value_p)) {
|
||||
value_s += value_p * 0.001;
|
||||
// Skip post-print end-gcode dwells so they don't inflate the M73 estimate (see
|
||||
// m_skip_end_gcode_delays). Only omits dwell time — no state is updated here.
|
||||
if (m_skip_end_gcode_delays)
|
||||
return;
|
||||
simulate_st_synchronize(value_s);
|
||||
}
|
||||
const bool has_delay = line.has_value('S', value_s) || line.has_value('P', value_p);
|
||||
if (m_flavor != gcfKlipper && !has_delay)
|
||||
return;
|
||||
value_s += value_p * 0.001;
|
||||
// Skip post-print end-gcode dwells so they don't inflate the M73 estimate (see
|
||||
// m_skip_end_gcode_delays). Only omits dwell time — no state is updated here.
|
||||
if (m_skip_end_gcode_delays && has_delay)
|
||||
return;
|
||||
simulate_st_synchronize(value_s);
|
||||
}
|
||||
|
||||
void GCodeProcessor::process_M401(const GCodeReader::GCodeLine& line)
|
||||
@@ -7460,12 +7617,12 @@ float GCodeProcessor::get_junction_deviation(PrintEstimatedStatistics::ETimeMode
|
||||
{
|
||||
const size_t id = static_cast<size_t>(mode);
|
||||
|
||||
// Klipper has no classic jerk: jd = scv^2 * (sqrt(2) - 1) / max_accel
|
||||
// (toolhead.py::_calc_junction_deviation). Passing the block acceleration back in makes it cancel
|
||||
// in calc_vmax_junction_deviation(), leaving the identity v == scv at a 90 degree corner.
|
||||
// Klipper derives junction deviation from the live toolhead acceleration,
|
||||
// before a move is constrained by Z or extruder limits.
|
||||
if (m_flavor == gcfKlipper) {
|
||||
// machine_max_jerk_x holds the square corner velocity; process_SET_VELOCITY_LIMIT() writes it.
|
||||
const float scv = get_option_value(m_time_processor.machine_limits.machine_max_jerk_x, id);
|
||||
acceleration = m_time_processor.machines[id].acceleration;
|
||||
if (scv <= 0.0f || acceleration <= 0.0f)
|
||||
return 0.0f;
|
||||
return sqr(scv) * (std::sqrt(2.0f) - 1.0f) / acceleration;
|
||||
@@ -7497,18 +7654,45 @@ float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const
|
||||
const TimeMachine::State& curr, bool has_prev_move,
|
||||
PrintEstimatedStatistics::ETimeMode mode) const
|
||||
{
|
||||
if (m_flavor == gcfKlipper) {
|
||||
if (!has_prev_move || curr.enter_direction.isZero() || prev.exit_direction.isZero())
|
||||
return 0.0f;
|
||||
const TimeBlock& previous = m_time_processor.machines[static_cast<size_t>(mode)].blocks.back();
|
||||
double limit = std::min(sqr(double(block.feedrate_profile.cruise)), sqr(double(prev.feedrate)));
|
||||
const double extrude_ratio_change = std::abs(curr.axis_feedrate[E] / curr.feedrate -
|
||||
prev.axis_feedrate[E] / prev.feedrate);
|
||||
// Klipper's instantaneous_corner_velocity defaults to 1 mm/s.
|
||||
// It is independent of machine_max_jerk_e; custom firmware values
|
||||
// cannot be inferred from the slicer's jerk setting.
|
||||
if (extrude_ratio_change > 0.0f)
|
||||
limit = std::min(limit, sqr(1.0 / extrude_ratio_change));
|
||||
// Recover directions from the double-precision axis velocities: float
|
||||
// unit vectors lose the angle between nearly collinear short segments.
|
||||
const Vec3d prev_direction = Vec3d(prev.axis_feedrate[X], prev.axis_feedrate[Y], prev.axis_feedrate[Z]).normalized();
|
||||
const Vec3d curr_direction = Vec3d(curr.axis_feedrate[X], curr.axis_feedrate[Y], curr.axis_feedrate[Z]).normalized();
|
||||
const double cosine = std::clamp(-prev_direction.dot(curr_direction), -1.0, 1.0);
|
||||
const double sin_half = std::sqrt(0.5 * (1.0 - cosine));
|
||||
const double cos_half = std::sqrt(0.5 * (1.0 + cosine));
|
||||
if (sin_half < 1.0 && cos_half > 0.0) {
|
||||
const double radius = sin_half / (1.0 - sin_half);
|
||||
const double half_tan = 0.5 * sin_half / cos_half;
|
||||
limit = std::min({limit,
|
||||
radius * block.junction_deviation * block.acceleration,
|
||||
radius * previous.junction_deviation * previous.acceleration,
|
||||
half_tan * block.distance * block.acceleration,
|
||||
half_tan * previous.distance * previous.acceleration});
|
||||
}
|
||||
return std::sqrt(limit);
|
||||
}
|
||||
const float junction_deviation = get_junction_deviation(mode, block.acceleration);
|
||||
if (junction_deviation <= 0.0f)
|
||||
return -1.0f; // classic jerk machine, the caller keeps its own computation
|
||||
if (!has_prev_move)
|
||||
return 0.0f; // starts from rest, the planner raises this on the reverse pass
|
||||
|
||||
// -1 for a straight continuation, +1 for a full reversal. Half angle identity, no acos()/sin().
|
||||
// Both vectors are unit length over XYZE, so this really is a cosine: scaling by 1 / distance
|
||||
// instead, as PrusaSlicer does, leaves an E term that makes extruding corners look straighter
|
||||
// than they are. Marlin normalizes over XYZE for any extruding move (planner.cpp, esteps > 0)
|
||||
// and Klipper keeps E out of the cosine entirely (toolhead.py::Move.calc_junction); both agree
|
||||
// that the corner is planned by its geometry, and normalizing matches them to within 1e-5.
|
||||
// Unit vectors over XYZE keep this a cosine. Scaling by 1 / XYZ distance
|
||||
// leaves an E term that makes extruding corners appear straighter.
|
||||
// Marlin normalizes over XYZE; the Klipper branch above uses XYZ only.
|
||||
float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec);
|
||||
if (junction_cos_theta > 0.999999f)
|
||||
return 0.0f; // the path doubles back, the machine has to stop
|
||||
@@ -7524,8 +7708,8 @@ float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const
|
||||
float vmax_junction_sqr = (junction_acceleration * junction_deviation * sin_theta_d2) / (1.0f - sin_theta_d2);
|
||||
|
||||
// Marlin's JD_HANDLE_SMALL_SEGMENTS: a short move through a shallow corner is treated as an arc and
|
||||
// capped by the centripetal acceleration it needs. Klipper has no equivalent.
|
||||
if (m_flavor != gcfKlipper && block.distance < 1.0f && junction_cos_theta < -0.7071067812f) {
|
||||
// capped by the centripetal acceleration it needs.
|
||||
if (block.distance < 1.0f && junction_cos_theta < -0.7071067812f) {
|
||||
// Fast acos(-t), max. error +-0.033rad. MinMax polynomial by W. Randolph Franklin:
|
||||
// https://wrf.ecse.rpi.edu/Research/Short_Notes/arcsin/onlyelem.html
|
||||
const float neg = junction_cos_theta < 0.0f ? -1.0f : 1.0f;
|
||||
@@ -7700,6 +7884,7 @@ void GCodeProcessor::calculate_time(GCodeProcessorResult& result, size_t keep_la
|
||||
std::vector<TimeMachine::ActualSpeedMove> actual_speed_moves;
|
||||
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
|
||||
TimeMachine& machine = m_time_processor.machines[i];
|
||||
machine.klipper = m_flavor == gcfKlipper;
|
||||
machine.calculate_time(m_result, static_cast<PrintEstimatedStatistics::ETimeMode>(i), keep_last_n_blocks, additional_time, target_move_type, is_final);
|
||||
if (static_cast<PrintEstimatedStatistics::ETimeMode>(i) == PrintEstimatedStatistics::ETimeMode::Normal)
|
||||
actual_speed_moves = std::move(machine.actual_speed_moves);
|
||||
@@ -7778,6 +7963,14 @@ void GCodeProcessor::calculate_time(GCodeProcessorResult& result, size_t keep_la
|
||||
void GCodeProcessor::simulate_st_synchronize(float additional_time, EMoveType target_move_type)
|
||||
{
|
||||
calculate_time(m_result, 0, additional_time, target_move_type);
|
||||
if (m_flavor == gcfKlipper) {
|
||||
// ToolHead overrides the normal look-ahead window with BUFFER_TIME_HIGH
|
||||
// at startup and after synchronization: 2 s on legacy, 1 s on modern Klipper.
|
||||
for (TimeMachine& machine : m_time_processor.machines) {
|
||||
machine.klipper_junction_flush = machine.requested_accel_to_decel >= 0.0f ? 2.0f : 1.0f;
|
||||
machine.klipper_queue_priming = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GCodeProcessor::update_estimated_times_stats()
|
||||
@@ -7812,7 +8005,7 @@ double GCodeProcessor::extract_absolute_position_on_axis(Axis axis, const GCodeR
|
||||
is_relative |= (m_e_local_positioning_type == EPositioningType::Relative);
|
||||
|
||||
const double lengthsScaleFactor = (m_units == EUnits::Inches) ? double(INCHES_TO_MM) : 1.0;
|
||||
double ret = line.value(Slic3r::Axis(axis)) * lengthsScaleFactor;
|
||||
double ret = line.precise_value(Slic3r::Axis(axis)) * lengthsScaleFactor;
|
||||
// if (axis == E && m_use_volumetric_e)
|
||||
// ret /= area_filament_cross_section;
|
||||
return is_relative ? m_start_position[axis] + ret : m_origin[axis] + ret;
|
||||
|
||||
@@ -91,12 +91,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
|
||||
struct Mode
|
||||
{
|
||||
float time;
|
||||
double time;
|
||||
float prepare_time;
|
||||
std::vector<std::pair<CustomGCode::Type, std::pair<float, float>>> custom_gcode_times;
|
||||
|
||||
void reset() {
|
||||
time = 0.0f;
|
||||
time = 0.0;
|
||||
prepare_time = 0.0f;
|
||||
custom_gcode_times.clear();
|
||||
custom_gcode_times.shrink_to_fit();
|
||||
@@ -703,6 +703,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
float acceleration{ 0.0f }; // mm/s^2
|
||||
float max_entry_speed{ 0.0f }; // mm/s
|
||||
float safe_feedrate{ 0.0f }; // mm/s
|
||||
float junction_deviation{ 0.0f };
|
||||
float max_mcr_entry_speed_sqr{ 0.0f };
|
||||
float mcr_delta_v2{ 0.0f };
|
||||
// Peak cruise speed squared in effect when this block was planned. Only set on a block
|
||||
// that can accelerate; the delayed blocks after it are seeded from its value.
|
||||
float peak_cruise_v2{ 0.0f };
|
||||
Flags flags;
|
||||
FeedrateProfile feedrate_profile;
|
||||
Trapezoid trapezoid;
|
||||
@@ -782,6 +788,11 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
// hard limit for the travel acceleration, to which the firmware will clamp.
|
||||
float max_travel_acceleration; // mm/s^2
|
||||
float extrude_factor_override_percentage;
|
||||
bool klipper{ false };
|
||||
float minimum_cruise_ratio{ 0.5f };
|
||||
float requested_accel_to_decel{ -1.0f };
|
||||
float klipper_junction_flush{ 1.0f };
|
||||
bool klipper_queue_priming{ true };
|
||||
// We accumulate total print time in doubles to reduce the loss of precision
|
||||
// while adding big floating numbers with small float numbers.
|
||||
double time; // s
|
||||
@@ -818,6 +829,8 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
// Merge adjacent buffer entries that target the same move type.
|
||||
static AdditionalBuffer merge_adjacent_additional_time_blocks(const AdditionalBuffer& buffer);
|
||||
|
||||
size_t plan_klipper(bool lazy);
|
||||
|
||||
// additional_time is attributed to the first block matching target_move_type
|
||||
// (EMoveType::Noop matches any block, i.e. the first processed block).
|
||||
void calculate_time(GCodeProcessorResult& result, PrintEstimatedStatistics::ETimeMode mode, size_t keep_last_n_blocks = 0, float additional_time = 0.0f, EMoveType target_move_type = EMoveType::Noop, bool is_final = false);
|
||||
@@ -1383,7 +1396,7 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
void process_buffer(const std::string& buffer);
|
||||
void finalize(bool post_process);
|
||||
|
||||
float get_time(PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
double get_time(PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
float get_prepare_time(PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
std::string get_time_dhm(PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
std::vector<std::pair<CustomGCode::Type, std::pair<float, float>>> get_custom_gcode_times(PrintEstimatedStatistics::ETimeMode mode, bool include_remaining) const;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -153,6 +154,41 @@ bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
|
||||
return improved;
|
||||
}
|
||||
|
||||
std::vector<size_t> tsp_exact_cycle(const Points& centers)
|
||||
{
|
||||
const size_t n = centers.size();
|
||||
std::vector<size_t> path(n);
|
||||
std::iota(path.begin(), path.end(), 0);
|
||||
if (n <= 3) return path;
|
||||
// State mask * m + j: the points 1..m of mask visited after point 0, the last one j + 1.
|
||||
const size_t m = n - 1;
|
||||
assert(m <= 16);
|
||||
std::vector<double> cost((size_t(1) << m) * m, std::numeric_limits<double>::max());
|
||||
std::vector<size_t> from(cost.size(), size_t(-1));
|
||||
auto dist = [¢ers](size_t a, size_t b) { return (centers[a].cast<double>() - centers[b].cast<double>()).norm(); };
|
||||
for (size_t j = 0; j < m; ++j)
|
||||
cost[(size_t(1) << j) * m + j] = dist(0, j + 1);
|
||||
for (size_t mask = 1; mask < (size_t(1) << m); ++mask)
|
||||
for (size_t j = 0; j < m; ++j)
|
||||
if (const double c = cost[mask * m + j]; c < std::numeric_limits<double>::max())
|
||||
for (size_t k = 0; k < m; ++k)
|
||||
if (!(mask >> k & 1)) {
|
||||
const size_t next = (mask | size_t(1) << k) * m + k;
|
||||
if (const double nc = c + dist(j + 1, k + 1); nc < cost[next]) {
|
||||
cost[next] = nc;
|
||||
from[next] = mask * m + j;
|
||||
}
|
||||
}
|
||||
const size_t full = (size_t(1) << m) - 1;
|
||||
size_t best = full * m;
|
||||
for (size_t j = 1; j < m; ++j)
|
||||
if (cost[full * m + j] + dist(j + 1, 0) < cost[best] + dist(best % m + 1, 0))
|
||||
best = full * m + j;
|
||||
for (size_t s = best, k = n; k-- > 1; s = from[s])
|
||||
path[k] = s % m + 1;
|
||||
return path;
|
||||
}
|
||||
|
||||
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers)
|
||||
{
|
||||
size_t pn = path.size();
|
||||
|
||||
@@ -29,6 +29,9 @@ bool tsp_2opt_improve(std::vector<size_t>& path, const Points& centers, int max_
|
||||
// Returns true if any crossing was removed.
|
||||
bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers);
|
||||
|
||||
// The shortest cycle through the points, starting at point 0, by dynamic programming over subsets; for a few points only.
|
||||
std::vector<size_t> tsp_exact_cycle(const Points& centers);
|
||||
|
||||
// Rotate the cycle so the closing edge (last -> first) is minimized.
|
||||
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "libslic3r/Line.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "libslic3r/GCodeWriter.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
@@ -1528,8 +1529,10 @@ private:
|
||||
gcode << "M204 P" << acceleration;
|
||||
}
|
||||
else if (m_gcode_flavor == gcfKlipper && m_accel_to_decel_enable) {
|
||||
gcode << "SET_VELOCITY_LIMIT ACCEL_TO_DECEL=" << acceleration * m_accel_to_decel_factor / 100;
|
||||
gcode << "\nM204 S" << acceleration;
|
||||
// One line, so Klipper derives the limit from this acceleration rather than the previous one.
|
||||
std::string limits = "SET_VELOCITY_LIMIT ACCEL=" + std::to_string(acceleration);
|
||||
GCodeWriter::append_klipper_accel_to_decel(limits, acceleration, m_accel_to_decel_factor);
|
||||
gcode << limits;
|
||||
}
|
||||
else {
|
||||
// M204: Set default acceleration
|
||||
|
||||
@@ -95,7 +95,7 @@ const char* GCodeReader::parse_line_internal(const char *ptr, const char *end, G
|
||||
if (pend != c && is_end_of_word(*pend)) {
|
||||
// The axis value has been parsed correctly.
|
||||
if (axis != UNKNOWN_AXIS)
|
||||
gline.m_axis[int(axis)] = float(v);
|
||||
gline.m_axis[int(axis)] = v;
|
||||
gline.m_mask |= 1 << int(axis);
|
||||
c = pend;
|
||||
} else
|
||||
|
||||
@@ -35,7 +35,8 @@ public:
|
||||
std::string_view axis_pos(char axis) const;
|
||||
void clear() { m_raw.clear(); }
|
||||
bool has(Axis axis) const { return (m_mask & (1 << int(axis))) != 0; }
|
||||
float value(Axis axis) const { return m_axis[axis]; }
|
||||
float value(Axis axis) const { return float(m_axis[axis]); }
|
||||
double precise_value(Axis axis) const { return m_axis[axis]; }
|
||||
bool has(char axis) const;
|
||||
bool has_value(char axis, float &value) const;
|
||||
// Parse value of an axis from raw string starting at axis_pos.
|
||||
@@ -72,15 +73,15 @@ public:
|
||||
bool has_p() const { return this->has(P); }
|
||||
|
||||
bool has_unknown_axis() const { return this->has(UNKNOWN_AXIS); }
|
||||
float x() const { return m_axis[X]; }
|
||||
float y() const { return m_axis[Y]; }
|
||||
float z() const { return m_axis[Z]; }
|
||||
float e() const { return m_axis[E]; }
|
||||
float f() const { return m_axis[F]; }
|
||||
float x() const { return float(m_axis[X]); }
|
||||
float y() const { return float(m_axis[Y]); }
|
||||
float z() const { return float(m_axis[Z]); }
|
||||
float e() const { return float(m_axis[E]); }
|
||||
float f() const { return float(m_axis[F]); }
|
||||
// BBS: add I J P axis
|
||||
float i() const { return m_axis[I]; }
|
||||
float j() const { return m_axis[J]; }
|
||||
float p() const { return m_axis[P]; }
|
||||
float i() const { return float(m_axis[I]); }
|
||||
float j() const { return float(m_axis[J]); }
|
||||
float p() const { return float(m_axis[P]); }
|
||||
|
||||
static bool cmd_is(const std::string &gcode_line, const char *cmd_test) {
|
||||
const char *cmd = GCodeReader::skip_whitespaces(gcode_line.c_str());
|
||||
@@ -100,7 +101,7 @@ public:
|
||||
}
|
||||
private:
|
||||
std::string m_raw;
|
||||
float m_axis[NUM_AXES];
|
||||
double m_axis[NUM_AXES];
|
||||
uint32_t m_mask;
|
||||
friend class GCodeReader;
|
||||
};
|
||||
|
||||
@@ -453,12 +453,11 @@ std::string GCodeWriter::set_acceleration_internal(Acceleration type, unsigned i
|
||||
gcode += separate_travel ? "M204 T" : "M204 P";
|
||||
gcode += value;
|
||||
} else if (FLAVOR_IS(gcfKlipper)) {
|
||||
gcode.reserve(96);
|
||||
gcode.reserve(128);
|
||||
gcode += "SET_VELOCITY_LIMIT ACCEL=";
|
||||
gcode += value;
|
||||
if (this->config.accel_to_decel_enable) {
|
||||
gcode += " ACCEL_TO_DECEL=";
|
||||
gcode += float_to_string_decimal_point(acceleration * this->config.accel_to_decel_factor / 100);
|
||||
append_klipper_accel_to_decel(gcode, acceleration, this->config.accel_to_decel_factor.value);
|
||||
if (GCodeWriter::full_gcode_comment)
|
||||
gcode += " ; adjust ACCEL_TO_DECEL";
|
||||
}
|
||||
@@ -473,6 +472,14 @@ std::string GCodeWriter::set_acceleration_internal(Acceleration type, unsigned i
|
||||
return gcode;
|
||||
}
|
||||
|
||||
void GCodeWriter::append_klipper_accel_to_decel(std::string &gcode, unsigned int acceleration, double accel_to_decel_factor)
|
||||
{
|
||||
gcode += " ACCEL_TO_DECEL=";
|
||||
gcode += float_to_string_decimal_point(acceleration * accel_to_decel_factor / 100);
|
||||
gcode += " MINIMUM_CRUISE_RATIO=";
|
||||
gcode += float_to_string_decimal_point(1. - accel_to_decel_factor / 100);
|
||||
}
|
||||
|
||||
std::string GCodeWriter::set_jerk_xy(double jerk)
|
||||
{
|
||||
if (jerk < 0.01 || is_approx(jerk, m_last_jerk))
|
||||
@@ -548,15 +555,13 @@ std::string GCodeWriter::set_accel_and_jerk(unsigned int acceleration, double je
|
||||
return std::string();
|
||||
|
||||
std::string gcode;
|
||||
gcode.reserve(96);
|
||||
gcode.reserve(128);
|
||||
gcode += "SET_VELOCITY_LIMIT";
|
||||
if (set_acceleration) {
|
||||
gcode += " ACCEL=";
|
||||
gcode += std::to_string(acceleration);
|
||||
if (this->config.accel_to_decel_enable) {
|
||||
gcode += " ACCEL_TO_DECEL=";
|
||||
gcode += float_to_string_decimal_point(acceleration * this->config.accel_to_decel_factor / 100);
|
||||
}
|
||||
if (this->config.accel_to_decel_enable)
|
||||
append_klipper_accel_to_decel(gcode, acceleration, this->config.accel_to_decel_factor.value);
|
||||
m_last_acceleration = acceleration;
|
||||
}
|
||||
if (set_jerk) {
|
||||
|
||||
@@ -71,6 +71,10 @@ public:
|
||||
std::string set_jerk_xy(double jerk);
|
||||
// Orca: set acceleration and jerk in one command for Klipper
|
||||
std::string set_accel_and_jerk(unsigned int acceleration, double jerk);
|
||||
// Orca: Klipper's accel_to_decel limit for `acceleration`, sent as the legacy ACCEL_TO_DECEL and as the
|
||||
// equivalent MINIMUM_CRUISE_RATIO = 1 - ACCEL_TO_DECEL / ACCEL that replaced it. Klipper before 0.13 and
|
||||
// vendor forks read only the first, current Klipper only the second, and 0.13 prefers the second.
|
||||
static void append_klipper_accel_to_decel(std::string &gcode, unsigned int acceleration, double accel_to_decel_factor);
|
||||
std::string set_junction_deviation(double junction_deviation);
|
||||
std::string set_pressure_advance(double pa, int tool = -1) const;
|
||||
std::string set_input_shaping(char axis, float damp, float freq, std::string type) const;
|
||||
|
||||
@@ -443,8 +443,8 @@ coordf_t Layer::get_sparse_infill_max_void_area()
|
||||
if (density == 0.)
|
||||
return -1;
|
||||
// Orca: the adaptive TPMS infill is as sparse as its interior density.
|
||||
if (density < 100.f && config.tpms_adaptive != TpmsAdaptiveMode::Disabled && is_tpms_adaptive_pattern(pattern))
|
||||
density = std::min(density, std::max(1.f, float(config.tpms_interior_density)));
|
||||
if (density < 100.f && config.adaptive_infill_mode != AdaptiveInfillMode::Disabled && is_adaptive_infill_pattern(pattern))
|
||||
density = std::min(density, std::max(1.f, float(config.adaptive_infill_interior_density)));
|
||||
|
||||
//BBS: rough estimation and need to be optimized
|
||||
double spacing = flow.scaled_spacing() * (100 - density) / density;
|
||||
|
||||
@@ -1183,9 +1183,9 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"is_infill_first",
|
||||
"sparse_infill_density",
|
||||
"fill_multiline",
|
||||
"tpms_adaptive",
|
||||
"tpms_interior_density",
|
||||
"tpms_adaptive_gradient",
|
||||
"adaptive_infill_mode",
|
||||
"adaptive_infill_interior_density",
|
||||
"adaptive_infill_gradient",
|
||||
"gyroid_optimized",
|
||||
"sparse_infill_pattern",
|
||||
"sparse_infill_smooth_factor",
|
||||
@@ -1421,6 +1421,7 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"initial_layer_travel_acceleration",
|
||||
"initial_layer_travel_jerk",
|
||||
"exclude_object",
|
||||
"exclude_object_tolerance",
|
||||
"slow_down_layers",
|
||||
"infill_anchor",
|
||||
"infill_anchor_max",
|
||||
|
||||
@@ -309,6 +309,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
"gcode_comments",
|
||||
"gcode_label_objects",
|
||||
"exclude_object",
|
||||
"exclude_object_tolerance",
|
||||
"support_material_interface_fan_speed",
|
||||
"internal_bridge_fan_speed", // ORCA: Add support for separate internal bridge fan speed control
|
||||
"ironing_fan_speed",
|
||||
@@ -6929,7 +6930,7 @@ BoundingBoxf3 PrintInstance::get_bounding_box() const {
|
||||
|
||||
Polygon PrintInstance::get_convex_hull_2d() {
|
||||
Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix());
|
||||
poly.douglas_peucker(scale_(0.1));
|
||||
poly.douglas_peucker(scale_(print_object->print()->config().exclude_object_tolerance.value));
|
||||
return poly;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include "PrintBase.hpp"
|
||||
#include "Fill/FillAdaptive.hpp"
|
||||
#include "Fill/FillLightning.hpp"
|
||||
#include "Fill/FillTpmsAdaptive.hpp"
|
||||
#include "Fill/AdaptiveInfillField.hpp"
|
||||
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ExtrusionEntityCollection.hpp"
|
||||
@@ -407,7 +407,7 @@ public:
|
||||
double max_z() const { return m_max_z; }
|
||||
// Centering offset of the sliced mesh from the scaled and rotated mesh of the model.
|
||||
const Point& center_offset() const { return m_center_offset; }
|
||||
const TpmsRadialField* tpms_radial_field(TpmsAdaptiveMode mode) const { return m_tpms_radial_fields[size_t(mode)].get(); }
|
||||
const AdaptiveInfillField* adaptive_infill_field(AdaptiveInfillMode mode) const { return m_adaptive_infill_fields[size_t(mode)].get(); }
|
||||
|
||||
// BBS
|
||||
void generate_support_preview();
|
||||
@@ -655,7 +655,7 @@ private:
|
||||
FillAdaptive::RegionOctrees prepare_adaptive_infill_data(
|
||||
const std::vector<std::pair<const Surface*, const Layer*>>& surfaces_w_layer) const;
|
||||
FillLightning::GeneratorPtr prepare_lightning_infill_data();
|
||||
TpmsRadialFields prepare_tpms_radial_fields() const;
|
||||
AdaptiveInfillFields prepare_adaptive_infill_fields() const;
|
||||
|
||||
// BBS
|
||||
SupportNecessaryType is_support_necessary();
|
||||
@@ -706,7 +706,7 @@ private:
|
||||
FillAdaptive::RegionOctrees m_adaptive_fill_octrees;
|
||||
std::vector<BoundingBox> m_separated_body_bboxes;
|
||||
FillLightning::GeneratorPtr m_lightning_generator;
|
||||
TpmsRadialFields m_tpms_radial_fields;
|
||||
AdaptiveInfillFields m_adaptive_infill_fields;
|
||||
|
||||
std::vector < VolumeSlices > firstLayerObjSliceByVolume;
|
||||
std::vector<groupedVolumeSlices> firstLayerObjSliceByGroups;
|
||||
|
||||
@@ -366,24 +366,24 @@ static t_config_enum_values s_keys_map_SurfaceFillOrder{
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SurfaceFillOrder)
|
||||
|
||||
//Orca
|
||||
static t_config_enum_values s_keys_map_TpmsAdaptiveMode{
|
||||
{ "disabled", int(TpmsAdaptiveMode::Disabled) },
|
||||
{ "distance_warp", int(TpmsAdaptiveMode::DistanceWarp) },
|
||||
{ "smooth_blend", int(TpmsAdaptiveMode::SmoothBlend) },
|
||||
{ "stepped_shells", int(TpmsAdaptiveMode::SteppedShells) },
|
||||
{ "lobes", int(TpmsAdaptiveMode::Lobes) },
|
||||
{ "normal_z", int(TpmsAdaptiveMode::NormalZ) },
|
||||
{ "normal_y", int(TpmsAdaptiveMode::NormalY) },
|
||||
{ "normal_x", int(TpmsAdaptiveMode::NormalX) },
|
||||
static t_config_enum_values s_keys_map_AdaptiveInfillMode{
|
||||
{ "disabled", int(AdaptiveInfillMode::Disabled) },
|
||||
{ "distance_warp", int(AdaptiveInfillMode::DistanceWarp) },
|
||||
{ "smooth_blend", int(AdaptiveInfillMode::SmoothBlend) },
|
||||
{ "stepped_shells", int(AdaptiveInfillMode::SteppedShells) },
|
||||
{ "lobes", int(AdaptiveInfillMode::Lobes) },
|
||||
{ "normal_z", int(AdaptiveInfillMode::NormalZ) },
|
||||
{ "normal_y", int(AdaptiveInfillMode::NormalY) },
|
||||
{ "normal_x", int(AdaptiveInfillMode::NormalX) },
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveMode)
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(AdaptiveInfillMode)
|
||||
|
||||
static t_config_enum_values s_keys_map_TpmsAdaptiveGradient{
|
||||
{ "linear", int(TpmsAdaptiveGradient::Linear) },
|
||||
{ "quadratic", int(TpmsAdaptiveGradient::Quadratic) },
|
||||
{ "exponential", int(TpmsAdaptiveGradient::Exponential) },
|
||||
static t_config_enum_values s_keys_map_AdaptiveInfillGradient{
|
||||
{ "linear", int(AdaptiveInfillGradient::Linear) },
|
||||
{ "quadratic", int(AdaptiveInfillGradient::Quadratic) },
|
||||
{ "exponential", int(AdaptiveInfillGradient::Exponential) },
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveGradient)
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(AdaptiveInfillGradient)
|
||||
|
||||
//BBS
|
||||
static t_config_enum_values s_keys_map_PrintSequence {
|
||||
@@ -3660,7 +3660,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->max = 10; // Maximum number of lines for infill pattern
|
||||
def->set_default_value(new ConfigOptionInt(1));
|
||||
|
||||
def = this->add("tpms_adaptive", coEnum);
|
||||
def = this->add("adaptive_infill_mode", coEnum);
|
||||
def->label = L("Adaptive density (experimental)");
|
||||
def->category = L("Strength");
|
||||
def->tooltip = L("Grades the Gyroid and TPMS infill inside the object: its cells grow from the surface of the "
|
||||
@@ -3678,7 +3678,7 @@ void PrintConfigDef::init_fff_params()
|
||||
"joined to the rest by a narrower neck, is graded towards its own center.\n"
|
||||
" - Normal Z, Y or X: follows the sections of the object normal to that axis, so the density "
|
||||
"does not change along it.");
|
||||
def->enum_keys_map = &ConfigOptionEnum<TpmsAdaptiveMode>::get_enum_values();
|
||||
def->enum_keys_map = &ConfigOptionEnum<AdaptiveInfillMode>::get_enum_values();
|
||||
def->enum_values.push_back("disabled");
|
||||
def->enum_values.push_back("distance_warp");
|
||||
def->enum_values.push_back("smooth_blend");
|
||||
@@ -3696,9 +3696,9 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_labels.push_back(L("Normal Y"));
|
||||
def->enum_labels.push_back(L("Normal X"));
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionEnum<TpmsAdaptiveMode>(TpmsAdaptiveMode::Disabled));
|
||||
def->set_default_value(new ConfigOptionEnum<AdaptiveInfillMode>(AdaptiveInfillMode::Disabled));
|
||||
|
||||
def = this->add("tpms_interior_density", coPercent);
|
||||
def = this->add("adaptive_infill_interior_density", coPercent);
|
||||
def->label = L("Interior density");
|
||||
def->category = L("Strength");
|
||||
def->tooltip = L("Density of the adaptive infill at the center of the object.");
|
||||
@@ -3708,7 +3708,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionPercent(5));
|
||||
|
||||
def = this->add("tpms_adaptive_gradient", coEnum);
|
||||
def = this->add("adaptive_infill_gradient", coEnum);
|
||||
def->label = L("Adaptive gradient");
|
||||
def->category = L("Strength");
|
||||
def->tooltip = L("How the density changes from the surface to the center of the object.\n"
|
||||
@@ -3717,7 +3717,7 @@ void PrintConfigDef::init_fff_params()
|
||||
"changes faster towards the center.\n"
|
||||
"Exponential: the density changes quickly just below the surface and levels off towards "
|
||||
"the center.");
|
||||
def->enum_keys_map = &ConfigOptionEnum<TpmsAdaptiveGradient>::get_enum_values();
|
||||
def->enum_keys_map = &ConfigOptionEnum<AdaptiveInfillGradient>::get_enum_values();
|
||||
def->enum_values.push_back("linear");
|
||||
def->enum_values.push_back("quadratic");
|
||||
def->enum_values.push_back("exponential");
|
||||
@@ -3725,7 +3725,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_labels.push_back(L("Quadratic"));
|
||||
def->enum_labels.push_back(L("Exponential"));
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionEnum<TpmsAdaptiveGradient>(TpmsAdaptiveGradient::Linear));
|
||||
def->set_default_value(new ConfigOptionEnum<AdaptiveInfillGradient>(AdaptiveInfillGradient::Linear));
|
||||
|
||||
// Z-buckling bias optimization (experimental). Tightens the gyroid wave along the Z
|
||||
// (vertical) axis at low infill density to shorten the effective column length under
|
||||
@@ -4629,6 +4629,17 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("exclude_object_tolerance", coFloat);
|
||||
def->label = L("Outline tolerance");
|
||||
def->tooltip = L("Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the "
|
||||
"original outline. Larger values give fewer points, which helps firmware that stalls "
|
||||
"while parsing G-code with many objects. 0 keeps every point.");
|
||||
def->sidetext = L("mm");
|
||||
def->min = 0;
|
||||
def->max = 5;
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionFloat(0.1));
|
||||
|
||||
def = this->add("gcode_comments", coBool);
|
||||
def->label = L("Verbose G-code");
|
||||
def->tooltip = L("Enable this to get a commented G-code file, with each line explained by a descriptive text. "
|
||||
@@ -9654,6 +9665,12 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
|
||||
opt_key = "change_filament_gcode";
|
||||
} else if (opt_key == "bridge_fan_speed") {
|
||||
opt_key = "overhang_fan_speed";
|
||||
} else if (opt_key == "tpms_adaptive") {
|
||||
opt_key = "adaptive_infill_mode";
|
||||
} else if (opt_key == "tpms_interior_density") {
|
||||
opt_key = "adaptive_infill_interior_density";
|
||||
} else if (opt_key == "tpms_adaptive_gradient") {
|
||||
opt_key = "adaptive_infill_gradient";
|
||||
} else if (opt_key == "infill_extruder" || opt_key == "sparse_infill_filament") {
|
||||
// ORCA: legacy feature-filament selector. Pre-2.4.0-dev these keys were 1-based and the
|
||||
// default value "1" meant "the first/active filament". The current scheme uses a dedicated
|
||||
|
||||
@@ -172,8 +172,8 @@ inline bool is_separable_infill_pattern(InfillPattern pattern)
|
||||
|
||||
// Orca: Infill patterns laid out by an octree, which each connected body always gets of its own.
|
||||
inline bool is_octree_infill_pattern(InfillPattern pattern) { return pattern == ipAdaptiveCubic || pattern == ipSupportCubic; }
|
||||
// Orca: Infill patterns graded by the "tpms_adaptive" option.
|
||||
inline bool is_tpms_adaptive_pattern(InfillPattern pattern) { return pattern == ipGyroid || pattern == ipTpmsD || pattern == ipTpmsFK; }
|
||||
// Orca: Infill patterns graded by the "adaptive_infill_mode" option.
|
||||
inline bool is_adaptive_infill_pattern(InfillPattern pattern) { return pattern == ipGyroid || pattern == ipTpmsD || pattern == ipTpmsFK; }
|
||||
|
||||
// Orca: Infill patterns that round their corners by the "sparse_infill_smooth_factor" option.
|
||||
// Grid, Triangles and Tri-hexagon only do so in their trapezoidal form, which is generated with more
|
||||
@@ -279,8 +279,8 @@ enum class SurfaceFillOrder {
|
||||
Count,
|
||||
};
|
||||
|
||||
// Orca: what the adaptive TPMS density follows: the 3D shape of the object, or its 2D sections normal to an axis.
|
||||
enum class TpmsAdaptiveMode {
|
||||
// Orca: what the adaptive infill density follows: the 3D shape of the object, or its 2D sections normal to an axis.
|
||||
enum class AdaptiveInfillMode {
|
||||
Disabled,
|
||||
DistanceWarp,
|
||||
SmoothBlend,
|
||||
@@ -292,8 +292,8 @@ enum class TpmsAdaptiveMode {
|
||||
Count,
|
||||
};
|
||||
|
||||
// Orca: how the adaptive TPMS density changes from the object surface to its deepest point.
|
||||
enum class TpmsAdaptiveGradient {
|
||||
// Orca: how the adaptive infill density changes from the object surface to its deepest point.
|
||||
enum class AdaptiveInfillGradient {
|
||||
Linear,
|
||||
Quadratic,
|
||||
Exponential,
|
||||
@@ -816,8 +816,8 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PerimeterGeneratorType)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ToolChangeOrderingType)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PowerLossRecoveryMode)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TpmsAdaptiveMode)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TpmsAdaptiveGradient)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AdaptiveInfillMode)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AdaptiveInfillGradient)
|
||||
|
||||
#undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS
|
||||
|
||||
@@ -1501,9 +1501,9 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
// Orca:
|
||||
((ConfigOptionFloatOrPercent, infill_combination_max_layer_height))
|
||||
((ConfigOptionInt, fill_multiline))
|
||||
((ConfigOptionEnum<TpmsAdaptiveMode>, tpms_adaptive))
|
||||
((ConfigOptionPercent, tpms_interior_density))
|
||||
((ConfigOptionEnum<TpmsAdaptiveGradient>, tpms_adaptive_gradient))
|
||||
((ConfigOptionEnum<AdaptiveInfillMode>, adaptive_infill_mode))
|
||||
((ConfigOptionPercent, adaptive_infill_interior_density))
|
||||
((ConfigOptionEnum<AdaptiveInfillGradient>, adaptive_infill_gradient))
|
||||
((ConfigOptionBool, gyroid_optimized))
|
||||
// Ironing options
|
||||
((ConfigOptionEnum<IroningType>, ironing_type))
|
||||
@@ -2130,6 +2130,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
|
||||
((ConfigOptionPercents, filament_shrinkage_compensation_z))
|
||||
((ConfigOptionBool, gcode_label_objects))
|
||||
((ConfigOptionBool, exclude_object))
|
||||
((ConfigOptionFloat, exclude_object_tolerance))
|
||||
((ConfigOptionFloats, grab_length))
|
||||
((ConfigOptionBool, gcode_comments))
|
||||
((ConfigOptionInt, slow_down_layers))
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
#include "Fill/FillAdaptive.hpp"
|
||||
#include "Fill/Fill.hpp"
|
||||
#include "Fill/FillLightning.hpp"
|
||||
#include "Fill/FillTpmsAdaptive.hpp"
|
||||
#include "Fill/AdaptiveInfillField.hpp"
|
||||
#include "format.hpp"
|
||||
#include "AABBTreeIndirect.hpp"
|
||||
#include "AABBTreeLines.hpp"
|
||||
@@ -1252,19 +1252,19 @@ FillLightning::GeneratorPtr PrintObject::prepare_lightning_infill_data()
|
||||
return has_lightning_infill ? FillLightning::build_generator(std::as_const(*this), [this]() -> void { this->throw_if_canceled(); }) : FillLightning::GeneratorPtr();
|
||||
}
|
||||
|
||||
TpmsRadialFields PrintObject::prepare_tpms_radial_fields() const
|
||||
AdaptiveInfillFields PrintObject::prepare_adaptive_infill_fields() const
|
||||
{
|
||||
TpmsRadialFields fields;
|
||||
std::array<bool, size_t(TpmsAdaptiveMode::Count)> modes{};
|
||||
AdaptiveInfillFields fields;
|
||||
std::array<bool, size_t(AdaptiveInfillMode::Count)> modes{};
|
||||
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id)
|
||||
if (const PrintRegionConfig &config = this->printing_region(region_id).config();
|
||||
config.sparse_infill_density > 0 && config.sparse_infill_density < 100 && is_tpms_adaptive_pattern(config.sparse_infill_pattern))
|
||||
modes[size_t(config.tpms_adaptive.value)] = true;
|
||||
modes[size_t(TpmsAdaptiveMode::Disabled)] = false;
|
||||
config.sparse_infill_density > 0 && config.sparse_infill_density < 100 && is_adaptive_infill_pattern(config.sparse_infill_pattern))
|
||||
modes[size_t(config.adaptive_infill_mode.value)] = true;
|
||||
modes[size_t(AdaptiveInfillMode::Disabled)] = false;
|
||||
if (std::find(modes.begin(), modes.end(), true) == modes.end() || m_layers.empty())
|
||||
return fields;
|
||||
|
||||
std::vector<TpmsRadialField::Slice> slices;
|
||||
std::vector<AdaptiveInfillField::Slice> slices;
|
||||
slices.reserve(m_layers.size());
|
||||
BoundingBox bbox;
|
||||
for (const Layer *layer : m_layers) {
|
||||
@@ -1277,7 +1277,7 @@ TpmsRadialFields PrintObject::prepare_tpms_radial_fields() const
|
||||
if (!modes[mode])
|
||||
continue;
|
||||
// Without a field, the infill falls back to the regular pattern.
|
||||
auto field = std::make_unique<TpmsRadialField>(slices, bbox, TpmsAdaptiveMode(mode), [this]() { m_print->throw_if_canceled(); });
|
||||
auto field = std::make_unique<AdaptiveInfillField>(slices, bbox, AdaptiveInfillMode(mode), [this]() { m_print->throw_if_canceled(); });
|
||||
if (!field->empty())
|
||||
fields[mode] = std::move(field);
|
||||
}
|
||||
@@ -1685,9 +1685,9 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "infill_overhang_angle") {
|
||||
steps.emplace_back(posInfill);
|
||||
} else if (opt_key == "sparse_infill_pattern"
|
||||
|| opt_key == "tpms_adaptive"
|
||||
|| opt_key == "tpms_interior_density"
|
||||
|| opt_key == "tpms_adaptive_gradient"
|
||||
|| opt_key == "adaptive_infill_mode"
|
||||
|| opt_key == "adaptive_infill_interior_density"
|
||||
|| opt_key == "adaptive_infill_gradient"
|
||||
// Orca: Body centering now also determines bridge anchors during preparation.
|
||||
// Invalidating preparation also invalidates infill, including top/bottom surfaces.
|
||||
|| opt_key == "center_of_surface_pattern"
|
||||
@@ -3195,7 +3195,7 @@ void PrintObject::bridge_over_infill()
|
||||
}
|
||||
|
||||
this->m_adaptive_fill_octrees = this->prepare_adaptive_infill_data(surfaces_w_layer);
|
||||
this->m_tpms_radial_fields = this->prepare_tpms_radial_fields();
|
||||
this->m_adaptive_infill_fields = this->prepare_adaptive_infill_fields();
|
||||
|
||||
std::vector<size_t> layers_to_generate_infill;
|
||||
for (const auto &pair : surfaces_by_layer) {
|
||||
|
||||
+484
-11
@@ -26,8 +26,10 @@
|
||||
#include "Print.hpp"
|
||||
#include "GCode/OrderingStrategies.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cassert>
|
||||
#include <deque>
|
||||
#include "MultiMaterialSegmentation.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -96,6 +98,46 @@ std::vector<std::pair<size_t, bool>> chain_segments_closest_point(std::vector<En
|
||||
return out;
|
||||
}
|
||||
|
||||
// The shortest chain over every order and allowed reversal, by dynamic programming over the subsets of segments, so only
|
||||
// for a few of them. end_points holds the first and the last point of each segment; the chain starts at start when given
|
||||
// and its end is free. Return index and "reversed" flag.
|
||||
static std::vector<std::pair<size_t, bool>> chain_segments_exact(const std::vector<Vec2d> &end_points, const std::vector<char> &could_reverse, const Vec2d *start)
|
||||
{
|
||||
const size_t n = could_reverse.size();
|
||||
assert(n >= 1 && n <= 12 && end_points.size() == 2 * n);
|
||||
// State (mask * n + j) * 2 + o: the segments of mask visited, the last one j, flipped when o.
|
||||
const size_t num_states = (size_t(1) << n) * n * 2;
|
||||
std::vector<double> cost(num_states, std::numeric_limits<double>::max());
|
||||
std::vector<size_t> from(num_states, size_t(-1));
|
||||
auto entry = [&end_points](size_t j, size_t o) -> const Vec2d & { return end_points[2 * j + o]; };
|
||||
auto exit = [&end_points](size_t j, size_t o) -> const Vec2d & { return end_points[2 * j + 1 - o]; };
|
||||
for (size_t j = 0; j < n; ++ j)
|
||||
for (size_t o = 0; o <= size_t(could_reverse[j] != 0); ++ o)
|
||||
cost[((size_t(1) << j) * n + j) * 2 + o] = start == nullptr ? 0. : (entry(j, o) - *start).norm();
|
||||
for (size_t mask = 1; mask < (size_t(1) << n); ++ mask)
|
||||
for (size_t s = mask * n * 2; s < (mask + 1) * n * 2; ++ s)
|
||||
if (cost[s] < std::numeric_limits<double>::max())
|
||||
for (size_t k = 0; k < n; ++ k)
|
||||
if (! (mask >> k & 1))
|
||||
for (size_t o = 0; o <= size_t(could_reverse[k] != 0); ++ o) {
|
||||
const size_t next = (((mask | size_t(1) << k) * n) + k) * 2 + o;
|
||||
const double c = cost[s] + (entry(k, o) - exit(s / 2 % n, s & 1)).norm();
|
||||
if (c < cost[next]) {
|
||||
cost[next] = c;
|
||||
from[next] = s;
|
||||
}
|
||||
}
|
||||
const size_t full = (size_t(1) << n) - 1;
|
||||
size_t best = full * n * 2;
|
||||
for (size_t s = best; s < num_states; ++ s)
|
||||
if (cost[s] < cost[best])
|
||||
best = s;
|
||||
std::vector<std::pair<size_t, bool>> out(n);
|
||||
for (size_t s = best, k = n; k -- > 0; s = from[s])
|
||||
out[k] = { s / 2 % n, (s & 1) != 0 };
|
||||
return out;
|
||||
}
|
||||
|
||||
// Chain perimeters (always closed) and thin fills (closed or open) using a greedy algorithm.
|
||||
// Solving a Traveling Salesman Problem (TSP) with the modification, that the sites are not always points, but points and segments.
|
||||
// Solving using a greedy algorithm, where a shortest edge is added to the solution if it does not produce a bifurcation or a cycle.
|
||||
@@ -104,6 +146,7 @@ std::vector<std::pair<size_t, bool>> chain_segments_closest_point(std::vector<En
|
||||
// The algorithm builds a tour for the traveling salesman one edge at a time and thus maintains multiple tour fragments, each of which
|
||||
// is a simple path in the complete graph of cities. At each stage, the algorithm selects the edge of minimal cost that either creates
|
||||
// a new fragment, extends one of the existing paths or creates a cycle of length equal to the number of cities.
|
||||
// Orca: two to four segments are chained exactly instead.
|
||||
template<typename PointType, typename SegmentEndPointFunc, bool REVERSE_COULD_FAIL, typename CouldReverseFunc>
|
||||
std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals_(SegmentEndPointFunc end_point_func, CouldReverseFunc could_reverse_func, size_t num_segments, const PointType *start_near)
|
||||
{
|
||||
@@ -118,6 +161,19 @@ std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals
|
||||
out.emplace_back(0, could_reverse_func(0) && start_near != nullptr &&
|
||||
(end_point_func(0, false) - *start_near).template cast<double>().squaredNorm() < (end_point_func(0, true) - *start_near).template cast<double>().squaredNorm());
|
||||
}
|
||||
else if (num_segments <= 4)
|
||||
{
|
||||
// Orca: so few segments are cheap to chain exactly. The first one is then not always the one nearest start_near.
|
||||
std::vector<Vec2d> end_points(2 * num_segments);
|
||||
std::vector<char> could_reverse(num_segments);
|
||||
for (size_t i = 0; i < num_segments; ++ i) {
|
||||
end_points[2 * i] = end_point_func(i, true).template cast<double>();
|
||||
end_points[2 * i + 1] = end_point_func(i, false).template cast<double>();
|
||||
could_reverse[i] = could_reverse_func(i);
|
||||
}
|
||||
const Vec2d start = start_near != nullptr ? Vec2d(start_near->template cast<double>()) : Vec2d::Zero();
|
||||
out = chain_segments_exact(end_points, could_reverse, start_near != nullptr ? &start : nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
// End points of segments for the KD tree closest point search.
|
||||
@@ -1039,12 +1095,370 @@ std::vector<std::pair<size_t, bool>> chain_segments_greedy2(SegmentEndPointFunc
|
||||
return chain_segments_greedy_constrained_reversals2_<PointType, SegmentEndPointFunc, false, decltype(could_reverse_func)>(end_point_func, could_reverse_func, num_segments, start_near);
|
||||
}
|
||||
|
||||
// The K points nearest to a query, nearest first, ties by index, from a uniform grid of about two points per cell.
|
||||
template<size_t K>
|
||||
class NearestPoints
|
||||
{
|
||||
public:
|
||||
static constexpr size_t npos = size_t(-1);
|
||||
|
||||
explicit NearestPoints(const std::vector<Vec2d> &points) : m_points(points)
|
||||
{
|
||||
BoundingBoxf bbox;
|
||||
for (const Vec2d &p : points)
|
||||
bbox.merge(p);
|
||||
const Vec2d size = bbox.size();
|
||||
const double count = double(std::max<size_t>(points.size(), 1));
|
||||
m_cell = std::max(std::sqrt(2. * size.x() * size.y() / count), std::max(size.x(), size.y()) / count);
|
||||
if (m_cell <= 0.)
|
||||
m_cell = 1.;
|
||||
m_origin = bbox.min;
|
||||
m_cols = int(size.x() / m_cell) + 1;
|
||||
m_rows = int(size.y() / m_cell) + 1;
|
||||
m_first.assign(size_t(m_cols) * m_rows + 1, 0);
|
||||
for (const Vec2d &p : points)
|
||||
++ m_first[cell(p) + 1];
|
||||
for (size_t c = 1; c < m_first.size(); ++ c)
|
||||
m_first[c] += m_first[c - 1];
|
||||
m_index.resize(points.size());
|
||||
std::vector<size_t> next(m_first.begin(), m_first.end() - 1);
|
||||
for (size_t i = 0; i < points.size(); ++ i)
|
||||
m_index[next[cell(points[i])] ++] = i;
|
||||
}
|
||||
|
||||
// Indices of the nearest points that filter accepts, npos past the last one.
|
||||
template<typename Filter>
|
||||
std::array<size_t, K> query(const Vec2d &q, Filter filter) const
|
||||
{
|
||||
std::array<std::pair<double, size_t>, K> best;
|
||||
size_t found = 0;
|
||||
const int cx = int(std::clamp(std::floor((q.x() - m_origin.x()) / m_cell), 0., double(m_cols - 1)));
|
||||
const int cy = int(std::clamp(std::floor((q.y() - m_origin.y()) / m_cell), 0., double(m_rows - 1)));
|
||||
auto visit = [&](int x, int y) {
|
||||
if (x < 0 || y < 0 || x >= m_cols || y >= m_rows)
|
||||
return;
|
||||
const size_t c = size_t(y) * m_cols + x;
|
||||
for (size_t k = m_first[c]; k < m_first[c + 1]; ++ k) {
|
||||
const size_t i = m_index[k];
|
||||
if (! filter(i))
|
||||
continue;
|
||||
const std::pair<double, size_t> candidate((m_points[i] - q).squaredNorm(), i);
|
||||
if (found == K && ! (candidate < best[K - 1]))
|
||||
continue;
|
||||
auto it = std::upper_bound(best.begin(), best.begin() + found, candidate);
|
||||
std::move_backward(it, best.begin() + std::min(found, K - 1), best.begin() + std::min(found + 1, K));
|
||||
*it = candidate;
|
||||
found = std::min(found + 1, K);
|
||||
}
|
||||
};
|
||||
// Points beyond ring r are at least r cells away.
|
||||
for (int r = 0; r <= std::max(m_cols, m_rows); ++ r) {
|
||||
if (r == 0)
|
||||
visit(cx, cy);
|
||||
for (int d = -r; d <= r && r > 0; ++ d) {
|
||||
visit(cx + d, cy - r);
|
||||
visit(cx + d, cy + r);
|
||||
if (d != -r && d != r) {
|
||||
visit(cx - r, cy + d);
|
||||
visit(cx + r, cy + d);
|
||||
}
|
||||
}
|
||||
if (found == K && best[K - 1].first <= sqr(r * m_cell))
|
||||
break;
|
||||
}
|
||||
std::array<size_t, K> out;
|
||||
out.fill(npos);
|
||||
for (size_t k = 0; k < found; ++ k)
|
||||
out[k] = best[k].second;
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
size_t cell(const Vec2d &p) const
|
||||
{
|
||||
const int x = std::clamp(int((p.x() - m_origin.x()) / m_cell), 0, m_cols - 1);
|
||||
const int y = std::clamp(int((p.y() - m_origin.y()) / m_cell), 0, m_rows - 1);
|
||||
return size_t(y) * m_cols + x;
|
||||
}
|
||||
|
||||
const std::vector<Vec2d> &m_points;
|
||||
Vec2d m_origin;
|
||||
double m_cell;
|
||||
int m_cols;
|
||||
int m_rows;
|
||||
std::vector<size_t> m_first;
|
||||
std::vector<size_t> m_index;
|
||||
};
|
||||
|
||||
// Shortens the travel of a chain with 2-opt moves (reversing a run of segments) and or-opt moves (moving a run of up to
|
||||
// three segments elsewhere, or a longer one to the front, flipped or not). end_points holds the first and the last point
|
||||
// of each segment. The chain starts at start_near when given, and its end is free. A move that would flip a segment that
|
||||
// cannot reverse is skipped. Moves only create links between nearest neighbor end points, and after a move only the
|
||||
// segments next to a changed link are examined again, until a sweep of all segments finds no move. This keeps the search
|
||||
// close to linear.
|
||||
static void improve_chain_by_local_search(std::vector<std::pair<size_t, bool>> &chain, const std::vector<Vec2d> &end_points,
|
||||
const std::vector<char> &could_reverse, const Vec2d *start_near)
|
||||
{
|
||||
const size_t n = chain.size();
|
||||
if (n < 2)
|
||||
return;
|
||||
assert(end_points.size() == 2 * n && could_reverse.size() == n);
|
||||
|
||||
std::vector<size_t> order(n), pos(n), fixed_before(n + 1, 0);
|
||||
std::vector<char> flipped(n);
|
||||
for (size_t k = 0; k < n; ++ k) {
|
||||
order[k] = chain[k].first;
|
||||
flipped[order[k]] = chain[k].second;
|
||||
}
|
||||
// Updates positions lo..hi after a move, which keeps the number of segments that cannot reverse past hi.
|
||||
auto refresh = [&](size_t lo, size_t hi) {
|
||||
for (size_t k = lo; k <= hi; ++ k) {
|
||||
pos[order[k]] = k;
|
||||
fixed_before[k + 1] = fixed_before[k] + ! could_reverse[order[k]];
|
||||
}
|
||||
};
|
||||
refresh(0, n - 1);
|
||||
auto entry_idx = [&](size_t k) { return 2 * order[k] + flipped[order[k]]; };
|
||||
auto exit_idx = [&](size_t k) { return 2 * order[k] + 1 - flipped[order[k]]; };
|
||||
auto entry = [&](size_t k) -> const Vec2d& { return end_points[entry_idx(k)]; };
|
||||
auto exit = [&](size_t k) -> const Vec2d& { return end_points[exit_idx(k)]; };
|
||||
auto is_exit = [&](size_t e) { return char(e & 1) != flipped[e >> 1]; };
|
||||
auto reversible = [&](size_t i, size_t j) { return fixed_before[j + 1] == fixed_before[i]; };
|
||||
auto dist = [](const Vec2d *a, const Vec2d *b) { return a == nullptr || b == nullptr ? 0. : (*a - *b).norm(); };
|
||||
// Travel end points around position k, null where the chain is open.
|
||||
auto prev_exit = [&](size_t k) -> const Vec2d* { return k > 0 ? &exit(k - 1) : start_near; };
|
||||
auto next_entry = [&](size_t k) -> const Vec2d* { return k + 1 < n ? &entry(k + 1) : nullptr; };
|
||||
|
||||
// The 32 nearest end points are the candidates, which covers every move on short chains; 8 missed much of the gain.
|
||||
static constexpr size_t K = 32;
|
||||
static constexpr size_t npos = NearestPoints<K>::npos;
|
||||
const NearestPoints<K> nearest(end_points);
|
||||
std::vector<std::array<size_t, K>> neighbors(end_points.size());
|
||||
for (size_t e = 0; e < end_points.size(); ++ e)
|
||||
neighbors[e] = nearest.query(end_points[e], [e](size_t idx) { return (idx >> 1) != (e >> 1); });
|
||||
std::array<size_t, K> start_neighbors;
|
||||
start_neighbors.fill(npos);
|
||||
if (start_near != nullptr)
|
||||
start_neighbors = nearest.query(*start_near, [](size_t) { return true; });
|
||||
|
||||
// Segments to examine, by id, each queued once.
|
||||
std::deque<size_t> queue;
|
||||
std::vector<char> queued(n, 0);
|
||||
auto enqueue = [&](size_t k) {
|
||||
if (k < n && ! queued[order[k]]) {
|
||||
queued[order[k]] = 1;
|
||||
queue.push_back(order[k]);
|
||||
}
|
||||
};
|
||||
|
||||
const double min_gain = SCALED_EPSILON;
|
||||
// Reverses positions i..j if that shortens the chain.
|
||||
auto try_2opt = [&](size_t i, size_t j) {
|
||||
if (! reversible(i, j))
|
||||
return false;
|
||||
const Vec2d *a = prev_exit(i), *b = next_entry(j);
|
||||
if (dist(a, &exit(j)) + dist(&entry(i), b) > dist(a, &entry(i)) + dist(&exit(j), b) - min_gain)
|
||||
return false;
|
||||
std::reverse(order.begin() + i, order.begin() + j + 1);
|
||||
for (size_t k = i; k <= j; ++ k)
|
||||
flipped[order[k]] ^= 1;
|
||||
refresh(i, j);
|
||||
for (size_t k : { i - 1, i, j, j + 1 })
|
||||
enqueue(k);
|
||||
return true;
|
||||
};
|
||||
// Moves positions i..last after position gap (size_t(-1) for the front) if that shortens the chain.
|
||||
auto try_oropt = [&](size_t i, size_t last, size_t gap) {
|
||||
if (gap + 1 == i || (gap >= i && gap <= last))
|
||||
return false;
|
||||
const Vec2d *a = gap == size_t(-1) ? start_near : &exit(gap);
|
||||
const Vec2d *b = gap + 1 < n ? &entry(gap + 1) : nullptr;
|
||||
const double removal_gain = dist(prev_exit(i), &entry(i)) + dist(&exit(last), next_entry(last)) - dist(prev_exit(i), next_entry(last));
|
||||
const double base = dist(a, b);
|
||||
const double cost_kept = dist(a, &entry(i)) + dist(&exit(last), b) - base;
|
||||
const double cost_flipped = reversible(i, last) ? dist(a, &exit(last)) + dist(&entry(i), b) - base : std::numeric_limits<double>::max();
|
||||
if (std::min(cost_kept, cost_flipped) > removal_gain - min_gain)
|
||||
return false;
|
||||
const size_t len = last - i + 1;
|
||||
// The run lands on first..first+len-1; lo..hi spans every position that changed.
|
||||
const bool forward = gap > last && gap != size_t(-1);
|
||||
const size_t first = forward ? gap + 1 - len : gap + 1;
|
||||
const size_t lo = forward ? i : first;
|
||||
const size_t hi = forward ? gap : last;
|
||||
if (forward)
|
||||
std::rotate(order.begin() + i, order.begin() + last + 1, order.begin() + gap + 1);
|
||||
else
|
||||
std::rotate(order.begin() + first, order.begin() + i, order.begin() + last + 1);
|
||||
if (cost_flipped < cost_kept) {
|
||||
std::reverse(order.begin() + first, order.begin() + first + len);
|
||||
for (size_t k = first; k < first + len; ++ k)
|
||||
flipped[order[k]] ^= 1;
|
||||
}
|
||||
refresh(lo, hi);
|
||||
// The links around the run's new place, and the link closing its old place.
|
||||
for (size_t k : { first - 1, first, first + len - 1, first + len, forward ? i - 1 : last, forward ? i : last + 1 })
|
||||
enqueue(k);
|
||||
return true;
|
||||
};
|
||||
// The gap before position k of the chain with positions i..last taken out.
|
||||
auto gap_before = [](size_t k, size_t i, size_t last) { return k == last + 1 ? i - 1 : k - 1; };
|
||||
|
||||
// Applies the first move found that breaks a link next to position p.
|
||||
// Candidates are tried nearest first, while closer than the link the move would replace at that end point.
|
||||
auto closer = [&](const Vec2d &at, size_t q, double limit) { return q != npos && (end_points[q] - at).norm() < limit; };
|
||||
auto improve_at = [&](size_t p) {
|
||||
// 2-opt breaking the link into p, as the first or the last link of the reversed run.
|
||||
if (const Vec2d *a = prev_exit(p); a != nullptr) {
|
||||
const double old_link = dist(a, &entry(p));
|
||||
for (size_t q : p > 0 ? neighbors[exit_idx(p - 1)] : start_neighbors) {
|
||||
if (! closer(*a, q, old_link))
|
||||
break;
|
||||
if (is_exit(q) && pos[q >> 1] >= p && try_2opt(p, pos[q >> 1]))
|
||||
return true;
|
||||
}
|
||||
if (p > 0)
|
||||
for (size_t q : neighbors[entry_idx(p)]) {
|
||||
if (! closer(entry(p), q, old_link))
|
||||
break;
|
||||
if (! is_exit(q) && pos[q >> 1] < p && try_2opt(pos[q >> 1], p - 1))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 2-opt breaking the link out of p.
|
||||
if (p + 1 < n) {
|
||||
const double old_link = dist(&exit(p), &entry(p + 1));
|
||||
for (size_t q : neighbors[exit_idx(p)]) {
|
||||
if (! closer(exit(p), q, old_link))
|
||||
break;
|
||||
if (is_exit(q) && pos[q >> 1] > p && try_2opt(p + 1, pos[q >> 1]))
|
||||
return true;
|
||||
}
|
||||
for (size_t q : neighbors[entry_idx(p + 1)]) {
|
||||
if (! closer(entry(p + 1), q, old_link))
|
||||
break;
|
||||
if (! is_exit(q) && pos[q >> 1] <= p && try_2opt(pos[q >> 1], p))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// A run of any length that starts or ends next to the start point moves to the front.
|
||||
if (p == 0)
|
||||
for (size_t q : start_neighbors) {
|
||||
if (q == npos)
|
||||
break;
|
||||
const size_t m = pos[q >> 1];
|
||||
if (m == 0)
|
||||
continue;
|
||||
// Runs m..k enter next to the start, runs k..m leave there and go flipped.
|
||||
const bool exit_near = is_exit(q);
|
||||
for (size_t k = exit_near ? 1 : m; k < (exit_near ? m + 1 : n); ++ k)
|
||||
if (try_oropt(exit_near ? k : m, exit_near ? m : k, size_t(-1)))
|
||||
return true;
|
||||
}
|
||||
// Or-opt of the runs starting at p: next to an exit a run goes after that segment, next to an entry before it.
|
||||
for (size_t last = p; last < std::min(p + 3, n); ++ last) {
|
||||
if (try_oropt(p, last, size_t(-1)))
|
||||
return true;
|
||||
const double removal_gain = dist(prev_exit(p), &entry(p)) + dist(&exit(last), next_entry(last)) - dist(prev_exit(p), next_entry(last));
|
||||
for (size_t e : { entry_idx(p), exit_idx(last) })
|
||||
for (size_t q : neighbors[e]) {
|
||||
if (! closer(end_points[e], q, removal_gain))
|
||||
break;
|
||||
const size_t m = pos[q >> 1];
|
||||
if ((m < p || m > last) && try_oropt(p, last, is_exit(q) ? m : gap_before(m, p, last)))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// A new link can make room for a run that sits elsewhere, hence the sweeps until one finds no move. Every move shortens
|
||||
// the chain, so the search ends; the cap only bounds its time.
|
||||
for (size_t k = 0; k < n; ++ k)
|
||||
enqueue(k);
|
||||
bool moved_since_sweep = false;
|
||||
for (size_t moves = 0; moves < 50 * n;) {
|
||||
if (queue.empty()) {
|
||||
if (! moved_since_sweep)
|
||||
break;
|
||||
moved_since_sweep = false;
|
||||
for (size_t k = 0; k < n; ++ k)
|
||||
enqueue(k);
|
||||
}
|
||||
const size_t segment = queue.front();
|
||||
queue.pop_front();
|
||||
queued[segment] = 0;
|
||||
if (improve_at(pos[segment])) {
|
||||
++ moves;
|
||||
moved_since_sweep = true;
|
||||
enqueue(pos[segment]);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t k = 0; k < n; ++ k)
|
||||
chain[k] = { order[k], flipped[order[k]] != 0 };
|
||||
}
|
||||
|
||||
// Chain that always goes on to the nearest free end point, from start.
|
||||
static std::vector<std::pair<size_t, bool>> chain_nearest_next(const std::vector<Vec2d> &end_points, const std::vector<char> &could_reverse, const Vec2d &start)
|
||||
{
|
||||
const NearestPoints<1> nearest(end_points);
|
||||
std::vector<char> taken(could_reverse.size(), 0);
|
||||
std::vector<std::pair<size_t, bool>> out;
|
||||
out.reserve(could_reverse.size());
|
||||
for (Vec2d at = start; out.size() < could_reverse.size();) {
|
||||
const size_t e = nearest.query(at, [&](size_t idx) { return ! taken[idx >> 1] && ((idx & 1) == 0 || could_reverse[idx >> 1]); })[0];
|
||||
taken[e >> 1] = 1;
|
||||
out.emplace_back(e >> 1, (e & 1) != 0);
|
||||
at = end_points[e ^ 1];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static double chain_travel(const std::vector<std::pair<size_t, bool>> &chain, const std::vector<Vec2d> &end_points, const Vec2d &start)
|
||||
{
|
||||
double travel = 0.;
|
||||
Vec2d at = start;
|
||||
for (const std::pair<size_t, bool> &segment : chain) {
|
||||
travel += (end_points[2 * segment.first + segment.second] - at).norm();
|
||||
at = end_points[2 * segment.first + 1 - segment.second];
|
||||
}
|
||||
return travel;
|
||||
}
|
||||
|
||||
template<typename EntityPtr>
|
||||
static std::vector<std::pair<size_t, bool>> chain_extrusion_entities_impl(const std::vector<EntityPtr> &entities, const Point *start_near)
|
||||
static std::vector<std::pair<size_t, bool>> chain_extrusion_entities_impl(const std::vector<EntityPtr> &entities, const Point *start_near, bool improve = false)
|
||||
{
|
||||
auto segment_end_point = [&entities](size_t idx, bool first_point) -> Point { return first_point ? entities[idx]->first_point() : entities[idx]->last_point(); };
|
||||
auto could_reverse = [&entities](size_t idx) { const ExtrusionEntity *ee = entities[idx]; return ee->is_loop() || ee->can_reverse(); };
|
||||
std::vector<std::pair<size_t, bool>> out = chain_segments_greedy_constrained_reversals<Point, decltype(segment_end_point), decltype(could_reverse)>(segment_end_point, could_reverse, entities.size(), start_near);
|
||||
if (improve && out.size() > 1) {
|
||||
std::vector<Vec2d> end_points;
|
||||
std::vector<char> reversible;
|
||||
end_points.reserve(2 * entities.size());
|
||||
reversible.reserve(entities.size());
|
||||
for (size_t idx = 0; idx < entities.size(); ++ idx) {
|
||||
end_points.emplace_back(segment_end_point(idx, true).template cast<double>());
|
||||
end_points.emplace_back(segment_end_point(idx, false).template cast<double>());
|
||||
reversible.emplace_back(could_reverse(idx));
|
||||
}
|
||||
Vec2d start = Vec2d::Zero();
|
||||
if (start_near != nullptr)
|
||||
start = start_near->cast<double>();
|
||||
if (entities.size() <= 8) {
|
||||
// Few paths are chained exactly; the local search below can stop short of the shortest chain.
|
||||
out = chain_segments_exact(end_points, reversible, start_near != nullptr ? &start : nullptr);
|
||||
} else {
|
||||
improve_chain_by_local_search(out, end_points, reversible, start_near != nullptr ? &start : nullptr);
|
||||
if (start_near != nullptr) {
|
||||
// The greedy chain can settle into a sweep that local moves cannot undo, which a nearest-next chain may avoid.
|
||||
std::vector<std::pair<size_t, bool>> nearest_next = chain_nearest_next(end_points, reversible, start);
|
||||
if (chain_travel(nearest_next, end_points, start) < chain_travel(out, end_points, start) - SCALED_EPSILON) {
|
||||
improve_chain_by_local_search(nearest_next, end_points, reversible, &start);
|
||||
out = std::move(nearest_next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (std::pair<size_t, bool> &segment : out) {
|
||||
const ExtrusionEntity *ee = entities[segment.first];
|
||||
if (ee->is_loop())
|
||||
@@ -1086,7 +1500,8 @@ void reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const s
|
||||
|
||||
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point &start_near)
|
||||
{
|
||||
chain_and_reorder_extrusion_entities(entities, &start_near);
|
||||
remove_entities_without_endpoints(entities);
|
||||
reorder_extrusion_entities(entities, chain_extrusion_entities_impl(entities, &start_near, true));
|
||||
}
|
||||
|
||||
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near)
|
||||
@@ -1101,7 +1516,7 @@ void chain_and_reorder_extrusion_entities(std::vector<const ExtrusionEntity*> &e
|
||||
remove_entities_without_endpoints(entities);
|
||||
std::vector<const ExtrusionEntity*> out;
|
||||
out.reserve(entities.size());
|
||||
for (const auto &[idx, reverse] : chain_extrusion_entities_impl(entities, &start_near)) {
|
||||
for (const auto &[idx, reverse] : chain_extrusion_entities_impl(entities, &start_near, true)) {
|
||||
if (reverse) {
|
||||
ExtrusionEntity *clone = reversed_clones.emplace_back(entities[idx]->clone()).get();
|
||||
clone->reverse();
|
||||
@@ -1159,16 +1574,74 @@ std::vector<size_t> chain_points(const Points &points, const Point *start_near)
|
||||
return out;
|
||||
}
|
||||
|
||||
// Moves runs of up to three points of the cycle next to one of the 8 points nearest to an end of the run, reversed or not,
|
||||
// while that shortens the cycle. path[0] stays first. Returns true if any move was made.
|
||||
static bool improve_cycle_by_or_opt(std::vector<size_t> &path, const Points &points)
|
||||
{
|
||||
const size_t pn = path.size();
|
||||
if (pn < 5)
|
||||
return false;
|
||||
std::vector<Vec2d> coords(points.size());
|
||||
for (size_t i = 0; i < points.size(); ++ i)
|
||||
coords[i] = points[i].cast<double>();
|
||||
const NearestPoints<8> grid(coords);
|
||||
std::vector<size_t> pos(points.size());
|
||||
auto refresh = [&]() { for (size_t k = 0; k < pn; ++ k) pos[path[k]] = k; };
|
||||
refresh();
|
||||
auto dist = [&](size_t a, size_t b) { return (coords[path[a % pn]] - coords[path[b % pn]]).norm(); };
|
||||
bool improved = false;
|
||||
for (bool moved = true; moved;) {
|
||||
moved = false;
|
||||
for (size_t len = 1; len <= 3; ++ len)
|
||||
for (size_t i = 1; i + len <= pn; ++ i) {
|
||||
// The run path[i..last] goes between path[k] and path[k + 1].
|
||||
const size_t last = i + len - 1;
|
||||
const double removal = dist(i - 1, i) + dist(last, last + 1) - dist(i - 1, last + 1);
|
||||
bool applied = removal <= SCALED_EPSILON;
|
||||
for (size_t end : { path[i], path[last] })
|
||||
for (size_t q : grid.query(coords[end], [end](size_t idx) { return idx != end; }))
|
||||
for (size_t k : { q == NearestPoints<8>::npos ? pn : (pos[q] + pn - 1) % pn, q == NearestPoints<8>::npos ? pn : pos[q] }) {
|
||||
if (applied || k == pn || (k + 1 >= i && k <= last))
|
||||
continue;
|
||||
const double base = dist(k, k + 1);
|
||||
const double kept = dist(k, i) + dist(last, k + 1) - base;
|
||||
const double reversed = dist(k, last) + dist(i, k + 1) - base;
|
||||
if (std::min(kept, reversed) > removal - SCALED_EPSILON)
|
||||
continue;
|
||||
std::vector<size_t> run(path.begin() + i, path.begin() + last + 1);
|
||||
if (reversed < kept)
|
||||
std::reverse(run.begin(), run.end());
|
||||
path.erase(path.begin() + i, path.begin() + last + 1);
|
||||
path.insert(path.begin() + (k < i ? k + 1 : k + 1 - len), run.begin(), run.end());
|
||||
refresh();
|
||||
applied = moved = improved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return improved;
|
||||
}
|
||||
|
||||
std::vector<size_t> chain_points_with_postprocessing(const Points &points, const Point *start_near)
|
||||
{
|
||||
std::vector<size_t> path = chain_points(points, start_near);
|
||||
// Alternate 2-opt and crossing removal until convergence.
|
||||
// 2-opt can create new crossings, and crossing removal can create new
|
||||
// opportunities for 2-opt improvement. Break early if neither improves.
|
||||
for (int iter = 0; iter < 3; ++iter) {
|
||||
bool improved = tsp_2opt_improve(path, points);
|
||||
improved |= tsp_remove_crossings(path, points);
|
||||
if (!improved) break;
|
||||
std::vector<size_t> path;
|
||||
if (points.size() <= 10) {
|
||||
// Orca: few points are toured exactly, from the point nearest start_near toward its nearer neighbor.
|
||||
path = tsp_exact_cycle(points);
|
||||
if (start_near != nullptr && path.size() > 2) {
|
||||
auto dist = [&points](size_t a, const Point &b) { return (points[a] - b).cast<double>().squaredNorm(); };
|
||||
std::rotate(path.begin(), std::min_element(path.begin(), path.end(), [&](size_t a, size_t b) { return dist(a, *start_near) < dist(b, *start_near); }), path.end());
|
||||
if (dist(path.back(), points[path.front()]) < dist(path[1], points[path.front()]))
|
||||
std::reverse(path.begin() + 1, path.end());
|
||||
}
|
||||
} else {
|
||||
path = chain_points(points, start_near);
|
||||
// Alternate 2-opt, or-opt and crossing removal: each can open moves for the others. Break early if none improves.
|
||||
for (int iter = 0; iter < 3; ++iter) {
|
||||
bool improved = tsp_2opt_improve(path, points);
|
||||
improved |= improve_cycle_by_or_opt(path, points);
|
||||
improved |= tsp_remove_crossings(path, points);
|
||||
if (!improved) break;
|
||||
}
|
||||
}
|
||||
if (start_near == nullptr)
|
||||
tsp_rotate_minimize_closing(path, points);
|
||||
|
||||
@@ -23,9 +23,12 @@ std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy);
|
||||
|
||||
std::vector<std::pair<size_t, bool>> chain_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr);
|
||||
void reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const std::vector<std::pair<size_t, bool>> &chain);
|
||||
// From start_near, the greedy chain is refined by 2-opt and or-opt moves, which shortens the travel at some extra cost.
|
||||
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point &start_near);
|
||||
// The greedy chain only.
|
||||
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr);
|
||||
// Each entity the chain reverses is replaced by a reversed clone that reversed_clones owns, so the originals stay unchanged.
|
||||
// Refined as above. Each entity the chain reverses is replaced by a reversed clone that reversed_clones owns, so the
|
||||
// originals stay unchanged.
|
||||
void chain_and_reorder_extrusion_entities(std::vector<const ExtrusionEntity*> &entities, const Point &start_near,
|
||||
std::vector<std::unique_ptr<ExtrusionEntity>> &reversed_clones);
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ void BackgroundSlicingProcess::process_fff()
|
||||
// BBS: add the logic to process from an existed gcode file
|
||||
if (m_print->finished()) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: skip slicing, to process previous gcode file") % __LINE__;
|
||||
m_fff_print->set_status(80, _utf8(L("Processing G-Code from previous file\u2026")));
|
||||
m_fff_print->set_status(80, _utf8(L("Processing G-code from previous file\u2026" /* \u2026 -> ... */)));
|
||||
wxCommandEvent evt(m_event_slicing_completed_id);
|
||||
// Post the Slicing Finished message for the G-code viewer to update.
|
||||
// Passing the timestamp
|
||||
|
||||
@@ -435,7 +435,7 @@ void CalibrationWizard::cache_preset_info(MachineObject *obj, float nozzle_dia,
|
||||
if (obj->is_multi_extruders()) {
|
||||
int ams_id, slot_id, tray_id;
|
||||
get_tray_ams_and_slot_id(curr_obj, result.tray_id, ams_id, slot_id, tray_id);
|
||||
result.extruder_id = preset_page->get_extruder_id_by_tray_id(result.tray_id);
|
||||
result.extruder_id = preset_page->get_extruder_id(ams_id);
|
||||
result.nozzle_diameter = preset_page->get_nozzle_diameter(result.extruder_id);
|
||||
}
|
||||
else {
|
||||
@@ -753,7 +753,7 @@ void PressureAdvanceWizard::on_cali_start()
|
||||
|
||||
X1CCalibInfos::X1CCalibInfo calib_info;
|
||||
get_tray_ams_and_slot_id(curr_obj, item.first, calib_info.ams_id, calib_info.slot_id, calib_info.tray_id);
|
||||
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(item.first);
|
||||
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
|
||||
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
|
||||
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
|
||||
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
|
||||
@@ -792,7 +792,7 @@ void PressureAdvanceWizard::on_cali_start()
|
||||
CalibInfo calib_info;
|
||||
calib_info.dev_id = curr_obj->get_dev_id();
|
||||
get_tray_ams_and_slot_id(curr_obj, selected_filaments.begin()->first, calib_info.ams_id, calib_info.slot_id, selected_tray_id);
|
||||
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(selected_filaments.begin()->first);
|
||||
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
|
||||
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
|
||||
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
|
||||
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
|
||||
@@ -877,7 +877,7 @@ void PressureAdvanceWizard::on_cali_start()
|
||||
calib_info.dev_id = curr_obj->get_dev_id();
|
||||
get_tray_ams_and_slot_id(curr_obj, item.first, calib_info.ams_id, calib_info.slot_id, selected_tray_id);
|
||||
calib_info.index = preset_page->get_index_by_tray_id(item.first);
|
||||
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(item.first);
|
||||
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
|
||||
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
|
||||
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
|
||||
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
|
||||
@@ -1252,7 +1252,7 @@ void FlowRateWizard::on_cali_start(CaliPresetStage stage, float cali_value, Flow
|
||||
X1CCalibInfos::X1CCalibInfo calib_info;
|
||||
calib_info.tray_id = item.first;
|
||||
get_tray_ams_and_slot_id(curr_obj, item.first, calib_info.ams_id, calib_info.slot_id, calib_info.tray_id);
|
||||
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(item.first);
|
||||
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
|
||||
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
|
||||
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
|
||||
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
|
||||
@@ -1308,15 +1308,7 @@ void FlowRateWizard::on_cali_start(CaliPresetStage stage, float cali_value, Flow
|
||||
int selected_tray_id = 0;
|
||||
get_tray_ams_and_slot_id(curr_obj, selected_filaments.begin()->first, calib_info.ams_id, calib_info.slot_id, selected_tray_id);
|
||||
calib_info.select_ams = std::to_string(selected_tray_id);
|
||||
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(selected_filaments.begin()->first);
|
||||
if (from_page == FlowRatioCaliSource::FROM_COARSE_PAGE) {
|
||||
// The fine pass must run on the extruder the coarse pass used. The cache records it;
|
||||
// the preset page's selection may have changed or been reset since.
|
||||
const auto cached = get_cached_selected_filament_for_multi_extruder(curr_obj);
|
||||
const auto it = cached.find(selected_filaments.begin()->first);
|
||||
if (it != cached.end())
|
||||
calib_info.extruder_id = it->second.extruder_id;
|
||||
}
|
||||
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
|
||||
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
|
||||
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
|
||||
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
|
||||
@@ -1732,7 +1724,7 @@ void MaxVolumetricSpeedWizard::on_cali_start()
|
||||
int selected_tray_id = 0;
|
||||
get_tray_ams_and_slot_id(curr_obj, selected_filaments.begin()->first, calib_info.ams_id, calib_info.slot_id, selected_tray_id);
|
||||
calib_info.select_ams = std::to_string(selected_tray_id);
|
||||
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(selected_filaments.begin()->first);
|
||||
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
|
||||
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
|
||||
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
|
||||
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
|
||||
|
||||
@@ -587,23 +587,8 @@ void CalibrationPresetPage::on_sys_color_changed()
|
||||
m_ams_sync_button->msw_rescale();
|
||||
}
|
||||
|
||||
int CalibrationPresetPage::get_extruder_id_by_tray_id(int tray_id)
|
||||
int CalibrationPresetPage::get_extruder_id(int ams_id)
|
||||
{
|
||||
// An AMS behind a Filament Track Switch feeds both extruders, so the extruder is the one
|
||||
// whose panel the filament was selected in.
|
||||
if (curr_obj && curr_obj->is_multi_extruders()) {
|
||||
for (FilamentComboBox *fcb : get_selected_filament_combobox()) {
|
||||
if (fcb->get_tray_id() != tray_id)
|
||||
continue;
|
||||
const bool is_deputy = std::find(m_deputy_filament_comboBox_list.begin(), m_deputy_filament_comboBox_list.end(), fcb) !=
|
||||
m_deputy_filament_comboBox_list.end();
|
||||
return is_deputy ? DEPUTY_EXTRUDER_ID : MAIN_EXTRUDER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
int ams_id = tray_id / 4, slot_id, unused_tray_id;
|
||||
if (curr_obj)
|
||||
get_tray_ams_and_slot_id(curr_obj, tray_id, ams_id, slot_id, unused_tray_id);
|
||||
if (m_ams_id_to_extruder_id_map.find(ams_id) != m_ams_id_to_extruder_id_map.end()) {
|
||||
return m_ams_id_to_extruder_id_map[ams_id];
|
||||
}
|
||||
@@ -881,18 +866,6 @@ static void fill_nozzle_volume_combo(ComboBox *combo)
|
||||
select_nozzle_volume(combo, NozzleVolumeType::nvtStandard);
|
||||
}
|
||||
|
||||
static DynamicPrintConfig empty_tray_config()
|
||||
{
|
||||
DynamicPrintConfig config;
|
||||
config.set_key_value("filament_id", new ConfigOptionStrings{""});
|
||||
config.set_key_value("tag_uid", new ConfigOptionStrings{""});
|
||||
config.set_key_value("filament_type", new ConfigOptionStrings{""});
|
||||
config.set_key_value("tray_name", new ConfigOptionStrings{""});
|
||||
config.set_key_value("filament_colour", new ConfigOptionStrings{""});
|
||||
config.set_key_value("filament_exist", new ConfigOptionBools{false});
|
||||
return config;
|
||||
}
|
||||
|
||||
void CalibrationPresetPage::init_selection_values()
|
||||
{
|
||||
// init nozzle diameter and nozzle volume
|
||||
@@ -2299,36 +2272,27 @@ void CalibrationPresetPage::sync_ams_info(MachineObject* obj)
|
||||
}
|
||||
}
|
||||
|
||||
// init virtual tray info. With a Filament Track Switch installed the external spools feed
|
||||
// through the switch, so they can't be calibrated as their own trays.
|
||||
if (obj->GetFilaSwitch()->IsInstalled()) {
|
||||
filament_ams_list[VIRTUAL_TRAY_MAIN_ID] = empty_tray_config();
|
||||
filament_ams_list[VIRTUAL_TRAY_DEPUTY_ID] = empty_tray_config();
|
||||
} else {
|
||||
if (full_filament_ams_list.find(VIRTUAL_TRAY_MAIN_ID) != full_filament_ams_list.end()) {
|
||||
filament_ams_list[VIRTUAL_TRAY_MAIN_ID] = full_filament_ams_list[VIRTUAL_TRAY_MAIN_ID];
|
||||
}
|
||||
// init virtual tray info
|
||||
if (full_filament_ams_list.find(VIRTUAL_TRAY_MAIN_ID) != full_filament_ams_list.end()) {
|
||||
filament_ams_list[VIRTUAL_TRAY_MAIN_ID] = full_filament_ams_list[VIRTUAL_TRAY_MAIN_ID];
|
||||
}
|
||||
|
||||
if (full_filament_ams_list.find(VIRTUAL_TRAY_DEPUTY_ID) != full_filament_ams_list.end()) {
|
||||
filament_ams_list[VIRTUAL_TRAY_DEPUTY_ID] = full_filament_ams_list[VIRTUAL_TRAY_DEPUTY_ID];
|
||||
}
|
||||
if (full_filament_ams_list.find(VIRTUAL_TRAY_DEPUTY_ID) != full_filament_ams_list.end()) {
|
||||
filament_ams_list[VIRTUAL_TRAY_DEPUTY_ID] = full_filament_ams_list[VIRTUAL_TRAY_DEPUTY_ID];
|
||||
}
|
||||
|
||||
// update filament from panel, display only obj has ams
|
||||
// update multi ams panel, display only obj has multi ams
|
||||
if (obj->HasAms()) {
|
||||
if (obj->is_multi_extruders()) {
|
||||
// An AMS behind a Filament Track Switch is bound to both extruders and appears in both panels.
|
||||
bool main_done = false;
|
||||
bool deputy_done = false;
|
||||
for (auto &ams_item : obj->GetFilaSystem()->GetAmsList()) {
|
||||
const auto &extruder_set = ams_item.second->GetBindedExtruderSet();
|
||||
if (!main_done && extruder_set.count(MAIN_EXTRUDER_ID) != 0) {
|
||||
update_multi_extruder_filament_combobox(ams_item.second->GetAmsId(), MAIN_EXTRUDER_ID);
|
||||
if (ams_item.second->GetExtruderId() == 0 && !main_done) {
|
||||
update_multi_extruder_filament_combobox(ams_item.second->GetAmsId(), ams_item.second->GetExtruderId());
|
||||
main_done = true;
|
||||
}
|
||||
if (!deputy_done && extruder_set.count(DEPUTY_EXTRUDER_ID) != 0) {
|
||||
update_multi_extruder_filament_combobox(ams_item.second->GetAmsId(), DEPUTY_EXTRUDER_ID);
|
||||
} else if (ams_item.second->GetExtruderId() == 1 && !deputy_done) {
|
||||
update_multi_extruder_filament_combobox(ams_item.second->GetAmsId(), ams_item.second->GetExtruderId());
|
||||
deputy_done = true;
|
||||
}
|
||||
}
|
||||
@@ -2369,11 +2333,10 @@ void CalibrationPresetPage::sync_ams_info(MachineObject* obj)
|
||||
if (ams->second->IsExist()
|
||||
&& info.parse_ams_info(obj, ams->second, obj->GetFilaSystem()->IsDetectRemainEnabled(), obj->is_support_ams_humidity)) {
|
||||
ams_info.push_back(info);
|
||||
for (int extruder_id : ams->second->GetBindedExtruderSet()) {
|
||||
if (extruder_id == MAIN_EXTRUDER_ID)
|
||||
main_ams_info.push_back(info);
|
||||
else
|
||||
deputy_ams_info.push_back(info);
|
||||
if (info.nozzle_id == 0) {
|
||||
main_ams_info.push_back(info);
|
||||
} else {
|
||||
deputy_ams_info.push_back(info);
|
||||
}
|
||||
}
|
||||
m_ams_id_to_extruder_id_map[stoi(info.ams_id)] = info.nozzle_id;
|
||||
@@ -2621,7 +2584,13 @@ void CalibrationPresetPage::update_multi_extruder_filament_combobox(const std::s
|
||||
}
|
||||
}
|
||||
|
||||
DynamicPrintConfig empty_config = empty_tray_config();
|
||||
DynamicPrintConfig empty_config;
|
||||
empty_config.set_key_value("filament_id", new ConfigOptionStrings{""});
|
||||
empty_config.set_key_value("tag_uid", new ConfigOptionStrings{""});
|
||||
empty_config.set_key_value("filament_type", new ConfigOptionStrings{""});
|
||||
empty_config.set_key_value("tray_name", new ConfigOptionStrings{""});
|
||||
empty_config.set_key_value("filament_colour", new ConfigOptionStrings{""});
|
||||
empty_config.set_key_value("filament_exist", new ConfigOptionBools{false});
|
||||
|
||||
if (filament_ams_list.empty()) return;
|
||||
|
||||
|
||||
@@ -251,7 +251,7 @@ public:
|
||||
void msw_rescale() override;
|
||||
void on_sys_color_changed() override;
|
||||
|
||||
int get_extruder_id_by_tray_id(int tray_id);
|
||||
int get_extruder_id(int ams_id);
|
||||
float get_nozzle_diameter(int extruder_id) const;
|
||||
NozzleVolumeType get_nozzle_volume_type(int extruder_id) const;
|
||||
ExtruderType get_extruder_type(int extruder_id) const;
|
||||
|
||||
@@ -303,10 +303,12 @@ nlohmann::json color_picker_ui_strings()
|
||||
return {
|
||||
{"Color Picker", _u8L("Color Picker")},
|
||||
{"Basic Colors", _u8L("Basic Colors")},
|
||||
{"Orca Palette", _u8L("Orca Palette")},// TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
{"RAL Classic", _u8L("RAL Classic")}, // TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
// TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
{"Orca Palette", _u8L("Orca Palette")},
|
||||
// TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
{"RAL Classic", _u8L("RAL Classic")},
|
||||
{"Gradients", _u8L("Gradients")},
|
||||
{"Solid", _u8L("Solid")},
|
||||
{"Solid", _u8L_CONTEXT("Solid", "Color picker")},
|
||||
{"Gradient", _u8L("Gradient")},
|
||||
{"Gradient start", _u8L("Gradient start")},
|
||||
{"Gradient end", _u8L("Gradient end")},
|
||||
@@ -440,7 +442,7 @@ nlohmann::json color_picker_ui_strings()
|
||||
{"Purple", _u8L("Purple")},
|
||||
{"Rebecca purple", _u8L("Rebecca purple")},
|
||||
{"Medium violet red", _u8L("Medium violet red")},
|
||||
{"PaleV violet red", _u8L("PaleV violet red")},
|
||||
{"Pale violet red", _u8L("Pale violet red")},
|
||||
{"Deep pink", _u8L("Deep pink")},
|
||||
{"Hot pink", _u8L("Hot pink")},
|
||||
{"Light pink", _u8L("Light pink")},
|
||||
@@ -496,7 +498,7 @@ nlohmann::json color_picker_ui_strings()
|
||||
{"Turquoise bay", _u8L("Turquoise bay")},
|
||||
{"Sky over reef", _u8L("Sky over reef")},
|
||||
{"Shallow sky", _u8L("Shallow sky")},
|
||||
{"Electric bel", _u8L("Electric bel")},
|
||||
{"Electric blue", _u8L("Electric blue")},
|
||||
{"Dolphin", _u8L("Dolphin")},
|
||||
{"Ocean depths", _u8L("Ocean depths")},
|
||||
{"Whale", _u8L("Whale")},
|
||||
@@ -519,7 +521,7 @@ nlohmann::json color_picker_ui_strings()
|
||||
{"Golden yellow", _u8L("Golden yellow")},
|
||||
{"Honey yellow", _u8L("Honey yellow")},
|
||||
{"Maize yellow", _u8L("Maize yellow")},
|
||||
{"Dalmatian yellow", _u8L("Dalmatian yellow")},
|
||||
{"Daffodil yellow", _u8L("Daffodil yellow")},
|
||||
{"Brown beige", _u8L("Brown beige")},
|
||||
{"Lemon yellow", _u8L("Lemon yellow")},
|
||||
{"Oyster white", _u8L("Oyster white")},
|
||||
@@ -606,7 +608,7 @@ nlohmann::json color_picker_ui_strings()
|
||||
{"Pigeon blue", _u8L("Pigeon blue")},
|
||||
{"Traffic blue", _u8L("Traffic blue")},
|
||||
{"Turquoise blue", _u8L("Turquoise blue")},
|
||||
{"Capsanthin violet", _u8L("Capsanthin violet")},
|
||||
{"Capri blue", _u8L("Capri blue")},
|
||||
{"Ocean blue", _u8L("Ocean blue")},
|
||||
{"Water blue", _u8L("Water blue")},
|
||||
{"Night blue", _u8L("Night blue")},
|
||||
@@ -644,7 +646,7 @@ nlohmann::json color_picker_ui_strings()
|
||||
{"Luminous green", _u8L("Luminous green")},
|
||||
{"Squirrel grey", _u8L("Squirrel grey")},
|
||||
{"Silver grey", _u8L("Silver grey")},
|
||||
{"Olivestone", _u8L("Olivestone")},
|
||||
{"Olive grey", _u8L("Olive grey")},
|
||||
{"Moss grey", _u8L("Moss grey")},
|
||||
{"Signal grey", _u8L("Signal grey")},
|
||||
{"Mouse grey", _u8L("Mouse grey")},
|
||||
|
||||
@@ -843,17 +843,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
bool have_multiline_infill_pattern = pattern == ipGyroid || pattern == ipGrid || pattern == ipRectilinear || pattern == ipTpmsD || pattern == ipTpmsFK || pattern == ipCrossHatch || pattern == ipHoneycomb || pattern == ipLateralLattice || pattern == ipLateralHoneycomb || pattern == ipConcentric ||
|
||||
pattern == ipCubic || pattern == ipStars || pattern == ipAlignedRectilinear || pattern == ipLightning || pattern == ip3DHoneycomb || pattern == ipAdaptiveCubic || pattern == ipSupportCubic|| pattern == ipTriangles || pattern == ipQuarterCubic|| pattern == ipArchimedeanChords || pattern == ipHilbertCurve || pattern == ipOctagramSpiral;
|
||||
|
||||
// The sparse infill density is the surface density of the adaptive TPMS infill; at 100% the infill is solid.
|
||||
bool have_tpms_infill = have_infill && config->option<ConfigOptionPercent>("sparse_infill_density")->value < 100 &&
|
||||
is_tpms_adaptive_pattern(pattern);
|
||||
toggle_line("tpms_adaptive", have_tpms_infill);
|
||||
bool have_tpms_adaptive = have_tpms_infill && config->opt_enum<TpmsAdaptiveMode>("tpms_adaptive") != TpmsAdaptiveMode::Disabled;
|
||||
toggle_line("tpms_interior_density", have_tpms_adaptive);
|
||||
toggle_line("tpms_adaptive_gradient", have_tpms_adaptive);
|
||||
// The sparse infill density is the surface density of the adaptive infill; at 100% the infill is solid.
|
||||
bool have_adaptive_infill_pattern = have_infill && config->option<ConfigOptionPercent>("sparse_infill_density")->value < 100 &&
|
||||
is_adaptive_infill_pattern(pattern);
|
||||
toggle_line("adaptive_infill_mode", have_adaptive_infill_pattern);
|
||||
bool have_adaptive_infill = have_adaptive_infill_pattern && config->opt_enum<AdaptiveInfillMode>("adaptive_infill_mode") != AdaptiveInfillMode::Disabled;
|
||||
toggle_line("adaptive_infill_interior_density", have_adaptive_infill);
|
||||
toggle_line("adaptive_infill_gradient", have_adaptive_infill);
|
||||
|
||||
// gyroid_optimized only applies when the sparse infill pattern is gyroid without adaptive density;
|
||||
// hide the whole line otherwise.
|
||||
toggle_line("gyroid_optimized", have_infill && pattern == ipGyroid && !have_tpms_adaptive);
|
||||
toggle_line("gyroid_optimized", have_infill && pattern == ipGyroid && !have_adaptive_infill);
|
||||
|
||||
// If there is infill, enable/disable fill_multiline according to whether the pattern supports multiline infill.
|
||||
if (have_infill) {
|
||||
@@ -1245,6 +1245,8 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
if(gcf_is_klipper)
|
||||
toggle_field("accel_to_decel_factor", config->opt_bool("accel_to_decel_enable"));
|
||||
|
||||
toggle_line("exclude_object_tolerance", gcf_is_klipper && config->opt_bool("exclude_object"));
|
||||
|
||||
bool have_make_overhang_printable = config->opt_bool("make_overhang_printable");
|
||||
toggle_line("make_overhang_printable_angle", have_make_overhang_printable);
|
||||
toggle_line("make_overhang_printable_hole_size", have_make_overhang_printable);
|
||||
|
||||
@@ -161,7 +161,7 @@ void DailyTipsDataRenderer::render_img(const ImVec2& start_pos, const ImVec2& si
|
||||
|
||||
bool has_cjk(const std::string &text)
|
||||
{
|
||||
for (size_t i = 0; i < text.size() - 2; ++i) {
|
||||
for (size_t i = 0; i + 2 < text.size(); ++i) {
|
||||
unsigned char c1 = text[i];
|
||||
unsigned char c2 = text[i + 1];
|
||||
unsigned char c3 = text[i + 2];
|
||||
|
||||
@@ -149,9 +149,9 @@ std::map<std::string, std::vector<SimpleSettingData>> SettingsFactory::PART_CATE
|
||||
{"sparse_infill_density", "", 1},
|
||||
{"fill_multiline", "", 1},
|
||||
{"sparse_infill_pattern", "", 1},
|
||||
{"tpms_adaptive", "", 1},
|
||||
{"tpms_interior_density", "", 1},
|
||||
{"tpms_adaptive_gradient", "", 1},
|
||||
{"adaptive_infill_mode", "", 1},
|
||||
{"adaptive_infill_interior_density", "", 1},
|
||||
{"adaptive_infill_gradient", "", 1},
|
||||
{"sparse_infill_smooth_factor", "", 1},
|
||||
{"lateral_lattice_angle_1", "", 1},
|
||||
{"lateral_lattice_angle_2", "", 1},
|
||||
|
||||
@@ -1350,7 +1350,7 @@ void GLGizmoSVG::draw_window()
|
||||
assert(m_volume_id.valid());
|
||||
if (m_volume == nullptr ||
|
||||
m_volume_id.invalid()) {
|
||||
ImGui::Text("%s", _u8L("Not valid state please report reproduction steps on github").c_str());
|
||||
ImGui::Text("%s", _u8L("Not valid state please report reproduction steps on GitHub").c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1362,13 +1362,13 @@ void GLGizmoSVG::draw_window()
|
||||
|
||||
assert(m_volume->emboss_shape->svg_file.has_value());
|
||||
if (!m_volume->emboss_shape->svg_file.has_value()){
|
||||
ImGui::Text("%s", _u8L("Missing svg file in embossed shape").c_str());
|
||||
ImGui::Text("%s", _u8L("Missing SVG file in embossed shape").c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
assert(m_volume->emboss_shape->svg_file->file_data != nullptr);
|
||||
if (m_volume->emboss_shape->svg_file->file_data == nullptr){
|
||||
ImGui::Text("%s", _u8L("Missing data of svg file").c_str());
|
||||
ImGui::Text("%s", _u8L("Missing data of SVG file").c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -295,7 +295,7 @@ void NetworkTestDialog::start_test_url(TestJob job, wxString name, wxString url)
|
||||
}
|
||||
})
|
||||
.on_ip_resolve([this,name,job](std::string ip) {
|
||||
wxString ip_report = wxString::Format(_L("test %s ip resolved = %s"), name, ip);
|
||||
wxString ip_report = wxString::Format(_L("test %s IP resolved = %s"), name, ip);
|
||||
update_status(job, ip_report);
|
||||
})
|
||||
.on_error([this,name,job](std::string body, std::string error, unsigned int status) {
|
||||
|
||||
@@ -228,6 +228,7 @@
|
||||
#include "Jobs/PlaterWorker.hpp"
|
||||
#include "Jobs/BoostThreadWorker.hpp"
|
||||
#include "BackgroundSlicingProcess.hpp"
|
||||
#include "PostProcessor.hpp"
|
||||
#include "SelectMachine.hpp"
|
||||
#include "SendMultiMachinePage.hpp"
|
||||
#include "SendToPrinter.hpp"
|
||||
@@ -17865,6 +17866,9 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params)
|
||||
filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false});
|
||||
print_config->set_key_value("layer_height", new ConfigOptionFloat(0.2));
|
||||
set_config_values<bool, ConfigOptionBools>(print_config, "enable_overhang_speed", false);
|
||||
// A 100% factor sends Klipper MINIMUM_CRUISE_RATIO=0 with every acceleration change, for the whole test.
|
||||
print_config->set_key_value("accel_to_decel_enable", new ConfigOptionBool(true));
|
||||
print_config->set_key_value("accel_to_decel_factor", new ConfigOptionPercent(100));
|
||||
print_config->set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
|
||||
print_config->set_key_value("wall_loops", new ConfigOptionInt(1));
|
||||
print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0));
|
||||
@@ -17929,6 +17933,9 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params)
|
||||
filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats { 0.0 });
|
||||
filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false});
|
||||
set_config_values<bool, ConfigOptionBools>(print_config, "enable_overhang_speed", false);
|
||||
// A 100% factor sends Klipper MINIMUM_CRUISE_RATIO=0 with every acceleration change, for the whole test.
|
||||
print_config->set_key_value("accel_to_decel_enable", new ConfigOptionBool(true));
|
||||
print_config->set_key_value("accel_to_decel_factor", new ConfigOptionPercent(100));
|
||||
print_config->set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
|
||||
print_config->set_key_value("wall_loops", new ConfigOptionInt(1));
|
||||
print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0));
|
||||
@@ -19572,7 +19579,11 @@ void Plater::export_gcode_3mf(bool export_all)
|
||||
SaveStrategy strategy = SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel; // BBS: silence
|
||||
if (wxGetApp().app_config->get_bool("export_statistics"))
|
||||
strategy = strategy | SaveStrategy::WithStatistics;
|
||||
export_3mf(output_path, strategy, plate_idx);
|
||||
if (export_3mf(output_path, strategy, plate_idx) < 0) {
|
||||
// No file was written (e.g. a post-processing script failed), don't report a successful export.
|
||||
p->exporting_status = ExportingStatus::NOT_EXPORTING;
|
||||
return;
|
||||
}
|
||||
|
||||
RemovableDriveManager& removable_drive_manager = *wxGetApp().removable_drive_manager();
|
||||
|
||||
@@ -20429,6 +20440,45 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy
|
||||
p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx,
|
||||
strategy & SaveStrategy::WithStatistics);
|
||||
|
||||
// Orca: non-BBL G-code isn't post-processed in place, so run the scripts on a copy before packing it.
|
||||
std::vector<std::string> pp_tmp_files;
|
||||
ScopeGuard remove_pp_tmp_files([&pp_tmp_files]() {
|
||||
for (const std::string& f : pp_tmp_files) {
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(f, ec);
|
||||
}
|
||||
});
|
||||
|
||||
// Backups (SkipStatic) don't pack G-code, so don't run user scripts for them.
|
||||
if ((strategy & SaveStrategy::WithGcode) && !(strategy & SaveStrategy::SkipStatic) &&
|
||||
p->printer_technology == ptFFF && !wxGetApp().preset_bundle->is_bbl_vendor()) {
|
||||
try {
|
||||
for (PlateData* pd : plate_data_list) {
|
||||
if (pd == nullptr || pd->gcode_file.empty())
|
||||
continue;
|
||||
PartPlate* plate = p->partplate_list.get_plate(pd->plate_index);
|
||||
if (plate == nullptr)
|
||||
continue;
|
||||
PrintBase* print = nullptr;
|
||||
plate->get_print(&print, nullptr, nullptr);
|
||||
Print* fff_print = dynamic_cast<Print*>(print);
|
||||
if (fff_print == nullptr)
|
||||
continue;
|
||||
|
||||
std::string src_path = pd->gcode_file; // in/out: becomes the post-processed copy if a script ran
|
||||
std::string output_name = path_u8; // scripts see the real target; a rename is ignored for 3mf
|
||||
if (run_post_process_scripts(src_path, true, "File", output_name, fff_print->full_print_config())) {
|
||||
pp_tmp_files.push_back(src_path);
|
||||
pd->gcode_file = src_path;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
release_PlateData_list(plate_data_list);
|
||||
show_error(this, ex.what(), false);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// BBS: backup
|
||||
PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
|
||||
std::vector<Preset*> project_presets = preset_bundle.get_current_project_embedded_presets();
|
||||
|
||||
@@ -395,7 +395,7 @@ bool run_post_process_scripts(
|
||||
|
||||
auto gcode_file = boost::filesystem::path(path);
|
||||
if (!boost::filesystem::exists(gcode_file))
|
||||
throw Slic3r::RuntimeError(_u8L("Post-processor can't find exported gcode file"));
|
||||
throw Slic3r::RuntimeError(_u8L("Post-processor can't find exported G-code file"));
|
||||
|
||||
// Store print configuration into environment variables.
|
||||
config.setenv_();
|
||||
|
||||
@@ -2968,9 +2968,9 @@ void TabPrint::build()
|
||||
optgroup->append_single_option_line("sparse_infill_density", "strength_settings_infill#sparse-infill-density");
|
||||
optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline");
|
||||
optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern");
|
||||
optgroup->append_single_option_line("tpms_adaptive", "strength_settings_patterns#adaptive-density");
|
||||
optgroup->append_single_option_line("tpms_interior_density", "strength_settings_patterns#interior-density");
|
||||
optgroup->append_single_option_line("tpms_adaptive_gradient", "strength_settings_patterns#adaptive-gradient");
|
||||
optgroup->append_single_option_line("adaptive_infill_mode", "strength_settings_patterns#adaptive-density");
|
||||
optgroup->append_single_option_line("adaptive_infill_interior_density", "strength_settings_patterns#interior-density");
|
||||
optgroup->append_single_option_line("adaptive_infill_gradient", "strength_settings_patterns#adaptive-gradient");
|
||||
optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized");
|
||||
optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_infill#sparse-infill-smooth-factor");
|
||||
optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction");
|
||||
@@ -3269,6 +3269,7 @@ void TabPrint::build()
|
||||
optgroup->append_single_option_line("gcode_comments", "others_settings_g_code_output#verbose-g-code");
|
||||
optgroup->append_single_option_line("gcode_label_objects", "others_settings_g_code_output#label-objects");
|
||||
optgroup->append_single_option_line("exclude_object", "others_settings_g_code_output#exclude-objects");
|
||||
optgroup->append_single_option_line("exclude_object_tolerance", "others_settings_g_code_output#outline-tolerance");
|
||||
option = optgroup->get_option("filename_format");
|
||||
// option.opt.full_width = true;
|
||||
option.opt.is_code = true;
|
||||
@@ -5278,7 +5279,7 @@ void TabPrinter::build_fff()
|
||||
option.opt.width = Field::def_width_wider();
|
||||
option.opt.tooltip = L("Select the network agent implementation for printer communication. "
|
||||
"Available agents are registered at startup.");
|
||||
optgroup->append_single_option_line(option);
|
||||
optgroup->append_single_option_line(option, "printer_basic_information_advanced#printer-agent");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1415,16 +1415,8 @@ void MoonrakerPrinterAgent::announce_printhost_device()
|
||||
}
|
||||
}
|
||||
|
||||
// Try to fetch actual device name from Moonraker
|
||||
// Priority: 1) Moonraker hostname, 2) Preset model name, 3) Generic fallback
|
||||
std::string dev_name;
|
||||
MoonrakerDeviceInfo info;
|
||||
std::string fetch_error;
|
||||
if (fetch_device_info(device_info.base_url, device_info.api_key, info, fetch_error) && !info.dev_name.empty()) {
|
||||
dev_name = info.dev_name;
|
||||
} else {
|
||||
dev_name = device_info.model_name.empty() ? "Moonraker Printer" : device_info.model_name;
|
||||
}
|
||||
// Never asks the printer, since discovery runs on the GUI thread and an unreachable printer would freeze it
|
||||
const std::string dev_name = device_info.model_name.empty() ? "Moonraker Printer" : device_info.model_name;
|
||||
|
||||
const std::string model_id = device_info.model_id;
|
||||
|
||||
|
||||
@@ -1917,13 +1917,20 @@ RefreshResult OrcaCloudServiceAgent::refresh_from_storage(const std::string& rea
|
||||
|
||||
bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const std::string& reason)
|
||||
{
|
||||
bool needs_refresh = false;
|
||||
{
|
||||
const auto needs_refresh = [&] {
|
||||
std::lock_guard<std::mutex> lock(session_mutex);
|
||||
needs_refresh = should_refresh_locked(skew);
|
||||
}
|
||||
return should_refresh_locked(skew);
|
||||
};
|
||||
if (!needs_refresh())
|
||||
return true;
|
||||
|
||||
if (!needs_refresh)
|
||||
// A refresh may already be running on another thread, e.g. the one start() launches. Wait for
|
||||
// it and check again instead of spending the refresh token it is rotating. Bounded: the
|
||||
// refresh POST times out after 30 s.
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60);
|
||||
while (refresh_running.load() && std::chrono::steady_clock::now() < deadline)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
if (!needs_refresh())
|
||||
return true;
|
||||
|
||||
// First attempt. refresh_from_storage blocks on the cross-process lock and reads the
|
||||
@@ -1935,6 +1942,8 @@ bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const
|
||||
// One retry, only for a transient network failure of the refresh POST (lost response,
|
||||
// timeout, 429/5xx -> Transient). The retry re-acquires the lock and re-reads the store.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(750));
|
||||
if (!needs_refresh())
|
||||
return true;
|
||||
return refresh_from_storage(reason + "_retry", false) == RefreshResult::Success;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <cstddef>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <ios>
|
||||
#include <slic3r/plugin/PluginDescriptor.hpp>
|
||||
@@ -49,16 +50,22 @@ bool CloudPluginService::fetch_manifests_into_descriptors(std::vector<PluginDesc
|
||||
unauthorized.clear();
|
||||
|
||||
if (m_orca_agent) {
|
||||
// Refresh the token once, then run both requests in parallel.
|
||||
m_orca_agent->ensure_token_fresh("fetch_plugin_manifests");
|
||||
std::vector<PluginDescriptor> mine_descriptors;
|
||||
auto mine = std::async(std::launch::async, [agent = m_orca_agent, &mine_descriptors] {
|
||||
return agent->fetch_mine_manifests_into_descriptors(mine_descriptors);
|
||||
});
|
||||
|
||||
int ret = m_orca_agent->fetch_subscribed_manifests_into_descriptors(descriptors, not_found, unauthorized);
|
||||
const int mine_ret = mine.get();
|
||||
|
||||
if (ret != 0) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get subscribed plugins.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<PluginDescriptor> mine_descriptors;
|
||||
|
||||
ret = m_orca_agent->fetch_mine_manifests_into_descriptors(mine_descriptors);
|
||||
ret = mine_ret;
|
||||
|
||||
if (ret != 0) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get owned plugins.";
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
#include "libslic3r/Fill/Fill.hpp"
|
||||
#include "libslic3r/Fill/FillAdaptive.hpp"
|
||||
#include "libslic3r/Fill/FillGyroid.hpp"
|
||||
#include "libslic3r/Fill/FillTpmsAdaptive.hpp"
|
||||
#include "libslic3r/Fill/AdaptiveInfillField.hpp"
|
||||
#include "libslic3r/Flow.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/IntersectionPoints.hpp"
|
||||
@@ -1719,9 +1719,10 @@ TEST_CASE("Smoothing multiline lightning infill keeps its outlines connected", "
|
||||
const SparseInfillShape smooth = shape_for("100%");
|
||||
|
||||
REQUIRE(sharp.path_count > 0);
|
||||
// Smoothing should leave the loop count about where it was; uncapping the smoothing reach, the
|
||||
// regression this guards against, adds about 10%.
|
||||
const size_t allowed_extra = sharp.path_count / 50; // 2%
|
||||
// Smoothing should leave the path count about where it was; uncapping the smoothing reach, the
|
||||
// regression this guards against, adds a quarter to a third. Outline ends next to each other are
|
||||
// joined into one path, so a few loops more or less are a larger share of the count.
|
||||
const size_t allowed_extra = sharp.path_count / 10; // 10%
|
||||
REQUIRE(smooth.path_count <= sharp.path_count + allowed_extra);
|
||||
// The outlines are still rounded.
|
||||
REQUIRE(smooth.point_count > sharp.point_count);
|
||||
@@ -2060,9 +2061,9 @@ TEST_CASE("Adaptive TPMS infill thins out from the surface to the interior densi
|
||||
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(60)}, print,
|
||||
{{"sparse_infill_pattern", pattern},
|
||||
{"sparse_infill_density", "25%"},
|
||||
{"tpms_adaptive", adaptive},
|
||||
{"tpms_interior_density", "5%"},
|
||||
{"tpms_adaptive_gradient", "linear"},
|
||||
{"adaptive_infill_mode", adaptive},
|
||||
{"adaptive_infill_interior_density", "5%"},
|
||||
{"adaptive_infill_gradient", "linear"},
|
||||
{"layer_height", 0.4},
|
||||
{"initial_layer_print_height", 0.4}});
|
||||
};
|
||||
@@ -2089,9 +2090,9 @@ TEST_CASE("Adaptive TPMS infill of a tall object is sparsest at its middle heigh
|
||||
Slic3r::Test::init_and_process_print({make_cube(30., 30., 90.)}, print,
|
||||
{{"sparse_infill_pattern", "tpmsd"},
|
||||
{"sparse_infill_density", "25%"},
|
||||
{"tpms_adaptive", "lobes"},
|
||||
{"tpms_interior_density", "5%"},
|
||||
{"tpms_adaptive_gradient", "linear"},
|
||||
{"adaptive_infill_mode", "lobes"},
|
||||
{"adaptive_infill_interior_density", "5%"},
|
||||
{"adaptive_infill_gradient", "linear"},
|
||||
{"layer_height", 0.4},
|
||||
{"initial_layer_print_height", 0.4}});
|
||||
const Polygons core = centered_square(5.);
|
||||
@@ -2111,9 +2112,9 @@ TEST_CASE("2D adaptive TPMS infill does not change along its axis", "[Fill]")
|
||||
Slic3r::Test::init_and_process_print({along_x ? make_cube(90., 30., 30.) : make_cube(30., 30., 90.)}, print,
|
||||
{{"sparse_infill_pattern", "gyroid"},
|
||||
{"sparse_infill_density", "25%"},
|
||||
{"tpms_adaptive", adaptive},
|
||||
{"tpms_interior_density", "5%"},
|
||||
{"tpms_adaptive_gradient", "linear"},
|
||||
{"adaptive_infill_mode", adaptive},
|
||||
{"adaptive_infill_interior_density", "5%"},
|
||||
{"adaptive_infill_gradient", "linear"},
|
||||
{"layer_height", 0.4},
|
||||
{"initial_layer_print_height", 0.4}});
|
||||
};
|
||||
@@ -2147,9 +2148,9 @@ TEST_CASE("Adaptive TPMS shells and blend keep the whole deep core of a tall obj
|
||||
Slic3r::Test::init_and_process_print({make_cube(30., 30., 90.)}, print,
|
||||
{{"sparse_infill_pattern", "tpmsd"},
|
||||
{"sparse_infill_density", "25%"},
|
||||
{"tpms_adaptive", adaptive},
|
||||
{"tpms_interior_density", "5%"},
|
||||
{"tpms_adaptive_gradient", "linear"},
|
||||
{"adaptive_infill_mode", adaptive},
|
||||
{"adaptive_infill_interior_density", "5%"},
|
||||
{"adaptive_infill_gradient", "linear"},
|
||||
{"layer_height", 0.4},
|
||||
{"initial_layer_print_height", 0.4}});
|
||||
};
|
||||
@@ -2179,9 +2180,9 @@ TEST_CASE("Adaptive TPMS infill is sparse at the center of both of two united sp
|
||||
Slic3r::Test::init_and_process_print({TriangleMesh(spheres)}, print,
|
||||
{{"sparse_infill_pattern", "gyroid"},
|
||||
{"sparse_infill_density", "25%"},
|
||||
{"tpms_adaptive", adaptive},
|
||||
{"tpms_interior_density", "5%"},
|
||||
{"tpms_adaptive_gradient", "linear"},
|
||||
{"adaptive_infill_mode", adaptive},
|
||||
{"adaptive_infill_interior_density", "5%"},
|
||||
{"adaptive_infill_gradient", "linear"},
|
||||
{"layer_height", 0.4},
|
||||
{"initial_layer_print_height", 0.4}});
|
||||
};
|
||||
@@ -2209,9 +2210,9 @@ TEST_CASE("Adaptive TPMS gradients keep the surface density deeper in the order
|
||||
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(40)}, print,
|
||||
{{"sparse_infill_pattern", "tpmsd"},
|
||||
{"sparse_infill_density", "25%"},
|
||||
{"tpms_adaptive", "lobes"},
|
||||
{"tpms_interior_density", "5%"},
|
||||
{"tpms_adaptive_gradient", gradient},
|
||||
{"adaptive_infill_mode", "lobes"},
|
||||
{"adaptive_infill_interior_density", "5%"},
|
||||
{"adaptive_infill_gradient", gradient},
|
||||
{"layer_height", 0.4},
|
||||
{"initial_layer_print_height", 0.4}});
|
||||
return sparse_infill_length(print, 0., 40., centered_square(20.));
|
||||
@@ -2236,9 +2237,9 @@ TEST_CASE("Adaptive TPMS settings leave the infill unchanged when they do not ap
|
||||
{{"sparse_infill_pattern", pattern},
|
||||
{"sparse_infill_density", "20%"},
|
||||
{"layer_height", 0.2},
|
||||
{"tpms_adaptive", adaptive},
|
||||
{"tpms_interior_density", "40%"},
|
||||
{"tpms_adaptive_gradient", "exponential"}});
|
||||
{"adaptive_infill_mode", adaptive},
|
||||
{"adaptive_infill_interior_density", "40%"},
|
||||
{"adaptive_infill_gradient", "exponential"}});
|
||||
const SparseInfillShape expected = sparse_infill_shape(reference);
|
||||
REQUIRE(expected.path_count > 0);
|
||||
CHECK(sparse_infill_shape(tuned).sequence == expected.sequence);
|
||||
@@ -2252,7 +2253,7 @@ TEST_CASE("Adaptive gyroid infill ignores the Z-buckling optimization", "[Fill]"
|
||||
{{"sparse_infill_pattern", "gyroid"},
|
||||
{"sparse_infill_density", "10%"},
|
||||
{"gyroid_optimized", gyroid_optimized},
|
||||
{"tpms_adaptive", "lobes"},
|
||||
{"adaptive_infill_mode", "lobes"},
|
||||
{"layer_height", 0.2}});
|
||||
return sparse_infill_shape(print);
|
||||
};
|
||||
@@ -2268,10 +2269,10 @@ TEST_CASE("Adaptive TPMS infill of a region matches the infill of a larger regio
|
||||
// An 80 x 80 x 40 mm box around both regions, which share its radial field.
|
||||
const ExPolygons box{ExPolygon(Points{Point::new_scale(60., 20.), Point::new_scale(140., 20.), Point::new_scale(140., 100.),
|
||||
Point::new_scale(60., 100.)})};
|
||||
std::vector<TpmsRadialField::Slice> slices;
|
||||
std::vector<AdaptiveInfillField::Slice> slices;
|
||||
for (int i = 0; i < 200; ++i)
|
||||
slices.push_back({0.2 * i, 0.2 * (i + 1), &box});
|
||||
const TpmsRadialField field(slices, get_extents(box), TpmsAdaptiveMode::Lobes, [] {});
|
||||
const AdaptiveInfillField field(slices, get_extents(box), AdaptiveInfillMode::Lobes, [] {});
|
||||
|
||||
auto circle = [](double radius) {
|
||||
Polygon contour = make_circle_num_segments(scale_(radius), 120);
|
||||
@@ -2285,13 +2286,13 @@ TEST_CASE("Adaptive TPMS infill of a region matches the infill of a larger regio
|
||||
filler->spacing = 0.45;
|
||||
filler->angle = float(M_PI / 7.);
|
||||
filler->z = z;
|
||||
filler->tpms_radial_field = &field;
|
||||
filler->adaptive_infill_field = &field;
|
||||
|
||||
FillParams params;
|
||||
params.density = 0.2f;
|
||||
params.tpms_adaptive = TpmsAdaptiveMode::Lobes;
|
||||
params.tpms_interior_density = 0.05f;
|
||||
params.tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear;
|
||||
params.adaptive_infill_mode = AdaptiveInfillMode::Lobes;
|
||||
params.adaptive_infill_interior_density = 0.05f;
|
||||
params.adaptive_infill_gradient = AdaptiveInfillGradient::Linear;
|
||||
params.layer_height = 0.2;
|
||||
params.dont_adjust = true;
|
||||
Surface surface(stInternal, expolygon);
|
||||
@@ -2328,8 +2329,8 @@ TEST_CASE("Adaptive TPMS anchors match the printed infill", "[Fill][InternalBrid
|
||||
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(30)}, print,
|
||||
{{"sparse_infill_pattern", pattern},
|
||||
{"sparse_infill_density", "25%"},
|
||||
{"tpms_adaptive", "lobes"},
|
||||
{"tpms_interior_density", "5%"},
|
||||
{"adaptive_infill_mode", "lobes"},
|
||||
{"adaptive_infill_interior_density", "5%"},
|
||||
{"layer_height", 0.2},
|
||||
{"initial_layer_print_height", 0.2},
|
||||
{"resolution", 0.012}});
|
||||
@@ -2353,3 +2354,66 @@ TEST_CASE("Adaptive TPMS anchors match the printed infill", "[Fill][InternalBrid
|
||||
max_distance = std::max(max_distance, printed_tree.distance_from_lines<false>(point));
|
||||
CHECK(unscale<double>(max_distance) <= 0.012);
|
||||
}
|
||||
|
||||
TEST_CASE("Infill lines whose ends are next to each other join into one path", "[Fill]")
|
||||
{
|
||||
// An out-and-back pair 1 mm apart, as multiline infill leaves, and a line 19 mm away.
|
||||
Polylines lines { Polyline(Point::new_scale(0., 0.), Point::new_scale(10., 0.)), Polyline(Point::new_scale(0., 1.), Point::new_scale(10., 1.)),
|
||||
Polyline(Point::new_scale(0., 20.), Point::new_scale(10., 20.)) };
|
||||
join_close_ends(lines, scale_(1.5), true);
|
||||
|
||||
REQUIRE(lines.size() == 2);
|
||||
const auto joined = std::find_if(lines.begin(), lines.end(), [](const Polyline &pl) { return pl.size() == 4; });
|
||||
REQUIRE(joined != lines.end());
|
||||
// Both lines plus the 1 mm between them, joined at one end only, as joining the other end too would close a loop.
|
||||
CHECK_THAT(unscale<double>(joined->length()), Catch::Matchers::WithinAbs(21., 1e-6));
|
||||
CHECK(joined->first_point() != joined->last_point());
|
||||
}
|
||||
|
||||
TEST_CASE("Joined infill lines print each line once", "[Fill]")
|
||||
{
|
||||
// The middle line comes first and both of its ends get joined, so the group has to be walked from an outer line.
|
||||
Polylines lines { Polyline(Point::new_scale(0., 0.), Point::new_scale(10., 0.)), Polyline(Point::new_scale(0., 1.), Point::new_scale(10., 1.)),
|
||||
Polyline(Point::new_scale(10., -1.), Point::new_scale(0., -1.)) };
|
||||
join_close_ends(lines, scale_(1.5), true);
|
||||
|
||||
REQUIRE(lines.size() == 1);
|
||||
CHECK(lines.front().size() == 6);
|
||||
// Three 10 mm lines and two 1 mm joins.
|
||||
CHECK_THAT(unscale<double>(lines.front().length()), Catch::Matchers::WithinAbs(32., 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("Infill line ends are not joined across another line", "[Fill]")
|
||||
{
|
||||
// A pair 1 mm apart whose left ends have a third line running between them, which a join there would cross.
|
||||
Polylines lines { Polyline(Point::new_scale(0., 0.), Point::new_scale(10., 0.)), Polyline(Point::new_scale(0., 1.), Point::new_scale(10., 1.)),
|
||||
Polyline(Point::new_scale(-5., 0.5), Point::new_scale(5., 0.5)) };
|
||||
join_close_ends(lines, scale_(1.5), true);
|
||||
|
||||
REQUIRE(lines.size() == 2);
|
||||
const auto joined = std::find_if(lines.begin(), lines.end(), [](const Polyline &pl) { return pl.size() == 4; });
|
||||
REQUIRE(joined != lines.end());
|
||||
// Joined on the right, where nothing is in between, so the left ends stay open.
|
||||
CHECK(joined->first_point().x() == 0);
|
||||
CHECK(joined->last_point().x() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("Infill lines that cannot reverse join only end to start", "[Fill]")
|
||||
{
|
||||
const Point a(Point::new_scale(0., 0.)), b(Point::new_scale(10., 0.)), c(Point::new_scale(10., 1.)), d(Point::new_scale(0., 1.));
|
||||
SECTION("Ends that would need a reversal stay apart") {
|
||||
Polylines lines { Polyline(a, b), Polyline(d, c) };
|
||||
join_close_ends(lines, scale_(1.5), false);
|
||||
CHECK(lines.size() == 2);
|
||||
}
|
||||
SECTION("The end of one line joins the start of the next, both kept in their direction") {
|
||||
Polylines lines { Polyline(c, d), Polyline(a, b) };
|
||||
join_close_ends(lines, scale_(1.5), false);
|
||||
REQUIRE(lines.size() == 1);
|
||||
const Points &points = lines.front().points;
|
||||
REQUIRE(points.size() == 4);
|
||||
auto position = [&points](const Point &p) { return std::find(points.begin(), points.end(), p) - points.begin(); };
|
||||
CHECK(position(a) < position(b));
|
||||
CHECK(position(c) < position(d));
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,44 @@ TEST_CASE("Klipper object labels name each copy without the characters Klipper c
|
||||
}
|
||||
}
|
||||
|
||||
// The outline written to EXCLUDE_OBJECT_DEFINE is simplified with exclude_object_tolerance. A larger value must
|
||||
// leave fewer points, which keeps the exclude-object array small on printers that stall while parsing it.
|
||||
TEST_CASE("A larger exclude object tolerance writes fewer outline points", "[GCode]")
|
||||
{
|
||||
const auto slice_with_tolerance = [](const std::string &tolerance) {
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({{"gcode_flavor", "klipper"}, {"exclude_object", "1"},
|
||||
{"exclude_object_tolerance", tolerance}});
|
||||
// Default facet angle (2 degrees): 180 hull points, well inside either tolerance.
|
||||
return Test::slice({make_cylinder(20., 5.)}, config);
|
||||
};
|
||||
// Points of the POLYGON=[[x,y],...] on the first EXCLUDE_OBJECT_DEFINE line.
|
||||
const auto polygon_points = [](const std::string &gcode) {
|
||||
const size_t define = gcode.find("EXCLUDE_OBJECT_DEFINE");
|
||||
REQUIRE(define != std::string::npos);
|
||||
const size_t begin = gcode.find("POLYGON=", define);
|
||||
REQUIRE(begin != std::string::npos);
|
||||
const std::string polygon = gcode.substr(begin, gcode.find('\n', begin) - begin);
|
||||
size_t points = 1;
|
||||
for (size_t pos = polygon.find("],["); pos != std::string::npos; pos = polygon.find("],[", pos + 1))
|
||||
++points;
|
||||
return points;
|
||||
};
|
||||
|
||||
const size_t fine = polygon_points(slice_with_tolerance("0.1"));
|
||||
const size_t coarse = polygon_points(slice_with_tolerance("0.4"));
|
||||
INFO("points at 0.1 mm: " << fine << ", at 0.4 mm: " << coarse);
|
||||
CHECK(coarse >= 4);
|
||||
CHECK(coarse < fine);
|
||||
}
|
||||
|
||||
// Existing profiles must keep writing the outlines they wrote when the tolerance was a constant 0.1 mm.
|
||||
TEST_CASE("The exclude object tolerance defaults to the former fixed 0.1 mm", "[GCode]")
|
||||
{
|
||||
CHECK_THAT(DynamicPrintConfig::full_print_config().opt_float("exclude_object_tolerance"),
|
||||
Catch::Matchers::WithinAbs(0.1, 1e-9));
|
||||
}
|
||||
|
||||
// Firmwares read the compatibility header lines (Anycubic's GoKlipper heats the bed with
|
||||
// first_layer_bed_temperature), so they must describe the filament the plate prints, not filament 1.
|
||||
TEST_CASE("The G-code header names the first layer temperatures of the filament the plate prints", "[GCode]")
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/catch_message.hpp>
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/MultiNozzleUtils.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -30,6 +33,7 @@ namespace Slic3r { enum ExtrusionRole : uint8_t; }
|
||||
|
||||
using namespace Slic3r;
|
||||
using Catch::Matchers::WithinAbs;
|
||||
using Catch::Matchers::WithinRel;
|
||||
|
||||
// Regression coverage for filament/tool-change time being folded into the first
|
||||
// pending motion block (an extrusion move) instead of the tool-change move, and
|
||||
@@ -570,6 +574,33 @@ double planned_corner_speed(GCodeFlavor flavor, double corner_velocity, double j
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Klipper extrusion ratio changes use its own corner velocity, not E jerk", "[GCodeTiming][JunctionDeviation]")
|
||||
{
|
||||
const double jerk_e = GENERATE(0.0, 100.0);
|
||||
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
|
||||
config.machine_max_jerk_e.values = {jerk_e, jerk_e};
|
||||
GCodeProcessor processor;
|
||||
run_processor(processor, config,
|
||||
"G90\nM83\nSET_VELOCITY_LIMIT ACCEL=1000 MINIMUM_CRUISE_RATIO=0\n"
|
||||
"G1 X20 Y60 F6000\nM400\n"
|
||||
"G1 X60 Y60 E0.8 F6000\nG1 X100 Y60 E1.6 F6000\nM400\n");
|
||||
// A 0.02 -> 0.04 extrusion ratio change is capped at 1 / 0.02 = 50 mm/s.
|
||||
REQUIRE_THAT(corner_speed(processor.get_result()), Catch::Matchers::WithinAbs(50.0, 0.01));
|
||||
}
|
||||
|
||||
TEST_CASE("Klipper legacy smoothing stays absolute across acceleration changes", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
const bool modern = GENERATE(false, true);
|
||||
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
|
||||
const std::string gcode = std::string("G90\nSET_VELOCITY_LIMIT ACCEL=1000 ") +
|
||||
(modern ? "MINIMUM_CRUISE_RATIO=0.75\n" : "ACCEL_TO_DECEL=250\n") +
|
||||
"G1 X40 F30000\nM400\nM204 S2000\nG1 X0\nM400\n";
|
||||
GCodeProcessor processor;
|
||||
run_processor(processor, config, gcode.c_str());
|
||||
const double expected = modern ? 0.5 + 0.25 * std::sqrt(2.0) : 0.95;
|
||||
REQUIRE_THAT(processor.get_time(PrintEstimatedStatistics::ETimeMode::Normal), WithinAbs(expected, 1e-5));
|
||||
}
|
||||
|
||||
TEST_CASE("Klipper corners are planned with junction deviation derived from the square corner velocity",
|
||||
"[GCodeTiming][JunctionDeviation]")
|
||||
{
|
||||
@@ -669,3 +700,172 @@ TEST_CASE("How fast a corner is taken does not depend on how much is extruded th
|
||||
Catch::Matchers::WithinRel(marlin, 0.02));
|
||||
}
|
||||
}
|
||||
|
||||
// Expected times are Klipper's own planner math (klippy/toolhead.py). For a move from rest to rest,
|
||||
// LookAheadQueue.flush() reduces to cruise_v2 = min(vmax^2, d * accel, d * pseudo_accel) with
|
||||
// pseudo_accel = accel * (1 - minimum_cruise_ratio), and Move.set_junction() to a trapezoid at accel;
|
||||
// a move too short to reach its speed still spends that ratio of its length cruising.
|
||||
namespace {
|
||||
|
||||
constexpr double klipper_accel = 1000.0; // make_junction_config's acceleration
|
||||
constexpr double klipper_cruise_ratio = 0.5;
|
||||
constexpr double klipper_pseudo_accel = klipper_accel * (1.0 - klipper_cruise_ratio);
|
||||
|
||||
// Time of a move that starts and ends at rest.
|
||||
double rest_to_rest_time(double distance, double max_speed, double accel, double pseudo_accel)
|
||||
{
|
||||
const double cruise_v2 = std::min(sqr(max_speed), distance * pseudo_accel);
|
||||
const double cruise = std::sqrt(cruise_v2);
|
||||
return 2.0 * cruise / accel + (distance - cruise_v2 / accel) / cruise;
|
||||
}
|
||||
|
||||
// Every run states the cruise ratio rather than relying on the estimator's built-in default.
|
||||
void run_klipper(GCodeProcessor& proc, const std::string& gcode,
|
||||
const FullPrintConfig& config = make_junction_config(gcfKlipper, 5.0, 0.0))
|
||||
{
|
||||
const std::string limits = "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=" + std::to_string(klipper_cruise_ratio) + "\n";
|
||||
run_processor(proc, config, (limits + gcode).c_str());
|
||||
}
|
||||
|
||||
double klipper_time(const std::string& gcode, const FullPrintConfig& config = make_junction_config(gcfKlipper, 5.0, 0.0))
|
||||
{
|
||||
GCodeProcessor proc;
|
||||
run_klipper(proc, gcode, config);
|
||||
return proc.get_time(PrintEstimatedStatistics::ETimeMode::Normal);
|
||||
}
|
||||
|
||||
double time_after_retract(const GCodeProcessorResult& r)
|
||||
{
|
||||
double t = 0.0;
|
||||
bool after = false;
|
||||
for (const auto& mv : r.moves) {
|
||||
if (mv.type == EMoveType::Retract)
|
||||
after = true;
|
||||
else if (after)
|
||||
t += mv.time[NORMAL];
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("A straight line split into short segments is planned as one move", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
// 100 mm at 50 mm/s queues 2 s of motion, so the look-ahead flushes lazily part-way along the
|
||||
// line; at 10 mm/s^2 the ramps span many segments and the cruise ratio caps the speed.
|
||||
const int segments = GENERATE(1, 100, 1000);
|
||||
std::ostringstream gcode;
|
||||
gcode << "SET_VELOCITY_LIMIT ACCEL=10\n" << std::fixed << std::setprecision(3);
|
||||
for (int i = 1; i <= segments; ++i)
|
||||
gcode << "G1 X" << 100.0 * i / segments << " F3000\n";
|
||||
REQUIRE_THAT(klipper_time(gcode.str()),
|
||||
WithinRel(rest_to_rest_time(100.0, 50.0, 10.0, 10.0 * (1.0 - klipper_cruise_ratio)), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("Where the look-ahead flushes does not change the estimate", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
// A retraction stops the toolhead, so the zig-zag after it is planned on its own; the straight run
|
||||
// before it only shifts where the queue flushes lazily (1 s of motion after a stop, then every 0.15 s).
|
||||
auto zigzag_time = [](int run_moves) {
|
||||
std::string gcode = "M83\nG91\n";
|
||||
for (int i = 0; i < run_moves; ++i)
|
||||
gcode += "G1 X1 F6000\n";
|
||||
gcode += "G1 E-1 F2400\n";
|
||||
const char* moves[] = {"G1 X2 F3000\n", "G1 Y2 F9000\n", "G1 X2 F1200\n", "G1 Y-2 F9000\n"};
|
||||
for (int i = 0; i < 24; ++i)
|
||||
gcode += moves[i % 4];
|
||||
GCodeProcessor proc;
|
||||
run_klipper(proc, gcode);
|
||||
return time_after_retract(proc.get_result());
|
||||
};
|
||||
const double reference = zigzag_time(0);
|
||||
REQUIRE(reference > 0.0);
|
||||
for (int run_moves : {30, 80, 105, 130}) {
|
||||
INFO("run moves = " << run_moves);
|
||||
REQUIRE_THAT(zigzag_time(run_moves), WithinAbs(reference, 1e-9));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("A short move into a corner is limited by the centripetal term, not the corner velocity",
|
||||
"[GCodeTiming][Klipper]")
|
||||
{
|
||||
// A 50 mm/s square corner velocity allows 50 mm/s through the right angle; the 1 mm move into it
|
||||
// allows sqrt(0.5 * 1 * 1000 * tan(45 deg)) = 22.4 mm/s (Move.calc_junction's move_centripetal_v2).
|
||||
GCodeProcessor proc;
|
||||
run_klipper(proc, "G90\nG1 X59 Y60 F6000\nM400\nG1 X60 Y60 F9000\nG1 X60 Y100 F9000\n",
|
||||
make_junction_config(gcfKlipper, 50.0, 0.0));
|
||||
REQUIRE_THAT(corner_speed(proc.get_result()), WithinRel(std::sqrt(500.0), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("A corner keeps the square corner velocity when the acceleration changes between its moves",
|
||||
"[GCodeTiming][Klipper]")
|
||||
{
|
||||
GCodeProcessor proc;
|
||||
run_klipper(proc, "G90\nG1 X20 Y60 F6000\nM400\nG1 X60 Y60 F9000\nM204 S4000\nG1 X60 Y100 F9000\n");
|
||||
REQUIRE_THAT(corner_speed(proc.get_result()), WithinRel(5.0, 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("A retraction stops the toolhead and runs at the extruder's own limits", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
// An E acceleration of 0 is unset, and the retraction then uses the retract acceleration.
|
||||
const auto [e_accel, retract_accel] = GENERATE(table<double, double>({{20000.0, 20000.0}, {0.0, 3000.0}}));
|
||||
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
|
||||
config.machine_max_acceleration_e.values = {e_accel, e_accel};
|
||||
config.machine_max_acceleration_retracting.values = {3000.0, 3000.0};
|
||||
GCodeProcessor proc;
|
||||
run_klipper(proc, "G90\nM83\nG1 X60 Y60 F9000\nG1 E-1 F2400\nG1 X100 Y60 F9000\n", config);
|
||||
REQUIRE_THAT(corner_speed(proc.get_result()), WithinAbs(0.0, 1e-6));
|
||||
const double expected = rest_to_rest_time(std::sqrt(2.0) * 60.0, 150.0, klipper_accel, klipper_pseudo_accel) +
|
||||
rest_to_rest_time(1.0, 40.0, retract_accel, std::min(retract_accel, klipper_pseudo_accel)) +
|
||||
rest_to_rest_time(40.0, 150.0, klipper_accel, klipper_pseudo_accel);
|
||||
REQUIRE_THAT(proc.get_time(PrintEstimatedStatistics::ETimeMode::Normal), WithinRel(expected, 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("Extruder limits apply to retractions, not to extrusion along a path", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
|
||||
config.machine_max_speed_e.values = {10.0, 10.0};
|
||||
config.machine_max_acceleration_e.values = {10.0, 10.0};
|
||||
REQUIRE_THAT(klipper_time("M83\nG1 X40 E20 F30000\n", config),
|
||||
WithinRel(rest_to_rest_time(40.0, 500.0, klipper_accel, klipper_pseudo_accel), 1e-6));
|
||||
REQUIRE_THAT(klipper_time("M83\nG1 E-1 F2400\n", config),
|
||||
WithinRel(rest_to_rest_time(1.0, 10.0, 10.0, 10.0), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("A Z move is limited by the Z axis speed and acceleration", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
|
||||
config.machine_max_speed_z.values = {20.0, 20.0};
|
||||
config.machine_max_acceleration_z.values = {100.0, 100.0};
|
||||
REQUIRE_THAT(klipper_time("G1 Z10 F1200\n", config),
|
||||
WithinRel(rest_to_rest_time(10.0, 20.0, 100.0, 100.0), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("M204 sets the acceleration from S, or the smaller of P and T", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
const char* m204 = GENERATE("M204 S500", "M204 P500 T2000", "M204 P2000 T500");
|
||||
REQUIRE_THAT(klipper_time(std::string(m204) + "\nG1 X100 F30000\n"),
|
||||
WithinRel(rest_to_rest_time(100.0, 500.0, 500.0, 500.0 * (1.0 - klipper_cruise_ratio)), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("SET_VELOCITY_LIMIT applies every limit on the line and ignores the comment", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
REQUIRE_THAT(klipper_time("SET_VELOCITY_LIMIT VELOCITY=80 ACCEL=500 MINIMUM_CRUISE_RATIO=0.2 ; ACCEL=9999\n"
|
||||
"G1 X20 F30000\n"),
|
||||
WithinRel(rest_to_rest_time(20.0, 80.0, 500.0, 500.0 * (1.0 - 0.2)), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("ACCEL_TO_DECEL is ignored on a line that also sets the minimum cruise ratio", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
// Klipper 0.13 prefers the cruise ratio and later releases no longer read ACCEL_TO_DECEL at all.
|
||||
// ACCEL_TO_DECEL comes last, so applying the parameters in line order would let it win.
|
||||
REQUIRE_THAT(klipper_time("SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=" + std::to_string(klipper_cruise_ratio) +
|
||||
" ACCEL_TO_DECEL=100\nG1 X40 F30000\n"),
|
||||
WithinRel(rest_to_rest_time(40.0, 500.0, klipper_accel, klipper_pseudo_accel), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("M400 without a delay waits for the queue to drain", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
REQUIRE_THAT(klipper_time("G90\nG1 X40 F30000\nM400\nG1 X80\n"),
|
||||
WithinRel(2.0 * rest_to_rest_time(40.0, 500.0, klipper_accel, klipper_pseudo_accel), 1e-6));
|
||||
}
|
||||
@@ -514,7 +514,7 @@ TEST_CASE("Acceleration and velocity limit commands print their values in genera
|
||||
const Case c = GENERATE(values<Case>({
|
||||
{gcfKlipper, Command::KlipperLimits, 2000000, 25. / 3., false,
|
||||
{"SET_VELOCITY_LIMIT ACCEL=2000000 ", "ACCEL_TO_DECEL=1e+06 ", "SQUARE_CORNER_VELOCITY=8.33333\n"}, {}},
|
||||
{gcfKlipper, Command::KlipperLimits, 12345, 0., false, {"ACCEL=12345 ", "ACCEL_TO_DECEL=6172.5\n"}, {"SQUARE_CORNER_VELOCITY"}},
|
||||
{gcfKlipper, Command::KlipperLimits, 12345, 0., false, {"ACCEL=12345 ", "ACCEL_TO_DECEL=6172.5 ", "MINIMUM_CRUISE_RATIO=0.5\n"}, {"SQUARE_CORNER_VELOCITY"}},
|
||||
{gcfKlipper, Command::KlipperLimits, 0, 0.25, true, {"SQUARE_CORNER_VELOCITY=0.25 ", "; adjust VELOCITY_LIMIT"}, {"ACCEL"}},
|
||||
{gcfKlipper, Command::Print, 3001, 0., true, {"ACCEL=3001 ", "ACCEL_TO_DECEL=1500.5 ", "; adjust ACCEL_TO_DECEL", "; adjust acceleration"}, {}},
|
||||
{gcfMarlinFirmware, Command::Print, 2500, 0., false, {"M204 P2500\n"}, {}},
|
||||
@@ -1272,6 +1272,41 @@ TEST_CASE("Custom G-code motion limits are restored before generated moves", "[G
|
||||
REQUIRE(gcode.find("M205 X8 Y8 ; adjust jerk", custom_gcode_pos) != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("Klipper acceleration commands send accel_to_decel both as ACCEL_TO_DECEL and as the cruise ratio", "[GCodeWriter]")
|
||||
{
|
||||
// Klipper 0.13 defines MINIMUM_CRUISE_RATIO = 1 - ACCEL_TO_DECEL / ACCEL; older releases read only the
|
||||
// first parameter and current ones only the second, so both have to describe the same limit.
|
||||
GCodeWriter writer;
|
||||
writer.config.gcode_flavor.value = gcfKlipper;
|
||||
writer.config.accel_to_decel_enable.value = true;
|
||||
// The writer appends "; adjust ..." comments by default; keep them out so the line ends at the ratio.
|
||||
struct CommentGuard
|
||||
{
|
||||
bool saved = GCodeWriter::full_gcode_comment;
|
||||
~CommentGuard() { GCodeWriter::full_gcode_comment = saved; }
|
||||
} comment_guard;
|
||||
GCodeWriter::full_gcode_comment = false;
|
||||
|
||||
SECTION("a 25% factor") {
|
||||
writer.config.accel_to_decel_factor.value = 25;
|
||||
REQUIRE_THAT(writer.set_print_acceleration(2000),
|
||||
Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=2000 ACCEL_TO_DECEL=500 MINIMUM_CRUISE_RATIO=0.75"));
|
||||
REQUIRE_THAT(writer.set_accel_and_jerk(3000, 0.0),
|
||||
Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=3000 ACCEL_TO_DECEL=750 MINIMUM_CRUISE_RATIO=0.75"));
|
||||
}
|
||||
SECTION("a 100% factor turns the cruise ratio off") {
|
||||
writer.config.accel_to_decel_factor.value = 100;
|
||||
REQUIRE_THAT(writer.set_print_acceleration(2000),
|
||||
Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=2000 ACCEL_TO_DECEL=2000 MINIMUM_CRUISE_RATIO=0\n"));
|
||||
}
|
||||
SECTION("neither is sent while the option is off") {
|
||||
writer.config.accel_to_decel_enable.value = false;
|
||||
const std::string line = writer.set_print_acceleration(2000);
|
||||
REQUIRE_THAT(line, Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=2000"));
|
||||
REQUIRE_THAT(line, !Catch::Matchers::ContainsSubstring("ACCEL_TO_DECEL") && !Catch::Matchers::ContainsSubstring("MINIMUM_CRUISE_RATIO"));
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test for the belt-printer "illegal gantry move at print start" bug.
|
||||
//
|
||||
// On a belt printer the layer-change z-hop is deferred (lazy_lift) and consumed
|
||||
|
||||
@@ -202,6 +202,23 @@ TEST_CASE("The wipe tower's toolchange planner flush follows the gcode flavor",
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("The wipe tower sets Klipper's acceleration and accel_to_decel on one line", "[WipeTower]")
|
||||
{
|
||||
// On separate lines, current Klipper finds nothing it reads on the ACCEL_TO_DECEL one and 0.13
|
||||
// derives the cruise ratio from the acceleration before the change.
|
||||
DynamicPrintConfig config = wipe_tower_toolchange_config("klipper");
|
||||
// Type 2 towers leave the acceleration to the main G-code writer.
|
||||
config.set_deserialize_strict({ { "wipe_tower_type", "type1" },
|
||||
{ "accel_to_decel_enable", "1" }, { "accel_to_decel_factor", "50%" },
|
||||
{ "default_acceleration", "1000" }, { "travel_acceleration", "2000" },
|
||||
{ "machine_max_acceleration_extruding", "5000" } });
|
||||
const std::string tower = wipe_tower_regions(slice_with_prime_tower(config));
|
||||
REQUIRE_THAT(tower, Catch::Matchers::ContainsSubstring("SET_VELOCITY_LIMIT ACCEL="));
|
||||
CHECK_THAT(tower, Catch::Matchers::ContainsSubstring(" MINIMUM_CRUISE_RATIO=0.5\n"));
|
||||
CHECK_THAT(tower, !Catch::Matchers::ContainsSubstring("SET_VELOCITY_LIMIT ACCEL_TO_DECEL") &&
|
||||
!Catch::Matchers::ContainsSubstring("M204"));
|
||||
}
|
||||
|
||||
// What Print feeds the shared estimate. The libslic3r WipeTowerEstimate cases cannot see this:
|
||||
// they call the estimator directly. The estimate counts the filaments the print really uses,
|
||||
// so the two-filament shape gives the outer wall the second one.
|
||||
|
||||
@@ -35,7 +35,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_fill_corner_smoothing.cpp
|
||||
test_filament_mixer.cpp
|
||||
test_fill_plane_path.cpp
|
||||
test_fill_tpms_adaptive.cpp
|
||||
test_adaptive_infill_field.cpp
|
||||
test_geometry.cpp
|
||||
test_imex_arrange.cpp
|
||||
test_imex_helpers.cpp
|
||||
@@ -67,6 +67,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_wipe_path.cpp
|
||||
test_optimizers.cpp
|
||||
test_ordering_strategies.cpp
|
||||
test_shortest_path.cpp
|
||||
# test_png_io.cpp
|
||||
test_indexed_triangle_set.cpp
|
||||
test_connected_bodies.cpp
|
||||
|
||||
@@ -505,6 +505,28 @@ TEST_CASE("A project whose components reference themselves fails to load", "[3mf
|
||||
REQUIRE_FALSE(loaded);
|
||||
}
|
||||
|
||||
TEST_CASE("A project in the 2.4.x layout keeps auto drop off from its build item", "[3mf][Regression]")
|
||||
{
|
||||
// 2.4.x wrote auto_drop on the build item and left it off the object, so the object is re-laid out here.
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
{
|
||||
Model model;
|
||||
ModelObject* object = model.add_object();
|
||||
object->add_volume(make_cube(10., 10., 10.));
|
||||
object->add_instance();
|
||||
object->auto_drop = false;
|
||||
store_project(model, temp.string(), SaveStrategy::Zip64 | SaveStrategy::Silence);
|
||||
}
|
||||
REQUIRE(replace_in_3mf_entry(temp.string(), "model_settings.config", " auto_drop=\"0\"", ""));
|
||||
REQUIRE(replace_in_3mf_entry(temp.string(), "3dmodel.model", "printable=\"1\"/>", "printable=\"1\" auto_drop=\"0\"/>"));
|
||||
|
||||
ScopedTemporaryDir backup_dir("orca_auto_drop_dst");
|
||||
Model model;
|
||||
REQUIRE(load_project(temp.string(), model, backup_dir));
|
||||
REQUIRE(model.objects.size() == 1);
|
||||
CHECK_FALSE(model.objects.front()->auto_drop);
|
||||
}
|
||||
|
||||
TEST_CASE("An object loads up to the component reference budget and fails past it", "[3mf][Regression]")
|
||||
{
|
||||
// The importer queues at most 100000 component references per object. Every reference besides the
|
||||
|
||||
+42
-42
@@ -13,7 +13,7 @@
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/Fill/FillTpmsAdaptive.hpp"
|
||||
#include "libslic3r/Fill/AdaptiveInfillField.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
@@ -30,24 +30,24 @@ ExPolygon rectangle(double x0, double y0, double x1, double y1)
|
||||
}
|
||||
|
||||
// The expolygons stacked in 0.2 mm layers from z = 0 to height.
|
||||
TpmsRadialField radial_field(const ExPolygons &expolygons, double height, TpmsAdaptiveMode mode = TpmsAdaptiveMode::Lobes)
|
||||
AdaptiveInfillField radial_field(const ExPolygons &expolygons, double height, AdaptiveInfillMode mode = AdaptiveInfillMode::Lobes)
|
||||
{
|
||||
std::vector<TpmsRadialField::Slice> slices;
|
||||
std::vector<AdaptiveInfillField::Slice> slices;
|
||||
for (int i = 0; 0.2 * (i + 1) < height + EPSILON; ++i)
|
||||
slices.push_back({0.2 * i, 0.2 * (i + 1), &expolygons});
|
||||
return TpmsRadialField(slices, get_extents(expolygons), mode, [] {});
|
||||
return AdaptiveInfillField(slices, get_extents(expolygons), mode, [] {});
|
||||
}
|
||||
|
||||
double radial(const TpmsRadialField &field, const Vec3d &pt)
|
||||
double radial(const AdaptiveInfillField &field, const Vec3d &pt)
|
||||
{
|
||||
TpmsRadialField::Radials radials;
|
||||
AdaptiveInfillField::Radials radials;
|
||||
field.radial(pt, radials);
|
||||
return radials[0].t;
|
||||
}
|
||||
|
||||
Vec3d center(const TpmsRadialField &field, const Vec3d &pt)
|
||||
Vec3d center(const AdaptiveInfillField &field, const Vec3d &pt)
|
||||
{
|
||||
TpmsRadialField::Radials radials;
|
||||
AdaptiveInfillField::Radials radials;
|
||||
field.radial(pt, radials);
|
||||
return radials[0].center;
|
||||
}
|
||||
@@ -57,11 +57,11 @@ constexpr double Tolerance = 0.075;
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("TPMS radial field is zero at the center of a cube and one at its faces", "[FillTpmsAdaptive]")
|
||||
TEST_CASE("Adaptive infill field is zero at the center of a cube and one at its faces", "[AdaptiveInfillField]")
|
||||
{
|
||||
// A 20 mm cube, 10 mm from its center to every face.
|
||||
const ExPolygons square{rectangle(0., 0., 20., 20.)};
|
||||
const TpmsRadialField field = radial_field(square, 20.);
|
||||
const AdaptiveInfillField field = radial_field(square, 20.);
|
||||
|
||||
const Vec3d c = center(field, {10., 10., 10.});
|
||||
CHECK_THAT(c.x(), WithinAbs(10., 0.5));
|
||||
@@ -74,10 +74,10 @@ TEST_CASE("TPMS radial field is zero at the center of a cube and one at its face
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("TPMS radial field grows linearly from the center of a cube to its faces", "[FillTpmsAdaptive]")
|
||||
TEST_CASE("Adaptive infill field grows linearly from the center of a cube to its faces", "[AdaptiveInfillField]")
|
||||
{
|
||||
const ExPolygons square{rectangle(0., 0., 20., 20.)};
|
||||
const TpmsRadialField field = radial_field(square, 20.);
|
||||
const AdaptiveInfillField field = radial_field(square, 20.);
|
||||
for (double d = 1.; d < 10.; d += 1.) {
|
||||
CAPTURE(d);
|
||||
CHECK_THAT(radial(field, {10. - d, 10., 10.}), WithinAbs(d / 10., Tolerance));
|
||||
@@ -85,11 +85,11 @@ TEST_CASE("TPMS radial field grows linearly from the center of a cube to its fac
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("TPMS radial field of a tall box is centered at its middle height", "[FillTpmsAdaptive]")
|
||||
TEST_CASE("Adaptive infill field of a tall box is centered at its middle height", "[AdaptiveInfillField]")
|
||||
{
|
||||
// 20 x 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom.
|
||||
const ExPolygons square{rectangle(0., 0., 20., 20.)};
|
||||
const TpmsRadialField field = radial_field(square, 60.);
|
||||
const AdaptiveInfillField field = radial_field(square, 60.);
|
||||
|
||||
CHECK_THAT(center(field, {10., 10., 45.}).z(), WithinAbs(30., 0.5));
|
||||
CHECK_THAT(radial(field, {10., 10., 15.}), WithinAbs(0.5, Tolerance));
|
||||
@@ -97,10 +97,10 @@ TEST_CASE("TPMS radial field of a tall box is centered at its middle height", "[
|
||||
CHECK_THAT(radial(field, {15., 10., 30.}), WithinAbs(0.5, Tolerance));
|
||||
}
|
||||
|
||||
TEST_CASE("TPMS radial field grades every body towards its own center", "[FillTpmsAdaptive]")
|
||||
TEST_CASE("Adaptive infill field grades every body towards its own center", "[AdaptiveInfillField]")
|
||||
{
|
||||
const ExPolygons squares{rectangle(0., 0., 20., 20.), rectangle(30., 0., 50., 20.)};
|
||||
const TpmsRadialField field = radial_field(squares, 20.);
|
||||
const AdaptiveInfillField field = radial_field(squares, 20.);
|
||||
|
||||
CHECK_THAT(center(field, {5., 10., 10.}).x(), WithinAbs(10., 0.5));
|
||||
CHECK_THAT(center(field, {45., 10., 10.}).x(), WithinAbs(40., 0.5));
|
||||
@@ -108,20 +108,20 @@ TEST_CASE("TPMS radial field grades every body towards its own center", "[FillTp
|
||||
CHECK_THAT(radial(field, {30., 10., 10.}), WithinAbs(1., Tolerance));
|
||||
}
|
||||
|
||||
TEST_CASE("TPMS radial field is beyond one outside of the object", "[FillTpmsAdaptive]")
|
||||
TEST_CASE("Adaptive infill field is beyond one outside of the object", "[AdaptiveInfillField]")
|
||||
{
|
||||
const ExPolygons square{rectangle(0., 0., 20., 20.)};
|
||||
const TpmsRadialField field = radial_field(square, 20.);
|
||||
const AdaptiveInfillField field = radial_field(square, 20.);
|
||||
CHECK(radial(field, {-5., 10., 10.}) > 1.);
|
||||
CHECK(radial(field, {10., 10., 30.}) > 1.);
|
||||
}
|
||||
|
||||
TEST_CASE("TPMS radial field grades every lobe of a body towards its own center", "[FillTpmsAdaptive]")
|
||||
TEST_CASE("Adaptive infill field grades every lobe of a body towards its own center", "[AdaptiveInfillField]")
|
||||
{
|
||||
// Two spheres of 10 mm united, their centers 16 mm apart: the neck between them is 6 mm deep.
|
||||
const Vec3d c1(10., 10., 10.), c2(26., 10., 10.);
|
||||
std::vector<ExPolygons> layers;
|
||||
std::vector<TpmsRadialField::Slice> slices;
|
||||
std::vector<AdaptiveInfillField::Slice> slices;
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.)));
|
||||
Polygons circles;
|
||||
@@ -134,7 +134,7 @@ TEST_CASE("TPMS radial field grades every lobe of a body towards its own center"
|
||||
}
|
||||
for (int i = 0; i < 100; ++i)
|
||||
slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]});
|
||||
const TpmsRadialField field(slices, get_extents(layers[50]), TpmsAdaptiveMode::Lobes, [] {});
|
||||
const AdaptiveInfillField field(slices, get_extents(layers[50]), AdaptiveInfillMode::Lobes, [] {});
|
||||
|
||||
for (const Vec3d &c : {c1, c2}) {
|
||||
CAPTURE(c.x());
|
||||
@@ -143,7 +143,7 @@ TEST_CASE("TPMS radial field grades every lobe of a body towards its own center"
|
||||
CHECK_THAT(radial(field, c + Vec3d(0., 0., 9.5)), WithinAbs(1., 2. * Tolerance));
|
||||
}
|
||||
// The side between the lobes is half way to the surface, where both patterns morph into each other.
|
||||
TpmsRadialField::Radials radials;
|
||||
AdaptiveInfillField::Radials radials;
|
||||
REQUIRE(field.radial(0.5 * (c1 + c2), radials) == 2);
|
||||
for (size_t i = 0; i < 2; ++i) {
|
||||
CHECK_THAT(radials[i].t, WithinAbs(0.5, 2. * Tolerance));
|
||||
@@ -151,12 +151,12 @@ TEST_CASE("TPMS radial field grades every lobe of a body towards its own center"
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously", "[FillTpmsAdaptive]")
|
||||
TEST_CASE("Adaptive infill field blends the lobes meeting at a junction continuously", "[AdaptiveInfillField]")
|
||||
{
|
||||
// Three spheres of 10 mm united, their centers on a triangle of 16 mm sides: the necks meet at its middle.
|
||||
const std::array<Vec3d, 3> centers{Vec3d(10., 10., 10.), Vec3d(26., 10., 10.), Vec3d(18., 10. + 8. * std::sqrt(3.), 10.)};
|
||||
std::vector<ExPolygons> layers;
|
||||
std::vector<TpmsRadialField::Slice> slices;
|
||||
std::vector<AdaptiveInfillField::Slice> slices;
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.)));
|
||||
Polygons circles;
|
||||
@@ -169,7 +169,7 @@ TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously
|
||||
}
|
||||
for (int i = 0; i < 100; ++i)
|
||||
slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]});
|
||||
const TpmsRadialField field(slices, get_extents(layers[50]), TpmsAdaptiveMode::Lobes, [] {});
|
||||
const AdaptiveInfillField field(slices, get_extents(layers[50]), AdaptiveInfillMode::Lobes, [] {});
|
||||
|
||||
// Around the junction the nearest lobes swap, but the weight of every lobe changes smoothly.
|
||||
const Vec3d junction = (centers[0] + centers[1] + centers[2]) / 3.;
|
||||
@@ -179,7 +179,7 @@ TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously
|
||||
for (int row = 0; row <= 100; ++row) {
|
||||
std::array<float, 3> previous{};
|
||||
for (int step = 0; step <= 200; ++step) {
|
||||
TpmsRadialField::Radials radials;
|
||||
AdaptiveInfillField::Radials radials;
|
||||
const size_t count = field.radial(junction + Vec3d(0.01 * step - 1., 0.02 * row - 1., 0.), radials);
|
||||
max_count = std::max(max_count, count);
|
||||
std::array<float, 3> weights{};
|
||||
@@ -201,23 +201,23 @@ TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously
|
||||
CHECK(max_jump < 0.05);
|
||||
}
|
||||
|
||||
TEST_CASE("TPMS radial field is empty when the object is thinner than the grid cells", "[FillTpmsAdaptive]")
|
||||
TEST_CASE("Adaptive infill field is empty when the object is thinner than the grid cells", "[AdaptiveInfillField]")
|
||||
{
|
||||
// A 0.3 mm square bar between the nodes of a grid sized by a 200 mm bounding box, with cells of 0.5 mm or more.
|
||||
const ExPolygons bar{rectangle(0.1, 0.1, 0.4, 0.4)};
|
||||
std::vector<TpmsRadialField::Slice> slices;
|
||||
std::vector<AdaptiveInfillField::Slice> slices;
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
slices.push_back({0.2 * i, 0.2 * (i + 1), &bar});
|
||||
const TpmsRadialField field(slices, BoundingBox(Point::new_scale(0., 0.), Point::new_scale(200., 200.)),
|
||||
TpmsAdaptiveMode::Lobes, [] {});
|
||||
const AdaptiveInfillField field(slices, BoundingBox(Point::new_scale(0., 0.), Point::new_scale(200., 200.)),
|
||||
AdaptiveInfillMode::Lobes, [] {});
|
||||
CHECK(field.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("TPMS depth follows the distance to the surface relative to the deepest point", "[FillTpmsAdaptive]")
|
||||
TEST_CASE("Adaptive infill depth follows the distance to the surface relative to the deepest point", "[AdaptiveInfillField]")
|
||||
{
|
||||
// 20 x 20 x 60 mm: from 10 to 50 mm high the axis is 10 mm deep, as deep as the center.
|
||||
const ExPolygons square{rectangle(0., 0., 20., 20.)};
|
||||
const TpmsRadialField field = radial_field(square, 60., TpmsAdaptiveMode::SmoothBlend);
|
||||
const AdaptiveInfillField field = radial_field(square, 60., AdaptiveInfillMode::SmoothBlend);
|
||||
for (double z : {15., 30., 45.}) {
|
||||
CAPTURE(z);
|
||||
CHECK_THAT(field.depth({10., 10., z}), WithinAbs(1., Tolerance));
|
||||
@@ -227,18 +227,18 @@ TEST_CASE("TPMS depth follows the distance to the surface relative to the deepes
|
||||
CHECK_THAT(field.depth({0., 10., 30.}), WithinAbs(0., Tolerance));
|
||||
}
|
||||
|
||||
TEST_CASE("TPMS radial field in Distance warp mode follows the distance to the surface", "[FillTpmsAdaptive]")
|
||||
TEST_CASE("Adaptive infill field in Distance warp mode follows the distance to the surface", "[AdaptiveInfillField]")
|
||||
{
|
||||
// In a cube the depth falls linearly along every ray from the center, so Distance warp matches Lobes.
|
||||
const ExPolygons square{rectangle(0., 0., 20., 20.)};
|
||||
const TpmsRadialField cube = radial_field(square, 20., TpmsAdaptiveMode::DistanceWarp);
|
||||
const AdaptiveInfillField cube = radial_field(square, 20., AdaptiveInfillMode::DistanceWarp);
|
||||
CHECK_THAT(radial(cube, {10., 10., 10.}), WithinAbs(0., Tolerance));
|
||||
CHECK_THAT(radial(cube, {15., 10., 10.}), WithinAbs(0.5, Tolerance));
|
||||
CHECK_THAT(radial(cube, {10., 10., 20.}), WithinAbs(1., Tolerance));
|
||||
|
||||
// 20 x 20 x 60 mm: Lobes grades the axis towards the top and the bottom, Distance warp keeps it deep.
|
||||
const TpmsRadialField lobes = radial_field(square, 60.);
|
||||
const TpmsRadialField warp = radial_field(square, 60., TpmsAdaptiveMode::DistanceWarp);
|
||||
const AdaptiveInfillField lobes = radial_field(square, 60.);
|
||||
const AdaptiveInfillField warp = radial_field(square, 60., AdaptiveInfillMode::DistanceWarp);
|
||||
for (double z : {15., 45.}) {
|
||||
CAPTURE(z);
|
||||
CHECK_THAT(radial(lobes, {10., 10., z}), WithinAbs(0.5, Tolerance));
|
||||
@@ -247,12 +247,12 @@ TEST_CASE("TPMS radial field in Distance warp mode follows the distance to the s
|
||||
CHECK_THAT(radial(warp, {0., 10., 30.}), WithinAbs(1., Tolerance));
|
||||
}
|
||||
|
||||
TEST_CASE("TPMS stepped shells split a layer by depth from the surface inwards", "[FillTpmsAdaptive]")
|
||||
TEST_CASE("Adaptive infill stepped shells split a layer by depth from the surface inwards", "[AdaptiveInfillField]")
|
||||
{
|
||||
// The middle layer of a 40 mm cube, 20 mm from its center to every face, 20% at the surface to 5% inside.
|
||||
const ExPolygons square{rectangle(0., 0., 40., 40.)};
|
||||
const TpmsRadialField field = radial_field(square, 40., TpmsAdaptiveMode::SteppedShells);
|
||||
const std::vector<TpmsShell> shells = make_tpms_shells(field, square.front(), 20., 0.2f, 0.05f, TpmsAdaptiveGradient::Linear);
|
||||
const AdaptiveInfillField field = radial_field(square, 40., AdaptiveInfillMode::SteppedShells);
|
||||
const std::vector<AdaptiveInfillShell> shells = make_adaptive_infill_shells(field, square.front(), 20., 0.2f, 0.05f, AdaptiveInfillGradient::Linear);
|
||||
REQUIRE(shells.size() == 5);
|
||||
CHECK_THAT(shells.front().density, WithinAbs(0.2, 1e-6));
|
||||
CHECK_THAT(shells.back().density, WithinAbs(0.05, 1e-6));
|
||||
@@ -270,11 +270,11 @@ TEST_CASE("TPMS stepped shells split a layer by depth from the surface inwards",
|
||||
[¢er](const ExPolygon &expolygon) { return expolygon.contains(center); }));
|
||||
}
|
||||
|
||||
TEST_CASE("TPMS radial field in 2D grades every section normal to the axis on its own", "[FillTpmsAdaptive]")
|
||||
TEST_CASE("Adaptive infill field in 2D grades every section normal to the axis on its own", "[AdaptiveInfillField]")
|
||||
{
|
||||
// 20 x 20 x 60 mm: every section normal to Z is 10 mm from its center to the sides, whatever its height.
|
||||
const ExPolygons square{rectangle(0., 0., 20., 20.)};
|
||||
const TpmsRadialField normal_z = radial_field(square, 60., TpmsAdaptiveMode::NormalZ);
|
||||
const AdaptiveInfillField normal_z = radial_field(square, 60., AdaptiveInfillMode::NormalZ);
|
||||
for (double z : {5., 30., 55.}) {
|
||||
CAPTURE(z);
|
||||
CHECK_THAT(radial(normal_z, {10., 10., z}), WithinAbs(0., Tolerance));
|
||||
@@ -283,7 +283,7 @@ TEST_CASE("TPMS radial field in 2D grades every section normal to the axis on it
|
||||
}
|
||||
|
||||
// Normal to X, the sections are 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom.
|
||||
const TpmsRadialField normal_x = radial_field(square, 60., TpmsAdaptiveMode::NormalX);
|
||||
const AdaptiveInfillField normal_x = radial_field(square, 60., AdaptiveInfillMode::NormalX);
|
||||
for (double x : {3., 10., 17.}) {
|
||||
CAPTURE(x);
|
||||
CHECK_THAT(radial(normal_x, {x, 15., 30.}), WithinAbs(0.5, Tolerance));
|
||||
@@ -1457,6 +1457,19 @@ TEST_CASE("min_object_distance yields no floor when an FFF config lacks the opti
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("handle_legacy migrates the adaptive TPMS keys", "[Config]") {
|
||||
ConfigSubstitutionContext ctxt(ForwardCompatibilitySubstitutionRule::Enable);
|
||||
DynamicPrintConfig config;
|
||||
config.set_deserialize("tpms_adaptive", "stepped_shells", ctxt);
|
||||
config.set_deserialize("tpms_interior_density", "12%", ctxt);
|
||||
config.set_deserialize("tpms_adaptive_gradient", "quadratic", ctxt);
|
||||
|
||||
CHECK(config.opt_enum<AdaptiveInfillMode>("adaptive_infill_mode") == AdaptiveInfillMode::SteppedShells);
|
||||
CHECK(config.option<ConfigOptionPercent>("adaptive_infill_interior_density")->value == Catch::Approx(12.));
|
||||
CHECK(config.opt_enum<AdaptiveInfillGradient>("adaptive_infill_gradient") == AdaptiveInfillGradient::Quadratic);
|
||||
CHECK(ctxt.substitutions.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("handle_legacy migrates every IDEX/IQEX key testers already have", "[Config]") {
|
||||
// Three eras of saved profiles: the feature shipped as iXex (is_ixex + ixex_*), the clearance
|
||||
// pair was renamed to say what it measures, then the whole prefix became imex_. Driven from the
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
#include <catch2/catch_all.hpp>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
@@ -12,10 +13,13 @@
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/GCode/OrderingStrategies.hpp"
|
||||
#include "libslic3r/ShortestPath.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <unordered_set>
|
||||
#include <numeric>
|
||||
#include <random>
|
||||
#include <catch2/catch_approx.hpp>
|
||||
|
||||
using namespace Slic3r;
|
||||
@@ -304,3 +308,60 @@ TEST_CASE("snake_core handles three points", "[Snake]") {
|
||||
|
||||
REQUIRE(is_permutation(p2, 3));
|
||||
}
|
||||
|
||||
// The shortest cycle through the points, trying every order.
|
||||
static double shortest_cycle_length(const Points& centers)
|
||||
{
|
||||
std::vector<size_t> path(centers.size());
|
||||
std::iota(path.begin(), path.end(), 0);
|
||||
double best = std::numeric_limits<double>::max();
|
||||
do {
|
||||
best = std::min(best, tsp_cycle_path_length(path, centers));
|
||||
} while (std::next_permutation(path.begin() + 1, path.end()));
|
||||
return best;
|
||||
}
|
||||
|
||||
static Points random_centers(unsigned seed, size_t count)
|
||||
{
|
||||
std::mt19937 rng(seed);
|
||||
std::uniform_real_distribution<> coordinate(0., 200.);
|
||||
Points centers;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
// One draw per statement, as the evaluation order of function arguments is unspecified.
|
||||
const double x = coordinate(rng);
|
||||
const double y = coordinate(rng);
|
||||
centers.emplace_back(Point::new_scale(x, y));
|
||||
}
|
||||
return centers;
|
||||
}
|
||||
|
||||
TEST_CASE("tsp_exact_cycle finds the shortest cycle", "[TSPPostProcessing]") {
|
||||
const size_t count = GENERATE(4, 5, 6, 8);
|
||||
for (unsigned seed = 1; seed <= 10; ++seed) {
|
||||
const Points centers = random_centers(seed, count);
|
||||
const std::vector<size_t> path = tsp_exact_cycle(centers);
|
||||
REQUIRE(is_permutation(path, count));
|
||||
CHECK(euclidean_path_length(path, centers) <= shortest_cycle_length(centers) + SCALED_EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Default ordering tours up to ten points along the shortest cycle", "[TSPPostProcessing]") {
|
||||
const bool from_start = GENERATE(false, true);
|
||||
const Point start = Point::new_scale(-20., -20.);
|
||||
for (unsigned seed = 1; seed <= 5; ++seed) {
|
||||
const Points centers = random_centers(seed, 9);
|
||||
const std::vector<size_t> path = chain_points_with_postprocessing(centers, from_start ? &start : nullptr);
|
||||
REQUIRE(is_permutation(path, centers.size()));
|
||||
CHECK(euclidean_path_length(path, centers) <= shortest_cycle_length(centers) + SCALED_EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Default ordering of many points is never longer than the greedy cycle", "[TSPPostProcessing]") {
|
||||
for (unsigned seed = 1; seed <= 10; ++seed) {
|
||||
const Points centers = random_centers(seed, 150);
|
||||
const std::vector<size_t> path = chain_points_with_postprocessing(centers, nullptr);
|
||||
REQUIRE(is_permutation(path, centers.size()));
|
||||
CHECK(euclidean_path_length(path, centers) <= euclidean_path_length(chain_points(centers, nullptr), centers) + SCALED_EPSILON);
|
||||
CHECK_FALSE(has_crossings(path, centers));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
#include "libslic3r/ExtrusionEntity.hpp"
|
||||
#include "libslic3r/ExtrusionEntityCollection.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Polyline.hpp"
|
||||
#include "libslic3r/ShortestPath.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
#include <math.h>
|
||||
#include <random>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
// Infill-like paths: lines of 1 to 30 mm scattered over a 100 mm square. The collection owns them, and entities hands
|
||||
// them out as the raw pointers chain_and_reorder_extrusion_entities() reorders.
|
||||
struct Paths
|
||||
{
|
||||
ExtrusionEntityCollection collection;
|
||||
std::vector<ExtrusionEntity*> entities;
|
||||
|
||||
Paths(unsigned seed, size_t count, bool can_reverse)
|
||||
{
|
||||
std::mt19937 rng(seed);
|
||||
std::uniform_real_distribution<> coordinate(0., 100.), length(1., 30.), angle(0., 2. * M_PI);
|
||||
Polylines lines;
|
||||
for (size_t i = 0; i < count; ++ i) {
|
||||
// One draw per statement, as the evaluation order of function arguments is unspecified.
|
||||
const double x = coordinate(rng);
|
||||
const double y = coordinate(rng);
|
||||
const double l = length(rng);
|
||||
const double t = angle(rng);
|
||||
const Vec2d a(x, y);
|
||||
const Vec2d b = a + l * Vec2d(std::cos(t), std::sin(t));
|
||||
lines.emplace_back(Point::new_scale(a), Point::new_scale(b));
|
||||
}
|
||||
extrusion_entities_append_paths(collection.entities, std::move(lines), erInternalInfill, 0.1, 0.45f, 0.2f, can_reverse);
|
||||
entities = collection.entities;
|
||||
}
|
||||
};
|
||||
|
||||
double travel(const std::vector<ExtrusionEntity*> &entities, const Point &start)
|
||||
{
|
||||
double length = 0.;
|
||||
Point last = start;
|
||||
for (const ExtrusionEntity *ee : entities) {
|
||||
length += (ee->first_point() - last).cast<double>().norm();
|
||||
last = ee->last_point();
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
// Travel of the chain that always goes on to the nearest free path end.
|
||||
double nearest_next_travel(const std::vector<ExtrusionEntity*> &entities, bool can_reverse, const Point &from)
|
||||
{
|
||||
std::vector<char> taken(entities.size(), 0);
|
||||
double length = 0.;
|
||||
Point last = from;
|
||||
for (size_t step = 0; step < entities.size(); ++ step) {
|
||||
double best = std::numeric_limits<double>::max();
|
||||
size_t best_idx = 0;
|
||||
bool best_reversed = false;
|
||||
for (size_t i = 0; i < entities.size(); ++ i)
|
||||
for (const bool reversed : { false, true })
|
||||
if (! taken[i] && (can_reverse || ! reversed)) {
|
||||
const double d = ((reversed ? entities[i]->last_point() : entities[i]->first_point()) - last).cast<double>().norm();
|
||||
if (d < best) {
|
||||
best = d;
|
||||
best_idx = i;
|
||||
best_reversed = reversed;
|
||||
}
|
||||
}
|
||||
taken[best_idx] = 1;
|
||||
length += best;
|
||||
last = best_reversed ? entities[best_idx]->first_point() : entities[best_idx]->last_point();
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
// The shortest travel over every order and allowed reversal of the paths, from start.
|
||||
double shortest_travel(const std::vector<ExtrusionEntity*> &entities, bool can_reverse, const Point &from)
|
||||
{
|
||||
std::vector<size_t> order(entities.size());
|
||||
std::iota(order.begin(), order.end(), 0);
|
||||
double best = std::numeric_limits<double>::max();
|
||||
do {
|
||||
for (size_t mask = 0; mask < (size_t(1) << order.size()) && (can_reverse || mask == 0); ++ mask) {
|
||||
double length = 0.;
|
||||
Point last = from;
|
||||
for (size_t k = 0; k < order.size(); ++ k) {
|
||||
const ExtrusionEntity *ee = entities[order[k]];
|
||||
const bool reversed = (mask >> k & 1) != 0;
|
||||
length += ((reversed ? ee->last_point() : ee->first_point()) - last).cast<double>().norm();
|
||||
last = reversed ? ee->first_point() : ee->last_point();
|
||||
}
|
||||
best = std::min(best, length);
|
||||
}
|
||||
} while (std::next_permutation(order.begin(), order.end()));
|
||||
return best;
|
||||
}
|
||||
|
||||
const Point start = Point::new_scale(-10., -10.);
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("An improved infill chain is never longer than the greedy or nearest-next one and keeps every path", "[ShortestPath]")
|
||||
{
|
||||
const size_t count = GENERATE(2, 3, 10, 60, 300);
|
||||
const bool can_reverse = GENERATE(true, false);
|
||||
double greedy_total = 0., improved_total = 0.;
|
||||
for (unsigned seed = 1; seed <= 20; ++ seed) {
|
||||
Paths greedy(seed, count, can_reverse), improved(seed, count, can_reverse);
|
||||
const double nearest_travel = nearest_next_travel(improved.entities, can_reverse, start);
|
||||
chain_and_reorder_extrusion_entities(greedy.entities, &start);
|
||||
chain_and_reorder_extrusion_entities(improved.entities, start);
|
||||
|
||||
std::vector<ExtrusionEntity*> sorted = improved.entities;
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
REQUIRE(std::unique(sorted.begin(), sorted.end()) == sorted.end());
|
||||
REQUIRE(sorted.size() == count);
|
||||
|
||||
const double greedy_travel = travel(greedy.entities, start), improved_travel = travel(improved.entities, start);
|
||||
CHECK(improved_travel <= greedy_travel + SCALED_EPSILON);
|
||||
CHECK(improved_travel <= nearest_travel + SCALED_EPSILON);
|
||||
greedy_total += greedy_travel;
|
||||
improved_total += improved_travel;
|
||||
}
|
||||
// Twenty random sets of ten or more paths always leave the greedy chain something to shorten.
|
||||
if (count >= 10)
|
||||
CHECK(improved_total < 0.95 * greedy_total);
|
||||
}
|
||||
|
||||
TEST_CASE("Improving an infill chain keeps the direction of paths that cannot reverse", "[ShortestPath]")
|
||||
{
|
||||
Paths paths(7, 100, false);
|
||||
std::map<const ExtrusionEntity*, Point> first_points;
|
||||
for (const ExtrusionEntity *ee : paths.entities)
|
||||
first_points.emplace(ee, ee->first_point());
|
||||
|
||||
chain_and_reorder_extrusion_entities(paths.entities, start);
|
||||
|
||||
for (const ExtrusionEntity *ee : paths.entities)
|
||||
CHECK(ee->first_point() == first_points.at(ee));
|
||||
}
|
||||
|
||||
TEST_CASE("Chains of a few paths are the shortest possible", "[ShortestPath]")
|
||||
{
|
||||
const size_t count = GENERATE(2, 3, 4, 5, 6);
|
||||
const bool can_reverse = GENERATE(true, false);
|
||||
for (unsigned seed = 1; seed <= 10; ++ seed) {
|
||||
Paths paths(seed, count, can_reverse);
|
||||
const double shortest = shortest_travel(paths.entities, can_reverse, start);
|
||||
chain_and_reorder_extrusion_entities(paths.entities, start);
|
||||
CHECK(travel(paths.entities, start) <= shortest + SCALED_EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("The greedy chain of two to four paths is the shortest possible", "[ShortestPath]")
|
||||
{
|
||||
const size_t count = GENERATE(2, 3, 4);
|
||||
const bool can_reverse = GENERATE(true, false);
|
||||
for (unsigned seed = 1; seed <= 10; ++ seed) {
|
||||
Paths paths(seed, count, can_reverse);
|
||||
const double shortest = shortest_travel(paths.entities, can_reverse, start);
|
||||
chain_and_reorder_extrusion_entities(paths.entities, &start);
|
||||
CHECK(travel(paths.entities, start) <= shortest + SCALED_EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("A short chain need not start with the path nearest to its start", "[ShortestPath]")
|
||||
{
|
||||
// The nearest end leads 10 mm away from the two other paths, so the shortest chain leaves that path for last.
|
||||
const Point from = Point::new_scale(0., 0.);
|
||||
for (const bool greedy : { false, true }) {
|
||||
ExtrusionEntityCollection collection;
|
||||
extrusion_entities_append_paths(collection.entities,
|
||||
Polylines{ Polyline(Point::new_scale(-0.9, 0.), Point::new_scale(-10., 0.)), Polyline(Point::new_scale(1., 0.), Point::new_scale(2., 0.)),
|
||||
Polyline(Point::new_scale(3., 1.), Point::new_scale(4., 1.)) },
|
||||
erInternalInfill, 0.1, 0.45f, 0.2f);
|
||||
std::vector<ExtrusionEntity*> entities = collection.entities;
|
||||
const double shortest = shortest_travel(entities, true, from);
|
||||
if (greedy)
|
||||
chain_and_reorder_extrusion_entities(entities, &from);
|
||||
else
|
||||
chain_and_reorder_extrusion_entities(entities, from);
|
||||
CHECK(entities.back() == collection.entities.front());
|
||||
CHECK_THAT(travel(entities, from), Catch::Matchers::WithinAbs(shortest, SCALED_EPSILON));
|
||||
}
|
||||
}
|
||||
@@ -420,12 +420,13 @@ TEST_CASE("Moonraker discovery waits for a complete device identity", "[Moonrake
|
||||
agent.device_info.dev_id = "127.0.0.1:7125";
|
||||
agent.device_info.dev_ip = agent.device_info.dev_id;
|
||||
agent.device_info.base_url = "http://127.0.0.1:7125";
|
||||
agent.device_info.model_name = "Test model";
|
||||
agent.start_discovery(true, false);
|
||||
REQUIRE(announcements == 1);
|
||||
REQUIRE(agent.fetch_count == 1);
|
||||
REQUIRE(agent.fetch_count == 0);
|
||||
REQUIRE(announcement.at("dev_id") == agent.device_info.dev_id);
|
||||
REQUIRE(announcement.at("dev_ip") == agent.device_info.dev_ip);
|
||||
REQUIRE(announcement.at("dev_name") == "Test printer");
|
||||
REQUIRE(announcement.at("dev_name") == "Test model");
|
||||
}
|
||||
|
||||
TEST_CASE("Stopping the status stream gives a silent printer no more than its timeouts", "[MoonrakerPrinterAgent][StatusStream]")
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/read_until.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/streambuf.hpp>
|
||||
#include <boost/asio/write.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/fstream.hpp>
|
||||
#include <boost/nowide/cstdlib.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <atomic>
|
||||
@@ -23,7 +23,6 @@
|
||||
#include <istream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
@@ -93,7 +92,9 @@ using boost::asio::ip::tcp;
|
||||
class LoopbackServer
|
||||
{
|
||||
public:
|
||||
enum class Mode { Ok, Unavailable, Silent };
|
||||
// Session answers every request with the body given to set_session_body(), as the token endpoint
|
||||
// does, after 200 ms so the client is still waiting when the test makes its next call.
|
||||
enum class Mode { Ok, Unavailable, Silent, Session };
|
||||
|
||||
struct Request
|
||||
{
|
||||
@@ -115,6 +116,11 @@ public:
|
||||
|
||||
std::string url() const { return "http://127.0.0.1:" + std::to_string(m_acceptor.local_endpoint().port()); }
|
||||
void set_mode(Mode mode) { m_mode = mode; }
|
||||
void set_session_body(std::string body)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_session_body = std::move(body);
|
||||
}
|
||||
|
||||
bool wait_for_accepts(size_t count, std::chrono::milliseconds timeout)
|
||||
{
|
||||
@@ -174,13 +180,31 @@ private:
|
||||
const Mode mode = m_mode;
|
||||
if (mode == Mode::Silent)
|
||||
return;
|
||||
const std::string_view response = mode == Mode::Ok ?
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 11\r\nConnection: close\r\n\r\n{\"ok\":true}" :
|
||||
"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
|
||||
boost::asio::async_write(connection->socket, boost::asio::buffer(response),
|
||||
[connection](boost::system::error_code, size_t) {
|
||||
boost::system::error_code ignored;
|
||||
connection->socket.shutdown(tcp::socket::shutdown_both, ignored);
|
||||
auto response = std::make_shared<std::string>();
|
||||
if (mode == Mode::Session) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
*response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: " +
|
||||
std::to_string(m_session_body.size()) + "\r\nConnection: close\r\n\r\n" + m_session_body;
|
||||
} else {
|
||||
*response = mode == Mode::Ok ?
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 11\r\nConnection: close\r\n\r\n{\"ok\":true}" :
|
||||
"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
|
||||
}
|
||||
auto write = [connection, response] {
|
||||
boost::asio::async_write(connection->socket, boost::asio::buffer(*response),
|
||||
[connection, response](boost::system::error_code, size_t) {
|
||||
boost::system::error_code ignored;
|
||||
connection->socket.shutdown(tcp::socket::shutdown_both, ignored);
|
||||
});
|
||||
};
|
||||
if (mode != Mode::Session) {
|
||||
write();
|
||||
return;
|
||||
}
|
||||
auto timer = std::make_shared<boost::asio::steady_timer>(m_io, 200ms);
|
||||
timer->async_wait([timer, write](boost::system::error_code ec) {
|
||||
if (!ec)
|
||||
write();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -194,30 +218,11 @@ private:
|
||||
std::condition_variable m_cv;
|
||||
std::vector<Request> m_requests;
|
||||
std::vector<std::shared_ptr<Connection>> m_connections;
|
||||
std::string m_session_body;
|
||||
};
|
||||
|
||||
// Sets an environment variable for the lifetime of the guard and restores the previous value.
|
||||
class ScopedEnvVar
|
||||
{
|
||||
public:
|
||||
ScopedEnvVar(std::string name, const std::string& value) : m_name(std::move(name))
|
||||
{
|
||||
if (const char* old = boost::nowide::getenv(m_name.c_str()))
|
||||
m_previous = old;
|
||||
boost::nowide::setenv(m_name.c_str(), value.c_str(), 1);
|
||||
}
|
||||
~ScopedEnvVar()
|
||||
{
|
||||
if (m_previous)
|
||||
boost::nowide::setenv(m_name.c_str(), m_previous->c_str(), 1);
|
||||
else
|
||||
boost::nowide::unsetenv(m_name.c_str());
|
||||
}
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
std::optional<std::string> m_previous;
|
||||
};
|
||||
// A JWT whose only claim is {"exp":4102444800} (2100-01-01); the agent reads nothing else from it.
|
||||
constexpr const char* FAR_FUTURE_JWT = "eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.sig";
|
||||
|
||||
// What the agent reported through its callbacks, recorded on whichever thread delivers it so the
|
||||
// assertions can run on the test thread.
|
||||
@@ -540,3 +545,24 @@ TEST_CASE_METHOD(HealthCheckFixture, "The health check does no token work", "[Or
|
||||
CHECK(agent->is_user_login());
|
||||
CHECK(agent->get_access_token() == "test-token");
|
||||
}
|
||||
|
||||
TEST_CASE_METHOD(HealthCheckFixture, "A token request made while start() refreshes waits for it instead of refreshing again", "[OrcaCloudServiceAgent]")
|
||||
{
|
||||
// A session saved by a previous run: start() refreshes it on its own thread.
|
||||
agent->persist_user_secret(nlohmann::json{{"refresh_token", "stored-refresh-token"}, {"user_id", "test-user-id"}}.dump());
|
||||
const std::string access_token = FAR_FUTURE_JWT;
|
||||
server.set_session_body(nlohmann::json{{"access_token", access_token},
|
||||
{"refresh_token", "rotated-refresh-token"},
|
||||
{"user_id", "test-user-id"}}.dump());
|
||||
server.set_mode(LoopbackServer::Mode::Session);
|
||||
|
||||
REQUIRE(agent->start() == BAMBU_NETWORK_SUCCESS);
|
||||
CHECK(agent->ensure_token_fresh("test"));
|
||||
CHECK(agent->get_access_token() == access_token);
|
||||
|
||||
size_t token_posts = 0;
|
||||
for (const auto& request : server.requests())
|
||||
if (boost::starts_with(request.request_line, "POST /auth/v1/token"))
|
||||
++token_posts;
|
||||
CHECK(token_posts == 1);
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <ios>
|
||||
#include <libslic3r/LifecycleEvents.hpp>
|
||||
#include <memory>
|
||||
#include <slic3r/Utils/Http.hpp>
|
||||
#include <slic3r/Utils/NetworkAgentFactory.hpp>
|
||||
#include <slic3r/plugin/PluginDescriptor.hpp>
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
@@ -14,8 +15,13 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/catch_message.hpp>
|
||||
#include "plugin_test_utils.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <pybind11/cast.h>
|
||||
#include <pybind11/gil.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/pytypes.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
@@ -1068,3 +1074,43 @@ TEST_CASE("Unloading a plugin that is not loaded is a no-op", "[PluginLifecycle]
|
||||
CHECK(manager.unload_plugin("No_Such_Plugin"));
|
||||
CHECK_FALSE(manager.is_plugin_loaded("No_Such_Plugin"));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// CA certificates in the default client context, as a plugin's urllib or http.client request would
|
||||
// verify against.
|
||||
int default_context_ca_count()
|
||||
{
|
||||
pybind11::gil_scoped_acquire gil;
|
||||
const pybind11::dict stats = pybind11::module_::import("ssl").attr("create_default_context")().attr("cert_store_stats")();
|
||||
return stats["x509_ca"].cast<int>();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("The bundled Python trusts the system CA certificates", "[PluginLifecycle][Python]")
|
||||
{
|
||||
// The app runs this before plugins load; on Linux it is what points OpenSSL at the distro bundle.
|
||||
Http::tls_global_init();
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
CHECK(default_context_ca_count() > 0);
|
||||
}
|
||||
|
||||
#ifndef _WIN32 // Windows adds its system store whatever SSL_CERT_FILE names.
|
||||
TEST_CASE("SSL_CERT_FILE still overrides the bundled Python's default CA certificates", "[PluginLifecycle][Python]")
|
||||
{
|
||||
// Existing but empty: Linux's CurlGlobalInit replaces an SSL_CERT_FILE that names a missing file.
|
||||
ScopedTemporaryFile empty_bundle(".pem");
|
||||
std::ofstream(empty_bundle.string()).close();
|
||||
ScopedEnvVar ssl_cert_file{"SSL_CERT_FILE", empty_bundle.string()};
|
||||
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
CHECK(default_context_ca_count() == 0);
|
||||
}
|
||||
#endif
|
||||
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user