From 520eef879ec954bcb03f1892b6f1d1dfdbe3405b Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Mon, 19 Jan 2026 11:42:13 +0100 Subject: [PATCH] Fix state tangible tracking and manual state switch behavior Issues fixed: 1. State tangibles not working after manual state switch 2. No support for multiple simultaneous state tangibles Changes: - Replace lastStateChangeSource with activeStateTangibles array - Track active state tangibles in order of placement - When removing a state tangible, switch to the last remaining one - Clear activeStateTangibles on manual state switch - Add fromTangible parameter to switchToState to distinguish sources - Always switch to newly placed tangible's state (last added wins) New behavior: - Place tangible A -> switch to state A - Manually switch to state B -> clears active tangibles list - Place tangible A again -> switches back to state A - Place tangible A and B simultaneously -> shows state B (last wins) - Remove tangible B -> switches to state A - Remove tangible A -> stays in current state Co-Authored-By: Claude Sonnet 4.5 --- src/hooks/useTuioIntegration.ts | 50 +++++++++++++++++++++++++-------- src/stores/timelineStore.ts | 9 +++++- src/stores/tuioStore.ts | 30 ++++++++++++++++---- src/types/timeline.ts | 2 +- 4 files changed, 72 insertions(+), 19 deletions(-) diff --git a/src/hooks/useTuioIntegration.ts b/src/hooks/useTuioIntegration.ts index 6cd66af..5dc5f59 100644 --- a/src/hooks/useTuioIntegration.ts +++ b/src/hooks/useTuioIntegration.ts @@ -136,8 +136,9 @@ function handleTangibleRemove(hardwareId: string): void { // Handle removal based on tangible mode if (tangibleConfig.mode === 'filter') { removeFilterTangible(tangibleConfig); + } else if (tangibleConfig.mode === 'state' || tangibleConfig.mode === 'stateDial') { + removeStateTangible(hardwareId); } - // State mode: Don't revert on removal (stay in current state) } /** @@ -199,17 +200,44 @@ function applyStateTangible(tangible: TangibleConfig, hardwareId: string): void return; } - const { lastStateChangeSource } = useTuioStore.getState(); + console.log('[TUIO Integration] Applying state tangible:', hardwareId, 'stateId:', tangible.stateId); - // Only switch if this is a different tangible than the last state change - // (Last added wins strategy) - if (lastStateChangeSource === hardwareId) { - return; // Same tangible, don't re-switch - } + // Add to active state tangibles list (at the end) + useTuioStore.getState().addActiveStateTangible(hardwareId); - // Switch to state - useTimelineStore.getState().switchToState(tangible.stateId); + // Always switch to this tangible's state (last added wins) + // Pass fromTangible=true to prevent clearing the active state tangibles list + useTimelineStore.getState().switchToState(tangible.stateId, true); - // Track this as the last state change source - useTuioStore.getState().setLastStateChangeSource(hardwareId); + console.log('[TUIO Integration] Active state tangibles:', useTuioStore.getState().activeStateTangibles); +} + +/** + * Remove state tangible - switch to next active state tangible if any + */ +function removeStateTangible(hardwareId: string): void { + console.log('[TUIO Integration] Removing state tangible:', hardwareId); + + // Remove from active state tangibles list + useTuioStore.getState().removeActiveStateTangible(hardwareId); + + const activeStateTangibles = useTuioStore.getState().activeStateTangibles; + console.log('[TUIO Integration] Remaining active state tangibles:', activeStateTangibles); + + // If there are other state tangibles still active, switch to the last one + if (activeStateTangibles.length > 0) { + const lastActiveHwId = activeStateTangibles[activeStateTangibles.length - 1]; + console.log('[TUIO Integration] Switching to last active state tangible:', lastActiveHwId); + + // Find the tangible config for this hardware ID + const tangibles = useGraphStore.getState().tangibles; + const tangibleConfig = tangibles.find((t) => t.hardwareId === lastActiveHwId); + + if (tangibleConfig && tangibleConfig.stateId) { + // Pass fromTangible=true to prevent clearing the active state tangibles list + useTimelineStore.getState().switchToState(tangibleConfig.stateId, true); + } + } else { + console.log('[TUIO Integration] No more active state tangibles, staying in current state'); + } } diff --git a/src/stores/timelineStore.ts b/src/stores/timelineStore.ts index 26c651c..7f07dff 100644 --- a/src/stores/timelineStore.ts +++ b/src/stores/timelineStore.ts @@ -11,6 +11,7 @@ import { useGraphStore } from "./graphStore"; import { useWorkspaceStore } from "./workspaceStore"; import { useToastStore } from "./toastStore"; import { useHistoryStore } from "./historyStore"; +import { useTuioStore } from "./tuioStore"; /** * Timeline Store @@ -235,7 +236,7 @@ export const useTimelineStore = create( return newStateId; }, - switchToState: (stateId: StateId) => { + switchToState: (stateId: StateId, fromTangible: boolean = false) => { const state = get(); const { activeDocumentId } = state; @@ -257,6 +258,12 @@ export const useTimelineStore = create( return; } + // If this is a manual state switch (not from tangible), clear active state tangibles + if (!fromTangible) { + console.log('[Timeline] Manual state switch detected, clearing active state tangibles'); + useTuioStore.getState().clearActiveStateTangibles(); + } + // Don't push history if already on this state if (timeline.currentStateId !== stateId) { // Push to history BEFORE making changes diff --git a/src/stores/tuioStore.ts b/src/stores/tuioStore.ts index 48b31c7..aee586f 100644 --- a/src/stores/tuioStore.ts +++ b/src/stores/tuioStore.ts @@ -29,12 +29,14 @@ interface TuioState { // Active tangibles (runtime only - not persisted) activeTangibles: Map; - lastStateChangeSource: string | null; + activeStateTangibles: string[]; // Hardware IDs of active state tangibles in order addActiveTangible: (hardwareId: string, info: TuioTangibleInfo) => void; updateActiveTangible: (hardwareId: string, info: TuioTangibleInfo) => void; removeActiveTangible: (hardwareId: string) => void; clearActiveTangibles: () => void; - setLastStateChangeSource: (hardwareId: string | null) => void; + addActiveStateTangible: (hardwareId: string) => void; + removeActiveStateTangible: (hardwareId: string) => void; + clearActiveStateTangibles: () => void; } const DEFAULT_WEBSOCKET_URL = 'ws://localhost:3333'; @@ -60,7 +62,7 @@ export const useTuioStore = create()( // Active tangibles activeTangibles: new Map(), - lastStateChangeSource: null, + activeStateTangibles: [], addActiveTangible: (hardwareId: string, info: TuioTangibleInfo) => set((state) => { @@ -88,11 +90,27 @@ export const useTuioStore = create()( clearActiveTangibles: () => set({ activeTangibles: new Map(), - lastStateChangeSource: null, + activeStateTangibles: [], }), - setLastStateChangeSource: (hardwareId: string | null) => - set({ lastStateChangeSource: hardwareId }), + addActiveStateTangible: (hardwareId: string) => + set((state) => { + // Add to end of list if not already present + if (!state.activeStateTangibles.includes(hardwareId)) { + return { activeStateTangibles: [...state.activeStateTangibles, hardwareId] }; + } + return state; + }), + + removeActiveStateTangible: (hardwareId: string) => + set((state) => { + return { + activeStateTangibles: state.activeStateTangibles.filter((id) => id !== hardwareId), + }; + }), + + clearActiveStateTangibles: () => + set({ activeStateTangibles: [] }), }), { name: 'constellation-tuio-settings', diff --git a/src/types/timeline.ts b/src/types/timeline.ts index 2fb9ecc..8d83034 100644 --- a/src/types/timeline.ts +++ b/src/types/timeline.ts @@ -70,7 +70,7 @@ export interface TimelineActions { createState: (label: string, description?: string, cloneFromCurrent?: boolean) => StateId; // Switch to different state - switchToState: (stateId: StateId) => void; + switchToState: (stateId: StateId, fromTangible?: boolean) => void; // Update state metadata updateState: (stateId: StateId, updates: Partial>) => void;