hapticks 3 3 3 3 3 3

This commit is contained in:
legop3
2026-07-21 18:38:01 -04:00
parent 358aa0b1d6
commit fb9565faf9
19 changed files with 155 additions and 26 deletions
@@ -11,6 +11,7 @@ import {
} from '../../controls/inputs/driveIntent.js';
import { useSettingsNamespace } from '../../settings/index.js';
import { INPUT_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
import { triggerTouchHaptic } from '../../lib/touchHaptics.js';
import FloatingJoystick from './FloatingJoystick.jsx';
import {
DRIVE_PAD_REPEAT_MS,
@@ -134,8 +135,12 @@ export default function ControlPadPanel({ compact = false, disabled = false }) {
const handleSpeedModeChange = useCallback(
(nextMode) => {
if (nextMode === speedModeRef.current) return;
setSpeedMode(nextMode);
speedModeRef.current = nextMode;
// Confirm the accepted mode transition here so tapping an already-active
// mode cannot produce feedback for a state change that did not happen.
triggerTouchHaptic('button');
setCameraPrecisionMode(nextMode === 'precision');
if (activeCellRef.current) {
sendDriveCell(activeCellRef.current, 'speed', nextMode);
@@ -3,6 +3,7 @@
// Scope: Owns pointer tracking, fixed overlay positioning, and active 3x3 drive-zone feedback.
import { useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { triggerTouchHaptic } from '../../lib/touchHaptics.js';
const PAD_MARGIN = 12;
const CELL_COUNT = 3;
@@ -146,6 +147,10 @@ export default function FloatingJoystick({
const cell = cellFromPointer(currentPad, event.clientX, event.clientY);
if (activeCellIdRef.current !== cell.id) {
activeCellIdRef.current = cell.id;
// Pulse only when the thumb crosses a real 3x3 cell boundary so
// continuous pointermove traffic cannot buzz constantly. The shared
// pointerup listener provides a Safari-compatible gesture-end pulse.
triggerTouchHaptic('drive');
onCellChange?.(cell);
}
setActivePad({
@@ -1,6 +1,8 @@
// Mobile Aux Button
// Purpose: Defines the Mobile Aux Button module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { triggerTouchHaptic } from '../../lib/touchHaptics.js';
export default function MobileAuxButton({ id, label, values, color, disabled, onPress, onRelease }) {
return (
<button
@@ -10,7 +12,12 @@ export default function MobileAuxButton({ id, label, values, color, disabled, on
event.preventDefault();
onPress(id, values);
}}
onPointerUp={() => onRelease(id)}
onPointerUp={() => {
onRelease(id);
// A completed held action is the semantic confirmation point. Keeping
// feedback beside release avoids buzzing for cancelled aux gestures.
triggerTouchHaptic('button');
}}
onPointerLeave={() => onRelease(id)}
onPointerCancel={() => onRelease(id)}
onContextMenu={(event) => event.preventDefault()}
@@ -2,6 +2,9 @@
// Purpose: Provides the mobile-only camera tilt slider that supports simultaneous two-finger mobile driving.
// Scope: Owns pointer tracking and visual slider state for the compact vertical mobile camera control.
import { useCallback, useMemo, useRef } from 'react';
import { triggerTouchHaptic } from '../../lib/touchHaptics.js';
const HAPTIC_MIN_ANGLE_DELTA_DEGREES = 2;
function clampCameraAngle(value, min, max) {
if (!Number.isFinite(value)) return min;
@@ -23,6 +26,7 @@ export default function VerticalCameraTilt({
}) {
const trackRef = useRef(null);
const pointerIdRef = useRef(null);
const lastHapticValueRef = useRef(null);
const valuePercent = useMemo(() => {
if (max === min) return 50;
@@ -47,6 +51,17 @@ export default function VerticalCameraTilt({
const sendPointerValue = useCallback(
(event) => {
const next = valueFromPointer(event);
/*
Camera commands must retain the configured 0.5 or 0.1 degree precision,
but physical feedback does not need to mirror every servo step. Require
two degrees of accumulated travel so the ticks describe slider position
without allowing pointer event frequency to control their cadence.
*/
if (Math.abs(next - lastHapticValueRef.current) >= HAPTIC_MIN_ANGLE_DELTA_DEGREES) {
triggerTouchHaptic('camera');
lastHapticValueRef.current = next;
}
onChange?.(next);
},
[onChange, valueFromPointer],
@@ -65,10 +80,13 @@ export default function VerticalCameraTilt({
*/
event.preventDefault();
pointerIdRef.current = event.pointerId;
// Seed the slider value so its first position update is not mistaken for
// two degrees of travel before the finger has actually moved that far.
lastHapticValueRef.current = value;
trackRef.current?.setPointerCapture?.(event.pointerId);
sendPointerValue(event);
},
[disabled, sendPointerValue],
[disabled, sendPointerValue, value],
);
const handlePointerMove = useCallback(