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
@@ -4,6 +4,22 @@ Alle Änderungen an Grotti. Format nach [Keep a Changelog](https://keepachangelo
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Neu
|
||||
|
||||
- Wand-Kacheln zeigen das Druck-Vorschaubild (Moonraker-Thumbnail) in der Job-Zeile.
|
||||
|
||||
### Behoben
|
||||
|
||||
- 3D-Pfad wirkte gesprenkelt: Ausdünnen behält ganze Schichten dicht (statt Stichproben entlang der Bahn); keine Parallel-Offsets.
|
||||
- Toolpath-API mit `Cache-Control: no-store`, damit der Browser keinen alten Pfad behält.
|
||||
- Senden: Dateinamen mit Nicht-ASCII (z. B. CJK) werden für Moonraker/Rinkhals auf ASCII normalisiert — sonst Fehler 10115 „gcode … not found“.
|
||||
|
||||
## [2.2.2] – 2026-10-15
|
||||
|
||||
### Behoben
|
||||
|
||||
- 3D-Pfad: bei großen G-codes zerbrach das Ausdünnen die Segment-Paare — Viewer zeigte nur Objekt-Umrisse, keinen Druckfortschritt (grün/grau).
|
||||
|
||||
## [2.2.1] – 2026-10-15
|
||||
|
||||
### Geändert
|
||||
|
||||
@@ -45,6 +45,10 @@ Im Cockpit (nur bei aktivem Druck/Heizen). Danach oft **Strom aus und wieder an*
|
||||
|
||||
Oft noch Verarbeitung auf dem Drucker. In der Sidebar unter **Senden** den Fortschritt beobachten. Nach einem Grotti-Update den Dienst einmal neu starten.
|
||||
|
||||
## „gcode … not found“ (10115)
|
||||
|
||||
Oft Dateinamen mit Nicht-ASCII-Zeichen (z. B. Chinesisch). Grotti normalisiert den Namen beim Senden auf ASCII. Nochmal senden; in der Bibliothek darf der Originalname bleiben.
|
||||
|
||||
## Wo nachschauen
|
||||
|
||||
- Sidebar: Alerts, Läuft, Wartet, Senden
|
||||
|
||||
@@ -12,7 +12,7 @@ Etikett: `/etikett/<id>`, Druck über den Browser. QR per lokalem `qrcode.min.js
|
||||
|
||||
## 3D-Pfad
|
||||
|
||||
`GET /api/v1/dateien/<id>/pfad` liefert einen gedünnten Toolpath (bis ca. 120 000 Punkte, plus `layer_breaks`, Cache-`version` 5) aus dem lokalen G-code. Die Dateien-Seite und das Cockpit zeichnen ihn mit Three.js.
|
||||
`GET /api/v1/dateien/<id>/pfad` liefert einen gedünnten Toolpath (bis ca. 220 000 Punkte, plus `layer_breaks`, Cache-`version` 8) aus dem lokalen G-code. Thinning behält ganze Z-Höhen dicht und lässt nur ganze Schichten weg (sonst gesprenkelte Wände). Die Dateien-Seite und das Cockpit zeichnen ihn mit Three.js.
|
||||
|
||||
Darstellung: nur Extrusionswege. Preview = orange. Live: gedruckt leicht verdickt grün, offen grau; Schnitt nach `progress`. Parser akzeptiert `E.39`/`Z.6`. Bett: Gitter 10 mm, mm-Sprites, Legende.
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ Ab 12.10.2026: Start legt Hintergrund-Aufträge an (`pending` → `running` →
|
||||
|
||||
`watch_dir` in `config.json` zeigt auf den Slice-Ordner der Bibliothek. Ohne Ordner geht Hochladen über die Dateien-Seite trotzdem.
|
||||
|
||||
**Dateinamen:** Moonraker-Multipart nutzt nur noch einen ASCII-`filename` (`moonraker.safe_upload_name`). Früher standen Nicht-ASCII-Zeichen in `filename*=UTF-8''…`, während `filename=` Unterstriche bekam — gklib speicherte A, Start suchte B → **10115 not found**.
|
||||
|
||||
## ACE / `.acm`
|
||||
|
||||
Drucker mit ACE/MMU-Plätzen (`mmu.gate_color` / `gate_material`, Grotti-`slots`) brauchen vor `print/start` eine Sidecar-Datei `*.gcode.acm`. In der UI unter Optionen → ACE-Zuordnung (Datei-Farbe → Platz). Ohne passende `.acm` lehnt der Drucker oft mit „cannot parse the file“ ab — Details hier, nicht als Banner in der App:
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1,25 @@
|
||||
"""ASCII-Dateinamen für Moonraker-Upload."""
|
||||
|
||||
from grotti.moonraker import multipart, safe_upload_name
|
||||
|
||||
|
||||
def test_safe_upload_strips_cjk():
|
||||
name = "1015-2159-虎钳式强力夹子_plate(01)_PLA_0.2_53m57s.gcode"
|
||||
safe = safe_upload_name(name)
|
||||
assert safe.isascii()
|
||||
assert safe.endswith(".gcode")
|
||||
assert "plate(01)" in safe
|
||||
assert "虎" not in safe
|
||||
|
||||
|
||||
def test_multipart_single_ascii_filename():
|
||||
name = "模型_test.gcode"
|
||||
_boundary, body = multipart(name, b"G28\n")
|
||||
head = body.split(b"G28\n", 1)[0].decode("ascii")
|
||||
assert "filename*=UTF-8" not in head
|
||||
assert 'filename="test.gcode"' in head
|
||||
assert all(ord(ch) < 128 for ch in head)
|
||||
|
||||
|
||||
def test_safe_upload_preserves_path():
|
||||
assert safe_upload_name("subdir/foo.gcode") == "subdir/foo.gcode"
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Toolpath-Thinning: ganze Schichten dicht, keine gesprenkelten Stichproben."""
|
||||
|
||||
from grotti.toolpath import TOOLPATH_VERSION, _thin_points, build_toolpath
|
||||
|
||||
|
||||
def _fake_points(layers=40, per_layer=800):
|
||||
points = []
|
||||
breaks = []
|
||||
seq = 0
|
||||
for layer in range(layers):
|
||||
z = 0.2 + layer * 0.2
|
||||
breaks.append(len(points))
|
||||
for i in range(per_layer):
|
||||
extrude = 1 if i > 0 else 0
|
||||
points.append([float(i), float(layer), z, extrude, seq])
|
||||
seq += 1
|
||||
return points, breaks
|
||||
|
||||
|
||||
def test_thin_keeps_consecutive_extrude_pairs():
|
||||
points, breaks = _fake_points()
|
||||
kept, new_breaks = _thin_points(points, breaks, max_points=8_000)
|
||||
assert len(kept) <= 8_000
|
||||
assert new_breaks
|
||||
consec = 0
|
||||
for i in range(1, len(kept)):
|
||||
if kept[i][3] and kept[i][4] == kept[i - 1][4] + 1:
|
||||
consec += 1
|
||||
assert consec > 500, "Thinning darf keine zeichnbaren Segmente übrig lassen"
|
||||
|
||||
|
||||
def test_thin_keeps_solid_layers():
|
||||
points, breaks = _fake_points(layers=50, per_layer=600)
|
||||
kept, _ = _thin_points(points, breaks, max_points=10_000)
|
||||
# Pro behaltener Z-Höhe: hoher Anteil aufeinanderfolgender Extrusionen
|
||||
by_z = {}
|
||||
for i in range(1, len(kept)):
|
||||
if not (kept[i][3] and kept[i][4] == kept[i - 1][4] + 1):
|
||||
continue
|
||||
z = round(kept[i][2], 2)
|
||||
by_z[z] = by_z.get(z, 0) + 1
|
||||
assert by_z, "keine Extrusionssegmente"
|
||||
assert max(by_z.values()) >= 40, "behaltene Schichten sollen dicht sein"
|
||||
|
||||
|
||||
def test_toolpath_version():
|
||||
assert TOOLPATH_VERSION >= 8
|
||||
|
||||
|
||||
def test_build_empty(tmp_path):
|
||||
path = tmp_path / "empty.gcode"
|
||||
path.write_text("; nix\n", encoding="utf-8")
|
||||
data = build_toolpath(path)
|
||||
assert data["points"] == []
|
||||
assert data["version"] == TOOLPATH_VERSION
|
||||
+4
-1
@@ -910,7 +910,10 @@ def create_app(cfg):
|
||||
pass
|
||||
data["file_id"] = file_id
|
||||
data["name"] = row["name"]
|
||||
return jsonify(data)
|
||||
data["version"] = TOOLPATH_VERSION
|
||||
resp = jsonify(data)
|
||||
resp.headers["Cache-Control"] = "no-store"
|
||||
return resp
|
||||
|
||||
@app.patch("/api/v1/dateien/<int:file_id>")
|
||||
def file_patch(file_id):
|
||||
|
||||
@@ -10,8 +10,8 @@ window.GrottiGcodeViewer = (function () {
|
||||
const COLOR_OBJ_EX = 0xef4444;
|
||||
const COLOR_OBJ_CUR = 0xfbbf24;
|
||||
const GRID_STEP = 10;
|
||||
// Nur leichte Doppelung — starke Offsets + Punkte wirkten wie Vollkörper mit Löchern
|
||||
const FAT_OFFSETS = [0, -0.22, 0.22];
|
||||
// Keine Parallel-Offsets: bei ausgedünnten Pfaden wirkten sie gesprenkelt
|
||||
const FAT_OFFSETS = [0];
|
||||
|
||||
function create(canvas) {
|
||||
const wrap = document.createElement("div");
|
||||
|
||||
+34
-3
@@ -620,12 +620,44 @@ article.machine.offline:hover { opacity: 1; }
|
||||
.job-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
gap: 0.55rem;
|
||||
min-width: 0;
|
||||
color: var(--muted);
|
||||
font-size: var(--t-sm);
|
||||
padding: 0.45rem 0.5rem;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
.job-thumb {
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
object-fit: contain;
|
||||
border-radius: 9px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--card-2);
|
||||
flex: none;
|
||||
}
|
||||
.job-thumb[hidden] { display: none; }
|
||||
.job-ico { display: inline-flex; flex: none; color: var(--faint); }
|
||||
.job-ico .ico { width: 1.1rem; height: 1.1rem; }
|
||||
.job-line.has-thumb .job-ico { display: none; }
|
||||
.job-meta {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.job-line .name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
color: var(--ink);
|
||||
font-weight: 650;
|
||||
}
|
||||
.job-line .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; color: var(--ink); }
|
||||
.job-line .swatches { padding: 0; min-height: 0; flex: none; }
|
||||
.badge {
|
||||
font-size: 0.65rem;
|
||||
@@ -1874,7 +1906,6 @@ body.desk-on main {
|
||||
.summary-text b { color: var(--ink); }
|
||||
.page-head-side { display: flex; align-items: center; gap: 0.75rem 1rem; flex-wrap: wrap; }
|
||||
.page-head-side .check { margin: 0; font-size: var(--t-sm); color: var(--muted); }
|
||||
.job-line > .ico { color: var(--faint); }
|
||||
.job-line[hidden] { display: none; }
|
||||
article.machine.flash { animation: flash 2s var(--ease); }
|
||||
@keyframes flash { 0%, 40% { box-shadow: 0 0 0 2px var(--accent), var(--shadow); } }
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script src="{{ url_for('static', filename='three.min.js') }}?v=1"></script>
|
||||
<script src="{{ url_for('static', filename='gcode-viewer.js') }}?v=8"></script>
|
||||
<script src="{{ url_for('static', filename='gcode-viewer.js') }}?v=10"></script>
|
||||
<script src="{{ url_for('static', filename='sweep-viewer.js') }}?v=1"></script>
|
||||
<script>
|
||||
const families = { kobra_s1: "Kobra S1", kobra_3: "Kobra 3" };
|
||||
@@ -198,7 +198,7 @@ async function showToolpath(file, progress) {
|
||||
if (viewerFileId !== file.id) {
|
||||
meta.textContent = "Pfad wird gelesen …";
|
||||
try {
|
||||
const data = await api("/dateien/" + file.id + "/pfad");
|
||||
const data = await api("/dateien/" + file.id + "/pfad?tp=8");
|
||||
viewer.setPath(data);
|
||||
viewerFileId = file.id;
|
||||
meta.textContent = (data.count || 0).toLocaleString("de-DE") + " Punkte";
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='brand/favicon-32.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="512x512" href="{{ url_for('static', filename='brand/favicon.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='brand/apple-touch-icon.png') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='grotti.css') }}?v=39">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='grotti.css') }}?v=41">
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
|
||||
+41
-5
@@ -86,7 +86,7 @@
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script src="{{ url_for('static', filename='three.min.js') }}?v=1"></script>
|
||||
<script src="{{ url_for('static', filename='gcode-viewer.js') }}?v=8"></script>
|
||||
<script src="{{ url_for('static', filename='gcode-viewer.js') }}?v=10"></script>
|
||||
<script>
|
||||
const labels = STATE_LABELS;
|
||||
const rank = { printing: 0, paused: 1, error: 2, heating: 3, complete: 4, cancelled: 5, standby: 6, offline: 7 };
|
||||
@@ -490,7 +490,14 @@ function buildCard(printer) {
|
||||
<div data-role="bar"></div>
|
||||
<div class="stats" data-role="stats"></div>
|
||||
<div data-role="temps"></div>
|
||||
<div class="job-line" data-role="jobline">${icon("cube")}<span class="name" data-role="job"></span><span class="swatches" data-role="swatches"></span></div>
|
||||
<div class="job-line" data-role="jobline">
|
||||
<img class="job-thumb" data-role="thumb" alt="" hidden>
|
||||
<span class="job-ico" data-role="jobico">${icon("cube")}</span>
|
||||
<div class="job-meta">
|
||||
<span class="name" data-role="job"></span>
|
||||
<span class="swatches" data-role="swatches"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div data-role="flow"></div>
|
||||
<div data-role="strips"></div>
|
||||
<div class="card-actions" data-role="controls"></div>
|
||||
@@ -551,7 +558,36 @@ function patchCard(article, printer) {
|
||||
setHtml(role("stats"), statsHtml(printer, state));
|
||||
setHtml(role("temps"), tempbarsHtml(printer, state));
|
||||
const jobline = role("jobline");
|
||||
if (jobline) jobline.hidden = !jobLineVisible(printer, state);
|
||||
const jobVisible = jobLineVisible(printer, state);
|
||||
const hasThumb = !!(jobVisible && printer.preview);
|
||||
if (jobline) {
|
||||
jobline.hidden = !jobVisible;
|
||||
jobline.classList.toggle("has-thumb", hasThumb);
|
||||
}
|
||||
const thumb = role("thumb");
|
||||
const jobico = role("jobico");
|
||||
if (thumb) {
|
||||
if (hasThumb) {
|
||||
const next = withToken(printer.preview);
|
||||
if (thumb.dataset.srcKey !== next) {
|
||||
thumb.dataset.srcKey = next;
|
||||
thumb.src = next;
|
||||
}
|
||||
thumb.hidden = false;
|
||||
thumb.onerror = () => {
|
||||
thumb.hidden = true;
|
||||
thumb.dataset.srcKey = "";
|
||||
if (jobline) jobline.classList.remove("has-thumb");
|
||||
if (jobico) jobico.hidden = false;
|
||||
};
|
||||
if (jobico) jobico.hidden = true;
|
||||
} else {
|
||||
thumb.hidden = true;
|
||||
thumb.removeAttribute("src");
|
||||
thumb.dataset.srcKey = "";
|
||||
if (jobico) jobico.hidden = false;
|
||||
}
|
||||
}
|
||||
const job = role("job");
|
||||
if (job) {
|
||||
job.textContent = jobName(printer.filename) || "";
|
||||
@@ -762,10 +798,10 @@ async function syncDeskViewer(printer) {
|
||||
}
|
||||
try {
|
||||
let data = toolpathCache.get(file.id);
|
||||
const needLoad = !data || data.version !== 5 || deskViewerFileId !== file.id;
|
||||
const needLoad = !data || data.version !== 8 || deskViewerFileId !== file.id;
|
||||
if (needLoad) {
|
||||
if (caption) caption.textContent = "wird gelesen …";
|
||||
data = await api("/dateien/" + file.id + "/pfad");
|
||||
data = await api("/dateien/" + file.id + "/pfad?tp=8");
|
||||
toolpathCache.set(file.id, data);
|
||||
const sameFile = deskViewerFileId === file.id;
|
||||
deskViewer.setPath(data, { keepView: sameFile });
|
||||
|
||||
Reference in new issue
Block a user