useFilamentLabels.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import { useMemo } from 'react';
  2. import { useQueries, useQuery } from '@tanstack/react-query';
  3. import { api } from '../../api/client';
  4. import { getColorName } from '../../utils/colors';
  5. /** Strip a leading brand token (the first whitespace-separated word) from a
  6. * resolved filament name so what remains can be matched against the color
  7. * catalog's ``material`` column. Examples:
  8. * "Bambu PLA Matte" → "PLA Matte"
  9. * "PolyLite ABS" → "ABS"
  10. * "Bambu PLA-CF" → "PLA-CF"
  11. * "PLA" → "PLA" (no brand to strip; pass through)
  12. * "Devil Design PLA" → "Design PLA" (won't match catalog → falls back
  13. * to priority-order answer, no regression)
  14. * Never returns ``""`` — the empty-material case is the same priority
  15. * fallback as omitting the param.
  16. */
  17. export function extractMaterialHint(name: string): string {
  18. const parts = name.trim().split(/\s+/);
  19. if (parts.length <= 1) return name.trim();
  20. return parts.slice(1).join(' ');
  21. }
  22. export interface FilamentLabel {
  23. /** Bambu sub-brand from the SKU lookup ("Bambu PLA Matte") falling back to
  24. * the raw 3MF ``type`` ("PLA") when the SKU is unknown to both maps. */
  25. resolvedName: string;
  26. /** Material-disambiguated catalogue color ("Charcoal") falling back to
  27. * ``getColorName(hex)`` when the by-material lookup hasn't resolved yet,
  28. * returned null, or errored. Always non-empty. */
  29. colorLabel: string;
  30. }
  31. interface FilamentReqLike {
  32. type: string;
  33. color: string;
  34. tray_info_idx?: string;
  35. }
  36. /**
  37. * Slicer preset id (``tray_info_idx``: GFA01, P-prefixed user presets) to its
  38. * name ("Bambu PLA Matte"). The cloud user-preset map wins over the builtin
  39. * entry for the same id. Shared by the print dialog and the queue page so both
  40. * name a colour within the same product line.
  41. */
  42. export function useFilamentNameByIdx(): Record<string, string> {
  43. const { data: builtinFilaments } = useQuery({
  44. queryKey: ['builtin-filaments'],
  45. queryFn: () => api.getBuiltinFilaments(),
  46. staleTime: 5 * 60 * 1000,
  47. });
  48. const { data: cloudFilamentIdMap } = useQuery({
  49. queryKey: ['filament-id-map'],
  50. queryFn: () => api.getFilamentIdMap(),
  51. staleTime: 5 * 60 * 1000,
  52. });
  53. return useMemo(() => {
  54. const map: Record<string, string> = {};
  55. for (const f of builtinFilaments || []) {
  56. if (f.filament_id) map[f.filament_id] = f.name;
  57. }
  58. // Cloud user-preset map wins when both have the same id — the user-
  59. // authored name is the more specific label.
  60. for (const [fid, name] of Object.entries(cloudFilamentIdMap || {})) {
  61. if (fid && name) map[fid] = name;
  62. }
  63. return map;
  64. }, [builtinFilaments, cloudFilamentIdMap]);
  65. }
  66. /** The colour catalogue's material for a filament: the product line its
  67. * preset names ("PLA Matte"), else its bare type ("PLA"). */
  68. export function productLineOf(
  69. trayInfoIdx: string | null | undefined,
  70. type: string,
  71. filamentNameByIdx: Record<string, string>,
  72. ): string {
  73. return extractMaterialHint((trayInfoIdx && filamentNameByIdx[trayInfoIdx]) || type);
  74. }
  75. /**
  76. * Resolve per-slot human-readable labels for the schedule modal's filament
  77. * panels (#1718). Both the model-mode ``FilamentOverride`` and the printer-
  78. * mode ``FilamentMapping`` consume this so the two panels render the same
  79. * sub-brand + disambiguated color for the same sliced 3MF. Extracted from
  80. * the inline implementation in ``FilamentOverride`` so the two callers can't
  81. * drift.
  82. *
  83. * Three queries back the resolution:
  84. * - ``/cloud/builtin-filaments`` → Bambu factory SKU → name map (GFA01 →
  85. * "Bambu PLA Matte" etc.).
  86. * - ``/cloud/filament-id-map`` → user custom cloud preset SKU → name
  87. * map (P-prefix). Wins over the builtin entry for the same id.
  88. * - ``/inventory/colors/by-material`` (one ``useQuery`` per slot via
  89. * ``useQueries``, keyed on hex + material hint) → catalog color name
  90. * disambiguated by material context.
  91. *
  92. * Output is positional — ``labels[i]`` corresponds to ``reqs[i]``. Returns
  93. * an empty array when ``reqs`` is undefined / empty so callers can safely
  94. * index without a length check.
  95. */
  96. export function useFilamentLabels(reqs: readonly FilamentReqLike[] | undefined): FilamentLabel[] {
  97. const filamentNameByIdx = useFilamentNameByIdx();
  98. // Compute the per-slot (resolvedName, materialHint) pairs up-front so the
  99. // ``useQueries`` call below has a stable shape and the render path below
  100. // can reuse the same resolvedName without recomputing.
  101. const perSlot = useMemo(() => {
  102. return (reqs || []).map((req) => {
  103. const resolvedName = (req.tray_info_idx && filamentNameByIdx[req.tray_info_idx]) || req.type;
  104. return {
  105. resolvedName,
  106. materialHint: extractMaterialHint(resolvedName),
  107. color: req.color,
  108. };
  109. });
  110. }, [reqs, filamentNameByIdx]);
  111. const colorQueries = useQueries({
  112. queries: perSlot.map(({ color, materialHint }) => ({
  113. queryKey: ['color-by-material', color, materialHint],
  114. queryFn: () => api.getColorByMaterial(color, materialHint),
  115. // Treat empty colour as "nothing to look up" so we don't spam the
  116. // endpoint for entries the 3MF left blank.
  117. enabled: !!color,
  118. staleTime: 5 * 60 * 1000,
  119. })),
  120. });
  121. return perSlot.map(({ resolvedName, color }, idx) => {
  122. const disambiguated = colorQueries[idx]?.data?.color_name ?? null;
  123. return {
  124. resolvedName,
  125. colorLabel: disambiguated || getColorName(color),
  126. };
  127. });
  128. }