| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- import { useState } from 'react';
- import { useMutation } from '@tanstack/react-query';
- import { api } from '../../api/client';
- import type { PrinterStatus, AMSUnit } from '../../api/client';
- import { Loader2 } from 'lucide-react';
- interface AMSSectionDualProps {
- printerId: number;
- status: PrinterStatus | null | undefined;
- nozzleCount: number;
- }
- function hexToRgb(hex: string | null): string {
- if (!hex) return 'rgb(128, 128, 128)';
- const cleanHex = hex.replace('#', '').substring(0, 6);
- const r = parseInt(cleanHex.substring(0, 2), 16) || 128;
- const g = parseInt(cleanHex.substring(2, 4), 16) || 128;
- const b = parseInt(cleanHex.substring(4, 6), 16) || 128;
- return `rgb(${r}, ${g}, ${b})`;
- }
- function isLightColor(hex: string | null): boolean {
- if (!hex) return false;
- const cleanHex = hex.replace('#', '').substring(0, 6);
- const r = parseInt(cleanHex.substring(0, 2), 16) || 0;
- const g = parseInt(cleanHex.substring(2, 4), 16) || 0;
- const b = parseInt(cleanHex.substring(4, 6), 16) || 0;
- const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
- return luminance > 0.5;
- }
- interface AMSPanelContentProps {
- units: AMSUnit[];
- side: 'left' | 'right';
- isPrinting: boolean;
- selectedAmsIndex: number;
- onSelectAms: (index: number) => void;
- selectedTray: number | null;
- onSelectTray: (trayId: number | null) => void;
- }
- function AMSPanelContent({
- units,
- side,
- isPrinting,
- selectedAmsIndex,
- onSelectAms,
- selectedTray,
- onSelectTray,
- }: AMSPanelContentProps) {
- const selectedUnit = units[selectedAmsIndex];
- const slotPrefix = side === 'left' ? 'A' : 'B';
- return (
- <div className="flex-1 min-w-0 overflow-visible">
- <div className="text-center text-[11px] font-semibold text-bambu-gray uppercase mb-2">
- {side === 'left' ? 'Left Nozzle' : 'Right Nozzle'}
- </div>
- {/* AMS Tab Selectors - only show connected units */}
- <div className="flex gap-1.5 mb-2.5 p-1.5 bg-bambu-dark/50 rounded-lg w-fit">
- {units.map((unit, index) => (
- <button
- key={unit.id}
- onClick={() => onSelectAms(index)}
- className={`flex items-center p-1.5 rounded border-2 transition-colors bg-bambu-dark ${
- selectedAmsIndex === index
- ? 'border-bambu-green'
- : 'border-transparent hover:border-bambu-gray'
- }`}
- >
- <div className="flex gap-0.5">
- {unit.tray.map((tray) => (
- <div
- key={tray.id}
- className="w-2.5 h-2.5 rounded-full"
- style={{
- backgroundColor: tray.tray_color ? hexToRgb(tray.tray_color) : '#808080',
- }}
- />
- ))}
- </div>
- </button>
- ))}
- </div>
- {/* AMS Content */}
- {selectedUnit && (
- <div className="bg-bambu-dark-secondary rounded-[10px] p-2.5 pb-0 overflow-visible">
- {/* AMS Header - Humidity & Temp */}
- <div className="flex items-center gap-2.5 text-xs text-bambu-gray mb-2">
- {selectedUnit.humidity !== null && (
- <span className="flex items-center gap-1">
- <img src="/icons/water.svg" alt="" className="w-3.5 icon-theme" />
- {selectedUnit.humidity} %
- </span>
- )}
- {selectedUnit.temp !== null && (
- <span className="flex items-center gap-1">
- <img src="/icons/temperature.svg" alt="" className="w-3.5 icon-theme" />
- {selectedUnit.temp}°C
- </span>
- )}
- <span className="text-yellow-500 text-sm">☀</span>
- </div>
- {/* Slot Labels */}
- <div className="flex justify-center gap-1.5 mb-1.5">
- {selectedUnit.tray.map((tray, index) => (
- <div
- key={tray.id}
- className="w-12 flex items-center justify-center gap-0.5 text-[10px] text-bambu-gray px-1.5 py-[3px] bg-bambu-dark rounded-full border border-bambu-dark-tertiary"
- >
- {slotPrefix}{index + 1}
- <img src="/icons/reload.svg" alt="" className="w-2.5 h-2.5 icon-theme" />
- </div>
- ))}
- </div>
- {/* AMS Slots with integrated wiring */}
- <div className="flex justify-center gap-1.5 mb-0">
- {selectedUnit.tray.map((tray) => {
- const globalTrayId = selectedUnit.id * 4 + tray.id;
- const isSelected = selectedTray === globalTrayId;
- const isEmpty = !tray.tray_type || tray.tray_type === '' || tray.tray_type === 'NONE';
- const isLight = isLightColor(tray.tray_color);
- return (
- <div key={tray.id} className="flex flex-col items-center">
- <button
- onClick={() => !isEmpty && onSelectTray(isSelected ? null : globalTrayId)}
- disabled={isEmpty || isPrinting}
- className={`w-12 h-[70px] rounded-md border-2 overflow-hidden transition-all bg-bambu-dark ${
- isSelected
- ? 'border-[#d4a84b]'
- : 'border-bambu-dark-tertiary hover:border-bambu-gray'
- } ${isEmpty ? 'opacity-50' : ''} disabled:cursor-not-allowed`}
- >
- <div
- className="w-full h-full flex flex-col items-center justify-end pb-[5px]"
- style={{
- backgroundColor: isEmpty ? undefined : hexToRgb(tray.tray_color),
- }}
- >
- <span
- className={`text-[11px] font-semibold mb-1 ${
- isLight ? 'text-gray-800' : 'text-white'
- } ${isLight ? '' : 'drop-shadow-sm'}`}
- >
- {isEmpty ? '--' : tray.tray_type}
- </span>
- {!isEmpty && (
- <img
- src="/icons/eye.svg"
- alt=""
- className={`w-3.5 h-3.5 ${isLight ? '' : 'invert'}`}
- style={{ opacity: 0.8 }}
- />
- )}
- </div>
- </button>
- {/* Vertical wire from slot center down */}
- <div className="w-[2px] h-[14px] bg-[#909090]" />
- </div>
- );
- })}
- </div>
- {/* Wiring visualization - horizontal bar and hub */}
- <div className="flex justify-center">
- <div className="relative h-[50px]" style={{ width: '210px' }}>
- {/* Horizontal bar connecting all slots (spans from first to last slot center) */}
- <div className="absolute left-[24px] right-[24px] top-0 border-t-2 border-[#909090]" />
- {/* Center hub box on the horizontal bar */}
- <div className="absolute left-1/2 -translate-x-1/2 top-[-6px] w-[28px] h-[14px] bg-[#c0c0c0] border border-[#909090] rounded-sm" />
- {/* Vertical wire from hub going down */}
- <div className="absolute left-1/2 -translate-x-[1px] top-[8px] h-[14px] border-l-2 border-[#909090]" />
- {/* Horizontal wire from hub toward the center of the panel (extends beyond panel edge) */}
- {side === 'left' && (
- <div className="absolute left-1/2 top-[21px] w-[calc(50%+30px)] border-t-2 border-[#909090]" />
- )}
- {side === 'right' && (
- <div className="absolute right-1/2 top-[21px] w-[calc(50%+30px)] border-t-2 border-[#909090]" />
- )}
- </div>
- </div>
- </div>
- )}
- {/* No AMS message */}
- {units.length === 0 && (
- <div className="bg-bambu-dark-secondary rounded-[10px] p-6 text-center text-bambu-gray text-sm">
- No AMS connected to {side} nozzle
- </div>
- )}
- </div>
- );
- }
- export function AMSSectionDual({ printerId, status, nozzleCount }: AMSSectionDualProps) {
- const isConnected = status?.connected ?? false;
- const isPrinting = status?.state === 'RUNNING';
- const isDualNozzle = nozzleCount > 1;
- const amsUnits: AMSUnit[] = status?.ams ?? [];
- // For dual nozzle, split AMS units between left and right
- // In real implementation, this would be based on actual nozzle assignment
- const leftUnits = isDualNozzle ? amsUnits.filter((_, i) => i % 2 === 0) : amsUnits;
- const rightUnits = isDualNozzle ? amsUnits.filter((_, i) => i % 2 === 1) : [];
- const [leftAmsIndex, setLeftAmsIndex] = useState(0);
- const [rightAmsIndex, setRightAmsIndex] = useState(0);
- const [selectedTray, setSelectedTray] = useState<number | null>(null);
- const loadMutation = useMutation({
- mutationFn: (trayId: number) => api.amsLoadFilament(printerId, trayId),
- });
- const unloadMutation = useMutation({
- mutationFn: () => api.amsUnloadFilament(printerId),
- });
- const handleLoad = () => {
- if (selectedTray !== null) {
- loadMutation.mutate(selectedTray);
- }
- };
- const handleUnload = () => {
- unloadMutation.mutate();
- };
- const isLoading = loadMutation.isPending || unloadMutation.isPending;
- return (
- <div className="bg-bambu-dark-tertiary rounded-[10px] p-3 relative overflow-visible">
- {/* Center wiring and Extruder - absolutely centered between the two AMS panels */}
- {isDualNozzle && (
- <>
- {/* Center wiring: two vertical lines going down to extruder inlets */}
- {/* Positioned to connect with horizontal wires from AMS panels */}
- <div className="absolute left-1/2 -translate-x-1/2 bottom-[62px] pointer-events-none" style={{ width: '24px', height: '30px' }}>
- {/* Left vertical line - connects to left AMS horizontal wire, goes to left extruder inlet */}
- <div className="absolute left-0 top-0 h-full border-l-2 border-[#909090]" />
- {/* Right vertical line - connects to right AMS horizontal wire, goes to right extruder inlet */}
- <div className="absolute right-0 top-0 h-full border-l-2 border-[#909090]" />
- </div>
- {/* Extruder */}
- <img
- src="/icons/extruder-left-right.png"
- alt="Extruder"
- className="absolute h-[50px] left-1/2 -translate-x-1/2 bottom-[12px]"
- />
- </>
- )}
- {/* Dual Panel Layout */}
- <div className="flex gap-5 overflow-visible">
- {/* Left Nozzle Panel */}
- <AMSPanelContent
- units={leftUnits}
- side="left"
- isPrinting={isPrinting}
- selectedAmsIndex={leftAmsIndex}
- onSelectAms={setLeftAmsIndex}
- selectedTray={selectedTray}
- onSelectTray={setSelectedTray}
- />
- {/* Right Nozzle Panel - only for dual nozzle */}
- {isDualNozzle && (
- <AMSPanelContent
- units={rightUnits}
- side="right"
- isPrinting={isPrinting}
- selectedAmsIndex={rightAmsIndex}
- onSelectAms={setRightAmsIndex}
- selectedTray={selectedTray}
- onSelectTray={setSelectedTray}
- />
- )}
- </div>
- {/* Action Buttons Row with Extruder */}
- <div className="flex items-start pt-2">
- {/* Left buttons */}
- <div className="flex items-center gap-2">
- <button className="w-10 h-10 rounded-lg bg-bambu-dark-secondary hover:bg-bambu-dark border border-bambu-dark-tertiary flex items-center justify-center">
- <img src="/icons/ams-settings.svg" alt="Settings" className="w-5 icon-theme" />
- </button>
- <button className="px-[18px] py-2.5 rounded-lg bg-bambu-dark-secondary hover:bg-bambu-dark border border-bambu-dark-tertiary text-sm text-bambu-gray flex items-center gap-1.5">
- Auto-refill
- </button>
- </div>
- {/* Spacer */}
- <div className="flex-1" />
- {/* Right buttons */}
- <div className="flex items-center gap-2">
- <button
- onClick={handleUnload}
- disabled={!isConnected || isPrinting || isLoading}
- className="px-7 py-2.5 rounded-lg bg-bambu-dark hover:bg-bambu-dark-secondary text-sm text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
- >
- {unloadMutation.isPending ? (
- <Loader2 className="w-4 h-4 animate-spin" />
- ) : (
- 'Unload'
- )}
- </button>
- <button
- onClick={handleLoad}
- disabled={!isConnected || isPrinting || selectedTray === null || isLoading}
- className="px-7 py-2.5 rounded-lg bg-bambu-dark hover:bg-bambu-dark-secondary text-sm text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
- >
- {loadMutation.isPending ? (
- <Loader2 className="w-4 h-4 animate-spin" />
- ) : (
- 'Load'
- )}
- </button>
- </div>
- </div>
- {/* Error messages */}
- {(loadMutation.error || unloadMutation.error) && (
- <p className="mt-2 text-sm text-red-500 text-center">
- {(loadMutation.error || unloadMutation.error)?.message}
- </p>
- )}
- </div>
- );
- }
|