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
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');
}
}

View file

@ -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<TimelineStore & TimelineActions>(
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<TimelineStore & TimelineActions>(
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

View file

@ -29,12 +29,14 @@ interface TuioState {
// Active tangibles (runtime only - not persisted)
activeTangibles: Map<string, TuioTangibleInfo>;
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<TuioState>()(
// Active tangibles
activeTangibles: new Map(),
lastStateChangeSource: null,
activeStateTangibles: [],
addActiveTangible: (hardwareId: string, info: TuioTangibleInfo) =>
set((state) => {
@ -88,11 +90,27 @@ export const useTuioStore = create<TuioState>()(
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',

View file

@ -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<Pick<ConstellationState, 'label' | 'description' | 'metadata'>>) => void;