mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
more tweaks
This commit is contained in:
@@ -12,7 +12,6 @@ import {
|
||||
import DriveDockAction, { useDriveDockState } from '../DriveDockAction/index.jsx';
|
||||
import NightVisionControl from '../NightVisionControl/index.jsx';
|
||||
import HornControl from '../HornControl/index.jsx';
|
||||
import CameraTiltControl from '../CameraTiltControl/index.jsx';
|
||||
import FloatingJoystick from './FloatingJoystick.jsx';
|
||||
import MobileAuxButton from './MobileAuxButton.jsx';
|
||||
import {
|
||||
@@ -37,6 +36,134 @@ function getSpeedModeConfig(speedMode) {
|
||||
return DRIVE_PAD_SPEED_MODES.find((mode) => mode.id === speedMode) || DRIVE_PAD_SPEED_MODES[1];
|
||||
}
|
||||
|
||||
function clampCameraAngle(value, min, max) {
|
||||
if (!Number.isFinite(value)) return min;
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function formatCameraAngle(value) {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) return '—';
|
||||
return `${value > 0 ? '+' : ''}${value.toFixed(1)}°`;
|
||||
}
|
||||
|
||||
function MobileCameraTiltSlider({
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step = 0.5,
|
||||
disabled = false,
|
||||
onChange,
|
||||
}) {
|
||||
const trackRef = useRef(null);
|
||||
const pointerIdRef = useRef(null);
|
||||
|
||||
const valuePercent = useMemo(() => {
|
||||
if (max === min) return 50;
|
||||
return ((clampCameraAngle(value, min, max) - min) / (max - min)) * 100;
|
||||
}, [max, min, value]);
|
||||
const disabledClass = disabled ? 'opacity-50' : '';
|
||||
|
||||
const valueFromPointer = useCallback(
|
||||
(event) => {
|
||||
const track = trackRef.current;
|
||||
if (!track) return value;
|
||||
const rect = track.getBoundingClientRect();
|
||||
const usableHeight = Math.max(1, rect.height);
|
||||
const rawPercent = 1 - (event.clientY - rect.top) / usableHeight;
|
||||
const unclamped = min + clampCameraAngle(rawPercent, 0, 1) * (max - min);
|
||||
const stepped = Math.round(unclamped / step) * step;
|
||||
return clampCameraAngle(stepped, min, max);
|
||||
},
|
||||
[max, min, step, value],
|
||||
);
|
||||
|
||||
const sendPointerValue = useCallback(
|
||||
(event) => {
|
||||
const next = valueFromPointer(event);
|
||||
onChange?.(next);
|
||||
},
|
||||
[onChange, valueFromPointer],
|
||||
);
|
||||
|
||||
const handlePointerDown = useCallback(
|
||||
(event) => {
|
||||
if (disabled) return;
|
||||
if (pointerIdRef.current !== null) return;
|
||||
|
||||
/*
|
||||
Native vertical range inputs are unreliable as a second simultaneous
|
||||
touch on mobile Safari while the drive pad owns another active pointer.
|
||||
This custom track captures only the camera finger, so the drive thumb can
|
||||
continue moving without stealing or cancelling camera tilt updates.
|
||||
*/
|
||||
event.preventDefault();
|
||||
pointerIdRef.current = event.pointerId;
|
||||
trackRef.current?.setPointerCapture?.(event.pointerId);
|
||||
sendPointerValue(event);
|
||||
},
|
||||
[disabled, sendPointerValue],
|
||||
);
|
||||
|
||||
const handlePointerMove = useCallback(
|
||||
(event) => {
|
||||
if (pointerIdRef.current !== event.pointerId) return;
|
||||
event.preventDefault();
|
||||
sendPointerValue(event);
|
||||
},
|
||||
[sendPointerValue],
|
||||
);
|
||||
|
||||
const handlePointerEnd = useCallback((event) => {
|
||||
if (pointerIdRef.current !== event.pointerId) return;
|
||||
event.preventDefault();
|
||||
pointerIdRef.current = null;
|
||||
trackRef.current?.releasePointerCapture?.(event.pointerId);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mobile-touch-control flex h-full items-center justify-center gap-0.5">
|
||||
<span className="mobile-touch-control text-sm font-semibold text-emerald-50 [writing-mode:vertical-rl] rotate-180">
|
||||
Camera tilt
|
||||
</span>
|
||||
<div
|
||||
ref={trackRef}
|
||||
role="slider"
|
||||
aria-label="Camera tilt"
|
||||
aria-valuemin={min}
|
||||
aria-valuemax={max}
|
||||
aria-valuenow={Number.isFinite(value) ? value : min}
|
||||
aria-valuetext={formatCameraAngle(value)}
|
||||
aria-disabled={disabled}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerEnd}
|
||||
onPointerCancel={handlePointerEnd}
|
||||
onLostPointerCapture={(event) => {
|
||||
if (pointerIdRef.current === event.pointerId) pointerIdRef.current = null;
|
||||
}}
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
/*
|
||||
touchAction stays inline as a second line of defense because this is the
|
||||
element that must keep browser panning/zooming out of the multi-touch
|
||||
camera gesture.
|
||||
*/
|
||||
style={{ touchAction: 'none' }}
|
||||
className={`mobile-touch-control mobile-drag-control relative h-full w-6 rounded-full border border-emerald-100/80 bg-emerald-950 shadow-inner focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-200 ${disabledClass}`.trim()}
|
||||
>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-1 bottom-1 rounded-full bg-emerald-400"
|
||||
style={{ height: `${valuePercent}%` }}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute left-1/2 h-3.5 w-3.5 -translate-x-1/2 rounded-full border border-emerald-950 bg-emerald-200 shadow"
|
||||
style={{ bottom: `clamp(0.25rem, calc(${valuePercent}% - 0.4375rem), calc(100% - 1.125rem))` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileJoystickPanel({ layout }) {
|
||||
const {
|
||||
state: { roverId, keymap: rawKeymap },
|
||||
@@ -297,23 +424,13 @@ function MobileActionsColumnContent() {
|
||||
// Match the desktop camera tilt card's emerald styling so the vertical
|
||||
// mobile control reads as the same feature in a phone-sized layout.
|
||||
<div className="mobile-touch-control flex-1 min-h-0 rounded-xl border-2 border-emerald-300/70 bg-emerald-900 px-1 py-1 text-emerald-50">
|
||||
<CameraTiltControl
|
||||
<MobileCameraTiltSlider
|
||||
value={cameraValue}
|
||||
min={cameraMin}
|
||||
max={cameraMax}
|
||||
step={0.5}
|
||||
disabled={cameraDisabled}
|
||||
onChange={setServoAngle}
|
||||
orientation="vertical"
|
||||
label="Camera tilt"
|
||||
labelClass="mobile-touch-control text-sm font-semibold text-emerald-50 [writing-mode:vertical-rl] rotate-180"
|
||||
labelRowClass="mobile-touch-control text-[0.7rem] text-emerald-100"
|
||||
valueClass="font-mono text-slate-100"
|
||||
className="h-full gap-0"
|
||||
sliderClass="mobile-touch-control mobile-drag-control h-full w-7"
|
||||
accentClass="accent-emerald-400"
|
||||
showEndpoints={false}
|
||||
showValue={false}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user