slicerSettings.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. /**
  2. * Conversion between the settings panel's editing values and the string forms
  3. * OrcaSlicer / BambuStudio write into a process preset JSON.
  4. *
  5. * This matters more than it looks. The values we send are merged into the
  6. * `--load-settings` process JSON, and that JSON is parsed by the slicer CLI,
  7. * which validates far more strictly than the GUI: a percent option written as
  8. * `"20"` instead of `"20%"` is a different value, and a bare `true` where the
  9. * config expects `"1"` fails the parse outright. The panel therefore always
  10. * serialises through the schema, never by guessing from the JavaScript type.
  11. */
  12. import type { ProcessOption, ProcessSchema, SettingValue } from '../types/slicerSettings';
  13. /** Option types whose config value is a per-extruder vector. */
  14. const VECTOR_TYPES = new Set(['coBools', 'coFloats', 'coFloatsOrPercents']);
  15. export const isVectorOption = (option: ProcessOption): boolean => VECTOR_TYPES.has(option.type);
  16. /**
  17. * Numeric bound from the schema, or `undefined` when it isn't a number at all.
  18. * Float literals are normalised by the generator, but a handful of bounds are
  19. * unresolved C++ expressions the extractor could not follow, and those must not
  20. * reach an input's `min`/`max`.
  21. */
  22. export function numericBound(bound: number | string | undefined): number | undefined {
  23. if (typeof bound === 'number') return Number.isFinite(bound) ? bound : undefined;
  24. if (typeof bound !== 'string') return undefined;
  25. const n = Number.parseFloat(bound);
  26. return Number.isFinite(n) ? n : undefined;
  27. }
  28. /**
  29. * A unit suffix worth showing. A few entries carry an unresolved C++ expression
  30. * where the extractor could not follow a reference (`def_x->sidetext`); showing
  31. * that to a user would be worse than showing no unit at all.
  32. */
  33. export function displaySidetext(option: ProcessOption): string | undefined {
  34. const s = option.sidetext;
  35. if (!s || s.includes('->') || s.includes('::')) return undefined;
  36. return s;
  37. }
  38. /**
  39. * What an untouched field shows.
  40. *
  41. * The picked preset's own value when we have it, else the option schema's
  42. * compiled-in default. The distinction is user-visible: `line_width` defaults
  43. * to 0 in OrcaSlicer's C++ (meaning "derive from the nozzle"), while a real
  44. * process preset sets something like 0.42 — showing the former for a preset
  45. * that sets the latter is simply wrong.
  46. */
  47. export function baselineForDisplay(option: ProcessOption, presetValue?: SettingValue): string {
  48. const d = presetValue !== undefined ? presetValue : option.default;
  49. if (d === undefined) return '';
  50. // Per-extruder vectors render as a comma-separated list. C++ literal
  51. // artefacts (`0.`, `0.3f`, `100.%`) are normalised by
  52. // scripts/generate-slicer-schema.mjs, so nothing needs unpicking here.
  53. if (Array.isArray(d)) return d.map(String).join(', ');
  54. if (typeof d === 'boolean') return d ? '1' : '0';
  55. return String(d);
  56. }
  57. /**
  58. * Serialises one edited value into its process-JSON form.
  59. *
  60. * Vector options are written back as arrays because that is how the config
  61. * stores them; scalars become strings, which is what every Bambu process preset
  62. * uses even for numeric options.
  63. */
  64. export function serializeSetting(option: ProcessOption, value: SettingValue): string | string[] {
  65. if (isVectorOption(option)) {
  66. const parts = Array.isArray(value) ? value.map(String) : String(value).split(',');
  67. return parts.map((p) => p.trim()).filter((p) => p !== '');
  68. }
  69. if (option.type === 'coBool') {
  70. if (typeof value === 'boolean') return value ? '1' : '0';
  71. return value === '1' || value === 'true' || value === 1 ? '1' : '0';
  72. }
  73. const raw = String(value).trim();
  74. if (option.type === 'coPercent') {
  75. // The config spells percents with the sign; the input edits the number.
  76. return raw.endsWith('%') ? raw : `${raw}%`;
  77. }
  78. return raw;
  79. }
  80. /** Serialises the panel's sparse override map for the slice request. */
  81. export function serializeOverrides(values: Record<string, SettingValue>, schema: ProcessSchema): Record<string, string | string[]> {
  82. const out: Record<string, string | string[]> = {};
  83. for (const [key, value] of Object.entries(values)) {
  84. const option = schema[key];
  85. // A key with no schema entry cannot be serialised correctly, and sending it
  86. // raw risks a slice failure that is hard to trace back to this panel.
  87. if (!option) continue;
  88. out[key] = serializeSetting(option, value);
  89. }
  90. return out;
  91. }
  92. /**
  93. * True when an edited value differs from the baseline this slice would
  94. * otherwise use. Marks modified rows, and decides what is worth sending: an
  95. * override equal to what the preset already says is noise in the process JSON.
  96. *
  97. * The baseline is the preset's value when known. Comparing against the schema
  98. * default instead would flag every field the preset moved off the C++ default
  99. * as "changed by the user", and would send back values nobody typed.
  100. */
  101. export function isModified(
  102. option: ProcessOption,
  103. value: SettingValue | undefined,
  104. presetValue?: SettingValue,
  105. ): boolean {
  106. if (value === undefined || value === '') return false;
  107. const flatten = (v: SettingValue): string => {
  108. const serialized = serializeSetting(option, Array.isArray(v) ? v.map(String).join(', ') : v);
  109. return Array.isArray(serialized) ? serialized.join(', ') : serialized;
  110. };
  111. const asString = flatten(value);
  112. const baseline = presetValue !== undefined ? presetValue : option.default;
  113. if (baseline === undefined) return asString !== '';
  114. return asString !== flatten(baseline as SettingValue);
  115. }