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 <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik Bruhn 2026-01-19 11:42:13 +01:00
parent d5450610f1
commit 520eef879e
4 changed files with 72 additions and 19 deletions

View file

@ -136,8 +136,9 @@ function handleTangibleRemove(hardwareId: string): void {
// Handle removal based on tangible mode // Handle removal based on tangible mode
if (tangibleConfig.mode === 'filter') { if (tangibleConfig.mode === 'filter') {
removeFilterTangible(tangibleConfig); 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; 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 // Add to active state tangibles list (at the end)
// (Last added wins strategy) useTuioStore.getState().addActiveStateTangible(hardwareId);
if (lastStateChangeSource === hardwareId) {
return; // Same tangible, don't re-switch // 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);
console.log('[TUIO Integration] Active state tangibles:', useTuioStore.getState().activeStateTangibles);
} }
// Switch to state /**
useTimelineStore.getState().switchToState(tangible.stateId); * Remove state tangible - switch to next active state tangible if any
*/
function removeStateTangible(hardwareId: string): void {
console.log('[TUIO Integration] Removing state tangible:', hardwareId);
// Track this as the last state change source // Remove from active state tangibles list
useTuioStore.getState().setLastStateChangeSource(hardwareId); 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');
}
} }

View file

@ -11,6 +11,7 @@ import { useGraphStore } from "./graphStore";
import { useWorkspaceStore } from "./workspaceStore"; import { useWorkspaceStore } from "./workspaceStore";
import { useToastStore } from "./toastStore"; import { useToastStore } from "./toastStore";
import { useHistoryStore } from "./historyStore"; import { useHistoryStore } from "./historyStore";
import { useTuioStore } from "./tuioStore";
/** /**
* Timeline Store * Timeline Store
@ -235,7 +236,7 @@ export const useTimelineStore = create<TimelineStore & TimelineActions>(
return newStateId; return newStateId;
}, },
switchToState: (stateId: StateId) => { switchToState: (stateId: StateId, fromTangible: boolean = false) => {
const state = get(); const state = get();
const { activeDocumentId } = state; const { activeDocumentId } = state;
@ -257,6 +258,12 @@ export const useTimelineStore = create<TimelineStore & TimelineActions>(
return; 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 // Don't push history if already on this state
if (timeline.currentStateId !== stateId) { if (timeline.currentStateId !== stateId) {
// Push to history BEFORE making changes // Push to history BEFORE making changes

View file

@ -29,12 +29,14 @@ interface TuioState {
// Active tangibles (runtime only - not persisted) // Active tangibles (runtime only - not persisted)
activeTangibles: Map<string, TuioTangibleInfo>; activeTangibles: Map<string, TuioTangibleInfo>;
lastStateChangeSource: string | null; activeStateTangibles: string[]; // Hardware IDs of active state tangibles in order
addActiveTangible: (hardwareId: string, info: TuioTangibleInfo) => void; addActiveTangible: (hardwareId: string, info: TuioTangibleInfo) => void;
updateActiveTangible: (hardwareId: string, info: TuioTangibleInfo) => void; updateActiveTangible: (hardwareId: string, info: TuioTangibleInfo) => void;
removeActiveTangible: (hardwareId: string) => void; removeActiveTangible: (hardwareId: string) => void;
clearActiveTangibles: () => 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'; const DEFAULT_WEBSOCKET_URL = 'ws://localhost:3333';
@ -60,7 +62,7 @@ export const useTuioStore = create<TuioState>()(
// Active tangibles // Active tangibles
activeTangibles: new Map(), activeTangibles: new Map(),
lastStateChangeSource: null, activeStateTangibles: [],
addActiveTangible: (hardwareId: string, info: TuioTangibleInfo) => addActiveTangible: (hardwareId: string, info: TuioTangibleInfo) =>
set((state) => { set((state) => {
@ -88,11 +90,27 @@ export const useTuioStore = create<TuioState>()(
clearActiveTangibles: () => clearActiveTangibles: () =>
set({ set({
activeTangibles: new Map(), activeTangibles: new Map(),
lastStateChangeSource: null, activeStateTangibles: [],
}), }),
setLastStateChangeSource: (hardwareId: string | null) => addActiveStateTangible: (hardwareId: string) =>
set({ lastStateChangeSource: hardwareId }), 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', name: 'constellation-tuio-settings',

View file

@ -70,7 +70,7 @@ export interface TimelineActions {
createState: (label: string, description?: string, cloneFromCurrent?: boolean) => StateId; createState: (label: string, description?: string, cloneFromCurrent?: boolean) => StateId;
// Switch to different state // Switch to different state
switchToState: (stateId: StateId) => void; switchToState: (stateId: StateId, fromTangible?: boolean) => void;
// Update state metadata // Update state metadata
updateState: (stateId: StateId, updates: Partial<Pick<ConstellationState, 'label' | 'description' | 'metadata'>>) => void; updateState: (stateId: StateId, updates: Partial<Pick<ConstellationState, 'label' | 'description' | 'metadata'>>) => void;