Improvements AMS card

This commit is contained in:
Martin Ziegler
2025-12-05 14:18:07 +01:00
parent 065f22a0e3
commit fc9f12d3f2
5 changed files with 8767 additions and 8603 deletions
@@ -11,6 +11,7 @@ interface AMSMaterialsModalProps {
printerId: number;
printerModel: string; // e.g., "H2D", "X1C", "P1S"
nozzleDiameter?: string;
extruderId?: number; // 0=right nozzle, 1=left nozzle (for filtering K-profiles)
onClose: () => void;
onConfirm?: (data: MaterialSettings) => void;
}
@@ -170,6 +171,7 @@ export function AMSMaterialsModal({
printerId,
printerModel,
nozzleDiameter = '0.4',
extruderId = 0,
onClose,
onConfirm,
}: AMSMaterialsModalProps) {
@@ -282,7 +284,40 @@ export function AMSMaterialsModal({
};
// Find best K-profile when data loads or filament type changes
const profiles = kProfilesData?.profiles || [];
// First filter all profiles by extruder ID (for dual-nozzle printers)
const allProfiles = kProfilesData?.profiles || [];
const profiles = allProfiles.filter(p => p.extruder_id === extruderId);
// Filter profiles to show only matching ones in dropdown
const getMatchingProfiles = (): KProfile[] => {
if (!profiles.length) return [];
const subBrands = isBambuSpool
? tray.tray_sub_brands?.toLowerCase()
: (selectedPresetName ? getCleanFilamentName(selectedPresetName).toLowerCase() : null);
const type = isBambuSpool
? tray.tray_type?.toUpperCase()
: (selectedFilamentType?.toUpperCase() || tray.tray_type?.toUpperCase());
// If we have tray_sub_brands, only show profiles matching that (not generic type)
// For Bambu spools, also require "bambu" in the profile name to exclude third-party profiles
if (subBrands) {
const matches = profiles.filter(p => p.name.toLowerCase().includes(subBrands));
if (isBambuSpool) {
return matches.filter(p => p.name.toLowerCase().includes('bambu'));
}
return matches;
}
// Only fall back to type matching if no sub_brands available
if (type) {
return profiles.filter(p => p.name.toUpperCase().includes(type));
}
return [];
};
const matchingProfiles = getMatchingProfiles();
// Get color name from Bambu tray_id_name (e.g., "A00-Y2" -> "Sunflower Yellow")
const bambuColorName = getColorNameFromTrayId(tray.tray_id_name);
@@ -511,14 +546,14 @@ export function AMSMaterialsModal({
{/* K Profile */}
<div className="flex items-center gap-4 mb-3">
<label className="w-28 text-sm text-gray-600 dark:text-bambu-gray flex-shrink-0">K Profile</label>
<label className="w-28 text-sm text-gray-600 dark:text-bambu-gray flex-shrink-0">PA Profile</label>
<select
value={selectedKProfile}
onChange={(e) => handleKProfileChange(e.target.value)}
className="flex-1 min-w-0 px-3 py-2 bg-gray-100 dark:bg-bambu-dark rounded-md border border-gray-300 dark:border-bambu-dark-tertiary text-gray-900 dark:text-white text-sm truncate"
>
<option value="Default">Default</option>
{profiles.map(profile => (
{matchingProfiles.map(profile => (
<option key={profile.slot_id} value={profile.name}>
{profile.name} (K={parseFloat(profile.k_value).toFixed(3)})
</option>
@@ -1,7 +1,7 @@
import { useState, useEffect, useRef } from 'react';
import { useMutation } from '@tanstack/react-query';
import { useQuery, useMutation } from '@tanstack/react-query';
import { api } from '../../api/client';
import type { PrinterStatus, AMSUnit, AMSTray } from '../../api/client';
import type { PrinterStatus, AMSUnit, AMSTray, KProfile } from '../../api/client';
import { Loader2, ChevronDown, ChevronUp, RotateCw } from 'lucide-react';
import { AMSHumidityModal } from './AMSHumidityModal';
import { AMSMaterialsModal } from './AMSMaterialsModal';
@@ -43,6 +43,84 @@ function isLightColor(hex: string | null): boolean {
return luminance > 0.45;
}
// Bambu Lab color codes from tray_id_name (e.g., "A00-Y2" -> "Sunflower Yellow")
const BAMBU_COLOR_CODES: Record<string, string> = {
'Y2': 'Sunflower Yellow', 'Y0': 'Yellow', 'Y1': 'Lemon Yellow',
'K0': 'Black', 'W0': 'White', 'W1': 'Ivory White',
'R0': 'Red', 'R1': 'Scarlet Red', 'R2': 'Magenta',
'B0': 'Blue', 'B1': 'Navy Blue', 'B2': 'Sky Blue', 'B3': 'Cyan',
'G0': 'Green', 'G1': 'Grass Green', 'G2': 'Jade Green',
'O0': 'Orange', 'O1': 'Mandarin Orange',
'P0': 'Purple', 'P1': 'Pink', 'P2': 'Sakura Pink',
'N0': 'Gray', 'N1': 'Silver Gray', 'N2': 'Charcoal',
'D0': 'Brown', 'D1': 'Chocolate',
'T0': 'Titan Gray', 'T1': 'Jade White',
};
function getColorNameFromTrayId(trayIdName: string | null): string | null {
if (!trayIdName) return null;
// tray_id_name format: "A00-Y2" or "G02-K0" - color code is after the dash
const parts = trayIdName.split('-');
if (parts.length < 2) return null;
const colorCode = parts[1];
return BAMBU_COLOR_CODES[colorCode] || null;
}
// Find best matching K-profile for a filament using cascading search
// Priority: 1) tray_sub_brands + color, 2) tray_sub_brands only, 3) tray_type only, 4) default
function findBestKProfile(
profiles: KProfile[],
traySubBrands: string | null,
trayType: string | null,
colorName: string | null
): KProfile | null {
if (!profiles.length) return null;
const subBrands = traySubBrands?.toLowerCase() || '';
const type = trayType?.toUpperCase() || '';
const color = colorName?.toLowerCase() || '';
// Priority 1: Match tray_sub_brands AND color (e.g., "PLA Basic" + "Sunflower Yellow")
if (subBrands && color) {
const exactColorMatch = profiles.find(p => {
const name = p.name.toLowerCase();
return name.includes(subBrands) && name.includes(color);
});
if (exactColorMatch) return exactColorMatch;
}
// Priority 2: Match tray_sub_brands without color (e.g., "PLA Basic" in "High Flow_Bambu PLA Basic")
if (subBrands) {
const subBrandsMatch = profiles.find(p =>
p.name.toLowerCase().includes(subBrands)
);
if (subBrandsMatch) return subBrandsMatch;
}
// Priority 3: Match filament type in profile name (e.g., "PLA" in "High Flow_Bambu PLA Basic")
if (type) {
const typeMatches = profiles.filter(p =>
p.name.toUpperCase().includes(type)
);
if (typeMatches.length > 0) {
// Prefer "Basic" profiles for generic type matching
const basicMatch = typeMatches.find(p =>
p.name.toLowerCase().includes('basic')
);
if (basicMatch) return basicMatch;
return typeMatches[0];
}
}
// Priority 4: Default profile
const defaultProfile = profiles.find(p =>
p.name.toLowerCase() === 'default' || p.slot_id === 0
);
return defaultProfile || null;
}
// Single humidity icon that fills based on level
// <25% = empty (dry/good)
// <40% = half filled
@@ -336,6 +414,8 @@ interface AMSPanelContentProps {
onSlotRefresh: (amsId: number, slotId: number) => void;
onEyeClick: (tray: AMSTray, slotLabel: string, amsId: number) => void;
refreshingSlot: { amsId: number; trayId: number } | null;
getKValue: (tray: AMSTray) => number | null;
kProfilesLoaded: boolean;
}
// Panel content - NO wiring, just slots and info
@@ -374,6 +454,8 @@ function AMSPanelContent({
onSlotRefresh,
onEyeClick,
refreshingSlot,
getKValue,
kProfilesLoaded,
}: AMSPanelContentProps) {
const selectedUnit = units[selectedAmsIndex];
const isHT = selectedUnit ? isAmsHT(selectedUnit.id) : false;
@@ -509,11 +591,19 @@ function AMSPanelContent({
{/* Content overlay */}
<div className="relative w-full h-full flex flex-col items-center justify-end pb-[5px]">
<span
className="text-[11px] font-semibold mb-1"
className="text-[11px] font-semibold"
style={{ color: isLight ? '#000000' : '#ffffff' }}
>
{isEmpty ? '--' : tray.tray_type}
</span>
{!isEmpty && kProfilesLoaded && (
<span
className="text-[10px] font-medium"
style={{ color: isLight ? 'rgba(0,0,0,0.8)' : 'rgba(255,255,255,0.85)' }}
>
K {(getKValue(tray) ?? 0.020).toFixed(3)}
</span>
)}
{!isEmpty && (
<button
onClick={(e) => {
@@ -769,6 +859,40 @@ export function AMSSectionDual({ printerId, printerModel, status, nozzleCount }:
// Note: JSON keys are always strings, so we use Record<string, number>
const amsExtruderMap: Record<string, number> = status?.ams_extruder_map ?? {};
// Get nozzle diameter for K-profile lookup (default to 0.4)
const nozzleDiameter = status?.nozzles?.[0]?.nozzle_diameter || '0.4';
// Fetch K-profiles for this printer
const { data: kProfilesData, isFetched: kProfilesFetched } = useQuery({
queryKey: ['kprofiles', printerId, nozzleDiameter],
queryFn: () => api.getKProfiles(printerId, nozzleDiameter),
enabled: isConnected,
staleTime: 30000, // Cache for 30 seconds to avoid flicker on re-renders
});
// Create K-value lookup using cascading search by tray properties
// Priority: 1) tray_sub_brands + color, 2) tray_sub_brands only, 3) tray_type only, 4) default
const getKValueForSlot = (tray: AMSTray): number | null => {
const profiles = kProfilesData?.profiles || [];
if (!profiles.length) return null;
// Get color name from tray_id_name (e.g., "A00-Y2" -> "Sunflower Yellow")
const colorName = getColorNameFromTrayId(tray.tray_id_name || null);
// Use cascading search to find best matching K-profile
const matchedProfile = findBestKProfile(
profiles,
tray.tray_sub_brands || null,
tray.tray_type || null,
colorName
);
if (matchedProfile) {
return parseFloat(matchedProfile.k_value) || null;
}
return null; // No profile found for this filament
};
// Distribute AMS units based on ams_extruder_map
// Each AMS unit's info field tells us which extruder it's connected to:
// UI layout: Left panel shows extruder 0 AMS units, Right panel shows extruder 1 AMS units
@@ -1124,6 +1248,8 @@ export function AMSSectionDual({ printerId, printerModel, status, nozzleCount }:
onSlotRefresh={handleSlotRefresh}
onEyeClick={handleEyeClick}
refreshingSlot={refreshingSlot}
getKValue={getKValueForSlot}
kProfilesLoaded={kProfilesFetched}
/>
{isDualNozzle && (
@@ -1139,6 +1265,8 @@ export function AMSSectionDual({ printerId, printerModel, status, nozzleCount }:
onSlotRefresh={handleSlotRefresh}
onEyeClick={handleEyeClick}
refreshingSlot={refreshingSlot}
getKValue={getKValueForSlot}
kProfilesLoaded={kProfilesFetched}
/>
)}
</div>
@@ -1243,6 +1371,7 @@ export function AMSSectionDual({ printerId, printerModel, status, nozzleCount }:
printerId={printerId}
printerModel={printerModel}
nozzleDiameter={status?.nozzles?.[0]?.nozzle_diameter || '0.4'}
extruderId={amsExtruderMap[materialsModal.amsId.toString()] ?? 0}
onClose={() => setMaterialsModal(null)}
/>
)}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
<script type="module" crossorigin src="/assets/index-DX2Pky1o.js"></script>
<script type="module" crossorigin src="/assets/index-CXerUj0x.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BpSfhfce.css">
</head>
<body>