| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109 |
- """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)
|