fix(slicer): wrong-printer slicing + sliced-archive filament list + per-instance MakerWorld compat

Five stacked slice-pipeline bugs that each made the modal's profile picker
  theatrical for 3MF inputs:

  (1) `_strip_3mf_embedded_settings` removed `model_settings.config` /
      `slice_info.config` / `cut_information.xml` along with
      `project_settings.config`. The CLI silently exited after
      "Initializing StaticPrintConfigs" — exit 0, no result.json — and
      Bambuddy masked the failure by re-running with embedded settings
      and the source's bound printer. Strip removed from the dispatch
      path entirely.

  (2) Standard-tier preset stubs lacked the `type` field, so the CLI
      rejected `--load-settings` with rc=-5 ("input preset file is
      invalid") and the same masking fallback fired. Added
      `_SLOT_TO_PROFILE_TYPE` so each stub carries the right
      machine/process/filament discriminator.

  (3) Sliced-archive cards listed every project-wide AMS slot (16+
      swatches for a 2-color print). `slice_and_persist_as_archive` now
      reads `filament_type` / `filament_color` from the sliced output's
      `slice_info.config` (which `ThreeMFParser` already gates on
      `used_g > 0`) instead of inheriting from the source archive.

  (4) SliceModal had no warning when the picked printer profile didn't
      match the source 3MF — the CLI rejects cross-printer slices
      (rc=-16) and fell back to embedded settings, producing wrong-printer
      g-code that errored at print dispatch. Plates response now exposes
      `source_printer_model`; the modal compares against the picked
      profile name and disables Slice + shows an inline warning on
      mismatch.

  (5) MakerWorld URL-paste resolver listed plate instances without
      showing which printer each was sliced for (`/instances/hits`
      omits compatibility info that lives on `design.instances[]
      .extention.modelInfo`). The resolve route now joins both payloads
      by instance ID and forwards `compatibility` + `otherCompatibility`
      onto each hit; the MakerWorld page renders "Sliced for {primary}"
      + "Also marked compatible: ..." per row.

  Tests: 6 unit tests for `extract_source_printer_model_from_3mf`, 1 for
  filtered filament metadata via ThreeMFParser, 2 for makerworld resolve
  compat-merge (happy path + missing modelInfo), 3 frontend SliceModal
  tests for the printer-mismatch warning + Slice-disabled gate. New i18n
  keys `slice.printerMismatch`, `makerworld.slicedFor`,
  `makerworld.alsoCompatible` across all 8 locales.
This commit is contained in:
maziggy
2026-04-28 13:58:56 +02:00
parent 6497eefe60
commit c1f69ee0cc
25 changed files with 688 additions and 127 deletions
+9
View File
@@ -31,6 +31,7 @@ from backend.app.utils.threemf_tools import (
extract_nozzle_mapping_from_3mf,
extract_plate_extruder_set_from_3mf,
extract_project_filaments_from_3mf,
extract_source_printer_model_from_3mf,
)
logger = logging.getLogger(__name__)
@@ -3072,12 +3073,20 @@ async def get_archive_plates(
# to preview gcode — the viewer, skip-objects — can gate on this instead of
# 404-ing on every plate request.
has_gcode = bool(gcode_files)
# SliceModal pre-check signal — see library.py for rationale.
source_printer_model: str | None = None
try:
with zipfile.ZipFile(file_path, "r") as zf:
source_printer_model = extract_source_printer_model_from_3mf(zf)
except (zipfile.BadZipFile, OSError):
pass
return {
"archive_id": archive_id,
"filename": archive.filename,
"plates": plates,
"is_multi_plate": len(plates) > 1,
"has_gcode": has_gcode,
"source_printer_model": source_printer_model,
}
+28 -5
View File
@@ -65,6 +65,7 @@ from backend.app.utils.threemf_tools import (
extract_nozzle_mapping_from_3mf,
extract_plate_extruder_set_from_3mf,
extract_project_filaments_from_3mf,
extract_source_printer_model_from_3mf,
)
logger = logging.getLogger(__name__)
@@ -2302,11 +2303,22 @@ async def get_library_file_plates(
except Exception as e:
logger.warning("Failed to parse plates from library file %s: %s", file_id, e)
# SliceModal pre-check signal: the source 3MF's bound printer model. The
# CLI cannot re-slice for a different printer; surface this so the modal
# can warn the user before they pick a mismatched profile.
source_printer_model: str | None = None
try:
with zipfile.ZipFile(file_path, "r") as zf:
source_printer_model = extract_source_printer_model_from_3mf(zf)
except (zipfile.BadZipFile, OSError):
pass
return {
"file_id": file_id,
"filename": lib_file.filename,
"plates": plates,
"is_multi_plate": len(plates) > 1,
"source_printer_model": source_printer_model,
}
@@ -2671,13 +2683,24 @@ async def _run_slicer_with_fallback(
detail=f"Unknown preferred_slicer setting: '{preferred}'. Expected 'orcaslicer' or 'bambu_studio'.",
)
# Note: an earlier version of this code stripped Metadata/project_settings.
# config + model_settings.config + slice_info.config + cut_information.xml
# before forwarding the 3MF, the theory being that --load-settings would
# then take precedence cleanly. That theory was wrong: model_settings.
# config carries the plate definitions the CLI needs to map `--slice N`
# to a real plate, and slice_info / project_settings supply baseline
# config the CLI's StaticPrintConfig pass needs at all. Stripping ANY
# of them caused the CLI to silently exit immediately after
# "Initializing StaticPrintConfigs" — exit code 0, no result.json, no
# stderr — which Node's child_process treated as failure and Bambuddy
# then masked by falling back to slice_without_profiles using the
# un-stripped bytes (and the source's embedded printer). Net effect:
# every 3MF slice with profiles silently produced wrong-printer output.
# Forwarding the original bytes lets --load-settings override the
# specific fields the user changed (printer/process/filament) while
# the embedded plate / model definitions remain intact.
is_3mf = model_filename.lower().endswith(".3mf")
primary_bytes = model_bytes
if is_3mf:
try:
primary_bytes = _strip_3mf_embedded_settings(model_bytes)
except (zipfile.BadZipFile, KeyError) as exc:
raise HTTPException(status_code=400, detail=f"Source 3MF is corrupt: {exc}") from exc
used_embedded_settings = False
service = SlicerApiService(api_url)
+30
View File
@@ -182,6 +182,36 @@ async def resolve_url(
if not isinstance(instances, list):
instances = []
# /instances/hits omits the per-instance printer compatibility info that
# /design.instances[].extention.modelInfo carries (compatibility +
# otherCompatibility). Merge it in so the frontend can show "this
# instance was sliced for A1" + "also marked compatible with: H2D, P1S,
# …" before the user picks one — without that, every instance row looks
# identical in the UI and users blindly pick the first one regardless of
# whether it matches their printer.
design_instances = design.get("instances") or []
if isinstance(design_instances, list):
compat_by_id = {}
for di in design_instances:
if not isinstance(di, dict):
continue
iid = di.get("id")
if iid is None:
continue
ext = (di.get("extention") or {}).get("modelInfo") or {}
compat_by_id[iid] = {
"compatibility": ext.get("compatibility"),
"otherCompatibility": ext.get("otherCompatibility"),
}
for inst in instances:
if not isinstance(inst, dict):
continue
iid = inst.get("id")
extra = compat_by_id.get(iid)
if extra:
inst["compatibility"] = extra["compatibility"]
inst["otherCompatibility"] = extra["otherCompatibility"]
# Find every library row whose source_url is either the model-level
# canonical URL (legacy whole-model imports) or any plate-level URL
# (``...#profileId-{n}``) under this model. The frontend surfaces this