From 2ffebb9eb76d5a95965d1729b563a458aa117b22 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Mon, 19 Jan 2026 20:27:50 +0100 Subject: [PATCH] Add TUIO connection in tangible config dialog and hardware ID suggestion Features: - TUIO connection now starts when tangible config dialog is open - Connection closes when dialog is closed - Last detected tangible ID is suggested for Hardware ID field - "Use: [ID]" link appears next to Hardware ID field when tangible detected - Clicking the link auto-fills the Hardware ID field Technical Changes: - Created useTuioConnection hook for shared TUIO connection management - Refactored useTuioIntegration to use new useTuioConnection hook - Added suggestedHardwareId prop to TangibleForm component - Updated QuickAddTangibleForm to get and pass suggested ID - Updated EditTangibleInline to get and pass suggested ID - TangibleConfig modal now uses useTuioConnection when isOpen is true UI Improvements: - Hardware ID suggestion link styled like auto-zoom toggle - Shows truncated ID if longer than 8 characters (e.g., "Use: abc123...") - Full ID shown in tooltip on hover Co-Authored-By: Claude Sonnet 4.5 --- src/components/Config/EditTangibleInline.tsx | 7 ++ .../Config/QuickAddTangibleForm.tsx | 7 ++ src/components/Config/TangibleConfig.tsx | 4 + src/components/Config/TangibleForm.tsx | 20 ++++- src/hooks/useTuioConnection.ts | 76 +++++++++++++++++++ src/hooks/useTuioIntegration.ts | 57 ++------------ 6 files changed, 119 insertions(+), 52 deletions(-) create mode 100644 src/hooks/useTuioConnection.ts diff --git a/src/components/Config/EditTangibleInline.tsx b/src/components/Config/EditTangibleInline.tsx index c931b6b..8618677 100644 --- a/src/components/Config/EditTangibleInline.tsx +++ b/src/components/Config/EditTangibleInline.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, KeyboardEvent } from "react"; import SaveIcon from "@mui/icons-material/Save"; +import { useTuioStore } from "../../stores/tuioStore"; import TangibleForm from "./TangibleForm"; import type { TangibleConfig, TangibleMode, LabelConfig, FilterConfig, NodeTypeConfig, EdgeTypeConfig } from "../../types"; import type { ConstellationState } from "../../types/timeline"; @@ -34,6 +35,11 @@ const EditTangibleInline = ({ onSave, onCancel, }: Props) => { + // Get the last detected tangible ID from TUIO store + const activeTangibles = useTuioStore((state) => state.activeTangibles); + const suggestedHardwareId = activeTangibles.size > 0 + ? Array.from(activeTangibles.keys()).pop() + : undefined; const [name, setName] = useState(""); const [mode, setMode] = useState("filter"); const [description, setDescription] = useState(""); @@ -128,6 +134,7 @@ const EditTangibleInline = ({ nodeTypes={nodeTypes} edgeTypes={edgeTypes} states={states} + suggestedHardwareId={suggestedHardwareId} onNameChange={setName} onModeChange={setMode} onDescriptionChange={setDescription} diff --git a/src/components/Config/QuickAddTangibleForm.tsx b/src/components/Config/QuickAddTangibleForm.tsx index 3e46213..d3760ed 100644 --- a/src/components/Config/QuickAddTangibleForm.tsx +++ b/src/components/Config/QuickAddTangibleForm.tsx @@ -1,4 +1,5 @@ import { useState, useRef, KeyboardEvent } from "react"; +import { useTuioStore } from "../../stores/tuioStore"; import TangibleForm from "./TangibleForm"; import type { TangibleMode, LabelConfig, FilterConfig, NodeTypeConfig, EdgeTypeConfig } from "../../types"; import type { ConstellationState } from "../../types/timeline"; @@ -19,6 +20,11 @@ interface Props { } const QuickAddTangibleForm = ({ labels, nodeTypes, edgeTypes, states, onAdd }: Props) => { + // Get the last detected tangible ID from TUIO store + const activeTangibles = useTuioStore((state) => state.activeTangibles); + const suggestedHardwareId = activeTangibles.size > 0 + ? Array.from(activeTangibles.keys()).pop() + : undefined; const [name, setName] = useState(""); const [hardwareId, setHardwareId] = useState(""); const [mode, setMode] = useState("filter"); @@ -115,6 +121,7 @@ const QuickAddTangibleForm = ({ labels, nodeTypes, edgeTypes, states, onAdd }: P nodeTypes={nodeTypes} edgeTypes={edgeTypes} states={states} + suggestedHardwareId={suggestedHardwareId} onNameChange={setName} onHardwareIdChange={setHardwareId} onModeChange={setMode} diff --git a/src/components/Config/TangibleConfig.tsx b/src/components/Config/TangibleConfig.tsx index 208375d..dc8f290 100644 --- a/src/components/Config/TangibleConfig.tsx +++ b/src/components/Config/TangibleConfig.tsx @@ -3,6 +3,7 @@ import { useGraphWithHistory } from '../../hooks/useGraphWithHistory'; import { useConfirm } from '../../hooks/useConfirm'; import { useToastStore } from '../../stores/toastStore'; import { useTimelineStore } from '../../stores/timelineStore'; +import { useTuioConnection } from '../../hooks/useTuioConnection'; import QuickAddTangibleForm from './QuickAddTangibleForm'; import TangibleManagementList from './TangibleManagementList'; import EditTangibleInline from './EditTangibleInline'; @@ -19,6 +20,9 @@ const TangibleConfigModal = ({ isOpen, onClose, initialEditingTangibleId }: Prop const { confirm, ConfirmDialogComponent } = useConfirm(); const { showToast } = useToastStore(); + // Connect to TUIO when dialog is open + useTuioConnection(isOpen); + const [editingTangible, setEditingTangible] = useState(null); // Get all available states for state mode diff --git a/src/components/Config/TangibleForm.tsx b/src/components/Config/TangibleForm.tsx index 70fe927..871cfbe 100644 --- a/src/components/Config/TangibleForm.tsx +++ b/src/components/Config/TangibleForm.tsx @@ -16,6 +16,7 @@ interface Props { nodeTypes: NodeTypeConfig[]; edgeTypes: EdgeTypeConfig[]; states: ConstellationState[]; + suggestedHardwareId?: string; onNameChange: (value: string) => void; onModeChange: (value: TangibleMode) => void; onDescriptionChange: (value: string) => void; @@ -39,6 +40,7 @@ const TangibleForm = ({ nodeTypes, edgeTypes, states, + suggestedHardwareId, onNameChange, onModeChange, onDescriptionChange, @@ -63,9 +65,21 @@ const TangibleForm = ({
- +
+ + {suggestedHardwareId && ( + + )} +
void; + onTangibleUpdate?: (hardwareId: string, info: TuioTangibleInfo) => void; + onTangibleRemove?: (hardwareId: string) => void; + } +) { + const clientRef = useRef(null); + const { websocketUrl, protocolVersion } = useTuioStore(); + + useEffect(() => { + if (!shouldConnect) { + // Disconnect if we should not be connected + if (clientRef.current) { + clientRef.current.disconnect(); + clientRef.current = null; + useTuioStore.getState().clearActiveTangibles(); + } + return; + } + + // Create TUIO client + const client = new TuioClientManager( + { + onTangibleAdd: (hardwareId: string, info: TuioTangibleInfo) => { + useTuioStore.getState().addActiveTangible(hardwareId, info); + callbacks?.onTangibleAdd?.(hardwareId, info); + }, + onTangibleUpdate: (hardwareId: string, info: TuioTangibleInfo) => { + useTuioStore.getState().updateActiveTangible(hardwareId, info); + callbacks?.onTangibleUpdate?.(hardwareId, info); + }, + onTangibleRemove: (hardwareId: string) => { + useTuioStore.getState().removeActiveTangible(hardwareId); + callbacks?.onTangibleRemove?.(hardwareId); + }, + onConnectionChange: (connected, error) => { + useTuioStore.getState().setConnectionState(connected, error); + }, + }, + protocolVersion + ); + + clientRef.current = client; + + // Connect to TUIO server + client + .connect(websocketUrl) + .catch(() => { + // Connection errors are handled by onConnectionChange callback + }); + + // Cleanup on unmount or when shouldConnect changes + return () => { + if (clientRef.current) { + clientRef.current.disconnect(); + clientRef.current = null; + useTuioStore.getState().clearActiveTangibles(); + } + }; + }, [shouldConnect, websocketUrl, protocolVersion, callbacks]); +} diff --git a/src/hooks/useTuioIntegration.ts b/src/hooks/useTuioIntegration.ts index b60e744..26d9960 100644 --- a/src/hooks/useTuioIntegration.ts +++ b/src/hooks/useTuioIntegration.ts @@ -1,9 +1,8 @@ -import { useEffect, useRef } from 'react'; -import { useTuioStore } from '../stores/tuioStore'; import { useSettingsStore } from '../stores/settingsStore'; import { useGraphStore } from '../stores/graphStore'; import { useTimelineStore } from '../stores/timelineStore'; -import { TuioClientManager } from '../lib/tuio/tuioClient'; +import { useTuioStore } from '../stores/tuioStore'; +import { useTuioConnection } from './useTuioConnection'; import type { TuioTangibleInfo } from '../lib/tuio/types'; import type { TangibleConfig } from '../types'; import { migrateTangibleConfig } from '../utils/tangibleMigration'; @@ -20,54 +19,14 @@ import { migrateTangibleConfig } from '../utils/tangibleMigration'; * - State mode: Switches timeline state */ export function useTuioIntegration() { - const clientRef = useRef(null); const { presentationMode } = useSettingsStore(); - const { websocketUrl, protocolVersion } = useTuioStore(); - useEffect(() => { - // Only connect in presentation mode - if (!presentationMode) { - // Disconnect if we're leaving presentation mode - if (clientRef.current) { - clientRef.current.disconnect(); - clientRef.current = null; - useTuioStore.getState().clearActiveTangibles(); - } - return; - } - - - // Create TUIO client if in presentation mode - const client = new TuioClientManager( - { - onTangibleAdd: handleTangibleAdd, - onTangibleUpdate: handleTangibleUpdate, - onTangibleRemove: handleTangibleRemove, - onConnectionChange: (connected, error) => { - useTuioStore.getState().setConnectionState(connected, error); - }, - }, - protocolVersion - ); - - clientRef.current = client; - - // Connect to TUIO server - client - .connect(websocketUrl) - .catch(() => { - // Connection errors are handled by onConnectionChange callback - }); - - // Cleanup on unmount or when presentation mode changes - return () => { - if (clientRef.current) { - clientRef.current.disconnect(); - clientRef.current = null; - useTuioStore.getState().clearActiveTangibles(); - } - }; - }, [presentationMode, websocketUrl, protocolVersion]); + // Use the shared TUIO connection hook with presentation mode callbacks + useTuioConnection(presentationMode, { + onTangibleAdd: handleTangibleAdd, + onTangibleUpdate: handleTangibleUpdate, + onTangibleRemove: handleTangibleRemove, + }); } /**