Compare commits

..
Author SHA1 Message Date
Kris Austin f64beb4379 Fix random crash while slicing with the Russian UI (daily tips) (#16664)
Fix crash when a daily tip has no body text

has_cjk() looped while i < text.size() - 2. For a string shorter than
two bytes the size_t subtraction wraps, so on an empty string the loop
read past the buffer and up the stack until it hit an unmapped page.

The Russian translation of the "Reverse on even" tip had lost the \n
after its title, so render_text() passed an empty body to has_cjk()
every time that tip was shown during slicing. Restore the line break
and bound the loop so a translation like this can't crash the app.
2026-10-19 16:25:07 -05:00
Kris Austin a2b1a2f3e7 Keep an offline Moonraker printer from freezing startup (#16668)
Moonraker discovery asked the printer for its name before announcing
it, on the GUI thread, with a 5 s connect timeout. When the selected
printer was off, the window ignored input for 5 s after every launch
and the Home page stayed white.

The request never produced a name. It read machine_name or hostname
from /server/info, and Moonraker sends neither there. Discovery now
announces the preset model name without asking the printer.
2026-10-19 16:21:59 -05:00
yw4z 0585836550 Fetch cloud plugin metadata faster (#16645)
When logged in to Orca Cloud, the plugin fetch at startup no longer refreshes the token a second time while start() is already refreshing it. It now waits for that refresh and checks the token again. The subscribed and owned plugin requests also run in parallel, so the fetch takes about half as long.
2026-10-19 15:56:52 -05:00
5a36120c58 fix(gcode): use Klipper motion planning for time estimates (#15545)
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
Co-authored-by: raistlin7447 <kris.austin@gmail.com>
2026-10-19 16:15:59 -03:00
Ian BassiandTobias Gloth 69193d5681 Optimize travel: exact/refined path chaining, object tours, and joined infill lines (#16492)
* Shorten the travel between extrusion paths

chain_and_reorder_extrusion_entities() builds the print order of the infill,
ironing and support paths of a region from the nozzle position. It took the
greedy chain, which leaves its longest joins for last; it now shortens it:

- Up to eight paths, chain_segments_exact() finds the shortest chain over
  every order and allowed reversal, by dynamic programming over the subsets
  of paths, with a fixed start and a free end.
- Longer chains are refined by local search: 2-opt, or-opt of up to three
  paths, and moving a run of any length next to the nozzle to the front, each
  tried only toward the 32 end points nearest a link, which come from a
  uniform grid. A move is applied only if it shortens the chain, so the result
  is never longer than the greedy one.
- A nearest-next chain is built too, and refined and used instead when it is
  already shorter than the refined greedy chain, which happens when the greedy
  chain sweeps away from the nozzle and has to come back with a long jump.

The shared greedy chaining also chains two to four segments exactly, which
covers the islands of a layer and the loops of a perimeter collection. Paths
that cannot reverse, such as Grid's, are only reordered, and collections that
keep their order are left alone. The overload without a start point keeps the
greedy chain.

Co-authored-by: Tobias Gloth <tobias.gloth@3dsystems.com>

* Order objects and islands along shorter tours

Objects under 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. Up to ten points, tsp_exact_cycle() finds the
shortest tour by dynamic programming over subsets, starting at the point
nearest the start point when one is given. Larger sets add or-opt to the
existing 2-opt and crossing removal: runs of up to three points move next to
one of the eight points nearest an end of the run, reversed or not, and
crossing removal runs last so no crossing is left. Snake ordering is
unchanged; Best of strategies gets the shorter Default tour as a candidate.

* Join infill lines into fewer paths

Every separate infill path costs a travel to it, so the fill generators now
leave fewer of them:

- connect_infill() pairs the line ends along each wall in the order that
  joins the most lines: besides shortest arc first, it replays arcs of about
  the same length taken along each contour, forwards and backwards, including
  the last pass that joins neighboring open ends, and keeps the order that
  joins the most. Regular patterns no longer leave loose ends where the
  pairs along a wall fall out of step, and multiline copies print out and back.
- Ends whose anchors would meet are joined along the wall instead, up to two
  anchors and a line width apart.
- An unpaired end anchors toward the nearest open end of another path, where
  the next path usually starts, instead of the side with more free wall. With
  multiline infill, an anchor toward a copy of the same line stops short of
  the copy instead of printing over it.
- Last, sparse infill lines whose ends are at most two line spacings apart are
  joined with a straight piece, shortest first, without closing a loop or
  crossing a line; with anchors off this is the only joining. Copies of
  neighboring multiline groups are joined at the same side, and lines that
  cannot reverse only join end to start.

* Describe the travel optimization design

How the extrusion chains, the object and island tours and the infill line joining shorten the travel, and why each part is built the way it is.

---------

Co-authored-by: Tobias Gloth <tobias.gloth@3dsystems.com>
2026-10-19 15:53:32 -03:00
Kris Austin cdb9bb57ae fix: Mesh Boolean crash on Windows builds made with clang-cl (#16651)
Build without precompiled headers when the compiler is clang-cl, in both the slicer and the wxWidgets dependency, and add a CI check that fails if broken exception throw info comes back.

With a precompiled header, clang-cl can leave the copy constructor out of the throw info it emits for an exception type (llvm/llvm-project#53486, open since 2022 and still present in clang 22.1.3). The linker keeps one throw info per type for the whole binary. When it keeps one from a precompiled header file, a copy of that exception is bitwise, and the copy and the original free the same data.

Mesh Boolean hits this on every operation. mcut destroys a std::promise that was never set, which stores a broken_promise future_error in an exception_ptr. Freeing it corrupts the heap, and Windows ends the process with 0xC0000374. The 2.5.0-alpha release has broken throw info for 64 exception classes, including std::runtime_error, std::bad_alloc and boost exceptions.

scripts/check_exception_throw_info.ps1 runs after the Windows clang build and its uploads, before any deploy. It fails when an exception class has throw info both with and without a copy constructor, when OrcaSlicer.pdb is missing, or when it finds no exception classes at all. scripts/test_check_exception_throw_info.ps1 tests it on canned dumps.

build_win.bat now treats -l as --no-pch. The Windows ccache key drops the cl.exe version, which only mattered for precompiled headers.

The bug came in with #15428, which moved Windows CI to clang-cl. 2.4.2 was built with MSVC.

Fixes #16581
2026-10-19 12:13:28 -05:00
Sergei f0c59208e7 Apply post-processing scripts to G-code packed into .gcode.3mf (non-BBL) (#16464)
Post-processing scripts now run on a copy of each sliced plate's G-code before it is packed into a .gcode.3mf. This covers Export plate sliced file, sending a .3mf to a print host, and the print dialog for non-BBL printers. BBL printers are skipped, because their G-code is post-processed in place after slicing. Backups are skipped too.

Export plate sliced file no longer reports success when export_3mf() fails.
2026-10-19 10:47:03 -05:00
Ian Bassi 9f6a344162 Make the adaptive infill settings pattern-neutral (#16654) 2026-10-19 12:05:55 -03:00
Ian Bassi c5cfe40a1f Update translations: fill empty entries in all languages and fix color picker strings (#16652) 2026-10-19 10:19:06 -03:00
SoftFever e5b6cbf091 Let plugins make HTTPS requests on macOS (#16638)
* Let the bundled Python verify HTTPS certificates on macOS

* Restore SSL_CERT_FILE after the bundled Python override test
2026-10-19 20:06:14 +08:00
Alexandre Folle de Menezes 3b9b871ba6 Standardize the spelling of "G-code", "SVG", "GitHub" and "IP" (#16618) 2026-10-19 09:01:39 -03:00
Surfoo cb2260f057 i18n(fr): update translations (#16591) 2026-10-19 09:00:32 -03:00
Alexandre Folle de Menezes 3fcbb2adf9 Translate color names to pt-BR (#16585) 2026-10-19 08:59:35 -03:00
Sergei 252bbbf9f5 Make the exclude-object outline tolerance a user setting (#16503)
* Make the exclude-object outline tolerance configurable

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>

* fix(config): move exclude_object_tolerance to developer mode

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>

* fix(config): move exclude_object_tolerance to expert mode

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>

* feat(ui): hide exclude_object_tolerance for non-Klipper firmwares

mend

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>

* Test that a larger exclude object tolerance writes fewer outline points

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>

---------

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>
2026-10-19 08:17:20 -03:00
Lam Wei Lun 4c2ae6d202 Keep Auto Drop off for projects saved by 2.4.x (#16633)
2.4.0 to 2.4.2 wrote auto_drop on each build item, and 2.4.x objects
carry no auto_drop attribute in model_settings.config. The importer only
read the object attribute and treated a missing one as on, so floating
objects were dropped to the bed on load and the flag was rewritten as on
on the next save.

Record object ids with an item auto_drop="0" and use that as the fallback
when the object has no attribute. The attribute's presence is now tracked
so the fallback applies only when it is absent. Any item set to off turns
auto drop off for the object, since the flag is shared by all instances.
2026-10-19 16:08:59 +08:00
HanifKoh ac022b56ad Wait for the Close Frame in the Moonraker Status Stream Test (#16635)
The fake printer counts the agent's close frame in its read handler, but
Beast answers the close before that read completes, so stop() can return
and the test can read the counter before it is incremented. On a loaded
runner the check saw 0.

Wait for the counter like the other fake-printer counters do, then keep
the exact-count check.
2026-10-19 16:06:41 +08:00
HanifKoh d3699dd9f6 Keep Python Bytecode out of the Bundled Python Home (#16550)
* Keep Python Bytecode out of the Bundled Python Home

The bundled stdlib ships .pyc files that are stale against their sources:
they are compiled when the Python deps are installed, and the copy into the
app does not keep the sources' mtimes. CPython then recompiles every module
it imports and writes the result next to the source. A drag-installed macOS
app rewrites files inside its own bundle on first launch, so its signature
no longer verifies, and read-only installs (AppImage, Flatpak, Program Files)
recompile the same modules on every launch.

Point pycache_prefix at <data_dir>/python/pycache so the interpreter reads
and writes bytecode there. The bundle is never written, the cache survives
between launches, and the data directory is already a plugin read/write root.

* Drop Cached Bytecode Through the Interpreter in the Module Release Test
2026-10-19 13:48:53 +08:00
101 changed files with 22520 additions and 10379 deletions

No files matched your search

+4 -4
View File
@@ -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():
+8
View File
@@ -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
+7 -9
View File
@@ -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
+1 -1
View File
@@ -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.
+8
View File
@@ -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
View File
@@ -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"
)
+17 -7
View File
@@ -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
+5 -4
View File
@@ -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.
+9 -1
View File
@@ -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.
+198
View File
@@ -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.
+55 -21
View File
@@ -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
+58 -21
View File
@@ -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
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
+110 -47
View File
@@ -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 " 不在合理范围内"
+111 -48
View File
@@ -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"},
+126
View File
@@ -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
+4
View File
@@ -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)
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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());
}
+3 -8
View File
@@ -377,8 +377,7 @@ public:
virtual void set(const ConfigOption* rhs, size_t start, size_t len) = 0;
virtual void set_with_restore(const ConfigOptionVectorBase* rhs, std::vector<int>& restore_index, int stride) = 0;
virtual void set_with_restore_2(const ConfigOptionVectorBase* rhs, std::vector<int>& restore_index, int start, int len, bool skip_error = false) = 0;
// keep_nil: copy a nil column from rhs as well, for options whose nil means "disabled" rather than "inherit".
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride, bool keep_nil = false) = 0;
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride) = 0;
virtual void set_to_index(const ConfigOptionVectorBase* rhs, std::vector<int>& dest_index, int stride) = 0;
virtual void set_with_nil(const ConfigOptionVectorBase* rhs, const ConfigOptionVectorBase* inherits, int stride) = 0;
// Resize the vector of values, copy the newly added values from opt_default if provided.
@@ -581,7 +580,7 @@ public:
//set a item related with extruder variants when loading user config, only set the different value of some extruder
//rhs: item from user config
//diff_index: which index in this vector need to be set
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride, bool keep_nil) override
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride) override
{
if (rhs->type() == this->type()) {
// Assign the first value of the rhs vector.
@@ -593,7 +592,7 @@ public:
for (size_t i = 0; i < diff_index.size(); i++) {
if (diff_index[i] != -1) {
for (size_t j = 0; j < stride; j++) {
if (keep_nil || !other->is_nil(diff_index[i] * stride))
if (!other->is_nil(diff_index[i] * stride))
this->values[i * stride + j] = other->values[diff_index[i] * stride + j];
}
}
@@ -2312,10 +2311,6 @@ public:
ConfigOptionType type = coNone;
// If a type is nullable, then it accepts a "nil" value (scalar) or "nil" values (vector).
bool nullable = false;
// A nullable option whose "nil" means "switched off" (a filament override), not "no value here,
// inherit the parent's". Child presets store and load such a nil verbatim instead of folding it
// into the parent's value.
bool nil_means_disabled = false;
// Default value of this option. The default value object is owned by ConfigDef, it is released in its destructor.
Slic3r::clonable_ptr<const ConfigOption> default_value;
void set_default_value(const ConfigOption* ptr) { this->default_value = Slic3r::clonable_ptr<const ConfigOption>(ptr); }
@@ -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 &center) const
Vec3d AdaptiveInfillField::offset(const Vec3d &pt, const Vec3d &center) const
{
Vec3d d = pt - center;
if (m_axis >= 0)
@@ -516,7 +516,7 @@ Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d &center) 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 &params, coordf_t spacing,
void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, 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);
@@ -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 &params, coordf_t spacing,
void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell);
} // namespace Slic3r
+23 -23
View File
@@ -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
View File
@@ -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
+9 -6
View File
@@ -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_
+6 -6
View File
@@ -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 = [&params, 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.
+6 -6
View File
@@ -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 = [&params, 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.
+7 -7
View File
@@ -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).
+15 -5
View File
@@ -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 << "\" ";
-8
View File
@@ -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 {
+293 -100
View File
@@ -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;
+16 -3
View File
@@ -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 = [&centers](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);
+5 -2
View File
@@ -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
+1 -1
View File
@@ -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
+11 -10
View File
@@ -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;
};
+13 -8
View File
@@ -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) {
+4
View File
@@ -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;
+2 -2
View File
@@ -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;
+9 -11
View File
@@ -797,7 +797,7 @@ bool Preset::save(DynamicPrintConfig* parent_config)
{
ConfigOption *opt_src = config.option(option);
ConfigOption *opt_dst = temp_config.option(option, true);
if (opt_dst->is_scalar() || !(opt_dst->nullable()) || is_filament_extruder_override_key(option))
if (opt_dst->is_scalar() || !(opt_dst->nullable()))
opt_dst->set(opt_src);
else {
ConfigOptionVectorBase* opt_vec_src = static_cast<ConfigOptionVectorBase*>(opt_src);
@@ -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",
@@ -2131,7 +2132,7 @@ Preset* PresetCollection::get_preset_differed_for_save(Preset& preset)
{
ConfigOption *opt_src = preset.config.option(option);
ConfigOption *opt_dst = temp_config.option(option, true);
if (opt_dst->is_scalar() || !(opt_dst->nullable()) || is_filament_extruder_override_key(option))
if (opt_dst->is_scalar() || !(opt_dst->nullable()))
opt_dst->set(opt_src);
else {
ConfigOptionVectorBase* opt_vec_src = static_cast<ConfigOptionVectorBase*>(opt_src);
@@ -2198,12 +2199,9 @@ int PresetCollection::get_differed_values_to_update(Preset& preset, std::map<std
// option's non-nil default (e.g. filament_retract_before_wipe -> 100%), silently
// resurrecting an override the user turned off. See GitHub issue on Retract Before Wipe.
if (m_type == Preset::TYPE_FILAMENT) {
for (const std::string& opt_key : preset.config.keys()) {
if (!is_filament_extruder_override_key(opt_key) || key_values.count(opt_key))
continue; // not an override, or already carried by the diff
const ConfigOptionDef *def = print_config_def.get(opt_key);
if (def == nullptr || !def->default_value || def->default_value->is_nil())
continue; // the option's default is nil too, so an absent key cannot come back as a value
for (const std::string& opt_key : filament_extruder_override_keys) {
if (key_values.count(opt_key))
continue; // already carried by the diff
const auto* opt_vec = dynamic_cast<const ConfigOptionVectorBase*>(preset.config.option(opt_key));
if (opt_vec == nullptr)
continue;
+2 -1
View File
@@ -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;
}
+4 -4
View File
@@ -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;
+45 -36
View File
@@ -118,14 +118,13 @@ const std::vector<std::string> filament_extruder_override_keys = {
"filament_retraction_distances_when_cut"
};
// A filament override is any option whose definition says its nil means "disabled" rather
// than "inherit": the keys generated from filament_extruder_override_keys, plus the ones
// defined separately (filament_retract_length_nc, the ironing overrides). Child presets
// store and load such a nil verbatim instead of folding it into the parent's value.
// Some filament override parameters are generated from filament_extruder_override_keys,
// while filament_retract_length_nc is defined separately. Keep the generator list
// unchanged and use this helper for behavior checks that need the full override set.
bool is_filament_extruder_override_key(const std::string &opt_key)
{
const ConfigOptionDef *def = print_config_def.get(opt_key);
return def != nullptr && def->nil_means_disabled;
return std::find(filament_extruder_override_keys.begin(), filament_extruder_override_keys.end(), opt_key) != filament_extruder_override_keys.end() ||
opt_key == "filament_retract_length_nc";
}
size_t get_extruder_index(const GCodeConfig& config, unsigned int filament_id)
@@ -367,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 {
@@ -3661,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 "
@@ -3679,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");
@@ -3697,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.");
@@ -3709,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"
@@ -3718,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");
@@ -3726,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
@@ -4134,7 +4133,6 @@ void PrintConfigDef::init_fff_params()
def->max = 100;
def->mode = comAdvanced;
def->nullable = true;
def->nil_means_disabled = true;
def->set_default_value(new ConfigOptionPercentsNullable{ ConfigOptionPercentsNullable::nil_value() });
def = this->add("filament_ironing_spacing", coFloats);
@@ -4147,7 +4145,6 @@ void PrintConfigDef::init_fff_params()
def->max = 1;
def->mode = comAdvanced;
def->nullable = true;
def->nil_means_disabled = true;
def->set_default_value(new ConfigOptionFloatsNullable{ ConfigOptionFloatsNullable::nil_value() });
def = this->add("filament_ironing_inset", coFloats);
@@ -4160,7 +4157,6 @@ void PrintConfigDef::init_fff_params()
def->max = 100;
def->mode = comAdvanced;
def->nullable = true;
def->nil_means_disabled = true;
def->set_default_value(new ConfigOptionFloatsNullable{ ConfigOptionFloatsNullable::nil_value() });
def = this->add("filament_ironing_speed", coFloats);
@@ -4172,7 +4168,6 @@ void PrintConfigDef::init_fff_params()
def->min = 1;
def->mode = comAdvanced;
def->nullable = true;
def->nil_means_disabled = true;
def->set_default_value(new ConfigOptionFloatsNullable{ ConfigOptionFloatsNullable::nil_value() });
def = this->add("fuzzy_skin", coEnum);
@@ -4634,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. "
@@ -8683,7 +8689,6 @@ void PrintConfigDef::init_fff_params()
auto it_opt = options.find(extruder_raw_key);
assert(it_opt != options.end());
def = this->add_nullable(opt_key, it_opt->second.type);
def->nil_means_disabled = true;
def->label = it_opt->second.label;
def->full_label = it_opt->second.full_label;
def->tooltip = it_opt->second.tooltip;
@@ -8833,7 +8838,6 @@ void PrintConfigDef::init_fff_params()
def->sidetext = L("mm");
def->mode = comDevelop;
def->nullable = true;
def->nil_means_disabled = true;
def->min = 0;
def->max = 18;
def->set_default_value(new ConfigOptionFloatsNullable { 10. });
@@ -9661,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
@@ -12262,8 +12272,7 @@ void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig&
opt_src->set(opt_target);
}
else
// An override's nil means "disabled" and is the child's value; every other nil column inherits.
opt_vec_src->set_only_diff(opt_vec_dest, variant_index, stride, is_filament_extruder_override_key(opt));
opt_vec_src->set_only_diff(opt_vec_dest, variant_index, stride);
}
}
}
+12 -11
View File
@@ -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))
+13 -13
View File
@@ -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
View File
@@ -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);
+4 -1
View File
@@ -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);
+1 -1
View File
@@ -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
+10 -8
View File
@@ -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")},
+10 -8
View File
@@ -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);
+1 -1
View File
@@ -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];
+3 -3
View File
@@ -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},
+3 -3
View File
@@ -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;
}
+1 -1
View File
@@ -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) {
+51 -1
View File
@@ -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();
+1 -1
View File
@@ -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_();
+5 -4
View File
@@ -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");
}
}
+2 -10
View File
@@ -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;
+14 -5
View File
@@ -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;
}
+10 -3
View File
@@ -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.";
+13
View File
@@ -584,6 +584,19 @@ bool PythonInterpreter::initialize()
}
BOOST_LOG_TRIVIAL(debug) << "Program name set successfully";
// Keep bytecode out of the Python home. The shipped .pyc files are stale against their
// sources, so a writable install would rewrite them inside the app (breaking a signed
// macOS bundle's seal) and a read-only one would recompile them on every launch.
const std::string pycache_prefix = data_dir() + "/python/pycache";
#ifdef _WIN32
status = PyConfig_SetString(&config, &config.pycache_prefix, boost::nowide::widen(pycache_prefix).c_str());
#else
status = PyConfig_SetBytesString(&config, &config.pycache_prefix, pycache_prefix.c_str());
#endif
if (PyStatus_Exception(status))
BOOST_LOG_TRIVIAL(warning) << "Failed to set the Python bytecode cache to " << pycache_prefix << ": "
<< (status.err_msg ? status.err_msg : "unknown error");
// Use pybind11's scoped_interpreter which properly initializes
// pybind11 internals for multi-threaded use — raw Py_InitializeFromConfig
// does not set up thread state tracking that pybind11 requires.
+98 -34
View File
@@ -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));
}
}
+38
View File
@@ -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]")
+200
View File
@@ -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));
}
+36 -1
View File
@@ -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
+17
View File
@@ -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.
+2 -1
View File
@@ -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
+22
View File
@@ -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
@@ -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",
[&center](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));
+13 -65
View File
@@ -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
@@ -1665,68 +1678,3 @@ TEST_CASE("Default options of enum lists get their definition's keys map", "[Con
}
CHECK(enum_lists > 0);
}
SCENARIO("update_diff_values_to_child_config keeps a child's disabled filament override", "[Config][Variant]")
{
std::set<std::string> no_keys;
GIVEN("A filament parent that overrides retract before wipe and ironing flow") {
Slic3r::DynamicPrintConfig parent;
parent.set_deserialize_strict("filament_retract_before_wipe", "100%");
parent.set_deserialize_strict("filament_ironing_flow", "15%");
WHEN("the child disables both overrides") {
Slic3r::DynamicPrintConfig child;
child.set_deserialize_strict("filament_retract_before_wipe", "nil");
child.set_deserialize_strict("filament_ironing_flow", "nil");
parent.update_diff_values_to_child_config(child, "", "filament_extruder_variant",
Slic3r::filament_options_with_variant, no_keys);
THEN("both overrides stay disabled") {
CHECK(parent.opt_serialize("filament_retract_before_wipe") == "nil");
CHECK(parent.opt_serialize("filament_ironing_flow") == "nil");
}
}
}
GIVEN("A two-variant filament parent") {
Slic3r::DynamicPrintConfig parent;
parent.set_deserialize_strict("filament_extruder_variant", "Direct Drive Standard;Direct Drive High Flow");
parent.set_deserialize_strict("filament_retract_before_wipe", "100%,100%");
parent.set_deserialize_strict("filament_flow_ratio", "1,1");
WHEN("the child disables the override on one variant and leaves a plain option nil on it") {
Slic3r::DynamicPrintConfig child;
child.set_deserialize_strict("filament_extruder_variant", "Direct Drive Standard;Direct Drive High Flow");
child.set_deserialize_strict("filament_retract_before_wipe", "nil,100%");
child.set_deserialize_strict("filament_flow_ratio", "nil,1.1");
parent.update_diff_values_to_child_config(child, "", "filament_extruder_variant",
Slic3r::filament_options_with_variant, no_keys);
THEN("the override column is disabled while the plain option's nil column inherits") {
CHECK(parent.opt_serialize("filament_retract_before_wipe") == "nil,100%");
CHECK(parent.opt_serialize("filament_flow_ratio") == "1,1.1");
}
}
WHEN("the child lists the same variants in the other order") {
Slic3r::DynamicPrintConfig child;
child.set_deserialize_strict("filament_extruder_variant", "Direct Drive High Flow;Direct Drive Standard");
child.set_deserialize_strict("filament_retract_before_wipe", "70%,nil");
child.set_deserialize_strict("filament_flow_ratio", "1.1,nil");
parent.update_diff_values_to_child_config(child, "", "filament_extruder_variant",
Slic3r::filament_options_with_variant, no_keys);
THEN("columns are matched by variant name, the disabled one included") {
CHECK(parent.opt_serialize("filament_retract_before_wipe") == "nil,70%");
CHECK(parent.opt_serialize("filament_flow_ratio") == "1,1.1");
}
}
}
}
TEST_CASE("Every filament override is the only kind of option whose nil means disabled", "[Config]")
{
// Listed by hand on purpose: the helper answers from the flag, so it cannot be the oracle here.
std::set<std::string> overrides(Slic3r::filament_extruder_override_keys.begin(), Slic3r::filament_extruder_override_keys.end());
overrides.insert({ "filament_retract_length_nc", "filament_ironing_flow", "filament_ironing_spacing", "filament_ironing_inset", "filament_ironing_speed" });
for (const auto &[key, def] : Slic3r::print_config_def.options) {
INFO(key);
CHECK(def.nil_means_disabled == (overrides.count(key) == 1));
CHECK(Slic3r::is_filament_extruder_override_key(key) == def.nil_means_disabled);
if (def.nil_means_disabled)
CHECK(def.nullable);
}
}
@@ -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));
}
}
@@ -8417,58 +8417,3 @@ TEST_CASE("An empty choice list in a G-code's settings keeps the value it had be
CHECK(config.opt_serialize("nozzle_volume_type") == "High Flow");
CHECK(config.option<ConfigOptionStrings>("filament_multi_colour")->values.empty());
}
TEST_CASE("A disabled filament override is saved and reloaded column by column", "[Preset][Bundle]")
{
ScopedPresetDataDir data_dir;
PresetBundle bundle;
Preset &parent = add_inmemory_preset(bundle.filaments, "Parent PLA @System");
parent.config.option<ConfigOptionStrings>("filament_extruder_variant", true)->values = { "Direct Drive Standard", "Direct Drive High Flow" };
parent.config.option<ConfigOptionPercentsNullable>("filament_retract_before_wipe", true)->values = { 100., 100. };
parent.config.option<ConfigOptionFloatsNullable>("filament_flow_ratio", true)->values = { 1., 1. };
// The child turns the override off on the first variant and changes a plain option on the second.
const fs::path file = data_dir.tmp.path() / PRESET_FILAMENT_NAME / "Child PLA.json";
Preset child(Preset::TYPE_FILAMENT, "Child PLA");
child.file = file.string();
child.version = *Semver::parse("1.0.0");
child.config = parent.config;
child.config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = parent.name;
child.config.option<ConfigOptionPercentsNullable>("filament_retract_before_wipe")->set_at_to_nil(0);
child.config.option<ConfigOptionFloatsNullable>("filament_flow_ratio")->values = { 1., 1.1 };
REQUIRE(child.save(&parent.config));
// On disk the override keeps every column, so its nil can only mean "off"; a plain
// option still stores nil for the columns that match the parent.
DynamicPrintConfig saved;
std::map<std::string, std::string> key_values;
std::string reason;
saved.load_from_json(file.string(), ForwardCompatibilitySubstitutionRule::EnableSilent, key_values, reason);
REQUIRE(reason.empty());
CHECK(saved.opt_serialize("filament_retract_before_wipe") == "nil,100%");
CHECK(saved.opt_serialize("filament_flow_ratio") == "nil,1.1");
PresetsConfigSubstitutions substitutions;
bundle.filaments.load_presets(data_dir.tmp.path().string(), PRESET_FILAMENT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::EnableSilent);
const Preset *loaded = bundle.filaments.find_preset("Child PLA");
REQUIRE(loaded != nullptr);
CHECK(loaded->config.opt_serialize("filament_retract_before_wipe") == "nil,100%");
CHECK(loaded->config.opt_serialize("filament_flow_ratio") == "1,1.1");
}
TEST_CASE("The cloud diff force-emits a disabled override only where the cloud would fill a value in", "[Preset][Bundle]")
{
PresetBundle bundle;
add_inmemory_preset(bundle.filaments, "Parent PLA @System");
Preset &child = add_inmemory_preset(bundle.filaments, "Child PLA", "Parent PLA @System");
// Every override is nil in both parent and child, so none is in the plain diff.
std::map<std::string, std::string> key_values;
REQUIRE(bundle.filaments.get_differed_values_to_update(child, key_values) >= 0);
// Retract Before Wipe defaults to 100%: an absent key would come back as 100%, so nil is sent.
CHECK(key_values.count("filament_retract_before_wipe") == 1);
CHECK(key_values["filament_retract_before_wipe"] == "nil");
// An ironing override defaults to nil: an absent key stays nil, so it is not sent.
CHECK(key_values.count("filament_ironing_flow") == 0);
}
+202
View File
@@ -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));
}
}
@@ -150,6 +150,13 @@ public:
return m_close_frames;
}
// The printer answers a close frame before its own read completes, so the agent's stop can return first.
bool wait_for_close_frames(int count)
{
std::unique_lock<std::mutex> lock(m_mutex);
return m_changed.wait_for(lock, 10s, [&] { return m_close_frames >= count; });
}
// Hangs up every connection that is waiting.
void release()
{
@@ -277,6 +284,7 @@ private:
if (ec == websocket::error::closed) {
std::lock_guard<std::mutex> lock(m_mutex);
++m_close_frames;
m_changed.notify_all();
}
if (ec)
reading = false;
@@ -412,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]")
@@ -449,6 +458,7 @@ TEST_CASE("Stopping the status stream sends the printer a close frame", "[Moonra
REQUIRE(f.printer.wait_for_ready_connections(1));
CHECK(f.stops_within(3s));
CHECK(f.printer.wait_for_close_frames(1));
CHECK(f.printer.close_frames() == 1);
}
+58 -32
View File
@@ -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);
}
+62 -2
View File
@@ -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>
@@ -615,8 +621,22 @@ TEST_CASE("Python module release removes package submodules and owned sys.path",
// Ensure the next import executes the new submodule rather than reusing a stale package child.
write_package("'new'");
boost::system::error_code ec;
fs::remove_all(package_root / "__pycache__", ec);
{
// The rewrite can keep the old mtime and size, so drop the bytecode wherever the
// interpreter caches it.
PythonGILState gil;
REQUIRE(gil);
PyObjectPtr util(PyImport_ImportModule("importlib.util"));
REQUIRE(util);
for (const char* file : {"__init__.py", "sub.py"}) {
PyObjectPtr source(PyUnicode_DecodeFSDefault((package_root / file).string().c_str()));
REQUIRE(source);
PyObjectPtr cached(PyObject_CallMethod(util.get(), "cache_from_source", "O", source.get()));
REQUIRE(cached);
boost::system::error_code ec;
fs::remove(fs::path(PyUnicode_AsUTF8(cached.get())), ec);
}
}
paths.clear();
modules.clear();
@@ -1054,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