From 2d6c9bf7c9e15da773bc038735ecebfdcaa31704 Mon Sep 17 00:00:00 2001 From: tariknz Date: Fri, 1 Aug 2025 14:41:07 +1200 Subject: [PATCH 1/6] some tidy up --- .../Input/InputSteer/InputSteer.stories.tsx | 4 +- .../Input/InputSteer/InputSteer.tsx | 36 ++++-- .../InputSteer/RotationIndicator.spec.tsx | 31 +++++ .../InputSteer/RotationIndicator.stories.tsx | 76 ++++++++++++ .../Input/InputSteer/RotationIndicator.tsx | 108 ++++++++++++++++++ .../Input/InputSteer/hooks/index.ts | 2 + .../hooks/useWheelRotation.spec.tsx | 75 ++++++++++++ .../InputSteer/hooks/useWheelRotation.tsx | 67 +++++++++++ 8 files changed, 388 insertions(+), 11 deletions(-) create mode 100644 src/frontend/components/Input/InputSteer/RotationIndicator.spec.tsx create mode 100644 src/frontend/components/Input/InputSteer/RotationIndicator.stories.tsx create mode 100644 src/frontend/components/Input/InputSteer/RotationIndicator.tsx create mode 100644 src/frontend/components/Input/InputSteer/hooks/index.ts create mode 100644 src/frontend/components/Input/InputSteer/hooks/useWheelRotation.spec.tsx create mode 100644 src/frontend/components/Input/InputSteer/hooks/useWheelRotation.tsx diff --git a/src/frontend/components/Input/InputSteer/InputSteer.stories.tsx b/src/frontend/components/Input/InputSteer/InputSteer.stories.tsx index af8bcb9..bcb4d97 100644 --- a/src/frontend/components/Input/InputSteer/InputSteer.stories.tsx +++ b/src/frontend/components/Input/InputSteer/InputSteer.stories.tsx @@ -7,8 +7,8 @@ export default { angleRad: { control: { type: 'range', - min: -3.14, - max: 3.14, + min: -2*3.14, + max: 2*3.14, step: 0.01, }, }, diff --git a/src/frontend/components/Input/InputSteer/InputSteer.tsx b/src/frontend/components/Input/InputSteer/InputSteer.tsx index 92cff88..31ff79c 100644 --- a/src/frontend/components/Input/InputSteer/InputSteer.tsx +++ b/src/frontend/components/Input/InputSteer/InputSteer.tsx @@ -1,3 +1,4 @@ +import { useMemo, useRef, useEffect } from 'react'; import { DefaultB, DefaultW, @@ -10,6 +11,7 @@ import { UshapeB, UshapeW, } from './wheels'; +import { RotationIndicator } from './RotationIndicator'; export type WheelStyle = 'formula' | 'lmp' | 'nascar' | 'ushape' | 'default'; @@ -42,27 +44,43 @@ export interface InputSteerProps { wheelColor?: 'dark' | 'light'; } -export const InputSteer = ({ +export function InputSteer({ angleRad = 0, wheelStyle = 'default', wheelColor = 'light', -}: InputSteerProps) => { - const WheelComponent = - wheelStyle in wheelComponentMap +}: InputSteerProps) { + const wheelRef = useRef(null); + + // Memoize the wheel component selection (only changes when style/color change) + const WheelComponent = useMemo(() => { + return wheelStyle in wheelComponentMap ? wheelComponentMap[wheelStyle][wheelColor] : wheelComponentMap.default[wheelColor]; + }, [wheelStyle, wheelColor]); + + // Use CSS custom properties for smooth updates without React re-renders + useEffect(() => { + if (wheelRef.current) { + wheelRef.current.style.setProperty('--wheel-rotation', `${angleRad * -1}rad`); + } + }, [angleRad]); return ( -
- +
+ > + +
+
); -}; +} diff --git a/src/frontend/components/Input/InputSteer/RotationIndicator.spec.tsx b/src/frontend/components/Input/InputSteer/RotationIndicator.spec.tsx new file mode 100644 index 0000000..9452df4 --- /dev/null +++ b/src/frontend/components/Input/InputSteer/RotationIndicator.spec.tsx @@ -0,0 +1,31 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { RotationIndicator } from './RotationIndicator'; + +describe('RotationIndicator', () => { + it('should render with zero degrees', () => { + render(); + + expect(screen.getByText('0°')).toBeInTheDocument(); + }); + + it('should show correct angle in degrees', () => { + render(); + + expect(screen.getByText('-90°')).toBeInTheDocument(); + }); + + it('should show negative angle in degrees', () => { + render(); + + expect(screen.getByText('45°')).toBeInTheDocument(); + }); + + it('should apply custom className', () => { + const { container } = render( + + ); + + expect(container.firstChild).toHaveClass('custom-class'); + }); +}); \ No newline at end of file diff --git a/src/frontend/components/Input/InputSteer/RotationIndicator.stories.tsx b/src/frontend/components/Input/InputSteer/RotationIndicator.stories.tsx new file mode 100644 index 0000000..5a78f11 --- /dev/null +++ b/src/frontend/components/Input/InputSteer/RotationIndicator.stories.tsx @@ -0,0 +1,76 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { RotationIndicator } from './RotationIndicator'; + +const meta: Meta = { + component: RotationIndicator, + parameters: { + layout: 'centered', + }, + argTypes: { + currentAngleRad: { + control: { + type: 'range', + min: -2*3.14, + max: 2*3.14, + step: 0.01, + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const CenterPosition: Story = { + args: { + currentAngleRad: 0, // 0 degrees - center + }, +}; + +export const SmallRight: Story = { + args: { + currentAngleRad: 0.25, // ~14 degrees - small right + }, +}; + +export const SmallLeft: Story = { + args: { + currentAngleRad: -0.25, // ~-14 degrees - small left + }, +}; + +export const MediumRight: Story = { + args: { + currentAngleRad: 0.5, // ~29 degrees - medium right + }, +}; + +export const MediumLeft: Story = { + args: { + currentAngleRad: -0.5, // ~-29 degrees - medium left + }, +}; + +export const FullRight: Story = { + args: { + currentAngleRad: 2 * Math.PI, // 360 degrees - full right + }, +}; + +export const FullLeft: Story = { + args: { + currentAngleRad: -2 * Math.PI, // -360 degrees - full left + }, +}; + +export const BeyondRange: Story = { + args: { + currentAngleRad: 3 * Math.PI, // 540 degrees - beyond range + }, +}; + +export const BeyondRangeNegative: Story = { + args: { + currentAngleRad: -3 * Math.PI, // -540 degrees - beyond range + }, +}; \ No newline at end of file diff --git a/src/frontend/components/Input/InputSteer/RotationIndicator.tsx b/src/frontend/components/Input/InputSteer/RotationIndicator.tsx new file mode 100644 index 0000000..8a6f192 --- /dev/null +++ b/src/frontend/components/Input/InputSteer/RotationIndicator.tsx @@ -0,0 +1,108 @@ +import { useMemo, useRef, useEffect } from 'react'; + +interface RotationIndicatorProps { + currentAngleRad: number; + className?: string; +} + +export function RotationIndicator({ + currentAngleRad, + className = '' +}: RotationIndicatorProps) { + const bubbleRef = useRef(null); + const textRef = useRef(null); + + // Memoize calculations to avoid recalculating on every render + const { clampedPosition, bubbleColor, opacity, textColor, angleDegrees, shouldShow } = useMemo(() => { + // Convert radians to degrees + const angleDegrees = (currentAngleRad * -1 * 180) / Math.PI; + + // Only show when beyond ±180 degrees + const shouldShow = Math.abs(angleDegrees) > 180; + + if (!shouldShow) { + return { + clampedPosition: 0, + bubbleColor: 'rgb(255, 255, 255)', + opacity: 0, + textColor: 'text-slate-300', + angleDegrees, + shouldShow + }; + } + + // Calculate bubble position for range -360° to +360° + // Scale the small steering wheel angles to map to the full range + const bubblePosition = (angleDegrees / (360)) * 100; + // Clamp to container bounds + const clampedPosition = Math.max(-100, Math.min(100, bubblePosition)); + + // Calculate color intensity based on proximity to ±360° + const absAngleDegrees = Math.abs(angleDegrees); + const colorIntensity = Math.min(absAngleDegrees / 360, 1); // 0 to 1 + const isRed = colorIntensity > 0.5; // Start turning red at 50% of max range + + // Calculate transparency based on distance from center + const centerDistance = Math.abs(angleDegrees) / 360; // 0 to 1 + const opacity = centerDistance; // 0% opacity at center, 100% at extremes + + // Interpolate between white and red + const bubbleColor = isRed + ? `rgb(${255}, ${255 - Math.floor(colorIntensity * 255)}, ${255 - Math.floor(colorIntensity * 255)})` + : 'rgb(255, 255, 255)'; + + const textColor = isRed ? 'text-red-400' : 'text-slate-300'; + + return { + clampedPosition, + bubbleColor, + opacity, + textColor, + angleDegrees, + shouldShow + }; + }, [currentAngleRad]); + + // Use CSS custom properties for smooth updates without React re-renders + useEffect(() => { + if (bubbleRef.current) { + bubbleRef.current.style.setProperty('--bubble-position', `${50 + clampedPosition}%`); + bubbleRef.current.style.setProperty('--bubble-color', bubbleColor); + bubbleRef.current.style.setProperty('--bubble-opacity', opacity.toString()); + } + }, [clampedPosition, bubbleColor, opacity]); + + // Update text content without re-rendering the entire component + useEffect(() => { + if (textRef.current) { + textRef.current.textContent = shouldShow ? `${angleDegrees.toFixed(0)}°` : ''; + } + }, [angleDegrees, shouldShow]); + + // Don't render anything if not showing + if (!shouldShow) { + return
; + } + + return ( +
+ {/* Floating bubble */} +
+ + {/* Centered text */} +
+
+ ); +} \ No newline at end of file diff --git a/src/frontend/components/Input/InputSteer/hooks/index.ts b/src/frontend/components/Input/InputSteer/hooks/index.ts new file mode 100644 index 0000000..9321cdf --- /dev/null +++ b/src/frontend/components/Input/InputSteer/hooks/index.ts @@ -0,0 +1,2 @@ +export { useWheelRotation } from './useWheelRotation'; +export type { WheelRotationState } from './useWheelRotation'; \ No newline at end of file diff --git a/src/frontend/components/Input/InputSteer/hooks/useWheelRotation.spec.tsx b/src/frontend/components/Input/InputSteer/hooks/useWheelRotation.spec.tsx new file mode 100644 index 0000000..ec571f9 --- /dev/null +++ b/src/frontend/components/Input/InputSteer/hooks/useWheelRotation.spec.tsx @@ -0,0 +1,75 @@ +import { renderHook } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { useWheelRotation } from './useWheelRotation'; + +describe('useWheelRotation', () => { + it('should initialize with default values', () => { + const { result } = renderHook(() => useWheelRotation(0)); + + expect(result.current.totalRotations).toBe(0); + expect(result.current.hasRotatedOver360).toBe(false); + expect(result.current.rotationDirection).toBe('none'); + }); + + it('should track clockwise rotation', () => { + const { result, rerender } = renderHook((angle) => useWheelRotation(angle), { + initialProps: 0, + }); + + // Simulate clockwise rotation + rerender(Math.PI / 2); // 90 degrees + rerender(Math.PI); // 180 degrees + rerender(3 * Math.PI / 2); // 270 degrees + rerender(2 * Math.PI); // 360 degrees + + expect(result.current.totalRotations).toBe(1); + expect(result.current.hasRotatedOver360).toBe(true); + }); + + it('should track counterclockwise rotation', () => { + const { result, rerender } = renderHook((angle) => useWheelRotation(angle), { + initialProps: 0, + }); + + // Simulate counterclockwise rotation + rerender(-Math.PI / 2); // -90 degrees + rerender(-Math.PI); // -180 degrees + rerender(-3 * Math.PI / 2); // -270 degrees + rerender(-2 * Math.PI); // -360 degrees + + expect(result.current.totalRotations).toBe(1); + expect(result.current.hasRotatedOver360).toBe(true); + }); + + it('should handle multiple rotations', () => { + const { result, rerender } = renderHook((angle) => useWheelRotation(angle), { + initialProps: 0, + }); + + // Simulate 2.5 rotations clockwise + rerender(Math.PI / 2); + rerender(Math.PI); + rerender(3 * Math.PI / 2); + rerender(2 * Math.PI); + rerender(5 * Math.PI / 2); + rerender(3 * Math.PI); + rerender(7 * Math.PI / 2); + rerender(4 * Math.PI); + rerender(9 * Math.PI / 2); + + expect(result.current.totalRotations).toBe(2); + expect(result.current.hasRotatedOver360).toBe(true); + }); + + it('should handle angle wraparound', () => { + const { result, rerender } = renderHook((angle) => useWheelRotation(angle), { + initialProps: 0, + }); + + // Simulate rotation that crosses the ±π boundary + rerender(Math.PI - 0.1); + rerender(Math.PI + 0.1); // Crosses the boundary + + expect(result.current.rotationDirection).toBe('clockwise'); + }); +}); \ No newline at end of file diff --git a/src/frontend/components/Input/InputSteer/hooks/useWheelRotation.tsx b/src/frontend/components/Input/InputSteer/hooks/useWheelRotation.tsx new file mode 100644 index 0000000..5c2b14f --- /dev/null +++ b/src/frontend/components/Input/InputSteer/hooks/useWheelRotation.tsx @@ -0,0 +1,67 @@ +import { useEffect, useRef, useState } from 'react'; + +export interface WheelRotationState { + totalRotations: number; + currentAngle: number; + rotationDirection: 'clockwise' | 'counterclockwise' | 'none'; + hasRotatedOver360: boolean; +} + +export const useWheelRotation = (angleRad = 0) => { + const [rotationState, setRotationState] = useState({ + totalRotations: 0, + currentAngle: 0, + rotationDirection: 'none', + hasRotatedOver360: false, + }); + + const prevAngleRef = useRef(0); + const totalRotationRef = useRef(0); + const lastDirectionRef = useRef<'clockwise' | 'counterclockwise' | 'none'>('none'); + + useEffect(() => { + const currentAngle = angleRad; + const prevAngle = prevAngleRef.current; + + if (prevAngle === 0 && currentAngle === 0) { + return; + } + + // Calculate the angle difference, handling wraparound + let angleDiff = currentAngle - prevAngle; + + // Handle wraparound at ±π + if (angleDiff > Math.PI) { + angleDiff -= 2 * Math.PI; + } else if (angleDiff < -Math.PI) { + angleDiff += 2 * Math.PI; + } + + // Determine rotation direction + let direction: 'clockwise' | 'counterclockwise' | 'none' = 'none'; + if (Math.abs(angleDiff) > 0.01) { // Threshold to avoid noise + direction = angleDiff > 0 ? 'clockwise' : 'counterclockwise'; + } + + // Update total rotation + if (direction !== 'none') { + totalRotationRef.current += angleDiff; + } + + // Calculate total rotations (360 degrees = 2π radians) + const totalRotations = Math.abs(totalRotationRef.current) / (2 * Math.PI); + const hasRotatedOver360 = totalRotations >= 1; + + setRotationState({ + totalRotations: Math.floor(totalRotations), + currentAngle, + rotationDirection: direction, + hasRotatedOver360, + }); + + prevAngleRef.current = currentAngle; + lastDirectionRef.current = direction; + }, [angleRad]); + + return rotationState; +}; \ No newline at end of file From dd6e198a6c24feb825266c797d04e06c1535b8d8 Mon Sep 17 00:00:00 2001 From: tariknz Date: Sat, 2 Aug 2025 11:56:00 +1200 Subject: [PATCH 2/6] tidy --- .../Input/InputSteer/RotationIndicator.tsx | 153 ++++++++---------- src/frontend/theme.css | 9 ++ 2 files changed, 74 insertions(+), 88 deletions(-) diff --git a/src/frontend/components/Input/InputSteer/RotationIndicator.tsx b/src/frontend/components/Input/InputSteer/RotationIndicator.tsx index 8a6f192..7ed457e 100644 --- a/src/frontend/components/Input/InputSteer/RotationIndicator.tsx +++ b/src/frontend/components/Input/InputSteer/RotationIndicator.tsx @@ -1,108 +1,85 @@ -import { useMemo, useRef, useEffect } from 'react'; +import { useMemo } from 'react'; +import { + ArrowsClockwiseIcon, + ArrowsCounterClockwiseIcon, +} from '@phosphor-icons/react'; interface RotationIndicatorProps { currentAngleRad: number; - className?: string; } -export function RotationIndicator({ - currentAngleRad, - className = '' +export function RotationIndicator({ + currentAngleRad }: RotationIndicatorProps) { - const bubbleRef = useRef(null); - const textRef = useRef(null); - // Memoize calculations to avoid recalculating on every render - const { clampedPosition, bubbleColor, opacity, textColor, angleDegrees, shouldShow } = useMemo(() => { - // Convert radians to degrees - const angleDegrees = (currentAngleRad * -1 * 180) / Math.PI; - - // Only show when beyond ±180 degrees - const shouldShow = Math.abs(angleDegrees) > 180; - - if (!shouldShow) { - return { - clampedPosition: 0, - bubbleColor: 'rgb(255, 255, 255)', - opacity: 0, - textColor: 'text-slate-300', - angleDegrees, - shouldShow - }; - } - - // Calculate bubble position for range -360° to +360° - // Scale the small steering wheel angles to map to the full range - const bubblePosition = (angleDegrees / (360)) * 100; - // Clamp to container bounds - const clampedPosition = Math.max(-100, Math.min(100, bubblePosition)); + const { shouldShow, angleDegrees, direction } = + useMemo(() => { + // Convert radians to degrees + const angleDegrees = (currentAngleRad * -1 * 180) / Math.PI; - // Calculate color intensity based on proximity to ±360° - const absAngleDegrees = Math.abs(angleDegrees); - const colorIntensity = Math.min(absAngleDegrees / 360, 1); // 0 to 1 - const isRed = colorIntensity > 0.5; // Start turning red at 50% of max range - - // Calculate transparency based on distance from center - const centerDistance = Math.abs(angleDegrees) / 360; // 0 to 1 - const opacity = centerDistance; // 0% opacity at center, 100% at extremes - - // Interpolate between white and red - const bubbleColor = isRed - ? `rgb(${255}, ${255 - Math.floor(colorIntensity * 255)}, ${255 - Math.floor(colorIntensity * 255)})` - : 'rgb(255, 255, 255)'; - - const textColor = isRed ? 'text-red-400' : 'text-slate-300'; + // Only show when beyond ±180 degrees + const shouldShow = Math.abs(angleDegrees) > 180; - return { - clampedPosition, - bubbleColor, - opacity, - textColor, - angleDegrees, - shouldShow - }; - }, [currentAngleRad]); + if (!shouldShow) { + return { + shouldShow, + angleDegrees, + direction: 'none' as const, + }; + } - // Use CSS custom properties for smooth updates without React re-renders - useEffect(() => { - if (bubbleRef.current) { - bubbleRef.current.style.setProperty('--bubble-position', `${50 + clampedPosition}%`); - bubbleRef.current.style.setProperty('--bubble-color', bubbleColor); - bubbleRef.current.style.setProperty('--bubble-opacity', opacity.toString()); - } - }, [clampedPosition, bubbleColor, opacity]); + // Determine direction to center + const direction = angleDegrees > 0 ? 'left' : 'right'; - // Update text content without re-rendering the entire component - useEffect(() => { - if (textRef.current) { - textRef.current.textContent = shouldShow ? `${angleDegrees.toFixed(0)}°` : ''; - } - }, [angleDegrees, shouldShow]); + return { + shouldShow, + angleDegrees, + direction, + }; + }, [currentAngleRad]); // Don't render anything if not showing if (!shouldShow) { - return
; + return null; } return ( -
- {/* Floating bubble */} -
- +
+ {/* Central flashing indicator */} +
+ {/* Background indicator */} +
+ + {/* Direction arrow */} +
+ {direction === 'left' ? ( + + ) : ( + + )} +
+
+ {/* Centered text */} -
+
+ {shouldShow ? `${Math.abs(angleDegrees).toFixed(0)}°` : ''} +
); -} \ No newline at end of file +} diff --git a/src/frontend/theme.css b/src/frontend/theme.css index 63a9360..52954c8 100644 --- a/src/frontend/theme.css +++ b/src/frontend/theme.css @@ -23,6 +23,15 @@ layer(base); opacity: 1; } } + + @keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + } } html { From 00f8dc7637f17e8312eb9ff1cbf3330b708966ac Mon Sep 17 00:00:00 2001 From: tariknz Date: Sat, 2 Aug 2025 12:03:27 +1200 Subject: [PATCH 3/6] fix tests --- .../InputSteer/RotationIndicator.spec.tsx | 32 +++++++++++-------- .../Input/InputSteer/RotationIndicator.tsx | 4 +-- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/frontend/components/Input/InputSteer/RotationIndicator.spec.tsx b/src/frontend/components/Input/InputSteer/RotationIndicator.spec.tsx index 9452df4..55eaa08 100644 --- a/src/frontend/components/Input/InputSteer/RotationIndicator.spec.tsx +++ b/src/frontend/components/Input/InputSteer/RotationIndicator.spec.tsx @@ -3,29 +3,33 @@ import { describe, it, expect } from 'vitest'; import { RotationIndicator } from './RotationIndicator'; describe('RotationIndicator', () => { - it('should render with zero degrees', () => { - render(); + it('should not render when angle is within normal range', () => { + const { container } = render(); - expect(screen.getByText('0°')).toBeInTheDocument(); + expect(container.firstChild).toBeNull(); }); - it('should show correct angle in degrees', () => { - render(); + it('should not render when angle is at threshold', () => { + const { container } = render(); - expect(screen.getByText('-90°')).toBeInTheDocument(); + expect(container.firstChild).toBeNull(); }); - it('should show negative angle in degrees', () => { - render(); + it('should render when angle exceeds threshold clockwise', () => { + render(); - expect(screen.getByText('45°')).toBeInTheDocument(); + expect(screen.getByText('280°')).toBeInTheDocument(); }); - it('should apply custom className', () => { - const { container } = render( - - ); + it('should render when angle exceeds threshold counterclockwise', () => { + render(); - expect(container.firstChild).toHaveClass('custom-class'); + expect(screen.getByText('280°')).toBeInTheDocument(); + }); + + it('should render with extreme angle', () => { + render(); + + expect(screen.getByText('350°')).toBeInTheDocument(); }); }); \ No newline at end of file diff --git a/src/frontend/components/Input/InputSteer/RotationIndicator.tsx b/src/frontend/components/Input/InputSteer/RotationIndicator.tsx index 7ed457e..eb58544 100644 --- a/src/frontend/components/Input/InputSteer/RotationIndicator.tsx +++ b/src/frontend/components/Input/InputSteer/RotationIndicator.tsx @@ -17,8 +17,8 @@ export function RotationIndicator({ // Convert radians to degrees const angleDegrees = (currentAngleRad * -1 * 180) / Math.PI; - // Only show when beyond ±180 degrees - const shouldShow = Math.abs(angleDegrees) > 180; + // Only show when beyond ±270 degrees + const shouldShow = Math.abs(angleDegrees) > 270; if (!shouldShow) { return { From f38f8743dd834d3a3dbf3ad3626ee290c2c88665 Mon Sep 17 00:00:00 2001 From: tariknz Date: Sat, 2 Aug 2025 12:22:08 +1200 Subject: [PATCH 4/6] tidy up --- src/frontend/components/Input/InputSteer/InputSteer.tsx | 5 ++--- src/frontend/components/Settings/types.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/frontend/components/Input/InputSteer/InputSteer.tsx b/src/frontend/components/Input/InputSteer/InputSteer.tsx index 31ff79c..adb6878 100644 --- a/src/frontend/components/Input/InputSteer/InputSteer.tsx +++ b/src/frontend/components/Input/InputSteer/InputSteer.tsx @@ -66,12 +66,11 @@ export function InputSteer({ }, [angleRad]); return ( -
+
Date: Sat, 2 Aug 2025 13:22:58 +1200 Subject: [PATCH 5/6] remove unused type --- src/frontend/components/Settings/types.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/frontend/components/Settings/types.ts b/src/frontend/components/Settings/types.ts index 47498d8..e3a7111 100644 --- a/src/frontend/components/Settings/types.ts +++ b/src/frontend/components/Settings/types.ts @@ -66,8 +66,3 @@ export interface InputWidgetSettings extends BaseWidgetSettings { steer: SteerWidgetSettings; }; } - -/* eslint-disable @typescript-eslint/no-empty-object-type */ -export interface AdvancedSettings extends BaseWidgetSettings { - // Add specific advanced settings here -} From bafcbf688aff63fbdff27cf8f2cbe9bbe7dd8d21 Mon Sep 17 00:00:00 2001 From: tariknz Date: Sat, 2 Aug 2025 13:48:23 +1200 Subject: [PATCH 6/6] remove unused hook --- .../Input/InputSteer/hooks/index.ts | 2 - .../hooks/useWheelRotation.spec.tsx | 75 ------------------- .../InputSteer/hooks/useWheelRotation.tsx | 67 ----------------- 3 files changed, 144 deletions(-) delete mode 100644 src/frontend/components/Input/InputSteer/hooks/index.ts delete mode 100644 src/frontend/components/Input/InputSteer/hooks/useWheelRotation.spec.tsx delete mode 100644 src/frontend/components/Input/InputSteer/hooks/useWheelRotation.tsx diff --git a/src/frontend/components/Input/InputSteer/hooks/index.ts b/src/frontend/components/Input/InputSteer/hooks/index.ts deleted file mode 100644 index 9321cdf..0000000 --- a/src/frontend/components/Input/InputSteer/hooks/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useWheelRotation } from './useWheelRotation'; -export type { WheelRotationState } from './useWheelRotation'; \ No newline at end of file diff --git a/src/frontend/components/Input/InputSteer/hooks/useWheelRotation.spec.tsx b/src/frontend/components/Input/InputSteer/hooks/useWheelRotation.spec.tsx deleted file mode 100644 index ec571f9..0000000 --- a/src/frontend/components/Input/InputSteer/hooks/useWheelRotation.spec.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import { describe, it, expect } from 'vitest'; -import { useWheelRotation } from './useWheelRotation'; - -describe('useWheelRotation', () => { - it('should initialize with default values', () => { - const { result } = renderHook(() => useWheelRotation(0)); - - expect(result.current.totalRotations).toBe(0); - expect(result.current.hasRotatedOver360).toBe(false); - expect(result.current.rotationDirection).toBe('none'); - }); - - it('should track clockwise rotation', () => { - const { result, rerender } = renderHook((angle) => useWheelRotation(angle), { - initialProps: 0, - }); - - // Simulate clockwise rotation - rerender(Math.PI / 2); // 90 degrees - rerender(Math.PI); // 180 degrees - rerender(3 * Math.PI / 2); // 270 degrees - rerender(2 * Math.PI); // 360 degrees - - expect(result.current.totalRotations).toBe(1); - expect(result.current.hasRotatedOver360).toBe(true); - }); - - it('should track counterclockwise rotation', () => { - const { result, rerender } = renderHook((angle) => useWheelRotation(angle), { - initialProps: 0, - }); - - // Simulate counterclockwise rotation - rerender(-Math.PI / 2); // -90 degrees - rerender(-Math.PI); // -180 degrees - rerender(-3 * Math.PI / 2); // -270 degrees - rerender(-2 * Math.PI); // -360 degrees - - expect(result.current.totalRotations).toBe(1); - expect(result.current.hasRotatedOver360).toBe(true); - }); - - it('should handle multiple rotations', () => { - const { result, rerender } = renderHook((angle) => useWheelRotation(angle), { - initialProps: 0, - }); - - // Simulate 2.5 rotations clockwise - rerender(Math.PI / 2); - rerender(Math.PI); - rerender(3 * Math.PI / 2); - rerender(2 * Math.PI); - rerender(5 * Math.PI / 2); - rerender(3 * Math.PI); - rerender(7 * Math.PI / 2); - rerender(4 * Math.PI); - rerender(9 * Math.PI / 2); - - expect(result.current.totalRotations).toBe(2); - expect(result.current.hasRotatedOver360).toBe(true); - }); - - it('should handle angle wraparound', () => { - const { result, rerender } = renderHook((angle) => useWheelRotation(angle), { - initialProps: 0, - }); - - // Simulate rotation that crosses the ±π boundary - rerender(Math.PI - 0.1); - rerender(Math.PI + 0.1); // Crosses the boundary - - expect(result.current.rotationDirection).toBe('clockwise'); - }); -}); \ No newline at end of file diff --git a/src/frontend/components/Input/InputSteer/hooks/useWheelRotation.tsx b/src/frontend/components/Input/InputSteer/hooks/useWheelRotation.tsx deleted file mode 100644 index 5c2b14f..0000000 --- a/src/frontend/components/Input/InputSteer/hooks/useWheelRotation.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -export interface WheelRotationState { - totalRotations: number; - currentAngle: number; - rotationDirection: 'clockwise' | 'counterclockwise' | 'none'; - hasRotatedOver360: boolean; -} - -export const useWheelRotation = (angleRad = 0) => { - const [rotationState, setRotationState] = useState({ - totalRotations: 0, - currentAngle: 0, - rotationDirection: 'none', - hasRotatedOver360: false, - }); - - const prevAngleRef = useRef(0); - const totalRotationRef = useRef(0); - const lastDirectionRef = useRef<'clockwise' | 'counterclockwise' | 'none'>('none'); - - useEffect(() => { - const currentAngle = angleRad; - const prevAngle = prevAngleRef.current; - - if (prevAngle === 0 && currentAngle === 0) { - return; - } - - // Calculate the angle difference, handling wraparound - let angleDiff = currentAngle - prevAngle; - - // Handle wraparound at ±π - if (angleDiff > Math.PI) { - angleDiff -= 2 * Math.PI; - } else if (angleDiff < -Math.PI) { - angleDiff += 2 * Math.PI; - } - - // Determine rotation direction - let direction: 'clockwise' | 'counterclockwise' | 'none' = 'none'; - if (Math.abs(angleDiff) > 0.01) { // Threshold to avoid noise - direction = angleDiff > 0 ? 'clockwise' : 'counterclockwise'; - } - - // Update total rotation - if (direction !== 'none') { - totalRotationRef.current += angleDiff; - } - - // Calculate total rotations (360 degrees = 2π radians) - const totalRotations = Math.abs(totalRotationRef.current) / (2 * Math.PI); - const hasRotatedOver360 = totalRotations >= 1; - - setRotationState({ - totalRotations: Math.floor(totalRotations), - currentAngle, - rotationDirection: direction, - hasRotatedOver360, - }); - - prevAngleRef.current = currentAngle; - lastDirectionRef.current = direction; - }, [angleRad]); - - return rotationState; -}; \ No newline at end of file