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:
1 parent
7da895bbcd
commit
b277ca48aa
16 files changed
+302
-50
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -1,3 +1,3 @@
|
||||
"""Version von Grotti (SemVer). Bei jeder Änderung CHANGELOG.md mitführen."""
|
||||
|
||||
__version__ = "2.2.1"
|
||||
__version__ = "2.2.6"
|
||||
Reference in new issue
Block a user