Files
bambuddy/frontend/src/components/PrintModal/FilamentOverride.tsx
T
maziggy 5a4a2260b1 Fix PA-CF/PA12-CF/PAHT-CF type matching and force color match click target (#688)
Bambu Lab firmware treats PA-CF, PA12-CF, and PAHT-CF as interchangeable,
  but the print scheduler and filament override UI used exact string matching.
  A 3MF requiring PA-CF wouldn't match PA12-CF in the AMS, leaving the
  override dropdown disabled and preventing scheduler assignment.

  Add a filament type equivalence system (both backend and frontend) so
  these PA variants are treated as compatible in scheduler assignment,
  AMS slot matching, force color match validation, and the override dropdown.

  Also fix the "Force color match" checkbox click target being too wide
  (entire row instead of just the checkbox/label) by changing flex to
  inline-flex on the label element.
2026-03-13 12:56:31 +01:00

156 lines
6.7 KiB
TypeScript

import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Circle, RotateCcw, Palette } from 'lucide-react';
import { getColorName } from '../../utils/colors';
import { canonicalFilamentType } from '../../utils/amsHelpers';
import type { FilamentReqsData } from './types';
interface FilamentOverrideProps {
filamentReqs: FilamentReqsData | undefined;
availableFilaments: Array<{ type: string; color: string; tray_info_idx: string; tray_sub_brands: string; extruder_id: number | null }>;
overrides: Record<number, { type: string; color: string }>;
onChange: (overrides: Record<number, { type: string; color: string }>) => void;
/** Per-slot force color match flags. Defaults to false (opt-in) when not provided. */
forceColorMatch?: Record<number, boolean>;
/** Called when a slot's force color match checkbox is toggled. */
onForceColorMatchChange?: (slotId: number, value: boolean) => void;
}
/**
* Filament override UI for model-based queue assignment.
* Allows users to override the 3MF's original filament choices with
* filaments available across printers of the selected model.
*/
export function FilamentOverride({
filamentReqs,
availableFilaments,
overrides,
onChange,
forceColorMatch,
onForceColorMatchChange,
}: FilamentOverrideProps) {
const { t } = useTranslation();
// Index available filaments by canonical type for per-slot filtering.
// Types in the same equivalence group (e.g. PA-CF / PA12-CF / PAHT-CF) share one bucket.
const filamentsByType = useMemo(() => {
const map: Record<string, Array<{ type: string; color: string; tray_info_idx: string; tray_sub_brands: string; extruder_id: number | null }>> = {};
for (const f of availableFilaments) {
const key = canonicalFilamentType(f.type);
if (!map[key]) map[key] = [];
map[key].push(f);
}
return map;
}, [availableFilaments]);
const filaments = filamentReqs?.filaments;
if (!filaments || filaments.length === 0 || availableFilaments.length === 0) {
return null;
}
const handleChange = (slotId: number, value: string) => {
if (value === '') {
// Reset to original
const next = { ...overrides };
delete next[slotId];
onChange(next);
} else {
// Parse "TYPE|COLOR" value
const [type, color] = value.split('|');
onChange({ ...overrides, [slotId]: { type, color } });
}
};
return (
<div className="mb-4">
<div className="flex items-center gap-2 text-sm text-bambu-gray mb-2">
<span>{t('printModal.filamentOverride')}</span>
</div>
<p className="text-xs text-bambu-gray mb-2">{t('printModal.filamentOverrideHint')}</p>
<div className="bg-bambu-dark rounded-lg p-3 space-y-2">
{filaments.map((req) => {
const override = overrides[req.slot_id];
const isOverridden = !!override;
// Only show filaments of the same type AND compatible nozzle/extruder
const sameType = filamentsByType[canonicalFilamentType(req.type)] || [];
// On dual-nozzle printers (H2D), filter to filaments on the correct extruder.
// nozzle_id from 3MF maps to extruder_id from AMS. If nozzle_id is undefined
// (single-nozzle) or extruder_id is null, no nozzle filtering is needed.
const compatible = req.nozzle_id != null
? sameType.filter((f) => f.extruder_id == null || f.extruder_id === req.nozzle_id)
: sameType;
return (
<div key={req.slot_id} className="space-y-1">
<div
className="grid items-center gap-2 text-xs"
style={{ gridTemplateColumns: '16px minmax(70px, 1fr) auto 2fr 20px' }}
>
{/* Original color swatch */}
<span title={`${t('printModal.originalFilament')}: ${req.type} - ${getColorName(req.color)}`}>
<Circle className="w-3 h-3" fill={req.color} stroke={req.color} />
</span>
{/* Original type + grams */}
<span className="text-white truncate">
{req.type} <span className="text-bambu-gray">({req.used_grams}g)</span>
</span>
{/* Arrow */}
<span className="text-bambu-gray">→</span>
{/* Override dropdown — only compatible (same-type) filaments */}
<select
value={isOverridden ? `${override.type}|${override.color}` : ''}
onChange={(e) => handleChange(req.slot_id, e.target.value)}
disabled={compatible.length === 0}
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 ${
isOverridden
? 'border-blue-400/50 text-blue-400'
: 'border-bambu-gray/30 text-bambu-gray'
}`}
>
<option value="" className="bg-bambu-dark text-bambu-gray">
{t('printModal.originalFilament')}: {req.type} ({getColorName(req.color)})
</option>
{compatible.map((f, idx) => (
<option
key={`${f.type}-${f.color}-${f.tray_sub_brands}-${idx}`}
value={`${f.type}|${f.color}`}
className="bg-bambu-dark text-white"
>
{f.tray_sub_brands || f.type} ({getColorName(f.color)})
</option>
))}
</select>
{/* Reset button */}
{isOverridden ? (
<button
type="button"
onClick={() => handleChange(req.slot_id, '')}
className="text-bambu-gray hover:text-white transition-colors"
title={t('printModal.resetToOriginal')}
>
<RotateCcw className="w-3 h-3" />
</button>
) : (
<span className="w-3" />
)}
</div>
{/* Force Color Match checkbox — shown below each filament row */}
<label className="inline-flex items-center gap-1.5 text-xs text-bambu-gray cursor-pointer select-none pl-5">
<input
type="checkbox"
checked={forceColorMatch?.[req.slot_id] ?? false}
onChange={(e) => onForceColorMatchChange?.(req.slot_id, e.target.checked)}
className="accent-bambu-green w-3 h-3"
/>
<Palette className="w-3 h-3" />
{t('printModal.forceColorMatch')}
</label>
</div>
);
})}
</div>
</div>
);
}