Compare commits

..
Author SHA1 Message Date
SoftFever 638dd9f100 Merge branch 'main' into feature/chromastack 2026-10-16 20:29:45 +08:00
ExPikaPaka a72aa236c4 ChromaStack: design doc
docs/HLSD/chromastack.md: what the colour model is and why it is
Beer-Lambert, what the solve does, how a stack attaches to a texture
displacement layer, why the swaps are height ranges, what the preview
shader draws, and the geometry the whole thing assumes.
2026-10-16 11:44:33 +02:00
ExPikaPaka 8baeba1341 ChromaStack: the layer card, the swaps and the preview shader
Add layer asks which kind of layer it is making - a relief layer, a height
map pushed into the surface, or a ChromaStack picture - and the layer
keeps the answer, so a card never shows a control that cannot do anything
on it. Depth, Midlevel, Smoothing, Invert, Edge fade, Tile and Blend are
absent from a picture card: each would move the relief's steps off the
print's layer boundaries, or has nothing to act on. Tile size becomes
Picture size. A picture cannot be reordered, since anything printed over
it buries the colours, and an object can hold only one, refused when the
layer is added rather than when its swaps would overwrite the first one's.

The card is the feature in one block: the picture and the predicted print
side by side, how far the prediction lands from the picture, the base
measured from the painted face, a row per band with the filament's name
and an editable TD, and the colours the stack reaches as a ramp - computed
from the stack alone, so it answers whether these filaments can print this
picture before a solve is spent finding out they cannot.

The swaps go onto the object as height ranges with extruder and
layer_height set, so they are object-relative, saved in the project and
visible in the object list; ObjectList::rebuild_layer_ranges() mirrors
them in the tree without touching the selection, which inside a gizmo
would tear the gizmo down.

The colours exist nowhere else - they are blends of translucent filaments,
not filaments, so the scene's per-triangle filament assignment cannot
express them - so the gizmo draws them itself, with a shader that reads
each fragment's own print Z. A swap happens at one Z across the whole
plate, so that is what decides a surface's colour, a band edge lands on
the layer boundary however coarse the mesh is, and it still works after a
bake when the height map is attached to nothing on screen. On top of the
colour it shades like printed plastic: a wrapped terminator and a grazing
glow scaled by the top filament's TD, and layer lines on the walls, all
fading out through fwidth once a pixel covers more than half a layer.
2026-10-16 11:44:33 +02:00
ExPikaPaka e0a6000d50 ChromaStack: the colour model, and transmission distance per filament
A picture is reproduced on one face by stacking thin layers of a few
filaments, swapping filament at fixed layer boundaries, and varying how
high each pixel is built up. Filament is translucent, so a column that
stops part-way through a run shows a blend of that run's filament and what
is under it - which is where the colours come from, far more of them than
there are filaments loaded.

libslic3r/ChromaStack.hpp holds the whole model, free of the GUI. A stack
is a list of bands (a filament and its layer count, bottom-up) on a base
of solid backing. chroma_palette() turns it into the colour shown at every
column height, by Beer-Lambert absorption with each filament's
transmission distance; chroma_solve() maps every pixel to the height whose
colour is closest in CIELAB, optionally diffusing the rounding error, and
returns the 8-bit height map and the colours it predicts. Beer-Lambert is
the only model that stays consistent applied per layer, so the palette
does not depend on how the stack was cut into bands, and a test asserts
it.

chroma_layers_to_opaque() and chroma_distinct_levels() are what make a
stack's behaviour legible: how many layers a filament needs to hide what
is under it, and how many of a picture's layers change the colour at all.
chroma_ranges() turns the bands into object height ranges, which is how
the slicer is told to print a Z band in another filament.

Transmission distance is a property of the material, so it is a filament
preset option: filament_transmission_distance, with no effect on slicing.
2026-10-16 11:44:33 +02:00
242 changed files with 14579 additions and 30451 deletions

No files matched your search

+4 -4
View File
@@ -1,6 +1,6 @@
---
name: orca-translations
description: Use when updating, filling, reviewing or fixing OrcaSlicer translations in localization/i18n — running scripts/run_gettext --full, translating empty or new msgids in the .po catalogs, AI translation passes and their "# AI Translated" / "# Needs human review" comments, plural forms, msgctxt homonyms, and keeping catalogs consistent with the localization glossary (OrcaSlicer_WIKI 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():
@@ -574,8 +574,7 @@ DMABUF workaround and then branches on `GDK_BACKEND` (an `x11` prefix means the
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`
DMABUF renderer. 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
@@ -568,18 +568,13 @@ 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`:
sets the NVIDIA DMABUF workaround and then branches on `GDK_BACKEND`:
- **Both paths**, before the split: when `/proc/driver/nvidia/version` exists and the linked WebKitGTK
honours the switch (>= 2.44, `WebView::SupportsForceSharedMemory()`), it sets
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WEBKIT_FORCE_DMABUF_RENDERER=1` non-replacing. The
proprietary driver cannot back WebKit's hardware DMABUF renderer, so the shared-memory transport keeps
accelerated compositing instead of dropping it; `WEBKIT_FORCE_DMABUF_RENDERER` clears the
Debian/Ubuntu/openSUSE `isNVIDIA()` early return, which otherwise returns before the FORCE_SHM check.
- **Hypervisor guests**, same position: when `is_hypervisor_guest()` (the `hypervisor` flag in
`/proc/cpuinfo`) and WebKitGTK >= 2.44, it sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` non-replacing.
Virtual GPUs (VirtualBox VMSVGA, VMware, QEMU) cannot back the hardware DMABUF path either, and
FORCE_SHM alone was confirmed to fix web views in VirtualBox; the `WEBKIT_FORCE_DMABUF_RENDERER` pair
is not needed without the Debian `isNVIDIA()` patch. The flag is x86-only, so ARM guests are not covered.
- **Default path** (`GDK_BACKEND` unset or not starting with `x11`): if wx lacks EGL (`!wxHAS_EGL`) and
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`, and in that same branch it sets
`WEBKIT_DISABLE_COMPOSITING_MODE=1` non-replacing when the linked WebKitGTK predates 2.42
@@ -595,9 +590,9 @@ Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_
(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
- **Rule:** On NVIDIA reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver 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
-8
View File
@@ -19,8 +19,6 @@ on:
- 'build_linux.sh'
- 'build_win.bat'
- 'scripts/test_build_win.ps1'
- 'scripts/check_exception_throw_info.ps1'
- 'scripts/test_check_exception_throw_info.ps1'
- 'build_release_macos.sh'
- 'scripts/build_preset_cache.*'
- 'scripts/flatpak/**'
@@ -43,8 +41,6 @@ on:
- 'build_linux.sh'
- 'build_win.bat'
- 'scripts/test_build_win.ps1'
- 'scripts/check_exception_throw_info.ps1'
- 'scripts/test_check_exception_throw_info.ps1'
- 'build_release_macos.sh'
- 'scripts/build_preset_cache.*'
- 'scripts/flatpak/**'
@@ -88,10 +84,6 @@ jobs:
shell: powershell
run: .\scripts\test_build_win.ps1
- name: Run the exception throw info check tests
shell: powershell
run: .\scripts\test_check_exception_throw_info.ps1
build_linux:
strategy:
fail-fast: false
+9 -7
View File
@@ -65,6 +65,15 @@ jobs:
shell: bash
run: |
leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}"
# clang-cl refuses a precompiled header from another cl.exe build and ccache
# does not hash that build, so each one gets its own cache. The build number
# is read from cl.exe itself; the toolset directory keeps its name across patches.
if [ "${{ runner.os }}" = Windows ]; then
vswhere='/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe'
toolset=$(tr -d '\r\n' < "$("$vswhere" -latest -products '*' -find 'VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt' | tr -d '\r')")
cl=$("$vswhere" -latest -products '*' -find 'VC\Tools\MSVC\'"$toolset"'\**\cl.exe' | tr -d '\r' | head -1)
leg="$leg-vc$("$cl" 2>&1 | grep -o -E 'Version [0-9.]+' | cut -d' ' -f2)"
fi
echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV"
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
@@ -543,13 +552,6 @@ jobs:
name: OrcaSlicer_profile_validator_Windows_${{ env.ver }}
path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
- name: Check exception throw info Win
if: runner.os == 'Windows' && inputs.compiler == 'clang'
working-directory: ${{ github.workspace }}
# After the uploads, so a failure still leaves the build and its PDB to inspect, and before any deploy.
shell: pwsh
run: scripts\check_exception_throw_info.ps1 -BuildDir $env:BUILD_DIR
- name: Deploy Windows release portable
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: ./.github/actions/deploy-nightly
-25
View File
@@ -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: |
+3 -58
View File
@@ -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"
+1 -1
View File
@@ -91,7 +91,7 @@ See `.claude/skills/orca-translations/references/source-audit.md` for the full c
### Terminology
- Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated.
- If a term's established translation changes, update both the affected `.po` files and the glossary (`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.
-8
View File
@@ -321,14 +321,6 @@ if (IS_CROSS_COMPILE)
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
endif ()
# With a precompiled header, clang-cl can omit an exception type's copy constructor from its throw info,
# so copies of the exception share and double-free their data: https://github.com/llvm/llvm-project/issues/53486
if (MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
message(STATUS "clang-cl: building without the precompiled header (llvm/llvm-project#53486)")
set_property(CACHE SLIC3R_PCH PROPERTY HELPSTRING "Use precompiled headers (ignored with clang-cl, llvm/llvm-project#53486)")
set(SLIC3R_PCH OFF)
endif ()
# Print out the SLIC3R_* cache options
get_cmake_property(_cache_vars CACHE_VARIABLES)
list (SORT _cache_vars)
+2 -2
View File
@@ -473,8 +473,8 @@ fi
# cmake 4.x compatibility workaround
export CMAKE_POLICY_VERSION_MINIMUM=3.5
DISTRIBUTION=$(awk -F= '/^ID=/ {print $2}' /etc/os-release | tr -d "'\"")
DISTRIBUTION_LIKE=$(awk -F= '/^ID_LIKE=/ {print $2}' /etc/os-release | tr -d "'\"")
DISTRIBUTION=$(awk -F= '/^ID=/ {print $2}' /etc/os-release | tr -d '"')
DISTRIBUTION_LIKE=$(awk -F= '/^ID_LIKE=/ {print $2}' /etc/os-release | tr -d '"')
# Check for direct distribution match to Ubuntu/Debian
if [ "${DISTRIBUTION}" == "ubuntu" ] || [ "${DISTRIBUTION}" == "linuxmint" ] ; then
DISTRIBUTION="debian"
+3 -25
View File
@@ -47,7 +47,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
u )
export BUILD_TARGET="universal"
;;
h ) echo "Usage: ./build_release_macos.sh [-d] [-- CMAKE_ARGS...]"
h ) echo "Usage: ./build_release_macos.sh [-d]"
echo " -d: Build deps only"
echo " -a: Set ARCHITECTURE (arm64 or x86_64 or universal)"
echo " -s: Build slicer only"
@@ -60,33 +60,13 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew"
echo " -j: Set the number of parallel build jobs (CMAKE_BUILD_PARALLEL_LEVEL)"
echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)"
echo " -- CMAKE_ARGS...: pass everything after '--' to the cmake configure step of the deps (-d) and slicer (-s) builds,"
echo " e.g. -DDESTDIR=<dir> for the deps, -DCMAKE_PREFIX_PATH=<dir>/usr/local -DSLIC3R_PCH=OFF for the slicer, --fresh."
echo " They come last on the cmake command line. Use absolute paths: cmake runs in the build directory."
exit 0
;;
: )
echo "Option -${OPTARG} requires an argument, aborting."
exit 1
;;
* )
echo "Unknown option -${OPTARG}, aborting. CMake arguments must follow '--'."
exit 1
;;
esac
done
# getopts stops at '--'; whatever follows is handed to cmake unchanged.
if [[ ${OPTIND} -le $# ]] ; then
separator_index=$((OPTIND-1))
if [[ "${!separator_index}" != "--" ]] ; then
echo "Unexpected argument '${!OPTIND}': cmake arguments must follow '--', aborting."
exit 1
fi
fi
shift $((OPTIND-1))
EXTRA_CMAKE_ARGS=("$@")
# Set defaults
if [ -z "$ARCH" ]; then
@@ -231,8 +211,7 @@ function build_deps() {
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT} \
"${EXTRA_CMAKE_ARGS[@]}"
${CMAKE_POLICY_COMPAT}
fi
cmake --build . --config "$BUILD_CONFIG" --target deps
)
@@ -335,8 +314,7 @@ function build_slicer() {
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT} \
"${EXTRA_CMAKE_ARGS[@]}"
${CMAKE_POLICY_COMPAT}
fi
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
)
+4 -7
View File
@@ -53,7 +53,7 @@ call :add_section "Build configuration"
call :add_arg config string "" config "release, debug, relwithdebinfo or minsizerel (default: release)"
call :add_arg target_arch string "" arch "x64 or arm64 (default: the host architecture)"
call :add_arg slicer_asan bool a asan "Build the slicer with ASAN enabled"
call :add_arg no_pch bool "" no-pch "Build without the precompiled header, implied by -l and --cache"
call :add_arg no_pch bool "" no-pch "Build without the precompiled header, implied by --cache"
call :add_section "Toolchain"
call :add_arg use_clang_cl bool l clang-cl "Use clang-cl as the compiler"
@@ -388,9 +388,6 @@ if "%use_ninja%" == "ON" if "%use_msbuild%" == "ON" (
exit /b 1
)
REM CMakeLists turns the precompiled header off for clang-cl (llvm/llvm-project#53486).
if "%use_clang_cl%" == "ON" set "no_pch=ON"
set "generator=!vs_gen_%vs_version%!"
if "%use_ninja%" == "ON" (
set "generator=Ninja Multi-Config"
@@ -546,7 +543,7 @@ REM real path rather than one glued onto the repository root.
for %%p in ("!build_dir!") do set "build_full=%%~fp"
echo Configuration: %build_type%, %arch%
if not "%clang_exe%" == "" echo Compiler: %clang_exe%
if "%build_slicer%" == "ON" if "%no_pch%" == "ON" echo Precompiled header: off
if "%no_pch%" == "ON" echo Precompiled header: off
if not "%cache_args%" == "" echo Compiler cache: %cache_exe%
set "SIG_FLAG="
@@ -719,8 +716,8 @@ if "%build_slicer%" == "ON" (
set "slicer_args=!slicer_args! -DSLIC3R_ASAN=ON"
)
REM Under cl, a later -DSLIC3R_PCH=ON in ORCA_SLICER_CMAKE_ARGS still wins,
REM because CMake takes the last definition on the command line.
REM A later -DSLIC3R_PCH=ON in ORCA_SLICER_CMAKE_ARGS still wins, because
REM CMake takes the last definition on the command line.
if "%no_pch%" == "ON" (
set "slicer_args=!slicer_args! -DSLIC3R_PCH=OFF"
)
+2 -6
View File
@@ -59,13 +59,9 @@ else ()
set(_cross_compile_arg --host=${TOOLCHAIN_PREFIX})
endif ()
# 6.3.0 is the first release whose arm64 assembly avoids x18, which macOS
# reserves and zeroes on every return to user space; 6.2.1 crashes on Apple
# silicon whenever CGAL falls back to exact arithmetic.
ExternalProject_Add(dep_GMP
URL https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz
https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.3.0/gmp-6.3.0.tar.xz
URL_HASH SHA256=a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898
URL https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.2.1/gmp-6.2.1.tar.bz2
URL_HASH SHA256=eae9326beb4158c386e39a356818031bd28f3124cf915f8c5b1dc4c7a36b4d7c
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/GMP
PATCH_COMMAND git apply ${GMP_DIRECTORY_FLAG} --verbose ${CMAKE_CURRENT_LIST_DIR}/0001-GMP_GCC15.patch
BUILD_IN_SOURCE ON
+2 -5
View File
@@ -26,7 +26,6 @@ orcaslicer_add_cmake_project(OpenCV
URL https://github.com/opencv/opencv/archive/refs/tags/4.6.0.tar.gz
URL_HASH SHA256=1ec1cba65f9f20fe5a41fda1586e01c70ea0c9a6d7b67c9e13edf0cfe2239277
PATCH_COMMAND git apply ${OpenCV_DIRECTORY_FLAG} --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-vs.patch ${CMAKE_CURRENT_LIST_DIR}/0002-clang19-macos.patch
DEPENDS ${JPEG_PKG}
CMAKE_ARGS
-DBUILD_SHARED_LIBS=0
-DBUILD_PERE_TESTS=OFF
@@ -35,8 +34,7 @@ orcaslicer_add_cmake_project(OpenCV
-DBUILD_EXAMPLES=OFF
-DBUILD_JASPER=OFF
-DBUILD_JAVA=OFF
# Share Orca's libjpeg to avoid conflicting codec symbols in the application.
-DBUILD_JPEG=OFF
-DBUILD_JPEG=ON
-DBUILD_APPS_LIST=version
-DBUILD_opencv_apps=OFF
-DBUILD_opencv_java=OFF
@@ -69,8 +67,7 @@ orcaslicer_add_cmake_project(OpenCV
-DWITH_OPENJPEG=OFF
-DWITH_QUIRC=OFF
-DWITH_VTK=OFF
# Textured GLB/OBJ imports decode embedded and external JPEGs with cv::imdecode.
-DWITH_JPEG=ON
-DWITH_JPEG=OFF
-DWITH_WEBP=OFF
-DWITH_TIFF=OFF
-DBUILD_TIFF=OFF
+7 -17
View File
@@ -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
+4 -5
View File
@@ -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.
+1 -9
View File
@@ -21,14 +21,6 @@ else ()
set(_wx_edge "-DwxUSE_WEBVIEW_EDGE=OFF")
endif ()
# With a precompiled header, clang-cl can omit an exception type's copy constructor from its throw info,
# so copies of the exception share and double-free their data: https://github.com/llvm/llvm-project/issues/53486
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
set(_wx_precomp OFF)
else ()
set(_wx_precomp ON)
endif ()
set(_wx_patch_command "")
if (APPLE)
set(_wx_patch_command
@@ -47,7 +39,7 @@ orcaslicer_add_cmake_project(
PATCH_COMMAND ${_wx_patch_command}
DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG}
CMAKE_ARGS
-DwxBUILD_PRECOMP=${_wx_precomp}
-DwxBUILD_PRECOMP=ON
${_wx_toolkit}
"-DCMAKE_DEBUG_POSTFIX:STRING=${_wx_debug_postfix}"
-DwxBUILD_DEBUG_LEVEL=0
@@ -1,8 +1,8 @@
# Adaptive infill — High Level Design
# Adaptive TPMS infill — High Level Design
## Purpose and scope
`adaptive_infill_mode` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
`tpms_adaptive` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
patterns inside the object: the cells grow continuously from the surface
towards the center of the object. `distance_warp`, `smooth_blend` and
`stepped_shells` follow the distance to the nearest surface, including the top
@@ -10,8 +10,8 @@ and bottom, like concentric shells; `lobes` follows the whole 3D shape towards
the center of each lobe of the object; `normal_z`, `normal_y` and `normal_x`
follow each section of the object normal to that axis, so the grading does not
change along the axis, as suits a profile extruded along it.
`sparse_infill_density` is the density at the surface, `adaptive_infill_interior_density`
the density at the center, and `adaptive_infill_gradient` picks how the density
`sparse_infill_density` is the density at the surface, `tpms_interior_density`
the density at the center, and `tpms_adaptive_gradient` picks how the density
goes from one to the other. Only internal sparse infill is graded; the Gyroid
Z-buckling optimization does not apply to it.
@@ -22,9 +22,9 @@ split into shells or blended between densities.
## Radial field
`AdaptiveInfillField` gives every point of an object the center of its lobe and a
`TpmsRadialField` gives every point of an object the center of its lobe and a
radial coordinate: 0 at the center, 1 at the surface along the ray from the
center. `PrintObject::prepare_adaptive_infill_fields()` builds it in
center. `PrintObject::prepare_tpms_radial_fields()` builds it in
`bridge_over_infill()`, next to the adaptive cubic octree, because the anchoring
infill generated there has to match the printed infill. A field is built for
every mode a region uses, and is shared by the regions using that mode: the
@@ -202,7 +202,7 @@ built for them.
## Constraints
- With `adaptive_infill_mode` disabled, or for other patterns, the fill parameters are reset
- With `tpms_adaptive` disabled, or for other patterns, the fill parameters are reset
to their defaults, so they neither change the infill nor split fill batches.
- `Layer::get_sparse_infill_max_void_area()` uses the sparser of the two
densities, as the voids at the center are that large.
+239
View File
@@ -0,0 +1,239 @@
# ChromaStack - High Level Design
## Purpose and scope
ChromaStack prints a colour picture on a single face of a model: a few filaments are printed one on
top of another, the printer swaps filament at fixed layer boundaries, and the *height* of every
pixel decides how many of those layers reach it. Filament is translucent, so a pixel that stops
part-way through a run shows a blend of that run's filament and whatever is under it, which is
where the colours come from - far more of them than there are filaments loaded.
This is that model inside OrcaSlicer, as a mode of a texture displacement layer. The picture and
the filament stack are the input; the output is a grayscale height map and a set of filament swaps.
Nothing about it is a new pipeline: the height map is handed to the layer the user painted, so the
existing projection, preview and bake turn it into real geometry, and the swaps are handed to the
object's height ranges, which is the slicer's own way of printing a Z band in another filament.
It does not import other filament-painting tools' project files, and it does not choose the filaments or
their order - that is the user's artistic decision, and the panel exists to make its result
visible before the print.
## The colour model
`libslic3r/ChromaStack.hpp` holds the whole of it, free of any GUI dependency, which is what lets the
solve run against plain image bytes and be unit tested.
A **stack** (`ChromaStack`) is a list of **bands** (`ChromaBand`), bottom-up: a filament and
how many layers it gets. Alongside them it carries the print's layer height, which the colours
depend on directly, and the **base**: how many layers of solid backing the picture sits on.
The base is not a band. It is a measurement of the model - everything below the painted face, often
a hundred layers or more on a tall part - while a band is a handful of layers the user chooses. It
prints in the bottom band's filament, because that is what is loaded when the print starts, so it
extends that band's height range down to the first layer instead of being a swap of its own. Only
the bands are quantized into the height map, which is why the 255-layer limit
(`CHROMA_MAX_RELIEF_LAYERS`) bounds the picture and not the stack.
Each filament carries its **transmission distance** (TD), the usual measure for how deep
light gets into it: about 1 for an opaque filament, 4 to 7 for a usefully translucent one, 8 and
above for a nearly clear one. TD lives in the filament preset, as
`filament_transmission_distance` - it is a property of the material, so it belongs with the
material and ships with a profile rather than being retyped per project. It has no effect on
slicing.
`chroma_palette()` turns a stack into the colour it shows at every possible column height, which
is all the solver ever needs. The model is Beer-Lambert absorption, layer by layer: a layer of
thickness `h` in a filament of transmission distance `TD` passes
T = exp(-h / (CHROMA_TD_OPAQUE_FRACTION * TD / 3))
of the light from under it and shows its own colour with the rest, so walking up from an opaque
base,
C_k = (1 - T_k) * c_k + T_k * C_(k-1), C_0 = c_1
The constants say what TD means in physical terms: a filament stops showing what is behind it at
about a tenth of its TD number (`CHROMA_TD_OPAQUE_FRACTION`), and three e-folding lengths of
absorption pass 5% of the light, which is where the eye stops seeing through. Those are calibrated
against how the community's TD step prints behave, not derived from first principles - TD is an
empirical scale.
How fast that happens is the single most useful fact about a stack, and `chroma_layers_to_opaque()`
is it: a filament hides what is under it at about a tenth of its TD, so how many *layers* that takes
depends entirely on how thick they are. TD 4 covers in two layers at 0.2 mm and in five at 0.08 mm.
Below about two layers a band has no intermediate colours at all - the picture can only use the
filament or not use it - which is why an otherwise sound stack prints as flat steps at a coarse
layer height. The panel reports the number under the palette ramp, and warns when it falls below
two, because nothing else makes it visible: the filaments look fine, the bands look long enough, and
the result looks like the colour model is wrong when it is the layer height.
How many of a picture's layers actually earn their place is a second question, and
`chroma_distinct_levels()` answers it: a band eventually hides what is under it, and another layer of
it then shows the colour the one below already showed. It is measured against the palette rather than
against TD, because how long a band keeps paying depends on what is under it as much as on the
filament - a light band over a dark one is finished in four or five layers, while a dark one over a
light one goes on moving the colour perceptibly well past the depth at which it is nominally opaque,
since the last few percent of light coming through still shifts a near-black colour. `Trim` shortens
each band to its last layer that still changes anything.
Beer-Lambert is also the only model that stays self-consistent applied per layer: transmittance
multiplies, so a run of n layers of one filament passes `exp(-n h / d)` whether it is composited in
one step or n, and the palette therefore does not depend on how the stack happens to be cut into
bands. A unit test asserts exactly that. Compositing is done on linear light, because the quantity
being attenuated is light rather than an sRGB code.
The base is assumed opaque. That is what makes a column's colour depend only on the layers above it,
and it is why the panel warns when the base is thin.
## The solve
`chroma_solve()` maps each pixel of the picture to the column height whose palette colour is
closest to it, perceptually: both sides go to CIELAB and the match is a CIE76 distance. A 1-D
palette of a few dozen entries makes the extra accuracy of CIEDE2000 hard to see and the cost of it
easy to feel - several hundred million comparisons for a large picture - so the cheaper metric is
the deliberate choice here, unlike the texture colour quantizer, which matches against a
two-dimensional palette of filament mixes and uses a precomputed CIEDE2000 cube.
Optional Floyd-Steinberg error diffusion, serpentine, trades the bands a photograph shows across a
sky or a cheek for fine stipple. Off by default, since stipple is the artifact rather than the cure
for flat-coloured artwork.
Three things come back: the per-pixel layer count, the same thing as an 8-bit grayscale height map
(0 at the base, 255 at the top of the stack), and the palette colours chosen, as an RGB image. The
quantization belongs to the solve rather than to the displacement because the relief's steps have
to land on layer boundaries, and a grey level is the only channel the displacement reads.
The solve also reports the mean and worst colour difference it had to accept. That is the only
honest answer to "will this stack reproduce my picture": under about 5 is a good match, over about
15 means the filaments cannot reach the picture's colours and no number of layers will help.
## How it attaches to a texture displacement layer
A ChromaStack layer is an ordinary `TextureDisplacementLayer` whose height map is computed rather than
imported:
- `chroma_source` keeps the picture the user chose, so the solve can be redone whenever the stack,
the layer height or the picture changes;
- `chroma` is the stack;
- `image_data` - the same field every other layer uses - holds whatever the last solve produced.
So everything downstream needs no changes at all and knows nothing about colour stacks. It also
means the solve writes `depth_mm` (the relief is exactly as tall as the layers above the base) and
pins `invert`, `midlevel` and `smoothing`: blurring a solved height map moves its steps off the
layer boundaries they were solved for.
Colour by filament swap and the layer's own `color_enabled` (one filament per triangle, via
`mmu_segmentation_facets`) are mutually exclusive, and the panel enforces it. They are two
different ways of colouring the same surface, and painting the relief per triangle would hide the
very stack the heights were solved against.
## The filament swaps
`chroma_ranges()` turns the bands into Z bands in the object's own coordinates, merging any two
adjacent bands that share a filament, since nothing is swapped between them. The panel writes them
onto the object as `layer_config_ranges`, each range carrying `extruder` - which
`apply_to_print_region_config()` spreads over every feature filament the range does not otherwise
override, i.e. the whole band prints in that filament - and `layer_height`, both because the solve
is only valid at the height it was run at and because `layer_height_profile_from_ranges()` reads
that option without checking it is there.
Height ranges rather than colour-change custom G-code: they are object-relative, so they survive
the object being moved, they are saved in the project file, and they appear in the object list where
the user can see and edit them. The cost is that an object can only have one set of them, so
applying a stack replaces whatever ranges the object had, with a confirmation when it had any.
`ObjectList::rebuild_layer_ranges()` mirrors them in the object tree without touching the
selection - changing the selection while a gizmo is open tears the gizmo down under the user.
## Geometry the model assumes
A filament swap happens at one Z across the whole plate, and the relief grows along the painted
surface's normal. The two agree only where that surface is horizontal, so:
- the painted patch's height above the bottom of the object is what the base layer count has to
match, and the panel measures it from the patch and offers the number rather than leaving the user
to work it out;
- a patch that is not flat in Z gets a warning, because its colours will only be right where it is
level. The relief itself is still computed, since a gently domed plaque is a legitimate thing to
print and only the user can judge how much error is acceptable.
The model itself is the user's: the stack needs a slab of the base thickness with a face to paint,
and nothing here creates or thickens geometry.
## Seeing the colours
The colours are blends of translucent filaments. No single filament makes them, so they cannot be
expressed as the per-triangle filament assignment the 3D scene paints with, and there is nothing for
the ordinary view to draw. They exist in two places: the sliced preview, which shows the filament
bands as they print, and an overlay the gizmo draws over whatever surface is on screen.
That overlay is a shader, `resources/shaders/*/chromastack.*`, and it reads the colour from each
fragment's own **print Z** rather than from the height map or from anything in the mesh. A filament
swap happens at one Z across the whole plate, so the layer a point sits in is what decides its
colour whatever the picture says. Two things follow. A band edge lands exactly on the layer boundary
however coarse the triangles are - a per-triangle colouring puts it wherever the mesh happens to
have an edge. And it keeps working after a bake, when the relief is real geometry, the paint has
been cleared and the height map is attached to nothing on screen; the overlay is built from the live
preview's displaced mesh before a bake and from the volume's own mesh after one.
The stack's colours reach the shader as a texture of one texel per printed layer, because a base can
be several hundred layers and no uniform array holds that. The mesh carries nothing about the stack,
so editing the stack costs a palette upload rather than a mesh rebuild.
On top of the colour the shader tries to look like printed plastic, because a flat shaded colour
reads as a rendering and the whole point of the overlay is judging a print:
- the terminator is **wrapped** and the grazing angles **glow**, both scaled by the transmission
distance of the filament on top - filament translucent enough to be worth stacking scatters light
rather than reflecting it, and an opaque stack correctly gets neither;
- the walls carry **layer lines**, a groove at every layer boundary that also modulates the
specular, so a wall glints in stripes as a printed one does. Terraces get none: a terrace is one
layer's top surface and has no lines to show.
Both effects are driven by the fragment's height, so they are free of any texture or UV, and both
fade out through `fwidth` once a pixel covers more than about half a layer - lines finer than a
pixel are noise, and a colour read from one layer out of the dozen under a pixel flickers as the
camera moves. The palette lookup slides off the texel centres over the same range so the linear
filter averages the bands a pixel covers instead of picking one of them.
The overlay is drawn pulled forward and without depth writes, the same pass the painted-colour
overlay uses, since it is the same surface as whatever is underneath it.
## What the panel shows
The kind of a layer is the layer's own, chosen when it is added and fixed from then on: **Add layer**
offers *Relief layer* or *ChromaStack picture*, and the card carries its kind as a chip. There is no
panel-wide mode, so one object can hold a picture on its face and a knurl on its rim, and a card
never shows a control that cannot do anything on it.
What the two cards do not share:
| Control | Relief card | ChromaStack card |
| --- | --- | --- |
| Depth | slider | the solve writes it; the card prints the result |
| Tile size | repeats the pattern | **Picture size**, the same number named for what it does |
| Midlevel, Smoothing, Invert, Edge fade | present | absent: each would move the steps off the layer boundaries |
| Colors, Mix filaments, Denoise | one filament per triangle | absent: swaps are how this card colours |
| Tile, tile method | present | absent: one copy, never tiled |
| Blend | four modes | absent: it is the top layer and replaces what is under it |
| Picture, prediction, stack, palette, match, Dither, Solve, Apply swaps | absent | present |
A ChromaStack card has no grip and cannot be reordered: its colours come from swapping filament at a
height, so anything printed above it would bury them. An object may hold only one, refused when the
layer is added rather than when the swaps are applied, because the swaps are the object's own height
ranges and there is one set of those.
The card itself, top to bottom: what it is in one line, the picture and the predicted print side by
side at the same size, the match figures, the base with the measurement from the painted face offered
beside it, a row per band - swatch, filament, layer count, TD - the colours the stack reaches as a
ramp, the totals, dither with the layer height it is solved against, and the two actions. Base and
band counts are typed fields rather than sliders: a base is measured in hundreds of layers and a band
in single digits, and dragging past the end of a slider's range is how a stack silently loses its
backing.
The ramp is computed from the stack alone, before any solve. It is what answers "can these filaments
print this picture" without spending a solve to find out they cannot: a short or flat ramp means the
filaments are too alike, or too opaque, to give the picture anything to work with.
A per-slot flag records that the stack has been edited since the last solve, because a stale height
map looks exactly like a good one.
+2 -5
View File
@@ -350,11 +350,8 @@ identity default and translates nothing. `NetworkAgent` forwards both to the liv
comparison sites below reach them through `wxGetApp().getAgent()` and leave an id untranslated
while no agent is live. Whole documents are Bambu's business alone:
`BBLPrinterAgent::to_orca_payload` and `from_orca_payload` rewrite every string under
`tray_info_idx`, `filament_id`, `filamentId`, `cate` or `fila_id` at any depth; text that does not
parse, or carries none of those keys, comes back unchanged. The last two are the H2C nozzle rack's:
`cate` in the auto nozzle mapping request, `fila_id` in the rack status. A new Bambu message that
carries a filament id under another key needs that key added to `filament_id_keys`, or the id
crosses the boundary untranslated. The map is loaded once, lazily; a missing or malformed
`tray_info_idx`, `filament_id` or `filamentId` at any depth; text that does not parse, or carries
none of those keys, comes back unchanged. The map is loaded once, lazily; a missing or malformed
file degrades to identity with a log line rather than failing.
| Boundary | Where it translates |
-7
View File
@@ -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
-17
View File
@@ -62,23 +62,6 @@ Clipper2 behaves differently by default, the wrapper adjusts it.
clip do not leave slivers.
- Open polylines are clipped with the non-zero rule and keep their direction.
### Tiled booleans
The sweep slows down with the number of edges crossing a scan line, so a layer
cut into thousands of pieces makes every whole-layer boolean expensive.
`diff_ex_by_piece()` and `intersection_ex_by_piece()` take a subject of
non-overlapping `ExPolygons`, group them into tiles with
`ClipperUtils::tile_expolygons()`, and run each tile in parallel against only
the clip polygons near it, cut to the tile's box. Below 128 pieces there is a
single tile, and they are the plain `diff_ex()` / `intersection_ex()`.
The result covers the same area as the plain call. Without the safety offset
the rings are the same. With it, each tile unites only the clip polygons near
it, so a clip edge that the whole-layer union splits where it crosses a distant
clip polygon stays whole, and a crossing with the subject can round 1 unit
differently. The tiles' results are concatenated in tile order, so the order of
the output `ExPolygons` differs from the plain call.
### Offsets
- Before offsetting, input vertices closer than
-198
View File
@@ -1,198 +0,0 @@
# Travel optimization — High Level Design
## Purpose and scope
Inside an island, the infill paths of each region are chained into one print
order by a greedy pass that joins the closest free path ends first. That pass
leaves its longest joins for last, and they often cross the whole island. Two
things shorten the travel between infill paths:
- the fill generators join their lines into as few paths as they can, since
every separate path needs a travel to it;
- the G-code export refines the greedy chain with a local search that shortens
the straight-line travel between the paths.
The refinement covers every chain that `chain_and_reorder_extrusion_entities()`
builds from a start point: the infill and ironing of `GCode::extrude_infill()`
and the support of `GCode::extrude_support()`, as well as the order of objects
and islands under `print_order` Default. It changes only the order and the
direction of the paths, not the paths or the walls. Neither part has a setting.
## The chain
`ObjectByExtruder::Island::Region::append()` flattens every sortable fill
collection into its paths, so the paths of all the fills of a region in one
island are chained together. A collection that must keep its order (`no_sort`:
monotonic, concentric, a forced fill order, the flow calibration) stays a single
entry that cannot be reversed. Paths that must keep their direction cannot be
reversed either: Grid's, since it alternates their direction between layers for
strength, and those of a fill with a forced order. A loop starts and ends at the
same point, so reversing it changes nothing.
`extrude_infill()` chains each region from the current nozzle position with
`chain_and_reorder_extrusion_entities()`, and chains the paths inside a nested
sortable collection the same way; `extrude_support()` chains the support paths
of a layer so too, unless they keep their order. The overload without a start
point, used to chain collections while slicing, keeps the greedy chain. The
order the fill generators give their paths does not reach the G-code for a
sortable fill, which is chained again.
## Connecting the lines
Most patterns cut their lines at the infill boundary, and `Fill::connect_infill()`
joins pairs of line ends that are neighbors on a boundary contour by the arc of
wall between them, shortest arcs first, as long as neither end is joined yet and
the join closes no loop. An arc is joined when it is shorter than
`infill_anchor_max`, or than two anchors and a line width if that is longer:
two ends further apart each get an anchor toward the other, and where those
would meet, the join prints about as much wall as one path instead of two.
Regular patterns leave many arcs of the same length, and the order among them
decides the pairing. In an arbitrary order the pairs along one wall can fall out
of step, such as lines 1–2 and 3–4, then 6–7 and 8–9, which leaves the end of
line 5 loose, and every loose end starts or ends a separate path. So the arcs
are also sorted in steps of half the line spacing and, within a step, along each
contour forwards and backwards, which pairs the ends of a wall in one phase.
Each order is replayed on the flags and a union-find of the lines, without
building any path, and the one that joins the most lines is used; a tie keeps
the shortest-first order.
The replay includes the last pass, which joins any two open ends that are
neighbors on a contour. With multiline infill the main pass leaves the copies of
one line to that pass, so that each copy reaches a wall of its own. Two
neighboring groups cut at both walls can then be joined at either wall by arcs
of the same length: at one, their open ends stay next to their own copies, which
the last pass joins, so each line prints out and back; at the other, the copies
end up in separate paths printed apart.
A line end left unpaired gets an anchor, a piece of the wall at most
`infill_anchor` long. It runs toward the nearest open end of another path,
looking up to 64 ends along the contour, since the next path usually starts
there; without one it takes the side with more free wall. With multiline infill
an anchor toward a copy of the same line stops short of the copy rather than
printing over it. A copy is a neighboring end less than the group width
(`multiline` spacings) across the line, at any wall angle, while the next group
lies a whole line spacing away. Meeting the wall at an angle θ, the copy covers
half a width over sin θ of the wall on each side of its end, so the anchor stops
that far and half its own width before it, or is left out; the end still counts
as anchored, so the last pass pairs the other ends as before.
Last, `Fill::fill_surface_extrusion()` joins the sparse infill lines whose ends
are at most two line spacings apart with a straight piece. With the anchors off
(`infill_anchor_max` 0) nothing above runs and this is the only joining; with
them on, it takes what the steps above leave next to each other, such as two
anchors that stop just short of each other or a short piece cut off by a corner.
A gap that short leaves no room to print anything in between, and as a travel it
is the one hop a reordering can trade for a longer travel that retracts.
Multiline copies leave the most such ends: one spacing apart across the line,
their ends where it meets a wall at an angle θ are one spacing over sin θ apart
along it, so two spacings reach walls met at 30° and steeper.
Ends that meet are joined first, then the gaps between the leading ends of their
lines, along each line's overall direction, then the rest, shortest first within
each, without closing a loop or crossing a line or another join. Taking the
leading ends first joins every pair of copies at the same side, which leaves
their open ends next to each other; by length alone, neighboring pairs were
joined at different walls and the travel between them crossed the island. Lines
that cannot reverse, such as Grid's, only join the end of one to the start of the
next, and fills that keep their order are left alone.
## The search
A chain of up to eight paths is found exactly, by dynamic programming over the
subsets of paths: a state is the paths visited so far, the last one and its
direction, about four thousand states for eight paths. The shared greedy
chaining, `chain_segments_greedy_constrained_reversals_()`, does the same for two
to four segments, which also orders up to four islands of a layer and the loops
of a perimeter collection exactly. Either way the first path is no longer
always the one nearest the start; with a free end, a path near the start can be
worth leaving for last.
Objects (`print_order` Default) and the tour of islands across objects are
visited in the same order on every layer, so `chain_points_with_postprocessing()`
orders them as a closed tour rather than an open chain. Up to ten points the
shortest tour is found exactly by the same dynamic programming with the first
point fixed; with a start point the tour then begins at the point nearest it.
Larger sets take the greedy chain and alternate 2-opt, or-opt and crossing
removal; or-opt moves runs of up to three points next to one of the eight points
nearest an end of the run, found on the same grid as the chain's candidates.
For longer chains, the search refines the greedy chain. On an island cut into many short pieces,
such as infill without anchors, that chain can sweep away from the nozzle and
come back for the rest with one long jump, which no local move below removes.
So a nearest-next chain, going from the nozzle to the nearest free path end each
time, is built too; when it is already shorter than the refined greedy chain, it
is refined and used instead. Elsewhere it costs one grid query per path.
`improve_chain_by_local_search()` sees the chain as a sequence of segments with
an entry and an exit point each. Its cost is the straight distance from the
nozzle to the first entry plus the distance from every exit to the next entry;
the end of the chain is free. It applies three kinds of moves:
- 2-opt reverses a run of segments, which flips each of them and replaces the
two links at the ends of the run.
- Or-opt moves a run of up to three segments between two others, flipped or
not.
- A run of any length that starts or ends next to the nozzle moves to the front.
Local search can settle on a chain that starts with a block of paths far from
the nozzle while the paths next to it sit in the middle, and moving that block
takes more than three segments at once.
A move that would flip a segment that cannot be reversed is skipped, so such
segments are only reordered.
A move is tried only if every link it creates joins an end point to one of the
32 end points nearest to it, which covers every move on a chain of up to 17
segments, as most islands are; with 8 candidates the search missed most of the
gain on grid-like patterns such as Triangles. The candidates come from a uniform
grid of about two end points per cell, searched in rings of cells around the
query. `KDTreeIndirect` visits its subtrees in a fixed order rather than the
nearer one first, so a query for 32 points walks most of the tree, and on long
chains finding the candidates would cost more than the search itself.
The candidates of an end point are tried nearest first, and only while they are
closer to it than the link the move would replace there. A 2-opt move that pays
off creates a link shorter than the one it replaces at one of its two ends, so
this misses no 2-opt move. An or-opt move is tried only while its new link is
shorter than what taking the run out saves, which skips a few or-opt moves that
would still pay off but cuts the search on long chains to a fraction.
The search works through a queue of segments and applies the first move that
shortens the chain around the segment it examines. After a move, only the
segments next to a changed link are queued again. A new link can also make room
for a run elsewhere that no segment next to it would try, so when the queue
empties after any move, every segment is queued once more, and the search ends
when such a sweep finds no move.
Every move shortens the chain by more than `SCALED_EPSILON`, so the result is
never longer than the greedy or the nearest-next chain and the search ends; a
cap of 50 moves per segment only bounds its time. The search has no randomness
and visits the segments in a fixed order, so the same input always gives the
same G-code.
## Cost
Finding the candidates of an end point visits a few cells, and the nearest-next
chain makes one such query per path, which only grows where the paths around the
nozzle are used up and the chain jumps. Examining a segment tries at most a few
hundred candidate links, usually only the nearest few, plus a scan of the chain
for the move to the front when it is the first segment, and a move rewrites only
the range of the chain it changes, so the search stays close to linear in the
number of segments. Most islands hold a few to a few dozen paths; the cost shows
on infill cut into thousands of pieces per island, such as dense infill with the
anchors off. The straight joins only look at the line segments near a candidate
join for crossings.
## Limitations
The search minimizes straight-line distance. It does not know which travels
leave the infill area and retract under `reduce_infill_retraction`, nor the
detours of `reduce_crossing_wall`, and a travel's time is not proportional to
its length: a short travel is bound by acceleration, so its time grows about
with the square root of its length. A chain that is shorter in total but trades
very short travels for longer ones, or for ones past `retraction_minimum_travel`,
can occasionally take a little longer to print.
The end of the chain is free: it is not steered toward the next island or the
start of the next layer. The regions of an island are chained one after the
other, in region order, each from where the previous one ended.
+21 -58
View File
@@ -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
+21 -58
View File
@@ -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
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
+47 -110
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Slic3rPE\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-11 12:37-0300\n"
"Last-Translator: Handle <mail@bysb.net>\n"
"Language-Team: \n"
@@ -1751,7 +1751,7 @@ msgid "Stroke of shape (%1%) contains unsupported: %2%."
msgstr "形状(%1%)的描边包含不受支持的内容:%2%。"
# AI Translated
msgid "Not valid state please report reproduction steps on GitHub"
msgid "Not valid state please report reproduction steps on github"
msgstr "状态无效,请在 GitHub 上报告复现步骤"
# AI Translated
@@ -1759,11 +1759,11 @@ msgid "No embossed file"
msgstr "没有浮雕文件"
# AI Translated
msgid "Missing SVG file in embossed shape"
msgid "Missing svg file in embossed shape"
msgstr "浮雕形状中缺少 SVG 文件"
# AI Translated
msgid "Missing data of SVG file"
msgid "Missing data of svg file"
msgstr "缺少 SVG 文件数据"
# AI Translated
@@ -5008,7 +5008,7 @@ msgid "Unknown C++ exception."
msgstr "未知的 C++ 异常。"
# AI Translated
msgid "Processing G-code from previous file…"
msgid "Processing G-Code from previous file…"
msgstr "正在处理之前文件的 G-code…"
msgid "Slicing complete"
@@ -6699,7 +6699,7 @@ msgstr "体积:"
msgid "Size:"
msgstr "尺寸:"
#, 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 "发现G-code路径在层%d,高度为%.2lf mm处有冲突。请将有冲突的对象分离得更远(%s <-> %s)。"
@@ -12013,10 +12013,6 @@ msgstr "上传文件到打印机存储失败,请重试。"
msgid "Send Failed"
msgstr "发送失败"
# AI Translated
msgid "No available printers"
msgstr "没有可用的打印机"
msgid "The name length exceeds the limit."
msgstr "名称长度超过限制。"
@@ -13926,8 +13922,8 @@ msgid "Left mouse"
msgstr "鼠标左键"
# AI Translated
msgid "Available anywhere in the window."
msgstr "在窗口任意位置可用。"
msgid "Available anywhere in the window, even while typing in a text field."
msgstr "在窗口任意位置可用,即使正在文本框中输入。"
# AI Translated
msgid "Run favorite 1 to 10"
@@ -14043,6 +14039,10 @@ msgstr "%s 已分配给 %s。是否将其重新分配给 %s?"
msgid "The default %s is assigned to %s. Reassign it to %s?"
msgstr "默认的 %s 已分配给 %s。是否将其重新分配给 %s?"
# AI Translated
msgid "Global shortcuts also apply while typing."
msgstr "全局快捷键在输入文字时同样生效。"
# AI Translated
msgid "A key that types a character cannot be a global shortcut."
msgstr "会输入字符的按键不能用作全局快捷键。"
@@ -18086,14 +18086,6 @@ msgstr "对象排除"
msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
msgstr "启用此选项以在G-code中添加EXCLUDE OBJECT命令。"
# AI Translated
msgid "Outline tolerance"
msgstr "轮廓线公差"
# AI Translated
msgid "Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the original outline. Larger values give fewer points, which helps firmware that stalls while parsing G-code with many objects. 0 keeps every point."
msgstr "写入 EXCLUDE_OBJECT_DEFINE 的对象轮廓线与原始轮廓线之间的最大偏差。数值越大,点数越少,有助于避免固件在解析包含大量对象的 G-code 时卡顿。0 表示保留所有点。"
msgid "Verbose G-code"
msgstr "注释G-code"
@@ -24012,7 +24004,7 @@ msgstr "开始测试 %s..."
# AI Translated
#, c-format, boost-format
msgid "test %s IP resolved = %s"
msgid "test %s ip resolved = %s"
msgstr "测试 %s:IP 解析结果 = %s"
# AI Translated
@@ -31506,21 +31498,20 @@ msgid "Basic Colors"
msgstr "基本颜色"
# AI Translated
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "Orca Palette"
msgstr "Orca 调色板"
# AI Translated
#. TRN: Keep the standard/brand name "RAL" unchanged.
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "RAL Classic"
msgstr "RAL 经典色"
# AI Translated
#. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "Gradients"
msgstr "渐变预设"
# AI Translated
msgctxt "Color picker"
msgid "Solid"
msgstr "纯色"
@@ -32019,7 +32010,7 @@ msgid "Medium violet red"
msgstr "中紫罗兰红"
# AI Translated
msgid "Pale violet red"
msgid "PaleV violet red"
msgstr "浅紫罗兰红"
# AI Translated
@@ -32235,7 +32226,8 @@ msgid "Shallow sky"
msgstr "浅空"
# AI Translated
msgid "Electric blue"
# Needs human review: The English “Electric bel” looks like a typo (Electric blue? Electric eel?); the color is #1E90FF, so 电光蓝 was chosen. Recheck once the source name is fixed.
msgid "Electric bel"
msgstr "电光蓝"
# AI Translated
@@ -32327,8 +32319,9 @@ msgid "Maize yellow"
msgstr "玉米黄"
# AI Translated
msgid "Daffodil yellow"
msgstr "水仙黄"
# Needs human review: RAL 1007 is “Daffodil yellow” (水仙黄); the English name in the palette looks wrong. Recheck once the source name is fixed.
msgid "Dalmatian yellow"
msgstr "达尔马提亚黄"
# AI Translated
msgid "Brown beige"
@@ -32675,8 +32668,8 @@ msgid "Turquoise blue"
msgstr "绿松石蓝"
# AI Translated
msgid "Capri blue"
msgstr "卡普里蓝"
msgid "Capsanthin violet"
msgstr "辣椒红素紫"
# AI Translated
msgid "Ocean blue"
@@ -32827,8 +32820,8 @@ msgid "Silver grey"
msgstr "银灰色"
# AI Translated
msgid "Olive grey"
msgstr "橄榄灰"
msgid "Olivestone"
msgstr "橄榄石色"
# AI Translated
msgid "Moss grey"
@@ -33604,25 +33597,13 @@ msgstr "从“我的纹理”中移除"
msgid "Built-in"
msgstr "内置"
# AI Translated
msgid "My textures"
msgstr "我的纹理"
# AI Translated
msgid "No textures found."
msgstr "未找到纹理。"
# AI Translated
msgid "Texture libraries"
msgstr "纹理库"
# AI Translated
msgid "Explore more texture libraries"
msgstr "探索更多纹理库"
# AI Translated
msgid "Opens OrcaCloud in the Home page to browse for plugins."
msgstr "在主页中打开 OrcaCloud,浏览插件。"
msgid "My textures"
msgstr "我的纹理"
# AI Translated
msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer."
@@ -36304,10 +36285,6 @@ msgstr "脚本"
msgid "Slicing Pipeline"
msgstr "切片流水线"
# AI Translated
msgid "Resource Texture"
msgstr "资源纹理"
# AI Translated
msgid "The plugin's configuration UI was empty. Showing the default editor."
msgstr "插件的配置界面为空。正在显示默认编辑器。"
@@ -36456,30 +36433,6 @@ msgstr "插件目录同时包含 .py 和 .whl 入口文件"
msgid "Plugin directory does not contain a .py or .whl entry file"
msgstr "插件目录不包含 .py 或 .whl 入口文件"
# AI Translated
msgid "Resource texture directory is not set"
msgstr "未设置资源纹理目录"
# AI Translated
msgid "Resource texture directory could not be resolved"
msgstr "无法解析资源纹理目录"
# AI Translated
msgid "Resource texture path is not a directory"
msgstr "资源纹理路径不是目录"
# AI Translated
msgid "Plugin folder could not be resolved"
msgstr "无法解析插件文件夹"
# AI Translated
msgid "Resource texture directory is outside the plugin folder"
msgstr "资源纹理目录位于插件文件夹之外"
# AI Translated
msgid "Resource texture directory could not be read"
msgstr "无法读取资源纹理目录"
# AI Translated
#, boost-format
msgid "Slicing pipeline plugin '%1%' error: %2%"
@@ -36938,7 +36891,7 @@ msgstr "加载配置文件“%1%”失败:%2%"
# AI Translated
#. 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 "不是由 %1% 生成的 G-code 文件。"
# AI Translated
@@ -37058,7 +37011,7 @@ msgid "Failed making a temporary copy of G-code file %1% before running a post-p
msgstr "运行后处理脚本之前,无法创建 G-code 文件 %1% 的临时副本:%2%"
# AI Translated
msgid "Post-processor can't find exported G-code file"
msgid "Post-processor can't find exported gcode file"
msgstr "后处理器找不到导出的 G-code 文件"
# AI Translated
@@ -37447,40 +37400,6 @@ msgstr ""
"避免翘曲\n"
"您知道吗?打印ABS这类易翘曲材料时,适当提高热床温度可以降低翘曲的概率。"
# AI Translated
# Needs human review: RAL 1007 is “Daffodil yellow” (水仙黄); the English name in the palette looks wrong. Recheck once the source name is fixed.
#~ msgid "Dalmatian yellow"
#~ msgstr "达尔马提亚黄"
# AI Translated
#~ msgid "Capsanthin violet"
#~ msgstr "辣椒红素紫"
# AI Translated
#~ msgid "Olivestone"
#~ msgstr "橄榄石色"
# AI Translated
#~ msgid "Available anywhere in the window, even while typing in a text field."
#~ msgstr "在窗口任意位置可用,即使正在文本框中输入。"
# AI Translated
#~ msgid "Global shortcuts also apply while typing."
#~ msgstr "全局快捷键在输入文字时同样生效。"
# AI Translated
#~ msgid "Solid"
#~ msgstr "纯色"
# AI Translated
#~ msgid "PaleV violet red"
#~ msgstr "浅紫罗兰红"
# AI Translated
# Needs human review: The English “Electric bel” looks like a typo (Electric blue? Electric eel?); the color is #1E90FF, so 电光蓝 was chosen. Recheck once the source name is fixed.
#~ msgid "Electric bel"
#~ msgstr "电光蓝"
#~ msgid "Please choose the filament color"
#~ msgstr "请选择耗材丝颜色"
@@ -37946,6 +37865,9 @@ msgstr ""
#~ msgid "no configured tool"
#~ msgstr "未配置的工具头"
#~ msgid "Add end G-Code when finishing the entire print."
#~ msgstr "所有打印结束时的结尾G-code"
#~ msgid "Maximum X speed"
#~ msgstr "X最大速度"
@@ -37962,11 +37884,26 @@ msgstr ""
#~ msgid "Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, including a final G-code post-processing step. Research/experimental."
#~ msgstr "在每个切片流水线步骤中调用的 Python 插件,用于读取和修改中间切片数据,包括最终的 G-code 后处理步骤。研究/实验性。"
# AI Translated
#~ msgid "When enabled, the slicer emits g-code with the primary zone's contents centered on bed origin (0,0). The printer firmware is responsible for placing copies/mirrors at each physical zone position. Enable only on printers whose firmware applies its own zone offsets in copy/mirror mode (e.g. RepRapFirmware IDEX duplication mode). Has no effect in Primary mode."
#~ msgstr "启用后,切片软件输出的 G-code 会以热床原点 (0,0) 为中心放置主区域的内容,由打印机固件负责把复制件/镜像件放到各个物理区域。仅当打印机固件在复制/镜像模式下会自行应用区域偏移时才启用(例如 RepRapFirmware 的 IDEX 复制模式)。主模式下无效。"
# AI Translated
# Needs human review: "pem-inversion defaults": confirm pem is the physical extruder map and that the default is its inverse.
#~ msgid "Per-plate override mapping physical heads to filament slots (1-based). Empty means use pem-inversion defaults."
#~ msgstr "按盘单独设置物理工具头到耗材丝槽位的映射(从 1 开始编号),覆盖默认映射。留空则使用由物理挤出机映射(pem)反推的默认值。"
# AI Translated
#~ msgid "Axis the mesh is rotated about before slicing. This is the belt printer's tilt: an isometric (no distortion) rotation that also drives bed rendering and support gravity tilt, and that the g-code back-transform inverts before the machine-frame shear/scale and remap. X is the typical gantry tilt (belt travels along Y)."
#~ msgstr "切片前模型网格的旋转轴。这一旋转即传送带打印机的倾斜:它是保距(无畸变)旋转,同时用于热床渲染和支撑的重力倾斜;生成 G-code 时,逆变换会先还原这一旋转,再进行机器坐标系剪切/缩放和重映射。X 是典型的龙门倾斜轴(传送带沿 Y 方向移动)。"
# AI Translated
#~ msgid "Expert override: set the machine-frame (g-code shear/scale) tilt angle independently of the pre-slice rotation angle. When disabled, the machine-frame transform is derived from the belt tilt angle, so a single angle drives both stages. Enable only to compensate for a machine whose physical gantry tilt differs from the slicing rotation."
#~ msgstr "专家选项:单独设置机器坐标系(G-code 剪切/缩放)的倾斜角度,与切片前的旋转角度互不影响。禁用时,机器坐标系变换由传送带倾斜角度推导,两个阶段使用同一个角度。仅当机器实际的龙门倾角与切片旋转角度不一致时,才需启用此项进行补偿。"
#~ msgid "The maximum deviation allowed when reducing the resolution for the 'Maximum wall resolution' setting. If you increase this, the print will be less accurate, but the G-Code will be smaller. 'Maximum wall deviation' limits 'Maximum wall resolution', so if the two conflict, 'Maximum wall deviation' takes precedence."
#~ msgstr "在为“最大墙体分辨率”设置降低分辨率时所允许的最大偏差。如果增大此值,打印精度会降低,但 G-code 会更小。“最大墙体偏差”会限制“最大墙体分辨率”,因此当两者冲突时,以“最大墙体偏差”为准。"
#~ msgid " not in range "
#~ msgstr " 不在合理范围内"
+48 -111
View File
@@ -9,7 +9,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: 2025-11-28 13:48-0600\n"
"Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n"
"Language-Team: \n"
@@ -1826,7 +1826,7 @@ msgid "Stroke of shape (%1%) contains unsupported: %2%."
msgstr "形狀(%1%)的筆劃包含不支援的內容:%2%。"
# AI Translated
msgid "Not valid state please report reproduction steps on GitHub"
msgid "Not valid state please report reproduction steps on github"
msgstr "狀態無效,請在 GitHub 上回報重現步驟"
# AI Translated
@@ -1834,11 +1834,11 @@ msgid "No embossed file"
msgstr "沒有浮雕檔案"
# AI Translated
msgid "Missing SVG file in embossed shape"
msgid "Missing svg file in embossed shape"
msgstr "浮雕形狀中缺少 SVG 檔案"
# AI Translated
msgid "Missing data of SVG file"
msgid "Missing data of svg file"
msgstr "缺少 SVG 檔案的資料"
msgid "Face the camera"
@@ -5219,7 +5219,7 @@ msgid "Unknown C++ exception."
msgstr "未知的 C++ 例外。"
# AI Translated
msgid "Processing G-code from previous file…"
msgid "Processing G-Code from previous file…"
msgstr "正在處理先前檔案的 G-code…"
msgid "Slicing complete"
@@ -6912,7 +6912,7 @@ msgid "Size:"
msgstr "尺寸:"
# AI Translated
#, 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 "在第 %d 層(Z = %.2lf mm)發現 G-code 路徑衝突。請將有衝突的物件分開得更遠(%s <-> %s)。"
@@ -12298,10 +12298,6 @@ msgstr "無法將檔案上傳至列印裝置的儲存空間,請重試。"
msgid "Send Failed"
msgstr "傳送失敗"
# AI Translated
msgid "No available printers"
msgstr "沒有可用的列印裝置"
msgid "The name length exceeds the limit."
msgstr "名稱長度超過限制。"
@@ -14255,8 +14251,8 @@ msgid "Left mouse"
msgstr "滑鼠左鍵"
# AI Translated
msgid "Available anywhere in the window."
msgstr "在視窗中任何位置皆可使用。"
msgid "Available anywhere in the window, even while typing in a text field."
msgstr "在視窗中任何位置皆可使用,即使正在文字欄位中輸入也可以。"
# AI Translated
msgid "Run favorite 1 to 10"
@@ -14376,6 +14372,10 @@ msgstr "%s 已指派給 %s。要改為指派給 %s 嗎?"
msgid "The default %s is assigned to %s. Reassign it to %s?"
msgstr "預設的 %s 已指派給 %s。要改為指派給 %s 嗎?"
# AI Translated
msgid "Global shortcuts also apply while typing."
msgstr "全域快速鍵在輸入文字時也會生效。"
# AI Translated
msgid "A key that types a character cannot be a global shortcut."
msgstr "會輸入字元的按鍵不能作為全域快速鍵。"
@@ -18513,14 +18513,6 @@ msgstr "物件排除"
msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
msgstr "啟用此選項以在 G-code 中加入 EXCLUDE OBJECT 指令。"
# AI Translated
msgid "Outline tolerance"
msgstr "輪廓線公差"
# AI Translated
msgid "Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the original outline. Larger values give fewer points, which helps firmware that stalls while parsing G-code with many objects. 0 keeps every point."
msgstr "寫入 EXCLUDE_OBJECT_DEFINE 的物件輪廓線與原始輪廓線之間的最大偏差。數值越大,點數越少,有助於避免韌體在解析包含大量物件的 G-code 時卡住。0 表示保留所有點。"
msgid "Verbose G-code"
msgstr "詳細 G-code"
@@ -24605,7 +24597,7 @@ msgstr "測試 %s 開始..."
# AI Translated
#, c-format, boost-format
msgid "test %s IP resolved = %s"
msgid "test %s ip resolved = %s"
msgstr "測試 %s IP 解析結果 = %s"
# AI Translated
@@ -32132,22 +32124,21 @@ msgid "Basic Colors"
msgstr "基本顏色"
# AI Translated
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "Orca Palette"
msgstr "Orca 調色盤"
# AI Translated
#. TRN: Keep the standard/brand name "RAL" unchanged.
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "RAL Classic"
msgstr "RAL 經典色"
# AI Translated
# Needs human review: Palette dropdown entry for the gradient presets (next to 基本顏色 / Orca 調色盤 / RAL 經典色); 漸層配色 keeps it apart from the Solid/Gradient switch label 漸層. Check it reads right in the dropdown.
#. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "Gradients"
msgstr "漸層配色"
# AI Translated
msgctxt "Color picker"
msgid "Solid"
msgstr "純色"
@@ -32646,7 +32637,7 @@ msgid "Medium violet red"
msgstr "中紫羅蘭紅"
# AI Translated
msgid "Pale violet red"
msgid "PaleV violet red"
msgstr "淡紫羅蘭紅"
# AI Translated
@@ -32862,7 +32853,8 @@ msgid "Shallow sky"
msgstr "淺色天空"
# AI Translated
msgid "Electric blue"
# Needs human review: Orca palette: the English is a typo for "Electric blue" (hex #1E90FF); translated as 電光藍. Revisit if the source keeps a different name.
msgid "Electric bel"
msgstr "電光藍"
# AI Translated
@@ -32954,8 +32946,9 @@ msgid "Maize yellow"
msgstr "玉米黃"
# AI Translated
msgid "Daffodil yellow"
msgstr "水仙黃"
# Needs human review: RAL 1007 in the RAL Classic list: the English name looks wrong (1007 is Daffodil yellow, 水仙黃); translated literally. Revisit once the source name is fixed.
msgid "Dalmatian yellow"
msgstr "達爾馬提亞黃"
# AI Translated
msgid "Brown beige"
@@ -33303,8 +33296,9 @@ msgid "Turquoise blue"
msgstr "綠松石藍"
# AI Translated
msgid "Capri blue"
msgstr "卡布里藍"
# Needs human review: RAL 5019 in the RAL Classic list: the English name is not a RAL Classic name (5019 is Capri blue); translated literally. Revisit once the source name is fixed.
msgid "Capsanthin violet"
msgstr "辣椒紅素紫"
# AI Translated
msgid "Ocean blue"
@@ -33455,8 +33449,8 @@ msgid "Silver grey"
msgstr "銀灰"
# AI Translated
msgid "Olive grey"
msgstr "橄欖灰"
msgid "Olivestone"
msgstr "橄欖石色"
# AI Translated
msgid "Moss grey"
@@ -34232,25 +34226,13 @@ msgstr "從「我的紋理」移除"
msgid "Built-in"
msgstr "內建"
# AI Translated
msgid "My textures"
msgstr "我的紋理"
# AI Translated
msgid "No textures found."
msgstr "找不到紋理。"
# AI Translated
msgid "Texture libraries"
msgstr "紋理庫"
# AI Translated
msgid "Explore more texture libraries"
msgstr "探索更多紋理庫"
# AI Translated
msgid "Opens OrcaCloud in the Home page to browse for plugins."
msgstr "在首頁中開啟 OrcaCloud,瀏覽外掛。"
msgid "My textures"
msgstr "我的紋理"
# AI Translated
msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer."
@@ -36946,10 +36928,6 @@ msgstr "指令碼"
msgid "Slicing Pipeline"
msgstr "切片管線"
# AI Translated
msgid "Resource Texture"
msgstr "資源紋理"
# AI Translated
msgid "The plugin's configuration UI was empty. Showing the default editor."
msgstr "外掛的設定介面為空。將顯示預設編輯器。"
@@ -37097,30 +37075,6 @@ msgstr "外掛目錄同時包含 .py 和 .whl 進入點檔案"
msgid "Plugin directory does not contain a .py or .whl entry file"
msgstr "外掛目錄不包含 .py 或 .whl 進入點檔案"
# AI Translated
msgid "Resource texture directory is not set"
msgstr "未設定資源紋理目錄"
# AI Translated
msgid "Resource texture directory could not be resolved"
msgstr "無法解析資源紋理目錄"
# AI Translated
msgid "Resource texture path is not a directory"
msgstr "資源紋理路徑不是目錄"
# AI Translated
msgid "Plugin folder could not be resolved"
msgstr "無法解析外掛資料夾"
# AI Translated
msgid "Resource texture directory is outside the plugin folder"
msgstr "資源紋理目錄位於外掛資料夾之外"
# AI Translated
msgid "Resource texture directory could not be read"
msgstr "無法讀取資源紋理目錄"
# AI Translated
#, boost-format
msgid "Slicing pipeline plugin '%1%' error: %2%"
@@ -37579,7 +37533,7 @@ msgstr "載入設定檔「%1%」失敗:%2%"
# AI Translated
#. 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 "不是由 %1% 產生的 G-code 檔案。"
# AI Translated
@@ -37699,7 +37653,7 @@ msgid "Failed making a temporary copy of G-code file %1% before running a post-p
msgstr "執行後處理腳本前,無法建立 G-code 檔案 %1% 的暫存副本:%2%"
# AI Translated
msgid "Post-processor can't find exported G-code file"
msgid "Post-processor can't find exported gcode file"
msgstr "後處理程式找不到匯出的 G-code 檔案"
# AI Translated
@@ -38095,41 +38049,6 @@ msgstr ""
"避免翹曲\n"
"您知道嗎?當列印容易翹曲的材料(如 ABS)時,適當提高熱床溫度可以降低翹曲的機率。"
# AI Translated
# Needs human review: RAL 1007 in the RAL Classic list: the English name looks wrong (1007 is Daffodil yellow, 水仙黃); translated literally. Revisit once the source name is fixed.
#~ msgid "Dalmatian yellow"
#~ msgstr "達爾馬提亞黃"
# AI Translated
# Needs human review: RAL 5019 in the RAL Classic list: the English name is not a RAL Classic name (5019 is Capri blue); translated literally. Revisit once the source name is fixed.
#~ msgid "Capsanthin violet"
#~ msgstr "辣椒紅素紫"
# AI Translated
#~ msgid "Olivestone"
#~ msgstr "橄欖石色"
# AI Translated
#~ msgid "Available anywhere in the window, even while typing in a text field."
#~ msgstr "在視窗中任何位置皆可使用,即使正在文字欄位中輸入也可以。"
# AI Translated
#~ msgid "Global shortcuts also apply while typing."
#~ msgstr "全域快速鍵在輸入文字時也會生效。"
# AI Translated
#~ msgid "Solid"
#~ msgstr "純色"
# AI Translated
#~ msgid "PaleV violet red"
#~ msgstr "淡紫羅蘭紅"
# AI Translated
# Needs human review: Orca palette: the English is a typo for "Electric blue" (hex #1E90FF); translated as 電光藍. Revisit if the source keeps a different name.
#~ msgid "Electric bel"
#~ msgstr "電光藍"
#~ msgid "Please choose the filament color"
#~ msgstr "請選擇線材顏色"
@@ -38594,6 +38513,9 @@ msgstr ""
#~ msgid "no configured tool"
#~ msgstr "未設定的工具頭"
#~ msgid "Add end G-Code when finishing the entire print."
#~ msgstr "所有列印結束時的結尾 G-code"
#~ msgid "Maximum X speed"
#~ msgstr "X 最大速度"
@@ -38610,10 +38532,25 @@ msgstr ""
#~ msgid "Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, including a final G-code post-processing step. Research/experimental."
#~ msgstr "在每個切片管線步驟呼叫的 Python 外掛,用於讀取與修改中介切片資料,包括最終的 G-code 後處理步驟。研究/實驗性質。"
# AI Translated
#~ msgid "When enabled, the slicer emits g-code with the primary zone's contents centered on bed origin (0,0). The printer firmware is responsible for placing copies/mirrors at each physical zone position. Enable only on printers whose firmware applies its own zone offsets in copy/mirror mode (e.g. RepRapFirmware IDEX duplication mode). Has no effect in Primary mode."
#~ msgstr "啟用後,切片軟體輸出的 G-code 會將主區域的內容以列印板原點 (0,0) 為中心放置,並由列印裝置韌體負責將複製件/鏡像件放到各個實體區域位置。僅在韌體會於複製/鏡像模式下自行套用區域偏移的列印裝置上啟用(例如 RepRapFirmware 的 IDEX 複製模式)。在主模式下無效。"
# AI Translated
#~ msgid "Per-plate override mapping physical heads to filament slots (1-based). Empty means use pem-inversion defaults."
#~ msgstr "依列印板覆寫實體工具頭到線材槽位的映射(從 1 開始編號)。留空表示使用由 pem 反推的預設映射。"
# AI Translated
#~ msgid "Axis the mesh is rotated about before slicing. This is the belt printer's tilt: an isometric (no distortion) rotation that also drives bed rendering and support gravity tilt, and that the g-code back-transform inverts before the machine-frame shear/scale and remap. X is the typical gantry tilt (belt travels along Y)."
#~ msgstr "切片前網格旋轉所繞的軸。這就是輸送帶列印裝置的傾斜:一種等距(無變形)旋轉,同時用於列印板渲染與支撐的重力傾斜;G-code 反向轉換會先還原此旋轉,再進行機器座標系的剪切/縮放與重新映射。X 是典型的龍門傾斜軸(輸送帶沿 Y 方向移動)。"
# AI Translated
#~ msgid "Expert override: set the machine-frame (g-code shear/scale) tilt angle independently of the pre-slice rotation angle. When disabled, the machine-frame transform is derived from the belt tilt angle, so a single angle drives both stages. Enable only to compensate for a machine whose physical gantry tilt differs from the slicing rotation."
#~ msgstr "專家覆寫選項:獨立於切片前的旋轉角度,單獨設定機器座標系(G-code 剪切/縮放)的傾斜角度。停用時,機器座標系變換由輸送帶傾斜角度推導,兩個階段使用同一個角度。僅在機器實際的龍門傾角與切片旋轉角度不同時,才啟用此選項進行補償。"
#~ msgid "The maximum deviation allowed when reducing the resolution for the 'Maximum wall resolution' setting. If you increase this, the print will be less accurate, but the G-Code will be smaller. 'Maximum wall deviation' limits 'Maximum wall resolution', so if the two conflict, 'Maximum wall deviation' takes precedence."
#~ msgstr "「最大牆解析度」設定在降低解析度時所允許的最大偏差。若增加此值,列印會較不精確,但 G-Code 會更小。「最大牆偏差」會限制「最大牆解析度」,因此若兩者衝突,將以「最大牆偏差」為準。"
#~ msgid " not in range "
#~ msgstr " 不在合理的區間"
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "Flashforge",
"url": "",
"version": "02.04.00.12",
"version": "02.04.00.11",
"force_update": "0",
"description": "Flashforge configurations",
"machine_model_list": [
@@ -8,7 +8,6 @@
"compatible_printers": [
"Flashforge Adventurer 5M 0.4 Nozzle"
],
"exclude_object_tolerance": "0.2",
"filename_format": "{input_filename_base}_{filament_type[initial_tool]}_{print_time}.gcode",
"post_process": "",
"only_one_wall_top": "0",
@@ -8,7 +8,6 @@
"compatible_printers": [
"Flashforge Adventurer 5M Pro 0.4 Nozzle"
],
"exclude_object_tolerance": "0.2",
"filename_format": "{input_filename_base}_{filament_type[0]}_{print_time}.gcode",
"post_process": "",
"only_one_wall_top": "0",
@@ -8,7 +8,6 @@
"compatible_printers": [
"Flashforge Adventurer 5M 0.6 Nozzle"
],
"exclude_object_tolerance": "0.2",
"filename_format": "{input_filename_base}_{filament_type[initial_tool]}_{print_time}.gcode",
"post_process": "",
"only_one_wall_top": "0",
@@ -8,7 +8,6 @@
"compatible_printers": [
"Flashforge Adventurer 5M Pro 0.6 Nozzle"
],
"exclude_object_tolerance": "0.2",
"filename_format": "{input_filename_base}_{filament_type[initial_tool]}_{print_time}.gcode",
"post_process": "",
"only_one_wall_top": "0",
+1 -1
View File
@@ -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",
+92
View File
@@ -0,0 +1,92 @@
#version 110
// What a ChromaStack print looks like, shaded per fragment.
//
// The colour is not a vertex attribute and not a texture on the surface: it is read from the
// fragment's own height. A filament swap happens at one print Z across the whole plate, so the layer
// a point sits in is what decides its colour - which means a band edge lands exactly on the layer
// boundary however coarse the mesh is, and the same shader is right before a bake (on the preview's
// displaced mesh) and after one (on the volume's own).
//
// On top of that it tries to look like printed plastic rather than like flat shaded colour: the
// terminator is wrapped and the grazing angles glow, because filament translucent enough to be worth
// stacking scatters light rather than reflecting it; and the walls carry layer lines, because that is
// the first thing the eye uses to read a print as a print.
#define INTENSITY_CORRECTION 0.6
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SHININESS 20.0
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
// The stack's colours, one texel per printed layer, bottom to top. A texture rather than a uniform
// array because a base can be several hundred layers and no uniform array survives that.
uniform sampler2D palette;
uniform float palette_size; // texels in `palette`, i.e. the stack's total printed layers
uniform float base_z; // world Z of the bottom of the object
uniform float layer_height; // mm
// How translucent the stack reads, 0 to 1, from the transmission distance of the filament on top.
// It drives the wrap and the rim: an opaque stack should not glow.
uniform float translucency;
uniform float alpha;
varying vec3 clipping_planes_dots;
varying vec3 eye_normal;
varying vec3 eye_position;
varying vec3 world_normal;
varying float world_z;
void main()
{
if (any(lessThan(clipping_planes_dots, vec3(0.0))))
discard;
float z_rel = world_z - base_z;
float layers = max(z_rel / layer_height, 0.0);
// How much of a layer one pixel covers. Everything below is faded out as this grows: layer lines
// finer than a pixel are noise, and a colour read from one layer out of the dozen under a pixel
// flickers as the camera moves.
float px = fwidth(layers);
// The layer this fragment sits in, 1-based. Up close that is an exact texel of the palette; once a
// pixel spans a layer or more it slides to a continuous coordinate so the linear filter averages
// the bands it covers instead of picking one of them at random.
float stepped = clamp(ceil(layers - 1.0e-4), 1.0, palette_size) - 0.5;
float smooth_ = clamp(layers, 0.5, palette_size - 0.5);
float u = mix(stepped, smooth_, clamp(px - 0.5, 0.0, 1.0)) / palette_size;
vec3 albedo = texture2D(palette, vec2(u, 0.5)).rgb;
vec3 N = normalize(eye_normal);
vec3 V = normalize(-eye_position);
// Layer lines, on the walls only: a terrace is one layer's top surface and has none to show. The
// groove runs round the part at every layer boundary, so it is a function of height alone.
float wall = 1.0 - abs(world_normal.z);
float phase = fract(layers);
float groove = smoothstep(0.0, 0.16, phase) * smoothstep(1.0, 0.84, phase);
groove = mix(groove, 0.65, clamp(px * 1.5, 0.0, 1.0)); // fade to flat when too fine to resolve
float lines = mix(1.0, 0.84 + 0.16 * groove, wall * 0.9);
albedo *= lines;
// Diffuse, with a wrapped terminator: light entering translucent filament comes back out a little
// way round the curve, so the shaded side never goes as dark as an opaque surface's would.
float wrap = 0.25 + 0.45 * translucency;
float ndotl = dot(N, LIGHT_TOP_DIR);
float diffuse = max((ndotl + wrap) / (1.0 + wrap), 0.0) * LIGHT_TOP_DIFFUSE;
diffuse += max((dot(N, LIGHT_FRONT_DIR) + wrap) / (1.0 + wrap), 0.0) * LIGHT_FRONT_DIFFUSE;
// Specular, strongest along a layer line, which is what makes a printed wall glint in stripes
// rather than evenly.
float spec = LIGHT_TOP_SPECULAR * pow(max(dot(V, reflect(-LIGHT_TOP_DIR, N)), 0.0), LIGHT_TOP_SHININESS);
spec *= mix(1.0, 0.35 + 0.65 * groove, wall * 0.9);
// Scattering out of the edges: a translucent part lights up where it is seen at a grazing angle
// and where it is thin. Tinted by the filament, because what comes back out has been through it.
float rim = pow(1.0 - clamp(dot(N, V), 0.0, 1.0), 4.0);
vec3 color = albedo * (INTENSITY_AMBIENT + diffuse) + vec3(spec) + albedo * rim * translucency * 0.55;
gl_FragColor = vec4(color, alpha);
}
+40
View File
@@ -0,0 +1,40 @@
#version 110
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
uniform mat3 view_normal_matrix;
uniform mat4 volume_world_matrix;
// The volume's normal matrix in world space, so the fragment shader can tell a wall from a terrace -
// which is what decides whether layer lines show at all.
uniform mat3 world_normal_matrix;
// Clipping plane, x = min z, y = max z (the FFF/SLA preview's top/bottom cut).
uniform vec2 z_range;
// Clipping plane, general orientation (the section view).
uniform vec4 clipping_plane;
// GLModel's P3N3 layout, which is what init_from(indexed_triangle_set) produces - this shader needs
// nothing per vertex that the mesh does not already carry, because the colour comes from the
// fragment's own height rather than from anything baked into the vertices.
attribute vec3 v_position;
attribute vec3 v_normal;
varying vec3 clipping_planes_dots;
varying vec3 eye_normal;
varying vec3 eye_position;
varying vec3 world_normal;
varying float world_z;
void main()
{
vec4 model_pos = vec4(v_position, 1.0);
vec4 world_pos = volume_world_matrix * model_pos;
world_z = world_pos.z;
world_normal = normalize(world_normal_matrix * v_normal);
eye_normal = normalize(view_normal_matrix * v_normal);
vec4 eye_pos = view_model_matrix * model_pos;
eye_position = eye_pos.xyz;
gl_Position = projection_matrix * eye_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
}
+94
View File
@@ -0,0 +1,94 @@
#version 140
// What a ChromaStack print looks like, shaded per fragment.
//
// The colour is not a vertex attribute and not a texture on the surface: it is read from the
// fragment's own height. A filament swap happens at one print Z across the whole plate, so the layer
// a point sits in is what decides its colour - which means a band edge lands exactly on the layer
// boundary however coarse the mesh is, and the same shader is right before a bake (on the preview's
// displaced mesh) and after one (on the volume's own).
//
// On top of that it tries to look like printed plastic rather than like flat shaded colour: the
// terminator is wrapped and the grazing angles glow, because filament translucent enough to be worth
// stacking scatters light rather than reflecting it; and the walls carry layer lines, because that is
// the first thing the eye uses to read a print as a print.
#define INTENSITY_CORRECTION 0.6
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SHININESS 20.0
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
// The stack's colours, one texel per printed layer, bottom to top. A texture rather than a uniform
// array because a base can be several hundred layers and no uniform array survives that.
uniform sampler2D palette;
uniform float palette_size; // texels in `palette`, i.e. the stack's total printed layers
uniform float base_z; // world Z of the bottom of the object
uniform float layer_height; // mm
// How translucent the stack reads, 0 to 1, from the transmission distance of the filament on top.
// It drives the wrap and the rim: an opaque stack should not glow.
uniform float translucency;
uniform float alpha;
in vec3 clipping_planes_dots;
in vec3 eye_normal;
in vec3 eye_position;
in vec3 world_normal;
in float world_z;
out vec4 out_color;
void main()
{
if (any(lessThan(clipping_planes_dots, vec3(0.0))))
discard;
float z_rel = world_z - base_z;
float layers = max(z_rel / layer_height, 0.0);
// How much of a layer one pixel covers. Everything below is faded out as this grows: layer lines
// finer than a pixel are noise, and a colour read from one layer out of the dozen under a pixel
// flickers as the camera moves.
float px = fwidth(layers);
// The layer this fragment sits in, 1-based. Up close that is an exact texel of the palette; once a
// pixel spans a layer or more it slides to a continuous coordinate so the linear filter averages
// the bands it covers instead of picking one of them at random.
float stepped = clamp(ceil(layers - 1.0e-4), 1.0, palette_size) - 0.5;
float smooth_ = clamp(layers, 0.5, palette_size - 0.5);
float u = mix(stepped, smooth_, clamp(px - 0.5, 0.0, 1.0)) / palette_size;
vec3 albedo = texture(palette, vec2(u, 0.5)).rgb;
vec3 N = normalize(eye_normal);
vec3 V = normalize(-eye_position);
// Layer lines, on the walls only: a terrace is one layer's top surface and has none to show. The
// groove runs round the part at every layer boundary, so it is a function of height alone.
float wall = 1.0 - abs(world_normal.z);
float phase = fract(layers);
float groove = smoothstep(0.0, 0.16, phase) * smoothstep(1.0, 0.84, phase);
groove = mix(groove, 0.65, clamp(px * 1.5, 0.0, 1.0)); // fade to flat when too fine to resolve
float lines = mix(1.0, 0.84 + 0.16 * groove, wall * 0.9);
albedo *= lines;
// Diffuse, with a wrapped terminator: light entering translucent filament comes back out a little
// way round the curve, so the shaded side never goes as dark as an opaque surface's would.
float wrap = 0.25 + 0.45 * translucency;
float ndotl = dot(N, LIGHT_TOP_DIR);
float diffuse = max((ndotl + wrap) / (1.0 + wrap), 0.0) * LIGHT_TOP_DIFFUSE;
diffuse += max((dot(N, LIGHT_FRONT_DIR) + wrap) / (1.0 + wrap), 0.0) * LIGHT_FRONT_DIFFUSE;
// Specular, strongest along a layer line, which is what makes a printed wall glint in stripes
// rather than evenly.
float spec = LIGHT_TOP_SPECULAR * pow(max(dot(V, reflect(-LIGHT_TOP_DIR, N)), 0.0), LIGHT_TOP_SHININESS);
spec *= mix(1.0, 0.35 + 0.65 * groove, wall * 0.9);
// Scattering out of the edges: a translucent part lights up where it is seen at a grazing angle
// and where it is thin. Tinted by the filament, because what comes back out has been through it.
float rim = pow(1.0 - clamp(dot(N, V), 0.0, 1.0), 4.0);
vec3 color = albedo * (INTENSITY_AMBIENT + diffuse) + vec3(spec) + albedo * rim * translucency * 0.55;
out_color = vec4(color, alpha);
}
+40
View File
@@ -0,0 +1,40 @@
#version 140
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
uniform mat3 view_normal_matrix;
uniform mat4 volume_world_matrix;
// The volume's normal matrix in world space, so the fragment shader can tell a wall from a terrace -
// which is what decides whether layer lines show at all.
uniform mat3 world_normal_matrix;
// Clipping plane, x = min z, y = max z (the FFF/SLA preview's top/bottom cut).
uniform vec2 z_range;
// Clipping plane, general orientation (the section view).
uniform vec4 clipping_plane;
// GLModel's P3N3 layout, which is what init_from(indexed_triangle_set) produces - this shader needs
// nothing per vertex that the mesh does not already carry, because the colour comes from the
// fragment's own height rather than from anything baked into the vertices.
in vec3 v_position;
in vec3 v_normal;
out vec3 clipping_planes_dots;
out vec3 eye_normal;
out vec3 eye_position;
out vec3 world_normal;
out float world_z;
void main()
{
vec4 model_pos = vec4(v_position, 1.0);
vec4 world_pos = volume_world_matrix * model_pos;
world_z = world_pos.z;
world_normal = normalize(world_normal_matrix * v_normal);
eye_normal = normalize(view_normal_matrix * v_normal);
vec4 eye_pos = view_model_matrix * model_pos;
eye_position = eye_pos.xyz;
gl_Position = projection_matrix * eye_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
}
@@ -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"},
-126
View File
@@ -1,126 +0,0 @@
<#
.SYNOPSIS
Fails when a Windows build has an exception class with broken throw info.
.DESCRIPTION
Clang on the MSVC ABI can leave the copy constructor out of the CatchableType it emits for
an exception class that comes through a precompiled header
(https://github.com/llvm/llvm-project/issues/53486). The linker keeps one throw info per
class, so when it keeps such a record, a copy of that exception (std::exception_ptr, a catch
by value) shares its data with the original and the data is freed twice.
A class is either trivially copyable or not, so a binary should never have both a CatchableType
without a copy constructor (_CT??_R0?AV<class>@@@8<size>) and one with it
(_CT??_R0?AV<class>@@@8??0<class>...). The script lists each PDB's public symbols with the
llvm-pdbutil next to the build's compiler and reports every class that has both.
scripts\test_check_exception_throw_info.ps1 tests it.
.PARAMETER BuildDir
The slicer build directory. Its CMakeCache.txt names the compiler, and its src\<Config>
holds the PDBs to check, which must include OrcaSlicer.pdb.
.PARAMETER Config
The configuration whose PDBs to check (default: Release).
.PARAMETER Pdb
PDBs to check instead of the ones in <BuildDir>\src\<Config>, as a list or comma-separated.
.PARAMETER Dump
Saved `llvm-pdbutil dump --publics` output to check instead of PDBs. Needs no build.
.EXAMPLE
scripts\check_exception_throw_info.ps1 -BuildDir build
#>
param(
[string] $BuildDir,
[string] $Config = 'Release',
[string[]] $Pdb,
[string[]] $Dump
)
$ErrorActionPreference = 'Stop'
# powershell -File passes "a.pdb,b.pdb" as one string. A path that exists as given stays whole.
function Split-PathList([string[]] $values) {
@($values | Where-Object { $_ } | ForEach-Object {
if ((Test-Path -LiteralPath $_) -or $_ -notmatch ',') { $_ } else { $_ -split ',' | Where-Object { $_ } }
})
}
$Dump = Split-PathList $Dump
if (-not $Dump) {
if (-not $BuildDir) {
throw 'Name the slicer build directory with -BuildDir.'
}
$cache = Join-Path $BuildDir 'CMakeCache.txt'
if (-not (Test-Path $cache)) {
throw "No CMakeCache.txt in $BuildDir. Name the slicer build directory."
}
$entry = Select-String -Path $cache -Pattern '^CMAKE_CXX_COMPILER:\w+=(.+)$' | Select-Object -First 1
if (-not $entry) {
throw "No CMAKE_CXX_COMPILER in $cache."
}
$compiler = $entry.Matches[0].Groups[1].Value
# A tree configured with a bare compiler name found it on PATH.
if (-not (Split-Path $compiler)) {
$found = Get-Command $compiler -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $found) {
throw "$compiler, the compiler in $cache, is not on PATH."
}
$compiler = $found.Source
}
$pdbutil = Join-Path (Split-Path $compiler) 'llvm-pdbutil.exe'
if (-not (Test-Path $pdbutil)) {
throw "No llvm-pdbutil.exe next to $compiler. This check needs a build made with Clang."
}
$Pdb = Split-PathList $Pdb
if (-not $Pdb) {
# OrcaSlicer.dll holds the slicer itself, so a check without it proves nothing.
$pdbDir = Join-Path $BuildDir "src\$Config"
if (-not (Test-Path (Join-Path $pdbDir 'OrcaSlicer.pdb'))) {
throw "No OrcaSlicer.pdb in $pdbDir."
}
$Pdb = @(Get-ChildItem $pdbDir -Filter *.pdb | ForEach-Object FullName)
}
}
$catchable = [regex]'`_CT\?\?_R0\?A[UV](.+?)@@@8([^`]*)`'
$tmp = [IO.Path]::GetTempFileName()
$failed = $false
$classes = 0
try {
foreach ($file in $(if ($Dump) { $Dump } else { $Pdb })) {
$text = $file
if (-not $Dump) {
$run = Start-Process -FilePath $pdbutil -ArgumentList 'dump', '--publics', "`"$file`"" -NoNewWindow -Wait -PassThru -RedirectStandardOutput $tmp
if ($run.ExitCode -ne 0) {
throw "llvm-pdbutil failed on $file."
}
$text = $tmp
}
$kinds = @{}
foreach ($match in $catchable.Matches([IO.File]::ReadAllText((Resolve-Path -LiteralPath $text).Path))) {
$class = $match.Groups[1].Value
if (-not $kinds.ContainsKey($class)) { $kinds[$class] = @{} }
$kinds[$class][$(if ($match.Groups[2].Value.StartsWith('??0')) { 'copy' } else { 'bitwise' })] = $true
}
$broken = @($kinds.Keys | Where-Object { $kinds[$_].Count -eq 2 } | Sort-Object)
"$(Split-Path -Leaf $file): $($kinds.Count) exception classes, $($broken.Count) with broken throw info"
foreach ($class in $broken) { " $class" }
if ($broken.Count -gt 0) { $failed = $true }
$classes += $kinds.Count
}
} finally {
Remove-Item $tmp -ErrorAction SilentlyContinue
}
# The slicer's PDBs always contain exception throw info, so finding none means the dump format or the PDBs changed.
if ($classes -eq 0) {
'No exception classes found. Check that llvm-pdbutil still prints public symbols in backticks.'
$failed = $true
}
exit [int]$failed
@@ -226,10 +226,10 @@ modules:
sha256: f353e7b99bd0cbfc27ac9082de51acf32a8bc0b3e21ff9661ecca6f205ec1d81
dest: external-packages/OpenVDB
# GMP 6.3.0
# GMP 6.2.1
- type: file
url: https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz
sha256: a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898
url: https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.2.1/gmp-6.2.1.tar.bz2
sha256: eae9326beb4158c386e39a356818031bd28f3124cf915f8c5b1dc4c7a36b4d7c
dest: external-packages/GMP
# MPFR 4.2.2
+4 -251
View File
@@ -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)
-4
View File
@@ -301,10 +301,6 @@ $cases = @(
Contains = @('Precompiled header: off') }
@{ Name = 'the precompiled header is on unless asked'; Args = @('-s')
NotContains = @('SLIC3R_PCH') }
@{ Name = '-l turns the precompiled header off'; Args = @('-s', '-l')
Contains = @('-DSLIC3R_PCH=OFF', 'Precompiled header: off') }
@{ Name = 'a deps-only build says nothing about the precompiled header'; Args = @('-d', '-l')
NotContains = @('Precompiled header') }
@{ Name = '--no-pch works without a cache'; Args = @('-s', '--no-pch')
NotContains = @('COMPILER_LAUNCHER') }
@{ Name = 'the slicer build runs gettext'; Args = @('-s')
@@ -1,93 +0,0 @@
<#
.SYNOPSIS
Tests scripts/check_exception_throw_info.ps1 on canned llvm-pdbutil dumps.
.DESCRIPTION
Each case runs the check in its own powershell -File process, as CI does, and asserts on the
exit code and output. Dumps are written to a temporary folder, so no build is needed.
.EXAMPLE
powershell -File scripts/test_check_exception_throw_info.ps1
#>
$ErrorActionPreference = 'Stop'
$check = Join-Path $PSScriptRoot 'check_exception_throw_info.ps1'
$work = Join-Path ([IO.Path]::GetTempPath()) 'check_exception_throw_info_tests'
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $work | Out-Null
function Write-Dump([string] $path, [string[]] $symbols) {
New-Item -ItemType Directory -Force -Path (Split-Path $path) | Out-Null
$i = 0
$lines = @(' Public Symbols', '============================================================')
foreach ($symbol in $symbols) {
$i++
$lines += " $i | S_PUB32 [size = 48] ``$symbol``", ' flags = none, addr = 0007:0000'
}
Set-Content -Path $path -Encoding ascii -Value $lines
}
$bitwise = '_CT??_R0?AVfuture_error@std@@@840'
$copy = '_CT??_R0?AVfuture_error@std@@@8??0future_error@std@@QEAA@AEBV01@@Z40'
$trivial = '_CT??_R0?AUtag@@@81'
Write-Dump "$work\broken.txt" @($bitwise, $copy, $trivial)
Write-Dump "$work\clean.txt" @($copy, $trivial)
Write-Dump "$work\empty.txt" @()
Write-Dump "$work\a dir, with a comma\clean.txt" @($copy)
# A build directory whose compiler has an llvm-pdbutil beside it but no OrcaSlicer.pdb.
# Nothing runs the stand-in; the check stops before it gets there.
$build = "$work\build"
New-Item -ItemType Directory -Force -Path "$build\bin", "$build\src\Release" | Out-Null
Copy-Item "$env:SystemRoot\System32\where.exe" "$build\bin\llvm-pdbutil.exe"
Set-Content -Path "$build\CMakeCache.txt" -Encoding ascii -Value "CMAKE_CXX_COMPILER:FILEPATH=$build\bin\clang-cl.exe"
Set-Content -Path "$build\src\Release\orca-slicer.pdb" -Value ''
$bare = "$work\bare"
New-Item -ItemType Directory -Force -Path $bare | Out-Null
Set-Content -Path "$bare\CMakeCache.txt" -Encoding ascii -Value 'CMAKE_CXX_COMPILER:STRING=no-such-clang-cl.exe'
$cases = @(
@{ Name = 'a class with and without a copy constructor fails'; Args = @('-Dump', "$work\broken.txt")
Exit = 1; Contains = @('1 with broken throw info', 'future_error@std') }
@{ Name = 'copy constructors and trivially copyable classes pass'; Args = @('-Dump', "$work\clean.txt")
Exit = 0; Contains = @('2 exception classes, 0 with broken throw info') }
@{ Name = 'no exception classes at all fails'; Args = @('-Dump', "$work\empty.txt")
Exit = 1; Contains = @('No exception classes found') }
@{ Name = 'a comma-separated list checks each dump'; Args = @('-Dump', "$work\broken.txt,$work\clean.txt")
Exit = 1; Contains = @('broken.txt: 2 exception classes', 'clean.txt: 2 exception classes') }
@{ Name = 'a path with a comma stays whole'; Args = @('-Dump', "$work\a dir, with a comma\clean.txt")
Exit = 0; Contains = @('clean.txt: 1 exception classes') }
@{ Name = 'a build without OrcaSlicer.pdb fails'; Args = @('-BuildDir', $build)
Exit = 1; Contains = @('No OrcaSlicer.pdb in') }
@{ Name = 'a compiler that is not on PATH is named'; Args = @('-BuildDir', $bare)
Exit = 1; Contains = @('no-such-clang-cl.exe, the compiler in') }
@{ Name = 'a missing build directory is named'; Args = @('-BuildDir', "$work\nowhere")
Exit = 1; Contains = @('No CMakeCache.txt in') }
@{ Name = 'no build directory and no dump fails'; Args = @()
Exit = 1; Contains = @('Name the slicer build directory') }
)
$failed = 0
foreach ($case in $cases) {
$ErrorActionPreference = 'Continue'
$output = (& powershell -NoProfile -File $check @($case.Args) 2>&1 | Out-String)
$code = $LASTEXITCODE
$ErrorActionPreference = 'Stop'
$problems = @()
if ($code -ne $case.Exit) { $problems += "exit $code, expected $($case.Exit)" }
foreach ($text in $case.Contains) {
if (-not $output.Contains($text)) { $problems += "no '$text' in the output" }
}
if ($problems) {
$failed++
" FAIL $($case.Name)"
$problems | ForEach-Object { " $_" }
$output.TrimEnd() -split "`r?`n" | ForEach-Object { " | $_" }
} else {
" ok $($case.Name)"
}
}
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
"$($cases.Count - $failed) passed, $failed failed"
exit [int]($failed -gt 0)
+3 -309
View File
@@ -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
# ---------------------------------------------------------------------------
-16
View File
@@ -1126,19 +1126,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.
@@ -1189,9 +1176,6 @@ int CLI::run(int argc, char **argv)
::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.";
+7 -3
View File
@@ -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);
+4 -5
View File
@@ -182,8 +182,8 @@ set(lisbslic3r_sources
Fill/FillPlanePath.hpp
Fill/FillRectilinear.cpp
Fill/FillRectilinear.hpp
Fill/AdaptiveInfillField.cpp
Fill/AdaptiveInfillField.hpp
Fill/FillTpmsAdaptive.cpp
Fill/FillTpmsAdaptive.hpp
Fill/FillTpmsD.cpp
Fill/FillTpmsD.hpp
Fill/FillTpmsFK.cpp
@@ -327,6 +327,8 @@ set(lisbslic3r_sources
Geometry/VoronoiUtils.cpp
Geometry/VoronoiUtils.hpp
Geometry/VoronoiVisualUtils.hpp
ChromaStack.cpp
ChromaStack.hpp
IMEXArrange.cpp
IMEXArrange.hpp
IMEXHelpers.cpp
@@ -666,9 +668,6 @@ endif ()
encoding_check(libslic3r)
target_compile_definitions(libslic3r PUBLIC -DUSE_TBB -DTBB_USE_CAPTURED_EXCEPTION=0)
# CGAL changes Kd_tree's member layout when TBB is enabled. Match libslic3r_cgal
# in every consumer so inline constructors/destructors cannot mix the two layouts.
target_compile_definitions(libslic3r PUBLIC CGAL_LINKED_WITH_TBB)
if (USE_SLIC3R_CONSOLE_LOG)
target_compile_definitions(libslic3r PRIVATE $<$<CONFIG:RelWithDebInfo>:SLIC3R_CONSOLE_LOG>)
endif()
+394
View File
@@ -0,0 +1,394 @@
#include "ChromaStack.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <cstdlib>
#include <numeric>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <miniz.h>
namespace Slic3r {
// How many e-folding lengths of absorption count as "the colour beneath no longer shows". Three
// passes 5% of the light, which is about where the eye stops seeing what is under a layer.
static constexpr float OPAQUE_EFOLDS = 3.f;
// TD is a filament property, so a zero or negative value in a preset is a data error rather than a
// request for an infinitely opaque filament. Clamp instead: below this the filament is opaque at a
// single layer anyway.
static constexpr float TD_MIN_MM = 0.05f;
static constexpr float TD_MAX_MM = 100.f;
static float srgb_to_linear(float c)
{
return c <= 0.04045f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f);
}
static float linear_to_srgb(float c)
{
return c <= 0.0031308f ? c * 12.92f : 1.055f * std::pow(c, 1.f / 2.4f) - 0.055f;
}
// CIELAB under D65, from non-linear sRGB in [0, 1]. Only used for nearest-colour search, so the
// absolute white point matters far less than both sides using the same one.
struct Lab
{
float l = 0.f, a = 0.f, b = 0.f;
};
static Lab srgb_to_lab(float r, float g, float b)
{
const float rl = srgb_to_linear(r), gl = srgb_to_linear(g), bl = srgb_to_linear(b);
const float x = (0.4124f * rl + 0.3576f * gl + 0.1805f * bl) / 0.95047f;
const float y = (0.2126f * rl + 0.7152f * gl + 0.0722f * bl);
const float z = (0.0193f * rl + 0.1192f * gl + 0.9505f * bl) / 1.08883f;
auto f = [](float t) {
return t > 0.008856f ? std::cbrt(t) : (7.787f * t + 16.f / 116.f);
};
const float fx = f(x), fy = f(y), fz = f(z);
return Lab{ 116.f * fy - 16.f, 500.f * (fx - fy), 200.f * (fy - fz) };
}
static float delta_e(const Lab &p, const Lab &q)
{
const float dl = p.l - q.l, da = p.a - q.a, db = p.b - q.b;
return std::sqrt(dl * dl + da * da + db * db);
}
int ChromaStack::relief_layers() const
{
int n = 0;
for (const ChromaBand &band : bands)
n += std::max(0, band.layers);
return n;
}
int ChromaStack::total_layers() const { return std::max(0, base_layers) + relief_layers(); }
float ChromaStack::depth_mm() const { return float(relief_layers()) * layer_height_mm; }
std::vector<ColorRGB> chroma_palette(const ChromaStack &stack)
{
const int total = stack.total_layers();
// No bands means no picture and no filament to print the base in, whatever base_layers says.
if (total <= 0 || stack.bands.empty() || stack.layer_height_mm <= 0.f)
return {};
// The filament each printed layer is in, bottom-up: the base first, in the bottom band's filament,
// then the bands themselves.
std::vector<const ChromaFilament *> per_layer;
per_layer.reserve(size_t(total));
for (int i = 0; i < std::max(0, stack.base_layers); ++i)
per_layer.push_back(&stack.bands.front().filament);
for (const ChromaBand &band : stack.bands)
for (int i = 0; i < band.layers; ++i)
per_layer.push_back(&band.filament);
std::vector<ColorRGB> palette(size_t(total) + 1);
// Linear light, walking up from an opaque base: C_0 is the bottom layer's own colour, so a
// column one layer tall comes out exactly that colour however translucent the filament is.
std::array<float, 3> acc{ srgb_to_linear(per_layer.front()->color.r()), srgb_to_linear(per_layer.front()->color.g()),
srgb_to_linear(per_layer.front()->color.b()) };
auto store = [&palette](size_t k, const std::array<float, 3> &lin) {
palette[k] = ColorRGB(std::clamp(linear_to_srgb(lin[0]), 0.f, 1.f), std::clamp(linear_to_srgb(lin[1]), 0.f, 1.f),
std::clamp(linear_to_srgb(lin[2]), 0.f, 1.f));
};
store(0, acc);
for (int k = 1; k <= total; ++k) {
const ChromaFilament &f = *per_layer[size_t(k - 1)];
const float td = std::clamp(f.td_mm, TD_MIN_MM, TD_MAX_MM);
// The depth at which this filament hides what is behind it, turned into the e-folding length
// of its absorption; see CHROMA_TD_OPAQUE_FRACTION.
const float efold = CHROMA_TD_OPAQUE_FRACTION * td / OPAQUE_EFOLDS;
const float t = std::exp(-stack.layer_height_mm / efold);
const std::array<float, 3> own{ srgb_to_linear(f.color.r()), srgb_to_linear(f.color.g()),
srgb_to_linear(f.color.b()) };
for (int c = 0; c < 3; ++c)
acc[size_t(c)] = (1.f - t) * own[size_t(c)] + t * acc[size_t(c)];
store(size_t(k), acc);
}
return palette;
}
ChromaSolve chroma_solve(const uint8_t *rgb, int width, int height, const ChromaStack &stack)
{
ChromaSolve solve;
const int total = stack.total_layers();
const int base = std::max(0, stack.base_layers);
const int relief = stack.relief_layers();
if (rgb == nullptr || width <= 0 || height <= 0 || relief <= 0 || relief > CHROMA_MAX_RELIEF_LAYERS)
return solve;
const std::vector<ColorRGB> palette = chroma_palette(stack);
if (int(palette.size()) != total + 1)
return solve;
// Only the heights the solver may choose: base (nothing of the relief printed) up to total.
const int choices = relief + 1;
const size_t choice_count = size_t(choices);
std::vector<Lab> choice_lab(choice_count);
std::vector<std::array<uint8_t, 3>> choice_rgb(choice_count);
for (int i = 0; i < choices; ++i) {
const ColorRGB &c = palette[size_t(base + i)];
choice_lab[size_t(i)] = srgb_to_lab(c.r(), c.g(), c.b());
choice_rgb[size_t(i)] = { uint8_t(std::lround(c.r() * 255.f)), uint8_t(std::lround(c.g() * 255.f)),
uint8_t(std::lround(c.b() * 255.f)) };
}
const size_t count = size_t(width) * size_t(height);
solve.width = width;
solve.height = height;
solve.levels.resize(count);
solve.grey.resize(count);
solve.preview.resize(count * 3);
std::vector<float> errors(count, 0.f);
auto pick = [&](float r, float g, float b, float &out_error) {
const Lab target = srgb_to_lab(std::clamp(r, 0.f, 1.f), std::clamp(g, 0.f, 1.f), std::clamp(b, 0.f, 1.f));
int best = 0;
float best_d = std::numeric_limits<float>::max();
for (int i = 0; i < choices; ++i) {
const float d = delta_e(target, choice_lab[size_t(i)]);
if (d < best_d) {
best_d = d;
best = i;
}
}
out_error = best_d;
return best;
};
// The grey level a chosen height is written as. Rounded to the nearest of 256 levels, which for
// any practical layer budget (a few dozen) maps every height to its own level exactly - the
// relief's steps stay on layer boundaries, which is the whole point of quantizing here.
auto grey_of = [relief](int choice) {
return uint8_t(std::lround(255.0 * double(choice) / double(relief)));
};
auto write = [&](size_t px, int choice, float err) {
solve.levels[px] = uint16_t(base + choice);
solve.grey[px] = grey_of(choice);
solve.preview[px * 3] = choice_rgb[size_t(choice)][0];
solve.preview[px * 3 + 1] = choice_rgb[size_t(choice)][1];
solve.preview[px * 3 + 2] = choice_rgb[size_t(choice)][2];
errors[px] = err;
};
if (stack.dither) {
// Floyd-Steinberg, serpentine so the error does not drift consistently to one side. The
// diffusion buffer holds the two rows the kernel reaches, in sRGB - which is where the
// artifact being traded away lives, and what every reference implementation diffuses in.
const size_t row_len = size_t(width);
std::vector<std::array<float, 3>> row(row_len), next(row_len);
for (int x = 0; x < width; ++x) {
const uint8_t *src = rgb + size_t(x) * 3;
row[size_t(x)] = { src[0] / 255.f, src[1] / 255.f, src[2] / 255.f };
}
for (int y = 0; y < height; ++y) {
if (y + 1 < height)
for (int x = 0; x < width; ++x) {
const uint8_t *src = rgb + (size_t(y + 1) * size_t(width) + size_t(x)) * 3;
next[size_t(x)] = { src[0] / 255.f, src[1] / 255.f, src[2] / 255.f };
}
const bool ltr = (y % 2) == 0;
for (int i = 0; i < width; ++i) {
const int x = ltr ? i : width - 1 - i;
const size_t px = size_t(y) * size_t(width) + size_t(x);
float err = 0.f;
const int choice = pick(row[size_t(x)][0], row[size_t(x)][1], row[size_t(x)][2], err);
write(px, choice, err);
std::array<float, 3> residual{};
for (int c = 0; c < 3; ++c)
residual[size_t(c)] = row[size_t(x)][size_t(c)] - choice_rgb[size_t(choice)][size_t(c)] / 255.f;
const int ahead = ltr ? x + 1 : x - 1;
const int back = ltr ? x - 1 : x + 1;
auto spread = [&](std::vector<std::array<float, 3>> &dst, int at, float w) {
if (at < 0 || at >= width)
return;
for (int c = 0; c < 3; ++c)
dst[size_t(at)][size_t(c)] += residual[size_t(c)] * w;
};
spread(row, ahead, 7.f / 16.f);
if (y + 1 < height) {
spread(next, back, 3.f / 16.f);
spread(next, x, 5.f / 16.f);
spread(next, ahead, 1.f / 16.f);
}
}
row.swap(next);
}
} else {
tbb::parallel_for(tbb::blocked_range<int>(0, height), [&](const tbb::blocked_range<int> &range) {
for (int y = range.begin(); y < range.end(); ++y)
for (int x = 0; x < width; ++x) {
const size_t px = size_t(y) * size_t(width) + size_t(x);
const uint8_t *src = rgb + px * 3;
float err = 0.f;
write(px, pick(src[0] / 255.f, src[1] / 255.f, src[2] / 255.f, err), err);
}
});
}
solve.mean_delta_e = count == 0 ? 0. : std::accumulate(errors.begin(), errors.end(), 0.) / double(count);
solve.max_delta_e = errors.empty() ? 0. : double(*std::max_element(errors.begin(), errors.end()));
return solve;
}
float chroma_layers_to_opaque(float td_mm, float layer_height_mm)
{
if (layer_height_mm <= 0.f)
return 0.f;
return CHROMA_TD_OPAQUE_FRACTION * std::clamp(td_mm, TD_MIN_MM, TD_MAX_MM) / layer_height_mm;
}
// Below about one dE two colours are the same colour to the eye. Half of that is the threshold for
// "this layer changed something", so a step has to be comfortably invisible before a layer is called
// wasted.
static constexpr float VISIBLE_DE = 0.5f;
static float step_delta_e(const ColorRGB &a, const ColorRGB &b)
{
return delta_e(srgb_to_lab(a.r(), a.g(), a.b()), srgb_to_lab(b.r(), b.g(), b.b()));
}
int chroma_distinct_levels(const ChromaStack &stack)
{
const std::vector<ColorRGB> palette = chroma_palette(stack);
const int base = std::max(0, stack.base_layers);
if (int(palette.size()) <= base + 1)
return 0;
int n = 0;
for (size_t i = size_t(base) + 1; i < palette.size(); ++i)
if (step_delta_e(palette[i - 1], palette[i]) > VISIBLE_DE)
++n;
return n;
}
void chroma_trim_bands(ChromaStack &stack)
{
// Walked over the stack's own palette rather than over the filaments' TD, because how fast a band
// runs out of anything to say depends on what is under it as much as on the filament. A light band
// over a dark one is finished in four or five layers; a dark one over a light one goes on paying
// for many more, long past the depth at which it is nominally opaque, because the last few percent
// of light coming through still moves a near-black colour perceptibly.
const std::vector<ColorRGB> palette = chroma_palette(stack);
if (palette.empty())
return;
int layer = std::max(0, stack.base_layers); // palette index of the layer under the band being looked at
for (ChromaBand &band : stack.bands) {
int keep = 1;
for (int i = 1; i <= band.layers; ++i) {
const size_t cur = size_t(layer) + size_t(i);
if (cur >= palette.size())
break;
if (step_delta_e(palette[cur - 1], palette[cur]) > VISIBLE_DE)
keep = i; // the last layer of this band that still changes the colour
}
layer += band.layers;
band.layers = std::clamp(keep, 1, band.layers);
}
}
int chroma_level_at(const ChromaStack &stack, double z_mm)
{
const int total = stack.total_layers();
if (total <= 0 || stack.layer_height_mm <= 0.f)
return 0;
// Layer k occupies ((k-1)h, k h], so a point inside it rounds up. The epsilon keeps a surface
// sitting exactly on a layer boundary in the layer that printed it rather than in the one above,
// which is the difference between a terrace showing its own colour and showing the next band's.
const int k = int(std::ceil(z_mm / double(stack.layer_height_mm) - 1e-6));
return std::clamp(k, 1, total);
}
std::vector<unsigned char> chroma_encode_height_png(const ChromaSolve &solve)
{
if (solve.empty())
return {};
size_t size = 0;
void *png = tdefl_write_image_to_png_file_in_memory_ex(solve.grey.data(), solve.width, solve.height, 1, &size,
MZ_DEFAULT_LEVEL, MZ_FALSE);
if (png == nullptr || size == 0)
return {};
const unsigned char *bytes = static_cast<const unsigned char *>(png);
std::vector<unsigned char> out(bytes, bytes + size);
mz_free(png);
return out;
}
std::vector<ChromaRange> chroma_ranges(const ChromaStack &stack, double z_bottom)
{
std::vector<ChromaRange> out;
if (stack.layer_height_mm <= 0.f)
return out;
// The layer height is stored as a float, like every other dimension of a texture displacement
// layer, so multiplying it up leaves noise in the last few digits - 5 layers of 0.1 mm come out as
// 0.5000000074. The slicer derives its own layer boundaries from the print config in double
// precision and lands on 0.5 exactly, and a boundary that misses it by any amount is a boundary a
// layer can fall on the wrong side of. Rounding to the nanometre removes the noise without
// disturbing any layer height a profile can express.
const auto snap = [](double z) { return std::round(z * 1e6) / 1e6; };
// The base is printed in the bottom band's filament - it is what is loaded when the print starts -
// so it extends that band's range down to the first layer instead of being a swap of its own.
int layer = std::max(0, stack.base_layers);
bool first = true;
for (const ChromaBand &band : stack.bands) {
if (band.layers <= 0)
continue;
const double z_min = snap(z_bottom + (first ? 0. : double(layer) * double(stack.layer_height_mm)));
first = false;
layer += band.layers;
const double z_max = snap(z_bottom + double(layer) * double(stack.layer_height_mm));
if (!out.empty() && out.back().filament == band.filament.filament)
out.back().z_max = z_max; // no swap happens here, so it is one range
else
out.push_back(ChromaRange{ z_min, z_max, band.filament.filament });
}
return out;
}
ChromaStack chroma_default_stack(const std::vector<ChromaFilament> &filaments, float layer_height_mm,
int base_layers, int relief_layers)
{
ChromaStack stack;
stack.layer_height_mm = layer_height_mm > 0.f ? layer_height_mm : 0.08f;
stack.base_layers = std::max(0, base_layers);
if (filaments.empty() || relief_layers <= 0)
return stack;
std::vector<ChromaFilament> ordered = filaments;
std::stable_sort(ordered.begin(), ordered.end(), [](const ChromaFilament &a, const ChromaFilament &b) {
// Relative luminance of the linear colour: the perceptual order, so a saturated yellow
// counts as light and a saturated blue as dark, which a plain r+g+b sum gets wrong.
auto lum = [](const ColorRGB &c) {
return 0.2126f * srgb_to_linear(c.r()) + 0.7152f * srgb_to_linear(c.g()) + 0.0722f * srgb_to_linear(c.b());
};
return lum(a.color) > lum(b.color);
});
const int count = int(ordered.size());
int left = relief_layers;
for (int i = 0; i < count; ++i) {
// Spread the remainder over the first bands rather than piling it onto the last one.
const int share = left / (count - i) + ((left % (count - i)) > 0 ? 1 : 0);
ChromaBand band;
band.filament = ordered[size_t(i)];
band.layers = std::max(1, share);
left -= share;
stack.bands.push_back(band);
}
return stack;
}
} // namespace Slic3r
+286
View File
@@ -0,0 +1,286 @@
#ifndef slic3r_ChromaStack_hpp_
#define slic3r_ChromaStack_hpp_
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include <cereal/cereal.hpp>
#include <cereal/types/vector.hpp>
#include "Color.hpp"
namespace Slic3r {
// ChromaStack-style colour printing: a picture is reproduced on a single face by stacking thin layers
// of a few filaments, swapping filament at fixed layer boundaries, and varying the *height* of each
// pixel's column. Where the column is low, the layers above it are missing and the filament below
// shows; where it is high, the topmost filament covers everything beneath it. Because filament is
// translucent, a column that stops part-way through a run shows a blend of that run's filament and
// what is under it, which is what gives far more colours than there are filaments loaded.
//
// So there are only two things to compute, and they are both here:
// - the *palette*: the colour the stack shows at every possible column height (chroma_palette());
// - the *height map*: per pixel, the column height whose palette colour is closest to the picture
// (chroma_solve()).
//
// Everything else is machinery that already exists. The height map is handed to a texture
// displacement layer as its height texture, so the existing projection/preview/bake pipeline turns
// it into real geometry (see TextureDisplacement.hpp), and the filament swaps are handed to the
// object's height ranges (ModelObject::layer_config_ranges), which is the slicer's own way of
// saying "print this Z band with that filament".
// The depth of filament that blocks essentially all the light behind it, as a fraction of the
// filament's transmission distance (TD).
//
// TD's scale is empirical, not physical: its values run from about 1
// for an opaque filament to 10 and above for a nearly clear one, and the community's calibration
// prints (the "seashell" step test) read a TD off the step where the colour beneath stops showing.
// That step is about a tenth of the TD number, which is the only thing this constant encodes - a
// filament of TD 2 stops hiding what is under it at roughly 0.2 mm, i.e. after two or three layers,
// while one of TD 8 needs most of a millimetre. The model below turns that into a per-layer
// transmittance; see chroma_palette().
static constexpr float CHROMA_TD_OPAQUE_FRACTION = 0.1f;
// What a fresh stack gets above the base. Twenty-four layers is about two millimetres at a fine layer
// height: enough steps for a continuous tonal ramp, and still a print that finishes in an evening.
static constexpr int CHROMA_DEFAULT_RELIEF_LAYERS = 24;
// The most picture layers the solve can express. A column's height above the base is stored as one
// byte per pixel in the 8-bit grayscale height map the displacement reads, so beyond this two
// different heights would have to share a grey level and the relief's steps would stop landing on
// layer boundaries - which is the one thing the quantization exists to guarantee.
//
// It bounds the bands, not the stack: the base is whatever the model already is under the painted
// face, often hundreds of layers on a tall part, and nothing about it is quantized into the height
// map. As a limit on the picture it is not one in practice - 255 layers is 20 mm of relief at
// 0.08 mm, far past where a picture gains anything.
static constexpr int CHROMA_MAX_RELIEF_LAYERS = 255;
// The most filament changes one stack may hold. Every band is a swap the printer has to make, so this is
// a limit on patience rather than on the model; the palette itself is bounded by the layer count.
static constexpr size_t CHROMA_MAX_BANDS = 16;
// One filament as the colour model sees it: what colour it is, and how far light gets into it.
struct ChromaFilament
{
// 0-based index into the project's filaments, which is what a height range's `extruder` option
// (1-based) is written from. -1 for a filament that is not loaded, which the GUI refuses to
// solve with rather than silently printing in the wrong colour.
int filament = -1;
ColorRGB color;
// The filament preset's own name, so the panel can say which filament this is. Six similar greys
// are six identical swatches and six identical numbers otherwise. Stored rather than looked up on
// demand because a stack outlives the project's filament list - a preset can be swapped out from
// under a saved stack, and then the name is the only record of what it was solved against.
std::string name;
// Transmission distance, mm, as the filament preset reports it
// (`filament_transmission_distance`). Clamped to a sane range when the palette is built: zero
// would make a layer infinitely opaque and a negative value is meaningless.
float td_mm = 4.f;
template<class Archive> void serialize(Archive &ar)
{
float r = color.r(), g = color.g(), b = color.b();
ar(filament, r, g, b, td_mm, name);
color = ColorRGB(r, g, b);
}
};
// A run of consecutive layers printed in one filament. The stack is a list of these, bottom-up, so
// a band boundary is exactly where the print pauses and the filament is changed.
struct ChromaBand
{
ChromaFilament filament;
int layers = 1;
template<class Archive> void serialize(Archive &ar) { ar(filament, layers); }
};
// The whole recipe: which filaments, in which order, how thick a layer is, and how much of the
// bottom is solid backing rather than picture.
struct ChromaStack
{
std::vector<ChromaBand> bands; // bottom-up; bands.front() is the one the base is printed in
// The print's layer height, mm. Taken from the print profile rather than chosen here - a stack
// solved against 0.08 mm layers prints as the wrong picture at 0.2 mm, so the GUI re-solves when
// the profile's layer height changes.
float layer_height_mm = 0.08f;
// Layers of solid backing under the picture: everything the model already is below the painted
// face. Not part of any band - the bands are the picture - but printed in the bottom band's
// filament, since that is what is loaded when the print starts.
//
// Keeping it out of the bands is what lets it be what it really is: a measurement of the model,
// often a hundred layers or more, while a band is a handful. Folding the two together put a
// three-digit number on a slider meant for single digits and made "how thick is the backing" and
// "how many layers does the first filament get" the same control, which they are not.
//
// The colour model treats the base as opaque. That is what makes a column's colour depend only on
// the layers above it, and why the panel warns about a base thin enough to let the plate show
// through.
int base_layers = 4;
// Whether `base_layers` follows the model or was typed in. It follows by default, and it should:
// the painted face is wherever the geometry puts it, and a base that disagrees with it shifts
// every colour in the picture - the stack ends up anchored at the bottom of the part instead of
// under the face, so the picture's whole range is squeezed into the first few millimetres and
// everything above it clamps to the top band.
//
// It cannot be measured once and left: a layer is created, and its picture chosen, before there
// is any paint to measure, and the face moves again whenever the part is scaled or the layer
// height changes. So the panel re-measures it every frame while this is set.
bool base_auto = true;
// Spread the quantization error into neighbouring pixels (Floyd-Steinberg, serpentine) instead
// of rounding each pixel to its nearest palette colour on its own. A palette of twenty or thirty
// colours has to cover a photograph's whole range, so plain rounding leaves visible bands across
// a sky or a cheek; diffusing the error trades them for fine stipple, which at a tenth of a
// millimetre per pixel reads as the intermediate colour. Off for flat-coloured artwork, where
// stipple is the artifact rather than the cure.
bool dither = false;
// Total printed layers, base included: what the tallest column is.
int total_layers() const;
// Layers the picture is built from, i.e. the bands' own total above the base.
int relief_layers() const;
// The relief's height in mm, which is the `depth_mm` of the texture displacement layer the
// height map is handed to.
float depth_mm() const;
bool empty() const { return bands.empty(); }
template<class Archive> void serialize(Archive &ar) { ar(bands, layer_height_mm, base_layers, dither, base_auto); }
};
// The colour the stack shows at every column height: entry k is a column of exactly k printed
// layers, so the vector has total_layers() + 1 entries.
//
// The model is Beer-Lambert absorption, layer by layer. A layer of thickness h in a filament of
// transmission distance TD passes
//
// T = exp(-h / (CHROMA_TD_OPAQUE_FRACTION * TD / 3))
//
// of the light coming from under it and shows its own colour with the rest, so
//
// C_k = (1 - T_k) * c_k + T_k * C_(k-1)
//
// walking up from an opaque base, C_0 = c_1. The divisor is what makes "opaque at
// CHROMA_TD_OPAQUE_FRACTION * TD" true: three e-folding lengths pass 5% of the light, which is
// where the colour beneath stops being visible to the eye.
//
// Beer-Lambert is not just the physically honest choice here, it is the only one that stays
// consistent when applied per layer: transmittance multiplies, so a run of n layers of one filament
// passes exp(-n h / d) whether it is composited in one step or n, and the palette does not depend on
// how the stack happens to be cut into bands.
//
// Compositing is done on linear light and the entries are converted back to sRGB, because the
// quantity being attenuated is light, not an sRGB code.
std::vector<ColorRGB> chroma_palette(const ChromaStack &stack);
// What chroma_solve() produced.
struct ChromaSolve
{
int width = 0;
int height = 0;
// Per pixel, row-major and top-to-bottom like DecodedHeightTexture: the column's printed layer
// count, between base_layers and total_layers(). Sixteen bits because it counts from the build
// plate, and the base alone can be several hundred layers on a tall part.
std::vector<uint16_t> levels;
// The same thing as the 8-bit grayscale height map the displacement samples: 0 at base_layers,
// 255 at total_layers(). This is what gets encoded as a PNG into the layer's `image_data`, which
// is why the solve has to be the thing that owns the quantization - the relief's steps have to
// land on layer boundaries, and a grey level is the only channel the displacement reads.
std::vector<uint8_t> grey;
// Per pixel RGB: the palette colour the solve chose, i.e. what the print is predicted to look
// like. Shown in the panel next to the source picture, since the 3D view can only show the
// relief - the colours do not exist until the print swaps filament.
std::vector<uint8_t> preview;
// How far the prediction landed from the picture, as a mean and worst-case CIE76 dE over all
// pixels. The panel reports it because it is the only honest answer to "will this stack
// reproduce my image": under about 5 is a good match, over about 15 means the filaments cannot
// reach the picture's colours and no amount of layers will fix it.
double mean_delta_e = 0.;
double max_delta_e = 0.;
bool empty() const { return width <= 0 || height <= 0 || levels.empty(); }
};
// Solves `rgb` (width * height * 3 bytes, row-major, top-to-bottom) against the stack's palette.
// An empty stack, a zero-sized image or a stack with no relief layers gives an empty result.
ChromaSolve chroma_solve(const uint8_t *rgb, int width, int height, const ChromaStack &stack);
// How many layers of this filament it takes to hide what is under it, at this layer height. The
// single most useful number about a stack and the one nothing else makes visible: it is what decides
// whether a band is a gradient or a step.
//
// A filament hides what is behind it at about CHROMA_TD_OPAQUE_FRACTION of its TD (see
// chroma_palette()), so this is that depth in layers. Below two the band has no intermediate colours
// at all - the picture can only use the filament or not use it - which is what a coarse layer height
// does to an otherwise good stack: TD 4 at 0.2 mm covers in two layers, the same filament at 0.08 mm
// in five.
float chroma_layers_to_opaque(float td_mm, float layer_height_mm);
// How many of the picture's layers actually change the colour.
//
// A band eventually stops saying anything: once it hides what is under it, another layer of it shows
// the colour the one below already showed. Those layers still cost print time and still take up
// relief height, and nothing about the stack makes them visible - the band looks like ten layers of
// gradient and is, say, five of gradient and five of flat.
//
// Measured against the palette rather than against TD, because how long a band keeps paying depends
// on what is under it as much as on the filament: a light band over a dark one is finished in four or
// five layers, while a dark one over a light one goes on moving the colour perceptibly well past the
// depth at which it is nominally opaque.
int chroma_distinct_levels(const ChromaStack &stack);
// Shortens every band to its last layer that still changes the colour. Bands already shorter than
// that are left alone: they are still inside their fade, where every layer counts.
void chroma_trim_bands(ChromaStack &stack);
// The printed layer a point `z_mm` above the bottom of the object falls in: 1-based, clamped to the
// stack, 0 for a stack that prints nothing. Pair it with chroma_palette() to get the colour shown
// there.
//
// This, rather than the height map, is what decides the colour a surface shows. A filament swap
// happens at one print Z across the whole plate, so every surface crossing that height changes colour
// whatever the picture says - the terraces of the relief, the walls between them, and the sides of the
// model alike. Colouring a preview this way is therefore not an approximation of the print; it is what
// the print does, and it keeps working after the relief has been baked into the mesh and the height
// map is no longer attached to anything on screen.
int chroma_level_at(const ChromaStack &stack, double z_mm);
// The solve's height map as an 8-bit grayscale PNG, ready to be stored in a
// TextureDisplacementLayer's `image_data`.
std::vector<unsigned char> chroma_encode_height_png(const ChromaSolve &solve);
// One filament swap as an object height range: the Z band, in the object's own coordinates (0 at its
// bottom), that prints in `filament` (0-based).
struct ChromaRange
{
double z_min = 0.;
double z_max = 0.;
int filament = -1;
};
// The stack's bands as height ranges, in object coordinates, starting at `z_bottom` (which is 0 for
// a stack whose base starts at the object's first layer). Bands printed in the same filament as the
// band below are merged, so a stack that repeats a filament does not produce a pointless swap.
std::vector<ChromaRange> chroma_ranges(const ChromaStack &stack, double z_bottom = 0.);
// A starting point for a stack: the given filaments, lightest at the bottom, each with an equal
// share of `relief_layers` on top of `base_layers`.
//
// Light-to-dark is the convention filament-painting guides teach, and it is not arbitrary: a dark
// filament over a light one reads as a midtone at one or two layers and as the dark colour at four,
// so putting the dark filaments last is what buys the long tonal ramp. The reverse order gives a few
// washed-out steps and then nothing.
ChromaStack chroma_default_stack(const std::vector<ChromaFilament> &filaments, float layer_height_mm,
int base_layers, int relief_layers);
} // namespace Slic3r
#endif // slic3r_ChromaStack_hpp_
+25 -272
View File
@@ -7,13 +7,9 @@
#include <cmath>
#include <limits>
#include <numeric>
#include <optional>
#include <unordered_map>
#include <tbb/parallel_for.h>
#include "ClipperUtils.hpp"
#include "AABBTreeIndirect.hpp"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
#include "Point.hpp"
@@ -280,7 +276,6 @@ Polylines c2_to_polylines(const C2::Paths64 &paths)
return out;
}
// Only sizes the reserve of c2_to_expolygons(): for a tree Clipper2 misnested (see below) the count is approximate.
size_t c2_count_expolygons(const C2::PolyPath64 &outer)
{
size_t cnt = 1;
@@ -290,90 +285,17 @@ size_t c2_count_expolygons(const C2::PolyPath64 &outer)
return cnt;
}
// Clipper2 sometimes puts a path at the wrong level of its PolyTree: a hole beside the outer path around it instead of
// under it, or an island at the level of holes. Taken as they are, a misplaced hole becomes a clockwise contour and its
// outer an expolygon that covers it; whole-layer booleans cancel the two, but anything handling expolygons one at a time
// sees the hole filled. So the orientation of a path decides its role: a clockwise path where a contour belongs is
// collected in stray_holes, for c2_attach_stray_holes() to give to the contour around it.
void c2_append_expolygons(const C2::PolyPath64 &outer, ExPolygons &out, std::vector<const C2::Path64*> &stray_holes)
void c2_append_expolygons(const C2::PolyPath64 &outer, ExPolygons &out)
{
if (C2::Area(outer.Polygon()) < 0.) {
stray_holes.emplace_back(&outer.Polygon());
for (const auto &island : outer)
c2_append_expolygons(*island, out, stray_holes);
return;
}
// Indexed, as the recursion below may reallocate out.
const size_t idx = out.size();
out.emplace_back().contour.points = c2_to_points(outer.Polygon());
out[idx].holes.reserve(outer.Count());
for (const auto &child : outer)
if (C2::Area(child->Polygon()) > 0.)
c2_append_expolygons(*child, out, stray_holes);
else {
out[idx].holes.emplace_back().points = c2_to_points(child->Polygon());
// Islands inside the hole.
for (const auto &island : *child)
c2_append_expolygons(*island, out, stray_holes);
}
}
// Whether outer encloses inner, two paths of one Clipper2 result. Such paths don't cross, they only touch, and rounding
// both to the grid leaves a touching vertex up to about 1.5 units across the other path. So a vertex of inner that close
// to outer doesn't count, the first vertex clear of it decides, and a path touching at every vertex goes by a point
// strictly inside it: either lies on the same side of outer as the whole path. A sliver too thin to hold such a point is
// taken as not enclosed. A hole closely hugging an island inside it would pass, so callers skip any outer no larger than
// inner: a path inside another is always the smaller.
bool path_encloses(const Polygon &outer, const Polygon &inner)
{
// The rounding of the two paths, and of the projection Polygon::on_boundary() measures to.
static constexpr double touching = 3.;
for (const Point &pt : inner.points)
if (! outer.on_boundary(pt, touching))
return contains(outer, pt, true);
const std::optional<Point> pt = ClipperUtils::interior_point(inner);
return pt && contains(outer, *pt, true);
}
// Gives each stray hole to the innermost of the contours out[first..] around it. Contours of a Clipper2 result don't
// cross, so those around a hole are nested and the innermost one is the smallest: candidates are tried smallest first,
// from the first one larger than the hole.
void c2_attach_stray_holes(ExPolygons &out, size_t first, const std::vector<const C2::Path64*> &stray_holes)
{
if (stray_holes.empty())
return;
using Tree = AABBTreeIndirect::Tree<2, coord_t>;
std::vector<AABBTreeIndirect::BoundingBoxWrapper> bboxes;
bboxes.reserve(out.size() - first);
for (size_t i = first; i < out.size(); ++ i)
bboxes.emplace_back(i, get_extents(out[i].contour));
Tree tree;
tree.build_modify_input(bboxes);
// Contour areas, computed when a contour first becomes a candidate.
std::vector<double> areas(out.size(), 0.);
std::vector<size_t> candidates;
for (const C2::Path64 *path : stray_holes) {
Polygon hole;
hole.points = c2_to_points(*path);
const double hole_area = - C2::Area(*path);
const BoundingBox bbox = get_extents(hole);
const Tree::BoundingBox hole_bbox(bbox.min, bbox.max);
candidates.clear();
AABBTreeIndirect::traverse(tree,
[&hole_bbox](const Tree::Node &node) { return node.bbox.contains(hole_bbox); },
[&candidates](const Tree::Node &node) { candidates.emplace_back(node.idx); return true; });
for (size_t idx : candidates)
if (areas[idx] == 0.)
areas[idx] = out[idx].contour.area();
std::sort(candidates.begin(), candidates.end(), [&areas](size_t a, size_t b) { return areas[a] < areas[b]; });
auto larger = std::upper_bound(candidates.begin(), candidates.end(), hole_area, [&areas](double area, size_t idx) { return area < areas[idx]; });
if (auto it = std::find_if(larger, candidates.end(), [&out, &hole](size_t idx) { return path_encloses(out[idx].contour, hole); });
it == candidates.end())
// Nothing around it: keep the path as Clipper2 returned it.
out.emplace_back().contour = std::move(hole);
else
out[*it].holes.emplace_back(std::move(hole));
}
ExPolygon &expoly = out.emplace_back();
expoly.contour.points = c2_to_points(outer.Polygon());
expoly.holes.reserve(outer.Count());
for (const auto &hole : outer)
expoly.holes.emplace_back().points = c2_to_points(hole->Polygon());
// Islands inside the holes.
for (const auto &hole : outer)
for (const auto &island : *hole)
c2_append_expolygons(*island, out);
}
ExPolygons c2_to_expolygons(const C2::PolyTree64 &tree)
@@ -383,10 +305,8 @@ ExPolygons c2_to_expolygons(const C2::PolyTree64 &tree)
cnt += c2_count_expolygons(*outer);
ExPolygons out;
out.reserve(cnt);
std::vector<const C2::Path64*> stray_holes;
for (const auto &outer : tree)
c2_append_expolygons(*outer, out, stray_holes);
c2_attach_stray_holes(out, 0, stray_holes);
c2_append_expolygons(*outer, out);
return out;
}
@@ -880,114 +800,6 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r
{ return _clipper_ex(ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); }
Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset)
{ return _clipper_ex(ctIntersection, ClipperUtils::SurfacesPtrProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); }
namespace ClipperUtils {
std::vector<ExPolygonsTile> tile_expolygons(const ExPolygons &expolygons, size_t per_tile)
{
BoundingBox extent;
std::vector<BoundingBox> bboxes;
bboxes.reserve(expolygons.size());
for (const ExPolygon &expoly : expolygons) {
bboxes.emplace_back(get_extents(expoly));
extent.merge(bboxes.back());
}
if (! extent.defined)
return {};
const int tiles = std::clamp(int(std::sqrt(double(expolygons.size()) / double(std::max<size_t>(per_tile, 1)))), 1, 32);
const Point size = extent.size();
const coord_t tile_w = std::max<coord_t>(1, size.x() / tiles + 1), tile_h = std::max<coord_t>(1, size.y() / tiles + 1);
std::vector<ExPolygonsTile> out(size_t(tiles * tiles));
for (size_t i = 0; i < expolygons.size(); ++ i) {
const Point c = bboxes[i].center();
ExPolygonsTile &tile = out[size_t(std::clamp(int((c.y() - extent.min.y()) / tile_h), 0, tiles - 1) * tiles +
std::clamp(int((c.x() - extent.min.x()) / tile_w), 0, tiles - 1))];
tile.members.emplace_back(i);
tile.bbox.merge(bboxes[i]);
}
out.erase(std::remove_if(out.begin(), out.end(), [](const ExPolygonsTile &tile) { return tile.members.empty(); }), out.end());
return out;
}
// Between two consecutive heights of its vertices the polygon is a set of trapezoids, and the line through the middle
// of that band meets no vertex and no horizontal edge: its crossings with the edges alternate into and out of the
// polygon, and there each trapezoid is at least half as wide as at its widest. So the bands are tried widest first,
// until the middle of the widest span on that line rounds to a point strictly inside. A band under two units has no
// grid line inside it.
std::optional<Point> interior_point(const Polygon &polygon)
{
std::vector<coord_t> heights;
heights.reserve(polygon.size());
for (const Point &pt : polygon.points)
heights.emplace_back(pt.y());
std::sort(heights.begin(), heights.end());
heights.erase(std::unique(heights.begin(), heights.end()), heights.end());
std::vector<std::pair<coord_t, coord_t>> bands;
for (size_t i = 1; i < heights.size(); ++ i)
if (heights[i] - heights[i - 1] >= 2)
bands.emplace_back(heights[i - 1], heights[i]);
std::sort(bands.begin(), bands.end(), [](const auto &a, const auto &b) { return a.second - a.first > b.second - b.first; });
std::vector<double> crossings;
for (const auto &[low, high] : bands) {
const coord_t y = low + (high - low) / 2;
crossings.clear();
for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i ++) {
const Point &a = polygon.points[j];
const Point &b = polygon.points[i];
if ((a.y() > y) != (b.y() > y))
crossings.emplace_back(double(a.x()) + double(y - a.y()) * double(b.x() - a.x()) / double(b.y() - a.y()));
}
std::sort(crossings.begin(), crossings.end());
// Inside from the first crossing to the second, from the third to the fourth...
double left = 0., right = 0.;
for (size_t i = 1; i < crossings.size(); i += 2)
if (crossings[i] - crossings[i - 1] > right - left) {
left = crossings[i - 1];
right = crossings[i];
}
if (const Point pt(coord_t(std::round(0.5 * (left + right))), y); right > left && contains(polygon, pt, false))
return pt;
}
return std::nullopt;
}
}
static Slic3r::ExPolygons clipper_ex_by_piece(ClipType clipType, const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
{
// A few dozen subject ExPolygons to a tile, each tile one Clipper call with the clip cut to the tile's box.
const std::vector<ClipperUtils::ExPolygonsTile> tiles = ClipperUtils::tile_expolygons(subject, 32);
// One tile is the plain call: cutting the clip would only cost time.
if (tiles.size() <= 1)
return _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset);
std::vector<BoundingBox> clip_bboxes;
clip_bboxes.reserve(clip.size());
for (const Polygon &polygon : clip)
clip_bboxes.emplace_back(get_extents(polygon));
std::vector<Slic3r::ExPolygons> out_tiles(tiles.size());
tbb::parallel_for(size_t(0), tiles.size(), [&](size_t tile_idx) {
const ClipperUtils::ExPolygonsTile &tile = tiles[tile_idx];
Slic3r::ExPolygons local_subject;
local_subject.reserve(tile.members.size());
for (size_t i : tile.members)
local_subject.emplace_back(subject[i]);
// Grown so that the cut edges of the clip stay clear of the subject, also after the safety offset.
const BoundingBox bbox = tile.bbox.inflated(SCALED_EPSILON);
Polygons local_clip;
for (size_t i = 0; i < clip.size(); ++i)
if (clip_bboxes[i].overlap(bbox))
if (Polygon clipped = ClipperUtils::clip_clipper_polygon_with_subject_bbox(clip[i], bbox); ! clipped.empty())
local_clip.emplace_back(std::move(clipped));
out_tiles[tile_idx] = _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(local_subject), ClipperUtils::PolygonsProvider(local_clip), do_safety_offset);
});
Slic3r::ExPolygons out;
for (Slic3r::ExPolygons &out_tile : out_tiles)
append(out, std::move(out_tile));
return out;
}
Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
{ return clipper_ex_by_piece(ctDifference, subject, clip, do_safety_offset); }
Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
{ return clipper_ex_by_piece(ctIntersection, subject, clip, do_safety_offset); }
// May be used to "heal" unusual models (3DLabPrints etc.) by providing fill_type (pftEvenOdd, pftNonZero, pftPositive, pftNegative).
Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, PolyFillType fill_type)
{ return _clipper_ex(ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No, fill_type); }
@@ -1195,75 +1007,24 @@ Lines _clipper_ln(ClipType clipType, const Lines &subject, const Polygons &clip)
return retval;
}
// Chains the loops of one level of the tree, each followed by the loops under it, depth first. Clipper2 may put a loop
// beside the loop around it instead of under it (see c2_append_expolygons()), out of the nesting order the callers rely
// on: the spiral of FillSpiralInset continues only into a loop inside the last one. So a loop whose orientation doesn't
// fit its level (CCW under the top or a CW loop, CW under a CCW loop) goes under the smallest loop of the level around
// it, the one Clipper2 failed to put it under, and stays where it is if none is.
static void traverse_pt_outside_in(const std::vector<const C2::PolyPath64*> &nodes, bool holes, Polygons *retval)
static void traverse_pt_outside_in(const C2::PolyPath64 &parent, Polygons *retval)
{
struct Loop
{
const C2::PolyPath64 *node;
double area;
// The loops to traverse under this one: its children, then the misplaced loops it takes in.
std::vector<const C2::PolyPath64*> next;
bool moved = false;
};
std::vector<Loop> loops;
std::vector<size_t> fitting, misfits;
loops.reserve(nodes.size());
for (const C2::PolyPath64 *node : nodes) {
Loop &loop = loops.emplace_back(Loop{ node, C2::Area(node->Polygon()), {} });
loop.next.reserve(node->Count());
for (const auto &child : *node)
loop.next.emplace_back(child.get());
((loop.area < 0.) == holes ? fitting : misfits).emplace_back(loops.size() - 1);
}
if (! misfits.empty()) {
// Candidates are tried smallest first, from the first one larger than the misplaced loop (see path_encloses()).
std::sort(fitting.begin(), fitting.end(), [&loops](size_t a, size_t b) { return std::abs(loops[a].area) < std::abs(loops[b].area); });
std::vector<C2::Rect64> bounds(loops.size());
for (size_t i = 0; i < loops.size(); ++ i)
bounds[i] = C2::GetBounds(loops[i].node->Polygon());
for (size_t misfit : misfits) {
Loop &loop = loops[misfit];
const C2::Rect64 &box = bounds[misfit];
Polygon inner;
inner.points = c2_to_points(loop.node->Polygon());
for (size_t idx : fitting) {
// The slack of a vertex rounded across the candidate, as in c2_attach_stray_holes().
const C2::Rect64 &around = bounds[idx];
if (std::abs(loops[idx].area) <= std::abs(loop.area) || around.left - SCALED_EPSILON > box.left ||
around.right + SCALED_EPSILON < box.right || around.top - SCALED_EPSILON > box.top || around.bottom + SCALED_EPSILON < box.bottom)
continue;
Polygon outer;
outer.points = c2_to_points(loops[idx].node->Polygon());
if (path_encloses(outer, inner)) {
loops[idx].next.emplace_back(loop.node);
loop.moved = true;
break;
}
}
}
}
// collect ordering points
Points ordering_points;
ordering_points.reserve(parent.Count());
for (const auto &node : parent)
ordering_points.emplace_back(node->Polygon().front().x, node->Polygon().front().y);
// Chain the loops staying on this level, push results recursively.
std::vector<size_t> staying;
Points ordering_points;
for (size_t i = 0; i < loops.size(); ++ i)
if (! loops[i].moved) {
staying.emplace_back(i);
ordering_points.emplace_back(loops[i].node->Polygon().front().x, loops[i].node->Polygon().front().y);
}
// Perform the ordering, push results recursively.
//FIXME pass the last point to chain_points?
for (size_t idx : chain_points(ordering_points)) {
const Loop &loop = loops[staying[idx]];
retval->emplace_back().points = c2_to_points(loop.node->Polygon());
if (loop.area < 0.)
const C2::PolyPath64 &node = *parent.Child(idx);
retval->emplace_back().points = c2_to_points(node.Polygon());
if (node.IsHole())
// Orient a hole, which is clockwise oriented, to CCW.
retval->back().reverse();
traverse_pt_outside_in(loop.next, loop.area > 0., retval);
// traverse the next depth
traverse_pt_outside_in(node, retval);
}
}
@@ -1271,12 +1032,8 @@ Polygons union_pt_chained_outside_in(const Polygons &subject)
{
C2::PolyTree64 tree;
c2_clip(C2::ClipType::Union, to_paths64(ClipperUtils::PolygonsProvider(subject)), {}, C2::FillRule::EvenOdd, tree);
std::vector<const C2::PolyPath64*> top_level;
top_level.reserve(tree.Count());
for (const auto &node : tree)
top_level.emplace_back(node.get());
Polygons retval;
traverse_pt_outside_in(top_level, false, &retval);
traverse_pt_outside_in(tree, &retval);
return retval;
}
@@ -1313,8 +1070,6 @@ ExPolygons top_level_expolygons(const ExPolygons &expolygons, ExPolygons *nested
c2_clip(C2::ClipType::Union, to_paths64(ClipperUtils::ExPolygonsProvider(expolygons)), {}, C2::FillRule::EvenOdd, tree);
ExPolygons out;
out.reserve(tree.Count());
const size_t first_nested = nested ? nested->size() : 0;
std::vector<const C2::Path64*> stray_holes;
for (const auto &outer : tree) {
ExPolygon &expoly = out.emplace_back();
expoly.contour.points = c2_to_points(outer->Polygon());
@@ -1322,11 +1077,9 @@ ExPolygons top_level_expolygons(const ExPolygons &expolygons, ExPolygons *nested
expoly.holes.emplace_back().points = c2_to_points(hole->Polygon());
if (nested)
for (const auto &island : *hole)
c2_append_expolygons(*island, *nested, stray_holes);
c2_append_expolygons(*island, *nested);
}
}
if (nested)
c2_attach_stray_holes(*nested, first_nested, stray_holes);
return out;
}
-20
View File
@@ -5,14 +5,12 @@
#include "Polyline.hpp"
#include "Line.hpp"
#include "libslic3r.h"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
#include "Polygon.hpp"
#include "Surface.hpp"
#include <cstddef>
#include <iterator>
#include <cassert>
#include <optional>
#include <vector>
namespace Slic3r {
@@ -334,19 +332,6 @@ namespace ClipperUtils {
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygon &src, const BoundingBox &bbox, const bool get_entire_polygons = false);
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygons &src, const BoundingBox &bbox, const bool get_entire_polygons = false);
// Splits ExPolygons into tiles by the centres of their boxes, about `per_tile` of them to a tile, to run Clipper on a
// layer of many pieces tile by tile. Returns the non-empty tiles, each with the indices of its ExPolygons and their box.
struct ExPolygonsTile
{
BoundingBox bbox;
std::vector<size_t> members;
};
[[nodiscard]] std::vector<ExPolygonsTile> tile_expolygons(const ExPolygons &expolygons, size_t per_tile);
// A grid point strictly inside the polygon, or none if the polygon is at most a few units thick everywhere.
// Decides on which side of another path a path lies when every one of its vertices touches that path.
[[nodiscard]] std::optional<Point> interior_point(const Polygon &polygon);
}
// offset Polygons
@@ -542,11 +527,6 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r
Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::Surfaces &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
// diff_ex() / intersection_ex() of the subject split into tiles, each against only the part of the clip near it, the tiles in
// parallel. The same area as the operation on the whole subject when its ExPolygons do not overlap, and much faster for a
// subject of thousands of pieces spread over a layer: Clipper slows down with the number of edges crossing a scan line.
Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::Polygon &clip);
Slic3r::Polylines intersection_pl(const Slic3r::Polyline &subject, const Slic3r::ExPolygon &clip);
Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygon &clip);
+1 -1
View File
@@ -1432,7 +1432,7 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
}
if (!header_found) {
// TRN %1% is the application name.
std::string error_message = format(_u8L("Not a 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());
}
+16 -41
View File
@@ -377,8 +377,7 @@ public:
virtual void set(const ConfigOption* rhs, size_t start, size_t len) = 0;
virtual void set_with_restore(const ConfigOptionVectorBase* rhs, std::vector<int>& restore_index, int stride) = 0;
virtual void set_with_restore_2(const ConfigOptionVectorBase* rhs, std::vector<int>& restore_index, int start, int len, bool skip_error = false) = 0;
// keep_nil: copy a nil column from rhs as well, for options whose nil means "disabled" rather than "inherit".
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride, bool keep_nil = false) = 0;
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride) = 0;
virtual void set_to_index(const ConfigOptionVectorBase* rhs, std::vector<int>& dest_index, int stride) = 0;
virtual void set_with_nil(const ConfigOptionVectorBase* rhs, const ConfigOptionVectorBase* inherits, int stride) = 0;
// Resize the vector of values, copy the newly added values from opt_default if provided.
@@ -581,7 +580,7 @@ public:
//set a item related with extruder variants when loading user config, only set the different value of some extruder
//rhs: item from user config
//diff_index: which index in this vector need to be set
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride, bool keep_nil) override
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride) override
{
if (rhs->type() == this->type()) {
// Assign the first value of the rhs vector.
@@ -593,7 +592,7 @@ public:
for (size_t i = 0; i < diff_index.size(); i++) {
if (diff_index[i] != -1) {
for (size_t j = 0; j < stride; j++) {
if (keep_nil || !other->is_nil(diff_index[i] * stride))
if (!other->is_nil(diff_index[i] * stride))
this->values[i * stride + j] = other->values[diff_index[i] * stride + j];
}
}
@@ -842,12 +841,8 @@ public:
{
UNUSED(append);
std::istringstream iss(str);
double value;
iss >> value;
if (iss.fail())
return false;
this->value = value;
return true;
iss >> this->value;
return !iss.fail();
}
bool operator==(const ConfigOption &rhs) const override
@@ -1038,12 +1033,8 @@ public:
{
UNUSED(append);
std::istringstream iss(str);
int value;
iss >> value;
if (iss.fail())
return false;
this->value = value;
return true;
iss >> this->value;
return !iss.fail();
}
ConfigOptionInt& operator=(const ConfigOption *opt)
@@ -1261,12 +1252,8 @@ public:
UNUSED(append);
// don't try to parse the trailing % since it's optional
std::istringstream iss(str);
double value;
iss >> value;
if (iss.fail())
return false;
this->value = value;
return true;
iss >> this->value;
return !iss.fail();
}
private:
@@ -1378,14 +1365,10 @@ public:
bool deserialize(const std::string &str, bool append = false) override
{
UNUSED(append);
std::istringstream iss(str);
double value;
iss >> value;
if (iss.fail())
return false;
this->value = value;
this->percent = str.find_first_of("%") != std::string::npos;
return true;
std::istringstream iss(str);
iss >> this->value;
return !iss.fail();
}
private:
@@ -1555,12 +1538,8 @@ public:
{
UNUSED(append);
char dummy;
Vec2d value;
if (sscanf(str.data(), " %lf , %lf %c", &value(0), &value(1), &dummy) != 2 &&
sscanf(str.data(), " %lf x %lf %c", &value(0), &value(1), &dummy) != 2)
return false;
this->value = value;
return true;
return sscanf(str.data(), " %lf , %lf %c", &this->value(0), &this->value(1), &dummy) == 2 ||
sscanf(str.data(), " %lf x %lf %c", &this->value(0), &this->value(1), &dummy) == 2;
}
private:
@@ -1679,12 +1658,8 @@ public:
{
UNUSED(append);
char dummy;
Vec3d value;
if (sscanf(str.data(), " %lf , %lf , %lf %c", &value(0), &value(1), &value(2), &dummy) != 3 &&
sscanf(str.data(), " %lf x %lf x %lf %c", &value(0), &value(1), &value(2), &dummy) != 3)
return false;
this->value = value;
return true;
return sscanf(str.data(), " %lf , %lf , %lf %c", &this->value(0), &this->value(1), &this->value(2), &dummy) == 3 ||
sscanf(str.data(), " %lf x %lf x %lf %c", &this->value(0), &this->value(1), &this->value(2), &dummy) == 3;
}
private:
+1 -1
View File
@@ -616,7 +616,7 @@ struct PropagateDanielssonSingleStep {
if ((signs[addr] & 2) == 0) {
float *v = &L[addr << 1];
float l = v[0] * v[0] + v[1] * v[1];
float *v2s = v + 2 * addr_delta;
float *v2s = v + (addr_delta << 1);
float v2[2] = {
v2s[0] + INCX * resolution,
v2s[1] + INCY * resolution
+35 -54
View File
@@ -22,8 +22,6 @@
#include "../PrintConfig.hpp"
#include "../Surface.hpp"
#include <tbb/parallel_for.h>
#include "AABBTreeLines.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/ExPolygon.hpp"
@@ -314,9 +312,9 @@ struct SurfaceFillParams
bool gyroid_optimized = false;
// For TPMS: grade the density from the surface to the interior of the object.
AdaptiveInfillMode adaptive_infill_mode = AdaptiveInfillMode::Disabled;
float adaptive_infill_interior_density = 0.f;
AdaptiveInfillGradient adaptive_infill_gradient = AdaptiveInfillGradient::Linear;
TpmsAdaptiveMode tpms_adaptive = TpmsAdaptiveMode::Disabled;
float tpms_interior_density = 0.f;
TpmsAdaptiveGradient tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear;
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
@@ -360,9 +358,9 @@ struct SurfaceFillParams
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
RETURN_COMPARE_NON_EQUAL(adaptive_infill_mode);
RETURN_COMPARE_NON_EQUAL(adaptive_infill_interior_density);
RETURN_COMPARE_NON_EQUAL(adaptive_infill_gradient);
RETURN_COMPARE_NON_EQUAL(tpms_adaptive);
RETURN_COMPARE_NON_EQUAL(tpms_interior_density);
RETURN_COMPARE_NON_EQUAL(tpms_adaptive_gradient);
RETURN_COMPARE_NON_EQUAL(smooth_factor);
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
RETURN_COMPARE_NON_EQUAL(separated_infills);
@@ -396,9 +394,9 @@ struct SurfaceFillParams
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
this->separated_infills == rhs.separated_infills &&
this->gyroid_optimized == rhs.gyroid_optimized &&
this->adaptive_infill_mode == rhs.adaptive_infill_mode &&
this->adaptive_infill_interior_density == rhs.adaptive_infill_interior_density &&
this->adaptive_infill_gradient == rhs.adaptive_infill_gradient &&
this->tpms_adaptive == rhs.tpms_adaptive &&
this->tpms_interior_density == rhs.tpms_interior_density &&
this->tpms_adaptive_gradient == rhs.tpms_adaptive_gradient &&
this->smooth_factor == rhs.smooth_factor &&
this->fill_order == rhs.fill_order;
}
@@ -677,28 +675,24 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
if (!line_based_pattern) {
const coord_t scaled_spacing = scaled<coord_t>(fill.params.spacing);
// Each expolygon is split on its own, so they run in parallel and are collected in their original order.
std::vector<std::pair<ExPolygons, ExPolygons>> split_parts(fill.expolygons.size()); // normal, narrow
tbb::parallel_for(size_t(0), fill.expolygons.size(), [&](size_t idx) {
const ExPolygon &expolygon = fill.expolygons[idx];
for (const ExPolygon &expolygon : fill.expolygons) {
Polygons filled_area = to_polygons(expolygon);
// "Core" area: open (erode+dilate) to drop thin features, then clamp back to the original polygon.
Polygons inner_area = intersection(filled_area, opening(filled_area, scaled_spacing, scaled_spacing));
if (inner_area.empty()) {
split_parts[idx].second.emplace_back(expolygon);
return;
narrow_infill.emplace_back(expolygon);
continue;
}
ExPolygons inner_ex = union_ex(inner_area);
ExPolygons expolys{expolygon};
split_parts[idx].second = diff_ex(expolys, inner_ex); // narrow infill area
split_parts[idx].first = intersection_ex(expolys, inner_ex); // normal infill area
});
for (auto &[normal_ex, narrow_ex] : split_parts) {
append(normal_infill, std::move(normal_ex));
append(narrow_infill, std::move(narrow_ex));
ExPolygons narrow_ex = diff_ex(expolys, inner_ex);
ExPolygons normal_ex = intersection_ex(expolys, inner_ex);
append(normal_infill, normal_ex); // normal infill area
append(narrow_infill, narrow_ex); // narrow infill area
}
return;
@@ -720,10 +714,7 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
}
const double aligning_angle = -base_angle + PI;
// Each expolygon is reconstructed on its own, so they run in parallel and are collected in their original order.
std::vector<Polygons> split_reconstructed(fill.expolygons.size());
tbb::parallel_for(size_t(0), fill.expolygons.size(), [&](size_t expolygon_idx) {
const ExPolygon &expolygon = fill.expolygons[expolygon_idx];
for (const ExPolygon &expolygon : fill.expolygons) {
Polygons filled_area = to_polygons(expolygon);
polygons_rotate(filled_area, aligning_angle);
BoundingBox bb = get_extents(filled_area);
@@ -854,10 +845,8 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
}
}
split_reconstructed[expolygon_idx] = std::move(reconstructed_area);
});
for (Polygons &reconstructed_area : split_reconstructed)
polygons_append(normal_fill_areas, std::move(reconstructed_area));
polygons_append(normal_fill_areas, reconstructed_area);
}
polygons_rotate(normal_fill_areas, -aligning_angle);
@@ -1021,17 +1010,17 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface;
// Orca: only the TPMS sparse infill is graded; reset otherwise, as params is reused.
params.adaptive_infill_mode = is_adaptive_infill_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
region_config.adaptive_infill_mode.value : AdaptiveInfillMode::Disabled;
const bool adaptive_infill_mode = params.adaptive_infill_mode != AdaptiveInfillMode::Disabled;
params.adaptive_infill_interior_density = adaptive_infill_mode ? std::max(1.f, float(region_config.adaptive_infill_interior_density)) : 0.f;
params.adaptive_infill_gradient = adaptive_infill_mode ? region_config.adaptive_infill_gradient.value : AdaptiveInfillGradient::Linear;
params.tpms_adaptive = is_tpms_adaptive_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
region_config.tpms_adaptive.value : TpmsAdaptiveMode::Disabled;
const bool tpms_adaptive = params.tpms_adaptive != TpmsAdaptiveMode::Disabled;
params.tpms_interior_density = tpms_adaptive ? std::max(1.f, float(region_config.tpms_interior_density)) : 0.f;
params.tpms_adaptive_gradient = tpms_adaptive ? region_config.tpms_adaptive_gradient.value : TpmsAdaptiveGradient::Linear;
// Pass through gyroid_optimized only when the effective pattern is Gyroid,
// so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag
// (which would unnecessarily split fill batching). Adaptive density replaces it.
// Stored on SurfaceFillParams; copied to FillParams during conversion.
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !adaptive_infill_mode;
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !tpms_adaptive;
if (params.extrusion_role == erInternalInfill) {
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
@@ -1384,7 +1373,7 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
if (surface_fill.params.pattern == ipConcentricInternal) {
@@ -1434,9 +1423,9 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
params.smooth_factor = surface_fill.params.smooth_factor;
// BBS
@@ -1484,15 +1473,7 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox);
f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr;
// Only the part of the layer-wide no-overlap area under this expolygon matters, so clip it to the
// expolygon's box first (padded past the safety offset, which grows the clip side). The result is
// identical; the cost is not: a layer split into many small fills, e.g. by colour painting,
// otherwise intersects every one of them with the whole layer.
BoundingBox no_overlap_bbox = get_extents(expoly);
no_overlap_bbox.offset(SCALED_EPSILON);
f->no_overlap_expolygons = intersection_ex(
ClipperUtils::clip_clipper_polygons_with_subject_bbox(surface_fill.no_overlap_expolygons, no_overlap_bbox),
ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
if (params.symmetric_infill_y_axis) {
params.symmetric_y_axis = f->extended_object_bounding_box().center().x();
expoly.symmetric_y(params.symmetric_y_axis);
@@ -1617,7 +1598,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
@@ -1657,9 +1638,9 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.multiline = surface_fill.params.multiline;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
params.smooth_factor = surface_fill.params.smooth_factor;
// Orca: Match make_fills() when choosing the origin of plane-path patterns.
// Without the sparse extrusion role, the filler uses each surface's bounds
+14 -257
View File
@@ -11,10 +11,6 @@
#include <string>
#include <vector>
#include <utility>
#include <unordered_map>
#include <unordered_set>
#include <tuple>
#include "../AABBTreeLines.hpp"
#include "../ClipperUtils.hpp"
#include "../EdgeGrid.hpp"
#include "../Geometry.hpp"
@@ -210,9 +206,6 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para
thick_polylines.clear();
}
else {
// Orca: sparse infill lines whose ends are next to each other print as one path, without a travel between them.
if (params.extrusion_role == erInternalInfill && ! eec->no_sort)
join_close_ends(polylines, scale_(2. * this->spacing), params.can_reverse);
extrusion_entities_append_paths(
eec->entities, std::move(polylines),
params.extrusion_role,
@@ -1651,8 +1644,6 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
};
const double line_half_width = 0.5 * scale_(spacing);
// Two ends whose anchors would meet are joined instead, with about as much wall.
const double join_length = anchor_length > SCALED_EPSILON ? std::max(anchor_length_max, 2. * (anchor_length + line_half_width)) : anchor_length_max;
#if 0
// Connection from end of one infill line to the start of another infill line.
@@ -1729,10 +1720,6 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
}
#endif
// Line ends not yet joined to another line along the contour.
std::vector<char> open_end(graph.map_infill_end_point_to_boundary.size(), 1);
auto end_index = [&graph](const ContourIntersectionPoint *cp) { return size_t(cp - graph.map_infill_end_point_to_boundary.data()); };
struct Arc {
ContourIntersectionPoint *intersection;
double arc_length;
@@ -1744,81 +1731,6 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (cp.contour_idx != boundary_idx_unconnected && cp.next_on_contour != &cp && cp.could_connect_next())
arches.push_back({ &cp, path_length_along_contour_ccw(&cp, cp.next_on_contour, graph.boundary_params[cp.contour_idx].back()) });
std::sort(arches.begin(), arches.end(), [](const auto& l, const auto& r) { return l.arc_length < r.arc_length; });
// Lines the arcs would join in this order.
auto count_joins = [&](const std::vector<Arc> &order) {
std::vector<char> consumed(graph.map_infill_end_point_to_boundary.size());
for (size_t i = 0; i < consumed.size(); ++ i)
consumed[i] = graph.map_infill_end_point_to_boundary[i].consumed;
std::vector<size_t> parent(infill_ordered.size());
std::iota(parent.begin(), parent.end(), 0);
auto find = [&parent](size_t i) {
while (parent[i] != i)
i = parent[i] = parent[parent[i]];
return i;
};
size_t joins = 0;
for (const Arc &arc : order) {
const size_t end1 = end_index(arc.intersection), end2 = end_index(arc.intersection->next_on_contour);
if (consumed[end1] || consumed[end2] || (params.multiline > 1 && arc.arc_length < scale_(spacing) * params.multiline))
continue;
const size_t polyline1 = find(end1 / 2), polyline2 = find(end2 / 2);
if (polyline1 == polyline2)
continue;
if (arc.arc_length < join_length) {
parent[polyline2] = polyline1;
++ joins;
} else if (anchor_length <= SCALED_EPSILON)
continue;
consumed[end1] = consumed[end2] = 1;
}
// The last pass joins the open ends next to each other, also those of a multiline group.
for (size_t end = 0; end < consumed.size(); ++ end) {
const ContourIntersectionPoint &cp = graph.map_infill_end_point_to_boundary[end];
if (consumed[end] || cp.contour_idx == boundary_idx_unconnected)
continue;
const double length = graph.boundary_params[cp.contour_idx].back();
auto open = [&](const ContourIntersectionPoint *other, bool trimmed) {
return other != &cp && ! consumed[end_index(other)] && ! trimmed;
};
const double lprev = open(cp.prev_on_contour, cp.prev_trimmed || cp.prev_on_contour->next_trimmed) ?
path_length_along_contour_ccw(cp.prev_on_contour, &cp, length) : std::numeric_limits<double>::max();
const double lnext = open(cp.next_on_contour, cp.next_trimmed || cp.next_on_contour->prev_trimmed) ?
path_length_along_contour_ccw(&cp, cp.next_on_contour, length) : std::numeric_limits<double>::max();
consumed[end] = anchor_length > SCALED_EPSILON;
for (const double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
if (l > join_length)
break;
const size_t other = end_index(l == lprev ? cp.prev_on_contour : cp.next_on_contour);
const size_t polyline1 = find(end / 2), polyline2 = find(other / 2);
if (polyline1 == polyline2)
continue;
parent[polyline2] = polyline1;
consumed[end] = consumed[other] = 1;
++ joins;
break;
}
}
return joins;
};
// Equal arcs taken along the contour pair a wall in one phase; keep the order that joins the most lines.
size_t best_joins = count_joins(arches);
const double bucket = 0.5 * scale_(spacing);
for (const bool backwards : { false, true }) {
std::vector<Arc> along_contour = arches;
std::sort(along_contour.begin(), along_contour.end(), [bucket, backwards](const Arc &l, const Arc &r) {
const long long bucket_l = std::llround(l.arc_length / bucket), bucket_r = std::llround(r.arc_length / bucket);
if (bucket_l != bucket_r)
return bucket_l < bucket_r;
if (l.intersection->contour_idx != r.intersection->contour_idx)
return l.intersection->contour_idx < r.intersection->contour_idx;
return backwards ? l.intersection->param > r.intersection->param : l.intersection->param < r.intersection->param;
});
if (const size_t joins = count_joins(along_contour); joins > best_joins) {
best_joins = joins;
arches = std::move(along_contour);
}
}
}
//FIXME improve the Traveling Salesman problem with 2-opt and 3-opt local optimization.
@@ -1840,7 +1752,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (polyline_idx1 != polyline_idx2) {
Polyline &polyline1 = infill_ordered[polyline_idx1];
Polyline &polyline2 = infill_ordered[polyline_idx2];
if (arc.arc_length < join_length) {
if (arc.arc_length < anchor_length_max) {
// Not closing a loop, connecting the lines.
assert(contour[cp1->point_idx] == polyline1.points.front() || contour[cp1->point_idx] == polyline1.points.back());
if (contour[cp1->point_idx] == polyline1.points.front())
@@ -1849,7 +1761,6 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (contour[cp2->point_idx] == polyline2.points.back())
polyline2.reverse();
take(polyline1, polyline2, contour, cp1, cp2, false);
open_end[end_index(cp1)] = open_end[end_index(cp2)] = 0;
// Mark the second polygon as merged with the first one.
if (polyline_idx2 < polyline_idx1) {
polyline2 = std::move(polyline1);
@@ -1867,23 +1778,6 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
}
}
// Distance along the contour to the nearest open end of another polyline, within 64 ends.
auto distance_to_open_end = [&](const ContourIntersectionPoint &from, size_t polyline_idx, bool backwards) {
const double length = graph.boundary_params[from.contour_idx].back();
double distance = 0.;
const ContourIntersectionPoint *cp = &from;
for (size_t step = 0; step < 64; ++ step) {
const ContourIntersectionPoint *other = backwards ? cp->prev_on_contour : cp->next_on_contour;
if (other == nullptr || other == &from)
break;
distance += backwards ? closed_contour_distance_cw(cp->param, other->param, length) : closed_contour_distance_ccw(cp->param, other->param, length);
if (open_end[end_index(other)] && get_and_update_merged_with(end_index(other) / 2) != polyline_idx)
return distance;
cp = other;
}
return std::numeric_limits<double>::max();
};
// Connect the remaining open infill lines to the perimeter lines if possible.
for (ContourIntersectionPoint &contour_point : graph.map_infill_end_point_to_boundary)
if (! contour_point.consumed && contour_point.contour_idx != boundary_idx_unconnected) {
@@ -1902,7 +1796,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
assert(contour[contour_point.point_idx] == polyline.points.front() || contour[contour_point.point_idx] == polyline.points.back());
bool connected = false;
for (double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
if (l == std::numeric_limits<double>::max() || l > join_length)
if (l == std::numeric_limits<double>::max() || l > anchor_length_max)
break;
// Take the complete contour.
bool reversed = l == lprev;
@@ -1921,7 +1815,6 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (contour[cp2->point_idx] == polyline2.points.back())
polyline2.reverse();
take(polyline, polyline2, contour, &contour_point, cp2, reversed);
open_end[end_index(&contour_point)] = open_end[end_index(cp2)] = 0;
if (polyline_idx < polyline_idx2) {
// Mark the second polyline as merged with the first one.
merged_with[polyline_idx2] = polyline_idx;
@@ -1936,35 +1829,18 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
break;
}
if (! connected && anchor_length > SCALED_EPSILON) {
// Hook toward the nearest open end of another polyline, where the next path likely starts.
const double free_prev = contour_point.contour_not_taken_length_prev;
const double free_next = contour_point.contour_not_taken_length_next;
const double to_prev = distance_to_open_end(contour_point, polyline_idx, true);
const double to_next = distance_to_open_end(contour_point, polyline_idx, false);
bool backwards = free_prev > free_next;
if (to_prev < to_next && free_prev > SCALED_EPSILON)
backwards = true;
else if (to_next < to_prev && free_next > SCALED_EPSILON)
backwards = false;
// Orca: with multiline, a hook toward a copy stops short of it; the copy takes half a width over the sine of its wall angle.
double length = anchor_length;
if (const ContourIntersectionPoint *other = backwards ? contour_point.prev_on_contour : contour_point.next_on_contour;
params.multiline > 1 && other != &contour_point) {
const Point &end = contour[contour_point.point_idx];
Vec2d dir = Vec2d::Zero();
for (size_t i = 1; i < polyline.size() && dir.squaredNorm() < sqr(2. * scale_(spacing) * params.multiline); ++ i)
dir = (end - polyline.points[polyline.points.front() == end ? i : polyline.size() - 1 - i]).cast<double>();
const Vec2d to_copy = (contour[other->point_idx] - end).cast<double>();
const double across = dir.squaredNorm() > 0. ? std::abs(cross2(dir.normalized(), to_copy)) : 0.;
if (across > SCALED_EPSILON && across < scale_(spacing) * params.multiline) {
const double gap = backwards ? path_length_along_contour_ccw(other, &contour_point, contour_params.back()) :
path_length_along_contour_ccw(&contour_point, other, contour_params.back());
length = std::min(length, gap - line_half_width * (1. + to_copy.norm() / across));
}
// Which to take? One could optimize for:
// 1) Shortest path
// 2) Hook length
// ...
// Let's take the longer now, as this improves the chance of another hook to be placed on the other side of this contour point.
double l = std::max(contour_point.contour_not_taken_length_prev, contour_point.contour_not_taken_length_next);
if (l > SCALED_EPSILON) {
if (contour_point.contour_not_taken_length_prev > contour_point.contour_not_taken_length_next)
take_limited(polyline, contour, contour_params, &contour_point, contour_point.prev_on_contour, true, anchor_length, line_half_width);
else
take_limited(polyline, contour, contour_params, &contour_point, contour_point.next_on_contour, false, anchor_length, line_half_width);
}
if ((backwards ? free_prev : free_next) > SCALED_EPSILON)
take_limited(polyline, contour, contour_params, &contour_point, backwards ? contour_point.prev_on_contour : contour_point.next_on_contour,
backwards, std::max(0., length), line_half_width);
}
}
@@ -2914,7 +2790,7 @@ void multiline_fill(Polylines& polylines, const FillParams& params, float spacin
const float delta = float(scale_(t));
// Arc tolerance of 1/500 of the offset, Clipper2's default.
for (const Polygon &ring : offset(polylines, delta, jtMiter, 0.002 * delta, etOpenRound))
for (const Polygon &ring : offset(polylines, delta, jtRound, 0.002 * delta, etOpenRound))
if (ring.size() >= 3)
all_polylines.emplace_back(ring.split_at_first_point());
}
@@ -2922,123 +2798,4 @@ void multiline_fill(Polylines& polylines, const FillParams& params, float spacin
polylines = std::move(all_polylines);
}
void join_close_ends(Polylines &polylines, double max_gap, bool can_reverse)
{
const size_t n = polylines.size();
if (n < 2 || max_gap <= 0.)
return;
static constexpr size_t npos = size_t(-1);
// End 2 * i is the first point of polyline i, 2 * i + 1 its last.
auto end_point = [&polylines](size_t e) -> const Point & { return (e & 1) ? polylines[e >> 1].points.back() : polylines[e >> 1].points.front(); };
// Ends in cells of max_gap, so the ends within max_gap of one are in its cell or the eight around it.
auto cell_of = [max_gap](const Point &p) { return Vec2i64(int64_t(std::floor(p.x() / max_gap)), int64_t(std::floor(p.y() / max_gap))); };
auto key = [](const Vec2i64 &c) { return (uint64_t(c.x()) << 32) ^ uint64_t(uint32_t(c.y())); };
std::unordered_map<uint64_t, std::vector<size_t>> grid;
for (size_t e = 0; e < 2 * n; ++ e)
if (polylines[e >> 1].size() >= 2)
grid[key(cell_of(end_point(e)))].push_back(e);
// The end of a polyline that comes last along its overall direction, taken along the dominant axis.
std::vector<char> backwards(n, 0);
for (size_t i = 0; i < n; ++ i)
if (polylines[i].size() >= 2) {
const Vec2d dir = (polylines[i].points.back() - polylines[i].points.front()).cast<double>();
backwards[i] = std::abs(dir.x()) >= std::abs(dir.y()) ? dir.x() < 0. : dir.y() < 0.;
}
auto trailing = [&backwards](size_t e) { return bool(e & 1) != bool(backwards[e >> 1]); };
// Ends that meet first, then shortest gaps between leading ends, then the others: the copies of neighboring multiline
// groups are all joined at the same side, which leaves their open ends next to each other rather than on alternating walls.
std::vector<std::tuple<bool, bool, double, size_t, size_t>> gaps;
for (const auto &[cell_key, ends] : grid) {
const Vec2i64 c = cell_of(end_point(ends.front()));
for (int64_t dx = -1; dx <= 1; ++ dx)
for (int64_t dy = -1; dy <= 1; ++ dy)
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
for (size_t a : ends)
for (size_t b : it->second)
if (a < b && (a >> 1) != (b >> 1) && (can_reverse || (a & 1) != (b & 1)))
if (const double d = (end_point(a) - end_point(b)).cast<double>().norm(); d <= max_gap)
gaps.emplace_back(d > SCALED_EPSILON, d > SCALED_EPSILON && (trailing(a) || trailing(b)), d, a, b);
}
if (gaps.empty())
return;
std::sort(gaps.begin(), gaps.end());
std::vector<size_t> partner(2 * n, npos), parent(n);
std::iota(parent.begin(), parent.end(), 0);
auto find = [&parent](size_t i) {
while (parent[i] != i)
i = parent[i] = parent[parent[i]];
return i;
};
// A join may not cross a line or another join; touching them at its own ends is fine, as where a line ends on another.
// Only the segments through a cell next to a candidate join can cross one.
std::unordered_set<uint64_t> near_gaps;
for (const auto &[apart, trail, d, a, b] : gaps)
for (size_t e : { a, b })
for (int64_t dx = -1; dx <= 1; ++ dx)
for (int64_t dy = -1; dy <= 1; ++ dy)
near_gaps.insert(key(cell_of(end_point(e)) + Vec2i64(dx, dy)));
Lines near_lines;
for (const Polyline &pl : polylines)
for (size_t i = 1; i < pl.size(); ++ i) {
const Vec2i64 c0 = cell_of(pl.points[i - 1]), c1 = cell_of(pl.points[i]);
const Vec2i64 lo = c0.cwiseMin(c1), hi = c0.cwiseMax(c1);
bool near = (hi - lo).maxCoeff() > 3;
for (int64_t x = lo.x(); x <= hi.x() && ! near; ++ x)
for (int64_t y = lo.y(); y <= hi.y() && ! near; ++ y)
near = near_gaps.count(key(Vec2i64(x, y))) > 0;
if (near)
near_lines.emplace_back(pl.points[i - 1], pl.points[i]);
}
const AABBTreeLines::LinesDistancer<Line> lines(std::move(near_lines));
auto crosses = [&](size_t a, size_t b) {
const Vec2d p = end_point(a).cast<double>(), q = end_point(b).cast<double>();
const double length = (q - p).norm();
if (length <= 2. * SCALED_EPSILON)
return false;
const Vec2d dir = (q - p) / length;
const Line inner(Point((p + SCALED_EPSILON * dir).cast<coord_t>()), Point((q - SCALED_EPSILON * dir).cast<coord_t>()));
if (! lines.get_lines().empty() && ! lines.intersections_with_line<false>(inner).empty())
return true;
const Vec2i64 c = cell_of(end_point(a));
for (int64_t dx = -3; dx <= 3; ++ dx)
for (int64_t dy = -3; dy <= 3; ++ dy)
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
for (size_t e : it->second)
if (partner[e] != npos && Geometry::segments_intersect(inner.a, inner.b, end_point(e), end_point(partner[e])))
return true;
return false;
};
for (const auto &[apart, trail, d, a, b] : gaps)
if (partner[a] == npos && partner[b] == npos && find(a >> 1) != find(b >> 1) && ! crosses(a, b)) {
partner[a] = b;
partner[b] = a;
parent[find(a >> 1)] = find(b >> 1);
}
// Walk each group from a polyline with a free end, entering the next polyline at the end joined to the last one.
Polylines out;
std::vector<char> done(n, 0);
auto walk = [&](size_t first_end) {
Polyline joined;
for (size_t e = first_end; e != npos;) {
Polyline &pl = polylines[e >> 1];
done[e >> 1] = 1;
if (e & 1)
pl.reverse();
joined.points.insert(joined.points.end(), pl.points.begin() + (! joined.empty() && joined.points.back() == pl.points.front()), pl.points.end());
e = partner[e ^ 1];
}
out.emplace_back(std::move(joined));
};
for (size_t i = 0; i < n; ++ i)
if (! done[i] && partner[2 * i] == npos)
walk(2 * i);
for (size_t i = 0; i < n; ++ i)
if (! done[i] && partner[2 * i + 1] == npos)
walk(2 * i + 1);
polylines = std::move(out);
}
} // namespace Slic3r
+6 -9
View File
@@ -33,7 +33,7 @@ namespace Slic3r { class ExtrusionEntityCollection; }
namespace Slic3r {
class Surface;
class AdaptiveInfillField;
class TpmsRadialField;
enum InfillPattern : int;
namespace FillAdaptive {
@@ -93,9 +93,9 @@ struct FillParams
bool gyroid_optimized { false };
// For TPMS: grade the density from the surface to the interior of the object. Density fraction.
AdaptiveInfillMode adaptive_infill_mode { AdaptiveInfillMode::Disabled };
float adaptive_infill_interior_density { 0.f };
AdaptiveInfillGradient adaptive_infill_gradient { AdaptiveInfillGradient::Linear };
TpmsAdaptiveMode tpms_adaptive { TpmsAdaptiveMode::Disabled };
float tpms_interior_density { 0.f };
TpmsAdaptiveGradient tpms_adaptive_gradient { TpmsAdaptiveGradient::Linear };
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
@@ -162,7 +162,7 @@ public:
FillAdaptive::Octree* adapt_fill_octree = nullptr;
// Radial coordinate inside the object for the adaptive TPMS infill
const AdaptiveInfillField* adaptive_infill_field = nullptr;
const TpmsRadialField* tpms_radial_field = nullptr;
// PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring).
// Orca: also used by gap fill function.
@@ -258,11 +258,8 @@ public:
static coord_t _adjust_solid_spacing(const coord_t width, const coord_t distance);
};
//Fill Multiline
//Fill Multiline
void multiline_fill(Polylines& polylines, const FillParams& params, float spacing);
// Joins polylines whose ends are at most max_gap apart, shortest gaps first and without closing a loop, so each such
// group prints as one path. Without can_reverse only the end of one polyline joins the start of another.
void join_close_ends(Polylines &polylines, double max_gap, bool can_reverse);
} // namespace Slic3r
#endif // slic3r_FillBase_hpp_
+6 -6
View File
@@ -18,7 +18,7 @@
#include "FillGyroid.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "AdaptiveInfillField.hpp"
#include "FillTpmsAdaptive.hpp"
namespace Slic3r {
@@ -315,8 +315,8 @@ void FillGyroid::_fill_surface_single(
ExPolygon expolygon,
Polylines &polylines_out)
{
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
});
@@ -342,11 +342,11 @@ void FillGyroid::_fill_surface_single(
// generate pattern
Polylines polylines;
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
// Radians per mm of the regular pattern at a density.
auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.adaptive_infill_interior_density), params.adaptive_infill_gradient},
*this->adaptive_infill_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
*this->tpms_radial_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
} else if (params.gyroid_optimized) {
// Marching-squares path on the gyroid implicit field. Base period matches
// the standard parametric path's wavelength: 2*pi * spacing / density_adj.
Loaded 100 of 242 files, more files were not shown because too many files have changed in this diff. Show more