FilamentSlotCircle.tsx 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. /**
  2. * FilamentSlotCircle renders a small color circle with the 1-based slot
  3. * number centered inside, matching the style used on AMS cards in PrintersPage.
  4. *
  5. * Props:
  6. * trayColor - 6-char hex color string WITHOUT leading '#' (e.g. "FF0000").
  7. * Pass undefined / empty string when the slot is empty.
  8. * trayType - Filament material string (e.g. "PLA"). Used to decide the
  9. * fallback background when there is no color but a type is known.
  10. * isEmpty - Whether the slot contains no filament.
  11. * emptyKind - Optional refinement of the empty state used to render the
  12. * slot border (#1322 follow-up): "physical" for firmware-
  13. * confirmed no spool (state 9/10), "reset" for slots where
  14. * the user cleared the assignment but the firmware hasn't
  15. * positively confirmed emptiness. Ignored when isEmpty is false.
  16. * pendingColor - Colour of a spool assigned in Bambuddy to a slot the
  17. * printer reports loaded but blank (#3434). Drawn with an amber
  18. * dashed border: assigned, not configured on the printer yet.
  19. * slotNumber - 1-based slot number to display inside the circle. Accepts
  20. * a string for non-numeric labels (e.g. "L" / "R" for the
  21. * dual-nozzle external trays, where carrying a separate
  22. * Ext-L/Ext-R caption underneath made the row taller).
  23. */
  24. interface FilamentSlotCircleProps {
  25. trayColor?: string | null;
  26. trayType?: string | null;
  27. isEmpty: boolean;
  28. emptyKind?: 'physical' | 'reset' | null;
  29. pendingColor?: string | null;
  30. slotNumber: number | string;
  31. }
  32. function isLightFilamentColor(hex: string): boolean {
  33. if (!hex || hex.length < 6) return false;
  34. const r = parseInt(hex.slice(0, 2), 16);
  35. const g = parseInt(hex.slice(2, 4), 16);
  36. const b = parseInt(hex.slice(4, 6), 16);
  37. return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.6;
  38. }
  39. export function FilamentSlotCircle({ trayColor, trayType, isEmpty, emptyKind, pendingColor, slotNumber }: FilamentSlotCircleProps) {
  40. // Reset slots get a quieter border than physical-empty so they read as
  41. // "cleared but possibly still has a spool the firmware hasn't confirmed
  42. // gone" rather than "definitely no spool".
  43. const emptyBorderColor = emptyKind === 'reset' ? '#3d3d3d' : '#666';
  44. const pending = isEmpty && pendingColor ? pendingColor.slice(0, 6) : null;
  45. const fill = pending ?? trayColor;
  46. return (
  47. <div
  48. className="w-3.5 h-3.5 rounded-full mx-auto mb-0.5 border-2 flex items-center justify-center"
  49. data-pending-config={pending ? 'true' : undefined}
  50. style={{
  51. backgroundColor: fill ? `#${fill}` : (trayType ? '#333' : 'transparent'),
  52. borderColor: pending ? '#f59e0b' : isEmpty ? emptyBorderColor : 'rgba(255,255,255,0.1)',
  53. borderStyle: isEmpty ? 'dashed' : 'solid',
  54. }}
  55. >
  56. <span
  57. className="text-[6px] font-bold leading-none select-none"
  58. style={{ color: fill && isLightFilamentColor(fill) ? '#000' : '#fff' }}
  59. >
  60. {slotNumber}
  61. </span>
  62. </div>
  63. );
  64. }