mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-20 14:08:20 +00:00
Compare commits
78
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fd5a3c445 | ||
|
|
069bd402d9 | ||
|
|
6626377abc | ||
|
|
74d4392dae | ||
|
|
0d5b3a79d7 | ||
|
|
c9d897b4d0 | ||
|
|
6bff3c3cd6 | ||
|
|
6e1d15f8a2 | ||
|
|
4f63cd4051 | ||
|
|
c7489d431d | ||
|
|
506dd497db | ||
|
|
56da2f43b0 | ||
|
|
00838d59c1 | ||
|
|
8f8bfd6a28 | ||
|
|
90a5a0b964 | ||
|
|
f64beb4379 | ||
|
|
a2b1a2f3e7 | ||
|
|
0585836550 | ||
|
|
5a36120c58 | ||
|
|
69193d5681 | ||
|
|
cdb9bb57ae | ||
|
|
f0c59208e7 | ||
|
|
9f6a344162 | ||
|
|
c5cfe40a1f | ||
|
|
e5b6cbf091 | ||
|
|
3b9b871ba6 | ||
|
|
cb2260f057 | ||
|
|
3fcbb2adf9 | ||
|
|
252bbbf9f5 | ||
|
|
4c2ae6d202 | ||
|
|
ac022b56ad | ||
|
|
d3699dd9f6 | ||
|
|
a315a96c5c | ||
|
|
24cf1c6bcc | ||
|
|
d16ca97380 | ||
|
|
da4d766d46 | ||
|
|
06665d0558 | ||
|
|
0f59cd5d05 | ||
|
|
c1fb801ff3 | ||
|
|
1ee0b730a1 | ||
|
|
72ba7a2b35 | ||
|
|
8160f64cae | ||
|
|
fd28f6c189 | ||
|
|
9b2fcaa53a | ||
|
|
0acf1e6680 | ||
|
|
c6804fbb68 | ||
|
|
6d40ec93f6 | ||
|
|
82aee1abdd | ||
|
|
cefdd9b609 | ||
|
|
caade80624 | ||
|
|
cbd8ce98d5 | ||
|
|
99714afc4c | ||
|
|
3d07663ab2 | ||
|
|
29b6bbb8c9 | ||
|
|
8b6c074c69 | ||
|
|
109d09e0b8 | ||
|
|
b00237807d | ||
|
|
4149401c53 | ||
|
|
f6cf18fb96 | ||
|
|
4ee1b9c6e9 | ||
|
|
bc8d0a7858 | ||
|
|
e0651ec31d | ||
|
|
0caefcf4aa | ||
|
|
86905a8a20 | ||
|
|
d4fabbf7c8 | ||
|
|
51efb24752 | ||
|
|
cf9ec18df5 | ||
|
|
99de46b9d0 | ||
|
|
258f927a0f | ||
|
|
b8d40ed61f | ||
|
|
54e6df0235 | ||
|
|
79ed246d26 | ||
|
|
92b5301bf6 | ||
|
|
20c157b0f4 | ||
|
|
4cf16febf6 | ||
|
|
70e52a105f | ||
|
|
b525cb79e1 | ||
|
|
86ab598899 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: orca-translations
|
||||
description: Use when updating, filling, reviewing or fixing OrcaSlicer translations in localization/i18n — running scripts/run_gettext --full, translating empty or new msgids in the .po catalogs, AI translation passes and their "# AI Translated" / "# Needs human review" comments, plural forms, msgctxt homonyms, and keeping catalogs consistent with the localization glossary (OrcaSlicer_WIKI localization_glossary.tsv), including adding new glossary terms. Also use for msgfmt or run_gettext failures, placeholder or line-break mismatches, and reviewing a translation PR against the glossary.
|
||||
description: Use when updating, filling, reviewing or fixing OrcaSlicer translations in localization/i18n — running scripts/run_gettext --full, translating empty or new msgids in the .po catalogs, AI translation passes and their "# AI Translated" / "# Needs human review" comments, plural forms, msgctxt homonyms, and keeping catalogs consistent with the localization glossary (OrcaSlicer_WIKI developer_reference/glossary), including adding new glossary terms. Also use for msgfmt or run_gettext failures, placeholder or line-break mismatches, and reviewing a translation PR against the glossary.
|
||||
---
|
||||
|
||||
# OrcaSlicer translations
|
||||
@@ -9,7 +9,7 @@ The UI strings live in gettext catalogs: `localization/i18n/OrcaSlicer.pot` is t
|
||||
`localization/i18n/<lang>/OrcaSlicer_<lang>.po` holds each language. The `en` catalog is empty on
|
||||
purpose, so skip it. Recurring terms are fixed by the
|
||||
[localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md),
|
||||
whose source is `developer_reference/localization_glossary.tsv` in the **OrcaSlicer_WIKI** repository.
|
||||
whose source is the per-language files in `developer_reference/glossary/` (`terms.tsv`, `languages.txt` and one `<lang>.tsv`) in the **OrcaSlicer_WIKI** repository.
|
||||
A translation pass therefore touches two repositories: the catalogs here, and the glossary there.
|
||||
|
||||
All catalog editing goes through [`scripts/po_tool.py`](scripts/po_tool.py) (standard library only).
|
||||
@@ -153,7 +153,7 @@ Report:
|
||||
- anything left empty, and why;
|
||||
- the glossary rows added or filled.
|
||||
|
||||
The catalogs (and `OrcaSlicer.pot`) and the glossary (`localization_glossary.tsv` and `.md`) are
|
||||
The catalogs (and `OrcaSlicer.pot`) and the glossary (`developer_reference/glossary/*.tsv` and `localization_glossary.md`) are
|
||||
separate changes in separate repositories. The wiki change should land with, or before, the catalog
|
||||
change. Commit or open PRs only when asked.
|
||||
|
||||
@@ -210,7 +210,7 @@ with the translators, followed by the catalogs.
|
||||
When this skill and the checkouts disagree, the checkouts win:
|
||||
- `scripts/run_gettext.sh` / `.bat` for extraction and merging;
|
||||
- `localization/i18n/list.txt` for the scanned sources;
|
||||
- the wiki's `localization_guide.md` and `localization_glossary.md` / `.tsv` for the rules and terms;
|
||||
- the wiki's `localization_guide.md` and `localization_glossary.md` and `glossary/*.tsv` for the rules and terms;
|
||||
- `AGENTS.md` (Localization & translations) for the repository's conventions.
|
||||
|
||||
## Editing this skill
|
||||
|
||||
@@ -28,13 +28,14 @@ import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unicodedata
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[4]
|
||||
I18N = REPO / "localization" / "i18n"
|
||||
SKIP_LANGS = {"en"} # the English catalog is intentionally empty
|
||||
GLOSSARY_TSV = Path("developer_reference") / "localization_glossary.tsv"
|
||||
GLOSSARY_DIR = Path("developer_reference") / "glossary" # terms.tsv, languages.txt and one <lang>.tsv per language
|
||||
GLOSSARY_MD = Path("developer_reference") / "localization_glossary.md"
|
||||
MISSING = {"", "—"}
|
||||
RAW_URL = "https://raw.githubusercontent.com/OrcaSlicer/OrcaSlicer_WIKI/main/"
|
||||
@@ -252,30 +253,40 @@ def all_langs():
|
||||
|
||||
def find_wiki(arg):
|
||||
for cand in [arg, os.environ.get("ORCA_WIKI_DIR"), REPO.parent / "OrcaSlicer_WIKI", REPO.parent / "OrcaSlicer.wiki"]:
|
||||
if cand and (Path(cand) / GLOSSARY_TSV).exists():
|
||||
if cand and (Path(cand) / GLOSSARY_DIR / "terms.tsv").exists():
|
||||
return Path(cand)
|
||||
return None
|
||||
|
||||
|
||||
def _download(rel):
|
||||
cache = Path(tempfile.gettempdir()) / ("orca_glossary_" + rel.name)
|
||||
def _download(rel, required=True):
|
||||
cache = Path(tempfile.gettempdir()) / "orca_glossary" / rel
|
||||
if not cache.exists() or time.time() - cache.stat().st_mtime > 3600:
|
||||
with urllib.request.urlopen(RAW_URL + rel.as_posix(), timeout=30) as r:
|
||||
cache.write_bytes(r.read())
|
||||
try:
|
||||
with urllib.request.urlopen(RAW_URL + rel.as_posix(), timeout=30) as r:
|
||||
data = r.read()
|
||||
except urllib.error.HTTPError:
|
||||
if required:
|
||||
raise
|
||||
return None
|
||||
cache.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache.write_bytes(data)
|
||||
return cache
|
||||
|
||||
|
||||
class Glossary:
|
||||
def __init__(self, wiki_arg=None, quiet=False):
|
||||
"""The glossary: rows of [English, Description, <one translation per language loaded>]."""
|
||||
|
||||
def __init__(self, wiki_arg=None, quiet=False, langs=None):
|
||||
self.wiki = find_wiki(wiki_arg)
|
||||
self.langs = langs # only these languages' files are read (and downloaded); None means all
|
||||
if self.wiki:
|
||||
self.tsv, self.md = self.wiki / GLOSSARY_TSV, self.wiki / GLOSSARY_MD
|
||||
self.dir, self.md = self.wiki / GLOSSARY_DIR, self.wiki / GLOSSARY_MD
|
||||
if not quiet:
|
||||
self.report_clone_state()
|
||||
else:
|
||||
if not quiet:
|
||||
print("note: no OrcaSlicer_WIKI clone found; using the published glossary read-only", file=sys.stderr)
|
||||
self.tsv, self.md = _download(GLOSSARY_TSV), _download(GLOSSARY_MD)
|
||||
self.dir, self.md = _download(GLOSSARY_DIR / "terms.tsv").parent, _download(GLOSSARY_MD)
|
||||
self.load()
|
||||
|
||||
def report_clone_state(self):
|
||||
@@ -290,11 +301,23 @@ class Glossary:
|
||||
if behind and behind != "0":
|
||||
print(f"note: the wiki clone is {behind} commits behind origin/main (as of its last fetch)", file=sys.stderr)
|
||||
|
||||
def _read(self, name, required=True):
|
||||
path = self.dir / name if self.wiki else _download(GLOSSARY_DIR / name, required)
|
||||
return path.read_text(encoding="utf-8-sig").splitlines() if path and path.exists() else None
|
||||
|
||||
def load(self):
|
||||
rows = [l.split("\t") for l in self.tsv.read_text(encoding="utf-8-sig").splitlines() if l.strip()]
|
||||
self.header = rows[0]
|
||||
# "—" marks a language with no established rendering yet
|
||||
self.rows = [[c if c.strip() not in MISSING else "" for c in r] + [""] * (len(rows[0]) - len(r)) for r in rows[1:]]
|
||||
terms = [l.split(" ") for l in self._read("terms.tsv")[1:] if l.strip()]
|
||||
langs = [l.strip() for l in self._read("languages.txt") if l.strip()]
|
||||
langs = [l for l in langs if self.langs is None or l in self.langs]
|
||||
self.header = ["English", "Description"] + langs
|
||||
self.rows = [[r[0], r[1] if len(r) > 1 else ""] + [""] * len(langs) for r in terms]
|
||||
index = {r[0]: r for r in self.rows}
|
||||
for col, lang in enumerate(langs, start=2):
|
||||
for line in (self._read(f"{lang}.tsv", required=False) or [])[1:]:
|
||||
cells = line.split(" ")
|
||||
# "—" marks a language with no established rendering yet
|
||||
if len(cells) > 2 and cells[0] in index and cells[2].strip() not in MISSING:
|
||||
index[cells[0]][col] = cells[2].strip()
|
||||
self.matchers = []
|
||||
for row in self.rows:
|
||||
base = re.sub(r"\s*\([^)]*\)\s*$", "", row[0]).strip()
|
||||
@@ -494,7 +517,7 @@ def similar_entries(c, e, pool, cutoff=0.72):
|
||||
|
||||
def cmd_pending(a):
|
||||
c = Catalog(catalog_path(a.lang))
|
||||
g = Glossary(a.wiki)
|
||||
g = Glossary(a.wiki, langs=[a.lang])
|
||||
todo = [e for e in c.active() if needs_translation(e, c.nplurals, a.include_fuzzy)]
|
||||
batch = todo[a.offset:a.offset + a.limit] if a.limit else todo[a.offset:]
|
||||
pool = [(x.msgid, x.msgstrs()[0], x.obsolete) for x in c.entries
|
||||
@@ -533,7 +556,7 @@ def cmd_pending(a):
|
||||
|
||||
def cmd_apply(a):
|
||||
c = Catalog(catalog_path(a.lang))
|
||||
g = None if a.no_glossary else Glossary(a.wiki, quiet=True)
|
||||
g = None if a.no_glossary else Glossary(a.wiki, quiet=True, langs=[a.lang])
|
||||
data = json.loads(Path(a.input).read_text(encoding="utf-8"))
|
||||
items = data["entries"] if isinstance(data, dict) else data
|
||||
idx = c.index()
|
||||
@@ -588,7 +611,7 @@ def find_msgfmt():
|
||||
|
||||
|
||||
def cmd_check(a):
|
||||
g = None if a.no_glossary else Glossary(a.wiki, quiet=True)
|
||||
g = None if a.no_glossary else Glossary(a.wiki, quiet=True, langs=a.lang or None)
|
||||
msgfmt = find_msgfmt()
|
||||
status = 0
|
||||
for lang in a.lang or all_langs():
|
||||
@@ -646,7 +669,7 @@ def cmd_lookup(a):
|
||||
|
||||
|
||||
def cmd_glossary(a):
|
||||
g = Glossary(a.wiki)
|
||||
g = Glossary(a.wiki, langs=[a.lang])
|
||||
col = g.column(a.lang)
|
||||
if col is None:
|
||||
print(f"'{a.lang}' has no glossary column yet; add one with glossary-set --set {a.lang}=...")
|
||||
@@ -733,6 +756,17 @@ def cmd_candidates(a):
|
||||
print(f"{cnt:7} {langs:6} {text}{' [label]' if k in labels else ''} e.g. {ex[:70]}{'…' if len(ex) > 70 else ''}")
|
||||
|
||||
|
||||
def _edit_tsv(path, edit):
|
||||
"""Apply edit(lines) to a glossary TSV, keeping its BOM and line endings."""
|
||||
data = path.read_bytes()
|
||||
bom = data.startswith(b"\xef\xbb\xbf")
|
||||
text = data.decode("utf-8-sig")
|
||||
nl = "\r\n" if "\r\n" in text else "\n"
|
||||
lines = text.splitlines()
|
||||
edit(lines)
|
||||
path.write_bytes((b"\xef\xbb\xbf" if bom else b"") + (nl.join(lines) + nl).encode("utf-8"))
|
||||
|
||||
|
||||
def cmd_glossary_set(a):
|
||||
wiki = find_wiki(a.wiki)
|
||||
if not wiki:
|
||||
@@ -741,8 +775,21 @@ def cmd_glossary_set(a):
|
||||
f" {a.term}: " + ", ".join(a.set) + "\n"
|
||||
"Clone https://github.com/OrcaSlicer/OrcaSlicer_WIKI next to this repository, or pass --wiki.", file=sys.stderr)
|
||||
return 2
|
||||
tsv = wiki / GLOSSARY_TSV
|
||||
lock = tsv.with_suffix(".lock")
|
||||
gdir = wiki / GLOSSARY_DIR
|
||||
gen = wiki / "generate_glossary.py"
|
||||
if not gen.exists():
|
||||
sys.exit(f"{gen} not found")
|
||||
run_gen = lambda *args: subprocess.run([sys.executable, str(gen), *args], cwd=wiki, capture_output=True, text=True)
|
||||
langs = [l.strip() for l in (gdir / "languages.txt").read_text(encoding="utf-8-sig").splitlines() if l.strip()]
|
||||
values = {}
|
||||
for s in a.set:
|
||||
lang, _, val = s.partition("=")
|
||||
if lang not in langs:
|
||||
sys.exit(f"no glossary language '{lang}' (add it to {GLOSSARY_DIR.as_posix()}/languages.txt)")
|
||||
if "\t" in val or "\n" in val:
|
||||
sys.exit("values cannot contain tabs or line breaks")
|
||||
values[lang] = val.strip()
|
||||
lock = gdir / ".lock"
|
||||
for _ in range(100):
|
||||
try:
|
||||
fd = os.open(lock, os.O_CREAT | os.O_EXCL)
|
||||
@@ -752,50 +799,51 @@ def cmd_glossary_set(a):
|
||||
else:
|
||||
sys.exit(f"could not lock {lock}; remove it if no other run is active")
|
||||
try:
|
||||
data = tsv.read_bytes()
|
||||
bom = data.startswith(b"\xef\xbb\xbf")
|
||||
text = data.decode("utf-8-sig")
|
||||
nl = "\r\n" if "\r\n" in text else "\n"
|
||||
lines = text.splitlines()
|
||||
header = lines[0].split("\t")
|
||||
values = {}
|
||||
for s in a.set:
|
||||
lang, _, val = s.partition("=")
|
||||
if lang not in header:
|
||||
sys.exit(f"no glossary column '{lang}'")
|
||||
if "\t" in val or "\n" in val:
|
||||
sys.exit("values cannot contain tabs or line breaks")
|
||||
values[header.index(lang)] = val.strip()
|
||||
row_i = next((i for i, l in enumerate(lines[1:], 1) if l.split("\t")[0] == a.term), None)
|
||||
new = row_i is None
|
||||
if new:
|
||||
if not a.description:
|
||||
sys.exit("a new term needs --description (one sentence saying what it is)")
|
||||
row = [a.term, a.description] + [""] * (len(header) - 2)
|
||||
lines.append("")
|
||||
row_i = len(lines) - 1
|
||||
else:
|
||||
row = lines[row_i].split("\t")
|
||||
row += [""] * (len(header) - len(row))
|
||||
if a.description:
|
||||
row[1] = a.description
|
||||
conflicts = [f"{header[i]}: '{row[i]}' -> '{v}'" for i, v in values.items() if row[i].strip() not in MISSING and row[i] != v]
|
||||
terms = (gdir / "terms.tsv").read_text(encoding="utf-8-sig").splitlines()
|
||||
new = not any(l.split("\t")[0] == a.term for l in terms[1:])
|
||||
if new and not a.description:
|
||||
sys.exit("a new term needs --description (one sentence saying what it is)")
|
||||
# an established translation is only replaced on request
|
||||
conflicts = []
|
||||
for lang, v in values.items():
|
||||
for line in (gdir / f"{lang}.tsv").read_text(encoding="utf-8-sig").splitlines()[1:]:
|
||||
cells = line.split("\t")
|
||||
if cells[0] == a.term and len(cells) > 2 and cells[2].strip() not in MISSING and cells[2] != v:
|
||||
conflicts.append(f"{lang}: '{cells[2]}' -> '{v}'")
|
||||
if conflicts and not a.replace:
|
||||
sys.exit("established translations differ (pass --replace, and update the catalogs to match):\n " + "\n ".join(conflicts))
|
||||
for i, v in values.items():
|
||||
row[i] = v
|
||||
lines[row_i] = "\t".join(row)
|
||||
tsv.write_bytes((b"\xef\xbb\xbf" if bom else b"") + (nl.join(lines) + nl).encode("utf-8"))
|
||||
|
||||
def edit_terms(lines):
|
||||
for i, l in enumerate(lines):
|
||||
if i and l.split("\t")[0] == a.term:
|
||||
if a.description:
|
||||
lines[i] = f"{a.term}\t{a.description}"
|
||||
return
|
||||
lines.append(f"{a.term}\t{a.description}")
|
||||
if new or a.description:
|
||||
_edit_tsv(gdir / "terms.tsv", edit_terms)
|
||||
r = run_gen("--sync") # copies the term and description into every language file
|
||||
if r.returncode:
|
||||
sys.exit((r.stdout + r.stderr).strip())
|
||||
|
||||
def setter(lang, v):
|
||||
def edit(lines):
|
||||
for i, l in enumerate(lines):
|
||||
cells = l.split("\t")
|
||||
if i and cells[0] == a.term:
|
||||
lines[i] = "\t".join(cells[:2] + [v])
|
||||
return
|
||||
sys.exit(f"{lang}.tsv has no row for '{a.term}'")
|
||||
return edit
|
||||
for lang, v in values.items():
|
||||
_edit_tsv(gdir / f"{lang}.tsv", setter(lang, v))
|
||||
finally:
|
||||
os.close(fd)
|
||||
os.unlink(lock)
|
||||
print(f"glossary: {'added' if new else 'updated'} '{a.term}'")
|
||||
gen = wiki / "generate_glossary.py"
|
||||
if gen.exists():
|
||||
r = subprocess.run([sys.executable, str(gen), "--update"], cwd=wiki, capture_output=True, text=True)
|
||||
print((r.stdout + r.stderr).strip())
|
||||
return r.returncode
|
||||
return 0
|
||||
r = run_gen("--update")
|
||||
print((r.stdout + r.stderr).strip())
|
||||
return r.returncode
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -569,12 +569,19 @@ on the **first** call. So:
|
||||
#endif
|
||||
```
|
||||
|
||||
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) branches on
|
||||
`GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On the default path it forces `GDK_BACKEND=x11`
|
||||
when wx lacks EGL and `WAYLAND_DISPLAY` is set. It sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
|
||||
(non-replacing) only when both `DISPLAY` and `WAYLAND_DISPLAY` are set, and calls `XInitThreads()`
|
||||
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) sets the NVIDIA
|
||||
DMABUF workaround and then branches on `GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On both
|
||||
paths, when `/proc/driver/nvidia/version` exists and the linked WebKitGTK honours the switch (>= 2.44,
|
||||
`WebView::SupportsForceSharedMemory()`), it sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and
|
||||
`WEBKIT_FORCE_DMABUF_RENDERER=1` (non-replacing): the proprietary driver cannot back WebKit's hardware
|
||||
DMABUF renderer. On a hypervisor guest (`/proc/cpuinfo` `hypervisor` flag) and WebKitGTK >= 2.44 it sets
|
||||
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` alone, since virtual GPUs cannot back the hardware path either. On the default path it forces `GDK_BACKEND=x11` when wx lacks EGL and `WAYLAND_DISPLAY`
|
||||
is set, and sets `WEBKIT_DISABLE_COMPOSITING_MODE=1` (non-replacing) inside that branch when the linked
|
||||
WebKitGTK is older than 2.42: a forced X11 backend in a Wayland session is the only default-path case
|
||||
that runs through XWayland, which `DISPLAY` and `WAYLAND_DISPLAY` alone do not tell you (a native
|
||||
Wayland app inherits both). It calls `XInitThreads()`
|
||||
only when `DISPLAY` is set. With neither variable set, the GUI refuses to start ("Neither DISPLAY nor
|
||||
WAYLAND_DISPLAY set"). The details and the WebKit rule (c12912e0df) are in
|
||||
WAYLAND_DISPLAY set"). The details and the WebKit rules (c12912e0df) are in
|
||||
`references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions.
|
||||
|
||||
**wx's own detectors**, for reference:
|
||||
|
||||
@@ -61,7 +61,8 @@ available; GTK2/WebKit1 is an opt-out configuration the GUI does not support.
|
||||
11. A web host that re-themes in place handles `EVT_WEBVIEW_RECREATED` without `Skip()`; one that
|
||||
needs a reload lets it through. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
|
||||
12. Keep `WebViewWebKit`'s destructor removing the `"wx"` handler. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
|
||||
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond Orca's default-path XWayland case, and never
|
||||
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond the no-EGL default path where Orca forces
|
||||
`GDK_BACKEND=x11`, for WebKitGTK older than 2.42, and never
|
||||
make a page's progress depend only on a C++→JS callback. §[WebKitGTK on Linux](#webkitgtk-on-linux-sessions)
|
||||
14. GL attribute lists: legacy `int[]` lists end with `0` and spell out `WX_GL_RGBA` and
|
||||
`WX_GL_DOUBLEBUFFER`; `wxGLAttributes`/`wxGLContextAttrs` end with `EndList()`; MSAA is requested
|
||||
@@ -567,33 +568,74 @@ vue-resize workaround script and sets a persistent SQLite cookie store on the vi
|
||||
## WebKitGTK on Linux sessions
|
||||
|
||||
**Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises)
|
||||
branches on `GDK_BACKEND`:
|
||||
sets the DMABUF workarounds and then branches on `GDK_BACKEND`:
|
||||
- **Both paths**, before the split: when `/proc/driver/nvidia/version` exists and the linked WebKitGTK
|
||||
honours the switch (>= 2.44, `WebView::SupportsForceSharedMemory()`), it sets
|
||||
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WEBKIT_FORCE_DMABUF_RENDERER=1` non-replacing. The
|
||||
proprietary driver cannot back WebKit's hardware DMABUF renderer, so the shared-memory transport keeps
|
||||
accelerated compositing instead of dropping it; `WEBKIT_FORCE_DMABUF_RENDERER` clears the
|
||||
Debian/Ubuntu/openSUSE `isNVIDIA()` early return, which otherwise returns before the FORCE_SHM check.
|
||||
- **Hypervisor guests**, same position: when `is_hypervisor_guest()` (the `hypervisor` flag in
|
||||
`/proc/cpuinfo`) and WebKitGTK >= 2.44, it sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` non-replacing.
|
||||
Virtual GPUs (VirtualBox VMSVGA, VMware, QEMU) cannot back the hardware DMABUF path either, and
|
||||
FORCE_SHM alone was confirmed to fix web views in VirtualBox; the `WEBKIT_FORCE_DMABUF_RENDERER` pair
|
||||
is not needed without the Debian `isNVIDIA()` patch. The flag is x86-only, so ARM guests are not covered.
|
||||
- **Default path** (`GDK_BACKEND` unset or not starting with `x11`): if wx lacks EGL (`!wxHAS_EGL`) and
|
||||
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`; it sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
|
||||
non-replacing **only** when both `DISPLAY` and `WAYLAND_DISPLAY` are non-empty (a Wayland session with
|
||||
XWayland available — the session, not the backend GTK will pick); `XInitThreads()` runs only when
|
||||
`DISPLAY` is set.
|
||||
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`, and in that same branch it sets
|
||||
`WEBKIT_DISABLE_COMPOSITING_MODE=1` non-replacing when the linked WebKitGTK predates 2.42
|
||||
(`WebView::NeedsCompositingWorkaround()`). That forced X11 backend in a Wayland session is the only
|
||||
default-path case that really runs through XWayland; `DISPLAY` and `WAYLAND_DISPLAY` alone do not
|
||||
identify it, because a native Wayland app inherits both. `XInitThreads()` runs only when `DISPLAY` is
|
||||
set.
|
||||
- **`GDK_BACKEND=x11…` opt-in path**: sets `DRI_PRIME=1` (and the NVIDIA PRIME variables when
|
||||
`/proc/driver/nvidia/version` exists), calls `XInitThreads()`, and deliberately does **not** disable
|
||||
compositing (the comment: the WebKit2GTK XWayland bug appears fixed in WebKit2GTK ≥ 2.42, and leaving
|
||||
compositing on keeps hardware acceleration for Device / Setup Wizard / login pages).
|
||||
compositing, which keeps hardware acceleration for Device / Setup Wizard / login pages.
|
||||
|
||||
Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_running_on_x11()`
|
||||
(see `references/platforms.md`), not these environment variables.
|
||||
|
||||
**Pitfall.**
|
||||
- **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path in Wayland sessions,
|
||||
non-replacing (`setenv(..., false)`, so a value the user already exported wins), before GTK
|
||||
initialises; never unconditionally. Keep JS-side fallbacks so a page never
|
||||
- **Rule:** On NVIDIA and hypervisor guests reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
|
||||
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver and virtual GPUs cannot back WebKit's
|
||||
hardware DMABUF renderer, leaving web views blank or slow; FORCE_SHM keeps accelerated compositing and only
|
||||
skips the hardware path, while disabling compositing drops it. Threaded scrolling is a separate,
|
||||
release-dependent default (off in 2.44/2.45, on from 2.46). `WEBKIT_DISABLE_DMABUF_RENDERER` is not a
|
||||
substitute — it is read first, leaves the transport set empty (no compositing from 2.44 on, where the
|
||||
X11/Wayland fallback renderers were removed) and has a reported crash path. On Debian/Ubuntu and
|
||||
openSUSE the `isNVIDIA()` patch returns before the FORCE_SHM check, so `WEBKIT_FORCE_DMABUF_RENDERER=1`
|
||||
must clear it first; that variable exists only in those patches. WebKit reads all of these once per
|
||||
process, before the first WebView.
|
||||
The test is `/proc/driver/nvidia/version`, so a hybrid laptop that renders on its iGPU also takes the
|
||||
shared-memory path, which is a per-frame GPU readback rather than zero-copy; exporting
|
||||
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=0` in the environment restores the hardware path.
|
||||
```cpp
|
||||
// Wrong: kills compositing and threaded scrolling (and can crash on modern WebKit)
|
||||
::setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", false);
|
||||
// Right: keep compositing, skip only the hardware DMABUF path
|
||||
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
|
||||
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /*replace*/ false);
|
||||
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /*replace*/ false);
|
||||
}
|
||||
```
|
||||
Cite: WebKit 2.52.6 `Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.cpp`
|
||||
(`rendererBufferTransportMode()`), Debian `disable-nvidia-dmabuf.patch`, WebKit bug 262607.
|
||||
- **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path, in the branch where wx
|
||||
lacks EGL and Orca forces `GDK_BACKEND=x11`, non-replacing (`setenv(..., false)`, so a value the user
|
||||
already exported wins), before GTK
|
||||
initialises; never unconditionally, and only for the WebKitGTK releases that need it (older than
|
||||
2.42, `WebView::NeedsCompositingWorkaround()`). Keep JS-side fallbacks so a page never
|
||||
waits forever for a C++→JS call.
|
||||
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland, leaving web views
|
||||
blank (Setup Wizard stuck on "Loading..."); disabling it everywhere needlessly drops hardware
|
||||
acceleration; without the JS timeout the user is stuck when the signal is lost.
|
||||
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland before 2.42, leaving
|
||||
web views blank (Setup Wizard stuck on "Loading..."); the variable also turns off threaded scrolling,
|
||||
so applying it on a newer WebKit needlessly drops both GPU compositing and async scrolling (the
|
||||
Device tab loses its smoothness); without the JS timeout the user is stuck when the signal is lost.
|
||||
```cpp
|
||||
// Wrong: unconditional, kills HW acceleration everywhere
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false);
|
||||
// Right (default path only): non-replacing, before GTK init, Wayland session with XWayland
|
||||
if (display && *display && wayland && *wayland)
|
||||
// Right (default path only): non-replacing, before GTK init, WebKitGTK < 2.42,
|
||||
// and only in the branch that forces GDK_BACKEND=x11 (wx without EGL), the
|
||||
// case that runs through XWayland
|
||||
if (WebView::NeedsCompositingWorkaround() && wayland && *wayland)
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /*replace*/ false);
|
||||
```
|
||||
Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit`
|
||||
|
||||
@@ -19,6 +19,8 @@ on:
|
||||
- 'build_linux.sh'
|
||||
- 'build_win.bat'
|
||||
- 'scripts/test_build_win.ps1'
|
||||
- 'scripts/check_exception_throw_info.ps1'
|
||||
- 'scripts/test_check_exception_throw_info.ps1'
|
||||
- 'build_release_macos.sh'
|
||||
- 'scripts/build_preset_cache.*'
|
||||
- 'scripts/flatpak/**'
|
||||
@@ -41,6 +43,8 @@ on:
|
||||
- 'build_linux.sh'
|
||||
- 'build_win.bat'
|
||||
- 'scripts/test_build_win.ps1'
|
||||
- 'scripts/check_exception_throw_info.ps1'
|
||||
- 'scripts/test_check_exception_throw_info.ps1'
|
||||
- 'build_release_macos.sh'
|
||||
- 'scripts/build_preset_cache.*'
|
||||
- 'scripts/flatpak/**'
|
||||
@@ -84,6 +88,10 @@ jobs:
|
||||
shell: powershell
|
||||
run: .\scripts\test_build_win.ps1
|
||||
|
||||
- name: Run the exception throw info check tests
|
||||
shell: powershell
|
||||
run: .\scripts\test_check_exception_throw_info.ps1
|
||||
|
||||
build_linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
@@ -65,15 +65,6 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}"
|
||||
# clang-cl refuses a precompiled header from another cl.exe build and ccache
|
||||
# does not hash that build, so each one gets its own cache. The build number
|
||||
# is read from cl.exe itself; the toolset directory keeps its name across patches.
|
||||
if [ "${{ runner.os }}" = Windows ]; then
|
||||
vswhere='/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe'
|
||||
toolset=$(tr -d '\r\n' < "$("$vswhere" -latest -products '*' -find 'VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt' | tr -d '\r')")
|
||||
cl=$("$vswhere" -latest -products '*' -find 'VC\Tools\MSVC\'"$toolset"'\**\cl.exe' | tr -d '\r' | head -1)
|
||||
leg="$leg-vc$("$cl" 2>&1 | grep -o -E 'Version [0-9.]+' | cut -d' ' -f2)"
|
||||
fi
|
||||
echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV"
|
||||
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
|
||||
|
||||
@@ -552,6 +543,13 @@ jobs:
|
||||
name: OrcaSlicer_profile_validator_Windows_${{ env.ver }}
|
||||
path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
|
||||
|
||||
- name: Check exception throw info Win
|
||||
if: runner.os == 'Windows' && inputs.compiler == 'clang'
|
||||
working-directory: ${{ github.workspace }}
|
||||
# After the uploads, so a failure still leaves the build and its PDB to inspect, and before any deploy.
|
||||
shell: pwsh
|
||||
run: scripts\check_exception_throw_info.ps1 -BuildDir $env:BUILD_DIR
|
||||
|
||||
- name: Deploy Windows release portable
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
|
||||
@@ -232,6 +232,31 @@ jobs:
|
||||
mkdir -p ${{ runner.temp }}/profile-check-results
|
||||
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt
|
||||
|
||||
# Builds a readable diff of the custom G-code the PR changes, which GitHub shows as
|
||||
# one long line per setting, for check_profiles_comment.yml to post.
|
||||
- name: Diff custom G-code
|
||||
if: ${{ always() && github.event_name == 'pull_request' }}
|
||||
continue-on-error: true
|
||||
env:
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
git fetch --no-tags --depth=2 origin "$(git rev-parse HEAD)"
|
||||
results=${{ runner.temp }}/profile-check-results
|
||||
diff=${{ runner.temp }}/gcode_diff.md
|
||||
# HEAD is the PR's merge commit, so HEAD^1 is the base it merges onto.
|
||||
python3 ./scripts/orca_profile_tool.py gcode-diff --base HEAD^1 --head HEAD > "$diff"
|
||||
if [ -s "$diff" ]; then
|
||||
{
|
||||
echo "<!-- profile-gcode-diff-comment -->"
|
||||
cat "$diff"
|
||||
echo ""
|
||||
# The comment workflow finds this line as the only one starting with <sub>.
|
||||
echo "<sub>For commit ${PR_HEAD_SHA} of this PR.</sub>"
|
||||
} > "$results/gcode_diff.md"
|
||||
fi
|
||||
# Tells the comment workflow that a missing gcode_diff.md means no G-code changed.
|
||||
touch "$results/gcode_diff_done.txt"
|
||||
|
||||
- name: Prepare comment artifact
|
||||
if: ${{ always() && github.event_name == 'pull_request' && (steps.profile_tool.outcome == 'failure' || steps.legacy_compat.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
|
||||
run: |
|
||||
|
||||
@@ -38,6 +38,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
if [ ! -f pr_number.txt ]; then
|
||||
echo "No pr_number.txt in artifact, skipping."
|
||||
@@ -49,18 +50,72 @@ jobs:
|
||||
echo "Invalid PR number: $PR_NUMBER"
|
||||
exit 1
|
||||
fi
|
||||
# PR code wrote the artifact, so only a run for the named PR's current head may post.
|
||||
if [ -z "$HEAD_SHA" ]; then
|
||||
echo "The triggering run names no head commit."
|
||||
exit 1
|
||||
fi
|
||||
if ! PR_HEAD=$(gh api "repos/${GH_REPO}/pulls/${PR_NUMBER}" --jq .head.sha); then
|
||||
echo "Cannot look up PR #${PR_NUMBER}."
|
||||
exit 1
|
||||
fi
|
||||
if [ "$HEAD_SHA" != "$PR_HEAD" ]; then
|
||||
echo "${HEAD_SHA} is not the head of PR #${PR_NUMBER}; skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Lists every bot comment on the PR, one JSON object per line.
|
||||
COMMENTS=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" \
|
||||
--jq '.[] | select(.user.login == "github-actions[bot]") | {id, body} | @json')
|
||||
|
||||
# Delete prior comments matching the marker (from check_profiles.yml) or the legacy heading.
|
||||
OLD_IDS=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" \
|
||||
--jq '.[] | select(.user.login == "github-actions[bot]") | select((.body | startswith("<!-- profile-validation-comment -->")) or (.body | startswith("## :x: Profile Validation Errors"))) | .id')
|
||||
OLD_IDS=$(printf '%s\n' "$COMMENTS" | jq -r 'select((.body | startswith("<!-- profile-validation-comment -->")) or (.body | startswith("## :x: Profile Validation Errors"))) | .id')
|
||||
for comment_id in $OLD_IDS; do
|
||||
echo "Deleting outdated profile-validation comment ${comment_id}"
|
||||
gh api -X DELETE "repos/${GH_REPO}/issues/comments/${comment_id}" || true
|
||||
done
|
||||
|
||||
# Post a new comment only when validation failed (pr_comment.md present).
|
||||
# A failed post fails the job only after the G-code diff is handled.
|
||||
failed=0
|
||||
if [ -f pr_comment.md ]; then
|
||||
gh pr comment "$PR_NUMBER" --body-file pr_comment.md
|
||||
gh pr comment "$PR_NUMBER" --body-file pr_comment.md || failed=1
|
||||
else
|
||||
echo "Validation succeeded; cleaned up prior comments without posting."
|
||||
fi
|
||||
|
||||
# Makes the G-code diff comment match gcode_diff.md, or deletes it if there is none.
|
||||
update_gcode_diff_comment() {
|
||||
if [ ! -f gcode_diff_done.txt ]; then
|
||||
echo "The G-code diff step did not finish; leaving any G-code diff comment as it is."
|
||||
return 0
|
||||
fi
|
||||
GCODE_IDS=$(printf '%s\n' "$COMMENTS" | jq -r 'select(.body | startswith("<!-- profile-gcode-diff-comment -->")) | .id')
|
||||
if [ -f gcode_diff.md ] && [ "$(wc -c < gcode_diff.md)" -gt 65536 ]; then
|
||||
echo "gcode_diff.md is over GitHub's comment limit; not posting it."
|
||||
rm gcode_diff.md
|
||||
fi
|
||||
if [ -f gcode_diff.md ] && [ "$(echo "$GCODE_IDS" | wc -w)" -eq 1 ]; then
|
||||
OLD_BODY=$(printf '%s\n' "$COMMENTS" | jq -r --argjson id "$GCODE_IDS" 'select(.id == $id) | .body' | tr -d '\r')
|
||||
NEW_BODY=$(tr -d '\r' < gcode_diff.md)
|
||||
if [ "$OLD_BODY" = "$NEW_BODY" ]; then
|
||||
echo "G-code diff unchanged; keeping comment ${GCODE_IDS}."
|
||||
return 0
|
||||
fi
|
||||
# Edits in place when only the <sub> line differs, since edits notify nobody.
|
||||
if [ "$(grep -v '^<sub>' <<< "$OLD_BODY")" = "$(grep -v '^<sub>' <<< "$NEW_BODY")" ]; then
|
||||
echo "G-code diff unchanged at a new commit; updating comment ${GCODE_IDS}."
|
||||
gh api -X PATCH "repos/${GH_REPO}/issues/comments/${GCODE_IDS}" -F body=@gcode_diff.md > /dev/null
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
for comment_id in $GCODE_IDS; do
|
||||
echo "Deleting outdated G-code diff comment ${comment_id}"
|
||||
gh api -X DELETE "repos/${GH_REPO}/issues/comments/${comment_id}" || true
|
||||
done
|
||||
if [ -f gcode_diff.md ]; then
|
||||
gh pr comment "$PR_NUMBER" --body-file gcode_diff.md
|
||||
fi
|
||||
}
|
||||
update_gcode_diff_comment
|
||||
exit "$failed"
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -288,6 +288,19 @@ if (APPLE)
|
||||
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts.
|
||||
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO")
|
||||
|
||||
# The Xcode generator ignores CMAKE_<LANG>_COMPILER_LAUNCHER, so hand the launcher to
|
||||
# Xcode's own Compiler Launcher setting, which wraps every clang compile (C, C++,
|
||||
# Objective-C++ and the precompiled header) with one program: the C++ launcher wins when
|
||||
# both are set. The setting takes a single program path, not a CMake launcher list.
|
||||
# Xcode versions without it ignore it and build uncached.
|
||||
if (CMAKE_GENERATOR STREQUAL "Xcode")
|
||||
if (CMAKE_CXX_COMPILER_LAUNCHER)
|
||||
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_CXX_COMPILER_LAUNCHER}")
|
||||
elseif (CMAKE_C_COMPILER_LAUNCHER)
|
||||
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_C_COMPILER_LAUNCHER}")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN")
|
||||
@@ -308,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)
|
||||
|
||||
+17
-3
@@ -22,7 +22,7 @@ function usage() {
|
||||
echo " -g: run the requested build steps inside a Docker/Podman Ubuntu 24.04 container similar to the GitHub Actions Linux runner"
|
||||
echo " -h: prints this help text"
|
||||
echo " -i: build the Orca Slicer AppImage (optional)"
|
||||
echo " -p: boost ccache hit rate by disabling precompiled headers (default: ON)"
|
||||
echo " -p: disable precompiled headers (on by default): more ccache hits after header changes, slower misses"
|
||||
echo " -r: skip RAM and disk checks (low RAM compiling)"
|
||||
echo " -s: build the Orca Slicer (optional)"
|
||||
echo " -t: build tests (optional), requires -s flag"
|
||||
@@ -473,8 +473,8 @@ fi
|
||||
# cmake 4.x compatibility workaround
|
||||
export CMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
|
||||
DISTRIBUTION=$(awk -F= '/^ID=/ {print $2}' /etc/os-release | tr -d '"')
|
||||
DISTRIBUTION_LIKE=$(awk -F= '/^ID_LIKE=/ {print $2}' /etc/os-release | tr -d '"')
|
||||
DISTRIBUTION=$(awk -F= '/^ID=/ {print $2}' /etc/os-release | tr -d "'\"")
|
||||
DISTRIBUTION_LIKE=$(awk -F= '/^ID_LIKE=/ {print $2}' /etc/os-release | tr -d "'\"")
|
||||
# Check for direct distribution match to Ubuntu/Debian
|
||||
if [ "${DISTRIBUTION}" == "ubuntu" ] || [ "${DISTRIBUTION}" == "linuxmint" ] ; then
|
||||
DISTRIBUTION="debian"
|
||||
@@ -549,6 +549,20 @@ fi
|
||||
if [ -n "${CMAKE_CCACHE}" ] ; then
|
||||
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
|
||||
export CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
|
||||
# The same ccache settings as the CI build. Without this sloppiness ccache refuses every
|
||||
# compile that uses the precompiled header, which is most of OrcaSlicer, and skips headers
|
||||
# a checkout has just written. It is added to the sloppiness already configured, and
|
||||
# CCACHE_SLOPPINESS or CCACHE_(NO)DEPEND set in the environment win.
|
||||
if [[ "$(basename "${CMAKE_CCACHE}")" == ccache ]] ; then
|
||||
if [[ -z "${CCACHE_SLOPPINESS+x}" ]] ; then
|
||||
CCACHE_SLOPPINESS=$("${CMAKE_CCACHE}" --get-config sloppiness 2>/dev/null || true)
|
||||
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:+${CCACHE_SLOPPINESS},}pch_defines,time_macros,include_file_mtime,include_file_ctime"
|
||||
fi
|
||||
# Hash the includes the compiler reports instead of preprocessing every miss.
|
||||
if [[ -z "${CCACHE_DEPEND+x}${CCACHE_NODEPEND+x}" ]] ; then
|
||||
export CCACHE_DEPEND=1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
|
||||
fi
|
||||
|
||||
+75
-3
@@ -47,7 +47,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
||||
u )
|
||||
export BUILD_TARGET="universal"
|
||||
;;
|
||||
h ) echo "Usage: ./build_release_macos.sh [-d]"
|
||||
h ) echo "Usage: ./build_release_macos.sh [-d] [-- CMAKE_ARGS...]"
|
||||
echo " -d: Build deps only"
|
||||
echo " -a: Set ARCHITECTURE (arm64 or x86_64 or universal)"
|
||||
echo " -s: Build slicer only"
|
||||
@@ -60,13 +60,33 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
||||
echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew"
|
||||
echo " -j: Set the number of parallel build jobs (CMAKE_BUILD_PARALLEL_LEVEL)"
|
||||
echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)"
|
||||
echo " -- CMAKE_ARGS...: pass everything after '--' to the cmake configure step of the deps (-d) and slicer (-s) builds,"
|
||||
echo " e.g. -DDESTDIR=<dir> for the deps, -DCMAKE_PREFIX_PATH=<dir>/usr/local -DSLIC3R_PCH=OFF for the slicer, --fresh."
|
||||
echo " They come last on the cmake command line. Use absolute paths: cmake runs in the build directory."
|
||||
exit 0
|
||||
;;
|
||||
: )
|
||||
echo "Option -${OPTARG} requires an argument, aborting."
|
||||
exit 1
|
||||
;;
|
||||
* )
|
||||
echo "Unknown option -${OPTARG}, aborting. CMake arguments must follow '--'."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# getopts stops at '--'; whatever follows is handed to cmake unchanged.
|
||||
if [[ ${OPTIND} -le $# ]] ; then
|
||||
separator_index=$((OPTIND-1))
|
||||
if [[ "${!separator_index}" != "--" ]] ; then
|
||||
echo "Unexpected argument '${!OPTIND}': cmake arguments must follow '--', aborting."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
shift $((OPTIND-1))
|
||||
EXTRA_CMAKE_ARGS=("$@")
|
||||
|
||||
# Set defaults
|
||||
|
||||
if [ -z "$ARCH" ]; then
|
||||
@@ -111,6 +131,54 @@ else
|
||||
export CMAKE_POLICY_COMPAT=""
|
||||
fi
|
||||
|
||||
# Compiler cache, as in build_linux.sh. The program comes from CMAKE_CCACHE, else from a
|
||||
# launcher already set in the environment (the CI build sets ccache there), else sccache or
|
||||
# ccache from PATH; one program serves both languages. Each run applies its own choice to the
|
||||
# build directories, replacing a launcher an earlier run or a hand-run configure recorded.
|
||||
CMAKE_CCACHE_ARGS=()
|
||||
CCACHE_VAR=""
|
||||
for var in CMAKE_CCACHE CMAKE_CXX_COMPILER_LAUNCHER CMAKE_C_COMPILER_LAUNCHER; do
|
||||
if [ -n "${!var:-}" ]; then
|
||||
CCACHE_VAR=$var
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -n "$CCACHE_VAR" ]; then
|
||||
CCACHE_PROGRAM=${!CCACHE_VAR}
|
||||
echo "Checking ${CCACHE_VAR} environment variable for compiler cache program..."
|
||||
CMAKE_CCACHE=$(command -v "${CCACHE_PROGRAM}") || {
|
||||
echo "${CCACHE_VAR} environment variable is set to '${CCACHE_PROGRAM}' but it was not found in PATH."
|
||||
CMAKE_CCACHE=""
|
||||
}
|
||||
elif command -v sccache >/dev/null 2>&1 ; then
|
||||
CMAKE_CCACHE=$(command -v sccache)
|
||||
elif command -v ccache >/dev/null 2>&1 ; then
|
||||
CMAKE_CCACHE=$(command -v ccache)
|
||||
fi
|
||||
if [ -n "${CMAKE_CCACHE}" ] ; then
|
||||
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
|
||||
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
|
||||
# The same ccache settings as the CI build. Without this sloppiness ccache refuses every
|
||||
# compile that uses the precompiled header, which is most of OrcaSlicer, and skips headers
|
||||
# a checkout has just written. It is added to the sloppiness already configured, and
|
||||
# CCACHE_SLOPPINESS or CCACHE_(NO)DEPEND set in the environment win.
|
||||
if [[ "$(basename "${CMAKE_CCACHE}")" == ccache ]] ; then
|
||||
if [[ -z "${CCACHE_SLOPPINESS+x}" ]] ; then
|
||||
CCACHE_SLOPPINESS=$("${CMAKE_CCACHE}" --get-config sloppiness 2>/dev/null || true)
|
||||
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:+${CCACHE_SLOPPINESS},}pch_defines,time_macros,include_file_mtime,include_file_ctime"
|
||||
fi
|
||||
# Hash the includes the compiler reports instead of preprocessing every miss.
|
||||
if [[ -z "${CCACHE_DEPEND+x}${CCACHE_NODEPEND+x}" ]] ; then
|
||||
export CCACHE_DEPEND=1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
|
||||
# Clear a launcher recorded by an earlier configure. The entries must be set empty rather
|
||||
# than removed: CMake fills a missing launcher entry from the environment.
|
||||
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER:STRING= -DCMAKE_CXX_COMPILER_LAUNCHER:STRING=)
|
||||
fi
|
||||
|
||||
echo "Build params:"
|
||||
echo " - ARCH: $ARCH"
|
||||
echo " - BUILD_CONFIG: $BUILD_CONFIG"
|
||||
@@ -162,7 +230,9 @@ function build_deps() {
|
||||
-DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||
${CMAKE_POLICY_COMPAT}
|
||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||
${CMAKE_POLICY_COMPAT} \
|
||||
"${EXTRA_CMAKE_ARGS[@]}"
|
||||
fi
|
||||
cmake --build . --config "$BUILD_CONFIG" --target deps
|
||||
)
|
||||
@@ -264,7 +334,9 @@ function build_slicer() {
|
||||
-DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||
${CMAKE_POLICY_COMPAT}
|
||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||
${CMAKE_POLICY_COMPAT} \
|
||||
"${EXTRA_CMAKE_ARGS[@]}"
|
||||
fi
|
||||
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
|
||||
)
|
||||
|
||||
+7
-4
@@ -53,7 +53,7 @@ call :add_section "Build configuration"
|
||||
call :add_arg config string "" config "release, debug, relwithdebinfo or minsizerel (default: release)"
|
||||
call :add_arg target_arch string "" arch "x64 or arm64 (default: the host architecture)"
|
||||
call :add_arg slicer_asan bool a asan "Build the slicer with ASAN enabled"
|
||||
call :add_arg no_pch bool "" no-pch "Build without the precompiled header, implied by --cache"
|
||||
call :add_arg no_pch bool "" no-pch "Build without the precompiled header, implied by -l and --cache"
|
||||
|
||||
call :add_section "Toolchain"
|
||||
call :add_arg use_clang_cl bool l clang-cl "Use clang-cl as the compiler"
|
||||
@@ -388,6 +388,9 @@ if "%use_ninja%" == "ON" if "%use_msbuild%" == "ON" (
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM CMakeLists turns the precompiled header off for clang-cl (llvm/llvm-project#53486).
|
||||
if "%use_clang_cl%" == "ON" set "no_pch=ON"
|
||||
|
||||
set "generator=!vs_gen_%vs_version%!"
|
||||
if "%use_ninja%" == "ON" (
|
||||
set "generator=Ninja Multi-Config"
|
||||
@@ -543,7 +546,7 @@ REM real path rather than one glued onto the repository root.
|
||||
for %%p in ("!build_dir!") do set "build_full=%%~fp"
|
||||
echo Configuration: %build_type%, %arch%
|
||||
if not "%clang_exe%" == "" echo Compiler: %clang_exe%
|
||||
if "%no_pch%" == "ON" echo Precompiled header: off
|
||||
if "%build_slicer%" == "ON" if "%no_pch%" == "ON" echo Precompiled header: off
|
||||
if not "%cache_args%" == "" echo Compiler cache: %cache_exe%
|
||||
|
||||
set "SIG_FLAG="
|
||||
@@ -716,8 +719,8 @@ if "%build_slicer%" == "ON" (
|
||||
set "slicer_args=!slicer_args! -DSLIC3R_ASAN=ON"
|
||||
)
|
||||
|
||||
REM A later -DSLIC3R_PCH=ON in ORCA_SLICER_CMAKE_ARGS still wins, because
|
||||
REM CMake takes the last definition on the command line.
|
||||
REM Under cl, a later -DSLIC3R_PCH=ON in ORCA_SLICER_CMAKE_ARGS still wins,
|
||||
REM because CMake takes the last definition on the command line.
|
||||
if "%no_pch%" == "ON" (
|
||||
set "slicer_args=!slicer_args! -DSLIC3R_PCH=OFF"
|
||||
)
|
||||
|
||||
Vendored
+2
-17
@@ -1,22 +1,7 @@
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
if (JPEG_VERSION STREQUAL "6")
|
||||
message("Using Jpeg Lib 62")
|
||||
set(jpeg_flag "")
|
||||
elseif (JPEG_VERSION STREQUAL "7")
|
||||
message("Using Jpeg Lib 70")
|
||||
set(jpeg_flag "-DWITH_JPEG7=ON")
|
||||
else ()
|
||||
message("Using Jpeg Lib 80")
|
||||
set(jpeg_flag "-DWITH_JPEG8=ON")
|
||||
endif ()
|
||||
endif()
|
||||
|
||||
orcaslicer_add_cmake_project(JPEG
|
||||
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
|
||||
URL_HASH SHA256=d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
|
||||
DEPENDS ${ZLIB_PKG}
|
||||
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
|
||||
URL_HASH SHA256=1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
|
||||
CMAKE_ARGS
|
||||
-DENABLE_SHARED=OFF
|
||||
-DENABLE_STATIC=ON
|
||||
${jpeg_flag}
|
||||
)
|
||||
Vendored
+5
-2
@@ -26,6 +26,7 @@ orcaslicer_add_cmake_project(OpenCV
|
||||
URL https://github.com/opencv/opencv/archive/refs/tags/4.6.0.tar.gz
|
||||
URL_HASH SHA256=1ec1cba65f9f20fe5a41fda1586e01c70ea0c9a6d7b67c9e13edf0cfe2239277
|
||||
PATCH_COMMAND git apply ${OpenCV_DIRECTORY_FLAG} --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-vs.patch ${CMAKE_CURRENT_LIST_DIR}/0002-clang19-macos.patch
|
||||
DEPENDS ${JPEG_PKG}
|
||||
CMAKE_ARGS
|
||||
-DBUILD_SHARED_LIBS=0
|
||||
-DBUILD_PERE_TESTS=OFF
|
||||
@@ -34,7 +35,8 @@ orcaslicer_add_cmake_project(OpenCV
|
||||
-DBUILD_EXAMPLES=OFF
|
||||
-DBUILD_JASPER=OFF
|
||||
-DBUILD_JAVA=OFF
|
||||
-DBUILD_JPEG=ON
|
||||
# Share Orca's libjpeg to avoid conflicting codec symbols in the application.
|
||||
-DBUILD_JPEG=OFF
|
||||
-DBUILD_APPS_LIST=version
|
||||
-DBUILD_opencv_apps=OFF
|
||||
-DBUILD_opencv_java=OFF
|
||||
@@ -67,7 +69,8 @@ orcaslicer_add_cmake_project(OpenCV
|
||||
-DWITH_OPENJPEG=OFF
|
||||
-DWITH_QUIRC=OFF
|
||||
-DWITH_VTK=OFF
|
||||
-DWITH_JPEG=OFF
|
||||
# Textured GLB/OBJ imports decode embedded and external JPEGs with cv::imdecode.
|
||||
-DWITH_JPEG=ON
|
||||
-DWITH_WEBP=OFF
|
||||
-DWITH_TIFF=OFF
|
||||
-DBUILD_TIFF=OFF
|
||||
|
||||
Vendored
+17
-7
@@ -16,6 +16,8 @@ else()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(_openssl_dir ${DESTDIR})
|
||||
|
||||
if(WIN32)
|
||||
set(_openssl_msvc_env CC=cl CXX=cl RC=rc CL=/FS)
|
||||
# OpenSSL's perl Configure honors the CC environment variable, but the
|
||||
@@ -43,6 +45,14 @@ if(WIN32)
|
||||
else()
|
||||
if(APPLE)
|
||||
set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})
|
||||
# OPENSSLDIR is only where OpenSSL looks at runtime for cert.pem, certs/
|
||||
# and openssl.cnf; install_sw installs nothing there. Point it at the
|
||||
# system CA bundle, the one libcurl already uses here, so the bundled
|
||||
# Python's ssl module can verify HTTPS. Elsewhere it stays the prefix:
|
||||
# Windows Python reads the system certificate store, and on Linux, where
|
||||
# the bundle's location differs by distro, CurlGlobalInit points
|
||||
# SSL_CERT_FILE at it at startup.
|
||||
set(_openssl_dir /etc/ssl)
|
||||
else()
|
||||
# A static library that is embedded into a shared object must not export
|
||||
# its symbols. On Linux the running process also loads the system OpenSSL
|
||||
@@ -81,7 +91,7 @@ ExternalProject_Add(dep_OpenSSL
|
||||
# URL_HASH SHA256=8c776993154652d0bb393f506d850b811517c8bd8d24b1008aef57fbe55d3f31
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/OpenSSL
|
||||
CONFIGURE_COMMAND ${_conf_cmd} ${_cross_arch}
|
||||
"--openssldir=${DESTDIR}"
|
||||
"--openssldir=${_openssl_dir}"
|
||||
"--prefix=${DESTDIR}"
|
||||
# OpenSSL's linux-x86_64 target sets multilib=64, so it installs to
|
||||
# <prefix>/lib64 while every other dep uses <prefix>/lib. CPython's
|
||||
@@ -115,12 +125,12 @@ ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
|
||||
)
|
||||
|
||||
if (NOT WIN32 AND NOT APPLE)
|
||||
# OpenSSL's object rules do not depend on CFLAGS, so reconfiguring it (for
|
||||
# example to add -fvisibility=hidden) relinks the archives from stale
|
||||
# objects instead of recompiling them, and the change silently has no
|
||||
# effect. Drop the objects whenever this recipe changes so the next build
|
||||
# actually recompiles them.
|
||||
if (NOT WIN32)
|
||||
# OpenSSL's object rules do not depend on the configured flags or paths, so
|
||||
# reconfiguring it (a new compiler flag, a different --openssldir) relinks
|
||||
# the archives from stale objects instead of recompiling them, and the
|
||||
# change silently has no effect. Drop the objects whenever this recipe
|
||||
# changes so the next build actually recompiles them.
|
||||
ExternalProject_Get_Property(dep_OpenSSL SOURCE_DIR)
|
||||
ExternalProject_Add_Step(dep_OpenSSL clean_objects
|
||||
DEPENDEES configure
|
||||
|
||||
Vendored
+5
-4
@@ -365,10 +365,11 @@ if(TARGET dep_ZLIB)
|
||||
add_dependencies(dep_python3 dep_ZLIB)
|
||||
endif()
|
||||
|
||||
if (NOT WIN32 AND NOT APPLE)
|
||||
# CPython's Makefile rules for _ssl and _hashlib depend only on their own
|
||||
# sources, not on the OpenSSL archives, so a rebuilt OpenSSL does not make
|
||||
# them relink and they keep the previous symbols. On an incremental tree,
|
||||
if (NOT WIN32)
|
||||
# _ssl and _hashlib embed the static OpenSSL built in deps/ (Windows uses
|
||||
# CPython's prebuilt OpenSSL instead). Their Makefile rules depend only on
|
||||
# their own sources, not on the OpenSSL archives, so a rebuilt OpenSSL does
|
||||
# not make them relink and they keep the previous one. On an incremental tree,
|
||||
# drop the built modules and relink them against the current OpenSSL; a
|
||||
# fresh build is left alone (its PGO target builds them). "make" alone is a
|
||||
# no-op once PGO has run, so sharedmods is invoked explicitly.
|
||||
|
||||
Vendored
+9
-1
@@ -21,6 +21,14 @@ else ()
|
||||
set(_wx_edge "-DwxUSE_WEBVIEW_EDGE=OFF")
|
||||
endif ()
|
||||
|
||||
# With a precompiled header, clang-cl can omit an exception type's copy constructor from its throw info,
|
||||
# so copies of the exception share and double-free their data: https://github.com/llvm/llvm-project/issues/53486
|
||||
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
|
||||
set(_wx_precomp OFF)
|
||||
else ()
|
||||
set(_wx_precomp ON)
|
||||
endif ()
|
||||
|
||||
set(_wx_patch_command "")
|
||||
if (APPLE)
|
||||
set(_wx_patch_command
|
||||
@@ -39,7 +47,7 @@ orcaslicer_add_cmake_project(
|
||||
PATCH_COMMAND ${_wx_patch_command}
|
||||
DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG}
|
||||
CMAKE_ARGS
|
||||
-DwxBUILD_PRECOMP=ON
|
||||
-DwxBUILD_PRECOMP=${_wx_precomp}
|
||||
${_wx_toolkit}
|
||||
"-DCMAKE_DEBUG_POSTFIX:STRING=${_wx_debug_postfix}"
|
||||
-DwxBUILD_DEBUG_LEVEL=0
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Adaptive TPMS infill — High Level Design
|
||||
# Adaptive infill — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
`tpms_adaptive` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
|
||||
`adaptive_infill_mode` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
|
||||
patterns inside the object: the cells grow continuously from the surface
|
||||
towards the center of the object. `distance_warp`, `smooth_blend` and
|
||||
`stepped_shells` follow the distance to the nearest surface, including the top
|
||||
@@ -10,8 +10,8 @@ and bottom, like concentric shells; `lobes` follows the whole 3D shape towards
|
||||
the center of each lobe of the object; `normal_z`, `normal_y` and `normal_x`
|
||||
follow each section of the object normal to that axis, so the grading does not
|
||||
change along the axis, as suits a profile extruded along it.
|
||||
`sparse_infill_density` is the density at the surface, `tpms_interior_density`
|
||||
the density at the center, and `tpms_adaptive_gradient` picks how the density
|
||||
`sparse_infill_density` is the density at the surface, `adaptive_infill_interior_density`
|
||||
the density at the center, and `adaptive_infill_gradient` picks how the density
|
||||
goes from one to the other. Only internal sparse infill is graded; the Gyroid
|
||||
Z-buckling optimization does not apply to it.
|
||||
|
||||
@@ -22,9 +22,9 @@ split into shells or blended between densities.
|
||||
|
||||
## Radial field
|
||||
|
||||
`TpmsRadialField` gives every point of an object the center of its lobe and a
|
||||
`AdaptiveInfillField` gives every point of an object the center of its lobe and a
|
||||
radial coordinate: 0 at the center, 1 at the surface along the ray from the
|
||||
center. `PrintObject::prepare_tpms_radial_fields()` builds it in
|
||||
center. `PrintObject::prepare_adaptive_infill_fields()` builds it in
|
||||
`bridge_over_infill()`, next to the adaptive cubic octree, because the anchoring
|
||||
infill generated there has to match the printed infill. A field is built for
|
||||
every mode a region uses, and is shared by the regions using that mode: the
|
||||
@@ -202,7 +202,7 @@ built for them.
|
||||
|
||||
## Constraints
|
||||
|
||||
- With `tpms_adaptive` disabled, or for other patterns, the fill parameters are reset
|
||||
- With `adaptive_infill_mode` disabled, or for other patterns, the fill parameters are reset
|
||||
to their defaults, so they neither change the infill nor split fill batches.
|
||||
- `Layer::get_sparse_infill_max_void_area()` uses the sparser of the two
|
||||
densities, as the voids at the center are that large.
|
||||
@@ -350,8 +350,11 @@ identity default and translates nothing. `NetworkAgent` forwards both to the liv
|
||||
comparison sites below reach them through `wxGetApp().getAgent()` and leave an id untranslated
|
||||
while no agent is live. Whole documents are Bambu's business alone:
|
||||
`BBLPrinterAgent::to_orca_payload` and `from_orca_payload` rewrite every string under
|
||||
`tray_info_idx`, `filament_id` or `filamentId` at any depth; text that does not parse, or carries
|
||||
none of those keys, comes back unchanged. The map is loaded once, lazily; a missing or malformed
|
||||
`tray_info_idx`, `filament_id`, `filamentId`, `cate` or `fila_id` at any depth; text that does not
|
||||
parse, or carries none of those keys, comes back unchanged. The last two are the H2C nozzle rack's:
|
||||
`cate` in the auto nozzle mapping request, `fila_id` in the rack status. A new Bambu message that
|
||||
carries a filament id under another key needs that key added to `filament_id_keys`, or the id
|
||||
crosses the boundary untranslated. The map is loaded once, lazily; a missing or malformed
|
||||
file degrades to identity with a log line rather than failing.
|
||||
|
||||
| Boundary | Where it translates |
|
||||
|
||||
@@ -53,6 +53,13 @@ Space counts as typing. The speed dial's default is the one bare Space, and
|
||||
`MainFrame` leaves it to a focused control that uses Space itself (text fields, buttons,
|
||||
combo boxes), so it opens the dial from the canvases and the tab strip only.
|
||||
|
||||
`MainFrame::handle_global_shortcut` skips a chord that `KeyChord::edits_text()` reports
|
||||
while a text field has the focus (a wx text entry, spin control, web view or ImGui
|
||||
InputText), and skips every chord while an ImGui modal is open, as a wx modal dialog
|
||||
would. `ImGuiWrapper::owns_key()` reads the active InputText and the popup stack
|
||||
directly, since `WantCaptureKeyboard` lags a frame, and is called only when the focus is on
|
||||
a visible canvas from `OpenGLManager::create_wxglcanvas()`.
|
||||
|
||||
## Which event a chord matches
|
||||
|
||||
Letters, digits and special keys match on `wxEVT_KEY_DOWN`. Its key codes do not depend
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
# Travel optimization — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
Inside an island, the infill paths of each region are chained into one print
|
||||
order by a greedy pass that joins the closest free path ends first. That pass
|
||||
leaves its longest joins for last, and they often cross the whole island. Two
|
||||
things shorten the travel between infill paths:
|
||||
|
||||
- the fill generators join their lines into as few paths as they can, since
|
||||
every separate path needs a travel to it;
|
||||
- the G-code export refines the greedy chain with a local search that shortens
|
||||
the straight-line travel between the paths.
|
||||
|
||||
The refinement covers every chain that `chain_and_reorder_extrusion_entities()`
|
||||
builds from a start point: the infill and ironing of `GCode::extrude_infill()`
|
||||
and the support of `GCode::extrude_support()`, as well as the order of objects
|
||||
and islands under `print_order` Default. It changes only the order and the
|
||||
direction of the paths, not the paths or the walls. Neither part has a setting.
|
||||
|
||||
## The chain
|
||||
|
||||
`ObjectByExtruder::Island::Region::append()` flattens every sortable fill
|
||||
collection into its paths, so the paths of all the fills of a region in one
|
||||
island are chained together. A collection that must keep its order (`no_sort`:
|
||||
monotonic, concentric, a forced fill order, the flow calibration) stays a single
|
||||
entry that cannot be reversed. Paths that must keep their direction cannot be
|
||||
reversed either: Grid's, since it alternates their direction between layers for
|
||||
strength, and those of a fill with a forced order. A loop starts and ends at the
|
||||
same point, so reversing it changes nothing.
|
||||
|
||||
`extrude_infill()` chains each region from the current nozzle position with
|
||||
`chain_and_reorder_extrusion_entities()`, and chains the paths inside a nested
|
||||
sortable collection the same way; `extrude_support()` chains the support paths
|
||||
of a layer so too, unless they keep their order. The overload without a start
|
||||
point, used to chain collections while slicing, keeps the greedy chain. The
|
||||
order the fill generators give their paths does not reach the G-code for a
|
||||
sortable fill, which is chained again.
|
||||
|
||||
## Connecting the lines
|
||||
|
||||
Most patterns cut their lines at the infill boundary, and `Fill::connect_infill()`
|
||||
joins pairs of line ends that are neighbors on a boundary contour by the arc of
|
||||
wall between them, shortest arcs first, as long as neither end is joined yet and
|
||||
the join closes no loop. An arc is joined when it is shorter than
|
||||
`infill_anchor_max`, or than two anchors and a line width if that is longer:
|
||||
two ends further apart each get an anchor toward the other, and where those
|
||||
would meet, the join prints about as much wall as one path instead of two.
|
||||
|
||||
Regular patterns leave many arcs of the same length, and the order among them
|
||||
decides the pairing. In an arbitrary order the pairs along one wall can fall out
|
||||
of step, such as lines 1–2 and 3–4, then 6–7 and 8–9, which leaves the end of
|
||||
line 5 loose, and every loose end starts or ends a separate path. So the arcs
|
||||
are also sorted in steps of half the line spacing and, within a step, along each
|
||||
contour forwards and backwards, which pairs the ends of a wall in one phase.
|
||||
Each order is replayed on the flags and a union-find of the lines, without
|
||||
building any path, and the one that joins the most lines is used; a tie keeps
|
||||
the shortest-first order.
|
||||
|
||||
The replay includes the last pass, which joins any two open ends that are
|
||||
neighbors on a contour. With multiline infill the main pass leaves the copies of
|
||||
one line to that pass, so that each copy reaches a wall of its own. Two
|
||||
neighboring groups cut at both walls can then be joined at either wall by arcs
|
||||
of the same length: at one, their open ends stay next to their own copies, which
|
||||
the last pass joins, so each line prints out and back; at the other, the copies
|
||||
end up in separate paths printed apart.
|
||||
|
||||
A line end left unpaired gets an anchor, a piece of the wall at most
|
||||
`infill_anchor` long. It runs toward the nearest open end of another path,
|
||||
looking up to 64 ends along the contour, since the next path usually starts
|
||||
there; without one it takes the side with more free wall. With multiline infill
|
||||
an anchor toward a copy of the same line stops short of the copy rather than
|
||||
printing over it. A copy is a neighboring end less than the group width
|
||||
(`multiline` spacings) across the line, at any wall angle, while the next group
|
||||
lies a whole line spacing away. Meeting the wall at an angle θ, the copy covers
|
||||
half a width over sin θ of the wall on each side of its end, so the anchor stops
|
||||
that far and half its own width before it, or is left out; the end still counts
|
||||
as anchored, so the last pass pairs the other ends as before.
|
||||
|
||||
Last, `Fill::fill_surface_extrusion()` joins the sparse infill lines whose ends
|
||||
are at most two line spacings apart with a straight piece. With the anchors off
|
||||
(`infill_anchor_max` 0) nothing above runs and this is the only joining; with
|
||||
them on, it takes what the steps above leave next to each other, such as two
|
||||
anchors that stop just short of each other or a short piece cut off by a corner.
|
||||
A gap that short leaves no room to print anything in between, and as a travel it
|
||||
is the one hop a reordering can trade for a longer travel that retracts.
|
||||
Multiline copies leave the most such ends: one spacing apart across the line,
|
||||
their ends where it meets a wall at an angle θ are one spacing over sin θ apart
|
||||
along it, so two spacings reach walls met at 30° and steeper.
|
||||
|
||||
Ends that meet are joined first, then the gaps between the leading ends of their
|
||||
lines, along each line's overall direction, then the rest, shortest first within
|
||||
each, without closing a loop or crossing a line or another join. Taking the
|
||||
leading ends first joins every pair of copies at the same side, which leaves
|
||||
their open ends next to each other; by length alone, neighboring pairs were
|
||||
joined at different walls and the travel between them crossed the island. Lines
|
||||
that cannot reverse, such as Grid's, only join the end of one to the start of the
|
||||
next, and fills that keep their order are left alone.
|
||||
|
||||
## The search
|
||||
|
||||
A chain of up to eight paths is found exactly, by dynamic programming over the
|
||||
subsets of paths: a state is the paths visited so far, the last one and its
|
||||
direction, about four thousand states for eight paths. The shared greedy
|
||||
chaining, `chain_segments_greedy_constrained_reversals_()`, does the same for two
|
||||
to four segments, which also orders up to four islands of a layer and the loops
|
||||
of a perimeter collection exactly. Either way the first path is no longer
|
||||
always the one nearest the start; with a free end, a path near the start can be
|
||||
worth leaving for last.
|
||||
|
||||
Objects (`print_order` Default) and the tour of islands across objects are
|
||||
visited in the same order on every layer, so `chain_points_with_postprocessing()`
|
||||
orders them as a closed tour rather than an open chain. Up to ten points the
|
||||
shortest tour is found exactly by the same dynamic programming with the first
|
||||
point fixed; with a start point the tour then begins at the point nearest it.
|
||||
Larger sets take the greedy chain and alternate 2-opt, or-opt and crossing
|
||||
removal; or-opt moves runs of up to three points next to one of the eight points
|
||||
nearest an end of the run, found on the same grid as the chain's candidates.
|
||||
|
||||
For longer chains, the search refines the greedy chain. On an island cut into many short pieces,
|
||||
such as infill without anchors, that chain can sweep away from the nozzle and
|
||||
come back for the rest with one long jump, which no local move below removes.
|
||||
So a nearest-next chain, going from the nozzle to the nearest free path end each
|
||||
time, is built too; when it is already shorter than the refined greedy chain, it
|
||||
is refined and used instead. Elsewhere it costs one grid query per path.
|
||||
|
||||
`improve_chain_by_local_search()` sees the chain as a sequence of segments with
|
||||
an entry and an exit point each. Its cost is the straight distance from the
|
||||
nozzle to the first entry plus the distance from every exit to the next entry;
|
||||
the end of the chain is free. It applies three kinds of moves:
|
||||
|
||||
- 2-opt reverses a run of segments, which flips each of them and replaces the
|
||||
two links at the ends of the run.
|
||||
- Or-opt moves a run of up to three segments between two others, flipped or
|
||||
not.
|
||||
- A run of any length that starts or ends next to the nozzle moves to the front.
|
||||
Local search can settle on a chain that starts with a block of paths far from
|
||||
the nozzle while the paths next to it sit in the middle, and moving that block
|
||||
takes more than three segments at once.
|
||||
|
||||
A move that would flip a segment that cannot be reversed is skipped, so such
|
||||
segments are only reordered.
|
||||
|
||||
A move is tried only if every link it creates joins an end point to one of the
|
||||
32 end points nearest to it, which covers every move on a chain of up to 17
|
||||
segments, as most islands are; with 8 candidates the search missed most of the
|
||||
gain on grid-like patterns such as Triangles. The candidates come from a uniform
|
||||
grid of about two end points per cell, searched in rings of cells around the
|
||||
query. `KDTreeIndirect` visits its subtrees in a fixed order rather than the
|
||||
nearer one first, so a query for 32 points walks most of the tree, and on long
|
||||
chains finding the candidates would cost more than the search itself.
|
||||
|
||||
The candidates of an end point are tried nearest first, and only while they are
|
||||
closer to it than the link the move would replace there. A 2-opt move that pays
|
||||
off creates a link shorter than the one it replaces at one of its two ends, so
|
||||
this misses no 2-opt move. An or-opt move is tried only while its new link is
|
||||
shorter than what taking the run out saves, which skips a few or-opt moves that
|
||||
would still pay off but cuts the search on long chains to a fraction.
|
||||
|
||||
The search works through a queue of segments and applies the first move that
|
||||
shortens the chain around the segment it examines. After a move, only the
|
||||
segments next to a changed link are queued again. A new link can also make room
|
||||
for a run elsewhere that no segment next to it would try, so when the queue
|
||||
empties after any move, every segment is queued once more, and the search ends
|
||||
when such a sweep finds no move.
|
||||
|
||||
Every move shortens the chain by more than `SCALED_EPSILON`, so the result is
|
||||
never longer than the greedy or the nearest-next chain and the search ends; a
|
||||
cap of 50 moves per segment only bounds its time. The search has no randomness
|
||||
and visits the segments in a fixed order, so the same input always gives the
|
||||
same G-code.
|
||||
|
||||
## Cost
|
||||
|
||||
Finding the candidates of an end point visits a few cells, and the nearest-next
|
||||
chain makes one such query per path, which only grows where the paths around the
|
||||
nozzle are used up and the chain jumps. Examining a segment tries at most a few
|
||||
hundred candidate links, usually only the nearest few, plus a scan of the chain
|
||||
for the move to the front when it is the first segment, and a move rewrites only
|
||||
the range of the chain it changes, so the search stays close to linear in the
|
||||
number of segments. Most islands hold a few to a few dozen paths; the cost shows
|
||||
on infill cut into thousands of pieces per island, such as dense infill with the
|
||||
anchors off. The straight joins only look at the line segments near a candidate
|
||||
join for crossings.
|
||||
|
||||
## Limitations
|
||||
|
||||
The search minimizes straight-line distance. It does not know which travels
|
||||
leave the infill area and retract under `reduce_infill_retraction`, nor the
|
||||
detours of `reduce_crossing_wall`, and a travel's time is not proportional to
|
||||
its length: a short travel is bound by acceleration, so its time grows about
|
||||
with the square root of its length. A chain that is shorter in total but trades
|
||||
very short travels for longer ones, or for ones past `retraction_minimum_travel`,
|
||||
can occasionally take a little longer to print.
|
||||
|
||||
The end of the chain is free: it is not steered toward the next island or the
|
||||
start of the next layer. The regions of an island are chained one after the
|
||||
other, in region order, each from where the previous one ended.
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-15 17:40+0800\n"
|
||||
"POT-Creation-Date: 2026-10-19 09:48-0300\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -1637,16 +1637,16 @@ msgstr ""
|
||||
msgid "Stroke of shape (%1%) contains unsupported: %2%."
|
||||
msgstr ""
|
||||
|
||||
msgid "Not valid state please report reproduction steps on github"
|
||||
msgid "Not valid state please report reproduction steps on GitHub"
|
||||
msgstr ""
|
||||
|
||||
msgid "No embossed file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing svg file in embossed shape"
|
||||
msgid "Missing SVG file in embossed shape"
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing data of svg file"
|
||||
msgid "Missing data of SVG file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Face the camera"
|
||||
@@ -4579,7 +4579,7 @@ msgstr ""
|
||||
msgid "Unknown C++ exception."
|
||||
msgstr ""
|
||||
|
||||
msgid "Processing G-Code from previous file…"
|
||||
msgid "Processing G-code from previous file…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Slicing complete"
|
||||
@@ -6049,7 +6049,7 @@ msgstr ""
|
||||
msgid "Size:"
|
||||
msgstr ""
|
||||
|
||||
#, possible-boost-format
|
||||
#, possible-c-format, possible-boost-format
|
||||
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
|
||||
msgstr ""
|
||||
|
||||
@@ -10657,6 +10657,9 @@ msgstr ""
|
||||
msgid "Send Failed"
|
||||
msgstr ""
|
||||
|
||||
msgid "No available printers"
|
||||
msgstr ""
|
||||
|
||||
msgid "The name length exceeds the limit."
|
||||
msgstr ""
|
||||
|
||||
@@ -12315,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"
|
||||
@@ -12412,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 ""
|
||||
|
||||
@@ -15645,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 ""
|
||||
|
||||
@@ -20540,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
|
||||
@@ -26492,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 ""
|
||||
|
||||
@@ -26877,7 +26884,7 @@ msgstr ""
|
||||
msgid "Medium violet red"
|
||||
msgstr ""
|
||||
|
||||
msgid "PaleV violet red"
|
||||
msgid "Pale violet red"
|
||||
msgstr ""
|
||||
|
||||
msgid "Deep pink"
|
||||
@@ -27039,7 +27046,7 @@ msgstr ""
|
||||
msgid "Shallow sky"
|
||||
msgstr ""
|
||||
|
||||
msgid "Electric bel"
|
||||
msgid "Electric blue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dolphin"
|
||||
@@ -27108,7 +27115,7 @@ msgstr ""
|
||||
msgid "Maize yellow"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dalmatian yellow"
|
||||
msgid "Daffodil yellow"
|
||||
msgstr ""
|
||||
|
||||
msgid "Brown beige"
|
||||
@@ -27369,7 +27376,7 @@ msgstr ""
|
||||
msgid "Turquoise blue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Capsanthin violet"
|
||||
msgid "Capri blue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ocean blue"
|
||||
@@ -27483,7 +27490,7 @@ msgstr ""
|
||||
msgid "Silver grey"
|
||||
msgstr ""
|
||||
|
||||
msgid "Olivestone"
|
||||
msgid "Olive grey"
|
||||
msgstr ""
|
||||
|
||||
msgid "Moss grey"
|
||||
@@ -28071,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."
|
||||
@@ -30127,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 ""
|
||||
|
||||
@@ -30242,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 ""
|
||||
@@ -30598,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.
|
||||
@@ -30686,7 +30723,7 @@ msgstr ""
|
||||
msgid "Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%"
|
||||
msgstr ""
|
||||
|
||||
msgid "Post-processor can't find exported gcode file"
|
||||
msgid "Post-processor can't find exported G-code file"
|
||||
msgstr ""
|
||||
|
||||
#, possible-boost-format
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-15 17:40+0800\n"
|
||||
"POT-Creation-Date: 2026-10-19 09:48-0300\n"
|
||||
"PO-Revision-Date: 2026-06-17 15:44-0300\n"
|
||||
"Last-Translator: Alexandre Folle de Menezes\n"
|
||||
"Language-Team: \n"
|
||||
@@ -1633,16 +1633,16 @@ msgstr ""
|
||||
msgid "Stroke of shape (%1%) contains unsupported: %2%."
|
||||
msgstr ""
|
||||
|
||||
msgid "Not valid state please report reproduction steps on github"
|
||||
msgid "Not valid state please report reproduction steps on GitHub"
|
||||
msgstr ""
|
||||
|
||||
msgid "No embossed file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing svg file in embossed shape"
|
||||
msgid "Missing SVG file in embossed shape"
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing data of svg file"
|
||||
msgid "Missing data of SVG file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Face the camera"
|
||||
@@ -4575,7 +4575,7 @@ msgstr ""
|
||||
msgid "Unknown C++ exception."
|
||||
msgstr ""
|
||||
|
||||
msgid "Processing G-Code from previous file…"
|
||||
msgid "Processing G-code from previous file…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Slicing complete"
|
||||
@@ -6045,7 +6045,7 @@ msgstr ""
|
||||
msgid "Size:"
|
||||
msgstr ""
|
||||
|
||||
#, boost-format
|
||||
#, c-format, boost-format
|
||||
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
|
||||
msgstr ""
|
||||
|
||||
@@ -10653,6 +10653,9 @@ msgstr ""
|
||||
msgid "Send Failed"
|
||||
msgstr ""
|
||||
|
||||
msgid "No available printers"
|
||||
msgstr ""
|
||||
|
||||
msgid "The name length exceeds the limit."
|
||||
msgstr ""
|
||||
|
||||
@@ -12311,7 +12314,7 @@ msgstr ""
|
||||
msgid "Left mouse"
|
||||
msgstr ""
|
||||
|
||||
msgid "Available anywhere in the window, even while typing in a text field."
|
||||
msgid "Available anywhere in the window."
|
||||
msgstr ""
|
||||
|
||||
msgid "Run favorite 1 to 10"
|
||||
@@ -12408,9 +12411,6 @@ msgstr ""
|
||||
msgid "The default %s is assigned to %s. Reassign it to %s?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Global shortcuts also apply while typing."
|
||||
msgstr ""
|
||||
|
||||
msgid "A key that types a character cannot be a global shortcut."
|
||||
msgstr ""
|
||||
|
||||
@@ -15641,6 +15641,12 @@ msgstr ""
|
||||
msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
|
||||
msgstr ""
|
||||
|
||||
msgid "Outline tolerance"
|
||||
msgstr ""
|
||||
|
||||
msgid "Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the original outline. Larger values give fewer points, which helps firmware that stalls while parsing G-code with many objects. 0 keeps every point."
|
||||
msgstr ""
|
||||
|
||||
msgid "Verbose G-code"
|
||||
msgstr ""
|
||||
|
||||
@@ -20536,7 +20542,7 @@ msgid "test %s start..."
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
msgid "test %s ip resolved = %s"
|
||||
msgid "test %s IP resolved = %s"
|
||||
msgstr ""
|
||||
|
||||
#, c-format, boost-format
|
||||
@@ -26488,17 +26494,18 @@ msgstr ""
|
||||
msgid "Basic Colors"
|
||||
msgstr ""
|
||||
|
||||
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
msgid "Orca Palette"
|
||||
msgstr ""
|
||||
|
||||
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
#. TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
msgid "RAL Classic"
|
||||
msgstr ""
|
||||
|
||||
#. TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
msgid "Gradients"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "Color picker"
|
||||
msgid "Solid"
|
||||
msgstr ""
|
||||
|
||||
@@ -26873,7 +26880,7 @@ msgstr ""
|
||||
msgid "Medium violet red"
|
||||
msgstr ""
|
||||
|
||||
msgid "PaleV violet red"
|
||||
msgid "Pale violet red"
|
||||
msgstr ""
|
||||
|
||||
msgid "Deep pink"
|
||||
@@ -27035,7 +27042,7 @@ msgstr ""
|
||||
msgid "Shallow sky"
|
||||
msgstr ""
|
||||
|
||||
msgid "Electric bel"
|
||||
msgid "Electric blue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dolphin"
|
||||
@@ -27104,7 +27111,7 @@ msgstr ""
|
||||
msgid "Maize yellow"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dalmatian yellow"
|
||||
msgid "Daffodil yellow"
|
||||
msgstr ""
|
||||
|
||||
msgid "Brown beige"
|
||||
@@ -27365,7 +27372,7 @@ msgstr ""
|
||||
msgid "Turquoise blue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Capsanthin violet"
|
||||
msgid "Capri blue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ocean blue"
|
||||
@@ -27479,7 +27486,7 @@ msgstr ""
|
||||
msgid "Silver grey"
|
||||
msgstr ""
|
||||
|
||||
msgid "Olivestone"
|
||||
msgid "Olive grey"
|
||||
msgstr ""
|
||||
|
||||
msgid "Moss grey"
|
||||
@@ -28067,10 +28074,19 @@ msgstr ""
|
||||
msgid "Built-in"
|
||||
msgstr ""
|
||||
|
||||
msgid "My textures"
|
||||
msgstr ""
|
||||
|
||||
msgid "No textures found."
|
||||
msgstr ""
|
||||
|
||||
msgid "My textures"
|
||||
msgid "Texture libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Explore more texture libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Opens OrcaCloud in the Home page to browse for plugins."
|
||||
msgstr ""
|
||||
|
||||
msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer."
|
||||
@@ -30123,6 +30139,9 @@ msgstr ""
|
||||
msgid "Slicing Pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource Texture"
|
||||
msgstr ""
|
||||
|
||||
msgid "The plugin's configuration UI was empty. Showing the default editor."
|
||||
msgstr ""
|
||||
|
||||
@@ -30238,6 +30257,24 @@ msgstr ""
|
||||
msgid "Plugin directory does not contain a .py or .whl entry file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource texture directory is not set"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource texture directory could not be resolved"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource texture path is not a directory"
|
||||
msgstr ""
|
||||
|
||||
msgid "Plugin folder could not be resolved"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource texture directory is outside the plugin folder"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource texture directory could not be read"
|
||||
msgstr ""
|
||||
|
||||
#, boost-format
|
||||
msgid "Slicing pipeline plugin '%1%' error: %2%"
|
||||
msgstr ""
|
||||
@@ -30594,7 +30631,7 @@ msgstr ""
|
||||
|
||||
#. TRN %1% is the application name.
|
||||
#, boost-format
|
||||
msgid "Not a gcode file generated by %1%."
|
||||
msgid "Not a G-code file generated by %1%."
|
||||
msgstr ""
|
||||
|
||||
#. TRN %1% is a tag in the G-code file that must not be translated.
|
||||
@@ -30682,7 +30719,7 @@ msgstr ""
|
||||
msgid "Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%"
|
||||
msgstr ""
|
||||
|
||||
msgid "Post-processor can't find exported gcode file"
|
||||
msgid "Post-processor can't find exported G-code file"
|
||||
msgstr ""
|
||||
|
||||
#, boost-format
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+1046
-549
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+4103
-2310
File diff suppressed because it is too large.
Load diff
+5352
-3119
File diff suppressed because it is too large.
Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 132 KiB |
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><rect x="7" y="12" width="1" height="1" style="fill:#fefefe;"/><path d="M4.5,5.5c0-1.71,1.43-3.09,3.16-3,1.62.08,2.84,1.54,2.84,3.17h0c0,.61-.25,1.2-.69,1.62l-2.31,2.24v.97" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><rect x="1.5" y="3.5" width="12" height="8" rx="1" ry="1" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><path d="M3,5h1v1h-1Z M5,5h1v1h-1Z M7,5h1v1h-1Z M9,5h1v1h-1Z M11,5h1v1h-1Z M4,7h1v1h-1Z M6,7h1v1h-1Z M8,7h1v1h-1Z M10,7h1v1h-1Z M5,9h5v1h-5Z" style="fill:#fefefe;"/></svg>
|
||||
|
Before Width: | Height: | Size: 366 B After Width: | Height: | Size: 415 B |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Snapmaker",
|
||||
"version": "02.04.00.17",
|
||||
"version": "02.04.00.18",
|
||||
"force_update": "0",
|
||||
"description": "Snapmaker configurations",
|
||||
"machine_model_list": [
|
||||
|
||||
@@ -49,5 +49,8 @@
|
||||
],
|
||||
"pressure_advance": [
|
||||
"0.02"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"100"
|
||||
]
|
||||
}
|
||||
@@ -46,5 +46,8 @@
|
||||
],
|
||||
"fan_min_speed": [
|
||||
"0"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"100"
|
||||
]
|
||||
}
|
||||
@@ -45,7 +45,7 @@
|
||||
"250"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"178"
|
||||
"70"
|
||||
],
|
||||
"fan_max_speed": [
|
||||
"20"
|
||||
|
||||
@@ -4,5 +4,8 @@
|
||||
"inherits": "fdm_filament_petg",
|
||||
"from": "system",
|
||||
"filament_id": "OFW8lqnb",
|
||||
"instantiation": "false"
|
||||
"instantiation": "false",
|
||||
"temperature_vitrification": [
|
||||
"70"
|
||||
]
|
||||
}
|
||||
@@ -60,7 +60,7 @@
|
||||
"250"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"178"
|
||||
"70"
|
||||
],
|
||||
"fan_max_speed": [
|
||||
"20"
|
||||
|
||||
@@ -34,5 +34,8 @@
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"220"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"45"
|
||||
]
|
||||
}
|
||||
@@ -34,5 +34,8 @@
|
||||
],
|
||||
"filament_cooling_final_speed": [
|
||||
"60"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"45"
|
||||
]
|
||||
}
|
||||
@@ -34,5 +34,8 @@
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"220"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"45"
|
||||
]
|
||||
}
|
||||
@@ -40,5 +40,8 @@
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"220"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"45"
|
||||
]
|
||||
}
|
||||
@@ -31,5 +31,8 @@
|
||||
],
|
||||
"slow_down_layer_time": [
|
||||
"8"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"45"
|
||||
]
|
||||
}
|
||||
@@ -50,5 +50,8 @@
|
||||
"additional_cooling_fan_speed": [
|
||||
"70",
|
||||
"70"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"30"
|
||||
]
|
||||
}
|
||||
@@ -137,7 +137,7 @@
|
||||
"240"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"60"
|
||||
"30"
|
||||
],
|
||||
"nozzle_temperature_range_low": [
|
||||
"200"
|
||||
|
||||
@@ -302,7 +302,7 @@
|
||||
"-1"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"60"
|
||||
"30"
|
||||
],
|
||||
"textured_cool_plate_temp": [
|
||||
"40"
|
||||
|
||||
@@ -29,5 +29,8 @@
|
||||
"additional_cooling_fan_speed": [
|
||||
"70",
|
||||
"70"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"30"
|
||||
]
|
||||
}
|
||||
@@ -49,5 +49,8 @@
|
||||
"additional_cooling_fan_speed": [
|
||||
"70",
|
||||
"70"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"30"
|
||||
]
|
||||
}
|
||||
@@ -97,7 +97,7 @@
|
||||
"20",
|
||||
"12"
|
||||
],
|
||||
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0",
|
||||
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\n;===== top cover purifier =====\n{if min_vitrification_temperature <= 50}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=48 FAN_SPEED=0.6 DELAY_OFF=0 ; strong cooling\n{elsif min_vitrification_temperature <= 70}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=45 ALARM_TEMP=51 FAN_SPEED=0.6 DELAY_OFF=0 ; weak cooling\n{else}\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600 ; keep warm\n{endif}\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\n\n;==== 预挤出耗材 =====\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nT{initial_extruder}\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nSM_PRINT_PREEXTRUDE_FILAMENT INDEX={initial_extruder}\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nM107\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nM107\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nM106 S255\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\nM107\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\nM107\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; AlwLine truncated
|
||||
"machine_tool_change_time": "5",
|
||||
"max_layer_height": [
|
||||
"0.14",
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
"20",
|
||||
"12"
|
||||
],
|
||||
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0",
|
||||
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\n;===== top cover purifier =====\n{if min_vitrification_temperature <= 50}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=48 FAN_SPEED=0.6 DELAY_OFF=0 ; strong cooling\n{elsif min_vitrification_temperature <= 70}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=45 ALARM_TEMP=51 FAN_SPEED=0.6 DELAY_OFF=0 ; weak cooling\n{else}\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600 ; keep warm\n{endif}\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\n\n;==== 预挤出耗材 =====\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nT{initial_extruder}\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nSM_PRINT_PREEXTRUDE_FILAMENT INDEX={initial_extruder}\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nM107\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nM107\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nM106 S255\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\nM107\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\nM107\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; AlwLine truncated
|
||||
"machine_tool_change_time": "5",
|
||||
"max_layer_height": [
|
||||
"0.32",
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
"20",
|
||||
"12"
|
||||
],
|
||||
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0",
|
||||
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\n;===== top cover purifier =====\n{if min_vitrification_temperature <= 50}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=48 FAN_SPEED=0.6 DELAY_OFF=0 ; strong cooling\n{elsif min_vitrification_temperature <= 70}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=45 ALARM_TEMP=51 FAN_SPEED=0.6 DELAY_OFF=0 ; weak cooling\n{else}\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600 ; keep warm\n{endif}\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\n\n;==== 预挤出耗材 =====\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nT{initial_extruder}\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nSM_PRINT_PREEXTRUDE_FILAMENT INDEX={initial_extruder}\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nM107\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nM107\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nM106 S255\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\nM107\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\nM107\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; AlwLine truncated
|
||||
"machine_tool_change_time": "5",
|
||||
"max_layer_height": [
|
||||
"0.42",
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
"20",
|
||||
"12"
|
||||
],
|
||||
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0",
|
||||
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\n;===== top cover purifier =====\n{if min_vitrification_temperature <= 50}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=48 FAN_SPEED=0.6 DELAY_OFF=0 ; strong cooling\n{elsif min_vitrification_temperature <= 70}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=45 ALARM_TEMP=51 FAN_SPEED=0.6 DELAY_OFF=0 ; weak cooling\n{else}\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600 ; keep warm\n{endif}\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\n\n;==== 预挤出耗材 =====\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nT{initial_extruder}\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nSM_PRINT_PREEXTRUDE_FILAMENT INDEX={initial_extruder}\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nM107\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nM107\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nM106 S255\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\nM107\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\nM107\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; AlwLine truncated
|
||||
"machine_tool_change_time": "5",
|
||||
"max_layer_height": [
|
||||
"0.56",
|
||||
|
||||
@@ -36,11 +36,6 @@ uniform int palette_b[64];
|
||||
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
|
||||
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
|
||||
uniform bool has_color_tex;
|
||||
// False means the texture is a single placement, not a repeat. The sampler's wrap mode can express
|
||||
// that for the *height* - a zero border reads as no displacement - but not for colour, because a
|
||||
// zero border is indistinguishable from the image genuinely being black there. So the uv is tested
|
||||
// instead, which is what sample_layer_color() does on the CPU side.
|
||||
uniform bool tile_enabled;
|
||||
uniform bool volume_mirrored;
|
||||
|
||||
uniform mat4 view_model_matrix;
|
||||
@@ -50,14 +45,6 @@ uniform sampler2D height_tex;
|
||||
uniform vec2 height_tex_texel;
|
||||
uniform float depth_mm;
|
||||
uniform float tiling_scale;
|
||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - the square tile
|
||||
// every layer had before the two axes could differ - which is also what layer_tile_mm() does on the
|
||||
// CPU side, so an unset uniform behaves exactly as this shader always did.
|
||||
uniform float tiling_scale_v;
|
||||
// Mirror the texture along one axis of the tile. Both default to false, which GLSL guarantees for an
|
||||
// uninitialised bool uniform, so a shader whose caller has not set them behaves exactly as before.
|
||||
uniform bool mirror_u;
|
||||
uniform bool mirror_v;
|
||||
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
|
||||
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
|
||||
uniform float tex_aspect;
|
||||
@@ -67,12 +54,6 @@ uniform bool invert;
|
||||
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
|
||||
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
|
||||
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
|
||||
// The placement's rotation and its inverse, so this shader turns a world position into the texture
|
||||
// frame exactly as texture_displacement_bake_frame() does. Both are passed rather than one
|
||||
// transposed here, because GLSL 110 has no transpose(). Identity leaves the old world-aligned
|
||||
// behaviour, which is what an unset uniform gives.
|
||||
uniform mat3 tex_rot; // world -> texture frame
|
||||
uniform mat3 tex_rot_inv; // texture frame -> world
|
||||
uniform bool use_vertex_uv;
|
||||
// 2x3 affine (lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the dragged island's uv; see the
|
||||
// 140 variant. Identity when nothing is dragged.
|
||||
@@ -181,29 +162,13 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
||||
}
|
||||
}
|
||||
|
||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
||||
vec2 tile_mm()
|
||||
{
|
||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
||||
}
|
||||
|
||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
||||
vec2 mirror_sign()
|
||||
{
|
||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
{
|
||||
vec2 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
||||
// the same division, in the tile's own frame.
|
||||
r *= mirror_sign() / tile_mm();
|
||||
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
|
||||
r.y *= tex_aspect;
|
||||
return r + uv_offset;
|
||||
@@ -272,12 +237,9 @@ void main()
|
||||
|
||||
// World millimetres throughout, like the bake - see the 140 variant.
|
||||
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
|
||||
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); // the frame the texture is projected in, as the bake does
|
||||
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
|
||||
if (volume_mirrored)
|
||||
triangle_normal = -triangle_normal;
|
||||
// The same normal in that frame. triangle_normal itself stays in world, because the lighting at
|
||||
// the end of this shader needs it there.
|
||||
vec3 tex_normal = tex_rot * triangle_normal;
|
||||
|
||||
// Where the colour is read from. Both branches below already compute the uv this fragment's
|
||||
// *height* came from - including the parallax-marched one on the triplanar path - and the colour
|
||||
@@ -308,14 +270,14 @@ void main()
|
||||
} else if (weight > 0.0) {
|
||||
vec3 t, b;
|
||||
vec2 units_per_mm;
|
||||
projection_axes(tex_pos, tex_normal, t, b, units_per_mm);
|
||||
projection_axes(tex_pos, triangle_normal, t, b, units_per_mm);
|
||||
|
||||
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
|
||||
// first point where it drops below the displaced surface (see header).
|
||||
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
vec3 view_dir = normalize(tex_rot * eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, tex_normal);
|
||||
vec2 uv = project_uv(tex_pos, tex_normal);
|
||||
vec3 view_dir = normalize(eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, triangle_normal);
|
||||
vec2 uv = project_uv(tex_pos, triangle_normal);
|
||||
|
||||
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
|
||||
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
|
||||
@@ -327,18 +289,18 @@ void main()
|
||||
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
|
||||
// a texel there is no parallax to find and the march would be pure cost - which is the common
|
||||
// case of looking straight down at a surface.
|
||||
float sweep = length(view_dir - tex_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
|
||||
if (v_dot_n > 0.05 && sweep > 0.5 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) {
|
||||
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
|
||||
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
|
||||
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
|
||||
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
|
||||
// by construction, and step inward; the crossing is what this pixel actually sees.
|
||||
float s = h_hi / v_dot_n;
|
||||
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
|
||||
vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_normal);
|
||||
vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
|
||||
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
|
||||
for (int i = 0; i < PARALLAX_STEPS; ++i) {
|
||||
s -= ds;
|
||||
vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_normal);
|
||||
vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
|
||||
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
|
||||
if (gap <= 0.0) {
|
||||
// Crossed between the last two samples - interpolating the hit is what stops it
|
||||
@@ -360,23 +322,18 @@ void main()
|
||||
float hU = texture2D(height_tex, uv + vec2(0.0, height_tex_texel.y)).r;
|
||||
|
||||
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
|
||||
vec2 tile = tile_mm();
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
|
||||
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
// One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
|
||||
// the uv-space gradient carries its own factor before being rotated back into t/b.
|
||||
// Each component carries its own extent and its own mirror sign, both of which project_uv()
|
||||
// applied on the way in; without the sign a mirrored layer would light as though it were not.
|
||||
vec2 ms = mirror_sign();
|
||||
vec2 g = vec2(dh_duv.x * ms.x / tile.x, dh_duv.y * tex_aspect * ms.y / tile.y);
|
||||
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
|
||||
// component of the gradient carries the extra factor before being rotated back into t/b.
|
||||
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
|
||||
// ...and back out of raw-projection units into millimetres along t / b; see the 140 variant.
|
||||
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm;
|
||||
|
||||
// t and b came out of the texture frame, so the gradient is in it too and has to come back
|
||||
// to world before it perturbs the normal the lighting uses.
|
||||
vec3 gradient = tex_rot_inv * (slope.x * t + slope.y * b);
|
||||
vec3 gradient = slope.x * t + slope.y * b;
|
||||
gradient -= triangle_normal * dot(triangle_normal, gradient);
|
||||
triangle_normal = normalize(triangle_normal - gradient);
|
||||
}
|
||||
@@ -396,9 +353,7 @@ void main()
|
||||
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
|
||||
// the same material under the same light, and the relief this preview exists to show is unaffected.
|
||||
vec3 albedo = uniform_color.rgb;
|
||||
bool uv_covered = tile_enabled ||
|
||||
(all(greaterThanEqual(color_uv, vec2(0.0))) && all(lessThan(color_uv, vec2(1.0))));
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0 && uv_covered)
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
|
||||
albedo = palette_rgb[nearest_palette_entry(texture2D(color_tex, color_uv).rgb)];
|
||||
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
|
||||
}
|
||||
@@ -16,16 +16,7 @@ uniform bool volume_mirrored;
|
||||
uniform int mode;
|
||||
uniform float checker_freq;
|
||||
uniform float tiling_scale;
|
||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - see
|
||||
// layer_tile_mm() on the CPU side, so an unset uniform behaves exactly as this shader always did.
|
||||
uniform float tiling_scale_v;
|
||||
// Mirror along one tile axis; false by default, so an unset uniform is the old behaviour.
|
||||
uniform bool mirror_u;
|
||||
uniform bool mirror_v;
|
||||
uniform vec3 tex_anchor; // the volume's origin in world space
|
||||
// The placement's rotation, so this overlay projects the same frame the bake does. Identity, which
|
||||
// is what an unset uniform gives, leaves the old world-aligned behaviour.
|
||||
uniform mat3 tex_rot; // world -> texture frame
|
||||
uniform float rotation_rad;
|
||||
uniform vec2 uv_offset;
|
||||
uniform bool use_vertex_uv;
|
||||
@@ -88,30 +79,13 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||
}
|
||||
|
||||
|
||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
||||
vec2 tile_mm()
|
||||
{
|
||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
||||
}
|
||||
|
||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
||||
vec2 mirror_sign()
|
||||
{
|
||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
{
|
||||
vec2 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
||||
// the same division, in the tile's own frame.
|
||||
r *= mirror_sign() / tile_mm();
|
||||
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
|
||||
return r + uv_offset;
|
||||
}
|
||||
@@ -131,7 +105,7 @@ void main()
|
||||
|
||||
// World space anchored at the volume's origin, like the bake and the shaded preview.
|
||||
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
|
||||
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor);
|
||||
vec3 tex_pos = world_pos.xyz - tex_anchor;
|
||||
if (volume_mirrored)
|
||||
triangle_normal = -triangle_normal;
|
||||
|
||||
@@ -139,7 +113,7 @@ void main()
|
||||
if (mode == 1) {
|
||||
base = heatmap(distortion);
|
||||
} else {
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * triangle_normal);
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal);
|
||||
vec2 c = floor(uv * checker_freq);
|
||||
float check = mod(c.x + c.y, 2.0);
|
||||
base = (check < 0.5) ? vec3(0.22, 0.23, 0.26) : vec3(0.82, 0.83, 0.86);
|
||||
|
||||
@@ -95,11 +95,6 @@ uniform int palette_b[64];
|
||||
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
|
||||
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
|
||||
uniform bool has_color_tex;
|
||||
// False means the texture is a single placement, not a repeat. The sampler's wrap mode can express
|
||||
// that for the *height* - a zero border reads as no displacement - but not for colour, because a
|
||||
// zero border is indistinguishable from the image genuinely being black there. So the uv is tested
|
||||
// instead, which is what sample_layer_color() does on the CPU side.
|
||||
uniform bool tile_enabled;
|
||||
uniform bool volume_mirrored;
|
||||
|
||||
uniform mat4 view_model_matrix;
|
||||
@@ -109,14 +104,6 @@ uniform sampler2D height_tex;
|
||||
uniform vec2 height_tex_texel; // (1/width, 1/height) of height_tex
|
||||
uniform float depth_mm;
|
||||
uniform float tiling_scale;
|
||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - the square tile
|
||||
// every layer had before the two axes could differ - which is also what layer_tile_mm() does on the
|
||||
// CPU side, so an unset uniform behaves exactly as this shader always did.
|
||||
uniform float tiling_scale_v;
|
||||
// Mirror the texture along one axis of the tile. Both default to false, which GLSL guarantees for an
|
||||
// uninitialised bool uniform, so a shader whose caller has not set them behaves exactly as before.
|
||||
uniform bool mirror_u;
|
||||
uniform bool mirror_v;
|
||||
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
|
||||
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
|
||||
uniform float tex_aspect;
|
||||
@@ -126,12 +113,6 @@ uniform bool invert;
|
||||
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
|
||||
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
|
||||
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
|
||||
// The placement's rotation and its inverse, so this shader turns a world position into the texture
|
||||
// frame exactly as texture_displacement_bake_frame() does. Both are passed rather than one
|
||||
// transposed here, because GLSL 110 has no transpose(). Identity leaves the old world-aligned
|
||||
// behaviour, which is what an unset uniform gives.
|
||||
uniform mat3 tex_rot; // world -> texture frame
|
||||
uniform mat3 tex_rot_inv; // texture frame -> world
|
||||
uniform bool use_vertex_uv; // true: sample at vertex_uv with a derived tangent frame (LSCM)
|
||||
// A 2x3 affine (columns packed as lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the uv of
|
||||
// the island currently being dragged in the UV editor (island_active > 0.5). Identity when nothing is
|
||||
@@ -247,29 +228,13 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
||||
}
|
||||
}
|
||||
|
||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
||||
vec2 tile_mm()
|
||||
{
|
||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
||||
}
|
||||
|
||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
||||
vec2 mirror_sign()
|
||||
{
|
||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
{
|
||||
vec2 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
||||
// the same division, in the tile's own frame.
|
||||
r *= mirror_sign() / tile_mm();
|
||||
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
|
||||
r.y *= tex_aspect;
|
||||
return r + uv_offset;
|
||||
@@ -340,12 +305,9 @@ void main()
|
||||
// printed part whatever the instance's scale or rotation, so the preview has to project from the
|
||||
// world position and perturb the world normal.
|
||||
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
|
||||
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); // the frame the texture is projected in, as the bake does
|
||||
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
|
||||
if (volume_mirrored)
|
||||
triangle_normal = -triangle_normal;
|
||||
// The same normal in that frame. triangle_normal itself stays in world, because the lighting at
|
||||
// the end of this shader needs it there.
|
||||
vec3 tex_normal = tex_rot * triangle_normal;
|
||||
|
||||
// Where the colour is read from. Both branches below already compute the uv this fragment's
|
||||
// *height* came from - including the parallax-marched one on the triplanar path - and the colour
|
||||
@@ -389,14 +351,14 @@ void main()
|
||||
// closed-form uv here, unlike the LSCM case.
|
||||
vec3 t, b;
|
||||
vec2 units_per_mm;
|
||||
projection_axes(tex_pos, tex_normal, t, b, units_per_mm);
|
||||
projection_axes(tex_pos, triangle_normal, t, b, units_per_mm);
|
||||
|
||||
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
|
||||
// first point where it drops below the displaced surface (see header).
|
||||
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
vec3 view_dir = normalize(tex_rot * eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, tex_normal);
|
||||
vec2 uv = project_uv(tex_pos, tex_normal);
|
||||
vec3 view_dir = normalize(eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, triangle_normal);
|
||||
vec2 uv = project_uv(tex_pos, triangle_normal);
|
||||
|
||||
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
|
||||
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
|
||||
@@ -408,18 +370,18 @@ void main()
|
||||
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
|
||||
// a texel there is no parallax to find and the march would be pure cost - which is the common
|
||||
// case of looking straight down at a surface.
|
||||
float sweep = length(view_dir - tex_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
|
||||
if (v_dot_n > 0.05 && sweep > 0.5 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) {
|
||||
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
|
||||
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
|
||||
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
|
||||
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
|
||||
// by construction, and step inward; the crossing is what this pixel actually sees.
|
||||
float s = h_hi / v_dot_n;
|
||||
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
|
||||
vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_normal);
|
||||
vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
|
||||
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
|
||||
for (int i = 0; i < PARALLAX_STEPS; ++i) {
|
||||
s -= ds;
|
||||
vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_normal);
|
||||
vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
|
||||
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
|
||||
if (gap <= 0.0) {
|
||||
// Crossed between the last two samples - interpolating the hit is what stops it
|
||||
@@ -443,28 +405,23 @@ void main()
|
||||
// Central difference, per uv unit (not per texel).
|
||||
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
|
||||
|
||||
// uv -> mm is the reciprocal of the tile extents, applied per component below, which is what
|
||||
// turns the uv-space gradient into a real surface slope.
|
||||
vec2 tile = tile_mm();
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
// uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space
|
||||
// gradient into a real surface slope.
|
||||
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
|
||||
// uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the
|
||||
// gradient back into the axes' frame.
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
// One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
|
||||
// the uv-space gradient carries its own factor before being rotated back into t/b.
|
||||
// Each component carries its own extent and its own mirror sign, both of which project_uv()
|
||||
// applied on the way in; without the sign a mirrored layer would light as though it were not.
|
||||
vec2 ms = mirror_sign();
|
||||
vec2 g = vec2(dh_duv.x * ms.x / tile.x, dh_duv.y * tex_aspect * ms.y / tile.y);
|
||||
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
|
||||
// component of the gradient carries the extra factor before being rotated back into t/b.
|
||||
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
|
||||
// ...and back out of raw-projection units into millimetres of travel along t / b, which is a
|
||||
// no-op except for the spherical longitude (see projection_axes()). After the inverse rotation,
|
||||
// because units_per_mm is expressed in the t/b frame rather than in uv.
|
||||
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm;
|
||||
|
||||
// t and b came out of the texture frame, so the gradient is in it too and has to come back
|
||||
// to world before it perturbs the normal the lighting uses.
|
||||
vec3 gradient = tex_rot_inv * (slope.x * t + slope.y * b);
|
||||
vec3 gradient = slope.x * t + slope.y * b;
|
||||
gradient -= triangle_normal * dot(triangle_normal, gradient);
|
||||
triangle_normal = normalize(triangle_normal - gradient);
|
||||
}
|
||||
@@ -484,9 +441,7 @@ void main()
|
||||
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
|
||||
// the same material under the same light, and the relief this preview exists to show is unaffected.
|
||||
vec3 albedo = uniform_color.rgb;
|
||||
bool uv_covered = tile_enabled ||
|
||||
(all(greaterThanEqual(color_uv, vec2(0.0))) && all(lessThan(color_uv, vec2(1.0))));
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0 && uv_covered)
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
|
||||
albedo = palette_rgb[nearest_palette_entry(texture(color_tex, color_uv).rgb)];
|
||||
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
|
||||
}
|
||||
@@ -25,16 +25,7 @@ uniform bool volume_mirrored;
|
||||
uniform int mode; // 0 checker, 1 distortion
|
||||
uniform float checker_freq; // checker squares per uv unit (one uv unit == one texture tile)
|
||||
uniform float tiling_scale;
|
||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - see
|
||||
// layer_tile_mm() on the CPU side, so an unset uniform behaves exactly as this shader always did.
|
||||
uniform float tiling_scale_v;
|
||||
// Mirror along one tile axis; false by default, so an unset uniform is the old behaviour.
|
||||
uniform bool mirror_u;
|
||||
uniform bool mirror_v;
|
||||
uniform vec3 tex_anchor; // the volume's origin in world space
|
||||
// The placement's rotation, so this overlay projects the same frame the bake does. Identity, which
|
||||
// is what an unset uniform gives, leaves the old world-aligned behaviour.
|
||||
uniform mat3 tex_rot; // world -> texture frame
|
||||
uniform float rotation_rad;
|
||||
uniform vec2 uv_offset;
|
||||
uniform bool use_vertex_uv;
|
||||
@@ -99,30 +90,13 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||
}
|
||||
|
||||
|
||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
||||
vec2 tile_mm()
|
||||
{
|
||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
||||
}
|
||||
|
||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
||||
vec2 mirror_sign()
|
||||
{
|
||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
{
|
||||
vec2 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
||||
// the same division, in the tile's own frame.
|
||||
r *= mirror_sign() / tile_mm();
|
||||
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
|
||||
return r + uv_offset;
|
||||
}
|
||||
@@ -143,7 +117,7 @@ void main()
|
||||
|
||||
// World space anchored at the volume's origin, like the bake and the shaded preview.
|
||||
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
|
||||
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor);
|
||||
vec3 tex_pos = world_pos.xyz - tex_anchor;
|
||||
if (volume_mirrored)
|
||||
triangle_normal = -triangle_normal;
|
||||
|
||||
@@ -151,7 +125,7 @@ void main()
|
||||
if (mode == 1) {
|
||||
base = heatmap(distortion);
|
||||
} else {
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * triangle_normal);
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal);
|
||||
vec2 c = floor(uv * checker_freq);
|
||||
float check = mod(c.x + c.y, 2.0);
|
||||
// Two distinct greys, plus a faint tint on one set so orientation is readable at a glance.
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<script src="../js/plugin-config-ui.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
<script src="./plugin-sort.js"></script>
|
||||
<script src="../js/fuzzy-search.js"></script>
|
||||
<script src="../../js/fuzzy-search.js"></script>
|
||||
<script src="./plugin-search.js"></script>
|
||||
</head>
|
||||
<body onLoad="OnInit()">
|
||||
|
||||
@@ -156,12 +156,16 @@ function SetCloudMode( bOn )
|
||||
|
||||
m_CloudMode=bOn;
|
||||
$("body").toggleClass("cloud-mode", bOn);
|
||||
|
||||
if( bOn )
|
||||
{
|
||||
$(".BtnItem").removeClass("BtnItemSelected");
|
||||
$(".BtnShortcut").addClass("BtnItemSelected");
|
||||
}
|
||||
}
|
||||
|
||||
function GotoCloud()
|
||||
{
|
||||
$(".BtnItem").removeClass("BtnItemSelected");
|
||||
$(".BtnShortcut").addClass("BtnItemSelected");
|
||||
SetCloudMode(true);
|
||||
SendSimpleCommand("homepage_open_cloud");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -336,10 +336,10 @@ modules:
|
||||
sha256: 3d22d46c566b1761a0e15ea397589b3a5f36ac09b7c785382e6470156c04247f
|
||||
dest: external-packages/PNG
|
||||
|
||||
# libjpeg-turbo 3.0.1
|
||||
# libjpeg-turbo 3.2.0
|
||||
- type: file
|
||||
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
|
||||
sha256: d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
|
||||
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
|
||||
sha256: 1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
|
||||
dest: external-packages/JPEG
|
||||
|
||||
# Freetype 2.12.1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Work-around https://gitlab.gnome.org/GNOME/gnome-build-meta/-/issues/754
|
||||
grep -q org.freedesktop.Platform.GL.nvidia /.flatpak-info && export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
# WebKit's NVIDIA DMABUF workaround (gnome-build-meta#754) is applied by the
|
||||
# binary itself, gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
|
||||
|
||||
# Work-around https://github.com/bambulab/BambuStudio/issues/3440
|
||||
# Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues
|
||||
|
||||
@@ -12,6 +12,7 @@ commands:
|
||||
trim delete profile files no <vendor>.json list references
|
||||
update-index regenerate the *_list sections of <vendor>.json
|
||||
legacy-compat write the printers user presets can be migrated back to
|
||||
gcode-diff print a readable markdown diff of changed custom G-code
|
||||
|
||||
options shared by several commands:
|
||||
--vendor VENDOR act on one vendor bundle only; repeatable, empty means all
|
||||
@@ -94,11 +95,14 @@ parent in the OFL map. filament_vendor / filament_type resolve the same way.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import difflib
|
||||
import functools
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
from collections import Counter, defaultdict
|
||||
@@ -192,6 +196,7 @@ VECTOR_KEYS = {
|
||||
|
||||
PRINT_CONFIG_CPP = os.path.normpath(
|
||||
os.path.join(SCRIPTS_DIR, "..", "src", "libslic3r", "PrintConfig.cpp"))
|
||||
TAB_CPP = os.path.normpath(os.path.join(SCRIPTS_DIR, "..", "src", "slic3r", "GUI", "Tab.cpp"))
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
@@ -313,11 +318,11 @@ REMEDY_HINTS = {
|
||||
}
|
||||
|
||||
|
||||
def print_error(msg):
|
||||
print(f"\033[91m[ERROR]\033[0m {msg}") # Red
|
||||
def print_error(msg, file=None):
|
||||
print(f"\033[91m[ERROR]\033[0m {msg}", file=file) # Red
|
||||
|
||||
def print_warning(msg):
|
||||
print(f"\033[93m[WARNING]\033[0m {msg}") # Yellow
|
||||
def print_warning(msg, file=None):
|
||||
print(f"\033[93m[WARNING]\033[0m {msg}", file=file) # Yellow
|
||||
|
||||
def print_info(msg):
|
||||
print(f"\033[94m[INFO]\033[0m {msg}") # Blue
|
||||
@@ -3209,6 +3214,223 @@ def run_legacy_compat(base_dir, profiles_dir, output, check):
|
||||
return 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# gcode-diff
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# UTF-8 bytes, kept well under GitHub's 65,536-character comment limit.
|
||||
GCODE_DIFF_BUDGET = 60000
|
||||
# The file names a section's summary shows before "and N more".
|
||||
GCODE_DIFF_SUMMARY_FILES = 3
|
||||
|
||||
GCODE_DIFF_HEADER = (
|
||||
"## Custom G-code changes\n"
|
||||
"\n"
|
||||
"GitHub shows custom G-code in a profile as one long line, so below is a line by line "
|
||||
"diff of each changed G-code or other text setting.\n")
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def _text_option_keys():
|
||||
"""The string options PrintConfig.cpp defines as G-code or other multi-line text
|
||||
(every *_gcode option, and every one it marks multiline, such as notes), plus the
|
||||
ones Tab.cpp edits as code, such as filename_format."""
|
||||
with open(PRINT_CONFIG_CPP, encoding="utf-8") as f:
|
||||
source = f.read()
|
||||
keys = set()
|
||||
for body in re.split(r"\bdef\s*=\s*this->add(?:_nullable)?\(", source)[1:]:
|
||||
match = re.match(r'"(\w+)",\s*coStrings?\b', body)
|
||||
if match and (match.group(1).endswith("_gcode")
|
||||
or re.search(r"def->multiline\s*=\s*true", body)):
|
||||
keys.add(match.group(1))
|
||||
if "machine_start_gcode" not in keys:
|
||||
raise RuntimeError(f"{PRINT_CONFIG_CPP} no longer defines machine_start_gcode")
|
||||
# Tab.cpp only adds keys, so a missing file is not an error.
|
||||
try:
|
||||
with open(TAB_CPP, encoding="utf-8") as f:
|
||||
gui = f.read()
|
||||
except OSError:
|
||||
gui = ""
|
||||
keys.update(re.findall(r'get_option\("(\w+)"\);(?:(?!get_option\().)*?\bis_code\s*=\s*true',
|
||||
gui, re.DOTALL))
|
||||
return frozenset(keys)
|
||||
|
||||
|
||||
def _git(cwd, *args, stdin=None):
|
||||
return subprocess.run(["git", "-C", cwd, *args], input=stdin, check=True,
|
||||
capture_output=True).stdout
|
||||
|
||||
|
||||
def _blobs_at(profiles_dir, specs):
|
||||
"""{(ref, path): bytes, or None if not a file there} for (ref, path relative to
|
||||
profiles_dir) pairs, all read by one git cat-file process."""
|
||||
prefix = _git(profiles_dir, "rev-parse", "--show-prefix").decode("utf-8").strip()
|
||||
out = _git(profiles_dir, "cat-file", "--batch",
|
||||
stdin="".join(f"{ref}:{prefix}{rel}\n" for ref, rel in specs).encode("utf-8"))
|
||||
blobs, pos = {}, 0
|
||||
for spec in specs:
|
||||
end = out.index(b"\n", pos)
|
||||
header = out[pos:end]
|
||||
pos = end + 1
|
||||
# "<name> missing" and "<name> ambiguous" have no content; "<oid> <type> <size>" does.
|
||||
if header.endswith((b" missing", b" ambiguous")):
|
||||
blobs[spec] = None
|
||||
continue
|
||||
_oid, kind, size = header.split()
|
||||
blobs[spec] = out[pos:pos + int(size)] if kind == b"blob" else None
|
||||
pos += int(size) + 1
|
||||
return blobs
|
||||
|
||||
|
||||
def _parse_profile(raw, rel, where):
|
||||
"""The JSON in raw, or None with a warning when it is absent or not JSON."""
|
||||
if raw is None:
|
||||
print_warning(f"{rel}: skipped, not found at {where}", file=sys.stderr)
|
||||
return None
|
||||
try:
|
||||
return json.loads(raw.decode("utf-8-sig"))
|
||||
except ValueError as e:
|
||||
print_warning(f"{rel}: skipped, cannot read it at {where}: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def _changed_profiles(profiles_dir, base, head):
|
||||
"""(old path, new path) of each JSON file under profiles_dir that differs between
|
||||
base and head (the working tree, untracked files included, when None); the absent
|
||||
side of an added or deleted file is None, and so is a side that is not JSON. Renames
|
||||
are paired."""
|
||||
out = _git(profiles_dir, "diff", "--name-status", "-z", "-M", "--relative", base,
|
||||
*([head] if head else []), "--", ".")
|
||||
fields = out.decode("utf-8").split("\0")
|
||||
pairs, i = [], 0
|
||||
while i + 1 < len(fields):
|
||||
status = fields[i]
|
||||
old = new = fields[i + 1]
|
||||
if status[0] in "RC":
|
||||
new = fields[i + 2]
|
||||
i += 3
|
||||
else:
|
||||
i += 2
|
||||
old = None if status[0] == "A" or not old.endswith(".json") else old
|
||||
new = None if status[0] == "D" or not new.endswith(".json") else new
|
||||
if old or new:
|
||||
pairs.append((old, new))
|
||||
if head is None:
|
||||
out = _git(profiles_dir, "ls-files", "-z", "--others", "--exclude-standard", "--", ".")
|
||||
pairs += [(None, rel) for rel in out.decode("utf-8").split("\0")
|
||||
if rel.endswith(".json") and rel.split("/")[0] != USER_DIR]
|
||||
return pairs
|
||||
|
||||
|
||||
def _text_lines(value):
|
||||
return [line.rstrip() for line in value.split("\n")] if value else []
|
||||
|
||||
|
||||
def _text_changes(old, new):
|
||||
"""(setting, unified diff) for every G-code or other text value that differs
|
||||
between two profiles; a vector option with several values gives one per value."""
|
||||
changes = []
|
||||
for key in list(new) + [k for k in old if k not in new]:
|
||||
a, b = _as_list(old.get(key, [])), _as_list(new.get(key, []))
|
||||
if a == b or not all(isinstance(v, str) for v in a + b):
|
||||
continue
|
||||
if key not in _text_option_keys() and not any("\n" in v for v in a + b):
|
||||
continue
|
||||
width = max(len(a), len(b))
|
||||
for i in range(width):
|
||||
diff = list(difflib.unified_diff(
|
||||
_text_lines(a[i] if i < len(a) else ""), _text_lines(b[i] if i < len(b) else ""),
|
||||
lineterm=""))[2:] # drops the ---/+++ file header
|
||||
if diff:
|
||||
changes.append((key if width == 1 else f"{key}[{i}]", "\n".join(diff)))
|
||||
return changes
|
||||
|
||||
|
||||
def _gcode_diff_section(files, setting, diff):
|
||||
# A fence longer than any backtick run in the G-code cannot be closed by it.
|
||||
fence = "`" * max([3] + [len(run) + 1 for run in re.findall(r"`+", diff)])
|
||||
# In <code>, the "@BBL" of a filament file name does not mention a GitHub user.
|
||||
def code(text):
|
||||
return f"<code>{html.escape(text, quote=False)}</code>"
|
||||
names, listing = ", ".join(map(code, files)), ""
|
||||
if len(files) > GCODE_DIFF_SUMMARY_FILES + 1: # never "and 1 more"
|
||||
listing = f"<p>{names}</p>\n\n" # an HTML block, so markdown in names stays literal
|
||||
names = (", ".join(map(code, files[:GCODE_DIFF_SUMMARY_FILES]))
|
||||
+ f" and {len(files) - GCODE_DIFF_SUMMARY_FILES} more")
|
||||
return (f"\n<details>\n<summary><b>{names} - {code(setting)} (click to expand)</b>"
|
||||
f"</summary>\n\n{listing}{fence}diff\n{diff}\n{fence}\n\n</details>\n")
|
||||
|
||||
|
||||
def gcode_diff(profiles_dir, base, head=None, vendors=None):
|
||||
"""Markdown with one collapsed diff per changed G-code setting between base and
|
||||
head, or "" when none changed. Files whose diff of a setting is identical share
|
||||
one section. Cut to whole sections under GCODE_DIFF_BUDGET."""
|
||||
pairs = [(old, new) for old, new in _changed_profiles(profiles_dir, base, head)
|
||||
if not vendors or any(p and p.split("/")[0] in vendors for p in (old, new))]
|
||||
specs = ([(base, old) for old, _ in pairs if old]
|
||||
+ [(head, new) for _, new in pairs if new and head])
|
||||
blobs = _blobs_at(profiles_dir, specs) if specs else {}
|
||||
|
||||
def load(ref, rel):
|
||||
if rel is None:
|
||||
return {}
|
||||
if ref:
|
||||
return _parse_profile(blobs[(ref, rel)], rel, ref)
|
||||
try:
|
||||
return _parse_profile(Path(profiles_dir, rel).read_bytes(), rel, "the working tree")
|
||||
except OSError as e:
|
||||
print_warning(f"{rel}: skipped, cannot read it: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
groups = {} # (setting, diff) -> files, in the order git lists them
|
||||
for old_rel, new_rel in pairs:
|
||||
old, new = load(base, old_rel), load(head, new_rel)
|
||||
if not isinstance(old, dict) or not isinstance(new, dict):
|
||||
continue
|
||||
for setting, diff in _text_changes(old, new):
|
||||
groups.setdefault((setting, diff), []).append(new_rel or old_rel)
|
||||
if not groups:
|
||||
return ""
|
||||
|
||||
sections = [_gcode_diff_section(files, setting, diff)
|
||||
for (setting, diff), files in groups.items()]
|
||||
budget = GCODE_DIFF_BUDGET - len(GCODE_DIFF_HEADER.encode("utf-8"))
|
||||
shown = 0
|
||||
for section in sections:
|
||||
budget -= len(section.encode("utf-8"))
|
||||
if budget < 0:
|
||||
break
|
||||
shown += 1
|
||||
note, hidden = "", len(sections) - shown
|
||||
if hidden:
|
||||
note = (f"\n{hidden} of {len(sections)} sections {'is' if hidden == 1 else 'are'} not "
|
||||
"shown because the comment would be too long for GitHub. See the Files "
|
||||
"changed tab for the rest.\n")
|
||||
return GCODE_DIFF_HEADER + "".join(sections[:shown]) + note
|
||||
|
||||
|
||||
def _default_gcode_diff_base(profiles_dir, head):
|
||||
for main in ("upstream/main", "origin/main"):
|
||||
try:
|
||||
return _git(profiles_dir, "merge-base", head or "HEAD", main).decode().strip()
|
||||
except subprocess.CalledProcessError:
|
||||
continue
|
||||
raise RuntimeError("no --base given, and neither upstream/main nor origin/main exists")
|
||||
|
||||
|
||||
def run_gcode_diff(profiles_dir, vendors, base, head):
|
||||
try:
|
||||
markdown = gcode_diff(profiles_dir, base or _default_gcode_diff_base(profiles_dir, head),
|
||||
head, vendors)
|
||||
# UnicodeDecodeError means git listed a file name that is not UTF-8.
|
||||
except (OSError, RuntimeError, UnicodeDecodeError, subprocess.CalledProcessError) as e:
|
||||
stderr = getattr(e, "stderr", None)
|
||||
print_error(stderr.decode(errors="replace").strip() if stderr else str(e), file=sys.stderr)
|
||||
return 1
|
||||
sys.stdout.write(markdown)
|
||||
return 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -3233,6 +3455,9 @@ examples:
|
||||
orca_profile_tool.py legacy-compat --base /tmp/v2.4.2/resources/profiles
|
||||
rewrite resources/info/legacy_compatible_printers.json against 2.4.2's
|
||||
profiles (git archive v2.4.2 resources/profiles | tar -x -C /tmp/v2.4.2)
|
||||
orca_profile_tool.py gcode-diff > gcode.md
|
||||
the changed custom G-code of the working tree since it left upstream/main,
|
||||
as markdown with one collapsed diff per setting
|
||||
|
||||
after adding, renaming or deleting profile files, run in this order:
|
||||
normalize -> update-index -> generate-id -> check
|
||||
@@ -3387,6 +3612,25 @@ def build_parser():
|
||||
legacy_cmd.add_argument("--check", action="store_true",
|
||||
help="fail if the map differs from what --base implies; write nothing")
|
||||
|
||||
gcode_cmd = add(
|
||||
"gcode-diff", [vendor_opt, profiles_opt],
|
||||
"print a readable markdown diff of changed custom G-code",
|
||||
"Print, as markdown, a unified diff of every G-code or other multi-line\n"
|
||||
"setting that differs between --base and --head in each changed profile\n"
|
||||
"file, one collapsed section per setting. Files whose diff of a setting is\n"
|
||||
"identical share a section. Trailing spaces are removed from every line, and\n"
|
||||
"the output is cut to whole sections to fit a GitHub comment. Prints nothing\n"
|
||||
"when no such setting changed. CI posts it on profile PRs.\n"
|
||||
"\n"
|
||||
"The settings are the *_gcode and multiline string options PrintConfig.cpp\n"
|
||||
"defines, the options Tab.cpp edits as code (such as filename_format), and any\n"
|
||||
"string value that spans several lines.")
|
||||
gcode_cmd.add_argument("--base", metavar="REF",
|
||||
help="compare against this git ref (default: the merge base "
|
||||
"with upstream/main, else origin/main)")
|
||||
gcode_cmd.add_argument("--head", metavar="REF",
|
||||
help="compare this git ref (default: the working tree)")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
@@ -3423,6 +3667,9 @@ def main(argv=None):
|
||||
if args.command == "legacy-compat":
|
||||
return run_legacy_compat(args.base, profiles_dir, args.output, args.check)
|
||||
|
||||
if args.command == "gcode-diff":
|
||||
return run_gcode_diff(profiles_dir, vendors, args.base, args.head)
|
||||
|
||||
if args.command == "fix-variant":
|
||||
_changed, errors = fix_variant_arrays(profiles_dir, vendors, dry_run=args.dry_run,
|
||||
strict=args.strict)
|
||||
|
||||
@@ -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)
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for the tree-maintenance half of scripts/orca_profile_tool.py: the
|
||||
normalize, trim, update-index, fix-variant and check commands, and the subcommand
|
||||
dispatch that reaches them (stdlib unittest, no external deps).
|
||||
normalize, trim, update-index, fix-variant, check and gcode-diff commands, and the
|
||||
subcommand dispatch that reaches them (stdlib unittest, no external deps).
|
||||
|
||||
The id halves are covered by test_filament_id.py and test_setting_id.py.
|
||||
|
||||
@@ -14,9 +14,12 @@ import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
@@ -41,7 +44,24 @@ class Tree:
|
||||
os.makedirs(self.profiles)
|
||||
|
||||
def cleanup(self):
|
||||
shutil.rmtree(self.dir, ignore_errors=True)
|
||||
root = os.path.abspath(self.dir)
|
||||
|
||||
def writable_retry(func, path, _exc):
|
||||
# Windows refuses to delete read-only files such as git's objects, and POSIX
|
||||
# refuses to delete the entries of a read-only directory.
|
||||
if not os.path.lexists(path): # already removed, as on a second cleanup
|
||||
return
|
||||
parent = os.path.dirname(os.path.abspath(path))
|
||||
try:
|
||||
if os.path.commonpath([parent, root]) == root: # never the temp dir itself
|
||||
os.chmod(parent, stat.S_IRWXU)
|
||||
if not os.path.islink(path): # chmod would follow it out of the tree
|
||||
os.chmod(path, stat.S_IRWXU)
|
||||
func(path)
|
||||
except (OSError, ValueError): # commonpath raises ValueError across drives
|
||||
pass
|
||||
hook = "onexc" if sys.version_info >= (3, 12) else "onerror"
|
||||
shutil.rmtree(self.dir, **{hook: writable_retry})
|
||||
|
||||
def index_path(self, vendor):
|
||||
return os.path.join(self.profiles, vendor + ".json")
|
||||
@@ -1409,6 +1429,292 @@ class TestDispatch(TreeCase):
|
||||
self.assertEqual(self.t.read("V", "filament/A.json")["type"], "filament")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# gcode-diff
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@unittest.skipUnless(shutil.which("git"), "git not installed")
|
||||
class TestGcodeDiff(TreeCase):
|
||||
START = "G28\nM104 S200\nG1 Z5"
|
||||
|
||||
def git(self, *args):
|
||||
subprocess.run(["git", "-C", self.t.dir, "-c", "user.name=t", "-c", "user.email=t@t",
|
||||
"-c", "commit.gpgsign=false", *args], check=True, capture_output=True)
|
||||
|
||||
def printer(self, name, **extra):
|
||||
self.t.write("V", f"machine/{name}.json", {"type": "machine", "name": name, **extra})
|
||||
|
||||
def commit(self):
|
||||
self.git("add", "-A")
|
||||
self.git("commit", "-q", "--allow-empty", "-m", "x")
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# git's list of the variables that tie a process to a repository, asked with no
|
||||
# GIT_* set since a stray GIT_DIR fails the query.
|
||||
clean = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
|
||||
cls.repo_env_vars = set(subprocess.run(
|
||||
["git", "rev-parse", "--local-env-vars"], env=clean, check=True,
|
||||
capture_output=True, text=True).stdout.split())
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# A git hook's GIT_DIR or GIT_INDEX_FILE would point every git call here, the
|
||||
# tool's included, at the real repository instead of the temp tree.
|
||||
env = {k: v for k, v in os.environ.items() if k not in self.repo_env_vars}
|
||||
patcher = mock.patch.dict(os.environ, env, clear=True)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
self.git("init", "-q")
|
||||
for name in ("A", "B", "C"):
|
||||
self.printer(name, machine_start_gcode=self.START, nozzle_diameter=["0.4"])
|
||||
self.printer("Gone", machine_end_gcode="M84")
|
||||
self.commit()
|
||||
|
||||
def diff(self):
|
||||
rc, out = self.run_command("gcode-diff", "--base", "HEAD")
|
||||
self.assertEqual(rc, 0, out)
|
||||
return out
|
||||
|
||||
def test_identical_edits_share_one_section(self):
|
||||
for name in ("A", "B"):
|
||||
self.printer(name, machine_start_gcode="G28\nM104 S210 \nG1 Z5")
|
||||
self.printer("C", machine_start_gcode="G28\nM104 S220\nG1 Z5")
|
||||
out = self.diff()
|
||||
self.assertEqual(out.count("<details>"), 2)
|
||||
self.assertIn("<code>V/machine/A.json</code>, <code>V/machine/B.json</code> - "
|
||||
"<code>machine_start_gcode</code>", out)
|
||||
self.assertIn("<code>V/machine/C.json</code> - <code>machine_start_gcode</code>", out)
|
||||
# Trailing spaces are removed.
|
||||
self.assertIn("\n-M104 S200\n+M104 S210\n", out)
|
||||
self.assertIn("\n\n```diff\n@@ ", out)
|
||||
self.assertIn("\n```\n\n</details>", out)
|
||||
|
||||
def test_nothing_is_printed_without_a_text_change(self):
|
||||
self.printer("A", machine_start_gcode=self.START, nozzle_diameter=["0.6"])
|
||||
self.assertEqual(self.diff(), "")
|
||||
|
||||
def test_added_and_deleted_files_show_the_whole_value(self):
|
||||
# An untracked file counts as added; the validator's user dir is not a profile.
|
||||
self.printer("New", machine_start_gcode="G28\nG29")
|
||||
self.t.write("user", "default/machine/U.json", {"machine_start_gcode": "G28"})
|
||||
os.remove(self.t.path("V", "machine/Gone.json"))
|
||||
out = self.diff()
|
||||
self.assertIn("@@ -0,0 +1,2 @@\n+G28\n+G29\n", out)
|
||||
self.assertIn("<code>V/machine/Gone.json</code> - <code>machine_end_gcode</code>", out)
|
||||
self.assertIn("@@ -1 +0,0 @@\n-M84\n", out)
|
||||
self.assertNotIn("U.json", out)
|
||||
|
||||
def test_a_multi_line_value_counts_whatever_its_key(self):
|
||||
self.printer("A", machine_start_gcode=self.START, custom_note="one\ntwo")
|
||||
self.assertIn("<code>V/machine/A.json</code> - <code>custom_note</code>", self.diff())
|
||||
|
||||
def test_a_file_name_cannot_mention_a_user(self):
|
||||
self.t.write("V", "filament/PLA @BBL A1.json", {"filament_start_gcode": ["M106"]})
|
||||
self.assertIn("<code>V/filament/PLA @BBL A1.json</code>", self.diff())
|
||||
|
||||
def test_head_compares_two_commits(self):
|
||||
self.printer("A", machine_start_gcode="G28")
|
||||
self.commit()
|
||||
rc, out = self.run_command("gcode-diff", "--base", "HEAD~1", "--head", "HEAD")
|
||||
self.assertEqual(rc, 0, out)
|
||||
self.assertIn("<code>V/machine/A.json</code> - <code>machine_start_gcode</code>", out)
|
||||
|
||||
def test_a_renamed_file_diffs_against_its_old_self(self):
|
||||
os.rename(self.t.path("V", "machine/A.json"), self.t.path("V", "machine/A2.json"))
|
||||
self.printer("A2", machine_start_gcode="G28\nM104 S210\nG1 Z5", nozzle_diameter=["0.4"])
|
||||
self.commit()
|
||||
rc, out = self.run_command("gcode-diff", "--base", "HEAD~1", "--head", "HEAD")
|
||||
self.assertEqual(rc, 0, out)
|
||||
self.assertIn("<code>V/machine/A2.json</code> - <code>machine_start_gcode</code>", out)
|
||||
self.assertIn("\n G28\n-M104 S200\n+M104 S210\n G1 Z5\n", out)
|
||||
|
||||
def test_a_file_renamed_off_json_loses_its_values(self):
|
||||
os.rename(self.t.path("V", "machine/Gone.json"), self.t.path("V", "machine/Gone.json.bak"))
|
||||
self.commit()
|
||||
rc, out = self.run_command("gcode-diff", "--base", "HEAD~1", "--head", "HEAD")
|
||||
self.assertEqual(rc, 0, out)
|
||||
self.assertIn("<code>V/machine/Gone.json</code> - <code>machine_end_gcode</code>", out)
|
||||
self.assertIn("@@ -1 +0,0 @@\n-M84\n", out)
|
||||
|
||||
def test_a_file_renamed_onto_json_is_new(self):
|
||||
with open(self.t.path("V", "machine/Draft.txt"), "w", encoding="utf-8") as f:
|
||||
json.dump({"type": "machine", "machine_start_gcode": self.START}, f)
|
||||
self.commit()
|
||||
os.rename(self.t.path("V", "machine/Draft.txt"), self.t.path("V", "machine/Draft.json"))
|
||||
self.commit()
|
||||
rc, out = self.run_command("gcode-diff", "--base", "HEAD~1", "--head", "HEAD")
|
||||
self.assertEqual(rc, 0, out)
|
||||
self.assertIn("@@ -0,0 +1,3 @@\n+G28\n+M104 S200\n+G1 Z5\n", out)
|
||||
|
||||
def test_a_batch_read_survives_paths_that_are_not_files(self):
|
||||
blobs = apt._blobs_at(self.t.profiles, [("HEAD", "V/machine"), ("HEAD", "V/no such.json"),
|
||||
("HEAD", "V/machine/Gone.json")])
|
||||
self.assertIsNone(blobs[("HEAD", "V/machine")])
|
||||
self.assertIsNone(blobs[("HEAD", "V/no such.json")])
|
||||
self.assertEqual(json.loads(blobs[("HEAD", "V/machine/Gone.json")])["machine_end_gcode"],
|
||||
"M84")
|
||||
|
||||
def test_each_value_of_a_vector_option_is_its_own_setting(self):
|
||||
self.t.write("V", "filament/F.json", {"filament_start_gcode": ["M106", "M107"]})
|
||||
self.commit()
|
||||
self.t.write("V", "filament/F.json", {"filament_start_gcode": ["M106", "M106 S0"]})
|
||||
out = self.diff()
|
||||
self.assertEqual(out.count("<details>"), 1)
|
||||
self.assertIn("<code>filament_start_gcode[1]</code>", out)
|
||||
|
||||
def test_vendor_limits_the_files(self):
|
||||
self.printer("A", machine_start_gcode="G28")
|
||||
self.t.write("W", "machine/X.json", {"machine_start_gcode": "G28"})
|
||||
rc, out = self.run_command("gcode-diff", "--base", "HEAD", "--vendor", "V")
|
||||
self.assertEqual(rc, 0, out)
|
||||
self.assertIn("V/machine/A.json", out)
|
||||
self.assertNotIn("W/machine/X.json", out)
|
||||
|
||||
def test_vendor_includes_files_moved_out_of_it(self):
|
||||
self.t.add_vendor("W")
|
||||
os.rename(self.t.path("V", "machine/A.json"), self.t.path("W", "machine/A.json"))
|
||||
data = self.t.read("W", "machine/A.json")
|
||||
self.t.write("W", "machine/A.json", {**data, "machine_start_gcode": "G28\nM104 S210\nG1 Z5"})
|
||||
self.commit()
|
||||
rc, out = self.run_command("gcode-diff", "--base", "HEAD~1", "--head", "HEAD",
|
||||
"--vendor", "V")
|
||||
self.assertEqual(rc, 0, out)
|
||||
self.assertIn("<code>W/machine/A.json</code> - <code>machine_start_gcode</code>", out)
|
||||
|
||||
def same_edit(self, names):
|
||||
"""The diff after one start G-code edit to each of names; returns (summary, output)."""
|
||||
for name in names:
|
||||
self.printer(name, machine_start_gcode=self.START, nozzle_diameter=["0.4"])
|
||||
self.commit()
|
||||
for name in names:
|
||||
self.printer(name, machine_start_gcode="G28")
|
||||
out = self.diff()
|
||||
return re.search(r"<summary>(.*)</summary>", out).group(1), out
|
||||
|
||||
def test_a_long_file_list_moves_into_the_section(self):
|
||||
summary, out = self.same_edit("ABCDE")
|
||||
self.assertIn("<code>V/machine/C.json</code> and 2 more - ", summary)
|
||||
self.assertNotIn("D.json", summary)
|
||||
self.assertIn("<code>V/machine/D.json</code>, <code>V/machine/E.json</code></p>\n\n```diff",
|
||||
out)
|
||||
|
||||
def test_four_files_are_all_named_in_the_summary(self):
|
||||
summary, _out = self.same_edit("ABCD")
|
||||
self.assertIn("<code>V/machine/D.json</code> - ", summary)
|
||||
self.assertNotIn("more", summary)
|
||||
|
||||
def test_an_error_goes_to_stderr_only(self):
|
||||
err = io.StringIO()
|
||||
with contextlib.redirect_stderr(err):
|
||||
rc, out = self.run_command("gcode-diff", "--base", "no-such-ref")
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertEqual(out, "")
|
||||
self.assertIn("no-such-ref", err.getvalue())
|
||||
|
||||
def test_a_file_name_that_is_not_utf8_is_an_error(self):
|
||||
bad = UnicodeDecodeError("utf-8", b"\xe9.json", 0, 1, "invalid continuation byte")
|
||||
err = io.StringIO()
|
||||
with mock.patch.object(apt, "_changed_profiles", side_effect=bad), \
|
||||
contextlib.redirect_stderr(err):
|
||||
rc, out = self.run_command("gcode-diff", "--base", "HEAD")
|
||||
self.assertEqual((rc, out), (1, ""))
|
||||
self.assertIn("invalid continuation byte", err.getvalue())
|
||||
|
||||
def test_output_is_cut_to_whole_sections(self):
|
||||
for name in ("A", "B", "C"):
|
||||
self.printer(name, machine_start_gcode=name * 300)
|
||||
size = len(apt.gcode_diff(self.t.profiles, "HEAD").encode("utf-8"))
|
||||
with mock.patch.object(apt, "GCODE_DIFF_BUDGET", size - 1):
|
||||
out = self.diff()
|
||||
self.assertEqual(out.count("<details>"), 2)
|
||||
self.assertEqual(out.count("</details>"), 2)
|
||||
self.assertIn("\n1 of 3 sections is not shown", out)
|
||||
with mock.patch.object(apt, "GCODE_DIFF_BUDGET", 1):
|
||||
self.assertIn("\n3 of 3 sections are not shown", self.diff())
|
||||
|
||||
def test_cleanup_removes_the_repo(self):
|
||||
# setUp's commit leaves read-only git objects, and a read-only directory blocks
|
||||
# deletion on POSIX.
|
||||
os.chmod(self.t.path("V", "machine"), stat.S_IRUSR | stat.S_IXUSR)
|
||||
self.t.cleanup()
|
||||
self.assertFalse(os.path.exists(self.t.dir))
|
||||
with mock.patch.object(os, "chmod") as chmod:
|
||||
self.t.cleanup() # a second cleanup, as addCleanup runs, must not touch the temp dir
|
||||
chmod.assert_not_called()
|
||||
|
||||
def test_only_repository_variables_are_stripped(self):
|
||||
self.assertTrue({"GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"} <= self.repo_env_vars)
|
||||
self.assertFalse({"GIT_SSH", "GIT_EXEC_PATH", "GIT_EDITOR"} & self.repo_env_vars)
|
||||
|
||||
def test_cleanup_leaves_symlink_targets_alone(self):
|
||||
outside = tempfile.mkdtemp(prefix="profile_tool_outside_")
|
||||
self.addCleanup(shutil.rmtree, outside, True)
|
||||
target = os.path.join(outside, "f")
|
||||
open(target, "w").close()
|
||||
os.chmod(target, stat.S_IRUSR)
|
||||
self.addCleanup(os.chmod, target, stat.S_IRWXU) # runs before the rmtree above
|
||||
machine = self.t.path("V", "machine")
|
||||
try:
|
||||
os.symlink(target, os.path.join(machine, "link"))
|
||||
except (OSError, NotImplementedError):
|
||||
self.skipTest("cannot create symlinks here")
|
||||
os.chmod(machine, stat.S_IRUSR | stat.S_IXUSR) # so removing the link fails at first
|
||||
self.t.cleanup()
|
||||
self.assertFalse(os.path.exists(self.t.dir))
|
||||
self.assertEqual(stat.S_IMODE(os.stat(target).st_mode) & stat.S_IWUSR, 0)
|
||||
|
||||
def test_a_git_hook_environment_is_ignored(self):
|
||||
scripts = os.path.join(REPO_ROOT, "scripts")
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "unittest",
|
||||
"tests.test_profile_tool.TestGcodeDiff.test_head_compares_two_commits"],
|
||||
cwd=scripts, capture_output=True, text=True,
|
||||
env={**os.environ, "GIT_DIR": os.path.join(self.t.dir, "nowhere")})
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_the_options_are_read_from_the_engine(self):
|
||||
keys = apt._text_option_keys()
|
||||
self.assertTrue({"machine_start_gcode", "filament_start_gcode", "printing_by_object_gcode",
|
||||
"notes", "post_process"} <= keys)
|
||||
self.assertNotIn("emit_machine_limits_to_gcode", keys) # a coBool
|
||||
|
||||
START_DEF = 'def = this->add("machine_start_gcode", coString);\n'
|
||||
|
||||
def keys_from(self, print_config, tab=None):
|
||||
"""_text_option_keys read from these sources; no tab means no Tab.cpp."""
|
||||
paths = [os.path.join(self.t.dir, "PrintConfig.cpp"), os.path.join(self.t.dir, "Tab.cpp")]
|
||||
for path, text in zip(paths, (print_config, tab)):
|
||||
if text is not None:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
apt._text_option_keys.cache_clear()
|
||||
self.addCleanup(apt._text_option_keys.cache_clear)
|
||||
with mock.patch.object(apt, "PRINT_CONFIG_CPP", paths[0]), \
|
||||
mock.patch.object(apt, "TAB_CPP", paths[1]):
|
||||
return apt._text_option_keys()
|
||||
|
||||
def test_multiline_is_read_whatever_its_spacing(self):
|
||||
keys = self.keys_from(self.START_DEF + 'def = this->add("printer_notes", coString);\n'
|
||||
' def->multiline = true;\n')
|
||||
self.assertIn("printer_notes", keys)
|
||||
|
||||
def test_the_code_options_of_tab_cpp_are_read(self):
|
||||
keys = self.keys_from(self.START_DEF, 'option = optgroup->get_option("filename_format");\n'
|
||||
'option.opt.is_code = true;\n'
|
||||
'option = optgroup->get_option("layer_height");\n')
|
||||
self.assertIn("filename_format", keys)
|
||||
self.assertNotIn("layer_height", keys)
|
||||
|
||||
def test_a_missing_tab_cpp_is_not_an_error(self):
|
||||
self.assertIn("machine_start_gcode", self.keys_from(self.START_DEF))
|
||||
|
||||
def test_markdown_in_a_file_name_stays_literal(self):
|
||||
section = apt._gcode_diff_section(["V/a *b*.json", "V/c.json", "V/d.json", "V/e.json",
|
||||
"V/f.json"], "machine_start_gcode", "@@ -1 +1 @@")
|
||||
self.assertIn("\n<p><code>V/a *b*.json</code>, ", section)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# the real tree
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+37
-15
@@ -163,6 +163,7 @@ using json = nlohmann::json;
|
||||
#include <X11/Xlib.h>
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
#include "slic3r/GUI/Widgets/WebView.hpp"
|
||||
#endif
|
||||
|
||||
#ifdef SLIC3R_GUI
|
||||
@@ -1125,6 +1126,19 @@ static void load_downward_settings_list_from_config(std::string config_file, std
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __WXGTK__
|
||||
// Virtual GPUs (VirtualBox, VMware, QEMU) cannot back WebKit's hardware DMABUF renderer.
|
||||
static bool is_hypervisor_guest()
|
||||
{
|
||||
std::ifstream cpuinfo("/proc/cpuinfo");
|
||||
std::string line;
|
||||
while (std::getline(cpuinfo, line))
|
||||
if (boost::starts_with(line, "flags"))
|
||||
return line.find(" hypervisor") != std::string::npos;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
int CLI::run(int argc, char **argv)
|
||||
{
|
||||
// Mark the main thread for the debugger and for runtime checks.
|
||||
@@ -1156,8 +1170,10 @@ int CLI::run(int argc, char **argv)
|
||||
// opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it
|
||||
// worked around appears fixed in WebKit2GTK >= 2.42; leaving it
|
||||
// unset preserves WebKit hardware acceleration on Device / Setup
|
||||
// Wizard / login / store. The default path still applies it on
|
||||
// XWayland sessions as a conservative fallback for older WebKit.
|
||||
// Wizard / login / store. The default path applies it only where it
|
||||
// forces GDK_BACKEND=x11 below (a build without EGL, so the app really
|
||||
// runs through XWayland), and only when the linked WebKitGTK predates
|
||||
// the fix (< 2.42), as a conservative fallback for older WebKit.
|
||||
// ------------------------------------------------------------------
|
||||
{
|
||||
const char* gdk_backend = ::getenv("GDK_BACKEND");
|
||||
@@ -1165,6 +1181,17 @@ int CLI::run(int argc, char **argv)
|
||||
// honours the first backend in the comma-separated list).
|
||||
const bool x11_opt_in = gdk_backend && boost::starts_with(gdk_backend, "x11");
|
||||
|
||||
// NVIDIA's proprietary driver cannot back WebKitGTK's DMABUF renderer.
|
||||
// Force the shared-memory transport, which keeps compositing and threaded
|
||||
// scrolling; the second variable clears Debian/Ubuntu/openSUSE's isNVIDIA().
|
||||
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
|
||||
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /* replace */ false);
|
||||
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /* replace */ false);
|
||||
}
|
||||
|
||||
if (is_hypervisor_guest() && WebView::SupportsForceSharedMemory())
|
||||
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /* replace */ false);
|
||||
|
||||
if (x11_opt_in) {
|
||||
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
|
||||
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
|
||||
@@ -1204,23 +1231,18 @@ int CLI::run(int argc, char **argv)
|
||||
if (wayland_env && *wayland_env) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend.";
|
||||
::setenv("GDK_BACKEND", "x11", true);
|
||||
|
||||
// GTK will use X11 in a Wayland session here, which is the
|
||||
// one default-path case that really runs through XWayland.
|
||||
// WebKit2GTK accelerated compositing could fail silently
|
||||
// there on releases before 2.42, so force it off. Disabling
|
||||
// it also turns off threaded scrolling, hence the gate.
|
||||
if (WebView::NeedsCompositingWorkaround())
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// WebKit2GTK compositing can fail under XWayland on older
|
||||
// WebKit releases. Disable it only when both DISPLAY and
|
||||
// WAYLAND_DISPLAY are set (i.e. an XWayland session is in
|
||||
// use). On pure X11 or native Wayland, compositing is left
|
||||
// enabled so WebKit retains HW acceleration.
|
||||
{
|
||||
const char* display_env_wk = ::getenv("DISPLAY");
|
||||
const char* wayland_env_wk = ::getenv("WAYLAND_DISPLAY");
|
||||
if (display_env_wk && *display_env_wk && wayland_env_wk && *wayland_env_wk) {
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
|
||||
}
|
||||
}
|
||||
|
||||
// XInitThreads is needed before GStreamer may use Xlib. On
|
||||
// native Wayland without DISPLAY, GStreamer uses waylandsink
|
||||
// (no Xlib involved), so the call is skipped.
|
||||
|
||||
@@ -429,7 +429,8 @@ if [ "\$XDG_SESSION_TYPE" = "wayland" ] && [ "\$ZINK_DISABLE_OVERRIDE" != "1" ];
|
||||
export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json
|
||||
export MESA_LOADER_DRIVER_OVERRIDE=zink
|
||||
export GALLIUM_DRIVER=zink
|
||||
export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
# WebKit's NVIDIA DMABUF workaround is applied by the binary itself,
|
||||
# gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -812,7 +812,8 @@ bool WallToolPaths::removeEmptyToolPaths(std::vector<VariableWidthLines> &toolpa
|
||||
* Get the order constraints of the insets when printing walls per region / hole.
|
||||
* Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right.
|
||||
*
|
||||
* Odd walls should always go after their enclosing wall polygons.
|
||||
* Odd walls follow the same order as even ones, so that the centre line of a narrow wall is an inner wall
|
||||
* like any other when printing inner to outer.
|
||||
*
|
||||
* \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one.
|
||||
*/
|
||||
@@ -886,12 +887,7 @@ WallToolPaths::ExtrusionLineSet WallToolPaths::getRegionOrder(const std::vector<
|
||||
continue; // not directly adjacent
|
||||
if (!shorter_then(loc_here - lineloc_nearby.j.p, (lineloc_here.j.w + lineloc_nearby.j.w) / 2 * diagonal_extension))
|
||||
continue; // points are too far away from each other
|
||||
if (here->is_odd || nearby->is_odd) {
|
||||
if (here->is_odd && !nearby->is_odd && nearby->inset_idx < here->inset_idx)
|
||||
order_requirements.emplace(std::make_pair(nearby, here));
|
||||
if (nearby->is_odd && !here->is_odd && here->inset_idx < nearby->inset_idx)
|
||||
order_requirements.emplace(std::make_pair(here, nearby));
|
||||
} else if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
|
||||
if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
|
||||
order_requirements.emplace(std::make_pair(nearby, here));
|
||||
} else {
|
||||
assert((nearby->inset_idx > here->inset_idx) == outer_to_inner);
|
||||
|
||||
@@ -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
|
||||
@@ -666,6 +666,9 @@ endif ()
|
||||
encoding_check(libslic3r)
|
||||
|
||||
target_compile_definitions(libslic3r PUBLIC -DUSE_TBB -DTBB_USE_CAPTURED_EXCEPTION=0)
|
||||
# CGAL changes Kd_tree's member layout when TBB is enabled. Match libslic3r_cgal
|
||||
# in every consumer so inline constructors/destructors cannot mix the two layouts.
|
||||
target_compile_definitions(libslic3r PUBLIC CGAL_LINKED_WITH_TBB)
|
||||
if (USE_SLIC3R_CONSOLE_LOG)
|
||||
target_compile_definitions(libslic3r PRIVATE $<$<CONFIG:RelWithDebInfo>:SLIC3R_CONSOLE_LOG>)
|
||||
endif()
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
+41
-16
@@ -377,7 +377,8 @@ 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;
|
||||
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride) = 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_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.
|
||||
@@ -580,7 +581,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) override
|
||||
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride, bool keep_nil) override
|
||||
{
|
||||
if (rhs->type() == this->type()) {
|
||||
// Assign the first value of the rhs vector.
|
||||
@@ -592,7 +593,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 (!other->is_nil(diff_index[i] * stride))
|
||||
if (keep_nil || !other->is_nil(diff_index[i] * stride))
|
||||
this->values[i * stride + j] = other->values[diff_index[i] * stride + j];
|
||||
}
|
||||
}
|
||||
@@ -841,8 +842,12 @@ public:
|
||||
{
|
||||
UNUSED(append);
|
||||
std::istringstream iss(str);
|
||||
iss >> this->value;
|
||||
return !iss.fail();
|
||||
double value;
|
||||
iss >> value;
|
||||
if (iss.fail())
|
||||
return false;
|
||||
this->value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool operator==(const ConfigOption &rhs) const override
|
||||
@@ -1033,8 +1038,12 @@ public:
|
||||
{
|
||||
UNUSED(append);
|
||||
std::istringstream iss(str);
|
||||
iss >> this->value;
|
||||
return !iss.fail();
|
||||
int value;
|
||||
iss >> value;
|
||||
if (iss.fail())
|
||||
return false;
|
||||
this->value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
ConfigOptionInt& operator=(const ConfigOption *opt)
|
||||
@@ -1252,8 +1261,12 @@ public:
|
||||
UNUSED(append);
|
||||
// don't try to parse the trailing % since it's optional
|
||||
std::istringstream iss(str);
|
||||
iss >> this->value;
|
||||
return !iss.fail();
|
||||
double value;
|
||||
iss >> value;
|
||||
if (iss.fail())
|
||||
return false;
|
||||
this->value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -1365,10 +1378,14 @@ public:
|
||||
bool deserialize(const std::string &str, bool append = false) override
|
||||
{
|
||||
UNUSED(append);
|
||||
this->percent = str.find_first_of("%") != std::string::npos;
|
||||
std::istringstream iss(str);
|
||||
iss >> this->value;
|
||||
return !iss.fail();
|
||||
double value;
|
||||
iss >> value;
|
||||
if (iss.fail())
|
||||
return false;
|
||||
this->value = value;
|
||||
this->percent = str.find_first_of("%") != std::string::npos;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -1538,8 +1555,12 @@ public:
|
||||
{
|
||||
UNUSED(append);
|
||||
char dummy;
|
||||
return sscanf(str.data(), " %lf , %lf %c", &this->value(0), &this->value(1), &dummy) == 2 ||
|
||||
sscanf(str.data(), " %lf x %lf %c", &this->value(0), &this->value(1), &dummy) == 2;
|
||||
Vec2d value;
|
||||
if (sscanf(str.data(), " %lf , %lf %c", &value(0), &value(1), &dummy) != 2 &&
|
||||
sscanf(str.data(), " %lf x %lf %c", &value(0), &value(1), &dummy) != 2)
|
||||
return false;
|
||||
this->value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -1658,8 +1679,12 @@ public:
|
||||
{
|
||||
UNUSED(append);
|
||||
char dummy;
|
||||
return sscanf(str.data(), " %lf , %lf , %lf %c", &this->value(0), &this->value(1), &this->value(2), &dummy) == 3 ||
|
||||
sscanf(str.data(), " %lf x %lf x %lf %c", &this->value(0), &this->value(1), &this->value(2), &dummy) == 3;
|
||||
Vec3d value;
|
||||
if (sscanf(str.data(), " %lf , %lf , %lf %c", &value(0), &value(1), &value(2), &dummy) != 3 &&
|
||||
sscanf(str.data(), " %lf x %lf x %lf %c", &value(0), &value(1), &value(2), &dummy) != 3)
|
||||
return false;
|
||||
this->value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
@@ -616,7 +616,7 @@ struct PropagateDanielssonSingleStep {
|
||||
if ((signs[addr] & 2) == 0) {
|
||||
float *v = &L[addr << 1];
|
||||
float l = v[0] * v[0] + v[1] * v[1];
|
||||
float *v2s = v + (addr_delta << 1);
|
||||
float *v2s = v + 2 * addr_delta;
|
||||
float v2[2] = {
|
||||
v2s[0] + INCX * resolution,
|
||||
v2s[1] + INCY * resolution
|
||||
|
||||
+37
-37
@@ -1,4 +1,4 @@
|
||||
#include "FillTpmsAdaptive.hpp"
|
||||
#include "AdaptiveInfillField.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -146,11 +146,11 @@ void rasterize(const ExPolygons &expolygons, const Vec2d &origin, double cell, i
|
||||
}
|
||||
|
||||
// Scale of the pattern relative to the surface at a depth from 0 at the surface to 1 at the deepest point.
|
||||
double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth)
|
||||
double target_scale(double ratio, AdaptiveInfillGradient gradient, double depth)
|
||||
{
|
||||
switch (gradient) {
|
||||
case TpmsAdaptiveGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
|
||||
case TpmsAdaptiveGradient::Exponential: return std::pow(ratio, depth);
|
||||
case AdaptiveInfillGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
|
||||
case AdaptiveInfillGradient::Exponential: return std::pow(ratio, depth);
|
||||
default: return 1. + (ratio - 1.) * depth;
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth)
|
||||
// continuous level of the target of a depth.
|
||||
struct DensityLevels
|
||||
{
|
||||
DensityLevels(double ratio, double step, TpmsAdaptiveGradient gradient)
|
||||
DensityLevels(double ratio, double step, AdaptiveInfillGradient gradient)
|
||||
: ratio(ratio), gradient(gradient), count(int(std::ceil(std::abs(std::log(ratio)) / std::log(step) - EPSILON)))
|
||||
{}
|
||||
|
||||
@@ -170,7 +170,7 @@ struct DensityLevels
|
||||
}
|
||||
|
||||
double ratio;
|
||||
TpmsAdaptiveGradient gradient;
|
||||
AdaptiveInfillGradient gradient;
|
||||
int count;
|
||||
};
|
||||
|
||||
@@ -212,12 +212,12 @@ private:
|
||||
|
||||
} // namespace
|
||||
|
||||
TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
|
||||
AdaptiveInfillField::AdaptiveInfillField(const std::vector<Slice> &slices, const BoundingBox &bbox, AdaptiveInfillMode mode,
|
||||
const std::function<void()> &throw_if_canceled)
|
||||
: m_mode(mode)
|
||||
, m_axis(mode == TpmsAdaptiveMode::NormalX ? 0 : mode == TpmsAdaptiveMode::NormalY ? 1 : mode == TpmsAdaptiveMode::NormalZ ? 2 : -1)
|
||||
, m_axis(mode == AdaptiveInfillMode::NormalX ? 0 : mode == AdaptiveInfillMode::NormalY ? 1 : mode == AdaptiveInfillMode::NormalZ ? 2 : -1)
|
||||
{
|
||||
assert(!slices.empty() && mode != TpmsAdaptiveMode::Disabled);
|
||||
assert(!slices.empty() && mode != AdaptiveInfillMode::Disabled);
|
||||
const Vec3d min(unscaled(bbox.min.x()), unscaled(bbox.min.y()), slices.front().bottom_z);
|
||||
const Vec3d extent = Vec3d(unscaled(bbox.max.x()), unscaled(bbox.max.y()), slices.back().top_z) - min;
|
||||
|
||||
@@ -269,7 +269,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
|
||||
// Bodies are the connected inside nodes, none of which is on the border. The deepest nodes of a body are the
|
||||
// centers of its lobes, unless the depth between them stays above NeckRatio; where the depth ties, the center
|
||||
// is the node nearest to the middle of the tied nodes.
|
||||
const bool lobes = m_mode != TpmsAdaptiveMode::SteppedShells && m_mode != TpmsAdaptiveMode::SmoothBlend;
|
||||
const bool lobes = m_mode != AdaptiveInfillMode::SteppedShells && m_mode != AdaptiveInfillMode::SmoothBlend;
|
||||
m_body.assign(depth.size(), -1);
|
||||
std::vector<size_t> body_nodes;
|
||||
for (size_t seed = 0; seed < depth.size(); ++seed) {
|
||||
@@ -341,7 +341,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
|
||||
}
|
||||
throw_if_canceled();
|
||||
|
||||
if (m_mode == TpmsAdaptiveMode::SteppedShells || m_mode == TpmsAdaptiveMode::SmoothBlend || m_mode == TpmsAdaptiveMode::DistanceWarp) {
|
||||
if (m_mode == AdaptiveInfillMode::SteppedShells || m_mode == AdaptiveInfillMode::SmoothBlend || m_mode == AdaptiveInfillMode::DistanceWarp) {
|
||||
m_depth.assign(depth.size(), 0.f);
|
||||
for (size_t i = 0; i < depth.size(); ++i)
|
||||
if (m_body[i] >= 0)
|
||||
@@ -421,7 +421,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
|
||||
lobe.reach.resize(log_reach.size());
|
||||
std::transform(log_reach.begin(), log_reach.end(), lobe.reach.begin(), [](double v) { return float(std::exp(v)); });
|
||||
|
||||
if (m_mode == TpmsAdaptiveMode::DistanceWarp) {
|
||||
if (m_mode == AdaptiveInfillMode::DistanceWarp) {
|
||||
std::vector<double> mean(log_reach.size() * (Samples + 1));
|
||||
for (size_t d = 0; d < log_reach.size(); ++d) {
|
||||
const Vec3d dir = direction(int(d) / Azimuth, int(d) % Azimuth);
|
||||
@@ -488,7 +488,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
|
||||
}
|
||||
}
|
||||
|
||||
double TpmsRadialField::depth(const Vec3d &pt) const
|
||||
double AdaptiveInfillField::depth(const Vec3d &pt) const
|
||||
{
|
||||
assert(!m_depth.empty());
|
||||
const Vec3d f = (pt - m_origin) / m_cell;
|
||||
@@ -508,7 +508,7 @@ double TpmsRadialField::depth(const Vec3d &pt) const
|
||||
return value;
|
||||
}
|
||||
|
||||
Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d ¢er) const
|
||||
Vec3d AdaptiveInfillField::offset(const Vec3d &pt, const Vec3d ¢er) const
|
||||
{
|
||||
Vec3d d = pt - center;
|
||||
if (m_axis >= 0)
|
||||
@@ -516,7 +516,7 @@ Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d ¢er) const
|
||||
return d;
|
||||
}
|
||||
|
||||
double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const
|
||||
double AdaptiveInfillField::radial(const Lobe &lobe, const Vec3d &pt) const
|
||||
{
|
||||
const Vec3d d = this->offset(pt, lobe.center);
|
||||
const double r = d.norm();
|
||||
@@ -562,7 +562,7 @@ double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const
|
||||
return warp(mean);
|
||||
}
|
||||
|
||||
size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const
|
||||
size_t AdaptiveInfillField::radial(const Vec3d &pt, Radials &out) const
|
||||
{
|
||||
assert(!this->empty());
|
||||
Vec3i32 idx;
|
||||
@@ -588,7 +588,7 @@ size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const
|
||||
return count;
|
||||
}
|
||||
|
||||
size_t TpmsRadialField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const
|
||||
size_t AdaptiveInfillField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const
|
||||
{
|
||||
const Body &body = m_bodies[m_body[node]];
|
||||
if (body.lobes == 1) {
|
||||
@@ -641,7 +641,7 @@ struct AdaptiveTpmsField
|
||||
const long gsize = std::lround(gsizef / rsizef);
|
||||
|
||||
const AdaptiveTpms &tpms;
|
||||
const TpmsRadialField &radial_field;
|
||||
const AdaptiveInfillField &radial_field;
|
||||
RadialScale scale;
|
||||
DensityLevels levels;
|
||||
Point size;
|
||||
@@ -650,7 +650,7 @@ struct AdaptiveTpmsField
|
||||
double cos_angle;
|
||||
double sin_angle;
|
||||
|
||||
AdaptiveTpmsField(const AdaptiveTpms &tpms, const TpmsRadialField &radial_field, const BoundingBox &bbox, coordf_t z, float angle)
|
||||
AdaptiveTpmsField(const AdaptiveTpms &tpms, const AdaptiveInfillField &radial_field, const BoundingBox &bbox, coordf_t z, float angle)
|
||||
: tpms(tpms), radial_field(radial_field), scale(tpms, radial_field.axis() < 0 ? 3 : 2)
|
||||
, levels(std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3), BlendRatio, tpms.gradient)
|
||||
, size(bbox.size()), offs(bbox.min), z(z)
|
||||
@@ -666,7 +666,7 @@ struct AdaptiveTpmsField
|
||||
const double x = unscaled(pt.x());
|
||||
const double y = unscaled(pt.y());
|
||||
const Vec3d obj(cos_angle * x - sin_angle * y, sin_angle * x + cos_angle * y, z);
|
||||
if (radial_field.mode() == TpmsAdaptiveMode::SmoothBlend) {
|
||||
if (radial_field.mode() == AdaptiveInfillMode::SmoothBlend) {
|
||||
// The regular patterns of the two levels around the target of the depth, blended by a smoothstep.
|
||||
auto lattice = [this, x, y](int level) {
|
||||
const double frequency = tpms.surface_frequency * levels.scale(level);
|
||||
@@ -681,7 +681,7 @@ struct AdaptiveTpmsField
|
||||
return float((w < 1. ? (1. - w) * lattice(k) : 0.) + (w > 0. ? w * lattice(k + 1) : 0.));
|
||||
}
|
||||
const int axis = radial_field.axis();
|
||||
TpmsRadialField::Radials radials;
|
||||
AdaptiveInfillField::Radials radials;
|
||||
const size_t count = radial_field.radial(obj, radials);
|
||||
float value = 0.f;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
@@ -708,20 +708,20 @@ template<> struct _RasterTraits<AdaptiveTpmsField>
|
||||
};
|
||||
|
||||
// Continuous density level of the depth over a layer, in the object frame.
|
||||
struct TpmsLevelField
|
||||
struct AdaptiveLevelField
|
||||
{
|
||||
static constexpr float gsizef = 0.5f;
|
||||
static constexpr float rsizef = 0.05f;
|
||||
const coord_t rsize = scaled(rsizef);
|
||||
const long gsize = std::lround(gsizef / rsizef);
|
||||
|
||||
const TpmsRadialField &field;
|
||||
const AdaptiveInfillField &field;
|
||||
const DensityLevels &levels;
|
||||
Point size;
|
||||
Point offs;
|
||||
double z;
|
||||
|
||||
TpmsLevelField(const TpmsRadialField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
|
||||
AdaptiveLevelField(const AdaptiveInfillField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
|
||||
: field(field), levels(levels), size(bbox.size()), offs(bbox.min), z(z)
|
||||
{}
|
||||
|
||||
@@ -736,19 +736,19 @@ struct TpmsLevelField
|
||||
inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); }
|
||||
};
|
||||
|
||||
template<> struct _RasterTraits<TpmsLevelField>
|
||||
template<> struct _RasterTraits<AdaptiveLevelField>
|
||||
{
|
||||
using ValueType = float;
|
||||
static float get(const TpmsLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
|
||||
static size_t rows(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.y()); }
|
||||
static size_t cols(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.x()); }
|
||||
static float get(const AdaptiveLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
|
||||
static size_t rows(const AdaptiveLevelField &sf) { return sf.to_coordr(sf.size.y()); }
|
||||
static size_t cols(const AdaptiveLevelField &sf) { return sf.to_coordr(sf.size.x()); }
|
||||
};
|
||||
|
||||
} // namespace marchsq
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
|
||||
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField &field, BoundingBox bbox,
|
||||
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle)
|
||||
{
|
||||
// A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region.
|
||||
@@ -775,21 +775,21 @@ Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &fi
|
||||
return polylines;
|
||||
}
|
||||
|
||||
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
|
||||
float surface_density, float interior_density, TpmsAdaptiveGradient gradient)
|
||||
std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z,
|
||||
float surface_density, float interior_density, AdaptiveInfillGradient gradient)
|
||||
{
|
||||
const DensityLevels levels(interior_density / surface_density, ShellRatio, gradient);
|
||||
if (levels.count == 0)
|
||||
return {{surface_density, {expolygon}}};
|
||||
// A fixed sampling grid, so that every region of a layer gets the same shells.
|
||||
const coord_t cell = scaled(marchsq::TpmsLevelField::gsizef);
|
||||
const coord_t cell = scaled(marchsq::AdaptiveLevelField::gsizef);
|
||||
BoundingBox bbox = get_extents(expolygon);
|
||||
bbox.offset(cell);
|
||||
bbox.merge(align_to_grid(bbox.min, Point(cell, cell)));
|
||||
const marchsq::TpmsLevelField raster(field, levels, bbox, z);
|
||||
const marchsq::AdaptiveLevelField raster(field, levels, bbox, z);
|
||||
|
||||
// Each level takes the part deeper than the middle between it and the previous one.
|
||||
std::vector<TpmsShell> shells;
|
||||
std::vector<AdaptiveInfillShell> shells;
|
||||
ExPolygons remaining{expolygon};
|
||||
for (int level = 0; level < levels.count && !remaining.empty(); ++level) {
|
||||
Polygons deeper;
|
||||
@@ -808,12 +808,12 @@ std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPo
|
||||
return shells;
|
||||
}
|
||||
|
||||
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, coordf_t spacing,
|
||||
void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, coordf_t spacing,
|
||||
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell)
|
||||
{
|
||||
FillParams shell_params = params;
|
||||
shell_params.tpms_adaptive = TpmsAdaptiveMode::Disabled;
|
||||
for (const TpmsShell &shell : make_tpms_shells(field, expolygon, z, params.density, params.tpms_interior_density, params.tpms_adaptive_gradient)) {
|
||||
shell_params.adaptive_infill_mode = AdaptiveInfillMode::Disabled;
|
||||
for (const AdaptiveInfillShell &shell : make_adaptive_infill_shells(field, expolygon, z, params.density, params.adaptive_infill_interior_density, params.adaptive_infill_gradient)) {
|
||||
shell_params.density = shell.density;
|
||||
for (const ExPolygon &part : offset_ex(shell.expolygons, -float(scale_(0.5 * spacing))))
|
||||
fill_shell(shell_params, part);
|
||||
+12
-12
@@ -21,7 +21,7 @@ namespace Slic3r {
|
||||
// lobe, 1 at its surface. Lobes are parts of a body separated by a neck, like two spheres united. In the 2D modes,
|
||||
// every section normal to the axis has its own bodies and lobes, and distances are measured within the section. The
|
||||
// modes following the distance to the surface use the depth of every point instead; Distance warp also the lobes.
|
||||
class TpmsRadialField
|
||||
class AdaptiveInfillField
|
||||
{
|
||||
public:
|
||||
struct Slice
|
||||
@@ -39,7 +39,7 @@ public:
|
||||
};
|
||||
|
||||
// Slices sorted by z, in the XY coordinates of the fill and the print Z.
|
||||
TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
|
||||
AdaptiveInfillField(const std::vector<Slice> &slices, const BoundingBox &bbox, AdaptiveInfillMode mode,
|
||||
const std::function<void()> &throw_if_canceled);
|
||||
|
||||
// Lobes blended near the sides between them, from a body or from each of the two sections around a point.
|
||||
@@ -56,7 +56,7 @@ public:
|
||||
|
||||
// Axis normal to the sections in the 2D modes, -1 in the modes graded in 3D.
|
||||
int axis() const { return m_axis; }
|
||||
TpmsAdaptiveMode mode() const { return m_mode; }
|
||||
AdaptiveInfillMode mode() const { return m_mode; }
|
||||
|
||||
// In the modes following the distance to the surface: depth relative to the deepest point of the body, from 0 at
|
||||
// the surface to 1.
|
||||
@@ -91,7 +91,7 @@ private:
|
||||
static constexpr int Polar = 24;
|
||||
static constexpr int Azimuth = 48;
|
||||
|
||||
TpmsAdaptiveMode m_mode;
|
||||
AdaptiveInfillMode m_mode;
|
||||
int m_axis;
|
||||
Vec3d m_origin;
|
||||
double m_cell;
|
||||
@@ -106,9 +106,9 @@ private:
|
||||
std::vector<float> m_depth;
|
||||
};
|
||||
|
||||
using TpmsRadialFieldPtr = std::unique_ptr<TpmsRadialField>;
|
||||
using AdaptiveInfillFieldPtr = std::unique_ptr<AdaptiveInfillField>;
|
||||
// A field for every adaptive mode in use, indexed by the mode.
|
||||
using TpmsRadialFields = std::array<TpmsRadialFieldPtr, size_t(TpmsAdaptiveMode::Count)>;
|
||||
using AdaptiveInfillFields = std::array<AdaptiveInfillFieldPtr, size_t(AdaptiveInfillMode::Count)>;
|
||||
|
||||
struct AdaptiveTpms
|
||||
{
|
||||
@@ -117,14 +117,14 @@ struct AdaptiveTpms
|
||||
// Pattern frequencies at the surface and at the center, in radians per mm.
|
||||
double surface_frequency;
|
||||
double interior_frequency;
|
||||
TpmsAdaptiveGradient gradient;
|
||||
AdaptiveInfillGradient gradient;
|
||||
};
|
||||
|
||||
// Infill lines in the fill frame, the object frame rotated by -angle; z is the print_z of the layer.
|
||||
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
|
||||
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField &field, BoundingBox bbox,
|
||||
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle);
|
||||
|
||||
struct TpmsShell
|
||||
struct AdaptiveInfillShell
|
||||
{
|
||||
float density;
|
||||
ExPolygons expolygons;
|
||||
@@ -132,12 +132,12 @@ struct TpmsShell
|
||||
|
||||
// Stepped shells: the parts of an expolygon in the object frame at each density, from the surface inwards; z is the
|
||||
// middle of the layer.
|
||||
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
|
||||
float surface_density, float interior_density, TpmsAdaptiveGradient gradient);
|
||||
std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z,
|
||||
float surface_density, float interior_density, AdaptiveInfillGradient gradient);
|
||||
|
||||
// Stepped shells: fills every shell with fill_shell at its density, each shrunk by half a line like a filled region,
|
||||
// so the lines connected along the boundaries of two shells don't overlap.
|
||||
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, coordf_t spacing,
|
||||
void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, coordf_t spacing,
|
||||
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell);
|
||||
|
||||
} // namespace Slic3r
|
||||
+23
-23
@@ -312,9 +312,9 @@ struct SurfaceFillParams
|
||||
bool gyroid_optimized = false;
|
||||
|
||||
// For TPMS: grade the density from the surface to the interior of the object.
|
||||
TpmsAdaptiveMode tpms_adaptive = TpmsAdaptiveMode::Disabled;
|
||||
float tpms_interior_density = 0.f;
|
||||
TpmsAdaptiveGradient tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear;
|
||||
AdaptiveInfillMode adaptive_infill_mode = AdaptiveInfillMode::Disabled;
|
||||
float adaptive_infill_interior_density = 0.f;
|
||||
AdaptiveInfillGradient adaptive_infill_gradient = AdaptiveInfillGradient::Linear;
|
||||
|
||||
// Orca: corner smoothing factor in the range [0, 1].
|
||||
double smooth_factor { 0. };
|
||||
@@ -358,9 +358,9 @@ struct SurfaceFillParams
|
||||
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
|
||||
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
|
||||
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
|
||||
RETURN_COMPARE_NON_EQUAL(tpms_adaptive);
|
||||
RETURN_COMPARE_NON_EQUAL(tpms_interior_density);
|
||||
RETURN_COMPARE_NON_EQUAL(tpms_adaptive_gradient);
|
||||
RETURN_COMPARE_NON_EQUAL(adaptive_infill_mode);
|
||||
RETURN_COMPARE_NON_EQUAL(adaptive_infill_interior_density);
|
||||
RETURN_COMPARE_NON_EQUAL(adaptive_infill_gradient);
|
||||
RETURN_COMPARE_NON_EQUAL(smooth_factor);
|
||||
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
|
||||
RETURN_COMPARE_NON_EQUAL(separated_infills);
|
||||
@@ -394,9 +394,9 @@ struct SurfaceFillParams
|
||||
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
|
||||
this->separated_infills == rhs.separated_infills &&
|
||||
this->gyroid_optimized == rhs.gyroid_optimized &&
|
||||
this->tpms_adaptive == rhs.tpms_adaptive &&
|
||||
this->tpms_interior_density == rhs.tpms_interior_density &&
|
||||
this->tpms_adaptive_gradient == rhs.tpms_adaptive_gradient &&
|
||||
this->adaptive_infill_mode == rhs.adaptive_infill_mode &&
|
||||
this->adaptive_infill_interior_density == rhs.adaptive_infill_interior_density &&
|
||||
this->adaptive_infill_gradient == rhs.adaptive_infill_gradient &&
|
||||
this->smooth_factor == rhs.smooth_factor &&
|
||||
this->fill_order == rhs.fill_order;
|
||||
}
|
||||
@@ -1010,17 +1010,17 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
||||
params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface;
|
||||
|
||||
// Orca: only the TPMS sparse infill is graded; reset otherwise, as params is reused.
|
||||
params.tpms_adaptive = is_tpms_adaptive_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
|
||||
region_config.tpms_adaptive.value : TpmsAdaptiveMode::Disabled;
|
||||
const bool tpms_adaptive = params.tpms_adaptive != TpmsAdaptiveMode::Disabled;
|
||||
params.tpms_interior_density = tpms_adaptive ? std::max(1.f, float(region_config.tpms_interior_density)) : 0.f;
|
||||
params.tpms_adaptive_gradient = tpms_adaptive ? region_config.tpms_adaptive_gradient.value : TpmsAdaptiveGradient::Linear;
|
||||
params.adaptive_infill_mode = is_adaptive_infill_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
|
||||
region_config.adaptive_infill_mode.value : AdaptiveInfillMode::Disabled;
|
||||
const bool adaptive_infill_mode = params.adaptive_infill_mode != AdaptiveInfillMode::Disabled;
|
||||
params.adaptive_infill_interior_density = adaptive_infill_mode ? std::max(1.f, float(region_config.adaptive_infill_interior_density)) : 0.f;
|
||||
params.adaptive_infill_gradient = adaptive_infill_mode ? region_config.adaptive_infill_gradient.value : AdaptiveInfillGradient::Linear;
|
||||
|
||||
// Pass through gyroid_optimized only when the effective pattern is Gyroid,
|
||||
// so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag
|
||||
// (which would unnecessarily split fill batching). Adaptive density replaces it.
|
||||
// Stored on SurfaceFillParams; copied to FillParams during conversion.
|
||||
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !tpms_adaptive;
|
||||
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !adaptive_infill_mode;
|
||||
|
||||
if (params.extrusion_role == erInternalInfill) {
|
||||
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
|
||||
@@ -1373,7 +1373,7 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
|
||||
f->angle = surface_fill.params.angle;
|
||||
f->fixed_angle = surface_fill.params.fixed_angle;
|
||||
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
|
||||
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
|
||||
f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
|
||||
f->print_config = &this->object()->print()->config();
|
||||
f->print_object_config = &this->object()->config();
|
||||
if (surface_fill.params.pattern == ipConcentricInternal) {
|
||||
@@ -1423,9 +1423,9 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
|
||||
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
|
||||
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
|
||||
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
|
||||
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
|
||||
params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
|
||||
params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
|
||||
params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
|
||||
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||
|
||||
// BBS
|
||||
@@ -1598,7 +1598,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
|
||||
f->angle = surface_fill.params.angle;
|
||||
f->fixed_angle = surface_fill.params.fixed_angle;
|
||||
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
|
||||
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
|
||||
f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
|
||||
f->print_config = &this->object()->print()->config();
|
||||
f->print_object_config = &this->object()->config();
|
||||
|
||||
@@ -1638,9 +1638,9 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
|
||||
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
||||
params.multiline = surface_fill.params.multiline;
|
||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
|
||||
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
|
||||
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
|
||||
params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
|
||||
params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
|
||||
params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
|
||||
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||
// Orca: Match make_fills() when choosing the origin of plane-path patterns.
|
||||
// Without the sparse extrusion role, the filler uses each surface's bounds
|
||||
|
||||
+256
-13
@@ -11,6 +11,10 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <tuple>
|
||||
#include "../AABBTreeLines.hpp"
|
||||
#include "../ClipperUtils.hpp"
|
||||
#include "../EdgeGrid.hpp"
|
||||
#include "../Geometry.hpp"
|
||||
@@ -206,6 +210,9 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para
|
||||
thick_polylines.clear();
|
||||
}
|
||||
else {
|
||||
// Orca: sparse infill lines whose ends are next to each other print as one path, without a travel between them.
|
||||
if (params.extrusion_role == erInternalInfill && ! eec->no_sort)
|
||||
join_close_ends(polylines, scale_(2. * this->spacing), params.can_reverse);
|
||||
extrusion_entities_append_paths(
|
||||
eec->entities, std::move(polylines),
|
||||
params.extrusion_role,
|
||||
@@ -1644,6 +1651,8 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
};
|
||||
|
||||
const double line_half_width = 0.5 * scale_(spacing);
|
||||
// Two ends whose anchors would meet are joined instead, with about as much wall.
|
||||
const double join_length = anchor_length > SCALED_EPSILON ? std::max(anchor_length_max, 2. * (anchor_length + line_half_width)) : anchor_length_max;
|
||||
|
||||
#if 0
|
||||
// Connection from end of one infill line to the start of another infill line.
|
||||
@@ -1720,6 +1729,10 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
}
|
||||
#endif
|
||||
|
||||
// Line ends not yet joined to another line along the contour.
|
||||
std::vector<char> open_end(graph.map_infill_end_point_to_boundary.size(), 1);
|
||||
auto end_index = [&graph](const ContourIntersectionPoint *cp) { return size_t(cp - graph.map_infill_end_point_to_boundary.data()); };
|
||||
|
||||
struct Arc {
|
||||
ContourIntersectionPoint *intersection;
|
||||
double arc_length;
|
||||
@@ -1731,6 +1744,81 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (cp.contour_idx != boundary_idx_unconnected && cp.next_on_contour != &cp && cp.could_connect_next())
|
||||
arches.push_back({ &cp, path_length_along_contour_ccw(&cp, cp.next_on_contour, graph.boundary_params[cp.contour_idx].back()) });
|
||||
std::sort(arches.begin(), arches.end(), [](const auto& l, const auto& r) { return l.arc_length < r.arc_length; });
|
||||
|
||||
// Lines the arcs would join in this order.
|
||||
auto count_joins = [&](const std::vector<Arc> &order) {
|
||||
std::vector<char> consumed(graph.map_infill_end_point_to_boundary.size());
|
||||
for (size_t i = 0; i < consumed.size(); ++ i)
|
||||
consumed[i] = graph.map_infill_end_point_to_boundary[i].consumed;
|
||||
std::vector<size_t> parent(infill_ordered.size());
|
||||
std::iota(parent.begin(), parent.end(), 0);
|
||||
auto find = [&parent](size_t i) {
|
||||
while (parent[i] != i)
|
||||
i = parent[i] = parent[parent[i]];
|
||||
return i;
|
||||
};
|
||||
size_t joins = 0;
|
||||
for (const Arc &arc : order) {
|
||||
const size_t end1 = end_index(arc.intersection), end2 = end_index(arc.intersection->next_on_contour);
|
||||
if (consumed[end1] || consumed[end2] || (params.multiline > 1 && arc.arc_length < scale_(spacing) * params.multiline))
|
||||
continue;
|
||||
const size_t polyline1 = find(end1 / 2), polyline2 = find(end2 / 2);
|
||||
if (polyline1 == polyline2)
|
||||
continue;
|
||||
if (arc.arc_length < join_length) {
|
||||
parent[polyline2] = polyline1;
|
||||
++ joins;
|
||||
} else if (anchor_length <= SCALED_EPSILON)
|
||||
continue;
|
||||
consumed[end1] = consumed[end2] = 1;
|
||||
}
|
||||
// The last pass joins the open ends next to each other, also those of a multiline group.
|
||||
for (size_t end = 0; end < consumed.size(); ++ end) {
|
||||
const ContourIntersectionPoint &cp = graph.map_infill_end_point_to_boundary[end];
|
||||
if (consumed[end] || cp.contour_idx == boundary_idx_unconnected)
|
||||
continue;
|
||||
const double length = graph.boundary_params[cp.contour_idx].back();
|
||||
auto open = [&](const ContourIntersectionPoint *other, bool trimmed) {
|
||||
return other != &cp && ! consumed[end_index(other)] && ! trimmed;
|
||||
};
|
||||
const double lprev = open(cp.prev_on_contour, cp.prev_trimmed || cp.prev_on_contour->next_trimmed) ?
|
||||
path_length_along_contour_ccw(cp.prev_on_contour, &cp, length) : std::numeric_limits<double>::max();
|
||||
const double lnext = open(cp.next_on_contour, cp.next_trimmed || cp.next_on_contour->prev_trimmed) ?
|
||||
path_length_along_contour_ccw(&cp, cp.next_on_contour, length) : std::numeric_limits<double>::max();
|
||||
consumed[end] = anchor_length > SCALED_EPSILON;
|
||||
for (const double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
|
||||
if (l > join_length)
|
||||
break;
|
||||
const size_t other = end_index(l == lprev ? cp.prev_on_contour : cp.next_on_contour);
|
||||
const size_t polyline1 = find(end / 2), polyline2 = find(other / 2);
|
||||
if (polyline1 == polyline2)
|
||||
continue;
|
||||
parent[polyline2] = polyline1;
|
||||
consumed[end] = consumed[other] = 1;
|
||||
++ joins;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return joins;
|
||||
};
|
||||
// Equal arcs taken along the contour pair a wall in one phase; keep the order that joins the most lines.
|
||||
size_t best_joins = count_joins(arches);
|
||||
const double bucket = 0.5 * scale_(spacing);
|
||||
for (const bool backwards : { false, true }) {
|
||||
std::vector<Arc> along_contour = arches;
|
||||
std::sort(along_contour.begin(), along_contour.end(), [bucket, backwards](const Arc &l, const Arc &r) {
|
||||
const long long bucket_l = std::llround(l.arc_length / bucket), bucket_r = std::llround(r.arc_length / bucket);
|
||||
if (bucket_l != bucket_r)
|
||||
return bucket_l < bucket_r;
|
||||
if (l.intersection->contour_idx != r.intersection->contour_idx)
|
||||
return l.intersection->contour_idx < r.intersection->contour_idx;
|
||||
return backwards ? l.intersection->param > r.intersection->param : l.intersection->param < r.intersection->param;
|
||||
});
|
||||
if (const size_t joins = count_joins(along_contour); joins > best_joins) {
|
||||
best_joins = joins;
|
||||
arches = std::move(along_contour);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//FIXME improve the Traveling Salesman problem with 2-opt and 3-opt local optimization.
|
||||
@@ -1752,7 +1840,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (polyline_idx1 != polyline_idx2) {
|
||||
Polyline &polyline1 = infill_ordered[polyline_idx1];
|
||||
Polyline &polyline2 = infill_ordered[polyline_idx2];
|
||||
if (arc.arc_length < anchor_length_max) {
|
||||
if (arc.arc_length < join_length) {
|
||||
// Not closing a loop, connecting the lines.
|
||||
assert(contour[cp1->point_idx] == polyline1.points.front() || contour[cp1->point_idx] == polyline1.points.back());
|
||||
if (contour[cp1->point_idx] == polyline1.points.front())
|
||||
@@ -1761,6 +1849,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (contour[cp2->point_idx] == polyline2.points.back())
|
||||
polyline2.reverse();
|
||||
take(polyline1, polyline2, contour, cp1, cp2, false);
|
||||
open_end[end_index(cp1)] = open_end[end_index(cp2)] = 0;
|
||||
// Mark the second polygon as merged with the first one.
|
||||
if (polyline_idx2 < polyline_idx1) {
|
||||
polyline2 = std::move(polyline1);
|
||||
@@ -1778,6 +1867,23 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
}
|
||||
}
|
||||
|
||||
// Distance along the contour to the nearest open end of another polyline, within 64 ends.
|
||||
auto distance_to_open_end = [&](const ContourIntersectionPoint &from, size_t polyline_idx, bool backwards) {
|
||||
const double length = graph.boundary_params[from.contour_idx].back();
|
||||
double distance = 0.;
|
||||
const ContourIntersectionPoint *cp = &from;
|
||||
for (size_t step = 0; step < 64; ++ step) {
|
||||
const ContourIntersectionPoint *other = backwards ? cp->prev_on_contour : cp->next_on_contour;
|
||||
if (other == nullptr || other == &from)
|
||||
break;
|
||||
distance += backwards ? closed_contour_distance_cw(cp->param, other->param, length) : closed_contour_distance_ccw(cp->param, other->param, length);
|
||||
if (open_end[end_index(other)] && get_and_update_merged_with(end_index(other) / 2) != polyline_idx)
|
||||
return distance;
|
||||
cp = other;
|
||||
}
|
||||
return std::numeric_limits<double>::max();
|
||||
};
|
||||
|
||||
// Connect the remaining open infill lines to the perimeter lines if possible.
|
||||
for (ContourIntersectionPoint &contour_point : graph.map_infill_end_point_to_boundary)
|
||||
if (! contour_point.consumed && contour_point.contour_idx != boundary_idx_unconnected) {
|
||||
@@ -1796,7 +1902,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
assert(contour[contour_point.point_idx] == polyline.points.front() || contour[contour_point.point_idx] == polyline.points.back());
|
||||
bool connected = false;
|
||||
for (double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
|
||||
if (l == std::numeric_limits<double>::max() || l > anchor_length_max)
|
||||
if (l == std::numeric_limits<double>::max() || l > join_length)
|
||||
break;
|
||||
// Take the complete contour.
|
||||
bool reversed = l == lprev;
|
||||
@@ -1815,6 +1921,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (contour[cp2->point_idx] == polyline2.points.back())
|
||||
polyline2.reverse();
|
||||
take(polyline, polyline2, contour, &contour_point, cp2, reversed);
|
||||
open_end[end_index(&contour_point)] = open_end[end_index(cp2)] = 0;
|
||||
if (polyline_idx < polyline_idx2) {
|
||||
// Mark the second polyline as merged with the first one.
|
||||
merged_with[polyline_idx2] = polyline_idx;
|
||||
@@ -1829,18 +1936,35 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
break;
|
||||
}
|
||||
if (! connected && anchor_length > SCALED_EPSILON) {
|
||||
// Which to take? One could optimize for:
|
||||
// 1) Shortest path
|
||||
// 2) Hook length
|
||||
// ...
|
||||
// Let's take the longer now, as this improves the chance of another hook to be placed on the other side of this contour point.
|
||||
double l = std::max(contour_point.contour_not_taken_length_prev, contour_point.contour_not_taken_length_next);
|
||||
if (l > SCALED_EPSILON) {
|
||||
if (contour_point.contour_not_taken_length_prev > contour_point.contour_not_taken_length_next)
|
||||
take_limited(polyline, contour, contour_params, &contour_point, contour_point.prev_on_contour, true, anchor_length, line_half_width);
|
||||
else
|
||||
take_limited(polyline, contour, contour_params, &contour_point, contour_point.next_on_contour, false, anchor_length, line_half_width);
|
||||
// Hook toward the nearest open end of another polyline, where the next path likely starts.
|
||||
const double free_prev = contour_point.contour_not_taken_length_prev;
|
||||
const double free_next = contour_point.contour_not_taken_length_next;
|
||||
const double to_prev = distance_to_open_end(contour_point, polyline_idx, true);
|
||||
const double to_next = distance_to_open_end(contour_point, polyline_idx, false);
|
||||
bool backwards = free_prev > free_next;
|
||||
if (to_prev < to_next && free_prev > SCALED_EPSILON)
|
||||
backwards = true;
|
||||
else if (to_next < to_prev && free_next > SCALED_EPSILON)
|
||||
backwards = false;
|
||||
// Orca: with multiline, a hook toward a copy stops short of it; the copy takes half a width over the sine of its wall angle.
|
||||
double length = anchor_length;
|
||||
if (const ContourIntersectionPoint *other = backwards ? contour_point.prev_on_contour : contour_point.next_on_contour;
|
||||
params.multiline > 1 && other != &contour_point) {
|
||||
const Point &end = contour[contour_point.point_idx];
|
||||
Vec2d dir = Vec2d::Zero();
|
||||
for (size_t i = 1; i < polyline.size() && dir.squaredNorm() < sqr(2. * scale_(spacing) * params.multiline); ++ i)
|
||||
dir = (end - polyline.points[polyline.points.front() == end ? i : polyline.size() - 1 - i]).cast<double>();
|
||||
const Vec2d to_copy = (contour[other->point_idx] - end).cast<double>();
|
||||
const double across = dir.squaredNorm() > 0. ? std::abs(cross2(dir.normalized(), to_copy)) : 0.;
|
||||
if (across > SCALED_EPSILON && across < scale_(spacing) * params.multiline) {
|
||||
const double gap = backwards ? path_length_along_contour_ccw(other, &contour_point, contour_params.back()) :
|
||||
path_length_along_contour_ccw(&contour_point, other, contour_params.back());
|
||||
length = std::min(length, gap - line_half_width * (1. + to_copy.norm() / across));
|
||||
}
|
||||
}
|
||||
if ((backwards ? free_prev : free_next) > SCALED_EPSILON)
|
||||
take_limited(polyline, contour, contour_params, &contour_point, backwards ? contour_point.prev_on_contour : contour_point.next_on_contour,
|
||||
backwards, std::max(0., length), line_half_width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2798,4 +2922,123 @@ void multiline_fill(Polylines& polylines, const FillParams& params, float spacin
|
||||
polylines = std::move(all_polylines);
|
||||
}
|
||||
|
||||
void join_close_ends(Polylines &polylines, double max_gap, bool can_reverse)
|
||||
{
|
||||
const size_t n = polylines.size();
|
||||
if (n < 2 || max_gap <= 0.)
|
||||
return;
|
||||
static constexpr size_t npos = size_t(-1);
|
||||
// End 2 * i is the first point of polyline i, 2 * i + 1 its last.
|
||||
auto end_point = [&polylines](size_t e) -> const Point & { return (e & 1) ? polylines[e >> 1].points.back() : polylines[e >> 1].points.front(); };
|
||||
// Ends in cells of max_gap, so the ends within max_gap of one are in its cell or the eight around it.
|
||||
auto cell_of = [max_gap](const Point &p) { return Vec2i64(int64_t(std::floor(p.x() / max_gap)), int64_t(std::floor(p.y() / max_gap))); };
|
||||
auto key = [](const Vec2i64 &c) { return (uint64_t(c.x()) << 32) ^ uint64_t(uint32_t(c.y())); };
|
||||
std::unordered_map<uint64_t, std::vector<size_t>> grid;
|
||||
for (size_t e = 0; e < 2 * n; ++ e)
|
||||
if (polylines[e >> 1].size() >= 2)
|
||||
grid[key(cell_of(end_point(e)))].push_back(e);
|
||||
|
||||
// The end of a polyline that comes last along its overall direction, taken along the dominant axis.
|
||||
std::vector<char> backwards(n, 0);
|
||||
for (size_t i = 0; i < n; ++ i)
|
||||
if (polylines[i].size() >= 2) {
|
||||
const Vec2d dir = (polylines[i].points.back() - polylines[i].points.front()).cast<double>();
|
||||
backwards[i] = std::abs(dir.x()) >= std::abs(dir.y()) ? dir.x() < 0. : dir.y() < 0.;
|
||||
}
|
||||
auto trailing = [&backwards](size_t e) { return bool(e & 1) != bool(backwards[e >> 1]); };
|
||||
// Ends that meet first, then shortest gaps between leading ends, then the others: the copies of neighboring multiline
|
||||
// groups are all joined at the same side, which leaves their open ends next to each other rather than on alternating walls.
|
||||
std::vector<std::tuple<bool, bool, double, size_t, size_t>> gaps;
|
||||
for (const auto &[cell_key, ends] : grid) {
|
||||
const Vec2i64 c = cell_of(end_point(ends.front()));
|
||||
for (int64_t dx = -1; dx <= 1; ++ dx)
|
||||
for (int64_t dy = -1; dy <= 1; ++ dy)
|
||||
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
|
||||
for (size_t a : ends)
|
||||
for (size_t b : it->second)
|
||||
if (a < b && (a >> 1) != (b >> 1) && (can_reverse || (a & 1) != (b & 1)))
|
||||
if (const double d = (end_point(a) - end_point(b)).cast<double>().norm(); d <= max_gap)
|
||||
gaps.emplace_back(d > SCALED_EPSILON, d > SCALED_EPSILON && (trailing(a) || trailing(b)), d, a, b);
|
||||
}
|
||||
if (gaps.empty())
|
||||
return;
|
||||
std::sort(gaps.begin(), gaps.end());
|
||||
|
||||
std::vector<size_t> partner(2 * n, npos), parent(n);
|
||||
std::iota(parent.begin(), parent.end(), 0);
|
||||
auto find = [&parent](size_t i) {
|
||||
while (parent[i] != i)
|
||||
i = parent[i] = parent[parent[i]];
|
||||
return i;
|
||||
};
|
||||
// A join may not cross a line or another join; touching them at its own ends is fine, as where a line ends on another.
|
||||
// Only the segments through a cell next to a candidate join can cross one.
|
||||
std::unordered_set<uint64_t> near_gaps;
|
||||
for (const auto &[apart, trail, d, a, b] : gaps)
|
||||
for (size_t e : { a, b })
|
||||
for (int64_t dx = -1; dx <= 1; ++ dx)
|
||||
for (int64_t dy = -1; dy <= 1; ++ dy)
|
||||
near_gaps.insert(key(cell_of(end_point(e)) + Vec2i64(dx, dy)));
|
||||
Lines near_lines;
|
||||
for (const Polyline &pl : polylines)
|
||||
for (size_t i = 1; i < pl.size(); ++ i) {
|
||||
const Vec2i64 c0 = cell_of(pl.points[i - 1]), c1 = cell_of(pl.points[i]);
|
||||
const Vec2i64 lo = c0.cwiseMin(c1), hi = c0.cwiseMax(c1);
|
||||
bool near = (hi - lo).maxCoeff() > 3;
|
||||
for (int64_t x = lo.x(); x <= hi.x() && ! near; ++ x)
|
||||
for (int64_t y = lo.y(); y <= hi.y() && ! near; ++ y)
|
||||
near = near_gaps.count(key(Vec2i64(x, y))) > 0;
|
||||
if (near)
|
||||
near_lines.emplace_back(pl.points[i - 1], pl.points[i]);
|
||||
}
|
||||
const AABBTreeLines::LinesDistancer<Line> lines(std::move(near_lines));
|
||||
auto crosses = [&](size_t a, size_t b) {
|
||||
const Vec2d p = end_point(a).cast<double>(), q = end_point(b).cast<double>();
|
||||
const double length = (q - p).norm();
|
||||
if (length <= 2. * SCALED_EPSILON)
|
||||
return false;
|
||||
const Vec2d dir = (q - p) / length;
|
||||
const Line inner(Point((p + SCALED_EPSILON * dir).cast<coord_t>()), Point((q - SCALED_EPSILON * dir).cast<coord_t>()));
|
||||
if (! lines.get_lines().empty() && ! lines.intersections_with_line<false>(inner).empty())
|
||||
return true;
|
||||
const Vec2i64 c = cell_of(end_point(a));
|
||||
for (int64_t dx = -3; dx <= 3; ++ dx)
|
||||
for (int64_t dy = -3; dy <= 3; ++ dy)
|
||||
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
|
||||
for (size_t e : it->second)
|
||||
if (partner[e] != npos && Geometry::segments_intersect(inner.a, inner.b, end_point(e), end_point(partner[e])))
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
for (const auto &[apart, trail, d, a, b] : gaps)
|
||||
if (partner[a] == npos && partner[b] == npos && find(a >> 1) != find(b >> 1) && ! crosses(a, b)) {
|
||||
partner[a] = b;
|
||||
partner[b] = a;
|
||||
parent[find(a >> 1)] = find(b >> 1);
|
||||
}
|
||||
|
||||
// Walk each group from a polyline with a free end, entering the next polyline at the end joined to the last one.
|
||||
Polylines out;
|
||||
std::vector<char> done(n, 0);
|
||||
auto walk = [&](size_t first_end) {
|
||||
Polyline joined;
|
||||
for (size_t e = first_end; e != npos;) {
|
||||
Polyline &pl = polylines[e >> 1];
|
||||
done[e >> 1] = 1;
|
||||
if (e & 1)
|
||||
pl.reverse();
|
||||
joined.points.insert(joined.points.end(), pl.points.begin() + (! joined.empty() && joined.points.back() == pl.points.front()), pl.points.end());
|
||||
e = partner[e ^ 1];
|
||||
}
|
||||
out.emplace_back(std::move(joined));
|
||||
};
|
||||
for (size_t i = 0; i < n; ++ i)
|
||||
if (! done[i] && partner[2 * i] == npos)
|
||||
walk(2 * i);
|
||||
for (size_t i = 0; i < n; ++ i)
|
||||
if (! done[i] && partner[2 * i + 1] == npos)
|
||||
walk(2 * i + 1);
|
||||
polylines = std::move(out);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -33,7 +33,7 @@ namespace Slic3r { class ExtrusionEntityCollection; }
|
||||
namespace Slic3r {
|
||||
|
||||
class Surface;
|
||||
class TpmsRadialField;
|
||||
class AdaptiveInfillField;
|
||||
enum InfillPattern : int;
|
||||
|
||||
namespace FillAdaptive {
|
||||
@@ -93,9 +93,9 @@ struct FillParams
|
||||
bool gyroid_optimized { false };
|
||||
|
||||
// For TPMS: grade the density from the surface to the interior of the object. Density fraction.
|
||||
TpmsAdaptiveMode tpms_adaptive { TpmsAdaptiveMode::Disabled };
|
||||
float tpms_interior_density { 0.f };
|
||||
TpmsAdaptiveGradient tpms_adaptive_gradient { TpmsAdaptiveGradient::Linear };
|
||||
AdaptiveInfillMode adaptive_infill_mode { AdaptiveInfillMode::Disabled };
|
||||
float adaptive_infill_interior_density { 0.f };
|
||||
AdaptiveInfillGradient adaptive_infill_gradient { AdaptiveInfillGradient::Linear };
|
||||
|
||||
// Orca: corner smoothing factor in the range [0, 1].
|
||||
double smooth_factor { 0. };
|
||||
@@ -162,7 +162,7 @@ public:
|
||||
FillAdaptive::Octree* adapt_fill_octree = nullptr;
|
||||
|
||||
// Radial coordinate inside the object for the adaptive TPMS infill
|
||||
const TpmsRadialField* tpms_radial_field = nullptr;
|
||||
const AdaptiveInfillField* adaptive_infill_field = nullptr;
|
||||
|
||||
// PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring).
|
||||
// Orca: also used by gap fill function.
|
||||
@@ -258,8 +258,11 @@ public:
|
||||
|
||||
static coord_t _adjust_solid_spacing(const coord_t width, const coord_t distance);
|
||||
};
|
||||
//Fill Multiline
|
||||
//Fill Multiline
|
||||
void multiline_fill(Polylines& polylines, const FillParams& params, float spacing);
|
||||
// Joins polylines whose ends are at most max_gap apart, shortest gaps first and without closing a loop, so each such
|
||||
// group prints as one path. Without can_reverse only the end of one polyline joins the start of another.
|
||||
void join_close_ends(Polylines &polylines, double max_gap, bool can_reverse);
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_FillBase_hpp_
|
||||
@@ -18,7 +18,7 @@
|
||||
#include "FillGyroid.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "FillTpmsAdaptive.hpp"
|
||||
#include "AdaptiveInfillField.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -315,8 +315,8 @@ void FillGyroid::_fill_surface_single(
|
||||
ExPolygon expolygon,
|
||||
Polylines &polylines_out)
|
||||
{
|
||||
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
|
||||
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
|
||||
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
[&](const FillParams &shell_params, const ExPolygon &shell) {
|
||||
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
|
||||
});
|
||||
@@ -342,11 +342,11 @@ void FillGyroid::_fill_surface_single(
|
||||
|
||||
// generate pattern
|
||||
Polylines polylines;
|
||||
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
|
||||
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
|
||||
// Radians per mm of the regular pattern at a density.
|
||||
auto frequency = [¶ms, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
|
||||
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
|
||||
*this->tpms_radial_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
|
||||
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.adaptive_infill_interior_density), params.adaptive_infill_gradient},
|
||||
*this->adaptive_infill_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
|
||||
} else if (params.gyroid_optimized) {
|
||||
// Marching-squares path on the gyroid implicit field. Base period matches
|
||||
// the standard parametric path's wavelength: 2*pi * spacing / density_adj.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "FillTpmsD.hpp"
|
||||
#include "FillTpmsAdaptive.hpp"
|
||||
#include "AdaptiveInfillField.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -117,8 +117,8 @@ void FillTpmsD::_fill_surface_single(
|
||||
ExPolygon expolygon,
|
||||
Polylines &polylines_out)
|
||||
{
|
||||
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
|
||||
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
|
||||
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
[&](const FillParams &shell_params, const ExPolygon &shell) {
|
||||
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
|
||||
});
|
||||
@@ -130,13 +130,13 @@ void FillTpmsD::_fill_surface_single(
|
||||
expolygon.rotate(-infill_angle);
|
||||
|
||||
Polylines polylines;
|
||||
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
|
||||
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
|
||||
// Radians per mm of the regular pattern at a density.
|
||||
auto frequency = [¶ms, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
|
||||
BoundingBox bbox = expolygon.contour.bounding_box();
|
||||
bbox.offset(scale_((params.multiline + 1) * this->spacing));
|
||||
polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
|
||||
*this->tpms_radial_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
|
||||
polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.adaptive_infill_interior_density), params.adaptive_infill_gradient},
|
||||
*this->adaptive_infill_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
|
||||
} else {
|
||||
BoundingBox bb = expolygon.contour.bounding_box();
|
||||
// Density adjusted to have a good %of weight.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include "libslic3r/Fill/FillBase.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "FillTpmsFK.hpp"
|
||||
#include "FillTpmsAdaptive.hpp"
|
||||
#include "AdaptiveInfillField.hpp"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
@@ -132,8 +132,8 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
|
||||
ExPolygon expolygon,
|
||||
Polylines& polylines_out)
|
||||
{
|
||||
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
|
||||
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
|
||||
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
[&](const FillParams &shell_params, const ExPolygon &shell) {
|
||||
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
|
||||
});
|
||||
@@ -151,10 +151,10 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
|
||||
// Enlarge the bounding box by the multi-line width to avoid artifacts at the edges.
|
||||
bbox.offset(scale_((params.multiline + 1) * spacing));
|
||||
Polylines polylines;
|
||||
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
|
||||
polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.tpms_interior_density),
|
||||
params.tpms_adaptive_gradient},
|
||||
*this->tpms_radial_field, bbox, this->z, params.layer_height, spacing, infill_angle);
|
||||
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
|
||||
polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.adaptive_infill_interior_density),
|
||||
params.adaptive_infill_gradient},
|
||||
*this->adaptive_infill_field, bbox, this->z, params.layer_height, spacing, infill_angle);
|
||||
} else {
|
||||
marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, period(params.density));
|
||||
// Get simplified lines using coarse tolerance of 0.1mm (this is infill).
|
||||
|
||||
@@ -988,7 +988,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
MetadataList metadata;
|
||||
VolumeMetadataList volumes;
|
||||
bool auto_drop { true };
|
||||
std::optional<bool> auto_drop;
|
||||
};
|
||||
|
||||
struct CutObjectInfo
|
||||
@@ -1208,6 +1208,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
//IdToGeometryMap m_orig_geometries; // backup & restore
|
||||
CurrentConfig m_curr_config;
|
||||
IdToMetadataMap m_objects_metadata;
|
||||
std::set<int> m_items_auto_drop_off; // object ids with a build item auto_drop="0" (2.4.x layout)
|
||||
IdToCutObjectInfoMap m_cut_object_infos;
|
||||
IdToLayerHeightsProfileMap m_layer_heights_profiles;
|
||||
IdToLayerConfigRangesMap m_layer_config_ranges;
|
||||
@@ -1463,6 +1464,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
m_objects.clear();
|
||||
m_instances.clear();
|
||||
m_objects_metadata.clear();
|
||||
m_items_auto_drop_off.clear();
|
||||
m_curr_metadata_name.clear();
|
||||
m_curr_characters.clear();
|
||||
|
||||
@@ -1502,6 +1504,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
m_curr_config.object_id = -1;
|
||||
m_curr_config.volume_id = -1;
|
||||
m_objects_metadata.clear();
|
||||
m_items_auto_drop_off.clear();
|
||||
m_layer_heights_profiles.clear();
|
||||
m_layer_config_ranges.clear();
|
||||
m_brim_ear_points.clear();
|
||||
@@ -2257,11 +2260,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
ObjectMetadata::VolumeMetadataList volumes;
|
||||
ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr;
|
||||
|
||||
// 2.4.x stored auto_drop on build items, shared by all instances of an object; any "off" wins
|
||||
const bool legacy_auto_drop = !m_items_auto_drop_off.count(object.first.second);
|
||||
|
||||
IdToMetadataMap::iterator obj_metadata = m_objects_metadata.find(object.first.second);
|
||||
if (obj_metadata != m_objects_metadata.end()) {
|
||||
// config data has been found, this model was saved using slic3r pe
|
||||
|
||||
model_object->auto_drop = obj_metadata->second.auto_drop;
|
||||
model_object->auto_drop = obj_metadata->second.auto_drop.value_or(legacy_auto_drop);
|
||||
|
||||
// apply object's name and config data
|
||||
for (const Metadata& metadata : obj_metadata->second.metadata) {
|
||||
@@ -2279,6 +2285,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
}
|
||||
else {
|
||||
// config data not found, this model was not saved using slic3r pe
|
||||
model_object->auto_drop = legacy_auto_drop;
|
||||
|
||||
// add the entire geometry as the single volume to generate
|
||||
//volumes.emplace_back(0, (int)obj_geometry->second.triangles.size() - 1);
|
||||
@@ -4121,6 +4128,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
std::string path = bbs_get_attribute_value_string(attributes, num_attributes, PPATH_ATTR);
|
||||
Transform3d transform = bbs_get_transform_from_3mf_specs_string(bbs_get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR));
|
||||
int printable = bbs_get_attribute_value_bool(attributes, num_attributes, PRINTABLE_ATTR);
|
||||
if (const char* auto_drop = bbs_get_attribute_value_charptr(attributes, num_attributes, AUTO_DROP_ATTR); auto_drop != nullptr && !::atoi(auto_drop))
|
||||
m_items_auto_drop_off.insert(object_id);
|
||||
return !m_load_model || _create_object_instance(path, object_id, transform, printable, 1);
|
||||
}
|
||||
|
||||
@@ -4489,7 +4498,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
// int instances_count_id = bbs_get_attribute_value_int(attributes, num_attributes, INSTANCESCOUNT_ATTR);
|
||||
|
||||
ObjectMetadata metadata;
|
||||
metadata.auto_drop = bbs_get_attribute_value_bool(attributes, num_attributes, AUTO_DROP_ATTR);
|
||||
if (bbs_get_attribute_value_charptr(attributes, num_attributes, AUTO_DROP_ATTR) != nullptr)
|
||||
metadata.auto_drop = bbs_get_attribute_value_bool(attributes, num_attributes, AUTO_DROP_ATTR);
|
||||
m_objects_metadata.insert({ object_id, std::move(metadata) });
|
||||
m_curr_config.object_id = object_id;
|
||||
return true;
|
||||
@@ -10110,7 +10120,7 @@ bool to_xml(std::stringstream &stream, const EmbossShape::SvgFile &svg, const Mo
|
||||
assert(file_data != nullptr);
|
||||
if (file_data == nullptr && !svg.path.empty()) file_data = read_from_disk(svg.path);
|
||||
if (file_data == nullptr) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Can't write svg file no filedata";
|
||||
BOOST_LOG_TRIVIAL(warning) << "Can't write SVG file no filedata";
|
||||
return false;
|
||||
}
|
||||
const std::string &file_data_str = *file_data;
|
||||
@@ -10126,7 +10136,7 @@ void to_xml(std::stringstream &stream, const EmbossShape &es, const ModelVolume
|
||||
stream << " <" << SHAPE_TAG << " ";
|
||||
if (es.svg_file.has_value())
|
||||
if (!to_xml(stream, *es.svg_file, volume, archive, export_full_path)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Can't write svg file defiden embossed shape into 3mf";
|
||||
BOOST_LOG_TRIVIAL(warning) << "Can't write SVG file defiden embossed shape into 3mf";
|
||||
}
|
||||
|
||||
stream << SHAPE_SCALE_ATTR << "=\"" << es.scale << "\" ";
|
||||
|
||||
Loaded 100 of 260 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user