mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
Feature: Force Color Match (#625)
* Added "Force Color Match" feature to filament override * Update settings.py * Update settings.py * Label updates for warnings and checkbox case * Initial plan * Add noMatchingMaterial i18n translations to all 7 locale files Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com> * Use {{material}} interpolation in noMatchingMaterial; populate from filament_overrides with getColorName Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com> * Updates for PR#625 comments * Added "Force Color Match" feature to filament override * Update settings.py * Update settings.py * Label updates for warnings and checkbox case * Initial plan * Add noMatchingMaterial i18n translations to all 7 locale files Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com> * Use {{material}} interpolation in noMatchingMaterial; populate from filament_overrides with getColorName Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com> * Updates to PrintersPage and QueuePage * Update QueuePage.tsx * Rebase to 0.2.2b3 * Fix compile errors with FilamentOverride * Updates for PR#625 comments https://github.com/maziggy/bambuddy/pull/625#pullrequestreview-3908738261 * Fix force color match default, loadedFilamentTypes bug, remove dead translations --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com> Co-authored-by: MartinNYHC <mz@v8w.de>
This commit is contained in:
co-authored by
cadtoolbox
copilot-swe-agent[bot]
MartinNYHC
parent
7b74605ae4
commit
d7401a80df
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Circle, RotateCcw } from 'lucide-react';
|
||||
import { Circle, RotateCcw, Palette } from 'lucide-react';
|
||||
import { getColorName } from '../../utils/colors';
|
||||
import type { FilamentReqsData } from './types';
|
||||
|
||||
@@ -9,6 +9,11 @@ interface FilamentOverrideProps {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,6 +26,8 @@ export function FilamentOverride({
|
||||
availableFilaments,
|
||||
overrides,
|
||||
onChange,
|
||||
forceColorMatch,
|
||||
onForceColorMatchChange,
|
||||
}: FilamentOverrideProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -73,58 +80,70 @@ export function FilamentOverride({
|
||||
: sameType;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={req.slot_id}
|
||||
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'
|
||||
}`}
|
||||
<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' }}
|
||||
>
|
||||
<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')}
|
||||
{/* 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'
|
||||
}`}
|
||||
>
|
||||
<RotateCcw className="w-3 h-3" />
|
||||
</button>
|
||||
) : (
|
||||
<span className="w-3" />
|
||||
)}
|
||||
<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="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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import { useFilamentMapping } from '../../hooks/useFilamentMapping';
|
||||
import { useMultiPrinterFilamentMapping, type PerPrinterConfig } from '../../hooks/useMultiPrinterFilamentMapping';
|
||||
import { getColorName } from '../../utils/colors';
|
||||
import { isPlaceholderDate } from '../../utils/amsHelpers';
|
||||
import { getCurrencySymbol } from '../../utils/currency';
|
||||
import { toDateTimeLocalValue, parseUTCDate } from '../../utils/date';
|
||||
@@ -170,6 +171,18 @@ export function PrintModal({
|
||||
return {};
|
||||
});
|
||||
|
||||
// Per-slot force color match flags. Default is false (opt-in).
|
||||
const [forceColorMatch, setForceColorMatch] = useState<Record<number, boolean>>(() => {
|
||||
if (mode === 'edit-queue-item' && queueItem?.filament_overrides) {
|
||||
const flags: Record<number, boolean> = {};
|
||||
for (const o of queueItem.filament_overrides) {
|
||||
flags[o.slot_id] = o.force_color_match === true;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
// Track initial values for clearing mappings on change (edit mode only)
|
||||
const [initialPrinterIds] = useState(() => (mode === 'edit-queue-item' && queueItem?.printer_id ? [queueItem.printer_id] : []));
|
||||
const [initialPlateId] = useState(() => (mode === 'edit-queue-item' && queueItem ? queueItem.plate_id : null));
|
||||
@@ -334,6 +347,7 @@ export function PrintModal({
|
||||
// Don't clear on initial render in edit mode (values are initialized from queueItem)
|
||||
if (mode !== 'edit-queue-item' || prevTargetModel !== null) {
|
||||
setFilamentOverrides({});
|
||||
setForceColorMatch({});
|
||||
}
|
||||
}
|
||||
}, [targetModel, selectedPlate, prevTargetModel, prevPlateForOverrides, mode]);
|
||||
@@ -440,14 +454,38 @@ export function PrintModal({
|
||||
return amsMapping;
|
||||
};
|
||||
|
||||
// Convert filament overrides from Record to array format for API
|
||||
const filamentOverridesArray = Object.keys(filamentOverrides).length > 0
|
||||
? Object.entries(filamentOverrides).map(([slotId, { type, color }]) => ({
|
||||
slot_id: parseInt(slotId, 10),
|
||||
type,
|
||||
color,
|
||||
}))
|
||||
: undefined;
|
||||
// Convert filament overrides from Record to array format for API.
|
||||
// Include all slots that either have a user override or have force_color_match enabled
|
||||
// (which is the default for model-based assignment).
|
||||
const buildFilamentOverridesArray = () => {
|
||||
const entries: Array<{ slot_id: number; type: string; color: string; color_name: string; force_color_match: boolean }> = [];
|
||||
|
||||
// Process all slots from filament requirements (to capture force_color_match defaults)
|
||||
if (effectiveFilamentReqs?.filaments) {
|
||||
for (const req of effectiveFilamentReqs.filaments) {
|
||||
const userOverride = filamentOverrides[req.slot_id];
|
||||
const isForceColor = forceColorMatch[req.slot_id] ?? false;
|
||||
const effectiveType = userOverride?.type ?? req.type;
|
||||
const effectiveColor = userOverride?.color ?? req.color;
|
||||
|
||||
// Include slot if user changed the filament OR force_color_match is enabled
|
||||
if (userOverride || isForceColor) {
|
||||
entries.push({ slot_id: req.slot_id, type: effectiveType, color: effectiveColor, color_name: getColorName(effectiveColor), force_color_match: isForceColor });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: no filament requirements data — only include explicit user overrides
|
||||
for (const [slotId, { type, color }] of Object.entries(filamentOverrides)) {
|
||||
const id = parseInt(slotId, 10);
|
||||
const isForceColor = forceColorMatch[id] ?? false;
|
||||
entries.push({ slot_id: id, type, color, color_name: getColorName(color), force_color_match: isForceColor });
|
||||
}
|
||||
}
|
||||
|
||||
return entries.length > 0 ? entries : undefined;
|
||||
};
|
||||
|
||||
const filamentOverridesArray = buildFilamentOverridesArray();
|
||||
|
||||
// Common queue data for add-to-queue and edit modes
|
||||
const getQueueData = (printerId: number | null, plateOverride?: number | null): PrintQueueItemCreate => ({
|
||||
@@ -759,6 +797,10 @@ export function PrintModal({
|
||||
availableFilaments={availableFilaments}
|
||||
overrides={filamentOverrides}
|
||||
onChange={setFilamentOverrides}
|
||||
forceColorMatch={forceColorMatch}
|
||||
onForceColorMatchChange={(slotId, value) =>
|
||||
setForceColorMatch((prev) => ({ ...prev, [slotId]: value }))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user