mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
Slicing from Bambuddy meant taking a process preset as-is; any change meant a round trip through Bambu Studio. The slice dialog now carries OrcaSlicer's full process tree -- pages, groups, labels, tooltips, ranges and defaults extracted from the slicer's own sources. Enable/disable rules are evaluated from the slicer's own enable_if expressions via a recursive-descent interpreter (no eval, CSP), with enum comparisons validated against each option's declared values. Anything undecidable leaves the field editable rather than greyed. Overrides apply after the source's support config (#1881) and the designer's carried tweaks (#2622), so an explicit choice always wins; an untouched panel sends the same request as before. Adds slice_engine as a separate setting from preferred_slicer -- where slicing runs is a different axis from which binary the sidecar drives. Only the sidecar engine is registered, so no picker renders yet.
110 lines
4.4 KiB
Python
110 lines
4.4 KiB
Python
"""Apply the user's own process-setting choices to an outgoing slice.
|
|
|
|
Bambuddy's slice modal can edit OrcaSlicer's full process parameter set (layer
|
|
height, wall count, supports, speeds — the same tree the desktop slicer shows
|
|
under Print Settings). Those edits arrive as a sparse ``{key: value}`` map and
|
|
are written into the process JSON that goes out as ``--load-settings``, using
|
|
the same mechanism ``_patch_process_support_settings`` (#1881) and
|
|
``apply_design_overrides`` (#2622) already use.
|
|
|
|
Precedence is deliberate and is the reason this runs last: the picked preset is
|
|
the base, the source 3MF's support configuration and the designer's own tweaks
|
|
layer on top, and an explicit choice the user made in the modal beats all of
|
|
them. Anything else would silently discard a setting the user just typed.
|
|
|
|
Values are normalised to the string forms a process preset actually stores
|
|
(``"1"`` for a bool, ``"20%"`` for a percent, a list of strings for the
|
|
per-extruder vector options). The frontend already serialises through the option
|
|
schema, so this is a second line of defence for clients that don't — the slicer
|
|
CLI validates far more strictly than the GUI and a wrongly-typed value fails the
|
|
whole slice rather than being coerced.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import re
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Config keys are lowercase identifiers. Anything else did not come from the
|
|
# option schema, so it cannot be a real process setting.
|
|
_KEY_RE = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
|
|
# A process JSON is a flat string map; nesting a structure inside it produces a
|
|
# file the CLI rejects outright.
|
|
_ScalarTypes = (str, int, float, bool)
|
|
|
|
|
|
def _normalise_scalar(value: object) -> str | None:
|
|
"""Render one scalar the way a process preset stores it, or ``None`` if it
|
|
is not a value a process setting can hold."""
|
|
if isinstance(value, bool):
|
|
# Checked before int on purpose — bool is a subclass of int, and a
|
|
# process JSON spells booleans "1"/"0", never "True"/"False".
|
|
return "1" if value else "0"
|
|
if isinstance(value, (int, float)):
|
|
return str(value)
|
|
if isinstance(value, str):
|
|
return value
|
|
return None
|
|
|
|
|
|
def normalise_process_overrides(overrides: dict[str, object]) -> dict[str, str | list[str]]:
|
|
"""Filter and normalise a client-supplied override map.
|
|
|
|
Keys that don't look like config keys, and values that a process preset
|
|
cannot hold, are dropped with a warning rather than failing the slice: the
|
|
user's other settings are still worth applying, and a hard failure here
|
|
would be reported as "slicing failed" with no clue which field caused it.
|
|
"""
|
|
clean: dict[str, str | list[str]] = {}
|
|
for key, value in overrides.items():
|
|
if not isinstance(key, str) or not _KEY_RE.match(key):
|
|
logger.warning("Ignoring process override with unusable key: %r", key)
|
|
continue
|
|
|
|
if isinstance(value, list):
|
|
parts = [_normalise_scalar(v) for v in value]
|
|
if any(p is None for p in parts):
|
|
logger.warning("Ignoring process override %s: list contains a non-scalar entry", key)
|
|
continue
|
|
clean[key] = [p for p in parts if p is not None]
|
|
continue
|
|
|
|
scalar = _normalise_scalar(value)
|
|
if scalar is None:
|
|
logger.warning("Ignoring process override %s: unsupported value type %s", key, type(value).__name__)
|
|
continue
|
|
clean[key] = scalar
|
|
|
|
return clean
|
|
|
|
|
|
def apply_process_overrides(process_json: str, overrides: dict[str, object]) -> str:
|
|
"""Write the user's process settings into the outgoing process JSON.
|
|
|
|
Returns ``process_json`` unchanged when there is nothing to apply or the
|
|
JSON is unparseable, so a bad input degrades to a slice with the picked
|
|
preset rather than failing it — matching ``apply_design_overrides``.
|
|
"""
|
|
if not overrides:
|
|
return process_json
|
|
|
|
clean = normalise_process_overrides(overrides)
|
|
if not clean:
|
|
return process_json
|
|
|
|
try:
|
|
process_cfg = json.loads(process_json)
|
|
except json.JSONDecodeError:
|
|
logger.warning("Process preset JSON is unparseable; skipping %d user override(s)", len(clean))
|
|
return process_json
|
|
if not isinstance(process_cfg, dict):
|
|
return process_json
|
|
|
|
process_cfg.update(clean)
|
|
logger.info("Applying %d user process override(s): %s", len(clean), sorted(clean))
|
|
return json.dumps(process_cfg)
|