mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-05 13:41:36 +02:00
Improvements AMS card
This commit is contained in:
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user