mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-07 14:41:24 +02:00
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.
156 lines
6.7 KiB
TypeScript
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>
|
|
);
|
|
}
|