mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
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:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user