mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-20 00:38:38 +02:00
feat(slicer): filter process & filament profiles by selected printer (issue #1325)
The Slice dialog listed every process / filament preset regardless of the chosen printer (#1325). Picking a printer profile now filters both dropdowns to compatible presets; presets resolving to a different Bambu model move into a trailing "Other printers" group. Matching uses the slicer's own compatible_printers list for imported (local) presets, and falls back to the "@BBL <model>" name suffix for cloud / standard presets where no compatibility metadata is available, so all three tiers are covered. Compatibility-unknown presets (custom or untagged) are never hidden. The pre-pick and printer-switch paths follow the same rule. - UnifiedPreset gains compatible_printers, exposed for the local tier - new frontend util slicerPrinterMatch.ts with the matching logic - slice.otherPrinters added across all 9 locales
This commit is contained in:
@@ -20,6 +20,7 @@ import { useToast } from '../contexts/ToastContext';
|
||||
import { PlatePickerModal } from './PlatePickerModal';
|
||||
import type { PlateFilament } from '../types/plates';
|
||||
import { normalizeColorForCompare, colorsAreSimilar } from '../utils/amsHelpers';
|
||||
import { presetCompatibility, printerPresetCode } from '../utils/slicerPrinterMatch';
|
||||
|
||||
export type SliceSource =
|
||||
| { kind: 'libraryFile'; id: number; filename: string }
|
||||
@@ -50,6 +51,39 @@ function pickDefault(by: UnifiedPresetsResponse, slot: Slot): PresetRef | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Resolve a PresetRef back to its UnifiedPreset within the named slot, or
|
||||
// null if it no longer resolves (e.g. the preset was deleted between the
|
||||
// listing fetch and selection).
|
||||
function findPreset(
|
||||
by: UnifiedPresetsResponse,
|
||||
ref: PresetRef | null,
|
||||
slot: Slot,
|
||||
): UnifiedPreset | null {
|
||||
if (!ref) return null;
|
||||
return by[ref.source][slot].find((p) => p.id === ref.id) ?? null;
|
||||
}
|
||||
|
||||
// Process default (#1325): first preset compatible with the selected printer
|
||||
// in tier order, then the first whose compatibility is merely unknown, then
|
||||
// plain priority. Keeps the pre-pick honest with the printer filter instead
|
||||
// of blindly taking list[0].
|
||||
function pickProcessDefault(
|
||||
by: UnifiedPresetsResponse,
|
||||
printerName: string | null,
|
||||
printerCode: string | null,
|
||||
): PresetRef | null {
|
||||
for (const wanted of ['match', 'unknown'] as const) {
|
||||
for (const tier of SLICE_MODAL_TIER_ORDER) {
|
||||
for (const p of by[tier].process) {
|
||||
if (presetCompatibility(p, printerName, printerCode) === wanted) {
|
||||
return { source: p.source, id: p.id };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return pickDefault(by, 'process');
|
||||
}
|
||||
|
||||
const TIER_BONUS: Record<PresetSource, number> = {
|
||||
local: 1.5,
|
||||
cloud: 1.0,
|
||||
@@ -59,6 +93,8 @@ const TIER_BONUS: Record<PresetSource, number> = {
|
||||
function pickFilamentForSlot(
|
||||
by: UnifiedPresetsResponse,
|
||||
required: { type: string; color: string },
|
||||
printerName: string | null,
|
||||
printerCode: string | null,
|
||||
): PresetRef | null {
|
||||
// Score every filament preset against the plate slot's required (type,
|
||||
// colour) and pick the highest. Mirrors the AMS slot-mapping match in the
|
||||
@@ -80,6 +116,12 @@ function pickFilamentForSlot(
|
||||
else if (colorsAreSimilar(p.filament_colour ?? '', required.color)) score += 2;
|
||||
}
|
||||
score += TIER_BONUS[tier];
|
||||
// Demote printer-incompatible filaments (#1325): a penalty rather than a
|
||||
// hard skip so the pick still degrades gracefully if every filament
|
||||
// mismatches the selected printer.
|
||||
if (presetCompatibility(p, printerName, printerCode) === 'mismatch') {
|
||||
score -= 100;
|
||||
}
|
||||
if (best == null || score > best.score) {
|
||||
best = { ref: { source: p.source, id: p.id }, score };
|
||||
}
|
||||
@@ -357,32 +399,68 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
}, [selectedBundleId, bundlesQuery.data]);
|
||||
const isBundleMode = selectedBundle != null;
|
||||
|
||||
// Printer / process pre-pick: see SLICE_MODAL_TIER_ORDER. Runs once when
|
||||
// presets first arrive; subsequent re-renders preserve any manual choice.
|
||||
// Selected-printer context for the process / filament filter (#1325).
|
||||
const selectedPrinterName = useMemo<string | null>(() => {
|
||||
if (!presetsQuery.data || !printerPreset) return null;
|
||||
return findPreset(presetsQuery.data, printerPreset, 'printer')?.name ?? null;
|
||||
}, [presetsQuery.data, printerPreset]);
|
||||
const selectedPrinterCode = useMemo<string | null>(
|
||||
() => (selectedPrinterName ? printerPresetCode(selectedPrinterName) : null),
|
||||
[selectedPrinterName],
|
||||
);
|
||||
|
||||
// Printer pre-pick: see SLICE_MODAL_TIER_ORDER. Runs once when presets
|
||||
// first arrive; subsequent re-renders preserve any manual choice.
|
||||
useEffect(() => {
|
||||
if (!presetsQuery.data) return;
|
||||
if (printerPreset == null) setPrinterPreset(pickDefault(presetsQuery.data, 'printer'));
|
||||
if (processPreset == null) setProcessPreset(pickDefault(presetsQuery.data, 'process'));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [presetsQuery.data]);
|
||||
|
||||
// Filament pre-pick: re-runs whenever the active filament-slot count
|
||||
// changes (plate selection, single-plate metadata arriving). For each slot
|
||||
// we score every available filament preset against the slot's required
|
||||
// (type, colour) and keep the highest match. Slot count mismatch → reset
|
||||
// and re-pick everything; same length → preserve any user override.
|
||||
// Process pre-pick / re-pick (#1325): defaults to a process compatible with
|
||||
// the selected printer, and re-defaults when a printer change leaves the
|
||||
// current process incompatible. A compatible or unknown manual pick is kept.
|
||||
useEffect(() => {
|
||||
if (!presetsQuery.data) return;
|
||||
const data = presetsQuery.data;
|
||||
setFilamentPresets((current) => {
|
||||
if (current.length === filamentSlots.length && current.every((r) => r != null)) {
|
||||
return current;
|
||||
if (!data) return;
|
||||
setProcessPreset((current) => {
|
||||
if (current) {
|
||||
const p = findPreset(data, current, 'process');
|
||||
if (p && presetCompatibility(p, selectedPrinterName, selectedPrinterCode) !== 'mismatch') {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
return filamentSlots.map((slot) =>
|
||||
pickFilamentForSlot(data, { type: slot.type, color: slot.color }),
|
||||
);
|
||||
return pickProcessDefault(data, selectedPrinterName, selectedPrinterCode);
|
||||
});
|
||||
}, [presetsQuery.data, filamentSlots]);
|
||||
}, [presetsQuery.data, selectedPrinterName, selectedPrinterCode]);
|
||||
|
||||
// Filament pre-pick: re-runs when the active filament-slot count changes
|
||||
// (plate selection, single-plate metadata arriving) or the selected printer
|
||||
// changes. Each slot scores every available filament preset against the
|
||||
// slot's required (type, colour); an existing pick (incl. a user override)
|
||||
// is kept as long as it's still compatible with the selected printer, while
|
||||
// null slots and printer-incompatible picks are re-picked (#1325).
|
||||
useEffect(() => {
|
||||
const data = presetsQuery.data;
|
||||
if (!data) return;
|
||||
setFilamentPresets((current) => {
|
||||
return filamentSlots.map((slot, i) => {
|
||||
const cur = current[i] ?? null;
|
||||
if (cur) {
|
||||
const p = findPreset(data, cur, 'filament');
|
||||
if (p && presetCompatibility(p, selectedPrinterName, selectedPrinterCode) !== 'mismatch') {
|
||||
return cur;
|
||||
}
|
||||
}
|
||||
return pickFilamentForSlot(
|
||||
data,
|
||||
{ type: slot.type, color: slot.color },
|
||||
selectedPrinterName,
|
||||
selectedPrinterCode,
|
||||
);
|
||||
});
|
||||
});
|
||||
}, [presetsQuery.data, filamentSlots, selectedPrinterName, selectedPrinterCode]);
|
||||
|
||||
// Bundle-mode auto-pick: when the user picks a bundle (or the slot count
|
||||
// changes after the picker is open), default the process to the bundle's
|
||||
@@ -603,6 +681,8 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
value={processPreset}
|
||||
onChange={setProcessPreset}
|
||||
disabled={isEnqueuing}
|
||||
selectedPrinterName={selectedPrinterName}
|
||||
selectedPrinterCode={selectedPrinterCode}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -719,6 +799,8 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
}
|
||||
disabled={isEnqueuing || !isUsed}
|
||||
swatchColor={filamentSlots.length > 1 ? slot.color : undefined}
|
||||
selectedPrinterName={selectedPrinterName}
|
||||
selectedPrinterCode={selectedPrinterCode}
|
||||
/>
|
||||
);
|
||||
})
|
||||
@@ -864,29 +946,66 @@ interface PresetDropdownProps {
|
||||
// filament slots so the user can see at a glance which slot they're
|
||||
// configuring against the source 3MF's per-slot colour.
|
||||
swatchColor?: string;
|
||||
// Selected printer context (#1325). When provided for a process / filament
|
||||
// slot, presets that resolve to a different printer move into a trailing
|
||||
// "Other printers" group instead of the main tier list.
|
||||
selectedPrinterName?: string | null;
|
||||
selectedPrinterCode?: string | null;
|
||||
}
|
||||
|
||||
function PresetDropdown({ label, slot, data, value, onChange, disabled, swatchColor }: PresetDropdownProps) {
|
||||
function PresetDropdown({
|
||||
label,
|
||||
slot,
|
||||
data,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
swatchColor,
|
||||
selectedPrinterName,
|
||||
selectedPrinterCode,
|
||||
}: PresetDropdownProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sections: { tierLabel: string; entries: UnifiedPreset[] }[] = useMemo(() => {
|
||||
// Order matches SLICE_MODAL_TIER_ORDER: imported first, then cloud, then
|
||||
// standard fallback. Sections with no entries collapse out so a user
|
||||
// without cloud / local presets only sees the tiers they actually have.
|
||||
const tiers: { key: keyof UnifiedPresetsResponse; tier: 'cloud' | 'local' | 'standard'; label: string; fallback: string }[] = [
|
||||
{ key: 'local', tier: 'local', label: 'slice.tier.local', fallback: 'Imported' },
|
||||
{ key: 'cloud', tier: 'cloud', label: 'slice.tier.cloud', fallback: 'Cloud' },
|
||||
{ key: 'standard', tier: 'standard', label: 'slice.tier.standard', fallback: 'Standard' },
|
||||
// Tier sections (imported → cloud → standard), plus — for a process /
|
||||
// filament slot with a selected printer — a trailing group of presets that
|
||||
// resolve to a different printer (#1325). Compatibility-unknown presets
|
||||
// stay in their tier, so a custom / untagged preset is never hidden, and
|
||||
// empty sections collapse out.
|
||||
const { sections, otherEntries } = useMemo(() => {
|
||||
const tiers: { key: keyof UnifiedPresetsResponse; label: string; fallback: string }[] = [
|
||||
{ key: 'local', label: 'slice.tier.local', fallback: 'Imported' },
|
||||
{ key: 'cloud', label: 'slice.tier.cloud', fallback: 'Cloud' },
|
||||
{ key: 'standard', label: 'slice.tier.standard', fallback: 'Standard' },
|
||||
];
|
||||
return tiers
|
||||
.map(({ key, label: lk, fallback }) => ({
|
||||
tierLabel: t(lk, fallback),
|
||||
entries: (data[key] as UnifiedPresetsBySlot)[slot],
|
||||
}))
|
||||
.filter((s) => s.entries.length > 0);
|
||||
}, [data, slot, t]);
|
||||
const filterByPrinter = slot !== 'printer';
|
||||
const compatSections: { tierLabel: string; entries: UnifiedPreset[] }[] = [];
|
||||
const other: UnifiedPreset[] = [];
|
||||
for (const { key, label: lk, fallback } of tiers) {
|
||||
const entries = (data[key] as UnifiedPresetsBySlot)[slot];
|
||||
if (!filterByPrinter) {
|
||||
if (entries.length > 0) compatSections.push({ tierLabel: t(lk, fallback), entries });
|
||||
continue;
|
||||
}
|
||||
const compatible: UnifiedPreset[] = [];
|
||||
for (const p of entries) {
|
||||
if (
|
||||
presetCompatibility(p, selectedPrinterName ?? null, selectedPrinterCode ?? null) ===
|
||||
'mismatch'
|
||||
) {
|
||||
other.push(p);
|
||||
} else {
|
||||
compatible.push(p);
|
||||
}
|
||||
}
|
||||
if (compatible.length > 0) {
|
||||
compatSections.push({ tierLabel: t(lk, fallback), entries: compatible });
|
||||
}
|
||||
}
|
||||
return { sections: compatSections, otherEntries: other };
|
||||
}, [data, slot, t, selectedPrinterName, selectedPrinterCode]);
|
||||
|
||||
const totalEntries = sections.reduce((sum, s) => sum + s.entries.length, 0);
|
||||
const totalEntries =
|
||||
sections.reduce((sum, s) => sum + s.entries.length, 0) + otherEntries.length;
|
||||
|
||||
return (
|
||||
<label className="block">
|
||||
@@ -920,6 +1039,15 @@ function PresetDropdown({ label, slot, data, value, onChange, disabled, swatchCo
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
{otherEntries.length > 0 && (
|
||||
<optgroup label={t('slice.otherPrinters')}>
|
||||
{otherEntries.map((p) => (
|
||||
<option key={`${p.source}:${p.id}`} value={`${p.source}:${p.id}`}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user