forked from viewit/KX-Bridge-Release
148 lines
5.7 KiB
Python
148 lines
5.7 KiB
Python
"""OrcaSlicer Filament-Profil Parser.
|
|
|
|
Geteilt zwischen dem Generator (tools/gen_orca_filament_list.py) und dem
|
|
Custom-Profile-Import-Endpoint (bridge/kobrax_moonraker_bridge.py).
|
|
|
|
Liest Orca-Filament-JSON-Dateien (System- oder User-Profile) und gibt
|
|
sie als normalisierte Liste mit (id, name, vendor, type, color) zurück.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
|
|
|
|
def first_str(value, default: str = "") -> str:
|
|
"""Orca-Profile speichern manche Felder als ['wert']. Liefert erstes
|
|
Element als String."""
|
|
if isinstance(value, list):
|
|
return str(value[0]) if value else default
|
|
if isinstance(value, str):
|
|
return value
|
|
return default
|
|
|
|
|
|
def clean_name(raw: str) -> str:
|
|
"""Strippt printer-/varianten-spezifische Suffixe:
|
|
'PolyTerra PLA @base' → 'PolyTerra PLA'
|
|
'Anycubic PLA @Anycubic Kobra X 0.4 nozzle' → 'Anycubic PLA'
|
|
'Anker Generic PLA 0.4 nozzle' → 'Anker Generic PLA'
|
|
"""
|
|
name = re.sub(r"\s*@.*$", "", raw).strip()
|
|
name = re.sub(r"\s+\d+(\.\d+)?\s*nozzle\s*$", "", name, flags=re.IGNORECASE).strip()
|
|
return name or raw
|
|
|
|
|
|
def parse_profile(data: dict, by_name: dict | None = None,
|
|
path_vendor: str | None = None,
|
|
source_path: str = "",
|
|
system_index: list | None = None) -> dict | None:
|
|
"""Parsed ein einzelnes Orca-Filament-Profil zum Bridge-Schema.
|
|
|
|
`by_name` ist optional ein {name: [profile, …]}-Index für Inherits-Resolve
|
|
aus dem rohen Source-Tree (Generator). Bei Single-File-Import (User-Datei
|
|
aus OrcaSlicer-User-Dir) reichen wir stattdessen `system_index` rein —
|
|
die fertige System-Profile-Liste aus orca_filaments.json. Damit können
|
|
wir filament_id/vendor/type/color über die `inherits`-Kette aus dem
|
|
System-Parent ableiten, auch wenn das User-Profil diese Felder nicht
|
|
selbst setzt (typisch: User-Override-Profile haben nur Tweaks).
|
|
|
|
Liefert {id, name, vendor, type, color} oder None wenn das Profil
|
|
keine filament_id hat (z.B. abstrakte @base-Templates).
|
|
"""
|
|
if not isinstance(data, dict):
|
|
return None
|
|
# User-Profile aus dem OrcaSlicer-User-Dir setzen oft KEIN "type"-Feld —
|
|
# das kommt vom System-Parent. Wir akzeptieren das wenn entweder "type"
|
|
# explizit "filament" ist ODER ein "inherits" auf ein anderes Profil zeigt.
|
|
if data.get("type") not in (None, "filament") and not data.get("inherits"):
|
|
return None
|
|
if data.get("type") == "filament" and data.get("inherits") is None and not data.get("filament_id"):
|
|
# type=filament aber kein parent + keine ID → wertloses Stub
|
|
return None
|
|
inst = data.get("instantiation", "true")
|
|
if isinstance(inst, str) and inst.lower() == "false":
|
|
return None
|
|
|
|
# Build system-name-Index für den fallback-Lookup wenn system_index gesetzt.
|
|
sys_by_name: dict[str, dict] = {}
|
|
if system_index:
|
|
for p in system_index:
|
|
if isinstance(p, dict) and p.get("name"):
|
|
sys_by_name[p["name"]] = p
|
|
|
|
def _resolve(key: str, depth: int = 5):
|
|
cur_list = [data]
|
|
for _ in range(depth):
|
|
for cur in cur_list:
|
|
v = cur.get(key)
|
|
if v not in ("", [], None, [""]) and v is not None:
|
|
return v
|
|
# Erst raw-Inherits via by_name (Generator-Pfad)
|
|
if by_name:
|
|
next_list: list[dict] = []
|
|
for cur in cur_list:
|
|
parent_name = cur.get("inherits")
|
|
if parent_name and parent_name in by_name:
|
|
next_list.extend(by_name[parent_name])
|
|
if next_list:
|
|
cur_list = next_list
|
|
continue
|
|
break
|
|
return None
|
|
|
|
def _resolve_via_system_index(key: str):
|
|
"""Inherits-Kette über system_index (clean_name-Match)."""
|
|
parent_raw = data.get("inherits")
|
|
if not parent_raw or not sys_by_name:
|
|
return None
|
|
parent_clean = clean_name(parent_raw)
|
|
sys_p = sys_by_name.get(parent_clean)
|
|
if not sys_p:
|
|
return None
|
|
# System-JSON benutzt schon das normalisierte Schema
|
|
mapping = {
|
|
"filament_id": "id",
|
|
"filament_vendor": "vendor",
|
|
"filament_type": "type",
|
|
"default_filament_colour": "color",
|
|
}
|
|
return sys_p.get(mapping.get(key, key))
|
|
|
|
def _resolve_full(key: str):
|
|
v = _resolve(key)
|
|
if v not in ("", [], None, [""]) and v is not None:
|
|
return v
|
|
return _resolve_via_system_index(key)
|
|
|
|
fid = _resolve_full("filament_id")
|
|
if not fid or not isinstance(fid, str):
|
|
return None
|
|
|
|
name_raw = data.get("name", fid)
|
|
name = clean_name(name_raw)
|
|
vendor = first_str(_resolve_full("filament_vendor")) or (path_vendor or "Generic")
|
|
ftype = first_str(_resolve_full("filament_type"), "")
|
|
color = first_str(_resolve_full("default_filament_colour"), "")
|
|
|
|
return {
|
|
"id": fid,
|
|
"name": name,
|
|
"vendor": vendor,
|
|
"type": ftype,
|
|
"color": color,
|
|
}
|
|
|
|
|
|
def parse_profile_bytes(blob: bytes, source_name: str = "",
|
|
system_index: list | None = None) -> dict | None:
|
|
"""Liest ein einzelnes Profil aus JSON-Bytes. Für File-Upload-Pfad.
|
|
`system_index` ist optional die fertige Liste aus orca_filaments.json —
|
|
wird für die Inherits-Resolve von User-Profilen genutzt die das volle
|
|
Schema vom System-Parent erben."""
|
|
try:
|
|
data = json.loads(blob.decode("utf-8", errors="replace"))
|
|
except Exception:
|
|
return None
|
|
return parse_profile(data, source_path=source_name, system_index=system_index)
|