mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
Two related bugs in K-profile matching, same root cause. #1688 — spool form's PA-profile suggester (PAProfileSection via isMatchingCalibration in spool-form/utils.ts) matched K-profiles by parsing the profile NAME for material/brand/variant. Spools already store slicer_filament (the slicer preset id) and K-profiles already carry filament_id, but both were ignored — so a user's custom K-profile whose name doesn't agree with the slicer preset got silently dropped from suggestions even when the underlying filament_id was identical. #1689 — ConfigureAmsSlotModal's matchingKProfiles ran the same name-only logic on the slot's selected preset. A spool assigned under "Generic PLA" with a custom K-profile actively bound on the printer landed in the modal as "K profile not assigned, default 0.020 will be used", while the printer-card hover-card correctly showed the active profile. Two paths, only one was filtering by name. Shared root: spool preset ids and K-profile filament_ids look different but are equivalent after normalising. Spools store slicer_filament as the cloud setting_id form ("GFSG98_09" — _09 is the variant suffix, the S infix marks setting_id form); K-profiles store filament_id as the bare form ("GFG98"). Plain === doesn't work; both need normalising. This conversion already existed in the other direction at buildFilamentOptions (filament_id → "GFS" + filament_id.slice(2)), so the inverse toFilamentId helper is just the matching reverse, not new ground. Fix — one shared helper, two surfaces: - spool-form/utils.ts: new exports toFilamentId(id) (drops "_NN" variant suffix and strips the "S" in "GFS", so GFSG98_09 → GFG98) and isGenericFilamentId(id) (flags Bambu's generic GFx99 ids which are shared across many filaments and must NOT id-match — the name fallback handles those correctly). - isMatchingCalibration: gains slicer_filament?: string in formData, tries id-match (with generic exclusion) before the existing name parse. PAProfileSection already passes the full formData so no caller edit needed. Strictly additive precedence. - ConfigureAmsSlotModal.selectedPresetInfo: resolves a filamentId field (toFilamentId(cp.setting_id) for cloud presets, toFilamentId(builtinFilamentId) for builtin; empty for local / orca paths which fall through to name match). - ConfigureAmsSlotModal.matchingKProfiles: id-match check at the top of the per-profile predicate (preferred when both sides agree after normalisation), then the existing name-parse logic, then ALWAYS unshifts the slot's currently-active K-profile by slot_id === slotInfo.caliIdx — gated on activeIdx > 0 (so caliIdx 0/null doesn't leak unrelated profiles in), extruder-matched when slotInfo.extruderId is known. This is Spionkiller01's #1689 patch verbatim with the activeIdx > 0 guard added. SpoolBuddy: both kiosk K-profile surfaces reuse the shared components. SpoolBuddyWriteTagPage renders PAProfileSection; SpoolBuddyAmsPage renders ConfigureAmsSlotModal. Verified — fixes propagate automatically, no kiosk-specific edits. What this does NOT change: spools without slicer_filament, K-profiles without filament_id, and generic GFx99 ids all fall through to the existing name-based matching path. Strictly additive precedence; no input shape that matched under the old logic fails to match under the new. The #1053 cloud-preset PFUS* path is preserved because the toFilamentId regex /^GFS/ doesn't match a "PFU" prefix.
This commit is contained in:
@@ -5,6 +5,7 @@ import { X, Loader2, Settings2, ChevronDown, CheckCircle2, RotateCcw } from 'luc
|
||||
import { api } from '../api/client';
|
||||
import type { KProfile } from '../api/client';
|
||||
import { matchesPrinterModelSuffix } from '../utils/slicerPrinterMatch';
|
||||
import { toFilamentId, isGenericFilamentId } from './spool-form/utils';
|
||||
import { Button } from './Button';
|
||||
|
||||
interface SlotInfo {
|
||||
@@ -656,15 +657,21 @@ export function ConfigureAmsSlotModal({
|
||||
// Resolve the name from orca, cloud, local, or builtin presets. The
|
||||
// Orca branch tolerates both ``orca_<UUID>`` and a bare UUID — see the
|
||||
// configure-mutation comment for why the raw UUID also reaches us.
|
||||
// ``filamentId`` is the bare Bambu filament_id (e.g. "GFG98") when the
|
||||
// preset has one — used to id-match K-profiles directly without name
|
||||
// parsing (#1688). Empty string for paths with no usable filament_id
|
||||
// (orca presets, local presets), which makes the id-match branch skip.
|
||||
let presetName: string | null = null;
|
||||
let filamentId = '';
|
||||
if (selectedPresetId.startsWith('local_')) {
|
||||
const localId = parseInt(selectedPresetId.replace('local_', ''), 10);
|
||||
const lp = localPresets?.filament.find(p => p.id === localId);
|
||||
presetName = lp?.name || null;
|
||||
} else if (selectedPresetId.startsWith('builtin_')) {
|
||||
const filamentId = selectedPresetId.replace('builtin_', '');
|
||||
const bf = builtinFilaments?.find(b => b.filament_id === filamentId);
|
||||
const builtinFilamentId = selectedPresetId.replace('builtin_', '');
|
||||
const bf = builtinFilaments?.find(b => b.filament_id === builtinFilamentId);
|
||||
presetName = bf?.name || null;
|
||||
filamentId = toFilamentId(builtinFilamentId);
|
||||
} else {
|
||||
const orcaCandidateId = selectedPresetId.startsWith('orca_')
|
||||
? selectedPresetId.replace('orca_', '')
|
||||
@@ -675,6 +682,11 @@ export function ConfigureAmsSlotModal({
|
||||
} else if (cloudSettings?.filament) {
|
||||
const cp = cloudSettings.filament.find(p => p.setting_id === selectedPresetId);
|
||||
presetName = cp?.name || null;
|
||||
if (cp) {
|
||||
// SlicerSetting only carries setting_id ("GFSG98_09"); toFilamentId
|
||||
// drops the variant suffix and the "S" infix to yield "GFG98".
|
||||
filamentId = toFilamentId(cp.setting_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!presetName) {
|
||||
@@ -693,6 +705,7 @@ export function ConfigureAmsSlotModal({
|
||||
fullName: nameWithoutSuffix,
|
||||
material: parsed.material,
|
||||
brand: parsed.brand,
|
||||
filamentId,
|
||||
};
|
||||
}, [selectedPresetId, cloudSettings?.filament, localPresets?.filament, builtinFilaments, orcaCloudList?.filament]);
|
||||
|
||||
@@ -737,16 +750,28 @@ export function ConfigureAmsSlotModal({
|
||||
const matchingKProfiles = useMemo(() => {
|
||||
if (!kprofilesData?.profiles || !selectedPresetInfo) return [];
|
||||
|
||||
const { fullName, material, brand } = selectedPresetInfo;
|
||||
const { fullName, material, brand, filamentId } = selectedPresetInfo;
|
||||
const upperFullName = fullName.toUpperCase();
|
||||
const upperMaterial = material.toUpperCase();
|
||||
const upperBrand = brand.toUpperCase();
|
||||
const presetFid = filamentId; // already normalised via toFilamentId
|
||||
|
||||
// Material must be at least 2 chars to avoid false positives
|
||||
if (!upperMaterial || upperMaterial.length < 2) return [];
|
||||
|
||||
// Filter profiles - require brand match if brand is present in selected preset
|
||||
const filtered = kprofilesData.profiles.filter(p => {
|
||||
// Preferred: exact filament_id match (#1688). A user's custom K-profile
|
||||
// whose name doesn't agree with the slicer preset still surfaces when
|
||||
// both sides agree on filament_id. Generic GFx99 IDs are excluded —
|
||||
// they're shared across many filaments and over-match if id-compared.
|
||||
if (presetFid) {
|
||||
const calFid = toFilamentId(p.filament_id);
|
||||
if (calFid && calFid === presetFid && !isGenericFilamentId(calFid)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const profileName = p.name.toUpperCase();
|
||||
|
||||
// If the selected preset has a brand (e.g., "Azurefilm PLA Wood"),
|
||||
@@ -803,8 +828,26 @@ export function ConfigureAmsSlotModal({
|
||||
seen.set(key, profile);
|
||||
}
|
||||
}
|
||||
return Array.from(seen.values());
|
||||
}, [kprofilesData?.profiles, selectedPresetInfo, slotInfo.extruderId]);
|
||||
|
||||
const result = Array.from(seen.values());
|
||||
|
||||
// Always include the slot's currently-active K-profile (by cali_idx / slot_id),
|
||||
// even if its name and filament_id didn't match the selected preset (#1689).
|
||||
// A spool assigned under "Generic PLA" can have a K-profile actively bound on
|
||||
// the printer whose filament_id differs from "Generic PLA"; without this
|
||||
// safety net the modal shows "not assigned, default 0.020" while the printer
|
||||
// card's hover-card correctly shows the active profile.
|
||||
const activeIdx = slotInfo.caliIdx;
|
||||
if (activeIdx != null && activeIdx > 0 && !result.some(p => p.slot_id === activeIdx)) {
|
||||
const active = kprofilesData.profiles.find(
|
||||
p => p.slot_id === activeIdx
|
||||
&& (slotInfo.extruderId === undefined || p.extruder_id === slotInfo.extruderId),
|
||||
);
|
||||
if (active) result.unshift(active);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [kprofilesData?.profiles, selectedPresetInfo, slotInfo.extruderId, slotInfo.caliIdx]);
|
||||
|
||||
// Pre-select current profile when modal opens, reset when closes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -291,13 +291,52 @@ export function saveRecentColor(color: ColorPreset, currentRecent: ColorPreset[]
|
||||
return updated;
|
||||
}
|
||||
|
||||
// Normalise a Bambu filament identifier to its bare filament_id form (#1688).
|
||||
// Spools store ``slicer_filament`` as a setting_id like "GFSG98_09" (the "_NN"
|
||||
// suffix is the variant, the "S" infix marks it as a setting_id); printer
|
||||
// K-profiles store ``filament_id`` as "GFG98" (bare). Both shapes need
|
||||
// normalising before comparison.
|
||||
//
|
||||
// This is the inverse of the filament_id→setting_id mapping at
|
||||
// ``buildFilamentOptions`` ("GFS" + filament_id.slice(2)), so a round-trip
|
||||
// stays consistent. Non-Bambu IDs (numeric local-preset IDs, Orca UUIDs)
|
||||
// are returned unchanged uppercase — they won't match any K-profile's
|
||||
// filament_id and the caller falls through to name-based matching.
|
||||
export function toFilamentId(id: string | null | undefined): string {
|
||||
if (!id) return '';
|
||||
// Drop "_NN" variant suffix.
|
||||
let s = id.split('_')[0];
|
||||
// Strip the "S" infix in "GFS..." so "GFSG98" → "GFG98".
|
||||
if (/^GFS/i.test(s)) s = s.slice(0, 2) + s.slice(3);
|
||||
return s.toUpperCase();
|
||||
}
|
||||
|
||||
// "GFx99" identifiers (GFL99, GFG99, GFB99, ...) are Bambu's *generic* filament
|
||||
// IDs — shared across many different physical filaments. Matching K-profiles
|
||||
// by an exact generic ID would over-match, so the id-match path skips them and
|
||||
// the caller falls through to name-based matching.
|
||||
export function isGenericFilamentId(id: string | null | undefined): boolean {
|
||||
return !!id && /^GF[A-Z]99$/i.test(id);
|
||||
}
|
||||
|
||||
// Check if a calibration matches based on brand, material, and variant
|
||||
export function isMatchingCalibration(
|
||||
cal: { name?: string; filament_id?: string },
|
||||
formData: { material: string; brand: string; subtype: string },
|
||||
formData: { material: string; brand: string; subtype: string; slicer_filament?: string },
|
||||
): boolean {
|
||||
if (!formData.material) return false;
|
||||
|
||||
// Preferred path: exact filament_id match after normalising both sides
|
||||
// (#1688). When the spool has a non-generic preset assigned and it agrees
|
||||
// with the K-profile's filament_id, this is unambiguous — no name parsing
|
||||
// needed. A spool storing "GFSG98_09" matches a K-profile with filament_id
|
||||
// "GFG98" without going anywhere near parsePresetName.
|
||||
const spoolFid = toFilamentId(formData.slicer_filament);
|
||||
const calFid = toFilamentId(cal.filament_id);
|
||||
if (spoolFid && calFid && spoolFid === calFid && !isGenericFilamentId(calFid)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const profileName = cal.name || '';
|
||||
|
||||
// Remove flow type prefixes
|
||||
|
||||
Reference in New Issue
Block a user