wired-tools: hoist inspection selection (+ live state + action version) to the store

Five more useStates leave WiredCreatorToolsView: selectedFurni,
selectedFurniLiveState, selectedUser, selectedUserLiveState, and the
monotonic selectedUserActionVersion counter. All five now live in
useWiredCreatorToolsUiStore; the room-event listeners
(useObjectSelectedEvent, the per-kind useMessageEvent + useNitroEvent
handlers, the per-action effects that bump the version counter) stay
in the component because they need React's subscription lifecycle —
they just call the store actions instead of setState.

Same persistence benefit as the previous monitorSnapshot pass: the
currently-inspected target survives a panel close/reopen instead of
being dropped to null on remount. Live-state setters and the action
version counter accept Updater<T> so the many `previousValue => ...`
call sites stayed verbatim.

Tests: six new cases (setSelectedFurni + null clear, functional
updater on FurniLiveState, paired setSelectedUser + LiveState,
monotonic ActionVersion via updater, close/reopen persistence). The
test fixtures use the real interface shapes — InspectionFurniSelection
includes a renderer-typed `info: AvatarInfoFurni` that is cast
through `as never` so the test doesn't have to construct the full
avatar info shape. 187/187 passing.
This commit is contained in:
simoleo89
2026-05-16 12:25:31 +02:00
parent 7758af710e
commit 8182e06be4
3 changed files with 130 additions and 8 deletions
@@ -25,11 +25,16 @@ export const WiredCreatorToolsView: FC<{}> = () =>
const setInspectionType = useWiredCreatorToolsUiStore(s => s.setInspectionType);
const variablesType = useWiredCreatorToolsUiStore(s => s.variablesType);
const [ keepSelected, setKeepSelected ] = useState(false);
const [ selectedFurni, setSelectedFurni ] = useState<InspectionFurniSelection>(null);
const [ selectedFurniLiveState, setSelectedFurniLiveState ] = useState<InspectionFurniLiveState>(null);
const [ selectedUser, setSelectedUser ] = useState<InspectionUserSelection>(null);
const [ selectedUserLiveState, setSelectedUserLiveState ] = useState<InspectionUserLiveState>(null);
const [ selectedUserActionVersion, setSelectedUserActionVersion ] = useState(0);
const selectedFurni = useWiredCreatorToolsUiStore(s => s.selectedFurni);
const setSelectedFurni = useWiredCreatorToolsUiStore(s => s.setSelectedFurni);
const selectedFurniLiveState = useWiredCreatorToolsUiStore(s => s.selectedFurniLiveState);
const setSelectedFurniLiveState = useWiredCreatorToolsUiStore(s => s.setSelectedFurniLiveState);
const selectedUser = useWiredCreatorToolsUiStore(s => s.selectedUser);
const setSelectedUser = useWiredCreatorToolsUiStore(s => s.setSelectedUser);
const selectedUserLiveState = useWiredCreatorToolsUiStore(s => s.selectedUserLiveState);
const setSelectedUserLiveState = useWiredCreatorToolsUiStore(s => s.setSelectedUserLiveState);
const selectedUserActionVersion = useWiredCreatorToolsUiStore(s => s.selectedUserActionVersion);
const setSelectedUserActionVersion = useWiredCreatorToolsUiStore(s => s.setSelectedUserActionVersion);
const [ globalClock, setGlobalClock ] = useState(Date.now());
const [ roomEnteredAt, setRoomEnteredAt ] = useState(Date.now());
const monitorSnapshot = useWiredCreatorToolsUiStore(s => s.monitorSnapshot);
@@ -17,7 +17,12 @@ const INITIAL = {
variableManageTypeFilter: 'ALL',
variableManageSort: 'highest_value',
variableManagePage: 1,
monitorSnapshot: createEmptyMonitorSnapshot()
monitorSnapshot: createEmptyMonitorSnapshot(),
selectedFurni: null,
selectedFurniLiveState: null,
selectedUser: null,
selectedUserLiveState: null,
selectedUserActionVersion: 0
};
describe('useWiredCreatorToolsUiStore', () =>
@@ -46,6 +51,11 @@ describe('useWiredCreatorToolsUiStore', () =>
expect(state.variableManageSort).toBe('highest_value');
expect(state.variableManagePage).toBe(1);
expect(state.monitorSnapshot).toEqual(createEmptyMonitorSnapshot());
expect(state.selectedFurni).toBeNull();
expect(state.selectedFurniLiveState).toBeNull();
expect(state.selectedUser).toBeNull();
expect(state.selectedUserLiveState).toBeNull();
expect(state.selectedUserActionVersion).toBe(0);
});
describe('setIsVisible', () =>
@@ -233,4 +243,75 @@ describe('useWiredCreatorToolsUiStore', () =>
expect(useWiredCreatorToolsUiStore.getState().monitorSnapshot.usageCurrentWindow).toBe(3);
});
});
describe('inspection selection', () =>
{
const furniSelection = {
objectId: 42,
category: 10,
info: { id: 42, name: 'sofa', description: '', image: null } as never
};
const userSelection = {
kind: 'user' as const,
roomIndex: 7,
name: 'simoleo',
figure: 'hd-180-1.lg-3023-110',
gender: 'M',
userId: 99,
level: 12,
posture: 'std'
} as never;
it('setSelectedFurni stores the picked furni selection', () =>
{
useWiredCreatorToolsUiStore.getState().setSelectedFurni(furniSelection);
expect(useWiredCreatorToolsUiStore.getState().selectedFurni).toEqual(furniSelection);
});
it('setSelectedFurni(null) clears the selection (deselect path)', () =>
{
useWiredCreatorToolsUiStore.getState().setSelectedFurni(furniSelection);
useWiredCreatorToolsUiStore.getState().setSelectedFurni(null);
expect(useWiredCreatorToolsUiStore.getState().selectedFurni).toBeNull();
});
it('setSelectedFurniLiveState accepts a functional updater', () =>
{
const initial = { positionX: 1, positionY: 2, altitude: 3, rotation: 4, state: 5 };
useWiredCreatorToolsUiStore.getState().setSelectedFurniLiveState(initial);
useWiredCreatorToolsUiStore.getState().setSelectedFurniLiveState(prev => (prev ? { ...prev, state: prev.state + 1 } : null));
expect(useWiredCreatorToolsUiStore.getState().selectedFurniLiveState).toEqual({ ...initial, state: 6 });
});
it('setSelectedUser + setSelectedUserLiveState write the user selection / live state', () =>
{
useWiredCreatorToolsUiStore.getState().setSelectedUser(userSelection);
useWiredCreatorToolsUiStore.getState().setSelectedUserLiveState({ positionX: 5, positionY: 6, altitude: 0, direction: 2 });
expect(useWiredCreatorToolsUiStore.getState().selectedUser).toEqual(userSelection);
expect(useWiredCreatorToolsUiStore.getState().selectedUserLiveState).toEqual({ positionX: 5, positionY: 6, altitude: 0, direction: 2 });
});
it('setSelectedUserActionVersion bumps the monotonic counter via functional updater', () =>
{
useWiredCreatorToolsUiStore.getState().setSelectedUserActionVersion(prev => prev + 1);
useWiredCreatorToolsUiStore.getState().setSelectedUserActionVersion(prev => prev + 1);
useWiredCreatorToolsUiStore.getState().setSelectedUserActionVersion(prev => prev + 1);
expect(useWiredCreatorToolsUiStore.getState().selectedUserActionVersion).toBe(3);
});
it('the selection persists across the panel close/reopen lifecycle', () =>
{
useWiredCreatorToolsUiStore.getState().setSelectedFurni(furniSelection);
useWiredCreatorToolsUiStore.getState().setIsVisible(false);
useWiredCreatorToolsUiStore.getState().setIsVisible(true);
expect(useWiredCreatorToolsUiStore.getState().selectedFurni).toEqual(furniSelection);
});
});
});
@@ -1,6 +1,6 @@
import { createNitroStore } from '../../state/createNitroStore';
import { createEmptyMonitorSnapshot } from './WiredCreatorTools.helpers';
import { InspectionElementType, MonitorSnapshot, VariablesElementType, WiredToolsTab } from './WiredCreatorTools.types';
import { InspectionElementType, InspectionFurniLiveState, InspectionFurniSelection, InspectionUserLiveState, InspectionUserSelection, MonitorSnapshot, VariablesElementType, WiredToolsTab } from './WiredCreatorTools.types';
type MonitorSeverityFilter = 'ALL' | 'ERROR' | 'WARNING';
type Updater<T> = T | ((prev: T) => T);
@@ -37,6 +37,24 @@ interface WiredCreatorToolsUiState
*/
monitorSnapshot: MonitorSnapshot;
/**
* Inspection selection. The room-event listeners
* (`useObjectSelectedEvent` and the per-kind `useMessageEvent`
* handlers) still live in `WiredCreatorToolsView` — they need React
* lifecycle to subscribe/unsubscribe correctly — but the resulting
* state lives here so a closed/reopened panel keeps the last
* inspected target.
*
* `*ActionVersion` is a monotonic counter the user-action handlers
* bump to force the live-state recomputation effect to re-run even
* when neither `selectedUser` nor `roomIndex` changed identity.
*/
selectedFurni: InspectionFurniSelection | null;
selectedFurniLiveState: InspectionFurniLiveState | null;
selectedUser: InspectionUserSelection | null;
selectedUserLiveState: InspectionUserLiveState | null;
selectedUserActionVersion: number;
setIsVisible: (next: Updater<boolean>) => void;
setActiveTab: (next: WiredToolsTab) => void;
setInspectionType: (next: InspectionElementType) => void;
@@ -57,6 +75,12 @@ interface WiredCreatorToolsUiState
setMonitorSnapshot: (next: MonitorSnapshot) => void;
resetMonitorSnapshot: () => void;
setSelectedFurni: (next: InspectionFurniSelection | null) => void;
setSelectedFurniLiveState: (next: Updater<InspectionFurniLiveState | null>) => void;
setSelectedUser: (next: InspectionUserSelection | null) => void;
setSelectedUserLiveState: (next: Updater<InspectionUserLiveState | null>) => void;
setSelectedUserActionVersion: (next: Updater<number>) => void;
}
export const useWiredCreatorToolsUiStore = createNitroStore<WiredCreatorToolsUiState>()((set) => ({
@@ -80,6 +104,12 @@ export const useWiredCreatorToolsUiStore = createNitroStore<WiredCreatorToolsUiS
monitorSnapshot: createEmptyMonitorSnapshot(),
selectedFurni: null,
selectedFurniLiveState: null,
selectedUser: null,
selectedUserLiveState: null,
selectedUserActionVersion: 0,
setIsVisible: (next) => set(state => ({ isVisible: apply(state.isVisible, next) })),
setActiveTab: (next) => set({ activeTab: next }),
setInspectionType: (next) => set({ inspectionType: next }),
@@ -99,5 +129,11 @@ export const useWiredCreatorToolsUiStore = createNitroStore<WiredCreatorToolsUiS
setVariableManagePage: (next) => set(state => ({ variableManagePage: apply(state.variableManagePage, next) })),
setMonitorSnapshot: (next) => set({ monitorSnapshot: next }),
resetMonitorSnapshot: () => set({ monitorSnapshot: createEmptyMonitorSnapshot() })
resetMonitorSnapshot: () => set({ monitorSnapshot: createEmptyMonitorSnapshot() }),
setSelectedFurni: (next) => set({ selectedFurni: next }),
setSelectedFurniLiveState: (next) => set(state => ({ selectedFurniLiveState: apply(state.selectedFurniLiveState, next) })),
setSelectedUser: (next) => set({ selectedUser: next }),
setSelectedUserLiveState: (next) => set(state => ({ selectedUserLiveState: apply(state.selectedUserLiveState, next) })),
setSelectedUserActionVersion: (next) => set(state => ({ selectedUserActionVersion: apply(state.selectedUserActionVersion, next) }))
}));