process_overrides.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. """Apply the user's own process-setting choices to an outgoing slice.
  2. Bambuddy's slice modal can edit OrcaSlicer's full process parameter set (layer
  3. height, wall count, supports, speeds — the same tree the desktop slicer shows
  4. under Print Settings). Those edits arrive as a sparse ``{key: value}`` map and
  5. are written into the process JSON that goes out as ``--load-settings``, using
  6. the same mechanism ``_patch_process_support_settings`` (#1881) and
  7. ``apply_design_overrides`` (#2622) already use.
  8. Precedence is deliberate and is the reason this runs last: the picked preset is
  9. the base, the source 3MF's support configuration and the designer's own tweaks
  10. layer on top, and an explicit choice the user made in the modal beats all of
  11. them. Anything else would silently discard a setting the user just typed.
  12. Values are normalised to the string forms a process preset actually stores
  13. (``"1"`` for a bool, ``"20%"`` for a percent, a list of strings for the
  14. per-extruder vector options). The frontend already serialises through the option
  15. schema, so this is a second line of defence for clients that don't — the slicer
  16. CLI validates far more strictly than the GUI and a wrongly-typed value fails the
  17. whole slice rather than being coerced.
  18. """
  19. from __future__ import annotations
  20. import json
  21. import logging
  22. import re
  23. logger = logging.getLogger(__name__)
  24. # Config keys are lowercase identifiers. Anything else did not come from the
  25. # option schema, so it cannot be a real process setting.
  26. _KEY_RE = re.compile(r"^[a-z][a-z0-9_]*$")
  27. # A process JSON is a flat string map; nesting a structure inside it produces a
  28. # file the CLI rejects outright.
  29. _ScalarTypes = (str, int, float, bool)
  30. def _normalise_scalar(value: object) -> str | None:
  31. """Render one scalar the way a process preset stores it, or ``None`` if it
  32. is not a value a process setting can hold."""
  33. if isinstance(value, bool):
  34. # Checked before int on purpose — bool is a subclass of int, and a
  35. # process JSON spells booleans "1"/"0", never "True"/"False".
  36. return "1" if value else "0"
  37. if isinstance(value, (int, float)):
  38. return str(value)
  39. if isinstance(value, str):
  40. return value
  41. return None
  42. def normalise_process_overrides(overrides: dict[str, object]) -> dict[str, str | list[str]]:
  43. """Filter and normalise a client-supplied override map.
  44. Keys that don't look like config keys, and values that a process preset
  45. cannot hold, are dropped with a warning rather than failing the slice: the
  46. user's other settings are still worth applying, and a hard failure here
  47. would be reported as "slicing failed" with no clue which field caused it.
  48. """
  49. clean: dict[str, str | list[str]] = {}
  50. for key, value in overrides.items():
  51. if not isinstance(key, str) or not _KEY_RE.match(key):
  52. logger.warning("Ignoring process override with unusable key: %r", key)
  53. continue
  54. if isinstance(value, list):
  55. parts = [_normalise_scalar(v) for v in value]
  56. if any(p is None for p in parts):
  57. logger.warning("Ignoring process override %s: list contains a non-scalar entry", key)
  58. continue
  59. clean[key] = [p for p in parts if p is not None]
  60. continue
  61. scalar = _normalise_scalar(value)
  62. if scalar is None:
  63. logger.warning("Ignoring process override %s: unsupported value type %s", key, type(value).__name__)
  64. continue
  65. clean[key] = scalar
  66. return clean
  67. def apply_process_overrides(process_json: str, overrides: dict[str, object]) -> str:
  68. """Write the user's process settings into the outgoing process JSON.
  69. Returns ``process_json`` unchanged when there is nothing to apply or the
  70. JSON is unparseable, so a bad input degrades to a slice with the picked
  71. preset rather than failing it — matching ``apply_design_overrides``.
  72. """
  73. if not overrides:
  74. return process_json
  75. clean = normalise_process_overrides(overrides)
  76. if not clean:
  77. return process_json
  78. try:
  79. process_cfg = json.loads(process_json)
  80. except json.JSONDecodeError:
  81. logger.warning("Process preset JSON is unparseable; skipping %d user override(s)", len(clean))
  82. return process_json
  83. if not isinstance(process_cfg, dict):
  84. return process_json
  85. process_cfg.update(clean)
  86. logger.info("Applying %d user process override(s): %s", len(clean), sorted(clean))
  87. return json.dumps(process_cfg)