mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-19 21:51:06 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbaed8a512 | ||
|
|
597b2abac4 | ||
|
|
ba4de8c1fc | ||
|
|
7bce53a773 | ||
|
|
e65113af6d | ||
|
|
91ab04e823 |
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 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.
|
||||
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.
|
||||
---
|
||||
|
||||
# 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 the per-language files in `developer_reference/glossary/` (`terms.tsv`, `languages.txt` and one `<lang>.tsv`) in the **OrcaSlicer_WIKI** repository.
|
||||
whose source is `developer_reference/localization_glossary.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 (`developer_reference/glossary/*.tsv` and `localization_glossary.md`) are
|
||||
The catalogs (and `OrcaSlicer.pot`) and the glossary (`localization_glossary.tsv` and `.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` and `glossary/*.tsv` for the rules and terms;
|
||||
- the wiki's `localization_guide.md` and `localization_glossary.md` / `.tsv` for the rules and terms;
|
||||
- `AGENTS.md` (Localization & translations) for the repository's conventions.
|
||||
|
||||
## Editing this skill
|
||||
|
||||
@@ -28,14 +28,13 @@ 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_DIR = Path("developer_reference") / "glossary" # terms.tsv, languages.txt and one <lang>.tsv per language
|
||||
GLOSSARY_TSV = Path("developer_reference") / "localization_glossary.tsv"
|
||||
GLOSSARY_MD = Path("developer_reference") / "localization_glossary.md"
|
||||
MISSING = {"", "—"}
|
||||
RAW_URL = "https://raw.githubusercontent.com/OrcaSlicer/OrcaSlicer_WIKI/main/"
|
||||
@@ -253,40 +252,30 @@ 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_DIR / "terms.tsv").exists():
|
||||
if cand and (Path(cand) / GLOSSARY_TSV).exists():
|
||||
return Path(cand)
|
||||
return None
|
||||
|
||||
|
||||
def _download(rel, required=True):
|
||||
cache = Path(tempfile.gettempdir()) / "orca_glossary" / rel
|
||||
def _download(rel):
|
||||
cache = Path(tempfile.gettempdir()) / ("orca_glossary_" + rel.name)
|
||||
if not cache.exists() or time.time() - cache.stat().st_mtime > 3600:
|
||||
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)
|
||||
with urllib.request.urlopen(RAW_URL + rel.as_posix(), timeout=30) as r:
|
||||
cache.write_bytes(r.read())
|
||||
return cache
|
||||
|
||||
|
||||
class Glossary:
|
||||
"""The glossary: rows of [English, Description, <one translation per language loaded>]."""
|
||||
|
||||
def __init__(self, wiki_arg=None, quiet=False, langs=None):
|
||||
def __init__(self, wiki_arg=None, quiet=False):
|
||||
self.wiki = find_wiki(wiki_arg)
|
||||
self.langs = langs # only these languages' files are read (and downloaded); None means all
|
||||
if self.wiki:
|
||||
self.dir, self.md = self.wiki / GLOSSARY_DIR, self.wiki / GLOSSARY_MD
|
||||
self.tsv, self.md = self.wiki / GLOSSARY_TSV, 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.dir, self.md = _download(GLOSSARY_DIR / "terms.tsv").parent, _download(GLOSSARY_MD)
|
||||
self.tsv, self.md = _download(GLOSSARY_TSV), _download(GLOSSARY_MD)
|
||||
self.load()
|
||||
|
||||
def report_clone_state(self):
|
||||
@@ -301,23 +290,11 @@ 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):
|
||||
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()
|
||||
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:]]
|
||||
self.matchers = []
|
||||
for row in self.rows:
|
||||
base = re.sub(r"\s*\([^)]*\)\s*$", "", row[0]).strip()
|
||||
@@ -517,7 +494,7 @@ def similar_entries(c, e, pool, cutoff=0.72):
|
||||
|
||||
def cmd_pending(a):
|
||||
c = Catalog(catalog_path(a.lang))
|
||||
g = Glossary(a.wiki, langs=[a.lang])
|
||||
g = Glossary(a.wiki)
|
||||
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
|
||||
@@ -556,7 +533,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, langs=[a.lang])
|
||||
g = None if a.no_glossary else Glossary(a.wiki, quiet=True)
|
||||
data = json.loads(Path(a.input).read_text(encoding="utf-8"))
|
||||
items = data["entries"] if isinstance(data, dict) else data
|
||||
idx = c.index()
|
||||
@@ -611,7 +588,7 @@ def find_msgfmt():
|
||||
|
||||
|
||||
def cmd_check(a):
|
||||
g = None if a.no_glossary else Glossary(a.wiki, quiet=True, langs=a.lang or None)
|
||||
g = None if a.no_glossary else Glossary(a.wiki, quiet=True)
|
||||
msgfmt = find_msgfmt()
|
||||
status = 0
|
||||
for lang in a.lang or all_langs():
|
||||
@@ -669,7 +646,7 @@ def cmd_lookup(a):
|
||||
|
||||
|
||||
def cmd_glossary(a):
|
||||
g = Glossary(a.wiki, langs=[a.lang])
|
||||
g = Glossary(a.wiki)
|
||||
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}=...")
|
||||
@@ -756,17 +733,6 @@ 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:
|
||||
@@ -775,21 +741,8 @@ 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
|
||||
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"
|
||||
tsv = wiki / GLOSSARY_TSV
|
||||
lock = tsv.with_suffix(".lock")
|
||||
for _ in range(100):
|
||||
try:
|
||||
fd = os.open(lock, os.O_CREAT | os.O_EXCL)
|
||||
@@ -799,51 +752,50 @@ def cmd_glossary_set(a):
|
||||
else:
|
||||
sys.exit(f"could not lock {lock}; remove it if no other run is active")
|
||||
try:
|
||||
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}'")
|
||||
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]
|
||||
if conflicts and not a.replace:
|
||||
sys.exit("established translations differ (pass --replace, and update the catalogs to match):\n " + "\n ".join(conflicts))
|
||||
|
||||
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))
|
||||
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"))
|
||||
finally:
|
||||
os.close(fd)
|
||||
os.unlink(lock)
|
||||
print(f"glossary: {'added' if new else 'updated'} '{a.term}'")
|
||||
r = run_gen("--update")
|
||||
print((r.stdout + r.stderr).strip())
|
||||
return r.returncode
|
||||
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
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -569,19 +569,12 @@ on the **first** call. So:
|
||||
#endif
|
||||
```
|
||||
|
||||
**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()`
|
||||
**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()`
|
||||
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 rules (c12912e0df) are in
|
||||
WAYLAND_DISPLAY set"). The details and the WebKit rule (c12912e0df) are in
|
||||
`references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions.
|
||||
|
||||
**wx's own detectors**, for reference:
|
||||
|
||||
@@ -61,8 +61,7 @@ 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 the no-EGL default path where Orca forces
|
||||
`GDK_BACKEND=x11`, for WebKitGTK older than 2.42, and never
|
||||
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond Orca's default-path XWayland case, 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
|
||||
@@ -568,74 +567,33 @@ 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)
|
||||
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.
|
||||
branches on `GDK_BACKEND`:
|
||||
- **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`, 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.
|
||||
`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.
|
||||
- **`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, which keeps hardware acceleration for Device / Setup Wizard / login pages.
|
||||
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).
|
||||
|
||||
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:** 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
|
||||
- **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
|
||||
waits forever for a C++→JS call.
|
||||
**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.
|
||||
**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.
|
||||
```cpp
|
||||
// Wrong: unconditional, kills HW acceleration everywhere
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false);
|
||||
// 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)
|
||||
// Right (default path only): non-replacing, before GTK init, Wayland session with XWayland
|
||||
if (display && *display && wayland && *wayland)
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /*replace*/ false);
|
||||
```
|
||||
Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit`
|
||||
|
||||
@@ -232,31 +232,6 @@ 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,7 +38,6 @@ 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."
|
||||
@@ -50,72 +49,18 @@ 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=$(printf '%s\n' "$COMMENTS" | jq -r 'select((.body | startswith("<!-- profile-validation-comment -->")) or (.body | startswith("## :x: Profile Validation Errors"))) | .id')
|
||||
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')
|
||||
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 || failed=1
|
||||
gh pr comment "$PR_NUMBER" --body-file pr_comment.md
|
||||
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 (`developer_reference/glossary/<lang>.tsv` in the wiki, then regenerate) so they stay in sync.
|
||||
- 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.
|
||||
- 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,19 +288,6 @@ 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")
|
||||
|
||||
+1
-15
@@ -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: disable precompiled headers (on by default): more ccache hits after header changes, slower misses"
|
||||
echo " -p: boost ccache hit rate by disabling precompiled headers (default: ON)"
|
||||
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"
|
||||
@@ -549,20 +549,6 @@ 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
|
||||
|
||||
@@ -111,54 +111,6 @@ 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"
|
||||
@@ -210,7 +162,6 @@ function build_deps() {
|
||||
-DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||
${CMAKE_POLICY_COMPAT}
|
||||
fi
|
||||
cmake --build . --config "$BUILD_CONFIG" --target deps
|
||||
@@ -313,7 +264,6 @@ function build_slicer() {
|
||||
-DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||
${CMAKE_POLICY_COMPAT}
|
||||
fi
|
||||
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
|
||||
|
||||
Vendored
+17
-2
@@ -1,7 +1,22 @@
|
||||
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.2.0.zip
|
||||
URL_HASH SHA256=1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
|
||||
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
|
||||
URL_HASH SHA256=d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
|
||||
DEPENDS ${ZLIB_PKG}
|
||||
CMAKE_ARGS
|
||||
-DENABLE_SHARED=OFF
|
||||
-DENABLE_STATIC=ON
|
||||
${jpeg_flag}
|
||||
)
|
||||
Vendored
+7
-17
@@ -16,8 +16,6 @@ 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
|
||||
@@ -45,14 +43,6 @@ 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
|
||||
@@ -91,7 +81,7 @@ ExternalProject_Add(dep_OpenSSL
|
||||
# URL_HASH SHA256=8c776993154652d0bb393f506d850b811517c8bd8d24b1008aef57fbe55d3f31
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/OpenSSL
|
||||
CONFIGURE_COMMAND ${_conf_cmd} ${_cross_arch}
|
||||
"--openssldir=${_openssl_dir}"
|
||||
"--openssldir=${DESTDIR}"
|
||||
"--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
|
||||
@@ -125,12 +115,12 @@ ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
|
||||
)
|
||||
|
||||
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.
|
||||
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.
|
||||
ExternalProject_Get_Property(dep_OpenSSL SOURCE_DIR)
|
||||
ExternalProject_Add_Step(dep_OpenSSL clean_objects
|
||||
DEPENDEES configure
|
||||
|
||||
Vendored
+4
-5
@@ -365,11 +365,10 @@ if(TARGET dep_ZLIB)
|
||||
add_dependencies(dep_python3 dep_ZLIB)
|
||||
endif()
|
||||
|
||||
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,
|
||||
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,
|
||||
# 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.
|
||||
|
||||
@@ -53,13 +53,6 @@ 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
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-19 09:48-0300\n"
|
||||
"POT-Creation-Date: 2026-10-15 17:40+0800\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-c-format, possible-boost-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,9 +10657,6 @@ msgstr ""
|
||||
msgid "Send Failed"
|
||||
msgstr ""
|
||||
|
||||
msgid "No available printers"
|
||||
msgstr ""
|
||||
|
||||
msgid "The name length exceeds the limit."
|
||||
msgstr ""
|
||||
|
||||
@@ -12318,7 +12315,7 @@ msgstr ""
|
||||
msgid "Left mouse"
|
||||
msgstr ""
|
||||
|
||||
msgid "Available anywhere in the window."
|
||||
msgid "Available anywhere in the window, even while typing in a text field."
|
||||
msgstr ""
|
||||
|
||||
msgid "Run favorite 1 to 10"
|
||||
@@ -12415,6 +12412,9 @@ 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,12 +15645,6 @@ 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 ""
|
||||
|
||||
@@ -20546,7 +20540,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
|
||||
@@ -26498,18 +26492,17 @@ msgstr ""
|
||||
msgid "Basic Colors"
|
||||
msgstr ""
|
||||
|
||||
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
msgid "Orca Palette"
|
||||
msgstr ""
|
||||
|
||||
#. TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
msgid "RAL Classic"
|
||||
msgstr ""
|
||||
|
||||
#. TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
msgid "Gradients"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "Color picker"
|
||||
msgid "Solid"
|
||||
msgstr ""
|
||||
|
||||
@@ -26884,7 +26877,7 @@ msgstr ""
|
||||
msgid "Medium violet red"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pale violet red"
|
||||
msgid "PaleV violet red"
|
||||
msgstr ""
|
||||
|
||||
msgid "Deep pink"
|
||||
@@ -27046,7 +27039,7 @@ msgstr ""
|
||||
msgid "Shallow sky"
|
||||
msgstr ""
|
||||
|
||||
msgid "Electric blue"
|
||||
msgid "Electric bel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dolphin"
|
||||
@@ -27115,7 +27108,7 @@ msgstr ""
|
||||
msgid "Maize yellow"
|
||||
msgstr ""
|
||||
|
||||
msgid "Daffodil yellow"
|
||||
msgid "Dalmatian yellow"
|
||||
msgstr ""
|
||||
|
||||
msgid "Brown beige"
|
||||
@@ -27376,7 +27369,7 @@ msgstr ""
|
||||
msgid "Turquoise blue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Capri blue"
|
||||
msgid "Capsanthin violet"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ocean blue"
|
||||
@@ -27490,7 +27483,7 @@ msgstr ""
|
||||
msgid "Silver grey"
|
||||
msgstr ""
|
||||
|
||||
msgid "Olive grey"
|
||||
msgid "Olivestone"
|
||||
msgstr ""
|
||||
|
||||
msgid "Moss grey"
|
||||
@@ -28078,19 +28071,10 @@ msgstr ""
|
||||
msgid "Built-in"
|
||||
msgstr ""
|
||||
|
||||
msgid "My textures"
|
||||
msgstr ""
|
||||
|
||||
msgid "No textures found."
|
||||
msgstr ""
|
||||
|
||||
msgid "Texture libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Explore more texture libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Opens OrcaCloud in the Home page to browse for plugins."
|
||||
msgid "My textures"
|
||||
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."
|
||||
@@ -30143,9 +30127,6 @@ msgstr ""
|
||||
msgid "Slicing Pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource Texture"
|
||||
msgstr ""
|
||||
|
||||
msgid "The plugin's configuration UI was empty. Showing the default editor."
|
||||
msgstr ""
|
||||
|
||||
@@ -30261,24 +30242,6 @@ 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 ""
|
||||
@@ -30635,7 +30598,7 @@ msgstr ""
|
||||
|
||||
#. TRN %1% is the application name.
|
||||
#, possible-boost-format
|
||||
msgid "Not a G-code file generated by %1%."
|
||||
msgid "Not a gcode file generated by %1%."
|
||||
msgstr ""
|
||||
|
||||
#. TRN %1% is a tag in the G-code file that must not be translated.
|
||||
@@ -30723,7 +30686,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 G-code file"
|
||||
msgid "Post-processor can't find exported gcode 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-19 09:48-0300\n"
|
||||
"POT-Creation-Date: 2026-10-15 17:40+0800\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 ""
|
||||
|
||||
#, c-format, boost-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,9 +10653,6 @@ msgstr ""
|
||||
msgid "Send Failed"
|
||||
msgstr ""
|
||||
|
||||
msgid "No available printers"
|
||||
msgstr ""
|
||||
|
||||
msgid "The name length exceeds the limit."
|
||||
msgstr ""
|
||||
|
||||
@@ -12314,7 +12311,7 @@ msgstr ""
|
||||
msgid "Left mouse"
|
||||
msgstr ""
|
||||
|
||||
msgid "Available anywhere in the window."
|
||||
msgid "Available anywhere in the window, even while typing in a text field."
|
||||
msgstr ""
|
||||
|
||||
msgid "Run favorite 1 to 10"
|
||||
@@ -12411,6 +12408,9 @@ 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,12 +15641,6 @@ 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 ""
|
||||
|
||||
@@ -20542,7 +20536,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
|
||||
@@ -26494,18 +26488,17 @@ msgstr ""
|
||||
msgid "Basic Colors"
|
||||
msgstr ""
|
||||
|
||||
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
msgid "Orca Palette"
|
||||
msgstr ""
|
||||
|
||||
#. TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
|
||||
msgid "RAL Classic"
|
||||
msgstr ""
|
||||
|
||||
#. TRN: Keep the standard/brand name "RAL" unchanged.
|
||||
msgid "Gradients"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "Color picker"
|
||||
msgid "Solid"
|
||||
msgstr ""
|
||||
|
||||
@@ -26880,7 +26873,7 @@ msgstr ""
|
||||
msgid "Medium violet red"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pale violet red"
|
||||
msgid "PaleV violet red"
|
||||
msgstr ""
|
||||
|
||||
msgid "Deep pink"
|
||||
@@ -27042,7 +27035,7 @@ msgstr ""
|
||||
msgid "Shallow sky"
|
||||
msgstr ""
|
||||
|
||||
msgid "Electric blue"
|
||||
msgid "Electric bel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dolphin"
|
||||
@@ -27111,7 +27104,7 @@ msgstr ""
|
||||
msgid "Maize yellow"
|
||||
msgstr ""
|
||||
|
||||
msgid "Daffodil yellow"
|
||||
msgid "Dalmatian yellow"
|
||||
msgstr ""
|
||||
|
||||
msgid "Brown beige"
|
||||
@@ -27372,7 +27365,7 @@ msgstr ""
|
||||
msgid "Turquoise blue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Capri blue"
|
||||
msgid "Capsanthin violet"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ocean blue"
|
||||
@@ -27486,7 +27479,7 @@ msgstr ""
|
||||
msgid "Silver grey"
|
||||
msgstr ""
|
||||
|
||||
msgid "Olive grey"
|
||||
msgid "Olivestone"
|
||||
msgstr ""
|
||||
|
||||
msgid "Moss grey"
|
||||
@@ -28074,19 +28067,10 @@ msgstr ""
|
||||
msgid "Built-in"
|
||||
msgstr ""
|
||||
|
||||
msgid "My textures"
|
||||
msgstr ""
|
||||
|
||||
msgid "No textures found."
|
||||
msgstr ""
|
||||
|
||||
msgid "Texture libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Explore more texture libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Opens OrcaCloud in the Home page to browse for plugins."
|
||||
msgid "My textures"
|
||||
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."
|
||||
@@ -30139,9 +30123,6 @@ msgstr ""
|
||||
msgid "Slicing Pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resource Texture"
|
||||
msgstr ""
|
||||
|
||||
msgid "The plugin's configuration UI was empty. Showing the default editor."
|
||||
msgstr ""
|
||||
|
||||
@@ -30257,24 +30238,6 @@ 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 ""
|
||||
@@ -30631,7 +30594,7 @@ msgstr ""
|
||||
|
||||
#. TRN %1% is the application name.
|
||||
#, boost-format
|
||||
msgid "Not a G-code file generated by %1%."
|
||||
msgid "Not a gcode file generated by %1%."
|
||||
msgstr ""
|
||||
|
||||
#. TRN %1% is a tag in the G-code file that must not be translated.
|
||||
@@ -30719,7 +30682,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 G-code file"
|
||||
msgid "Post-processor can't find exported gcode file"
|
||||
msgstr ""
|
||||
|
||||
#, boost-format
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+548
-1045
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
+2309
-4102
File diff suppressed because it is too large.
Load diff
+3110
-5343
File diff suppressed because it is too large.
Load diff
Binary file not shown.
|
After 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="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>
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 415 B After Width: | Height: | Size: 366 B |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Snapmaker",
|
||||
"version": "02.04.00.18",
|
||||
"version": "02.04.00.17",
|
||||
"force_update": "0",
|
||||
"description": "Snapmaker configurations",
|
||||
"machine_model_list": [
|
||||
|
||||
@@ -49,8 +49,5 @@
|
||||
],
|
||||
"pressure_advance": [
|
||||
"0.02"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"100"
|
||||
]
|
||||
}
|
||||
@@ -46,8 +46,5 @@
|
||||
],
|
||||
"fan_min_speed": [
|
||||
"0"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"100"
|
||||
]
|
||||
}
|
||||
@@ -45,7 +45,7 @@
|
||||
"250"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"70"
|
||||
"178"
|
||||
],
|
||||
"fan_max_speed": [
|
||||
"20"
|
||||
|
||||
@@ -4,8 +4,5 @@
|
||||
"inherits": "fdm_filament_petg",
|
||||
"from": "system",
|
||||
"filament_id": "OFW8lqnb",
|
||||
"instantiation": "false",
|
||||
"temperature_vitrification": [
|
||||
"70"
|
||||
]
|
||||
"instantiation": "false"
|
||||
}
|
||||
@@ -60,7 +60,7 @@
|
||||
"250"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"70"
|
||||
"178"
|
||||
],
|
||||
"fan_max_speed": [
|
||||
"20"
|
||||
|
||||
@@ -34,8 +34,5 @@
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"220"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"45"
|
||||
]
|
||||
}
|
||||
@@ -34,8 +34,5 @@
|
||||
],
|
||||
"filament_cooling_final_speed": [
|
||||
"60"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"45"
|
||||
]
|
||||
}
|
||||
@@ -34,8 +34,5 @@
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"220"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"45"
|
||||
]
|
||||
}
|
||||
@@ -40,8 +40,5 @@
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"220"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"45"
|
||||
]
|
||||
}
|
||||
@@ -31,8 +31,5 @@
|
||||
],
|
||||
"slow_down_layer_time": [
|
||||
"8"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"45"
|
||||
]
|
||||
}
|
||||
@@ -50,8 +50,5 @@
|
||||
"additional_cooling_fan_speed": [
|
||||
"70",
|
||||
"70"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"30"
|
||||
]
|
||||
}
|
||||
@@ -137,7 +137,7 @@
|
||||
"240"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"30"
|
||||
"60"
|
||||
],
|
||||
"nozzle_temperature_range_low": [
|
||||
"200"
|
||||
|
||||
@@ -302,7 +302,7 @@
|
||||
"-1"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"30"
|
||||
"60"
|
||||
],
|
||||
"textured_cool_plate_temp": [
|
||||
"40"
|
||||
|
||||
@@ -29,8 +29,5 @@
|
||||
"additional_cooling_fan_speed": [
|
||||
"70",
|
||||
"70"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"30"
|
||||
]
|
||||
}
|
||||
@@ -49,8 +49,5 @@
|
||||
"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\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_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_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\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_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_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\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_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_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\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_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_tool_change_time": "5",
|
||||
"max_layer_height": [
|
||||
"0.56",
|
||||
|
||||
@@ -36,6 +36,11 @@ 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;
|
||||
@@ -45,6 +50,14 @@ 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;
|
||||
@@ -54,6 +67,12 @@ 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.
|
||||
@@ -162,13 +181,29 @@ 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;
|
||||
@@ -237,9 +272,12 @@ 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 = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
|
||||
vec3 tex_pos = tex_rot * (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
|
||||
@@ -270,14 +308,14 @@ void main()
|
||||
} else if (weight > 0.0) {
|
||||
vec3 t, b;
|
||||
vec2 units_per_mm;
|
||||
projection_axes(tex_pos, triangle_normal, t, b, units_per_mm);
|
||||
projection_axes(tex_pos, tex_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(eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, triangle_normal);
|
||||
vec2 uv = project_uv(tex_pos, triangle_normal);
|
||||
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);
|
||||
|
||||
// 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,
|
||||
@@ -289,18 +327,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 - 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) {
|
||||
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) {
|
||||
// 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, triangle_normal);
|
||||
vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_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, triangle_normal);
|
||||
vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_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
|
||||
@@ -322,18 +360,23 @@ 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));
|
||||
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);
|
||||
vec2 tile = tile_mm();
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
// 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);
|
||||
// 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);
|
||||
// ...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;
|
||||
|
||||
vec3 gradient = slope.x * t + slope.y * b;
|
||||
// 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);
|
||||
gradient -= triangle_normal * dot(triangle_normal, gradient);
|
||||
triangle_normal = normalize(triangle_normal - gradient);
|
||||
}
|
||||
@@ -353,7 +396,9 @@ 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;
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
|
||||
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)
|
||||
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,7 +16,16 @@ 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;
|
||||
@@ -79,13 +88,30 @@ 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;
|
||||
}
|
||||
@@ -105,7 +131,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 = world_pos.xyz - tex_anchor;
|
||||
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor);
|
||||
if (volume_mirrored)
|
||||
triangle_normal = -triangle_normal;
|
||||
|
||||
@@ -113,7 +139,7 @@ void main()
|
||||
if (mode == 1) {
|
||||
base = heatmap(distortion);
|
||||
} else {
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal);
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * 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,6 +95,11 @@ 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;
|
||||
@@ -104,6 +109,14 @@ 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;
|
||||
@@ -113,6 +126,12 @@ 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
|
||||
@@ -228,13 +247,29 @@ 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;
|
||||
@@ -305,9 +340,12 @@ 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 = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
|
||||
vec3 tex_pos = tex_rot * (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
|
||||
@@ -351,14 +389,14 @@ void main()
|
||||
// closed-form uv here, unlike the LSCM case.
|
||||
vec3 t, b;
|
||||
vec2 units_per_mm;
|
||||
projection_axes(tex_pos, triangle_normal, t, b, units_per_mm);
|
||||
projection_axes(tex_pos, tex_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(eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, triangle_normal);
|
||||
vec2 uv = project_uv(tex_pos, triangle_normal);
|
||||
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);
|
||||
|
||||
// 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,
|
||||
@@ -370,18 +408,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 - 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) {
|
||||
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) {
|
||||
// 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, triangle_normal);
|
||||
vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_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, triangle_normal);
|
||||
vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_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
|
||||
@@ -405,23 +443,28 @@ 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 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 -> 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 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 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);
|
||||
// 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);
|
||||
// ...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;
|
||||
|
||||
vec3 gradient = slope.x * t + slope.y * b;
|
||||
// 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);
|
||||
gradient -= triangle_normal * dot(triangle_normal, gradient);
|
||||
triangle_normal = normalize(triangle_normal - gradient);
|
||||
}
|
||||
@@ -441,7 +484,9 @@ 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;
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
|
||||
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)
|
||||
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,7 +25,16 @@ 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;
|
||||
@@ -90,13 +99,30 @@ 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;
|
||||
}
|
||||
@@ -117,7 +143,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 = world_pos.xyz - tex_anchor;
|
||||
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor);
|
||||
if (volume_mirrored)
|
||||
triangle_normal = -triangle_normal;
|
||||
|
||||
@@ -125,7 +151,7 @@ void main()
|
||||
if (mode == 1) {
|
||||
base = heatmap(distortion);
|
||||
} else {
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal);
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * 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: "Pale violet red"},
|
||||
{hex: "#DB7093", name: "PaleV 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 blue"},
|
||||
{hex: "#1E90FF", name: "Electric bel"},
|
||||
{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 Daffodil yellow"},
|
||||
{hex: "#FDD185", name: "1007 Dalmatian 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: "#1B5583", name: "5019 Capri blue"},
|
||||
{hex: "#5A3D5B", name: "5019 Capsanthin violet"},
|
||||
{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 Olive grey"},
|
||||
{hex: "#6F7368", name: "7002 Olivestone"},
|
||||
{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,16 +156,12 @@ 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");
|
||||
}
|
||||
|
||||
@@ -336,10 +336,10 @@ modules:
|
||||
sha256: 3d22d46c566b1761a0e15ea397589b3a5f36ac09b7c785382e6470156c04247f
|
||||
dest: external-packages/PNG
|
||||
|
||||
# libjpeg-turbo 3.2.0
|
||||
# libjpeg-turbo 3.0.1
|
||||
- type: file
|
||||
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
|
||||
sha256: 1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
|
||||
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
|
||||
sha256: d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
|
||||
dest: external-packages/JPEG
|
||||
|
||||
# Freetype 2.12.1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# 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://gitlab.gnome.org/GNOME/gnome-build-meta/-/issues/754
|
||||
grep -q org.freedesktop.Platform.GL.nvidia /.flatpak-info && export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
|
||||
# Work-around https://github.com/bambulab/BambuStudio/issues/3440
|
||||
# Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues
|
||||
|
||||
@@ -12,7 +12,6 @@ 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
|
||||
@@ -95,14 +94,11 @@ 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
|
||||
@@ -196,7 +192,6 @@ 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)
|
||||
@@ -318,11 +313,11 @@ REMEDY_HINTS = {
|
||||
}
|
||||
|
||||
|
||||
def print_error(msg, file=None):
|
||||
print(f"\033[91m[ERROR]\033[0m {msg}", file=file) # Red
|
||||
def print_error(msg):
|
||||
print(f"\033[91m[ERROR]\033[0m {msg}") # Red
|
||||
|
||||
def print_warning(msg, file=None):
|
||||
print(f"\033[93m[WARNING]\033[0m {msg}", file=file) # Yellow
|
||||
def print_warning(msg):
|
||||
print(f"\033[93m[WARNING]\033[0m {msg}") # Yellow
|
||||
|
||||
def print_info(msg):
|
||||
print(f"\033[94m[INFO]\033[0m {msg}") # Blue
|
||||
@@ -3214,223 +3209,6 @@ 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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -3455,9 +3233,6 @@ 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
|
||||
@@ -3612,25 +3387,6 @@ 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
|
||||
|
||||
|
||||
@@ -3667,9 +3423,6 @@ 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)
|
||||
|
||||
@@ -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, check and gcode-diff commands, and the
|
||||
subcommand dispatch that reaches them (stdlib unittest, no external deps).
|
||||
normalize, trim, update-index, fix-variant and check 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,12 +14,9 @@ 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__), "..")))
|
||||
|
||||
@@ -44,24 +41,7 @@ class Tree:
|
||||
os.makedirs(self.profiles)
|
||||
|
||||
def cleanup(self):
|
||||
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})
|
||||
shutil.rmtree(self.dir, ignore_errors=True)
|
||||
|
||||
def index_path(self, vendor):
|
||||
return os.path.join(self.profiles, vendor + ".json")
|
||||
@@ -1429,292 +1409,6 @@ 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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+15
-37
@@ -163,7 +163,6 @@ using json = nlohmann::json;
|
||||
#include <X11/Xlib.h>
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
#include "slic3r/GUI/Widgets/WebView.hpp"
|
||||
#endif
|
||||
|
||||
#ifdef SLIC3R_GUI
|
||||
@@ -1126,19 +1125,6 @@ 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.
|
||||
@@ -1170,10 +1156,8 @@ 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 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.
|
||||
// Wizard / login / store. The default path still applies it on
|
||||
// XWayland sessions as a conservative fallback for older WebKit.
|
||||
// ------------------------------------------------------------------
|
||||
{
|
||||
const char* gdk_backend = ::getenv("GDK_BACKEND");
|
||||
@@ -1181,17 +1165,6 @@ 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.";
|
||||
@@ -1231,18 +1204,23 @@ 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,8 +429,7 @@ 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
|
||||
# WebKit's NVIDIA DMABUF workaround is applied by the binary itself,
|
||||
# gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
|
||||
export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -812,8 +812,7 @@ 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 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.
|
||||
* Odd walls should always go after their enclosing wall polygons.
|
||||
*
|
||||
* \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one.
|
||||
*/
|
||||
@@ -887,7 +886,12 @@ 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 ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
|
||||
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) {
|
||||
order_requirements.emplace(std::make_pair(nearby, here));
|
||||
} else {
|
||||
assert((nearby->inset_idx > here->inset_idx) == outer_to_inner);
|
||||
|
||||
@@ -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 G-code file generated by %1%."), SLIC3R_APP_FULL_NAME);
|
||||
std::string error_message = format(_u8L("Not a gcode file generated by %1%."), SLIC3R_APP_FULL_NAME);
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << error_message;
|
||||
throw Slic3r::RuntimeError(error_message.c_str());
|
||||
}
|
||||
|
||||
@@ -988,7 +988,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
MetadataList metadata;
|
||||
VolumeMetadataList volumes;
|
||||
std::optional<bool> auto_drop;
|
||||
bool auto_drop { true };
|
||||
};
|
||||
|
||||
struct CutObjectInfo
|
||||
@@ -1208,7 +1208,6 @@ 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;
|
||||
@@ -1464,7 +1463,6 @@ 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();
|
||||
|
||||
@@ -1504,7 +1502,6 @@ 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();
|
||||
@@ -2260,14 +2257,11 @@ 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.value_or(legacy_auto_drop);
|
||||
model_object->auto_drop = obj_metadata->second.auto_drop;
|
||||
|
||||
// apply object's name and config data
|
||||
for (const Metadata& metadata : obj_metadata->second.metadata) {
|
||||
@@ -2285,7 +2279,6 @@ 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);
|
||||
@@ -4128,8 +4121,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -4498,8 +4489,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
// int instances_count_id = bbs_get_attribute_value_int(attributes, num_attributes, INSTANCESCOUNT_ATTR);
|
||||
|
||||
ObjectMetadata metadata;
|
||||
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);
|
||||
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;
|
||||
@@ -10120,7 +10110,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;
|
||||
@@ -10136,7 +10126,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 << "\" ";
|
||||
|
||||
+17
-36
@@ -432,36 +432,27 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
|
||||
std::string OozePrevention::pre_toolchange(GCode& gcodegen)
|
||||
{
|
||||
std::string gcode;
|
||||
|
||||
unsigned int extruder_id = gcodegen.writer().filament()->id();
|
||||
|
||||
// Check if this tool will ever be used again in this print.
|
||||
bool is_last_use = false;
|
||||
if (gcodegen.m_print != nullptr && !gcodegen.m_print->tool_ordering().empty()) {
|
||||
coordf_t current_z = gcodegen.layer() ? gcodegen.layer()->print_z : gcodegen.writer().get_position().z();
|
||||
const auto& tool_ordering = gcodegen.m_print->tool_ordering();
|
||||
const auto& cfg = gcodegen.config();
|
||||
const int heater = imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(extruder_id));
|
||||
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, extruder_id);
|
||||
for (unsigned int f : tool_ordering.all_extruders())
|
||||
if (is_last_use && imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(f)) == heater)
|
||||
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, f);
|
||||
}
|
||||
|
||||
int temp = 0;
|
||||
if (!is_last_use) {
|
||||
const auto& filament_idle_temp = gcodegen.config().idle_temperature;
|
||||
if (filament_idle_temp.get_at(extruder_id) == 0) {
|
||||
if (gcodegen.config().standby_temperature_delta.value == 0)
|
||||
return std::string();
|
||||
temp = this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value;
|
||||
} else {
|
||||
temp = filament_idle_temp.get_at(extruder_id);
|
||||
const auto& filament_idle_temp = gcodegen.config().idle_temperature;
|
||||
if (filament_idle_temp.get_at(extruder_id) == 0) {
|
||||
// There is no idle temperature defined in filament settings.
|
||||
// Use the delta value from print config.
|
||||
if (gcodegen.config().standby_temperature_delta.value != 0) {
|
||||
// we assume that heating is always slower than cooling, so no need to block
|
||||
gcode += gcodegen.writer().set_temperature
|
||||
(this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, extruder_id);
|
||||
gcode.pop_back();
|
||||
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
|
||||
}
|
||||
} else {
|
||||
// Use the value from filament settings. That one is absolute, not delta.
|
||||
gcode += gcodegen.writer().set_temperature(filament_idle_temp.get_at(extruder_id), false, extruder_id);
|
||||
gcode.pop_back();
|
||||
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
|
||||
}
|
||||
|
||||
std::string gcode = gcodegen.writer().set_temperature(temp, false, extruder_id);
|
||||
gcode.pop_back();
|
||||
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
|
||||
return gcode;
|
||||
}
|
||||
|
||||
@@ -3985,16 +3976,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
|
||||
this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
|
||||
this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
|
||||
// The final print statistics are known only after the whole G-code is generated, so they are filled in during post-processing too.
|
||||
// The running extruded_* totals are left alone; profiles use them as numbers.
|
||||
this->placeholder_parser().set("used_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
|
||||
this->placeholder_parser().set("print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
|
||||
this->placeholder_parser().set("normal_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
|
||||
this->placeholder_parser().set("silent_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Silent_Print_Time_Placeholder)));
|
||||
this->placeholder_parser().set("total_weight", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Weight_Placeholder)));
|
||||
this->placeholder_parser().set("total_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Cost_Placeholder)));
|
||||
this->placeholder_parser().set("total_wipe_tower_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Cost_Placeholder)));
|
||||
this->placeholder_parser().set("total_wipe_tower_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Filament_Placeholder)));
|
||||
|
||||
// Sync variant-mapped params into placeholder_parser before processing start gcode
|
||||
update_placeholder_parser_with_variant_params();
|
||||
|
||||
@@ -1004,7 +1004,6 @@ protected:
|
||||
size_t num_objects,
|
||||
size_t num_islands);
|
||||
|
||||
friend class OozePrevention;
|
||||
friend class Wipe;
|
||||
friend class WipeTowerIntegration;
|
||||
friend class PressureEqualizer;
|
||||
|
||||
@@ -120,13 +120,7 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags = {
|
||||
"@PRINT_TIME_HOUR@",
|
||||
"@PRINT_TIME_MINUTE@",
|
||||
"@PRINT_TIME_SEC@",
|
||||
"@USED_FILAMENT_LENGTH@",
|
||||
"@NORMAL_PRINT_TIME@",
|
||||
"@SILENT_PRINT_TIME@",
|
||||
"@TOTAL_WEIGHT@",
|
||||
"@TOTAL_COST@",
|
||||
"@TOTAL_WIPE_TOWER_COST@",
|
||||
"@TOTAL_WIPE_TOWER_FILAMENT@"
|
||||
"@USED_FILAMENT_LENGTH@"
|
||||
};
|
||||
|
||||
const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
|
||||
@@ -153,13 +147,7 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
|
||||
"@PRINT_TIME_HOUR@",
|
||||
"@PRINT_TIME_MINUTE@",
|
||||
"@PRINT_TIME_SEC@",
|
||||
"@USED_FILAMENT_LENGTH@",
|
||||
"@NORMAL_PRINT_TIME@",
|
||||
"@SILENT_PRINT_TIME@",
|
||||
"@TOTAL_WEIGHT@",
|
||||
"@TOTAL_COST@",
|
||||
"@TOTAL_WIPE_TOWER_COST@",
|
||||
"@TOTAL_WIPE_TOWER_FILAMENT@"
|
||||
"@USED_FILAMENT_LENGTH@"
|
||||
};
|
||||
|
||||
|
||||
@@ -1276,22 +1264,19 @@ void GCodeProcessor::run_post_process()
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Process inline placeholders (print_time_*, used_filament_length and the print statistics known only after export)
|
||||
// Process inline placeholders (print_time_total_sec, print_time_day, print_time_hour, print_time_minute, print_time_sec and used_filament_length)
|
||||
auto process_inline_placeholders = [&](std::string& gcode_line) {
|
||||
bool processed = false;
|
||||
// Every inline placeholder contains '@', so a line without one has nothing to replace.
|
||||
if (gcode_line.find('@') == std::string::npos)
|
||||
return processed;
|
||||
|
||||
auto replace_tag = [&gcode_line, &processed](ETags tag, const std::string& value) {
|
||||
const std::string& placeholder = reserved_tag(tag);
|
||||
size_t pos = gcode_line.find(placeholder);
|
||||
while (pos != std::string::npos) {
|
||||
gcode_line.replace(pos, placeholder.length(), value);
|
||||
processed = true;
|
||||
pos = gcode_line.find(placeholder, pos + value.length());
|
||||
}
|
||||
};
|
||||
const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder);
|
||||
const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder);
|
||||
const std::string& print_time_hour_placeholder = reserved_tag(ETags::Print_Time_Hour_Placeholder);
|
||||
const std::string& print_time_minute_placeholder = reserved_tag(ETags::Print_Time_Minute_Placeholder);
|
||||
const std::string& print_time_sec_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder);
|
||||
const std::string& used_filament_placeholder = reserved_tag(ETags::Used_Filament_Length_Placeholder);
|
||||
|
||||
double print_time_total_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time;
|
||||
if (print_time_total_sec < 0.0)
|
||||
@@ -1304,31 +1289,70 @@ void GCodeProcessor::run_post_process()
|
||||
int print_time_minute = (day_remainder_seconds % 3600) / 60;
|
||||
int print_time_sec = day_remainder_seconds % 60;
|
||||
|
||||
double total_filament_mm = 0.0;
|
||||
for (const auto& mm : filament_mm) {
|
||||
total_filament_mm += mm;
|
||||
// Replace print_time_total_sec
|
||||
size_t pos = gcode_line.find(print_time_total_placeholder);
|
||||
while (pos != std::string::npos) {
|
||||
char buf[64];
|
||||
sprintf(buf, "%.2f", print_time_total_sec);
|
||||
gcode_line.replace(pos, print_time_total_placeholder.length(), buf);
|
||||
processed = true;
|
||||
pos = gcode_line.find(print_time_total_placeholder, pos + strlen(buf));
|
||||
}
|
||||
|
||||
replace_tag(ETags::Print_Time_Total_Sec_Placeholder, Slic3r::float_to_string_decimal_point(print_time_total_sec, 2));
|
||||
replace_tag(ETags::Print_Time_Day_Placeholder, std::to_string(print_time_day));
|
||||
replace_tag(ETags::Print_Time_Hour_Placeholder, std::to_string(print_time_hour));
|
||||
replace_tag(ETags::Print_Time_Minute_Placeholder, std::to_string(print_time_minute));
|
||||
replace_tag(ETags::Print_Time_Sec_Placeholder, std::to_string(print_time_sec));
|
||||
replace_tag(ETags::Used_Filament_Length_Placeholder,
|
||||
Slic3r::float_to_string_decimal_point(total_filament_mm / 1000.0, 2)); // Convert mm to m
|
||||
// Same values as the print statistics that filename_format gets (PrintStatistics::config(), filled in by
|
||||
// update_print_estimated_stats() in GCode.cpp), which runs only after this post-processing.
|
||||
replace_tag(ETags::Normal_Print_Time_Placeholder, get_time_dhm(PrintEstimatedStatistics::ETimeMode::Normal));
|
||||
replace_tag(ETags::Silent_Print_Time_Placeholder, is_stealth_time_estimator_enabled() ?
|
||||
get_time_dhm(PrintEstimatedStatistics::ETimeMode::Stealth) :
|
||||
short_time("N/A"));
|
||||
const double total_cost = filament_total_cost + m_print->config().time_cost.value * print_time_total_sec / 3600.0;
|
||||
replace_tag(ETags::Total_Weight_Placeholder, Slic3r::float_to_string_decimal_point(filament_total_g, 2));
|
||||
replace_tag(ETags::Total_Cost_Placeholder, Slic3r::float_to_string_decimal_point(total_cost, 2));
|
||||
replace_tag(ETags::Total_Wipe_Tower_Cost_Placeholder,
|
||||
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_cost, 2));
|
||||
replace_tag(ETags::Total_Wipe_Tower_Filament_Placeholder,
|
||||
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_filament, 2));
|
||||
// Replace print_time_day
|
||||
pos = gcode_line.find(print_time_day_placeholder);
|
||||
while (pos != std::string::npos) {
|
||||
char buf[64];
|
||||
sprintf(buf, "%d", print_time_day);
|
||||
gcode_line.replace(pos, print_time_day_placeholder.length(), buf);
|
||||
processed = true;
|
||||
pos = gcode_line.find(print_time_day_placeholder, pos + strlen(buf));
|
||||
}
|
||||
|
||||
// Replace print_time_hour
|
||||
pos = gcode_line.find(print_time_hour_placeholder);
|
||||
while (pos != std::string::npos) {
|
||||
char buf[64];
|
||||
sprintf(buf, "%d", print_time_hour);
|
||||
gcode_line.replace(pos, print_time_hour_placeholder.length(), buf);
|
||||
processed = true;
|
||||
pos = gcode_line.find(print_time_hour_placeholder, pos + strlen(buf));
|
||||
}
|
||||
|
||||
// Replace print_time_minute
|
||||
pos = gcode_line.find(print_time_minute_placeholder);
|
||||
while (pos != std::string::npos) {
|
||||
char buf[64];
|
||||
sprintf(buf, "%d", print_time_minute);
|
||||
gcode_line.replace(pos, print_time_minute_placeholder.length(), buf);
|
||||
processed = true;
|
||||
pos = gcode_line.find(print_time_minute_placeholder, pos + strlen(buf));
|
||||
}
|
||||
|
||||
// Replace print_time_sec
|
||||
pos = gcode_line.find(print_time_sec_placeholder);
|
||||
while (pos != std::string::npos) {
|
||||
char buf[64];
|
||||
sprintf(buf, "%d", print_time_sec);
|
||||
gcode_line.replace(pos, print_time_sec_placeholder.length(), buf);
|
||||
processed = true;
|
||||
pos = gcode_line.find(print_time_sec_placeholder, pos + strlen(buf));
|
||||
}
|
||||
|
||||
// Replace used_filament_length
|
||||
pos = gcode_line.find(used_filament_placeholder);
|
||||
while (pos != std::string::npos) {
|
||||
double total_filament_mm = 0.0;
|
||||
for (const auto& mm : filament_mm) {
|
||||
total_filament_mm += mm;
|
||||
}
|
||||
double used_filament_length = total_filament_mm / 1000.0; // Convert mm to m
|
||||
char buf[64];
|
||||
sprintf(buf, "%.2f", used_filament_length);
|
||||
gcode_line.replace(pos, used_filament_placeholder.length(), buf);
|
||||
processed = true;
|
||||
pos = gcode_line.find(used_filament_placeholder, pos + strlen(buf));
|
||||
}
|
||||
|
||||
return processed;
|
||||
};
|
||||
|
||||
@@ -601,12 +601,6 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
Print_Time_Minute_Placeholder,
|
||||
Print_Time_Sec_Placeholder,
|
||||
Used_Filament_Length_Placeholder,
|
||||
Normal_Print_Time_Placeholder,
|
||||
Silent_Print_Time_Placeholder,
|
||||
Total_Weight_Placeholder,
|
||||
Total_Cost_Placeholder,
|
||||
Total_Wipe_Tower_Cost_Placeholder,
|
||||
Total_Wipe_Tower_Filament_Placeholder,
|
||||
};
|
||||
|
||||
static const std::string& reserved_tag(ETags tag) { return s_IsBBLPrinter ? Reserved_Tags[static_cast<unsigned char>(tag)] : Reserved_Tags_compatible[static_cast<unsigned char>(tag)]; }
|
||||
|
||||
@@ -1276,13 +1276,6 @@ void ToolOrdering::collect_extruder_statistics(bool prime_multi_material)
|
||||
sort_remove_duplicates(m_all_printing_extruders);
|
||||
}
|
||||
|
||||
m_last_layer_per_extruder.clear();
|
||||
for (const LayerTools& lt : m_layer_tools) {
|
||||
for (unsigned int ext : lt.extruders) {
|
||||
m_last_layer_per_extruder[ext] = lt.print_z;
|
||||
}
|
||||
}
|
||||
|
||||
if (prime_multi_material && ! m_all_printing_extruders.empty()) {
|
||||
// Reorder m_all_printing_extruders in the sequence they will be primed, the last one will be m_first_printing_extruder.
|
||||
// Then set m_first_printing_extruder to the 1st extruder primed.
|
||||
@@ -3671,11 +3664,5 @@ int WipingExtrusions::get_support_interface_extruder_overrides(const PrintObject
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool ToolOrdering::is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const
|
||||
{
|
||||
auto it = m_last_layer_per_extruder.find(extruder_id);
|
||||
if (it == m_last_layer_per_extruder.end())
|
||||
return true;
|
||||
return print_z >= it->second - EPSILON;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -370,7 +370,6 @@ public:
|
||||
std::vector<int> & initial_filaments);
|
||||
|
||||
bool has_non_support_filament(const PrintConfig &config);
|
||||
bool is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const;
|
||||
|
||||
private:
|
||||
void initialize_layers(std::vector<coordf_t> &zs);
|
||||
@@ -414,8 +413,6 @@ private:
|
||||
// which keeps every legacy per-object code path bit-identical (loops over an empty map are
|
||||
// no-ops; downstream emission falls through to the per-object branch).
|
||||
std::map<unsigned int, std::map<LayerTools::MixedSubLayerGroup::VolumeKey, std::vector<size_t>>> m_gradient_volume_layers;
|
||||
// Map: extruder_id -> print_z (z coordinate) of the last layer it is used on
|
||||
std::map<unsigned int, coordf_t> m_last_layer_per_extruder;
|
||||
const PrintObject* m_print_object_ptr = nullptr;
|
||||
Print* m_print;
|
||||
bool m_sorted = false;
|
||||
|
||||
@@ -211,35 +211,6 @@ bool decode_colored_png(const ReadBuf &in_buf, ImageColorscale &out_img)
|
||||
return decode_colored_png(stream, out_img);
|
||||
}
|
||||
|
||||
bool is_decodable_8bit_png(const ReadBuf &pngbuf)
|
||||
{
|
||||
static const constexpr int PNG_SIG_BYTES = 8;
|
||||
|
||||
struct ReadBufStream stream{pngbuf};
|
||||
std::vector<png_byte> sig(PNG_SIG_BYTES, 0);
|
||||
stream.read(sig.data(), PNG_SIG_BYTES);
|
||||
if (!png_check_sig(sig.data(), PNG_SIG_BYTES))
|
||||
return false;
|
||||
|
||||
PNGDescr dsc;
|
||||
dsc.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
||||
if (!dsc.png)
|
||||
return false;
|
||||
dsc.info = png_create_info_struct(dsc.png);
|
||||
if (!dsc.info)
|
||||
return false;
|
||||
|
||||
if (!png_read_header_guarded(dsc.png, dsc.info, &stream, PNG_SIG_BYTES))
|
||||
return false;
|
||||
|
||||
const size_t color_type = png_get_color_type(dsc.png, dsc.info);
|
||||
const size_t bit_depth = png_get_bit_depth(dsc.png, dsc.info);
|
||||
const bool grey = color_type == PNG_COLOR_TYPE_GRAY;
|
||||
const bool colour = color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA;
|
||||
return png_get_image_width(dsc.png, dsc.info) > 0 && png_get_image_height(dsc.png, dsc.info) > 0 &&
|
||||
bit_depth == 8 && (grey || colour);
|
||||
}
|
||||
|
||||
|
||||
// Down to earth function to store a packed RGB image to file. Mostly useful for debugging purposes.
|
||||
// Based on https://www.lemoda.net/c/write-png/
|
||||
|
||||
@@ -50,9 +50,6 @@ struct ReadBuf { const void *buf = nullptr; const size_t sz = 0; };
|
||||
|
||||
bool is_png(const ReadBuf &pngbuf);
|
||||
|
||||
// True if decode_png() or decode_colored_png() would accept these bytes, judged from the header alone.
|
||||
bool is_decodable_8bit_png(const ReadBuf &pngbuf);
|
||||
|
||||
struct ReadBufStream: public IStream {
|
||||
const ReadBuf &rbuf_ref;
|
||||
size_t pos = 0;
|
||||
|
||||
@@ -62,12 +62,7 @@ public:
|
||||
// Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole.
|
||||
unsigned short depth;
|
||||
// ORCA: an external hole perimeter touching the next outer wall all around, with nothing between them.
|
||||
// E.g. a 1mm tube wall with 0.5mm walls: 2 lines, the contour's outer wall and the hole's.
|
||||
bool is_thin_wall_hole = false;
|
||||
// ORCA: an external hole perimeter with no room for an inner wall beside it, at most gap fill. Every thin wall
|
||||
// hole is one, and so is e.g. a 1.5mm tube wall with 0.5mm walls: 3 lines, the two outer walls and gap fill
|
||||
// between them. A 2mm tube wall has room for an inner wall a side and is not. Only set for Inner/Outer/Inner.
|
||||
bool is_narrow_wall_hole = false;
|
||||
// Children contour, may be both CCW and CW oriented (outer contours or holes).
|
||||
std::vector<PerimeterGeneratorLoop> children;
|
||||
|
||||
@@ -577,8 +572,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
|
||||
extrusion_coll.append(std::move(extrusion_loop));
|
||||
// Orca: Reverse the order of paths for thin wall holes. We define thin wall hole as a hole with only one perimeter.
|
||||
const bool thin_wall_hole = !pg_extrusion.is_contour && pg_extrusions.size() == 2;
|
||||
if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner &&
|
||||
!perimeter_generator.outer_walls_first_for_brim())
|
||||
if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner)
|
||||
std::reverse(extrusion_coll.entities.begin(), extrusion_coll.entities.end());
|
||||
}
|
||||
else {
|
||||
@@ -1696,24 +1690,13 @@ void PerimeterGenerator::process_classic()
|
||||
|
||||
last = std::move(offsets);
|
||||
|
||||
// ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall, a
|
||||
// narrow wall hole none for an inner wall, the room the next loop is offset by below.
|
||||
// ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall.
|
||||
if (i == 0 && ! holes[0].empty()) {
|
||||
auto reach = [&last](double room) {
|
||||
return to_polygons(offset2_ex(last, -float(room), float(room) + float(SCALED_EPSILON)));
|
||||
};
|
||||
auto out_of_reach = [](const Polygon &hole, const Polygons &reach) {
|
||||
return intersection_pl(Polylines{ hole.split_at_first_point() },
|
||||
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole).inflated(SCALED_EPSILON))).empty();
|
||||
};
|
||||
// Only Inner/Outer/Inner orders narrow wall holes apart.
|
||||
const bool narrow_holes_ordered = this->config->wall_sequence == WallSequence::InnerOuterInner;
|
||||
const Polygons gap_fill_reach = reach(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
|
||||
const Polygons inner_wall_reach = narrow_holes_ordered ? reach(ext_perimeter_spacing2 + min_spacing / 2.) : Polygons();
|
||||
for (PerimeterGeneratorLoop &hole : holes[0]) {
|
||||
hole.is_thin_wall_hole = out_of_reach(hole.polygon, gap_fill_reach);
|
||||
hole.is_narrow_wall_hole = narrow_holes_ordered && out_of_reach(hole.polygon, inner_wall_reach);
|
||||
}
|
||||
const float room = float(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
|
||||
const Polygons reach = to_polygons(offset2_ex(last, -room, room + float(SCALED_EPSILON)));
|
||||
for (PerimeterGeneratorLoop &hole : holes[0])
|
||||
hole.is_thin_wall_hole = intersection_pl(Polylines{ hole.polygon.split_at_first_point() },
|
||||
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole.polygon).inflated(SCALED_EPSILON))).empty();
|
||||
}
|
||||
|
||||
//BBS: refer to superslicer
|
||||
@@ -1852,16 +1835,18 @@ void PerimeterGenerator::process_classic()
|
||||
|
||||
// if brim will be printed, reverse the order of perimeters so that
|
||||
// we continue inwards after having finished the brim
|
||||
// TODO: add test for perimeter order
|
||||
bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner;
|
||||
// Orca: a contour with only a narrow wall hole beside it has nothing to sandwich, and prints the hole first,
|
||||
// then the contour, then the gap fill between them, as Arachne does.
|
||||
const PerimeterGeneratorLoops &island_loops = contours.front();
|
||||
const bool narrow_wall_hole_island = island_loops.size() == 1 && island_loops.front().children.size() == 1 &&
|
||||
island_loops.front().children.front().is_narrow_wall_hole;
|
||||
if (is_outer_wall_first || this->outer_walls_first_for_brim())
|
||||
if (is_outer_wall_first ||
|
||||
//BBS: always print outer wall first when there indeed has brim.
|
||||
// btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact).
|
||||
(this->layer_id == 0 &&
|
||||
(this->object_config->brim_type == BrimType::btOuterOnly ||
|
||||
this->object_config->brim_type == BrimType::btLeadingEdgeOnly) &&
|
||||
this->object_config->brim_width.value > 0))
|
||||
entities.reverse();
|
||||
// Orca: sandwich mode. Apply after 1st layer.
|
||||
else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0 && !narrow_wall_hole_island){
|
||||
else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0){
|
||||
entities.reverse(); // reverse all entities - order them from external to internal
|
||||
if(entities.entities.size()>2){ // 3 walls minimum needed to do inner outer inner ordering
|
||||
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
|
||||
@@ -2427,25 +2412,11 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
|
||||
|
||||
// Function to reorder perimeters starting from a given reference index
|
||||
auto reorderFromReference = [&](int referenceIndex) {
|
||||
// Find first level touching perimeters. The odd centre line of a wall too narrow for two inner walls is shared
|
||||
// by the outer walls on both sides, so those join this island and are printed before it too, holes first.
|
||||
std::vector<int> outerIndices{referenceIndex};
|
||||
std::vector<int> firstLevelTouchingIndices;
|
||||
for (size_t next = 0; next < outerIndices.size(); ++next)
|
||||
for (int idx : findAllTouchingPerimeters(entities, {outerIndices[next]}, threshold_external, threshold_internal, 1)) {
|
||||
if (std::find(firstLevelTouchingIndices.begin(), firstLevelTouchingIndices.end(), idx) != firstLevelTouchingIndices.end())
|
||||
continue;
|
||||
firstLevelTouchingIndices.push_back(idx);
|
||||
if (!entities[idx].extrusion->is_odd)
|
||||
continue;
|
||||
for (size_t i = 0; i < entities.size(); ++i)
|
||||
if (entities[i].extrusion->inset_idx == 0 && includedIndices.count(int(i)) == 0 &&
|
||||
std::find(outerIndices.begin(), outerIndices.end(), int(i)) == outerIndices.end() &&
|
||||
arachne_lines_touch(*entities[i].extrusion, *entities[idx].extrusion, double(threshold_external)))
|
||||
outerIndices.push_back(int(i));
|
||||
}
|
||||
std::stable_partition(outerIndices.begin(), outerIndices.end(),
|
||||
[&entities](int idx) { return entities[idx].extrusion->is_closed && !entities[idx].is_contour; });
|
||||
std::unordered_set<int> firstLevelIndices;
|
||||
firstLevelIndices.insert(referenceIndex);
|
||||
|
||||
// Find first level touching perimeters
|
||||
std::vector<int> firstLevelTouchingIndices = findAllTouchingPerimeters(entities, firstLevelIndices, threshold_external, threshold_internal, 1);
|
||||
// Bring the largest first level perimeter to the front
|
||||
// The longest first neighbour is most likely the dominant proximate perimeter
|
||||
// hence printing it immediately after the external perimeter should speed things up
|
||||
@@ -2456,10 +2427,8 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
|
||||
std::iter_swap(maxIt, firstLevelTouchingIndices.end() - 1);
|
||||
}
|
||||
// Insert first level perimeters into reordered list
|
||||
for (int idx : outerIndices) {
|
||||
reordered.push_back(entities[idx]);
|
||||
includedIndices.insert(idx);
|
||||
}
|
||||
reordered.push_back(entities[referenceIndex]);
|
||||
includedIndices.insert(referenceIndex);
|
||||
|
||||
for (int idx : firstLevelTouchingIndices) {
|
||||
if (includedIndices.count(idx) == 0) {
|
||||
@@ -2788,7 +2757,7 @@ void PerimeterGenerator::process_arachne()
|
||||
|
||||
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
|
||||
is_outer_wall_first =
|
||||
this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim();
|
||||
this->config->wall_sequence == WallSequence::OuterInner;
|
||||
}
|
||||
if (is_outer_wall_first) {
|
||||
start_perimeter = 0;
|
||||
@@ -3073,16 +3042,6 @@ ExPolygons PerimeterGenerator::printable_slices(const ExPolygons &slices) const
|
||||
return min_width > EPSILON ? opening_ex(slices, float(scale_(min_width / 2.))) : slices;
|
||||
}
|
||||
|
||||
bool PerimeterGenerator::outer_walls_first_for_brim() const
|
||||
{
|
||||
return layer_id == 0 && brim_puts_outer_walls_first(object_config->brim_type.value, object_config->brim_width.value);
|
||||
}
|
||||
|
||||
bool brim_puts_outer_walls_first(BrimType brim_type, double brim_width)
|
||||
{
|
||||
return brim_width > 0 && (brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btLeadingEdgeOnly);
|
||||
}
|
||||
|
||||
std::vector<Polygons> PerimeterGenerator::generate_lower_polygons_series(float width)
|
||||
{
|
||||
float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1);
|
||||
|
||||
@@ -164,8 +164,6 @@ public:
|
||||
Polygons lower_slices_polygons() const { return m_lower_slices_polygons; }
|
||||
// ORCA: the slices less the slivers the wall generator prints nothing for, so they never count as support.
|
||||
ExPolygons printable_slices(const ExPolygons &slices) const;
|
||||
// ORCA: whether this layer prints its outer walls first for the brim, see brim_puts_outer_walls_first().
|
||||
bool outer_walls_first_for_brim() const;
|
||||
|
||||
private:
|
||||
std::vector<Polygons> generate_lower_polygons_series(float width);
|
||||
@@ -185,10 +183,6 @@ private:
|
||||
Polygons m_lower_slices_polygons;
|
||||
};
|
||||
|
||||
// ORCA: a brim against the outer walls makes the first layer print them first, whatever the wall sequence, so the
|
||||
// brim runs straight on into the outer wall. Auto, ear and painted brims may leave the outer walls bare.
|
||||
bool brim_puts_outer_walls_first(BrimType brim_type, double brim_width);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1421,7 +1421,6 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"initial_layer_travel_acceleration",
|
||||
"initial_layer_travel_jerk",
|
||||
"exclude_object",
|
||||
"exclude_object_tolerance",
|
||||
"slow_down_layers",
|
||||
"infill_anchor",
|
||||
"infill_anchor_max",
|
||||
@@ -2294,7 +2293,6 @@ void PresetCollection::load_project_embedded_presets(std::vector<Preset*>& proje
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file;
|
||||
continue;
|
||||
}
|
||||
extend_default_config_length(config, false, {});
|
||||
preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
||||
//preset->config.apply(std::move(config));
|
||||
Preset::normalize(preset->config);
|
||||
|
||||
@@ -756,14 +756,7 @@ public:
|
||||
Preset& get_edited_preset() { return m_edited_preset; }
|
||||
const Preset& get_edited_preset() const { return m_edited_preset; }
|
||||
|
||||
// The root of the selected preset's inherits chain, or the selected preset itself when
|
||||
// the chain names a preset that is not loaded (e.g. a vendor that is not installed).
|
||||
const Preset& get_selected_preset_base() const
|
||||
{
|
||||
const Preset &selected = m_presets[m_idx_selected];
|
||||
const Preset *base = get_preset_base(selected);
|
||||
return base ? *base : selected;
|
||||
}
|
||||
const Preset& get_selected_preset_base() const { return *get_preset_base(m_presets[m_idx_selected]); }
|
||||
|
||||
// Return the last saved preset.
|
||||
// const Preset& get_saved_preset() const { return m_saved_preset; }
|
||||
|
||||
@@ -309,7 +309,6 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
"gcode_comments",
|
||||
"gcode_label_objects",
|
||||
"exclude_object",
|
||||
"exclude_object_tolerance",
|
||||
"support_material_interface_fan_speed",
|
||||
"internal_bridge_fan_speed", // ORCA: Add support for separate internal bridge fan speed control
|
||||
"ironing_fan_speed",
|
||||
@@ -6930,7 +6929,7 @@ BoundingBoxf3 PrintInstance::get_bounding_box() const {
|
||||
|
||||
Polygon PrintInstance::get_convex_hull_2d() {
|
||||
Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix());
|
||||
poly.douglas_peucker(scale_(print_object->print()->config().exclude_object_tolerance.value));
|
||||
poly.douglas_peucker(scale_(0.1));
|
||||
return poly;
|
||||
}
|
||||
|
||||
|
||||
@@ -4629,17 +4629,6 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("exclude_object_tolerance", coFloat);
|
||||
def->label = L("Outline tolerance");
|
||||
def->tooltip = L("Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the "
|
||||
"original outline. Larger values give fewer points, which helps firmware that stalls "
|
||||
"while parsing G-code with many objects. 0 keeps every point.");
|
||||
def->sidetext = L("mm");
|
||||
def->min = 0;
|
||||
def->max = 5;
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionFloat(0.1));
|
||||
|
||||
def = this->add("gcode_comments", coBool);
|
||||
def->label = L("Verbose G-code");
|
||||
def->tooltip = L("Enable this to get a commented G-code file, with each line explained by a descriptive text. "
|
||||
|
||||
@@ -2130,7 +2130,6 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
|
||||
((ConfigOptionPercents, filament_shrinkage_compensation_z))
|
||||
((ConfigOptionBool, gcode_label_objects))
|
||||
((ConfigOptionBool, exclude_object))
|
||||
((ConfigOptionFloat, exclude_object_tolerance))
|
||||
((ConfigOptionFloats, grab_length))
|
||||
((ConfigOptionBool, gcode_comments))
|
||||
((ConfigOptionInt, slow_down_layers))
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
#include "I18N.hpp"
|
||||
#include "Layer.hpp"
|
||||
#include "MutablePolygon.hpp"
|
||||
#include "PerimeterGenerator.hpp"
|
||||
#include "PrintBase.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "SLA/IndexedMesh.hpp"
|
||||
@@ -1465,13 +1464,15 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "top_surface_speed") {
|
||||
// Brim is printed below supports, support invalidates brim and skirt.
|
||||
steps.emplace_back(posSupportMaterial);
|
||||
if (opt_key == "brim_type" || opt_key == "brim_width") {
|
||||
// Regenerate the walls when the brim starts or stops putting the first layer's outer walls first.
|
||||
auto outer_walls_first = [](const ConfigOptionResolver &config) {
|
||||
return brim_puts_outer_walls_first(config.option<ConfigOptionEnum<BrimType>>("brim_type")->value,
|
||||
config.option<ConfigOptionFloat>("brim_width")->value);
|
||||
};
|
||||
if (outer_walls_first(old_config) != outer_walls_first(new_config))
|
||||
if (opt_key == "brim_type") {
|
||||
const auto* old_brim_type = old_config.option<ConfigOptionEnum<BrimType>>(opt_key);
|
||||
const auto* new_brim_type = new_config.option<ConfigOptionEnum<BrimType>>(opt_key);
|
||||
//BBS: When switch to manual brim, the object must have brim, then re-generate perimeter
|
||||
//to make the wall order of first layer to be outer-first
|
||||
// btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it
|
||||
// takes part in the same first-layer wall order rule.
|
||||
if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly ||
|
||||
old_brim_type->value == btLeadingEdgeOnly || new_brim_type->value == btLeadingEdgeOnly)
|
||||
steps.emplace_back(posPerimeters);
|
||||
}
|
||||
} else if (
|
||||
|
||||
@@ -707,10 +707,8 @@ void tree_supports_generate_paths(
|
||||
false);
|
||||
}
|
||||
if (eec) {
|
||||
if (!eec->empty()) {
|
||||
std::reverse(eec->entities.begin(), eec->entities.end());
|
||||
dst.emplace_back(eec.release());
|
||||
}
|
||||
std::reverse(eec->entities.begin(), eec->entities.end());
|
||||
dst.emplace_back(eec.release());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1827,7 +1825,12 @@ void generate_support_toolpaths(
|
||||
no_sort = true;
|
||||
} else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic &&
|
||||
(config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) {
|
||||
tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params);
|
||||
// Orca: A special case for the hollow Organic supports
|
||||
// Orca: If the tree supports are too tall, use a double wall to make it stronger
|
||||
SupportParameters support_params2 = support_params;
|
||||
if (support_layer.print_z > 100.0)
|
||||
support_params2.tree_branch_diameter_double_wall_area_scaled = 0.1;
|
||||
tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params2);
|
||||
done = true;
|
||||
}
|
||||
if (! done)
|
||||
|
||||
@@ -2149,9 +2149,7 @@ void TreeSupport::draw_circles()
|
||||
const bool with_lightning_infill = m_support_params.base_fill_pattern == ipLightning;
|
||||
coordf_t support_extrusion_width = m_support_params.support_extrusion_width;
|
||||
const float tree_brim_width = config.tree_support_brim_width.value;
|
||||
// Reinforce branches moving more than half a support-line width between layers.
|
||||
// Square the threshold for comparison with the squared XY movement below.
|
||||
const double max_move_sq = sqr(double(scale_(m_object_config->support_line_width / 2)));
|
||||
|
||||
// Belt floor: the first object layer is not on a flat bed — it rests on the
|
||||
// tilted, moving belt. So the first-object-layer adhesion features (the tree
|
||||
// support brim, the hybrid first-layer base expansion) must be suppressed:
|
||||
@@ -2203,7 +2201,7 @@ void TreeSupport::draw_circles()
|
||||
size_t first_base_roof_area = 0;
|
||||
bool floor_interface_as_base = false;
|
||||
bool has_circle_node = false;
|
||||
ExPolygons extra_wall_areas;
|
||||
bool need_extra_wall = false;
|
||||
ExPolygons collision_sharp_tails;
|
||||
ExPolygons collision_base;
|
||||
auto get_collision = [&](bool sharp_tail) -> ExPolygons &{
|
||||
@@ -2281,6 +2279,7 @@ void TreeSupport::draw_circles()
|
||||
// area = diff_clipped({ ExPolygon(circle) }, get_collision(node.is_sharp_tail && node.distance_to_top <= 0));
|
||||
|
||||
if (!area.empty()) has_circle_node = true;
|
||||
if (node.need_extra_wall) need_extra_wall = true;
|
||||
|
||||
// merge overhang to get a smoother interface surface
|
||||
// Do not merge when buildplate_only is on, because some underneath nodes may have been deleted.
|
||||
@@ -2295,10 +2294,6 @@ void TreeSupport::draw_circles()
|
||||
}
|
||||
}
|
||||
|
||||
// Use the Pythagorean theorem to compare movement magnitude (dx² + dy²) without a square root.
|
||||
if (node.is_processed && node.movement.cast<double>().squaredNorm() > max_move_sq)
|
||||
append(extra_wall_areas, area);
|
||||
|
||||
if (obj_layer_nr>0 && node.distance_to_top < 0)
|
||||
append(roof_gap_areas, area);
|
||||
else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 &&
|
||||
@@ -2535,8 +2530,7 @@ void TreeSupport::draw_circles()
|
||||
//if (area(expoly) < SQ(scale_(1))) continue;
|
||||
area_groups.emplace_back(&expoly, SupportLayer::BaseType, max_layers_above_base);
|
||||
area_groups.back().need_infill = overlaps({ expoly }, area_poly);
|
||||
area_groups.back().need_extra_wall = !area_groups.back().need_infill &&
|
||||
!extra_wall_areas.empty() && overlaps({expoly}, extra_wall_areas);
|
||||
area_groups.back().need_extra_wall = need_extra_wall && !area_groups.back().need_infill;
|
||||
}
|
||||
for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) {
|
||||
auto &expoly = ts_layer->roof_areas[roof_idx];
|
||||
@@ -3439,6 +3433,11 @@ void TreeSupport::smooth_nodes()
|
||||
}
|
||||
}
|
||||
|
||||
float max_move = scale_(m_object_config->support_line_width / 2);
|
||||
// if the branch is very tall, the tip also needs extra wall
|
||||
float thresh_tall_branch = 100;
|
||||
float thresh_dist_to_top = 30;
|
||||
|
||||
for (int layer_nr = 0; layer_nr< contact_nodes.size(); layer_nr++) {
|
||||
std::vector<SupportNode *> &curr_layer_nodes = contact_nodes[layer_nr];
|
||||
if (curr_layer_nodes.empty()) continue;
|
||||
@@ -3448,17 +3447,20 @@ void TreeSupport::smooth_nodes()
|
||||
std::vector<double> radii;
|
||||
std::vector<SupportNode *> branch;
|
||||
SupportNode * p_node = node;
|
||||
float total_height = 0;
|
||||
// add a fixed head if it's not a polygon node, see STUDIO-4403
|
||||
// Polygon node can't be added because the move distance might be huge, making the nodes in between jump and dangling
|
||||
if (node->child && node->child->type!=ePolygon) {
|
||||
pts.push_back(p_node->child->position);
|
||||
radii.push_back(p_node->child->radius);
|
||||
branch.push_back(p_node->child);
|
||||
total_height += p_node->child->height;
|
||||
}
|
||||
do {
|
||||
pts.push_back(p_node->position);
|
||||
radii.push_back(p_node->radius);
|
||||
branch.push_back(p_node);
|
||||
total_height += p_node->height;
|
||||
p_node = p_node->parent;
|
||||
} while (p_node && !p_node->is_processed);
|
||||
if (pts.size() < 3) continue;
|
||||
@@ -3477,6 +3479,9 @@ void TreeSupport::smooth_nodes()
|
||||
branch[i]->radius = radii1[i];
|
||||
branch[i]->movement = (pts[i + 1] - pts[i - 1]) / 2;
|
||||
branch[i]->is_processed = true;
|
||||
if (branch[i]->parents.size() > 1 || (branch[i]->movement.x() > max_move || branch[i]->movement.y() > max_move) ||
|
||||
(total_height > thresh_tall_branch && branch[i]->dist_mm_to_top < thresh_dist_to_top))
|
||||
branch[i]->need_extra_wall = true;
|
||||
BOOST_LOG_TRIVIAL(info) << "smooth_nodes: layer_nr=" << layer_nr << ", i=" << i << ", pt=" << pt << ", movement=" << branch[i]->movement << ", radius=" << branch[i]->radius;
|
||||
}
|
||||
}
|
||||
@@ -3484,6 +3489,13 @@ void TreeSupport::smooth_nodes()
|
||||
std::swap(pts, pts1);
|
||||
std::swap(radii, radii1);
|
||||
}
|
||||
else {
|
||||
// interpolate need_extra_wall in the end
|
||||
for (size_t i = 1; i < branch.size() - 1; i++) {
|
||||
if (branch[i - 1]->need_extra_wall && branch[i + 1]->need_extra_wall)
|
||||
branch[i]->need_extra_wall = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,7 @@ struct SupportNode
|
||||
TreeNodeType type = eCircle;
|
||||
bool is_corner = false;
|
||||
bool is_processed = false;
|
||||
bool need_extra_wall = false;
|
||||
bool is_sharp_tail = false;
|
||||
bool valid = true;
|
||||
ExPolygon overhang; // when type==ePolygon, set this value to get original overhang area
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "TextureDisplacement.hpp"
|
||||
|
||||
#include <Eigen/Core>
|
||||
#include <Eigen/SVD>
|
||||
#include <Eigen/Geometry>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -283,7 +284,7 @@ bool height_texture_has_color(const TextureDisplacementLayer &layer)
|
||||
return decode_height_texture(layer).has_color();
|
||||
}
|
||||
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded)
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
DecodedHeightTexture result;
|
||||
if (layer.empty())
|
||||
@@ -351,15 +352,13 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
}
|
||||
}
|
||||
|
||||
if (keep_decoded) {
|
||||
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
|
||||
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
|
||||
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
|
||||
auto &entries = g_decoded_texture_cache.entries;
|
||||
for (auto it = entries.begin(); it != entries.end();)
|
||||
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
|
||||
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
|
||||
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
|
||||
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
|
||||
auto &entries = g_decoded_texture_cache.entries;
|
||||
for (auto it = entries.begin(); it != entries.end();)
|
||||
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
|
||||
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
|
||||
}
|
||||
|
||||
// The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before,
|
||||
@@ -724,7 +723,10 @@ V2Resolution recommend_v2_resolution(const indexed_triangle_set
|
||||
const DecodedHeightTexture &tex = decode_height_texture(layer);
|
||||
if (tex.width <= 0)
|
||||
continue;
|
||||
const float texel = layer.tiling_scale / float(tex.width);
|
||||
// Texels are square in both axes (the aspect correction is what keeps them so), so the finer
|
||||
// of the two tile extents sets the finest texel the refinement has to resolve.
|
||||
const Vec2f tile = layer_tile_mm(layer);
|
||||
const float texel = std::min(tile.x(), tile.y()) / float(tex.width);
|
||||
if (out.texel_mm <= 0.f || texel < out.texel_mm) {
|
||||
out.texel_mm = texel;
|
||||
out.pixels_per_edge = analyze_texture_detail(layer).pixels_per_edge;
|
||||
@@ -2166,7 +2168,7 @@ namespace {
|
||||
// of them was most of the per-sample arithmetic.
|
||||
struct UVTransform
|
||||
{
|
||||
float scale, cs, sn, aspect;
|
||||
float inv_u, inv_v, cs, sn, aspect;
|
||||
Vec2f offset;
|
||||
|
||||
// `normalized_uv` drops the two steps that only mean something for a coordinate measured on the
|
||||
@@ -2174,7 +2176,18 @@ struct UVTransform
|
||||
// rotation and offset. See apply_uv_transform(); TextureProjectionMethod::ModelUV is what needs it.
|
||||
UVTransform(const TextureDisplacementLayer &layer, float aspect_, bool normalized_uv = false)
|
||||
{
|
||||
scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
|
||||
// The mirror folds into the reciprocal: it is a negation of the same coordinate, so there is
|
||||
// nothing to do per sample beyond carrying the sign here. A normalized uv has no millimetre
|
||||
// tiling to divide by, but a mirror is still a mirror, so the sign applies either way.
|
||||
const Vec2f sign = layer_mirror_sign(layer);
|
||||
if (normalized_uv) {
|
||||
inv_u = sign.x();
|
||||
inv_v = sign.y();
|
||||
} else {
|
||||
const Vec2f tile = layer_tile_mm(layer);
|
||||
inv_u = sign.x() / tile.x();
|
||||
inv_v = sign.y() / tile.y();
|
||||
}
|
||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||
cs = std::cos(rad);
|
||||
sn = std::sin(rad);
|
||||
@@ -2183,9 +2196,11 @@ struct UVTransform
|
||||
}
|
||||
Vec2f operator()(const Vec2f &planar) const
|
||||
{
|
||||
const Vec2f scaled = planar * scale;
|
||||
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
||||
// See apply_uv_transform() for why the aspect correction follows the rotation.
|
||||
// See apply_uv_transform() for why both the tile extents and the aspect correction follow
|
||||
// the rotation rather than preceding it.
|
||||
Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
|
||||
rotated.x() *= inv_u;
|
||||
rotated.y() *= inv_v;
|
||||
if (aspect > 0.f && aspect != 1.f)
|
||||
rotated.y() *= aspect;
|
||||
return rotated + offset;
|
||||
@@ -2205,26 +2220,43 @@ inline Vec3f triplanar_weights(const Vec3f &normal)
|
||||
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect, bool normalized_uv)
|
||||
{
|
||||
// A uv that is already normalized has no millimetre scale to divide by and no aspect to correct -
|
||||
// see the header. UVTransform does the same, and the two must stay in step.
|
||||
const float scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
|
||||
const Vec2f scaled = planar * scale;
|
||||
|
||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||
const float cs = std::cos(rad);
|
||||
const float sn = std::sin(rad);
|
||||
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
||||
Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
|
||||
|
||||
// The tile extents. Dividing here rather than before the rotation is what makes a non-square
|
||||
// tile a rotated *rectangle* on the surface: scaling the two axes of an already-rotated
|
||||
// coordinate by different amounts would be a shear, so the rotation has to come first and the
|
||||
// extents divide in the tile's own frame. With a square tile the two orders agree exactly,
|
||||
// R being linear - which is why this is not a change in behaviour for any existing layer.
|
||||
//
|
||||
// The mirror rides along as a sign on the same division. It is in the tile's own frame too, so
|
||||
// it mirrors along the tile's axes rather than the world's, and because the offset is what
|
||||
// positions the tile, mirroring about the tile's middle is a matter of the offset the gizmo
|
||||
// writes alongside it, not of anything here.
|
||||
// A uv that is already normalized has no millimetre scale to divide by - see the header - but it
|
||||
// can still be mirrored, so the sign is applied in both cases and only the extents are skipped.
|
||||
// UVTransform does the same, and the two must stay in step.
|
||||
const Vec2f sign = layer_mirror_sign(layer);
|
||||
if (normalized_uv) {
|
||||
rotated.x() *= sign.x();
|
||||
rotated.y() *= sign.y();
|
||||
} else {
|
||||
const Vec2f tile = layer_tile_mm(layer);
|
||||
rotated.x() *= sign.x() / tile.x();
|
||||
rotated.y() *= sign.y() / tile.y();
|
||||
}
|
||||
|
||||
// Non-square textures. Without this the [0,1] square of uv covers the whole image whatever its
|
||||
// proportions, so a 2:1 image is squeezed into a square tile and every feature in it comes out
|
||||
// half as wide as it should be. `tiling_scale` is the tile's size along u; the tile is
|
||||
// `tiling_scale * height / width` mm along v, which is exactly what keeps texels square - so
|
||||
// dividing v by that extent is the same as multiplying it by width / height. A square texture has
|
||||
// aspect 1 and is untouched, which is why this changes nothing for the shipped library.
|
||||
// proportions, so a 2:1 image is squeezed into one tile and every feature in it comes out half
|
||||
// as wide as it should be. The tile is `layer_tile_mm().y() * height / width` mm along v, which
|
||||
// is exactly what keeps texels square - so dividing v by that extent is the same as multiplying
|
||||
// it by width / height. A square texture has aspect 1 and is untouched, which is why this
|
||||
// changes nothing for the shipped library.
|
||||
//
|
||||
// Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a
|
||||
// shear, and doing it the other way round would make "Rotation" skew the pattern instead of
|
||||
// turning it.
|
||||
// Applied after the rotation, like the extents, and for the same reason: scaling one axis of an
|
||||
// already-rotated coordinate before turning it would be a shear.
|
||||
if (!normalized_uv && aspect > 0.f && aspect != 1.f)
|
||||
rotated.y() *= aspect;
|
||||
|
||||
@@ -3568,13 +3600,36 @@ void texture_displacement_patch_frame(const indexed_triangle_set &patch, const s
|
||||
(an.y() <= an.x() && an.y() <= an.z()) ? Vec3f::UnitY() : Vec3f::UnitZ();
|
||||
}
|
||||
|
||||
Matrix3d texture_displacement_frame_rotation(const Transform3d &volume_to_world)
|
||||
{
|
||||
// Separated out by polar decomposition rather than by assuming the linear part is a clean
|
||||
// rotation times a scale: a sheared or mirrored placement is legal here. A reflection is left
|
||||
// behind with the scale on purpose, so a mirrored instance still makes the bake frame's
|
||||
// determinant negative and the bake still knows to flip the normals it derives.
|
||||
const Matrix3d linear = volume_to_world.linear();
|
||||
Eigen::JacobiSVD<Matrix3d> svd(linear, Eigen::ComputeFullU | Eigen::ComputeFullV);
|
||||
Matrix3d rotation = svd.matrixU() * svd.matrixV().transpose();
|
||||
if (rotation.determinant() < 0.0) {
|
||||
Matrix3d u = svd.matrixU();
|
||||
u.col(2) *= -1.0;
|
||||
rotation = u * svd.matrixV().transpose();
|
||||
}
|
||||
return rotation.transpose(); // world -> texture frame
|
||||
}
|
||||
|
||||
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world)
|
||||
{
|
||||
// World orientation and scale, but the origin moved to where the volume's own origin sits: the
|
||||
// texture then rides with the model when it is moved about the plate (and a single, untiled stamp
|
||||
// starts on the model rather than at the plate's corner), while a tile is still `tiling_scale`
|
||||
// printed millimetres whatever the instance's scale.
|
||||
return Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world;
|
||||
// The model's own orientation, world scale, and the origin moved to where the volume's origin
|
||||
// sits. So the texture rides with the model however it is moved *and* however it is turned,
|
||||
// while a tile is still `tiling_scale` printed millimetres whatever the instance's scale, and a
|
||||
// single untiled stamp starts on the model rather than at the plate's corner.
|
||||
//
|
||||
// Orientation used to come from the world too, which meant turning a part on the plate slid the
|
||||
// decoration across its surface. Turning a part is a print-time decision - clearance, supports,
|
||||
// warping - and it should not be a design one.
|
||||
Transform3d unrotate = Transform3d::Identity();
|
||||
unrotate.linear() = texture_displacement_frame_rotation(volume_to_world);
|
||||
return unrotate * Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world;
|
||||
}
|
||||
|
||||
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume)
|
||||
@@ -5785,7 +5840,13 @@ std::string texture_displacement_layers_to_json(const std::vector<TextureDisplac
|
||||
|
||||
j["depth_mm"] = l.depth_mm;
|
||||
j["tiling_scale"] = l.tiling_scale;
|
||||
j["rotation_deg"] = l.rotation_deg;
|
||||
// The v extent and the mirroring. Written unconditionally, including the <= 0 "same as u"
|
||||
// sentinel, so a project says plainly that the tile was square rather than leaving a reader
|
||||
// to infer it from a missing key.
|
||||
j["tiling_scale_v"] = l.tiling_scale_v;
|
||||
j["mirror_u"] = l.mirror_u;
|
||||
j["mirror_v"] = l.mirror_v;
|
||||
j["rotation_deg"] = l.rotation_deg;
|
||||
j["offset"] = vec2_to_json(l.offset);
|
||||
j["invert"] = l.invert;
|
||||
j["midlevel"] = l.midlevel;
|
||||
@@ -5893,6 +5954,12 @@ bool texture_displacement_layers_from_json(const std::string
|
||||
|
||||
read(j, "depth_mm", l.depth_mm);
|
||||
read(j, "tiling_scale", l.tiling_scale);
|
||||
// Absent in a project written before the tile could be rectangular or mirrored, and
|
||||
// then each keeps its default: -1 for the v extent, which layer_tile_mm() reads as
|
||||
// "square", and false for the mirrors. So an older project loads as what it was.
|
||||
read(j, "tiling_scale_v", l.tiling_scale_v);
|
||||
read(j, "mirror_u", l.mirror_u);
|
||||
read(j, "mirror_v", l.mirror_v);
|
||||
read(j, "rotation_deg", l.rotation_deg);
|
||||
if (const auto o = j.find("offset"); o != j.end())
|
||||
l.offset = vec2_from_json(*o, l.offset);
|
||||
|
||||
@@ -225,7 +225,17 @@ struct TextureDisplacementLayer
|
||||
std::shared_ptr<std::vector<unsigned char>> image_data;
|
||||
|
||||
float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm
|
||||
float tiling_scale = 12.5f; // size of one texture tile, in mm
|
||||
float tiling_scale = 12.5f; // size of one texture tile along u, in mm
|
||||
// Size of one tile along v, in mm. Anything <= 0 means "whatever tiling_scale is", which is both
|
||||
// the default and what a layer created before the tile could be non-square meant - so an older
|
||||
// undo snapshot still comes back as the square tile it was.
|
||||
float tiling_scale_v = -1.f;
|
||||
// Mirror the texture along one axis of the tile. Set by dragging a width or height handle back
|
||||
// through the tile's middle, which is how a side handle behaves in an image editor: the extent
|
||||
// cannot go negative, so the sign lives here instead. Both default to off, which is what every
|
||||
// layer written before this meant.
|
||||
bool mirror_u = false;
|
||||
bool mirror_v = false;
|
||||
float rotation_deg = 0.f;
|
||||
Vec2f offset = Vec2f::Zero();
|
||||
bool invert = false;
|
||||
@@ -362,7 +372,8 @@ struct TextureDisplacementLayer
|
||||
static_cast<int>(tile_method), static_cast<int>(projection_method), lscm_seam_angle_deg, islands,
|
||||
static_cast<int>(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right,
|
||||
view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups,
|
||||
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled);
|
||||
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled, tiling_scale_v,
|
||||
mirror_u, mirror_v);
|
||||
}
|
||||
template<class Archive> void load(Archive &ar)
|
||||
{
|
||||
@@ -374,7 +385,7 @@ struct TextureDisplacementLayer
|
||||
tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel,
|
||||
island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing,
|
||||
edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
|
||||
view_project_matrix, color_enabled);
|
||||
view_project_matrix, color_enabled, tiling_scale_v, mirror_u, mirror_v);
|
||||
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
|
||||
tile_method = static_cast<TextureTileMethod>(tile_method_int);
|
||||
projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
|
||||
@@ -383,6 +394,25 @@ struct TextureDisplacementLayer
|
||||
};
|
||||
|
||||
|
||||
// The layer's tile extents in mm, u first. The v extent falls back to the u one whenever
|
||||
// tiling_scale_v is unset, so "unset means square" is written down once and every consumer - the
|
||||
// bake, the preview shaders, the UV pane and the placement gizmo - agrees on what a layer with no
|
||||
// v extent means.
|
||||
inline Vec2f layer_tile_mm(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
const float su = (layer.tiling_scale > 1e-6f) ? layer.tiling_scale : 1.f;
|
||||
return Vec2f(su, (layer.tiling_scale_v > 1e-6f) ? layer.tiling_scale_v : su);
|
||||
}
|
||||
|
||||
// +1 or -1 per axis, for the layer's mirroring. A separate factor from the extents rather than a
|
||||
// signed extent: the extents are a size the panel shows and the refinement measures texels against,
|
||||
// and letting one of them go negative would mean every one of those places had to remember to take
|
||||
// the magnitude.
|
||||
inline Vec2f layer_mirror_sign(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
return Vec2f(layer.mirror_u ? -1.f : 1.f, layer.mirror_v ? -1.f : 1.f);
|
||||
}
|
||||
|
||||
// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next
|
||||
// to texture_displacement_layers and consumed by build_texture_displacement().
|
||||
struct TextureDisplacementOptions
|
||||
@@ -599,9 +629,8 @@ DecodedHeightTexture downscale_height_texture(const DecodedHeightTexture &src, i
|
||||
// colour PNGs are accepted; for a colour source the height is its luminance, using the same
|
||||
// coefficients wxImage::ConvertToGreyscale() uses, so a texture imported as colour displaces exactly
|
||||
// as it did when the importer flattened it to grey on the way in. Returns an empty
|
||||
// DecodedHeightTexture if image_data is empty or is not a PNG at all. keep_decoded = false leaves the
|
||||
// result out of the decode cache, for one-off reads such as a picker thumbnail that must not hold full-size pixels.
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded = true);
|
||||
// DecodedHeightTexture if image_data is empty or is not a PNG at all.
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
|
||||
|
||||
// decode_height_texture(layer).has_color(), answered from the decode cache rather than from a copy of the
|
||||
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
|
||||
@@ -669,14 +698,19 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
|
||||
// projection method, without going through project_texture_displacement_uv()'s own dispatch
|
||||
// (which only knows how to compute the *analytic* methods from a single vertex + normal).
|
||||
// `aspect` is the height map's width / height. It scales the v axis so a non-square image is not
|
||||
// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is
|
||||
// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the
|
||||
// square case and leaves the coordinate exactly as it always was.
|
||||
// squeezed into a square tile: the tile is `layer_tile_mm().y() * height / width` mm along v, which
|
||||
// keeps texels square. 1 (the default) is the square case and leaves the coordinate exactly as it
|
||||
// always was.
|
||||
//
|
||||
// The tile extents divide *after* the rotation, not before. With one extent that made no difference
|
||||
// (R is linear, so R(p / s) == R(p) / s), but with two it is what keeps a non-square tile a rotated
|
||||
// rectangle on the surface instead of a sheared parallelogram. The layer's mirroring is a sign on
|
||||
// that same division.
|
||||
//
|
||||
// `normalized_uv` says the coordinate is already a uv rather than a millimetre measurement on the
|
||||
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tiling scale (mm
|
||||
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tile extents (mm
|
||||
// per tile is meaningless for it) nor the aspect correction (the artist's layout already accounts for
|
||||
// the image's proportions) applies, and only the rotation and offset are left.
|
||||
// the image's proportions) applies, and only the rotation, the mirroring and the offset are left.
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f,
|
||||
bool normalized_uv = false);
|
||||
|
||||
@@ -1048,6 +1082,10 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume);
|
||||
// The frame the bake and the previews project the texture in: `volume_to_world` with its translation
|
||||
// removed, i.e. world orientation and scale about the volume's own origin. See build_texture_displacement().
|
||||
// The rotation that carries a world direction into the texture's frame, i.e. the inverse of the
|
||||
// placement's own rotation. Exposed because the preview shaders rebuild the frame themselves from a
|
||||
// world position and have to turn it the same way the bake does, or the two disagree.
|
||||
Matrix3d texture_displacement_frame_rotation(const Transform3d &volume_to_world);
|
||||
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world);
|
||||
|
||||
// Area-weighted vertex normals of `its` - the directions the bake both projects and displaces along.
|
||||
|
||||
@@ -842,13 +842,6 @@ void load_string_file(const boost::filesystem::path& p, std::string& str);
|
||||
|
||||
bool check_layer_id_pattern(const std::string& pattern, int layer_id);
|
||||
|
||||
// On Windows, return an equivalent path carrying the extended-length ("\\?\" / "\\?\UNC\")
|
||||
// prefix so the Win32 APIs accept it past MAX_PATH even when LongPathsEnabled is off. The path
|
||||
// is made absolute and lexically normalized first, because the prefix disables normalisation.
|
||||
// A path that is already extended (or a device path) is returned unchanged; on other platforms,
|
||||
// or when an absolute path cannot be formed, the input is returned unchanged.
|
||||
boost::filesystem::path extended_path(const boost::filesystem::path& path);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#if WIN32
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||
#include "boost/nowide/cstdio.hpp"
|
||||
#include "boost/nowide/convert.hpp"
|
||||
#endif
|
||||
|
||||
#include "I18N.hpp"
|
||||
@@ -63,11 +62,7 @@ bool open_zip(mz_zip_archive *zip, const char *fname, bool isread)
|
||||
|
||||
FILE *f = nullptr;
|
||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||
// fname is UTF-8 (the nowide contract). Route it through the shared extended-path helper so an
|
||||
// archive stored past MAX_PATH opens even with LongPathsEnabled off.
|
||||
const std::wstring wide_path = boost::nowide::widen(fname);
|
||||
const std::string extended = boost::nowide::narrow(Slic3r::extended_path(boost::filesystem::path(wide_path)).wstring());
|
||||
f = boost::nowide::fopen(extended.c_str(), mode);
|
||||
f = boost::nowide::fopen(fname, mode);
|
||||
#elif defined(__GNUC__) && defined(_LARGEFILE64_SOURCE)
|
||||
f = fopen64(fname, mode);
|
||||
#else
|
||||
@@ -106,14 +101,6 @@ bool close_zip(mz_zip_archive *zip, bool isread)
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
// mz_file_write_func writing an extracted entry into an already-open FILE*.
|
||||
size_t mz_write_to_cfile(void *opaque, mz_uint64 /*file_ofs*/, const void *buffer, size_t size)
|
||||
{
|
||||
return std::fwrite(buffer, 1, size, static_cast<FILE *>(opaque));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool open_zip_reader(mz_zip_archive *zip, const std::string &fname)
|
||||
@@ -129,36 +116,6 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname)
|
||||
bool close_zip_reader(mz_zip_archive *zip) { return close_zip(zip, true); }
|
||||
bool close_zip_writer(mz_zip_archive *zip) { return close_zip(zip, false); }
|
||||
|
||||
bool extract_zip_entry_to_file(mz_zip_archive *zip, mz_uint file_index, const boost::filesystem::path &dest_path)
|
||||
{
|
||||
if (!zip)
|
||||
return false;
|
||||
#ifdef WIN32
|
||||
// Open the destination ourselves with a Windows extended-length path: miniz's narrow
|
||||
// extract_to_file cannot be relied on past MAX_PATH with LongPathsEnabled off, and its wide
|
||||
// variant takes a plain (unprefixed) path and a text-mode stream. This keeps the write binary.
|
||||
FILE *file = ::_wfopen(Slic3r::extended_path(dest_path).wstring().c_str(), L"wb");
|
||||
if (!file) {
|
||||
zip->m_last_error = MZ_ZIP_FILE_OPEN_FAILED;
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool written = mz_zip_reader_extract_to_callback(zip, file_index, mz_write_to_cfile, file, 0) != 0;
|
||||
const bool closed = std::fclose(file) == 0;
|
||||
if (!written || !closed) {
|
||||
if (written && !closed)
|
||||
zip->m_last_error = MZ_ZIP_FILE_CLOSE_FAILED;
|
||||
// Drop the partial entry rather than leave a truncated file behind.
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(Slic3r::extended_path(dest_path), ec);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return mz_zip_reader_extract_to_file(zip, file_index, dest_path.string().c_str(), 0) != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat)
|
||||
{
|
||||
if (stat.m_is_utf8)
|
||||
@@ -199,13 +156,10 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
|
||||
}
|
||||
const std::string dest_file = dest_dir + "/" + stat.m_filename;
|
||||
try {
|
||||
// Use the extended-length form for every filesystem call so entries under a destination
|
||||
// past MAX_PATH are created even with LongPathsEnabled off.
|
||||
const boost::filesystem::path dest_path(dest_file);
|
||||
const boost::filesystem::path io_dest = extended_path(dest_path);
|
||||
if (stat.m_is_directory) {
|
||||
if (!boost::filesystem::exists(io_dest))
|
||||
boost::filesystem::create_directories(io_dest);
|
||||
const boost::filesystem::path dest_path(dest_file);
|
||||
if (!boost::filesystem::exists(dest_path))
|
||||
boost::filesystem::create_directories(dest_path);
|
||||
continue;
|
||||
}
|
||||
if (stat.m_uncomp_size == 0) {
|
||||
@@ -213,9 +167,10 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
|
||||
continue;
|
||||
}
|
||||
// Replace a symlink at the destination rather than writing through it.
|
||||
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(io_dest)))
|
||||
boost::filesystem::remove(io_dest);
|
||||
if (!extract_zip_entry_to_file(&archive, stat.m_file_index, dest_path)) {
|
||||
const boost::filesystem::path dest_path(dest_file);
|
||||
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(dest_path)))
|
||||
boost::filesystem::remove(dest_path);
|
||||
if (!mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
|
||||
close_zip_reader(&archive);
|
||||
return false;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#ifndef MINIZ_EXTENSION_HPP
|
||||
#define MINIZ_EXTENSION_HPP
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <string>
|
||||
#include <miniz.h>
|
||||
|
||||
@@ -14,10 +13,6 @@ bool close_zip_writer(mz_zip_archive *zip);
|
||||
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
|
||||
// Extracts every entry of the archive under dest_dir. Nothing is written if any entry would resolve outside dest_dir.
|
||||
bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir);
|
||||
// Extracts one entry into dest_path, binary-safe and overwriting. On Windows the destination is
|
||||
// opened through an extended-length path so it works past MAX_PATH even with LongPathsEnabled off;
|
||||
// a partial file is removed if the write fails. False for a directory/unsupported entry or on error.
|
||||
bool extract_zip_entry_to_file(mz_zip_archive *zip, mz_uint file_index, const boost::filesystem::path &dest_path);
|
||||
|
||||
class MZ_Archive {
|
||||
public:
|
||||
|
||||
Loaded 100 of 194 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user