Optimise re-renders (#1355)

Co-authored-by: Uriel <imurx@proton.me>
This commit is contained in:
lucas lelievre
2025-04-23 14:58:19 +02:00
committed by GitHub
parent 0dc073ca48
commit ca8d75e749
36 changed files with 346 additions and 297 deletions

View File

@@ -9,6 +9,7 @@
"@fontsource/poppins": "^5.1.0",
"@formatjs/intl-localematcher": "^0.2.32",
"@hookform/resolvers": "^3.6.0",
"@react-hookz/deep-equal": "^3.0.3",
"@react-three/drei": "^9.114.3",
"@react-three/fiber": "^8.17.10",
"@sentry/react": "^9.9.0",
@@ -26,6 +27,7 @@
"flatbuffers": "22.10.26",
"intl-pluralrules": "^2.0.1",
"ip-num": "^1.5.1",
"jotai": "^2.12.2",
"prompts": "^2.4.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",

View File

@@ -3,17 +3,17 @@ import { ClearMountingResetRequestT, RpcMessage } from 'solarxr-protocol';
import { useWebsocketAPI } from '@/hooks/websocket-api';
import { BigButton } from './commons/BigButton';
import { TrashIcon } from './commons/icon/TrashIcon';
import { useTrackers } from '@/hooks/tracker';
import { Quaternion } from 'three';
import { QuaternionFromQuatT, similarQuaternions } from '@/maths/quaternion';
import { useMemo } from 'react';
import { useAtomValue } from 'jotai';
import { assignedTrackersAtom } from '@/store/app-store';
const _q = new Quaternion();
export function ClearMountingButton() {
const { sendRPCPacket } = useWebsocketAPI();
const { useAssignedTrackers } = useTrackers();
const assignedTrackers = useAssignedTrackers();
const assignedTrackers = useAtomValue(assignedTrackersAtom);
const trackerWithMounting = useMemo(
() =>

View File

@@ -21,7 +21,6 @@ import { QuestionIcon } from './commons/icon/QuestionIcon';
import { useBreakpoint, useIsTauri } from '@/hooks/breakpoint';
import { GearIcon } from './commons/icon/GearIcon';
import { invoke } from '@tauri-apps/api/core';
import { useTrackers } from '@/hooks/tracker';
import { TrackersStillOnModal } from './TrackersStillOnModal';
import { useConfig } from '@/hooks/config';
import { listen, TauriEvent } from '@tauri-apps/api/event';
@@ -35,6 +34,8 @@ import {
getCurrentWindow,
UserAttentionType,
} from '@tauri-apps/api/window';
import { useAtomValue } from 'jotai';
import { connectedIMUTrackersAtom } from '@/store/app-store';
export function VersionTag() {
return (
@@ -63,8 +64,7 @@ export function TopBar({
const isTauri = useIsTauri();
const { isMobile } = useBreakpoint('mobile');
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
const { useConnectedIMUTrackers } = useTrackers();
const connectedIMUTrackers = useConnectedIMUTrackers();
const connectedIMUTrackers = useAtomValue(connectedIMUTrackersAtom);
const { config, setConfig, saveConfig } = useConfig();
const version = useContext(VersionContext);
const [localIp, setLocalIp] = useState<string | null>(null);

View File

@@ -11,13 +11,14 @@ import {
UnknownDeviceHandshakeNotificationT,
} from 'solarxr-protocol';
import { useDebouncedEffect } from '@/hooks/timeout';
import { useAppContext } from '@/hooks/app';
import { useAtom } from 'jotai';
import { ignoredTrackersAtom } from '@/store/app-store';
export function UnknownDeviceModal() {
const { l10n } = useLocalization();
const [open, setOpen] = useState(0);
const { pathname } = useLocation();
const { state, dispatch } = useAppContext();
const [ignoredTrackers, setIgnoredTracker] = useAtom(ignoredTrackersAtom);
const [currentTracker, setCurrentTracker] = useState<string | null>(null);
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
@@ -28,7 +29,7 @@ export function UnknownDeviceModal() {
['/onboarding/connect-trackers', '/settings/firmware-tool'].includes(
pathname
) ||
state.ignoredTrackers.has(macAddress as string) ||
(macAddress && ignoredTrackers.has(macAddress?.toString())) ||
(currentTracker !== null && currentTracker !== macAddress)
)
return;
@@ -91,9 +92,10 @@ export function UnknownDeviceModal() {
<Button
variant="tertiary"
onClick={() => {
dispatch({
type: 'ignoreTracker',
value: currentTracker as string,
setIgnoredTracker((state) => {
if (!currentTracker) throw 'should have a tracker';
state.add(currentTracker);
return state;
});
closeModal();
}}

View File

@@ -17,23 +17,42 @@ import {
import { useEffect, useMemo, useState } from 'react';
import { parseStatusToLocale, useStatusContext } from '@/hooks/status-system';
import { useWebsocketAPI } from '@/hooks/websocket-api';
import { useAppContext } from '@/hooks/app';
import { ClearMountingButton } from './ClearMountingButton';
import { ToggleableSkeletonVisualizerWidget } from './widgets/SkeletonVisualizerWidget';
import { useAtomValue } from 'jotai';
import { flatTrackersAtom } from '@/store/app-store';
function UnprioritizedStatuses() {
const { l10n } = useLocalization();
const trackers = useAtomValue(flatTrackersAtom);
const { statuses } = useStatusContext();
const unprioritizedStatuses = useMemo(
() => Object.values(statuses).filter((status) => !status.prioritized),
[statuses]
);
return (
<div className="w-full flex flex-col gap-3 mb-2">
{unprioritizedStatuses.map((status) => (
<Localized
id={`status_system-${StatusData[status.dataType]}`}
vars={parseStatusToLocale(status, trackers, l10n)}
key={status.id}
>
<TipBox whitespace={false} hideIcon>
{`Warning, you should fix ${StatusData[status.dataType]}`}
</TipBox>
</Localized>
))}
</div>
);
}
export function WidgetsComponent() {
const { config } = useConfig();
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
const [driftCompensationEnabled, setDriftCompensationEnabled] =
useState(false);
const { trackers } = useAppContext();
const { statuses } = useStatusContext();
const { l10n } = useLocalization();
const unprioritizedStatuses = useMemo(
() => Object.values(statuses).filter((status) => !status.prioritized),
[statuses]
);
useEffect(() => {
sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT());
}, []);
@@ -62,19 +81,7 @@ export function WidgetsComponent() {
<div className="mb-2">
<ToggleableSkeletonVisualizerWidget height={400} />
</div>
<div className="w-full flex flex-col gap-3 mb-2">
{unprioritizedStatuses.map((status) => (
<Localized
id={`status_system-${StatusData[status.dataType]}`}
vars={parseStatusToLocale(status, trackers, l10n)}
key={status.id}
>
<TipBox whitespace={false} hideIcon={true}>
{`Warning, you should fix ${StatusData[status.dataType]}`}
</TipBox>
</Localized>
))}
</div>
<UnprioritizedStatuses></UnprioritizedStatuses>
{config?.debug && (
<div className="w-full">
<DeveloperModeWidget></DeveloperModeWidget>

View File

@@ -8,9 +8,9 @@ import {
useState,
} from 'react';
import { BodyPart, TrackerDataT } from 'solarxr-protocol';
import { FlatDeviceTracker } from '@/hooks/app';
import { useTracker } from '@/hooks/tracker';
import { PersonFrontIcon } from './PersonFrontIcon';
import { FlatDeviceTracker } from '@/store/app-store';
interface SlotDot {
id: string;

View File

@@ -23,13 +23,14 @@ import {
TrackerStatus,
} from 'solarxr-protocol';
import { Button } from '@/components/commons/Button';
import { useAppContext } from '@/hooks/app';
import { Input } from '@/components/commons/Input';
import { Dropdown } from '@/components/commons/Dropdown';
import { useOnboarding } from '@/hooks/onboarding';
import { DeviceCardControl } from './DeviceCard';
import { getTrackerName } from '@/hooks/tracker';
import { ObjectSchema, object, string } from 'yup';
import { useAtomValue } from 'jotai';
import { devicesAtom } from '@/store/app-store';
interface FlashingMethodForm {
flashingMethod?: string;
@@ -191,10 +192,10 @@ function OTADevicesList({
}) {
const { l10n } = useLocalization();
const { selectDevices, newConfig } = useFirmwareTool();
const { state } = useAppContext();
const allDevices = useAtomValue(devicesAtom);
const devices =
state.datafeed?.devices.filter(({ trackers, hardwareInfo }) => {
allDevices.filter(({ trackers, hardwareInfo }) => {
// We make sure the device is not one of these types
if (
hardwareInfo?.officialBoardType === BoardType.SLIMEVR_LEGACY ||

View File

@@ -33,6 +33,8 @@ import { yupResolver } from '@hookform/resolvers/yup';
import { object } from 'yup';
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
import { A } from '@/components/commons/A';
import { useAtomValue } from 'jotai';
import { devicesAtom } from '@/store/app-store';
export function checkForUpdate(
currentFirmwareRelease: FirmwareRelease,
@@ -104,10 +106,9 @@ const StatusList = ({ status }: { status: Record<string, UpdateStatus> }) => {
const val = status[id];
if (!val) throw new Error('there should always be a val');
const { state } = useAppContext();
const device = state.datafeed?.devices.find(
({ id: dId }) => id === dId?.id.toString()
);
const devices = useAtomValue(devicesAtom);
const device = devices.find(({ id: dId }) => id === dId?.id.toString());
return (
<DeviceCardControl
@@ -132,11 +133,13 @@ export function FirmwareUpdate() {
const { l10n } = useLocalization();
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
const pendingDevicesRef = useRef<SelectedDevice[]>([]);
const { state, currentFirmwareRelease } = useAppContext();
const { currentFirmwareRelease } = useAppContext();
const [status, setStatus] = useState<Record<string, UpdateStatus>>({});
const allDevices = useAtomValue(devicesAtom);
const devices =
state.datafeed?.devices.filter(
allDevices.filter(
(device) =>
device.trackers.length > 0 &&
currentFirmwareRelease &&

View File

@@ -2,7 +2,6 @@ import { Localized, useLocalization } from '@fluent/react';
import { Link, NavLink, useNavigate } from 'react-router-dom';
import { StatusData, TrackerDataT } from 'solarxr-protocol';
import { useConfig } from '@/hooks/config';
import { useTrackers } from '@/hooks/tracker';
import { Typography } from '@/components/commons/Typography';
import { TrackerCard } from '@/components/tracker/TrackerCard';
import { TrackersTable } from '@/components/tracker/TrackersTable';
@@ -15,6 +14,8 @@ import { useMemo } from 'react';
import { WarningBox } from '@/components/commons/TipBox';
import { HeadsetIcon } from '@/components/commons/icon/HeadsetIcon';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import { flatTrackersAtom } from '@/store/app-store';
import { useVRCConfig } from '@/hooks/vrc-config';
const DONT_REPEAT_STATUSES = [StatusData.StatusTrackerReset];
@@ -22,7 +23,7 @@ const DONT_REPEAT_STATUSES = [StatusData.StatusTrackerReset];
export function Home() {
const { l10n } = useLocalization();
const { config } = useConfig();
const { trackers } = useTrackers();
const trackers = useAtomValue(flatTrackersAtom);
const { statuses } = useStatusContext();
const { invalidConfig } = useVRCConfig();
const navigate = useNavigate();

View File

@@ -1,12 +1,12 @@
import { useCallback, useMemo } from 'react';
import { BodyPart } from 'solarxr-protocol';
import { FlatDeviceTracker } from '@/hooks/app';
import { AssignMode } from '@/hooks/config';
import { useTrackers } from '@/hooks/tracker';
import { BodyInteractions } from '@/components/commons/BodyInteractions';
import { TrackerPartCard } from '@/components/tracker/TrackerPartCard';
import { BodyPartError } from './pages/trackers-assign/TrackerAssignment';
import { SIDES } from '@/components/commons/PersonFrontIcon';
import { useAtomValue } from 'jotai';
import { assignedTrackersAtom, FlatDeviceTracker } from '@/store/app-store';
export const ARMS_PARTS = new Set([
BodyPart.LEFT_UPPER_ARM,
@@ -112,9 +112,7 @@ export function BodyAssignment({
width?: number;
dotSize?: number;
}) {
const { useAssignedTrackers } = useTrackers();
const assignedTrackers = useAssignedTrackers();
const assignedTrackers = useAtomValue(assignedTrackersAtom);
const trackerPartGrouped = useMemo(
() =>

View File

@@ -8,11 +8,12 @@ import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
import { useCountdown } from '@/hooks/countdown';
import classNames from 'classnames';
import { TaybolIcon } from '@/components/commons/icon/TaybolIcon';
import { useTrackers } from '@/hooks/tracker';
import { useRestCalibrationTrackers } from '@/hooks/imu-logic';
import { averageVector, Vector3FromVec3fT } from '@/maths/vector3';
import { Vector3 } from 'three';
import { useTimeout } from '@/hooks/timeout';
import { useAtomValue } from 'jotai';
import { connectedIMUTrackersAtom } from '@/store/app-store';
export enum CalibrationStatus {
SUCCESS,
@@ -36,8 +37,7 @@ export function CalibrationTutorialPage() {
onCountdownEnd: () => setCalibrationStatus(CalibrationStatus.SUCCESS),
});
useTimeout(() => setSkipButton(true), 10000);
const { useConnectedIMUTrackers } = useTrackers();
const connectedIMUTrackers = useConnectedIMUTrackers();
const connectedIMUTrackers = useAtomValue(connectedIMUTrackersAtom);
const restCalibrationTrackers =
useRestCalibrationTrackers(connectedIMUTrackers);
const [rested, setRested] = useState(false);

View File

@@ -10,7 +10,6 @@ import {
WifiProvisioningStatusResponseT,
} from 'solarxr-protocol';
import { useOnboarding } from '@/hooks/onboarding';
import { useTrackers } from '@/hooks/tracker';
import { useWebsocketAPI } from '@/hooks/websocket-api';
import { ArrowLink } from '@/components/commons/ArrowLink';
import { Button } from '@/components/commons/Button';
@@ -21,6 +20,8 @@ import { Typography } from '@/components/commons/Typography';
import { TrackerCard } from '@/components/tracker/TrackerCard';
import { useIsRestCalibrationTrackers } from '@/hooks/imu-logic';
import './ConnectTracker.scss';
import { useAtomValue } from 'jotai';
import { connectedIMUTrackersAtom } from '@/store/app-store';
const statusLabelMap = {
[WifiProvisioningStatus.NONE]:
@@ -57,7 +58,7 @@ const statusProgressMap = {
export function ConnectTrackersPage() {
const { l10n } = useLocalization();
const { useConnectedIMUTrackers } = useTrackers();
const connectedIMUTrackers = useAtomValue(connectedIMUTrackersAtom);
const { applyProgress, state } = useOnboarding();
const navigate = useNavigate();
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
@@ -66,8 +67,6 @@ export function ConnectTrackersPage() {
applyProgress(0.4);
const connectedIMUTrackers = useConnectedIMUTrackers();
const bnoExists = useIsRestCalibrationTrackers(connectedIMUTrackers);
useEffect(() => {

View File

@@ -12,24 +12,24 @@ import {
SettingsRequestT,
SettingsResponseT,
} from 'solarxr-protocol';
import { useTrackers } from '@/hooks/tracker';
import { BodyDisplay } from '@/components/commons/BodyDisplay';
import { useWebsocketAPI } from '@/hooks/websocket-api';
import classNames from 'classnames';
import { useBreakpoint } from '@/hooks/breakpoint';
import { log } from '@/utils/logging';
import { useAtomValue } from 'jotai';
import { assignedTrackersAtom } from '@/store/app-store';
export function ResetTutorialPage() {
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
const { applyProgress } = useOnboarding();
const { useAssignedTrackers } = useTrackers();
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
const [curIndex, setCurIndex] = useState(0);
const [tapSettings, setTapSettings] = useState<number[]>([]);
applyProgress(0.8);
const assignedTrackers = useAssignedTrackers();
const assignedTrackers = useAtomValue(assignedTrackersAtom);
const highestTorsoTracker = useMemo(
() =>

View File

@@ -5,15 +5,15 @@ import { Button } from '@/components/commons/Button';
import { Input } from '@/components/commons/Input';
import { Typography } from '@/components/commons/Typography';
import classNames from 'classnames';
import { useTrackers } from '@/hooks/tracker';
import { useIsRestCalibrationTrackers } from '@/hooks/imu-logic';
import { useAtomValue } from 'jotai';
import { connectedIMUTrackersAtom } from '@/store/app-store';
export function WifiCredsPage() {
const { l10n } = useLocalization();
const { applyProgress, state } = useOnboarding();
const { control, handleSubmit, submitWifiCreds, formState } = useWifiForm();
const { useConnectedIMUTrackers } = useTrackers();
const connectedIMUTrackers = useConnectedIMUTrackers();
const connectedIMUTrackers = useAtomValue(connectedIMUTrackersAtom);
applyProgress(0.2);

View File

@@ -2,17 +2,17 @@ import { useLocalization } from '@fluent/react';
import { useOnboarding } from '@/hooks/onboarding';
import { Button } from '@/components/commons/Button';
import { Typography } from '@/components/commons/Typography';
import { useTrackers } from '@/hooks/tracker';
import { useIsRestCalibrationTrackers } from '@/hooks/imu-logic';
import { StickerSlime } from './StickerSlime';
import { TrackerArrow } from './TrackerArrow';
import { ExtensionArrow } from './ExtensionArrow';
import { useAtomValue } from 'jotai';
import { connectedIMUTrackersAtom } from '@/store/app-store';
export function AssignmentTutorialPage() {
const { l10n } = useLocalization();
const { applyProgress } = useOnboarding();
const { useConnectedIMUTrackers } = useTrackers();
const connectedIMUTrackers = useConnectedIMUTrackers();
const connectedIMUTrackers = useAtomValue(connectedIMUTrackersAtom);
const isRestCalibration = useIsRestCalibrationTrackers(connectedIMUTrackers);
applyProgress(0.46);

View File

@@ -22,9 +22,10 @@ import { save } from '@tauri-apps/plugin-dialog';
import { writeTextFile } from '@tauri-apps/plugin-fs';
import { error } from '@/utils/logging';
import classNames from 'classnames';
import { useAppContext } from '@/hooks/app';
import { RulerIcon } from '@/components/commons/icon/RulerIcon';
import { Tooltip } from '@/components/commons/Tooltip';
import { useAtomValue } from 'jotai';
import { computedTrackersAtom } from '@/store/app-store';
import { RulerIcon } from '@/components/commons/icon/RulerIcon';
import { PercentIcon } from '@/components/commons/icon/PercentIcon';
import { UploadFileIcon } from '@/components/commons/icon/UploadFileIcon';
import { FullResetIcon } from '@/components/commons/icon/ResetIcon';
@@ -320,7 +321,7 @@ function PreciseToggle({ control }: { control: ManualProportionControls }) {
function ButtonsControl({ control }: { control: ManualProportionControls }) {
const { state } = useOnboarding();
const nav = useNavigate();
const { computedTrackers } = useAppContext();
const computedTrackers = useAtomValue(computedTrackersAtom);
const { sendRPCPacket } = useWebsocketAPI();
const [showWarning, setShowWarning] = useState(false);

View File

@@ -8,19 +8,20 @@ import { CheckFloorHeightStep } from './autobone-steps/CheckFloorHeight';
import { ResetProportionsStep } from './scaled-steps/ResetProportions';
import { DoneStep } from './scaled-steps/Done';
import { useNavigate } from 'react-router-dom';
import { useMemo } from 'react';
import { ManualHeightStep } from './scaled-steps/ManualHeightStep';
import { useTrackers } from '@/hooks/tracker';
import { BodyPart } from 'solarxr-protocol';
import { Button } from '@/components/commons/Button';
import { WarningBox } from '@/components/commons/TipBox';
import { useMemo } from 'react';
import { useAtomValue } from 'jotai';
import { flatTrackersAtom } from '@/store/app-store';
import { BodyPart } from 'solarxr-protocol';
export function ScaledProportionsPage() {
const { l10n } = useLocalization();
const { applyProgress, state } = useOnboarding();
const heightContext = useProvideHeightContext();
const navigate = useNavigate();
const { trackers } = useTrackers();
const trackers = useAtomValue(flatTrackersAtom);
const { hasHmd, hasHandControllers } = useMemo(() => {
const hasHmd = trackers.some(

View File

@@ -1,10 +1,11 @@
import { useBreakpoint } from '@/hooks/breakpoint';
import { useTrackers } from '@/hooks/tracker';
import { BodyDisplay } from '@/components/commons/BodyDisplay';
import { Button } from '@/components/commons/Button';
import { TipBox } from '@/components/commons/TipBox';
import { Typography } from '@/components/commons/Typography';
import { useLocalization } from '@fluent/react';
import { useAtomValue } from 'jotai';
import { flatTrackersAtom } from '@/store/app-store';
export function PutTrackersOnStep({
nextStep,
@@ -15,7 +16,7 @@ export function PutTrackersOnStep({
}) {
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
const { trackers } = useTrackers();
const trackers = useAtomValue(flatTrackersAtom);
return (
<>

View File

@@ -1,8 +1,6 @@
import { useCallback, useMemo, useState } from 'react';
import { AssignTrackerRequestT, BodyPart, RpcMessage } from 'solarxr-protocol';
import { FlatDeviceTracker } from '@/hooks/app';
import { useOnboarding } from '@/hooks/onboarding';
import { useTrackers } from '@/hooks/tracker';
import { useWebsocketAPI } from '@/hooks/websocket-api';
import {
MountingOrientationDegreesToQuatT,
@@ -18,6 +16,8 @@ import { useLocalization } from '@fluent/react';
import { useBreakpoint } from '@/hooks/breakpoint';
import { Quaternion } from 'three';
import { AssignMode, defaultConfig, useConfig } from '@/hooks/config';
import { assignedTrackersAtom, FlatDeviceTracker } from '@/store/app-store';
import { useAtomValue } from 'jotai';
export function ManualMountingPage() {
const { isMobile } = useBreakpoint('mobile');
@@ -30,8 +30,7 @@ export function ManualMountingPage() {
applyProgress(0.7);
const { useAssignedTrackers } = useTrackers();
const assignedTrackers = useAssignedTrackers();
const assignedTrackers = useAtomValue(assignedTrackersAtom);
const trackerPartGrouped = useMemo(
() =>

View File

@@ -1,10 +1,11 @@
import { useBreakpoint } from '@/hooks/breakpoint';
import { useTrackers } from '@/hooks/tracker';
import { BodyDisplay } from '@/components/commons/BodyDisplay';
import { Button } from '@/components/commons/Button';
import { TipBox } from '@/components/commons/TipBox';
import { Typography } from '@/components/commons/Typography';
import { useLocalization } from '@fluent/react';
import { useAtomValue } from 'jotai';
import { flatTrackersAtom } from '@/store/app-store';
export function PutTrackersOnStep({
nextStep,
@@ -14,7 +15,7 @@ export function PutTrackersOnStep({
variant: 'alone' | 'onboarding';
}) {
const { isMobile } = useBreakpoint('mobile');
const { trackers } = useTrackers();
const trackers = useAtomValue(flatTrackersAtom);
const { l10n } = useLocalization();
return (

View File

@@ -3,10 +3,11 @@ import { Typography } from '@/components/commons/Typography';
import { AssignMode, defaultConfig, useConfig } from '@/hooks/config';
import { ASSIGNMENT_MODES } from '@/components/onboarding/BodyAssignment';
import { useLocalization } from '@fluent/react';
import { useTrackers } from '@/hooks/tracker';
import { useForm } from 'react-hook-form';
import { useEffect } from 'react';
import { Dropdown } from '@/components/commons/Dropdown';
import { useAtomValue } from 'jotai';
import { connectedIMUTrackersAtom } from '@/store/app-store';
// Ordered collection of assign modes with the number of IMU trackers
const ASSIGN_MODE_OPTIONS = [
@@ -26,8 +27,7 @@ export function TrackerAssignOptions({
variant: 'radio' | 'dropdown';
}) {
const { l10n } = useLocalization();
const { useConnectedIMUTrackers } = useTrackers();
const connectedIMUTrackers = useConnectedIMUTrackers().length;
const connectedIMUTrackers = useAtomValue(connectedIMUTrackersAtom);
const { config, setConfig } = useConfig();
const { control, watch, setValue } = useForm<{
@@ -44,11 +44,11 @@ export function TrackerAssignOptions({
}, [assignMode]);
useEffect(() => {
if (connectedIMUTrackers <= ASSIGN_MODE_OPTIONS[assignMode]) return;
if (connectedIMUTrackers.length <= ASSIGN_MODE_OPTIONS[assignMode]) return;
const selectedAssignMode =
(Object.entries(ASSIGN_MODE_OPTIONS).find(
([_, count]) => count >= connectedIMUTrackers
([_, count]) => count >= connectedIMUTrackers.length
)?.[0] as AssignMode) ?? AssignMode.All;
if (assignMode !== selectedAssignMode) {
@@ -114,7 +114,9 @@ export function TrackerAssignOptions({
name="assignMode"
control={control}
value={mode}
disabled={connectedIMUTrackers > trackersCount && mode !== AssignMode.All}
disabled={
connectedIMUTrackers.length > trackersCount && mode !== AssignMode.All
}
className="hidden"
>
<div className="flex flex-row md:gap-4 gap-2">

View File

@@ -14,10 +14,8 @@ import {
ChangeSettingsRequestT,
TapDetectionSetupNotificationT,
} from 'solarxr-protocol';
import { FlatDeviceTracker } from '@/hooks/app';
import { useChokerWarning } from '@/hooks/choker-warning';
import { useOnboarding } from '@/hooks/onboarding';
import { useTrackers } from '@/hooks/tracker';
import { useWebsocketAPI } from '@/hooks/websocket-api';
import { Button } from '@/components/commons/Button';
import { CheckBox } from '@/components/commons/Checkbox';
@@ -34,6 +32,12 @@ import { defaultConfig, useConfig } from '@/hooks/config';
import { playTapSetupSound } from '@/sounds/sounds';
import { useBreakpoint } from '@/hooks/breakpoint';
import { TrackerAssignOptions } from './TrackerAssignOptions';
import { useAtomValue } from 'jotai';
import {
assignedTrackersAtom,
FlatDeviceTracker,
flatTrackersAtom,
} from '@/store/app-store';
export type BodyPartError = {
label: string | undefined;
@@ -51,7 +55,6 @@ export function TrackersAssignPage() {
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
const { config, setConfig } = useConfig();
const { useAssignedTrackers, trackers } = useTrackers();
const { applyProgress, state } = useOnboarding();
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
const defaultValues = {
@@ -62,7 +65,10 @@ export function TrackersAssignPage() {
}>({ defaultValues });
const { mirrorView } = watch();
const [selectedRole, setSelectRole] = useState<BodyPart>(BodyPart.NONE);
const assignedTrackers = useAssignedTrackers();
const assignedTrackers = useAtomValue(assignedTrackersAtom);
const trackers = useAtomValue(flatTrackersAtom);
useEffect(() => {
setConfig({ mirrorView });
}, [mirrorView]);

View File

@@ -1,13 +1,17 @@
import classNames from 'classnames';
import ReactModal from 'react-modal';
import { BodyPart } from 'solarxr-protocol';
import { FlatDeviceTracker } from '@/hooks/app';
import { useTrackers } from '@/hooks/tracker';
import { Button } from '@/components/commons/Button';
import { TipBox } from '@/components/commons/TipBox';
import { Typography } from '@/components/commons/Typography';
import { TrackerCard } from '@/components/tracker/TrackerCard';
import { useLocalization } from '@fluent/react';
import { useAtomValue } from 'jotai';
import {
assignedTrackersAtom,
FlatDeviceTracker,
unassignedTrackersAtom,
} from '@/store/app-store';
export function TrackerSelectionMenu({
isOpen = true,
@@ -21,10 +25,9 @@ export function TrackerSelectionMenu({
onTrackerSelected: (tracker: FlatDeviceTracker | null) => void;
}) {
const { l10n } = useLocalization();
const { useAssignedTrackers, useUnassignedTrackers } = useTrackers();
const unassignedTrackers = useUnassignedTrackers();
const assignedTrackers = useAssignedTrackers();
const unassignedTrackers = useAtomValue(unassignedTrackersAtom);
const assignedTrackers = useAtomValue(assignedTrackersAtom);
return (
<>

View File

@@ -1,11 +1,11 @@
import classNames from 'classnames';
import { MouseEventHandler, useEffect, useMemo, useState } from 'react';
import { BodyPart, TrackerDataT } from 'solarxr-protocol';
import { FlatDeviceTracker } from '@/hooks/app';
import { useTracker } from '@/hooks/tracker';
import { Typography } from '@/components/commons/Typography';
import { useLocalization } from '@fluent/react';
import { WarningIcon } from '@/components/commons/icon/WarningIcon';
import { FlatDeviceTracker } from '@/store/app-store';
function Tracker({
tracker,

View File

@@ -39,6 +39,8 @@ import { useAppContext } from '@/hooks/app';
import { MagnetometerToggleSetting } from '@/components/settings/pages/MagnetometerToggleSetting';
import semver from 'semver';
import { checkForUpdate } from '@/components/firmware-update/FirmwareUpdate';
import { useSetAtom } from 'jotai';
import { ignoredTrackersAtom } from '@/store/app-store';
const rotationsLabels: [Quaternion, string][] = [
[rotationToQuatMap.BACK, 'tracker-rotation-back'],
@@ -72,7 +74,7 @@ export function TrackerSettingsPage() {
},
reValidateMode: 'onSubmit',
});
const { dispatch } = useAppContext();
const setIgnoredTracker = useSetAtom(ignoredTrackersAtom);
const { trackerName, allowDriftCompensation } = watch();
const tracker = useTrackerFromId(trackernum, deviceid);
@@ -534,7 +536,10 @@ export function TrackerSettingsPage() {
RpcMessage.ForgetDeviceRequest,
new ForgetDeviceRequestT(macAddress)
);
dispatch({ type: 'ignoreTracker', value: macAddress });
setIgnoredTracker((state) => {
state.add(macAddress);
return state;
});
}}
>
{l10n.getString('tracker-settings-forget-label')}

View File

@@ -7,7 +7,6 @@ import {
TrackerIdT,
TrackerStatus as TrackerStatusEnum,
} from 'solarxr-protocol';
import { FlatDeviceTracker } from '@/hooks/app';
import { useConfig } from '@/hooks/config';
import { useTracker } from '@/hooks/tracker';
import { BodyPartIcon } from '@/components/commons/BodyPartIcon';
@@ -17,6 +16,7 @@ import { TrackerBattery } from './TrackerBattery';
import { TrackerStatus } from './TrackerStatus';
import { TrackerWifi } from './TrackerWifi';
import { trackerStatusRelated, useStatusContext } from '@/hooks/status-system';
import { FlatDeviceTracker } from '@/store/app-store';
enum DisplayColumn {
NAME,

View File

@@ -1,7 +1,6 @@
import { Canvas, Object3DNode, extend, useThree } from '@react-three/fiber';
import { useAppContext } from '@/hooks/app';
import { Bone } from 'three';
import { useMemo, useEffect, useRef, useState } from 'react';
import { useMemo, useEffect, useState } from 'react';
import {
OrbitControls,
OrthographicCamera,
@@ -20,6 +19,8 @@ import { Button } from '@/components/commons/Button';
import { useLocalization } from '@fluent/react';
import { ErrorBoundary } from 'react-error-boundary';
import { Typography } from '@/components/commons/Typography';
import { useAtomValue } from 'jotai';
import { bonesAtom } from '@/store/app-store';
extend({ BasedSkeletonHelper });
@@ -135,22 +136,20 @@ export function ToggleableSkeletonVisualizerWidget({
}
export function SkeletonVisualizerWidget() {
const { bones: _bones } = useAppContext();
const _bones = useAtomValue(bonesAtom);
const { l10n } = useLocalization();
const bones = useMemo(
() => new Map(_bones.map((b) => [b.bodyPart, b])),
[JSON.stringify(_bones)]
const bones = useMemo(() => {
return new Map(_bones.map((b) => [b.bodyPart, b]));
}, [_bones]);
const skeleton = useMemo(
() => createChildren(bones, BoneKind.root),
[bones.size]
);
const skeleton = useRef<Bone[]>();
useEffect(() => {
skeleton.current = createChildren(bones, BoneKind.root);
}, [bones.size]);
useEffect(() => {
skeleton.current?.forEach(
skeleton.forEach(
(bone) => bone instanceof BoneKind && bone.updateData(bones)
);
}, [bones]);
@@ -166,15 +165,13 @@ export function SkeletonVisualizerWidget() {
return (yLength as BoneT[]).reduce((prev, cur) => prev + cur.boneLength, 0);
}, [bones]);
const bonesInitialized = bones.size > 0;
const targetCamera = useMemo(() => {
const hmd = bones.get(BodyPart.HEAD);
if (hmd?.headPositionG?.y && hmd.headPositionG.y > 0) {
return hmd.headPositionG.y / 2;
}
return heightOffset / 2;
}, [bonesInitialized]);
}, [bones]);
const yawReset = useMemo(() => {
const hmd = bones.get(BodyPart.HEAD);
@@ -190,14 +187,14 @@ export function SkeletonVisualizerWidget() {
new THREE.Vector3(quat.x, quat.y, quat.z).dot(VEC_Y) / VEC_Y.lengthSq()
);
return new THREE.Quaternion(vec.x, vec.y, vec.z, quat.w).normalize();
}, [bonesInitialized]);
}, [bones.size]);
const scale = useMemo(
() => Math.max(1.8, heightOffset) / 1.8,
[heightOffset]
);
if (!skeleton.current) return <></>;
if (!skeleton) return <></>;
return (
<ErrorBoundary
fallback={
@@ -209,9 +206,9 @@ export function SkeletonVisualizerWidget() {
<Canvas className={classNames('container mx-auto')}>
<gridHelper args={[10, 50, GROUND_COLOR, GROUND_COLOR]} />
<group position={[0, heightOffset, 0]} quaternion={yawReset}>
<SkeletonHelper object={skeleton.current[0]}></SkeletonHelper>
<SkeletonHelper object={skeleton[0]}></SkeletonHelper>
</group>
<primitive object={skeleton.current[0]} />
<primitive object={skeleton[0]} />
<PerspectiveCamera
makeDefault
position={[3, 2.5, -3]}

View File

@@ -1,24 +1,12 @@
import { createContext, useContext, useEffect, useState } from 'react';
import {
createContext,
Dispatch,
Reducer,
useContext,
useEffect,
useMemo,
useReducer,
useState,
} from 'react';
import {
BoneT,
DataFeedMessage,
DataFeedUpdateT,
DeviceDataT,
ResetResponseT,
ResetStatus,
ResetType,
RpcMessage,
StartDataFeedT,
TrackerDataT,
} from 'solarxr-protocol';
import { playSoundOnResetEnded, playSoundOnResetStarted } from '@/sounds/sounds';
import { useConfig } from './config';
@@ -26,6 +14,8 @@ import { useDataFeedConfig } from './datafeed-config';
import { useWebsocketAPI } from './websocket-api';
import { error } from '@/utils/logging';
import { cacheWrap } from './cache';
import { useAtomValue, useSetAtom } from 'jotai';
import { datafeedAtom, devicesAtom } from '@/store/app-store';
import { updateSentryContext } from '@/utils/sentry';
export interface FirmwareRelease {
@@ -35,41 +25,8 @@ export interface FirmwareRelease {
firmwareFile: string;
}
export interface FlatDeviceTracker {
device?: DeviceDataT;
tracker: TrackerDataT;
}
export type AppStateAction =
| { type: 'datafeed'; value: DataFeedUpdateT }
| { type: 'ignoreTracker'; value: string };
export interface AppState {
datafeed?: DataFeedUpdateT;
ignoredTrackers: Set<string>;
}
export interface AppContext {
currentFirmwareRelease: FirmwareRelease | null;
state: AppState;
trackers: FlatDeviceTracker[];
dispatch: Dispatch<AppStateAction>;
bones: BoneT[];
computedTrackers: FlatDeviceTracker[];
}
export function reducer(state: AppState, action: AppStateAction) {
switch (action.type) {
case 'datafeed':
return { ...state, datafeed: action.value };
case 'ignoreTracker':
return {
...state,
ignoredTrackers: new Set([...state.ignoredTrackers, action.value]),
};
default:
throw new Error(`unhandled state action ${(action as AppStateAction).type}`);
}
}
export function useProvideAppContext(): AppContext {
@@ -77,10 +34,9 @@ export function useProvideAppContext(): AppContext {
useWebsocketAPI();
const { config } = useConfig();
const { dataFeedConfig } = useDataFeedConfig();
const [state, dispatch] = useReducer<Reducer<AppState, AppStateAction>>(reducer, {
datafeed: new DataFeedUpdateT(),
ignoredTrackers: new Set(),
});
const setDatafeed = useSetAtom(datafeedAtom);
const devices = useAtomValue(devicesAtom);
const [currentFirmwareRelease, setCurrentFirmwareRelease] =
useState<FirmwareRelease | null>(null);
@@ -92,32 +48,13 @@ export function useProvideAppContext(): AppContext {
}
}, [isConnected]);
const trackers = useMemo(
() =>
(state.datafeed?.devices || []).reduce<FlatDeviceTracker[]>(
(curr, device) => [
...curr,
...device.trackers.map((tracker) => ({ tracker, device })),
],
[]
),
[state]
);
const computedTrackers: FlatDeviceTracker[] = useMemo(
() => (state.datafeed?.syntheticTrackers || []).map((tracker) => ({ tracker })),
[state]
);
const bones = useMemo(() => state.datafeed?.bones || [], [state]);
useDataFeedPacket(DataFeedMessage.DataFeedUpdate, (packet: DataFeedUpdateT) => {
dispatch({ type: 'datafeed', value: packet });
setDatafeed(packet);
});
useEffect(() => {
updateSentryContext(state);
}, [state.datafeed?.devices]);
updateSentryContext(devices);
}, [devices]);
useRPCPacket(RpcMessage.ResetResponse, ({ status, resetType }: ResetResponseT) => {
if (!config?.feedbackSound) return;
@@ -187,11 +124,6 @@ export function useProvideAppContext(): AppContext {
return {
currentFirmwareRelease,
state,
trackers,
dispatch,
bones,
computedTrackers,
};
}

View File

@@ -1,40 +1,44 @@
import { useCallback, useEffect } from 'react';
import { useEffect } from 'react';
import { useConfig } from './config';
import { useInterval } from './timeout';
import { useTrackers } from './tracker';
import { invoke } from '@tauri-apps/api/core';
import { warn } from '@/utils/logging';
import { useLocalization } from '@fluent/react';
import { connectedIMUTrackersAtom } from '@/store/app-store';
import { getDefaultStore } from 'jotai';
export function useDiscordPresence() {
const { config } = useConfig();
const { useConnectedIMUTrackers } = useTrackers();
const { l10n } = useLocalization();
const imuTrackers = useConnectedIMUTrackers();
const updatePresence = useCallback(() => {
(async () => {
try {
if (await checkDiscordClient()) {
// If discord client exists, try updating presence
await updateDiscordPresence({
details: l10n.getString(
'settings-general-interface-discord_presence-message',
{ amount: imuTrackers.length }
),
});
} else {
// else, try creating a discord client
await createDiscordClient();
}
} catch (e) {
warn(`failed to update presence, error: ${e}`);
}
})();
}, [imuTrackers.length, l10n]);
// Update presence every 6.9 seconds
useInterval(updatePresence, config?.discordPresence ? 6900 : null);
useInterval(
() => {
(async () => {
try {
// Better to do this instead of useAtomValue as we are doing polling with the interval
// useAtomValue can trigger re render of the dom and this hook is top level, so this
// would be really bad
const imuTrackers = getDefaultStore().get(connectedIMUTrackersAtom);
if (await checkDiscordClient()) {
// If discord client exists, try updating presence
await updateDiscordPresence({
details: l10n.getString(
'settings-general-interface-discord_presence-message',
{ amount: imuTrackers.length }
),
});
} else {
// else, try creating a discord client
await createDiscordClient();
}
} catch (e) {
warn(`failed to update presence, error: ${e}`);
}
})();
},
config?.discordPresence ? 6900 : null
);
// Clear presence on config being disabled
useEffect(() => {

View File

@@ -1,5 +1,5 @@
import { FlatDeviceTracker } from '@/store/app-store';
import { useMemo } from 'react';
import { FlatDeviceTracker } from './app';
const IGNORED_BOARDS = new Set(['Sony Mocopi', 'Haritora']);

View File

@@ -16,8 +16,8 @@ import {
} from 'solarxr-protocol';
import { useWebsocketAPI } from './websocket-api';
import { FluentVariable } from '@fluent/bundle';
import { FlatDeviceTracker } from './app';
import { ReactLocalization } from '@fluent/react';
import { FlatDeviceTracker } from '@/store/app-store';
type StatusSystemStateAction =
| StatusSystemStateFixedAction

View File

@@ -1,40 +1,12 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { BodyPart, TrackerDataT, TrackerInfoT, TrackerStatus } from 'solarxr-protocol';
import { BodyPart, TrackerDataT, TrackerInfoT } from 'solarxr-protocol';
import { QuaternionFromQuatT, QuaternionToEulerDegrees } from '@/maths/quaternion';
import { useAppContext } from './app';
import { ReactLocalization, useLocalization } from '@fluent/react';
import { useDataFeedConfig } from './datafeed-config';
import { Quaternion, Vector3 } from 'three';
import { Vector3FromVec3fT } from '@/maths/vector3';
export function useTrackers() {
const { trackers } = useAppContext();
return {
trackers,
useAssignedTrackers: () =>
useMemo(
() =>
trackers.filter(({ tracker }) => tracker.info?.bodyPart !== BodyPart.NONE),
[trackers]
),
useUnassignedTrackers: () =>
useMemo(
() =>
trackers.filter(({ tracker }) => tracker.info?.bodyPart === BodyPart.NONE),
[trackers]
),
useConnectedIMUTrackers: () =>
useMemo(
() =>
trackers.filter(
({ tracker }) =>
tracker.status !== TrackerStatus.DISCONNECTED && tracker.info?.isImu
),
[trackers]
),
};
}
import { useAtomValue } from 'jotai';
import { trackerFromIdAtom } from '@/store/app-store';
export function getTrackerName(l10n: ReactLocalization, info: TrackerInfoT | null) {
if (info?.customName) return info?.customName;
@@ -115,19 +87,9 @@ export function useTrackerFromId(
trackerNum: string | number | undefined,
deviceId: string | number | undefined
) {
const { trackers } = useAppContext();
const tracker = useMemo(
() =>
trackers.find(
({ tracker }) =>
trackerNum &&
deviceId &&
tracker?.trackerId?.trackerNum == trackerNum &&
tracker?.trackerId?.deviceId?.id == deviceId
),
[trackers, trackerNum, deviceId]
const trackerAtom = useMemo(
() => trackerFromIdAtom({ trackerNum, deviceId }),
[trackerNum, deviceId]
);
return tracker;
return useAtomValue(trackerAtom);
}

View File

@@ -0,0 +1,94 @@
import { atom } from 'jotai';
import {
BodyPart,
DataFeedUpdateT,
DeviceDataT,
TrackerDataT,
TrackerStatus,
} from 'solarxr-protocol';
import { selectAtom } from 'jotai/utils';
import { isEqual } from '@react-hookz/deep-equal';
export interface FlatDeviceTracker {
device?: DeviceDataT;
tracker: TrackerDataT;
}
export const ignoredTrackersAtom = atom(new Set<string>());
export const datafeedAtom = atom(new DataFeedUpdateT());
export const devicesAtom = selectAtom(
datafeedAtom,
(datafeed) => datafeed.devices,
isEqual
);
export const flatTrackersAtom = atom((get) => {
const devices = get(devicesAtom);
return devices.flatMap<FlatDeviceTracker>((device) =>
device.trackers.map((tracker) => ({ tracker, device }))
);
});
export const assignedTrackersAtom = atom((get) => {
const trackers = get(flatTrackersAtom);
return trackers.filter(({ tracker }) => tracker.info?.bodyPart !== BodyPart.NONE);
});
export const unassignedTrackersAtom = atom((get) => {
const trackers = get(flatTrackersAtom);
return trackers.filter(({ tracker }) => tracker.info?.bodyPart === BodyPart.NONE);
});
export const connectedIMUTrackersAtom = atom((get) => {
const trackers = get(flatTrackersAtom);
return trackers.filter(
({ tracker }) =>
tracker.status !== TrackerStatus.DISCONNECTED && tracker.info?.isImu
);
});
export const computedTrackersAtom = selectAtom(
datafeedAtom,
(datafeed) => datafeed.syntheticTrackers.map((tracker) => ({ tracker })),
isEqual
);
export const bonesAtom = selectAtom(
datafeedAtom,
(datafeed) => datafeed.bones,
isEqual
);
export const hasHMDTrackerAtom = atom((get) => {
const trackers = get(flatTrackersAtom);
return trackers.some(
(tracker) =>
tracker.tracker.info?.bodyPart === BodyPart.HEAD &&
(tracker.tracker.info.isHmd || tracker.tracker.position?.y !== undefined)
);
});
export const trackerFromIdAtom = ({
trackerNum,
deviceId,
}: {
trackerNum: string | number | undefined;
deviceId: string | number | undefined;
}) =>
selectAtom(
atom((get) =>
get(flatTrackersAtom).find(
({ tracker }) =>
trackerNum &&
deviceId &&
tracker?.trackerId?.trackerNum == trackerNum &&
tracker?.trackerId?.deviceId?.id == deviceId
)
),
(a) => a,
isEqual
);

View File

@@ -7,7 +7,7 @@ import {
useLocation,
useNavigationType,
} from 'react-router-dom';
import { AppState } from '@/hooks/app';
import { DeviceDataT } from 'solarxr-protocol';
export function getSentryOrCompute(enabled = false) {
// if sentry is already initialized - SKIP
@@ -65,21 +65,19 @@ export function getSentryOrCompute(enabled = false) {
return newClient;
}
export function updateSentryContext(state: AppState) {
export function updateSentryContext(devices: DeviceDataT[]) {
// We filter out the shit we dont want. We dont need rotation data or ip addresses
const trackers = (state.datafeed?.devices || []).map(
({ hardwareInfo, trackers, id }) => ({
id: id?.id,
hardwareInfo: { ...hardwareInfo, ipAddress: undefined },
trackers: trackers.map(({ info, trackerId }) => ({
info,
trackerId: {
trackerNum: trackerId?.trackerNum,
deviceId: trackerId?.deviceId?.id,
},
})),
})
);
const trackers = (devices || []).map(({ hardwareInfo, trackers, id }) => ({
id: id?.id,
hardwareInfo: { ...hardwareInfo, ipAddress: undefined },
trackers: trackers.map(({ info, trackerId }) => ({
info,
trackerId: {
trackerNum: trackerId?.trackerNum,
deviceId: trackerId?.deviceId?.id,
},
})),
}));
// Will send the latest context to sentry when an error happens
Sentry.setContext('trackers', {

View File

@@ -4,6 +4,7 @@ import { defineConfig, PluginOption } from 'vite';
import { execSync } from 'child_process';
import path from 'path';
import { visualizer } from 'rollup-plugin-visualizer';
import jotaiReactRefresh from 'jotai/babel/plugin-react-refresh';
const commitHash = execSync('git rev-parse --verify --short HEAD').toString().trim();
const versionTag = execSync('git --no-pager tag --sort -taggerdate --points-at HEAD')
@@ -41,7 +42,7 @@ export default defineConfig({
__GIT_CLEAN__: gitClean,
},
plugins: [
react(),
react({ babel: { plugins: [jotaiReactRefresh] } }),
i18nHotReload(),
visualizer() as PluginOption,
sentryVitePlugin({

49
pnpm-lock.yaml generated
View File

@@ -32,9 +32,12 @@ importers:
'@hookform/resolvers':
specifier: ^3.6.0
version: 3.6.0(react-hook-form@7.53.0(react@18.3.1))
'@react-hookz/deep-equal':
specifier: ^3.0.3
version: 3.0.3
'@react-three/drei':
specifier: ^9.114.3
version: 9.114.5(@react-three/fiber@8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.163.0))(@types/react@18.3.11)(@types/three@0.163.0)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.163.0)
version: 9.114.5(@react-three/fiber@8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.163.0))(@types/react@18.3.11)(@types/three@0.163.0)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.163.0)
'@react-three/fiber':
specifier: ^8.17.10
version: 8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.163.0)
@@ -83,6 +86,9 @@ importers:
ip-num:
specifier: ^1.5.1
version: 1.5.1
jotai:
specifier: ^2.12.2
version: 2.12.2(@types/react@18.3.11)(react@18.3.1)
prompts:
specifier: ^2.4.2
version: 2.4.2
@@ -819,6 +825,10 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@react-hookz/deep-equal@3.0.3':
resolution: {integrity: sha512-SLy+NmiDpncqc2d9TR4Y4R7f8lUFOQK9WbnIq02A6wDxy+dTHfA2Np0dPvj0SFp6i1nqERLmEUe9MxPLuO/IqA==}
engines: {node: '>=18.0.0'}
'@react-spring/animated@9.6.1':
resolution: {integrity: sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==}
peerDependencies:
@@ -2556,8 +2566,8 @@ packages:
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immer@9.0.21:
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
immer@10.1.1:
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
immutable@4.3.6:
resolution: {integrity: sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==}
@@ -2793,6 +2803,18 @@ packages:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true
jotai@2.12.2:
resolution: {integrity: sha512-oN8715y7MkjXlSrpyjlR887TOuc/NLZMs9gvgtfWH/JP47ChwO0lR2ijSwBvPMYyXRAPT+liIAhuBavluKGgtA==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=17.0.0'
react: '>=17.0.0'
peerDependenciesMeta:
'@types/react':
optional: true
react:
optional: true
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -5049,6 +5071,8 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@react-hookz/deep-equal@3.0.3': {}
'@react-spring/animated@9.6.1(react@18.3.1)':
dependencies:
'@react-spring/shared': 9.6.1(react@18.3.1)
@@ -5083,7 +5107,7 @@ snapshots:
'@react-spring/types@9.6.1': {}
'@react-three/drei@9.114.5(@react-three/fiber@8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.163.0))(@types/react@18.3.11)(@types/three@0.163.0)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.163.0)':
'@react-three/drei@9.114.5(@react-three/fiber@8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.163.0))(@types/react@18.3.11)(@types/three@0.163.0)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.163.0)':
dependencies:
'@babel/runtime': 7.24.7
'@mediapipe/tasks-vision': 0.10.8
@@ -5107,7 +5131,7 @@ snapshots:
three-mesh-bvh: 0.7.8(three@0.163.0)
three-stdlib: 2.30.3(three@0.163.0)
troika-three-text: 0.49.1(three@0.163.0)
tunnel-rat: 0.1.2(@types/react@18.3.11)(immer@9.0.21)(react@18.3.1)
tunnel-rat: 0.1.2(@types/react@18.3.11)(immer@10.1.1)(react@18.3.1)
utility-types: 3.11.0
uuid: 9.0.1
zustand: 3.7.2(react@18.3.1)
@@ -6983,7 +7007,7 @@ snapshots:
immediate@3.0.6: {}
immer@9.0.21:
immer@10.1.1:
optional: true
immutable@4.3.6: {}
@@ -7219,6 +7243,11 @@ snapshots:
jiti@1.21.6: {}
jotai@2.12.2(@types/react@18.3.11)(react@18.3.1):
optionalDependencies:
'@types/react': 18.3.11
react: 18.3.1
js-tokens@4.0.0: {}
js-yaml@4.1.0:
@@ -8817,9 +8846,9 @@ snapshots:
tslib: 1.14.1
typescript: 4.8.2
tunnel-rat@0.1.2(@types/react@18.3.11)(immer@9.0.21)(react@18.3.1):
tunnel-rat@0.1.2(@types/react@18.3.11)(immer@10.1.1)(react@18.3.1):
dependencies:
zustand: 4.5.2(@types/react@18.3.11)(immer@9.0.21)(react@18.3.1)
zustand: 4.5.2(@types/react@18.3.11)(immer@10.1.1)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
@@ -9145,12 +9174,12 @@ snapshots:
optionalDependencies:
react: 18.3.1
zustand@4.5.2(@types/react@18.3.11)(immer@9.0.21)(react@18.3.1):
zustand@4.5.2(@types/react@18.3.11)(immer@10.1.1)(react@18.3.1):
dependencies:
use-sync-external-store: 1.2.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.11
immer: 9.0.21
immer: 10.1.1
react: 18.3.1
zwitch@2.0.4: {}