mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-27 07:43:41 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
1c56066f47
commit
2ffebb9eb7
6 changed files with 119 additions and 52 deletions
|
|
@ -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<TangibleMode>("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}
|
||||
|
|
|
|||
|
|
@ -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<TangibleMode>("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}
|
||||
|
|
|
|||
|
|
@ -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<TangibleConfigType | null>(null);
|
||||
|
||||
// Get all available states for state mode
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="text-xs font-medium text-gray-700">
|
||||
Hardware ID (optional)
|
||||
</label>
|
||||
{suggestedHardwareId && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onHardwareIdChange(suggestedHardwareId)}
|
||||
className="text-xs text-blue-600 hover:text-blue-700 underline focus:outline-none"
|
||||
title={`Use detected ID: ${suggestedHardwareId}`}
|
||||
>
|
||||
Use: {suggestedHardwareId.length > 8 ? suggestedHardwareId.substring(0, 8) + '...' : suggestedHardwareId}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={hardwareId}
|
||||
|
|
|
|||
76
src/hooks/useTuioConnection.ts
Normal file
76
src/hooks/useTuioConnection.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
import { useTuioStore } from '../stores/tuioStore';
|
||||
import { TuioClientManager } from '../lib/tuio/tuioClient';
|
||||
import type { TuioTangibleInfo } from '../lib/tuio/types';
|
||||
|
||||
/**
|
||||
* Hook to manage TUIO client connection lifecycle
|
||||
*
|
||||
* @param shouldConnect - Whether to connect to TUIO server
|
||||
* @param onTangibleAdd - Optional callback when tangible is added
|
||||
* @param onTangibleUpdate - Optional callback when tangible is updated
|
||||
* @param onTangibleRemove - Optional callback when tangible is removed
|
||||
*/
|
||||
export function useTuioConnection(
|
||||
shouldConnect: boolean,
|
||||
callbacks?: {
|
||||
onTangibleAdd?: (hardwareId: string, info: TuioTangibleInfo) => void;
|
||||
onTangibleUpdate?: (hardwareId: string, info: TuioTangibleInfo) => void;
|
||||
onTangibleRemove?: (hardwareId: string) => void;
|
||||
}
|
||||
) {
|
||||
const clientRef = useRef<TuioClientManager | null>(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]);
|
||||
}
|
||||
|
|
@ -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<TuioClientManager | null>(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(
|
||||
{
|
||||
// Use the shared TUIO connection hook with presentation mode callbacks
|
||||
useTuioConnection(presentationMode, {
|
||||
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]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue