ExtruderControls.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import { useState } from 'react';
  2. import { useMutation } from '@tanstack/react-query';
  3. import { api, isConfirmationRequired } from '../../api/client';
  4. import type { PrinterStatus } from '../../api/client';
  5. import { ChevronUp, ChevronDown } from 'lucide-react';
  6. import { ConfirmModal } from '../ConfirmModal';
  7. interface ExtruderControlsProps {
  8. printerId: number;
  9. status: PrinterStatus | null | undefined;
  10. nozzleCount: number;
  11. }
  12. export function ExtruderControls({ printerId, status, nozzleCount }: ExtruderControlsProps) {
  13. const isConnected = status?.connected ?? false;
  14. const isPrinting = status?.state === 'RUNNING' || status?.state === 'PAUSE';
  15. const isDualNozzle = nozzleCount > 1;
  16. const [selectedNozzle, setSelectedNozzle] = useState<'left' | 'right'>('left');
  17. const [confirmModal, setConfirmModal] = useState<{
  18. token: string;
  19. warning: string;
  20. distance: number;
  21. } | null>(null);
  22. const extrudeMutation = useMutation({
  23. mutationFn: ({ distance, token }: { distance: number; token?: string }) => {
  24. // G-code for extrusion: relative mode, extrude, back to absolute
  25. // T0/T1 selects the tool for dual nozzle
  26. const toolSelect = isDualNozzle ? `T${selectedNozzle === 'left' ? 0 : 1}\n` : '';
  27. const gcode = `${toolSelect}G91\nG1 E${distance} F300\nG90`;
  28. return api.sendGcode(printerId, gcode, token);
  29. },
  30. onSuccess: (result, variables) => {
  31. if (isConfirmationRequired(result)) {
  32. setConfirmModal({
  33. token: result.token,
  34. warning: result.warning,
  35. distance: variables.distance,
  36. });
  37. }
  38. },
  39. });
  40. const handleExtrude = (distance: number) => {
  41. extrudeMutation.mutate({ distance });
  42. };
  43. const handleConfirm = () => {
  44. if (confirmModal) {
  45. extrudeMutation.mutate({ distance: confirmModal.distance, token: confirmModal.token });
  46. setConfirmModal(null);
  47. }
  48. };
  49. const isDisabled = !isConnected || isPrinting || extrudeMutation.isPending;
  50. return (
  51. <>
  52. <div className="flex flex-col items-center gap-1.5 justify-center">
  53. {/* Left/Right Toggle - only for dual nozzle */}
  54. {isDualNozzle && (
  55. <div className="flex rounded-md overflow-hidden border border-bambu-dark-tertiary mb-1 flex-shrink-0">
  56. <button
  57. onClick={() => setSelectedNozzle('left')}
  58. className={`px-3 py-1.5 text-sm border-r border-bambu-dark-tertiary transition-colors ${
  59. selectedNozzle === 'left'
  60. ? 'bg-bambu-green text-white'
  61. : 'bg-bambu-dark-secondary text-bambu-gray hover:bg-bambu-dark-tertiary'
  62. }`}
  63. >
  64. Left
  65. </button>
  66. <button
  67. onClick={() => setSelectedNozzle('right')}
  68. className={`px-3 py-1.5 text-sm transition-colors ${
  69. selectedNozzle === 'right'
  70. ? 'bg-bambu-green text-white'
  71. : 'bg-bambu-dark-secondary text-bambu-gray hover:bg-bambu-dark-tertiary'
  72. }`}
  73. >
  74. Right
  75. </button>
  76. </div>
  77. )}
  78. {/* Extrude Up Button */}
  79. <button
  80. onClick={() => handleExtrude(5)}
  81. disabled={isDisabled}
  82. className="w-9 h-[30px] rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary flex items-center justify-center text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
  83. title="Extrude 5mm"
  84. >
  85. <ChevronUp className="w-4 h-4" />
  86. </button>
  87. {/* Extruder Image */}
  88. <div className="h-[120px] flex items-center justify-center">
  89. <img
  90. src={isDualNozzle ? "/icons/dual-extruder.png" : "/icons/single-extruder1.png"}
  91. alt={isDualNozzle ? "Dual Extruder" : "Single Extruder"}
  92. className="h-full object-contain"
  93. onError={(e) => {
  94. (e.target as HTMLImageElement).style.display = 'none';
  95. }}
  96. />
  97. </div>
  98. {/* Retract Down Button */}
  99. <button
  100. onClick={() => handleExtrude(-5)}
  101. disabled={isDisabled}
  102. className="w-9 h-[30px] rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary flex items-center justify-center text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
  103. title="Retract 5mm"
  104. >
  105. <ChevronDown className="w-4 h-4" />
  106. </button>
  107. {/* Label */}
  108. <span className="text-xs text-bambu-gray mt-0.5">Extruder</span>
  109. </div>
  110. {/* Confirmation Modal */}
  111. {confirmModal && (
  112. <ConfirmModal
  113. title="Confirm Extrusion"
  114. message={confirmModal.warning}
  115. confirmText="Continue"
  116. variant="warning"
  117. onConfirm={handleConfirm}
  118. onCancel={() => setConfirmModal(null)}
  119. />
  120. )}
  121. </>
  122. );
  123. }