);
}
// Unified wiring layer - draws ALL wiring in one place
interface WiringLayerProps {
isDualNozzle: boolean;
leftSlotCount: number; // Number of slots on left panel (4 for regular AMS, 1-2 for AMS-HT)
rightSlotCount: number; // Number of slots on right panel
leftIsHT: boolean; // Is left panel an AMS-HT
rightIsHT: boolean; // Is right panel an AMS-HT
leftActiveSlot?: number | null; // Currently active slot index on left panel (0-3)
rightActiveSlot?: number | null; // Currently active slot index on right panel (0-3)
leftFilamentColor?: string | null; // Filament color for left active path
rightFilamentColor?: string | null; // Filament color for right active path
}
function WiringLayer({
isDualNozzle,
leftSlotCount,
rightSlotCount,
leftIsHT,
rightIsHT,
leftActiveSlot,
rightActiveSlot,
leftFilamentColor,
rightFilamentColor,
}: WiringLayerProps) {
if (!isDualNozzle) return null;
// All measurements relative to this container
// Container spans full width between panels
// Regular AMS: slots → hub → down → toward center → down to extruder
// AMS-HT: single slot on left → direct line down to extruder
// Regular AMS: Slots are w-14 (56px) with gap-2 (8px), 4 slots = 248px total, centered in each ~300px panel
// Left panel center ~150, slots start at 150 - 124 = 26
// Slot centers: 26+28=54, 54+64=118, 118+64=182, 182+64=246
// AMS-HT: Left aligned with pl-2 (8px), slot starts at 8px + 28px = 36px center
// For 2 slots: 36, 100 (36 + 64)
// Right panel calculations for regular AMS:
// Right panel center ~450, slots start at 450 - 124 = 326
// Slot centers: 326+28=354, 354+64=418, 418+64=482, 482+64=546
// Right panel AMS-HT: Left aligned, starts at ~308 (300 panel offset + 8px padding)
// Slot center: 308 + 28 = 336
// Determine colors for wiring paths
const defaultColor = '#909090';
const leftActiveColor = leftFilamentColor ? hexToRgb(leftFilamentColor) : null;
const rightActiveColor = rightFilamentColor ? hexToRgb(rightFilamentColor) : null;
// Slot X positions for regular AMS (4 slots)
const leftSlotX = [54, 118, 182, 246];
// Right slot positions
const rightSlotX = [354, 418, 482, 546];
return (
{/* Extruder image container - positioned at bottom center */}
{/* Image is 56x71 pixels, scaled to h=50px = width ~39px */}
{/* Scale factor: 50/71 = 0.704 */}
{/* Green circles in original image: left center ~(15.2,34.2), right center ~(41.0,33.9) */}
{/* Scaled positions: left x≈10.7, right x≈28.9, y≈24 from top */}
{/* Extruder inlet indicator circles - overlay on extruder image */}
{/* Left inlet (extruder 1) - left side of extruder */}
{/* Right inlet (extruder 0) - right side of extruder */}
);
}
export function AMSSectionDual({ printerId, printerModel, status, nozzleCount }: AMSSectionDualProps) {
const isConnected = status?.connected ?? false;
const isPrinting = status?.state === 'RUNNING';
const isDualNozzle = nozzleCount > 1;
const amsUnits: AMSUnit[] = status?.ams ?? [];
// Per-AMS extruder map: {ams_id: extruder_id} where extruder 0=right, 1=left
// This is extracted from each AMS unit's info field bit 8 in the backend
// Note: JSON keys are always strings, so we use Record
const amsExtruderMap: Record = status?.ams_extruder_map ?? {};
// Distribute AMS units based on ams_extruder_map
// Each AMS unit's info field tells us which extruder it's connected to:
// extruder 0 = right nozzle, extruder 1 = left nozzle
const leftUnits = (() => {
if (!isDualNozzle) return amsUnits;
if (Object.keys(amsExtruderMap).length > 0) {
// Filter AMS units assigned to extruder 1 (left nozzle)
// JSON keys are strings, so convert unit.id to string
return amsUnits.filter(unit => amsExtruderMap[String(unit.id)] === 1);
}
// Fallback: odd indices go to left (extruder 1)
return amsUnits.filter((_, i) => i % 2 === 1);
})();
const rightUnits = (() => {
if (!isDualNozzle) return [];
if (Object.keys(amsExtruderMap).length > 0) {
// Filter AMS units assigned to extruder 0 (right nozzle)
// JSON keys are strings, so convert unit.id to string
return amsUnits.filter(unit => amsExtruderMap[String(unit.id)] === 0);
}
// Fallback: even indices go to right (extruder 0)
return amsUnits.filter((_, i) => i % 2 === 0);
})();
const [leftAmsIndex, setLeftAmsIndex] = useState(0);
const [rightAmsIndex, setRightAmsIndex] = useState(0);
const [selectedTray, setSelectedTray] = useState(null);
// Track if load has been triggered (to disable Load button until unload or slot change)
const [loadTriggered, setLoadTriggered] = useState(false);
// Modal states
const [humidityModal, setHumidityModal] = useState<{ humidity: number; temp: number } | null>(null);
const [materialsModal, setMaterialsModal] = useState<{ tray: AMSTray; slotLabel: string; amsId: number } | null>(null);
// Track user-initiated filament change operations (for showing progress card immediately)
const [userFilamentChange, setUserFilamentChange] = useState<{ isLoading: boolean } | null>(null);
// Track the previous stage for detecting when operation completes
const prevStageRef = useRef(-1);
// Track if we've done initial sync from tray_now
const initialSyncDone = useRef(false);
// Sync selectedTray and loadTriggered from status.tray_now on initial load
// tray_now: 255 = no filament loaded, 0-253 = valid tray ID, 254 = external spool
useEffect(() => {
if (initialSyncDone.current) return;
const trayNow = status?.tray_now;
if (trayNow !== undefined && trayNow !== null) {
initialSyncDone.current = true;
if (trayNow !== 255 && trayNow !== 254) {
// Valid AMS tray is loaded - select it and set loadTriggered
console.log(`[AMSSectionDual] Initializing from tray_now: ${trayNow}`);
setSelectedTray(trayNow);
setLoadTriggered(true);
} else {
// No filament loaded or external spool
console.log(`[AMSSectionDual] tray_now=${trayNow} (no AMS filament loaded)`);
}
}
}, [status?.tray_now]);
const loadMutation = useMutation({
mutationFn: ({ trayId, extruderId }: { trayId: number; extruderId?: number }) =>
api.amsLoadFilament(printerId, trayId, extruderId),
onSuccess: (data, { trayId, extruderId }) => {
console.log(`[AMSSectionDual] Load filament success (tray ${trayId}, extruder ${extruderId}):`, data);
// Disable Load button after successful load
setLoadTriggered(true);
},
onError: (error, { trayId, extruderId }) => {
console.error(`[AMSSectionDual] Load filament error (tray ${trayId}, extruder ${extruderId}):`, error);
},
});
const unloadMutation = useMutation({
mutationFn: () => api.amsUnloadFilament(printerId),
onSuccess: (data) => {
console.log(`[AMSSectionDual] Unload filament success:`, data);
// Re-enable Load button after unload
setLoadTriggered(false);
},
onError: (error) => {
console.error(`[AMSSectionDual] Unload filament error:`, error);
},
});
// Handle tray selection - also re-enables Load button when changing slot
const handleTraySelect = (trayId: number | null) => {
if (trayId !== selectedTray) {
// Slot changed - re-enable Load button
setLoadTriggered(false);
}
setSelectedTray(trayId);
};
// Helper to get extruder ID for a given tray
const getExtruderIdForTray = (trayId: number): number | undefined => {
// For dual-nozzle printers, calculate which AMS unit the tray belongs to
// and look up which extruder it's connected to
if (!isDualNozzle) return undefined;
// Find which AMS unit contains this tray
// Global tray ID format: amsId * 4 + slotIndex (for regular AMS)
// For AMS-HT (id >= 128): amsId * 4 + slotIndex (but only 2 slots)
for (const unit of amsUnits) {
const slotsInUnit = unit.id >= 128 ? 2 : 4; // AMS-HT has 2 slots
const baseSlotId = unit.id * 4;
if (trayId >= baseSlotId && trayId < baseSlotId + slotsInUnit) {
// Found the AMS unit - look up its extruder
const extruderId = amsExtruderMap[String(unit.id)];
console.log(`[AMSSectionDual] Tray ${trayId} belongs to AMS ${unit.id}, extruder: ${extruderId}`);
return extruderId;
}
}
return undefined;
};
const handleLoad = () => {
console.log(`[AMSSectionDual] handleLoad called, selectedTray: ${selectedTray}`);
if (selectedTray !== null) {
const extruderId = getExtruderIdForTray(selectedTray);
console.log(`[AMSSectionDual] Calling loadMutation.mutate(tray: ${selectedTray}, extruder: ${extruderId})`);
// Show filament change card immediately
setUserFilamentChange({ isLoading: true });
loadMutation.mutate({ trayId: selectedTray, extruderId });
}
};
const handleUnload = () => {
// Show filament change card immediately
setUserFilamentChange({ isLoading: false });
unloadMutation.mutate();
};
const isLoading = loadMutation.isPending || unloadMutation.isPending;
// Handlers for modals and actions
const handleHumidityClick = (humidity: number, temp: number) => {
setHumidityModal({ humidity, temp });
};
const refreshMutation = useMutation({
mutationFn: ({ amsId, trayId }: { amsId: number; trayId: number }) =>
api.refreshAmsTray(printerId, amsId, trayId),
onSuccess: (data, variables) => {
console.log(`[AMSSectionDual] Tray refresh success (AMS ${variables.amsId}, Tray ${variables.trayId}):`, data);
},
onError: (error, variables) => {
console.error(`[AMSSectionDual] Tray refresh error (AMS ${variables.amsId}, Tray ${variables.trayId}):`, error);
},
});
const handleSlotRefresh = (amsId: number, slotId: number) => {
// Trigger RFID re-read for the specific tray
console.log(`[AMSSectionDual] Slot refresh triggered: AMS ${amsId}, Slot ${slotId}, printerId: ${printerId}`);
refreshMutation.mutate({ amsId, trayId: slotId });
};
const handleEyeClick = (tray: AMSTray, slotLabel: string, amsId: number) => {
setMaterialsModal({ tray, slotLabel, amsId });
};
// Determine if we're in a filament change stage (from MQTT)
const currentStage = status?.stg_cur ?? -1;
const isMqttFilamentChangeActive = [
STAGE_HEATING_NOZZLE,
STAGE_FILAMENT_UNLOADING,
STAGE_FILAMENT_LOADING,
STAGE_CHANGING_FILAMENT,
].includes(currentStage);
// Auto-close card when operation completes
// Track when we transition from an active filament change stage back to -1
useEffect(() => {
const wasInFilamentChange = [
STAGE_HEATING_NOZZLE,
STAGE_FILAMENT_UNLOADING,
STAGE_FILAMENT_LOADING,
STAGE_CHANGING_FILAMENT,
].includes(prevStageRef.current);
if (isMqttFilamentChangeActive) {
// MQTT is now reporting a stage, clear user-triggered state
// Card will continue showing because isMqttFilamentChangeActive is true
setUserFilamentChange(null);
} else if (wasInFilamentChange && currentStage === -1) {
// Transition from active stage to idle - operation completed
// Close the card by clearing user state
setUserFilamentChange(null);
}
// Update previous stage for next comparison
prevStageRef.current = currentStage;
}, [isMqttFilamentChangeActive, currentStage]);
// Show FilamentChangeCard when either MQTT reports active stage OR user just clicked load/unload
const showFilamentChangeCard = isMqttFilamentChangeActive || userFilamentChange !== null;
// Determine if loading or unloading for the card display
const isFilamentLoading = userFilamentChange !== null
? userFilamentChange.isLoading
: (currentStage === STAGE_FILAMENT_LOADING || currentStage === STAGE_HEATING_NOZZLE);
// Get the loaded tray info for wire coloring
// Wire coloring should show the path from the currently loaded filament to the extruder
// But ONLY if the currently displayed AMS panel is the one with the loaded filament
const trayNow = status?.tray_now ?? 255;
const getLoadedTrayInfo = (): {
leftActiveSlot: number | null;
rightActiveSlot: number | null;
leftFilamentColor: string | null;
rightFilamentColor: string | null;
} => {
// tray_now: 255 = no filament, 254 = external spool, 0-253 = valid tray ID
if (trayNow === 255 || trayNow === 254) {
return { leftActiveSlot: null, rightActiveSlot: null, leftFilamentColor: null, rightFilamentColor: null };
}
// Find which AMS and slot contains the loaded tray
for (const unit of amsUnits) {
const slotsInUnit = unit.id >= 128 ? 2 : 4;
const baseSlotId = unit.id * 4;
if (trayNow >= baseSlotId && trayNow < baseSlotId + slotsInUnit) {
const slotIndex = trayNow - baseSlotId;
const tray = unit.tray[slotIndex];
const color = tray?.tray_color ?? null;
// Determine if this AMS is on left or right side
const extruderId = amsExtruderMap[String(unit.id)];
// Check if this AMS unit is the one currently displayed in the panel
const currentLeftUnit = leftUnits[leftAmsIndex];
const currentRightUnit = rightUnits[rightAmsIndex];
if (extruderId === 1) {
// Left side (extruder 1)
// Only show colored wiring if the currently displayed AMS unit is the one with loaded filament
const isDisplayed = currentLeftUnit?.id === unit.id;
return {
leftActiveSlot: isDisplayed ? slotIndex : null,
rightActiveSlot: null,
leftFilamentColor: isDisplayed ? color : null, // Hide color if different AMS is selected
rightFilamentColor: null
};
} else {
// Right side (extruder 0)
const isDisplayed = currentRightUnit?.id === unit.id;
return {
leftActiveSlot: null,
rightActiveSlot: isDisplayed ? slotIndex : null,
leftFilamentColor: null,
rightFilamentColor: isDisplayed ? color : null // Hide color if different AMS is selected
};
}
}
}
return { leftActiveSlot: null, rightActiveSlot: null, leftFilamentColor: null, rightFilamentColor: null };
};
const { leftActiveSlot, rightActiveSlot, leftFilamentColor, rightFilamentColor } = getLoadedTrayInfo();
return (
{/* Dual Panel Layout - just the panels, no wiring */}
{isDualNozzle && (
)}
{/* Unified Wiring Layer - ALL wiring drawn here */}
{/* Action Buttons Row - aligned with extruder */}