Grotti 2.2.6: Wand-Vorschau, dichterer 3D-Pfad, ASCII-Dateinamen.

Kacheln zeigen Moonraker-Thumbnails; Toolpath-Thinning behält ganze Schichten; Nicht-ASCII-Namen vermeiden 10115 beim Senden.
This commit is contained in:
Dominic Gundel committed 2026-10-17 11:22:43 +02:00
1 parent 7da895bbcd
commit b277ca48aa
16 files changed
+302 -50

No files matched your search

+2
View File
@@ -374,6 +374,8 @@ def send_file(
progress("vorbereiten", 30, "Datei vorbereiten…")
filename, content, plate = library.printer_file(row)
# CJK/Unicode im Namen → Rinkhals 10115 not found (Upload ≠ Start-Name)
filename = moonraker.safe_upload_name(filename)
ace_tools = [item.get("paint_index") for item in mapping] if mapping else None
if (row.get("kind") or "") == "gcode" and str(filename).endswith(".gcode"):
content = prepare_rinkhals_gcode(content, tools=ace_tools)
+42 -5
View File
@@ -165,18 +165,53 @@ def post_json(host, path, payload, timeout=20):
raise PrinterError("Der Drucker ist nicht erreichbar.") from exc
def safe_upload_name(filename):
"""Dateiname für Moonraker/Rinkhals/gklib — nur ASCII.
`filename` vs. `filename*` mit CJK führte zu Upload unter `_` und Start
unter dem Originalnamen → Fehler 10115 „gcode … not found“.
"""
raw = (filename or "").replace("\\", "/")
parts = [part for part in raw.split("/") if part]
base = parts[-1] if parts else "job.gcode"
out = []
for ch in base:
if ch.isascii() and ch not in '"\\':
out.append(ch if ch not in " \t" else "_")
else:
out.append("_")
text = "".join(out)
while "__" in text:
text = text.replace("__", "_")
while "_-" in text or "-_" in text:
text = text.replace("_-", "-").replace("-_", "-")
text = text.strip("._-") or "job"
lower = text.lower()
if not (lower.endswith(".gcode") or lower.endswith(".gcode.3mf") or lower.endswith(".3mf") or lower.endswith(".acm")):
# Suffix vom Original behalten, falls nur der Stamm zerstört wurde
if base.lower().endswith(".gcode.3mf"):
text += ".gcode.3mf"
elif base.lower().endswith(".gcode"):
text += ".gcode"
elif base.lower().endswith(".acm"):
text += ".acm"
if parts:
parts[-1] = text
return "/".join(parts)
return text
def multipart(filename, content):
boundary = "----GrottiBoundary7f3a"
ascii_name = "".join(ch if ch.isascii() and ch not in '"\\' else "_" for ch in filename) or "job.gcode"
quoted = urllib.parse.quote(filename)
name = safe_upload_name(filename)
fields = (
"--%s\r\nContent-Disposition: form-data; name=\"root\"\r\n\r\ngcodes\r\n" % boundary
).encode("ascii")
head = (
"--%s\r\n"
"Content-Disposition: form-data; name=\"file\"; filename=\"%s\"; filename*=UTF-8''%s\r\n"
"Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n"
"Content-Type: application/octet-stream\r\n\r\n"
) % (boundary, ascii_name, quoted)
) % (boundary, name)
tail = ("\r\n--%s--\r\n" % boundary).encode("ascii")
return boundary, fields + head.encode("ascii") + content + tail
@@ -217,6 +252,7 @@ def upload_timeout(size):
def upload(host, filename, content, on_progress=None):
filename = safe_upload_name(filename)
boundary, body = multipart(filename, content)
stream = _UploadBody(body, on_progress=on_progress)
request = urllib.request.Request(
@@ -236,7 +272,8 @@ def upload(host, filename, content, on_progress=None):
except (urllib.error.URLError, TimeoutError) as exc:
raise PrinterError("Hochladen ist fehlgeschlagen.") from exc
item = (payload.get("result") or {}).get("item") or {}
return item.get("path") or filename
# Immer denselben ASCII-Namen nutzen, den wir hochgeladen haben
return safe_upload_name(item.get("path") or filename)
def start_print(host, filename, bed_leveling=False, ams_settings=None):
+70 -29
View File
@@ -8,8 +8,8 @@ MOVE = re.compile(r"^(?:G0|G00|G1|G01)\b(.*)$", re.IGNORECASE)
# Auch E.39 / Z.6 ohne führende Null (Anycubic/Prusa-Stil)
AXIS = re.compile(r"([XYZE])\s*(-?\d*\.?\d+)", re.IGNORECASE)
# Cache-/Viewer-Version: Segment-IDs, kein Sehnen-Thinning
TOOLPATH_VERSION = 5
# Cache-/Viewer-Version: ganze Z-Höhen dicht behalten (keine gesprenkelten Stichproben)
TOOLPATH_VERSION = 8
def _gcode_text(path: Path) -> str:
@@ -28,48 +28,89 @@ def _gcode_text(path: Path) -> str:
return path.read_text(encoding="utf-8", errors="replace")
def _z_key(z):
return round(float(z), 2)
def _thin_points(points, layer_breaks, max_points):
"""Extrusions-Segmente stichprobenartig behalten — nie Endpunkte einer Bahn
weglassen und die Nachbarn verbinden (sonst Sehnen statt Infill)."""
"""Pfad kürzen, ohne Wände zu sprenkeln.
Strategie: ganze Z-Höhen (Druckschichten) vollständig behalten und nur
ganze Höhen weglassen. Stichproben entlang der Bahn erzeugen Lücken.
"""
if len(points) <= max_points:
return points, layer_breaks
extrude_ends = [i for i in range(1, len(points)) if points[i][3]]
keep = {0, len(points) - 1}
for break_i in layer_breaks:
keep.add(max(0, min(len(points) - 1, break_i)))
if break_i > 0:
keep.add(break_i - 1)
# Nur Z-Höhen mit Extrusion — Z-Hops allein zählen nicht
z_order = []
seen = set()
for point in points:
if not point[3]:
continue
key = _z_key(point[2])
if key not in seen:
seen.add(key)
z_order.append(key)
if not z_order:
return points[:max_points], [0]
# Budget für Segment-Endpunkte (jedes Segment braucht 2 Indizes, viele teilen sich)
target_segs = max(2000, max_points // 2)
step = max(1, (len(extrude_ends) + target_segs - 1) // target_segs)
for j, end_i in enumerate(extrude_ends):
if j % step == 0:
def keep_for_zs(keep_zs):
keep = {0, len(points) - 1}
for i, point in enumerate(points):
if _z_key(point[2]) in keep_zs:
keep.add(i)
if i > 0:
keep.add(i - 1)
return sorted(keep)
# So viele Höhen wie ins Budget passen (Schätzung über mittlere Punkte/Höhe)
per_z = max(1, len(points) // max(1, len(z_order)))
max_zs = max(2, min(len(z_order), max_points // per_z))
stride = max(1, (len(z_order) + max_zs - 1) // max_zs)
def pick_zs(step):
chosen = {z_order[0], z_order[-1]}
for i in range(0, len(z_order), step):
chosen.add(z_order[i])
return chosen
step = stride
ordered = keep_for_zs(pick_zs(step))
while len(ordered) > max_points and step < len(z_order):
step += 1
ordered = keep_for_zs(pick_zs(step))
# Immer noch zu groß: innerhalb behaltener Höhen jedes n-te Extrusionssegment
if len(ordered) > max_points:
keep_zs = pick_zs(step)
segs = [
i
for i in range(1, len(points))
if points[i][3] and _z_key(points[i][2]) in keep_zs
]
budget = max(1, (max_points - 4) // 2)
seg_step = max(1, (len(segs) + budget - 1) // budget)
keep = {0, len(points) - 1}
for end_i in segs[::seg_step]:
keep.add(end_i - 1)
keep.add(end_i)
ordered = sorted(keep)
# Falls immer noch zu groß: Segmente grober
if len(ordered) > max_points:
step2 = max(1, (len(ordered) + max_points - 1) // max_points)
ordered = ordered[::step2]
if ordered[-1] != len(points) - 1:
ordered.append(len(points) - 1)
ordered = sorted(keep)
index_map = {old: new for new, old in enumerate(ordered)}
kept = [points[i] for i in ordered]
new_breaks = sorted({
index_map[b]
for b in layer_breaks
if b in index_map
})
new_breaks = []
last_z = None
for new_i, point in enumerate(kept):
z = _z_key(point[2])
if last_z is None or abs(z - last_z) > 0.01:
new_breaks.append(new_i)
last_z = z
if not new_breaks:
new_breaks = [0]
return kept, new_breaks
def build_toolpath(path: Path, max_points=120000):
def build_toolpath(path: Path, max_points=220000):
text = _gcode_text(path)
if not text:
return {
+1 -1
View File
@@ -1,3 +1,3 @@
"""Version von Grotti (SemVer). Bei jeder Änderung CHANGELOG.md mitführen."""
__version__ = "2.2.1"
__version__ = "2.2.6"