FilamentOverride.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { useMemo } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Circle, RotateCcw, Palette } from 'lucide-react';
  4. import { getColorName } from '../../utils/colors';
  5. import { canonicalFilamentType } from '../../utils/amsHelpers';
  6. import type { FilamentReqsData } from './types';
  7. interface FilamentOverrideProps {
  8. filamentReqs: FilamentReqsData | undefined;
  9. availableFilaments: Array<{ type: string; color: string; tray_info_idx: string; tray_sub_brands: string; extruder_id: number | null }>;
  10. overrides: Record<number, { type: string; color: string }>;
  11. onChange: (overrides: Record<number, { type: string; color: string }>) => void;
  12. /** Per-slot force color match flags. Defaults to false (opt-in) when not provided. */
  13. forceColorMatch?: Record<number, boolean>;
  14. /** Called when a slot's force color match checkbox is toggled. */
  15. onForceColorMatchChange?: (slotId: number, value: boolean) => void;
  16. }
  17. /**
  18. * Filament override UI for model-based queue assignment.
  19. * Allows users to override the 3MF's original filament choices with
  20. * filaments available across printers of the selected model.
  21. */
  22. export function FilamentOverride({
  23. filamentReqs,
  24. availableFilaments,
  25. overrides,
  26. onChange,
  27. forceColorMatch,
  28. onForceColorMatchChange,
  29. }: FilamentOverrideProps) {
  30. const { t } = useTranslation();
  31. // Index available filaments by canonical type for per-slot filtering.
  32. // Types in the same equivalence group (e.g. PA-CF / PA12-CF / PAHT-CF) share one bucket.
  33. const filamentsByType = useMemo(() => {
  34. const map: Record<string, Array<{ type: string; color: string; tray_info_idx: string; tray_sub_brands: string; extruder_id: number | null }>> = {};
  35. for (const f of availableFilaments) {
  36. const key = canonicalFilamentType(f.type);
  37. if (!map[key]) map[key] = [];
  38. map[key].push(f);
  39. }
  40. return map;
  41. }, [availableFilaments]);
  42. const filaments = filamentReqs?.filaments;
  43. if (!filaments || filaments.length === 0 || availableFilaments.length === 0) {
  44. return null;
  45. }
  46. const handleChange = (slotId: number, value: string) => {
  47. if (value === '') {
  48. // Reset to original
  49. const next = { ...overrides };
  50. delete next[slotId];
  51. onChange(next);
  52. } else {
  53. // Parse "TYPE|COLOR" value
  54. const [type, color] = value.split('|');
  55. onChange({ ...overrides, [slotId]: { type, color } });
  56. }
  57. };
  58. return (
  59. <div className="mb-4">
  60. <div className="flex items-center gap-2 text-sm text-bambu-gray mb-2">
  61. <span>{t('printModal.filamentOverride')}</span>
  62. </div>
  63. <p className="text-xs text-bambu-gray mb-2">{t('printModal.filamentOverrideHint')}</p>
  64. <div className="bg-bambu-dark rounded-lg p-3 space-y-2">
  65. {filaments.map((req) => {
  66. const override = overrides[req.slot_id];
  67. const isOverridden = !!override;
  68. // Only show filaments of the same type AND compatible nozzle/extruder
  69. const sameType = filamentsByType[canonicalFilamentType(req.type)] || [];
  70. // On dual-nozzle printers (H2D), filter to filaments on the correct extruder.
  71. // nozzle_id from 3MF maps to extruder_id from AMS. If nozzle_id is undefined
  72. // (single-nozzle) or extruder_id is null, no nozzle filtering is needed.
  73. const compatible = req.nozzle_id != null
  74. ? sameType.filter((f) => f.extruder_id == null || f.extruder_id === req.nozzle_id)
  75. : sameType;
  76. return (
  77. <div key={req.slot_id} className="space-y-1">
  78. <div
  79. className="grid items-center gap-2 text-xs"
  80. style={{ gridTemplateColumns: '16px minmax(70px, 1fr) auto 2fr 20px' }}
  81. >
  82. {/* Original color swatch */}
  83. <span title={`${t('printModal.originalFilament')}: ${req.type} - ${getColorName(req.color)}`}>
  84. <Circle className="w-3 h-3" fill={req.color} stroke={req.color} />
  85. </span>
  86. {/* Original type + grams */}
  87. <span className="text-white truncate">
  88. {req.type} <span className="text-bambu-gray">({req.used_grams}g)</span>
  89. </span>
  90. {/* Arrow */}
  91. <span className="text-bambu-gray">→</span>
  92. {/* Override dropdown — only compatible (same-type) filaments */}
  93. <select
  94. value={isOverridden ? `${override.type}|${override.color}` : ''}
  95. onChange={(e) => handleChange(req.slot_id, e.target.value)}
  96. disabled={compatible.length === 0}
  97. className={`flex-1 px-2 py-1 rounded border text-xs bg-bambu-dark-secondary focus:outline-none focus:ring-1 focus:ring-bambu-green ${
  98. isOverridden
  99. ? 'border-blue-400/50 text-blue-400'
  100. : 'border-bambu-gray/30 text-bambu-gray'
  101. }`}
  102. >
  103. <option value="" className="bg-bambu-dark text-bambu-gray">
  104. {t('printModal.originalFilament')}: {req.type} ({getColorName(req.color)})
  105. </option>
  106. {compatible.map((f, idx) => (
  107. <option
  108. key={`${f.type}-${f.color}-${f.tray_sub_brands}-${idx}`}
  109. value={`${f.type}|${f.color}`}
  110. className="bg-bambu-dark text-white"
  111. >
  112. {f.tray_sub_brands || f.type} ({getColorName(f.color)})
  113. </option>
  114. ))}
  115. </select>
  116. {/* Reset button */}
  117. {isOverridden ? (
  118. <button
  119. type="button"
  120. onClick={() => handleChange(req.slot_id, '')}
  121. className="text-bambu-gray hover:text-white transition-colors"
  122. title={t('printModal.resetToOriginal')}
  123. >
  124. <RotateCcw className="w-3 h-3" />
  125. </button>
  126. ) : (
  127. <span className="w-3" />
  128. )}
  129. </div>
  130. {/* Force Color Match checkbox — shown below each filament row */}
  131. <label className="inline-flex items-center gap-1.5 text-xs text-bambu-gray cursor-pointer select-none pl-5">
  132. <input
  133. type="checkbox"
  134. checked={forceColorMatch?.[req.slot_id] ?? false}
  135. onChange={(e) => onForceColorMatchChange?.(req.slot_id, e.target.checked)}
  136. className="accent-bambu-green w-3 h-3"
  137. />
  138. <Palette className="w-3 h-3" />
  139. {t('printModal.forceColorMatch')}
  140. </label>
  141. </div>
  142. );
  143. })}
  144. </div>
  145. </div>
  146. );
  147. }