slicerPrinterMatch.ts 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. // Printer-compatibility matching for the SliceModal's process / filament
  2. // dropdowns (#1325).
  3. //
  4. // Compatibility is resolved in this order, stopping on the first non-unknown
  5. // answer:
  6. //
  7. // 1. Imported (local-tier) presets carry the slicer's own
  8. // `compatible_printers` list — an exact list of printer-preset names.
  9. // 2. BambuStudio's own `@BBL <model>` naming convention on shipped cloud
  10. // / standard presets. The token → printer-fragment table is derived
  11. // from the backend's canonical PRINTER_MODEL_MAP (fetched via
  12. // /slicer/printer-models), not duplicated here.
  13. //
  14. // The result drives grouping, not hard hiding: a preset no rule covers
  15. // stays in the main list, and only a preset that resolves to a *different*
  16. // printer is pushed into an "Other printers" group.
  17. export type PrinterCompatibility = 'match' | 'mismatch' | 'unknown';
  18. // Lookup tables consumed by `presetCompatibility`. `bambuModelByShortCode`
  19. // is the @BBL token → printer-preset fragment map derived from the backend's
  20. // PRINTER_MODEL_MAP — e.g. `X1C` → `X1 Carbon`. An empty map means the @BBL
  21. // fallback still works when token and printer-name fragment match directly
  22. // (raw-token comparison), and gracefully degrades otherwise.
  23. export interface PrinterCompatibilityIndex {
  24. bambuModelByShortCode: Record<string, string>;
  25. }
  26. /** An empty index — used when the model map hasn't loaded yet. */
  27. export const EMPTY_COMPATIBILITY_INDEX: PrinterCompatibilityIndex = {
  28. bambuModelByShortCode: {},
  29. };
  30. // Bambu cloud started shipping terse model codes in `@BBL <code>` suffixes
  31. // mid-2026 — the most visible one is "A1 Mini" → "A1M" (#1649, reported by
  32. // @technopaw). User-authored profiles still use the long display name, so
  33. // both shapes have to match the same printer. The table is uppercase-normalised
  34. // for case-insensitive lookups; add a row when a future rename is spotted via
  35. // `/api/v1/cloud/settings`. Keep narrow on purpose — wide-net aliasing
  36. // (e.g. "X1" ⇄ "X1C") would silently group truly distinct printers.
  37. const PRINTER_MODEL_SUFFIX_ALIASES: Record<string, readonly string[]> = {
  38. 'A1 MINI': ['A1M'],
  39. };
  40. /**
  41. * True when ``presetSuffix`` (the token extracted from a "@BBL <code>" or
  42. * preset-name suffix) refers to the same printer as ``printerModel``
  43. * (the display name selected in the picker). Case-insensitive; consults
  44. * the alias table for short codes Bambu introduced after the long forms
  45. * shipped (#1649).
  46. */
  47. export function matchesPrinterModelSuffix(presetSuffix: string, printerModel: string): boolean {
  48. const p = presetSuffix.toUpperCase();
  49. const m = printerModel.toUpperCase();
  50. if (p === m) return true;
  51. const aliasesOfM = PRINTER_MODEL_SUFFIX_ALIASES[m];
  52. if (aliasesOfM && aliasesOfM.includes(p)) return true;
  53. const aliasesOfP = PRINTER_MODEL_SUFFIX_ALIASES[p];
  54. if (aliasesOfP && aliasesOfP.includes(m)) return true;
  55. return false;
  56. }
  57. /**
  58. * Invert the backend's PRINTER_MODEL_MAP into the shape the @BBL fallback
  59. * needs: short code → printer-preset fragment (the part of "Bambu Lab X1
  60. * Carbon" the user sees in a printer preset name, minus the "Bambu Lab "
  61. * brand prefix).
  62. *
  63. * Backend ships e.g. `{"Bambu Lab X1 Carbon": "X1C", "Bambu Lab A1 mini":
  64. * "A1 Mini", "Bambu Lab A1 Mini": "A1 Mini"}` — multiple long forms can map
  65. * to the same short. We pick the first long-form encountered for each short
  66. * code; case normalisation happens at match time so "A1 mini" vs "A1 Mini"
  67. * never matters.
  68. */
  69. function buildShortCodeMap(
  70. printerModels: Record<string, string>,
  71. ): Record<string, string> {
  72. const out: Record<string, string> = {};
  73. for (const [longName, shortCode] of Object.entries(printerModels)) {
  74. if (shortCode in out) continue;
  75. out[shortCode] = longName.replace(/^Bambu Lab\s+/, '');
  76. }
  77. return out;
  78. }
  79. /**
  80. * Build the compatibility index from the backend printer-model registry.
  81. */
  82. export function buildCompatibilityIndex(
  83. printerModels: Record<string, string> = {},
  84. ): PrinterCompatibilityIndex {
  85. return {
  86. bambuModelByShortCode: buildShortCodeMap(printerModels),
  87. };
  88. }
  89. function normalizeModelFragment(s: string): string {
  90. return s.replace(/\s+/g, '').toLowerCase();
  91. }
  92. // Bambu Studio's naming convention for bundled presets: the 0.4 nozzle is
  93. // the default and its variants drop the nozzle suffix; 0.2 / 0.6 / 0.8
  94. // carry an explicit "<size> nozzle" segment. So a process with no suffix
  95. // is implicitly a 0.4 process — required to compare correctly against a
  96. // 0.4 printer preset, which DOES carry the suffix.
  97. const DEFAULT_NOZZLE = '0.4';
  98. // Strip a trailing "<size> nozzle" segment, returning the nozzle string
  99. // (e.g. "0.6") or null when absent. Used by both BBL-token and printer-
  100. // preset extractors so the suffix is parsed identically on both sides.
  101. function takeNozzleSuffix(s: string): { stripped: string; nozzle: string | null } {
  102. const m = s.match(/^(.*?)\s+([\d.]+)\s*nozzle\s*$/i);
  103. if (!m) return { stripped: s.trim(), nozzle: null };
  104. return { stripped: m[1].trim(), nozzle: m[2] };
  105. }
  106. // Pull the model token and nozzle out of a "@BBL <token> [<size> nozzle]"
  107. // suffix. The token may contain a space (e.g. "A1 mini"), so we strip a
  108. // trailing nozzle segment rather than splitting on the first whitespace.
  109. function extractBblToken(presetName: string): { token: string; nozzle: string | null } | null {
  110. const marker = '@BBL ';
  111. const idx = presetName.indexOf(marker);
  112. if (idx < 0) return null;
  113. const rest = presetName.slice(idx + marker.length).trim();
  114. const { stripped, nozzle } = takeNozzleSuffix(rest);
  115. return stripped ? { token: stripped, nozzle } : null;
  116. }
  117. // Pull the model fragment and nozzle out of a "Bambu Lab <model> [<size>
  118. // nozzle]" printer preset name. Returns null for non-Bambu printer
  119. // presets — there is no reliable name-based match against those.
  120. function extractPrinterPresetModel(printerPresetName: string): { model: string; nozzle: string | null } | null {
  121. const m = printerPresetName.match(/^Bambu Lab\s+(.+)$/i);
  122. if (!m) return null;
  123. const { stripped, nozzle } = takeNozzleSuffix(m[1]);
  124. return stripped ? { model: stripped, nozzle } : null;
  125. }
  126. /**
  127. * Name-based fallback for presets BambuStudio ships with a `@BBL <model>`
  128. * tag (#1325 follow-up). Used only after `compatible_printers` has returned
  129. * `'unknown'`.
  130. *
  131. * Compares BOTH model AND nozzle. The nozzle filter is required because
  132. * Bambu ships per-nozzle process / filament variants (0.2 / 0.4 / 0.6 /
  133. * 0.8) — a 0.6-nozzle process is unusable on a 0.4-nozzle printer.
  134. * 0.4 is Bambu's default and its variants drop the nozzle suffix, so a
  135. * preset with no suffix counts as 0.4.
  136. */
  137. function classifyByBambuName(
  138. presetName: string,
  139. selectedPrinterName: string,
  140. bambuModelByShortCode: Record<string, string>,
  141. ): PrinterCompatibility {
  142. const parsed = extractBblToken(presetName);
  143. if (!parsed) return 'unknown';
  144. // If the token isn't in the table (a brand-new Bambu model whose short
  145. // code the backend registry hasn't added yet, or the model map hasn't
  146. // loaded yet), fall back to comparing the raw token. That keeps the
  147. // matcher working when token and printer-name fragment happen to be
  148. // identical — e.g. "Q1" preset against "Bambu Lab Q1 0.4 nozzle" —
  149. // without us having to ship a code update. When they differ in form
  150. // (X1C vs "X1 Carbon"), the registry is what makes the match work.
  151. const inferredModel = bambuModelByShortCode[parsed.token] ?? parsed.token;
  152. const selectedParts = extractPrinterPresetModel(selectedPrinterName);
  153. if (!selectedParts) return 'unknown';
  154. // The raw inferred model and the printer-preset fragment may differ only by
  155. // the Bambu short-code rename (e.g. preset token "A1M" vs printer "A1 Mini").
  156. // ``matchesPrinterModelSuffix`` consults the alias table before declaring a
  157. // mismatch — see #1649.
  158. if (
  159. normalizeModelFragment(selectedParts.model) !== normalizeModelFragment(inferredModel)
  160. && !matchesPrinterModelSuffix(parsed.token, selectedParts.model)
  161. ) {
  162. return 'mismatch';
  163. }
  164. // Nozzle compare — only when we have a usable size from the printer
  165. // side. A Bambu printer preset always carries one, so this branch is
  166. // taken in practice; the null path is defensive degrade for hand-typed
  167. // or non-Bambu printer names that happened to match the model.
  168. if (selectedParts.nozzle !== null) {
  169. const presetNozzle = parsed.nozzle ?? DEFAULT_NOZZLE;
  170. if (presetNozzle !== selectedParts.nozzle) return 'mismatch';
  171. }
  172. return 'match';
  173. }
  174. /**
  175. * Classify a process / filament preset against the selected printer.
  176. *
  177. * - 'match' — the preset is compatible with the selected printer.
  178. * - 'mismatch' — the preset resolves to a *different* printer.
  179. * - 'unknown' — compatibility can't be determined (no `compatible_printers`,
  180. * no recognizable `@BBL` tag, or no printer is selected);
  181. * the caller must not hide it.
  182. */
  183. export function presetCompatibility(
  184. preset: { name: string; compatible_printers?: string[] | null },
  185. _slot: 'process' | 'filament',
  186. selectedPrinterName: string | null,
  187. index: PrinterCompatibilityIndex,
  188. ): PrinterCompatibility {
  189. if (!selectedPrinterName) return 'unknown';
  190. // (1) Imported presets carry the slicer's own compatible_printers list —
  191. // authoritative when set.
  192. const compat = preset.compatible_printers;
  193. if (compat && compat.length > 0) {
  194. return compat.includes(selectedPrinterName) ? 'match' : 'mismatch';
  195. }
  196. // (2) BambuStudio's `@BBL <model>` name convention — covers cloud /
  197. // standard presets that don't carry compatible_printers.
  198. return classifyByBambuName(preset.name, selectedPrinterName, index.bambuModelByShortCode);
  199. }