mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
fix(queue): extend Charcoal-style label fix to Specific-Printer panel (#1718 round 3)
Round 2 fixed the model-mode FilamentOverride: tray_info_idx →
sub-brand, plus a material-disambiguated colour name from a new
/inventory/colors/by-material endpoint. The printer-mode panel that
renders the same 3MF (FilamentMapping) was reading the same raw
fields — item.type for the required label, getColorName(item.color)
for the swatch tooltip — and was not touched, so picking "Specific
Printer" still showed "Required: PLA - Black" for a slice the
"Any H2D" branch already labelled "Bambu PLA Matte - Charcoal".
Extract the three-query resolution machinery from FilamentOverride
into a shared hook useFilamentLabels (returns positional
{resolvedName, colorLabel} per slot). Both panels call it; both
read the same labels. The hook also owns extractMaterialHint so the
"strip leading brand token" rule has one source of truth.
FilamentMapping required-side now reads {resolvedName} instead of
{item.type}; swatch tooltip reads `Required: {resolvedName} -
{colorLabel}` instead of `Required: {item.type} -
getColorName(item.color)`.
This commit is contained in:
@@ -6,6 +6,7 @@ import { api } from '../../api/client';
|
||||
import { useFilamentMapping } from '../../hooks/useFilamentMapping';
|
||||
import { getGlobalTrayId } from '../../utils/amsHelpers';
|
||||
import { getColorName } from '../../utils/colors';
|
||||
import { useFilamentLabels } from './useFilamentLabels';
|
||||
import type { FilamentMappingProps } from './types';
|
||||
|
||||
/**
|
||||
@@ -44,6 +45,13 @@ export function FilamentMapping({
|
||||
const { loadedFilaments, filamentComparison, hasTypeMismatch, hasColorMismatch } =
|
||||
useFilamentMapping(filamentReqs, printerStatus, manualMappings);
|
||||
|
||||
// Per-slot sub-brand + material-disambiguated colour labels (#1718). Same
|
||||
// shared hook the model-mode FilamentOverride uses so both panels render
|
||||
// the same sliced-3MF identity. Falls back to the raw type / generic
|
||||
// colour bucket when the SKU is unknown or the by-material lookup hasn't
|
||||
// resolved — never blanks out the required row.
|
||||
const filamentLabels = useFilamentLabels(filamentReqs?.filaments);
|
||||
|
||||
const trayCostMap = useMemo(() => {
|
||||
const map = new Map<number, number | null>();
|
||||
for (const assignment of assignments || []) {
|
||||
@@ -192,6 +200,11 @@ export function FilamentMapping({
|
||||
// scheduler honors the flag in both modes; only the UI was missing.
|
||||
const slotId = item.slot_id ?? 0;
|
||||
const canForceMatch = slotId > 0 && onForceColorMatchChange != null;
|
||||
// #1718: same sub-brand + colour resolution as FilamentOverride.
|
||||
// Indexing is safe because ``useFilamentLabels`` mirrors the input
|
||||
// array shape; defensive fallback covers the empty-reqs render
|
||||
// path that shouldn't reach here anyway.
|
||||
const { resolvedName, colorLabel } = filamentLabels[idx] ?? { resolvedName: item.type, colorLabel: getColorName(item.color) };
|
||||
return (
|
||||
<div key={idx} className="space-y-1">
|
||||
<div
|
||||
@@ -199,7 +212,7 @@ export function FilamentMapping({
|
||||
style={{ gridTemplateColumns: '16px minmax(70px, 1fr) auto 2fr 16px' }}
|
||||
>
|
||||
{/* Required color */}
|
||||
<span title={`Required: ${item.type} - ${getColorName(item.color)}`}>
|
||||
<span title={`Required: ${resolvedName} - ${colorLabel}`}>
|
||||
<Circle className="w-3 h-3" fill={item.color} stroke={item.color} />
|
||||
</span>
|
||||
{/* Required type + grams + nozzle badge */}
|
||||
@@ -212,7 +225,7 @@ export function FilamentMapping({
|
||||
{item.nozzle_id === 1 ? t('printModal.leftNozzle') : t('printModal.rightNozzle')}
|
||||
</span>
|
||||
)}
|
||||
{item.type} <span className="text-bambu-gray">({item.used_grams}g)</span>
|
||||
{resolvedName} <span className="text-bambu-gray">({item.used_grams}g)</span>
|
||||
</span>
|
||||
{/* Arrow */}
|
||||
<span className="text-bambu-gray">→</span>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Circle, RotateCcw, Palette } from 'lucide-react';
|
||||
import { getColorName } from '../../utils/colors';
|
||||
import { canonicalFilamentType } from '../../utils/amsHelpers';
|
||||
import { useFilamentLabels } from './useFilamentLabels';
|
||||
import type { FilamentReqsData } from './types';
|
||||
|
||||
interface FilamentOverrideProps {
|
||||
@@ -32,6 +33,11 @@ export function FilamentOverride({
|
||||
}: FilamentOverrideProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Per-slot sub-brand + material-disambiguated colour labels (#1718). The
|
||||
// shared hook fronts the three queries that power the resolution so this
|
||||
// component and ``FilamentMapping`` cannot drift apart on label content.
|
||||
const labels = useFilamentLabels(filamentReqs?.filaments);
|
||||
|
||||
// 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(() => {
|
||||
@@ -69,7 +75,7 @@ export function FilamentOverride({
|
||||
</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) => {
|
||||
{filaments.map((req, slotIdx) => {
|
||||
const override = overrides[req.slot_id];
|
||||
const isOverridden = !!override;
|
||||
// Only show filaments of the same type AND compatible nozzle/extruder
|
||||
@@ -81,6 +87,14 @@ export function FilamentOverride({
|
||||
? sameType.filter((f) => f.extruder_id == null || f.extruder_id === req.nozzle_id)
|
||||
: sameType;
|
||||
|
||||
// #1718: sub-brand resolved from the 3MF's tray_info_idx via the
|
||||
// builtin / cloud-id maps, plus the material-disambiguated catalogue
|
||||
// colour for the hex. Both fall back gracefully (resolvedName →
|
||||
// req.type when the SKU is unknown; colorLabel → getColorName(hex)
|
||||
// when the by-material lookup hasn't resolved yet, returned null,
|
||||
// or errored) so a slow query never blanks out the row.
|
||||
const { resolvedName, colorLabel } = labels[slotIdx] ?? { resolvedName: req.type, colorLabel: getColorName(req.color) };
|
||||
|
||||
return (
|
||||
<div key={req.slot_id} className="space-y-1">
|
||||
<div
|
||||
@@ -88,12 +102,12 @@ export function FilamentOverride({
|
||||
style={{ gridTemplateColumns: '16px minmax(70px, 1fr) auto 2fr 20px' }}
|
||||
>
|
||||
{/* Original color swatch */}
|
||||
<span title={`${t('printModal.originalFilament')}: ${req.type} - ${getColorName(req.color)}`}>
|
||||
<span title={`${t('printModal.originalFilament')}: ${resolvedName} - ${colorLabel}`}>
|
||||
<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>
|
||||
{resolvedName} <span className="text-bambu-gray">({req.used_grams}g)</span>
|
||||
</span>
|
||||
{/* Arrow */}
|
||||
<span className="text-bambu-gray">→</span>
|
||||
@@ -109,7 +123,7 @@ export function FilamentOverride({
|
||||
}`}
|
||||
>
|
||||
<option value="" className="bg-bambu-dark text-bambu-gray">
|
||||
{t('printModal.originalFilament')}: {req.type} ({getColorName(req.color)})
|
||||
{t('printModal.originalFilament')}: {resolvedName} ({colorLabel})
|
||||
</option>
|
||||
{compatible.map((f, idx) => (
|
||||
<option
|
||||
|
||||
@@ -183,6 +183,10 @@ export interface FilamentReqsData {
|
||||
used_grams: number;
|
||||
used_meters: number;
|
||||
nozzle_id?: number;
|
||||
/** Bambu SKU code from the 3MF (e.g. `GFA01` = Bambu PLA Matte, `P4d64437`
|
||||
* = user custom). Used to resolve the "original" filament label in
|
||||
* FilamentOverride against the builtin + cloud user-preset maps. #1718. */
|
||||
tray_info_idx?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQueries, useQuery } from '@tanstack/react-query';
|
||||
import { api } from '../../api/client';
|
||||
import { getColorName } from '../../utils/colors';
|
||||
|
||||
/** Strip a leading brand token (the first whitespace-separated word) from a
|
||||
* resolved filament name so what remains can be matched against the color
|
||||
* catalog's ``material`` column. Examples:
|
||||
* "Bambu PLA Matte" → "PLA Matte"
|
||||
* "PolyLite ABS" → "ABS"
|
||||
* "Bambu PLA-CF" → "PLA-CF"
|
||||
* "PLA" → "PLA" (no brand to strip; pass through)
|
||||
* "Devil Design PLA" → "Design PLA" (won't match catalog → falls back
|
||||
* to priority-order answer, no regression)
|
||||
* Never returns ``""`` — the empty-material case is the same priority
|
||||
* fallback as omitting the param.
|
||||
*/
|
||||
export function extractMaterialHint(name: string): string {
|
||||
const parts = name.trim().split(/\s+/);
|
||||
if (parts.length <= 1) return name.trim();
|
||||
return parts.slice(1).join(' ');
|
||||
}
|
||||
|
||||
export interface FilamentLabel {
|
||||
/** Bambu sub-brand from the SKU lookup ("Bambu PLA Matte") falling back to
|
||||
* the raw 3MF ``type`` ("PLA") when the SKU is unknown to both maps. */
|
||||
resolvedName: string;
|
||||
/** Material-disambiguated catalogue color ("Charcoal") falling back to
|
||||
* ``getColorName(hex)`` when the by-material lookup hasn't resolved yet,
|
||||
* returned null, or errored. Always non-empty. */
|
||||
colorLabel: string;
|
||||
}
|
||||
|
||||
interface FilamentReqLike {
|
||||
type: string;
|
||||
color: string;
|
||||
tray_info_idx?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve per-slot human-readable labels for the schedule modal's filament
|
||||
* panels (#1718). Both the model-mode ``FilamentOverride`` and the printer-
|
||||
* mode ``FilamentMapping`` consume this so the two panels render the same
|
||||
* sub-brand + disambiguated color for the same sliced 3MF. Extracted from
|
||||
* the inline implementation in ``FilamentOverride`` so the two callers can't
|
||||
* drift.
|
||||
*
|
||||
* Three queries back the resolution:
|
||||
* - ``/cloud/builtin-filaments`` → Bambu factory SKU → name map (GFA01 →
|
||||
* "Bambu PLA Matte" etc.).
|
||||
* - ``/cloud/filament-id-map`` → user custom cloud preset SKU → name
|
||||
* map (P-prefix). Wins over the builtin entry for the same id.
|
||||
* - ``/inventory/colors/by-material`` (one ``useQuery`` per slot via
|
||||
* ``useQueries``, keyed on hex + material hint) → catalog color name
|
||||
* disambiguated by material context.
|
||||
*
|
||||
* Output is positional — ``labels[i]`` corresponds to ``reqs[i]``. Returns
|
||||
* an empty array when ``reqs`` is undefined / empty so callers can safely
|
||||
* index without a length check.
|
||||
*/
|
||||
export function useFilamentLabels(reqs: readonly FilamentReqLike[] | undefined): FilamentLabel[] {
|
||||
const { data: builtinFilaments } = useQuery({
|
||||
queryKey: ['builtin-filaments'],
|
||||
queryFn: () => api.getBuiltinFilaments(),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
const { data: cloudFilamentIdMap } = useQuery({
|
||||
queryKey: ['filament-id-map'],
|
||||
queryFn: () => api.getFilamentIdMap(),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const filamentNameByIdx = useMemo(() => {
|
||||
const map: Record<string, string> = {};
|
||||
for (const f of builtinFilaments || []) {
|
||||
if (f.filament_id) map[f.filament_id] = f.name;
|
||||
}
|
||||
// Cloud user-preset map wins when both have the same id — the user-
|
||||
// authored name is the more specific label.
|
||||
for (const [fid, name] of Object.entries(cloudFilamentIdMap || {})) {
|
||||
if (fid && name) map[fid] = name;
|
||||
}
|
||||
return map;
|
||||
}, [builtinFilaments, cloudFilamentIdMap]);
|
||||
|
||||
// Compute the per-slot (resolvedName, materialHint) pairs up-front so the
|
||||
// ``useQueries`` call below has a stable shape and the render path below
|
||||
// can reuse the same resolvedName without recomputing.
|
||||
const perSlot = useMemo(() => {
|
||||
return (reqs || []).map((req) => {
|
||||
const resolvedName = (req.tray_info_idx && filamentNameByIdx[req.tray_info_idx]) || req.type;
|
||||
return {
|
||||
resolvedName,
|
||||
materialHint: extractMaterialHint(resolvedName),
|
||||
color: req.color,
|
||||
};
|
||||
});
|
||||
}, [reqs, filamentNameByIdx]);
|
||||
|
||||
const colorQueries = useQueries({
|
||||
queries: perSlot.map(({ color, materialHint }) => ({
|
||||
queryKey: ['color-by-material', color, materialHint],
|
||||
queryFn: () => api.getColorByMaterial(color, materialHint),
|
||||
// Treat empty colour as "nothing to look up" so we don't spam the
|
||||
// endpoint for entries the 3MF left blank.
|
||||
enabled: !!color,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})),
|
||||
});
|
||||
|
||||
return perSlot.map(({ resolvedName, color }, idx) => {
|
||||
const disambiguated = colorQueries[idx]?.data?.color_name ?? null;
|
||||
return {
|
||||
resolvedName,
|
||||
colorLabel: disambiguated || getColorName(color),
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user