JogPad.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import { useMutation } from '@tanstack/react-query';
  2. import { api, isConfirmationRequired } from '../../api/client';
  3. import type { PrinterStatus } from '../../api/client';
  4. import { useState, useId } from 'react';
  5. import { ConfirmModal } from '../ConfirmModal';
  6. interface JogPadProps {
  7. printerId: number;
  8. status: PrinterStatus | null | undefined;
  9. disabled?: boolean;
  10. }
  11. // Image map coordinates for 220x220 jog pad
  12. // The jog pad has concentric rings: outer (10mm), inner (1mm), center (home)
  13. const SIZE = 220;
  14. const CENTER = SIZE / 2; // 110
  15. // Ring radii (approximate based on typical jog pad design)
  16. const OUTER_RADIUS = 108; // Outer edge
  17. const OUTER_INNER = 72; // Inner edge of outer ring (10mm zone)
  18. const INNER_INNER = 35; // Inner edge of inner ring (1mm zone)
  19. const HOME_RADIUS = 28; // Home button radius
  20. // Generate polygon points for a ring segment (pie slice)
  21. function ringSegment(
  22. cx: number, cy: number,
  23. innerR: number, outerR: number,
  24. startAngle: number, endAngle: number,
  25. steps: number = 8
  26. ): string {
  27. const points: string[] = [];
  28. // Outer arc (clockwise)
  29. for (let i = 0; i <= steps; i++) {
  30. const angle = startAngle + (endAngle - startAngle) * (i / steps);
  31. const x = Math.round(cx + outerR * Math.cos(angle));
  32. const y = Math.round(cy + outerR * Math.sin(angle));
  33. points.push(`${x},${y}`);
  34. }
  35. // Inner arc (counter-clockwise)
  36. for (let i = steps; i >= 0; i--) {
  37. const angle = startAngle + (endAngle - startAngle) * (i / steps);
  38. const x = Math.round(cx + innerR * Math.cos(angle));
  39. const y = Math.round(cy + innerR * Math.sin(angle));
  40. points.push(`${x},${y}`);
  41. }
  42. return points.join(',');
  43. }
  44. // Angle definitions (in radians, 0 = right, going clockwise)
  45. // Each direction covers 90 degrees (π/2), offset by 45 degrees (π/4)
  46. const ANGLES = {
  47. up: { start: -Math.PI * 3/4, end: -Math.PI / 4 }, // Top: -135° to -45°
  48. right: { start: -Math.PI / 4, end: Math.PI / 4 }, // Right: -45° to 45°
  49. down: { start: Math.PI / 4, end: Math.PI * 3/4 }, // Bottom: 45° to 135°
  50. left: { start: Math.PI * 3/4, end: Math.PI * 5/4 }, // Left: 135° to 225° (or -135°)
  51. };
  52. export function JogPad({ printerId, status, disabled = false }: JogPadProps) {
  53. const isConnected = (status?.connected ?? false) && !disabled;
  54. const mapId = useId();
  55. const [confirmModal, setConfirmModal] = useState<{
  56. action: string;
  57. token: string;
  58. warning: string;
  59. onConfirm: () => void;
  60. } | null>(null);
  61. const homeMutation = useMutation({
  62. mutationFn: ({ axes, token }: { axes: string; token?: string }) =>
  63. api.homeAxes(printerId, axes, token),
  64. onSuccess: (result) => {
  65. if (isConfirmationRequired(result)) {
  66. setConfirmModal({
  67. action: 'home',
  68. token: result.token,
  69. warning: result.warning,
  70. onConfirm: () => homeMutation.mutate({ axes: 'XY', token: result.token }),
  71. });
  72. }
  73. },
  74. });
  75. const moveMutation = useMutation({
  76. mutationFn: ({ axis, distance, token }: { axis: string; distance: number; token?: string }) =>
  77. api.moveAxis(printerId, axis, distance, 3000, token),
  78. onSuccess: (result, variables) => {
  79. if (isConfirmationRequired(result)) {
  80. setConfirmModal({
  81. action: 'move',
  82. token: result.token,
  83. warning: result.warning,
  84. onConfirm: () =>
  85. moveMutation.mutate({
  86. axis: variables.axis,
  87. distance: variables.distance,
  88. token: result.token,
  89. }),
  90. });
  91. }
  92. },
  93. });
  94. const handleHome = () => {
  95. if (isDisabled) return;
  96. homeMutation.mutate({ axes: 'XY' });
  97. };
  98. const handleMove = (axis: string, distance: number) => {
  99. if (isDisabled) return;
  100. moveMutation.mutate({ axis, distance });
  101. };
  102. const handleConfirm = () => {
  103. if (confirmModal) {
  104. confirmModal.onConfirm();
  105. setConfirmModal(null);
  106. }
  107. };
  108. const isLoading = homeMutation.isPending || moveMutation.isPending;
  109. const isDisabled = !isConnected || isLoading;
  110. // Generate coordinates for circle (home button)
  111. const homeCoords = Array.from({ length: 16 }, (_, i) => {
  112. const angle = (i / 16) * Math.PI * 2;
  113. const x = Math.round(CENTER + HOME_RADIUS * Math.cos(angle));
  114. const y = Math.round(CENTER + HOME_RADIUS * Math.sin(angle));
  115. return `${x},${y}`;
  116. }).join(',');
  117. return (
  118. <>
  119. <div className="relative w-[220px] h-[220px] mb-3.5">
  120. <img
  121. src="/icons/jogpad.svg"
  122. alt="Jog Pad"
  123. useMap={`#${mapId}`}
  124. className="w-full h-full jogpad-theme"
  125. />
  126. <map name={mapId}>
  127. {/* Outer ring - 10mm moves */}
  128. <area
  129. shape="poly"
  130. coords={ringSegment(CENTER, CENTER, OUTER_INNER, OUTER_RADIUS, ANGLES.up.start, ANGLES.up.end)}
  131. onClick={() => handleMove('Y', 10)}
  132. title="Y+10mm"
  133. style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
  134. />
  135. <area
  136. shape="poly"
  137. coords={ringSegment(CENTER, CENTER, OUTER_INNER, OUTER_RADIUS, ANGLES.down.start, ANGLES.down.end)}
  138. onClick={() => handleMove('Y', -10)}
  139. title="Y-10mm"
  140. style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
  141. />
  142. <area
  143. shape="poly"
  144. coords={ringSegment(CENTER, CENTER, OUTER_INNER, OUTER_RADIUS, ANGLES.left.start, ANGLES.left.end)}
  145. onClick={() => handleMove('X', -10)}
  146. title="X-10mm"
  147. style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
  148. />
  149. <area
  150. shape="poly"
  151. coords={ringSegment(CENTER, CENTER, OUTER_INNER, OUTER_RADIUS, ANGLES.right.start, ANGLES.right.end)}
  152. onClick={() => handleMove('X', 10)}
  153. title="X+10mm"
  154. style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
  155. />
  156. {/* Inner ring - 1mm moves */}
  157. <area
  158. shape="poly"
  159. coords={ringSegment(CENTER, CENTER, INNER_INNER, OUTER_INNER, ANGLES.up.start, ANGLES.up.end)}
  160. onClick={() => handleMove('Y', 1)}
  161. title="Y+1mm"
  162. style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
  163. />
  164. <area
  165. shape="poly"
  166. coords={ringSegment(CENTER, CENTER, INNER_INNER, OUTER_INNER, ANGLES.down.start, ANGLES.down.end)}
  167. onClick={() => handleMove('Y', -1)}
  168. title="Y-1mm"
  169. style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
  170. />
  171. <area
  172. shape="poly"
  173. coords={ringSegment(CENTER, CENTER, INNER_INNER, OUTER_INNER, ANGLES.left.start, ANGLES.left.end)}
  174. onClick={() => handleMove('X', -1)}
  175. title="X-1mm"
  176. style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
  177. />
  178. <area
  179. shape="poly"
  180. coords={ringSegment(CENTER, CENTER, INNER_INNER, OUTER_INNER, ANGLES.right.start, ANGLES.right.end)}
  181. onClick={() => handleMove('X', 1)}
  182. title="X+1mm"
  183. style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
  184. />
  185. {/* Center - Home button */}
  186. <area
  187. shape="poly"
  188. coords={homeCoords}
  189. onClick={handleHome}
  190. title="Home XY"
  191. style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
  192. />
  193. </map>
  194. </div>
  195. {/* Confirmation Modal */}
  196. {confirmModal && (
  197. <ConfirmModal
  198. title="Confirm Action"
  199. message={confirmModal.warning}
  200. confirmText="Continue"
  201. variant="warning"
  202. onConfirm={handleConfirm}
  203. onCancel={() => setConfirmModal(null)}
  204. />
  205. )}
  206. </>
  207. );
  208. }