useFilamentLabels.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  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. * Resolve per-slot human-readable labels for the schedule modal's filament
  38. * panels (#1718). Both the model-mode ``FilamentOverride`` and the printer-
  39. * mode ``FilamentMapping`` consume this so the two panels render the same
  40. * sub-brand + disambiguated color for the same sliced 3MF. Extracted from
  41. * the inline implementation in ``FilamentOverride`` so the two callers can't
  42. * drift.
  43. *
  44. * Three queries back the resolution:
  45. * - ``/cloud/builtin-filaments`` → Bambu factory SKU → name map (GFA01 →
  46. * "Bambu PLA Matte" etc.).
  47. * - ``/cloud/filament-id-map`` → user custom cloud preset SKU → name
  48. * map (P-prefix). Wins over the builtin entry for the same id.
  49. * - ``/inventory/colors/by-material`` (one ``useQuery`` per slot via
  50. * ``useQueries``, keyed on hex + material hint) → catalog color name
  51. * disambiguated by material context.
  52. *
  53. * Output is positional — ``labels[i]`` corresponds to ``reqs[i]``. Returns
  54. * an empty array when ``reqs`` is undefined / empty so callers can safely
  55. * index without a length check.
  56. */
  57. export function useFilamentLabels(reqs: readonly FilamentReqLike[] | undefined): FilamentLabel[] {
  58. const { data: builtinFilaments } = useQuery({
  59. queryKey: ['builtin-filaments'],
  60. queryFn: () => api.getBuiltinFilaments(),
  61. staleTime: 5 * 60 * 1000,
  62. });
  63. const { data: cloudFilamentIdMap } = useQuery({
  64. queryKey: ['filament-id-map'],
  65. queryFn: () => api.getFilamentIdMap(),
  66. staleTime: 5 * 60 * 1000,
  67. });
  68. const filamentNameByIdx = useMemo(() => {
  69. const map: Record<string, string> = {};
  70. for (const f of builtinFilaments || []) {
  71. if (f.filament_id) map[f.filament_id] = f.name;
  72. }
  73. // Cloud user-preset map wins when both have the same id — the user-
  74. // authored name is the more specific label.
  75. for (const [fid, name] of Object.entries(cloudFilamentIdMap || {})) {
  76. if (fid && name) map[fid] = name;
  77. }
  78. return map;
  79. }, [builtinFilaments, cloudFilamentIdMap]);
  80. // Compute the per-slot (resolvedName, materialHint) pairs up-front so the
  81. // ``useQueries`` call below has a stable shape and the render path below
  82. // can reuse the same resolvedName without recomputing.
  83. const perSlot = useMemo(() => {
  84. return (reqs || []).map((req) => {
  85. const resolvedName = (req.tray_info_idx && filamentNameByIdx[req.tray_info_idx]) || req.type;
  86. return {
  87. resolvedName,
  88. materialHint: extractMaterialHint(resolvedName),
  89. color: req.color,
  90. };
  91. });
  92. }, [reqs, filamentNameByIdx]);
  93. const colorQueries = useQueries({
  94. queries: perSlot.map(({ color, materialHint }) => ({
  95. queryKey: ['color-by-material', color, materialHint],
  96. queryFn: () => api.getColorByMaterial(color, materialHint),
  97. // Treat empty colour as "nothing to look up" so we don't spam the
  98. // endpoint for entries the 3MF left blank.
  99. enabled: !!color,
  100. staleTime: 5 * 60 * 1000,
  101. })),
  102. });
  103. return perSlot.map(({ resolvedName, color }, idx) => {
  104. const disambiguated = colorQueries[idx]?.data?.color_name ?? null;
  105. return {
  106. resolvedName,
  107. colorLabel: disambiguated || getColorName(color),
  108. };
  109. });
  110. }