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 { useState, useEffect, KeyboardEvent } from "react";
|
||||||
import SaveIcon from "@mui/icons-material/Save";
|
import SaveIcon from "@mui/icons-material/Save";
|
||||||
|
import { useTuioStore } from "../../stores/tuioStore";
|
||||||
import TangibleForm from "./TangibleForm";
|
import TangibleForm from "./TangibleForm";
|
||||||
import type { TangibleConfig, TangibleMode, LabelConfig, FilterConfig, NodeTypeConfig, EdgeTypeConfig } from "../../types";
|
import type { TangibleConfig, TangibleMode, LabelConfig, FilterConfig, NodeTypeConfig, EdgeTypeConfig } from "../../types";
|
||||||
import type { ConstellationState } from "../../types/timeline";
|
import type { ConstellationState } from "../../types/timeline";
|
||||||
|
|
@ -34,6 +35,11 @@ const EditTangibleInline = ({
|
||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: Props) => {
|
}: 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 [name, setName] = useState("");
|
||||||
const [mode, setMode] = useState<TangibleMode>("filter");
|
const [mode, setMode] = useState<TangibleMode>("filter");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
|
|
@ -128,6 +134,7 @@ const EditTangibleInline = ({
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
edgeTypes={edgeTypes}
|
edgeTypes={edgeTypes}
|
||||||
states={states}
|
states={states}
|
||||||
|
suggestedHardwareId={suggestedHardwareId}
|
||||||
onNameChange={setName}
|
onNameChange={setName}
|
||||||
onModeChange={setMode}
|
onModeChange={setMode}
|
||||||
onDescriptionChange={setDescription}
|
onDescriptionChange={setDescription}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useState, useRef, KeyboardEvent } from "react";
|
import { useState, useRef, KeyboardEvent } from "react";
|
||||||
|
import { useTuioStore } from "../../stores/tuioStore";
|
||||||
import TangibleForm from "./TangibleForm";
|
import TangibleForm from "./TangibleForm";
|
||||||
import type { TangibleMode, LabelConfig, FilterConfig, NodeTypeConfig, EdgeTypeConfig } from "../../types";
|
import type { TangibleMode, LabelConfig, FilterConfig, NodeTypeConfig, EdgeTypeConfig } from "../../types";
|
||||||
import type { ConstellationState } from "../../types/timeline";
|
import type { ConstellationState } from "../../types/timeline";
|
||||||
|
|
@ -19,6 +20,11 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const QuickAddTangibleForm = ({ labels, nodeTypes, edgeTypes, states, onAdd }: 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 [name, setName] = useState("");
|
||||||
const [hardwareId, setHardwareId] = useState("");
|
const [hardwareId, setHardwareId] = useState("");
|
||||||
const [mode, setMode] = useState<TangibleMode>("filter");
|
const [mode, setMode] = useState<TangibleMode>("filter");
|
||||||
|
|
@ -115,6 +121,7 @@ const QuickAddTangibleForm = ({ labels, nodeTypes, edgeTypes, states, onAdd }: P
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
edgeTypes={edgeTypes}
|
edgeTypes={edgeTypes}
|
||||||
states={states}
|
states={states}
|
||||||
|
suggestedHardwareId={suggestedHardwareId}
|
||||||
onNameChange={setName}
|
onNameChange={setName}
|
||||||
onHardwareIdChange={setHardwareId}
|
onHardwareIdChange={setHardwareId}
|
||||||
onModeChange={setMode}
|
onModeChange={setMode}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { useGraphWithHistory } from '../../hooks/useGraphWithHistory';
|
||||||
import { useConfirm } from '../../hooks/useConfirm';
|
import { useConfirm } from '../../hooks/useConfirm';
|
||||||
import { useToastStore } from '../../stores/toastStore';
|
import { useToastStore } from '../../stores/toastStore';
|
||||||
import { useTimelineStore } from '../../stores/timelineStore';
|
import { useTimelineStore } from '../../stores/timelineStore';
|
||||||
|
import { useTuioConnection } from '../../hooks/useTuioConnection';
|
||||||
import QuickAddTangibleForm from './QuickAddTangibleForm';
|
import QuickAddTangibleForm from './QuickAddTangibleForm';
|
||||||
import TangibleManagementList from './TangibleManagementList';
|
import TangibleManagementList from './TangibleManagementList';
|
||||||
import EditTangibleInline from './EditTangibleInline';
|
import EditTangibleInline from './EditTangibleInline';
|
||||||
|
|
@ -19,6 +20,9 @@ const TangibleConfigModal = ({ isOpen, onClose, initialEditingTangibleId }: Prop
|
||||||
const { confirm, ConfirmDialogComponent } = useConfirm();
|
const { confirm, ConfirmDialogComponent } = useConfirm();
|
||||||
const { showToast } = useToastStore();
|
const { showToast } = useToastStore();
|
||||||
|
|
||||||
|
// Connect to TUIO when dialog is open
|
||||||
|
useTuioConnection(isOpen);
|
||||||
|
|
||||||
const [editingTangible, setEditingTangible] = useState<TangibleConfigType | null>(null);
|
const [editingTangible, setEditingTangible] = useState<TangibleConfigType | null>(null);
|
||||||
|
|
||||||
// Get all available states for state mode
|
// Get all available states for state mode
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ interface Props {
|
||||||
nodeTypes: NodeTypeConfig[];
|
nodeTypes: NodeTypeConfig[];
|
||||||
edgeTypes: EdgeTypeConfig[];
|
edgeTypes: EdgeTypeConfig[];
|
||||||
states: ConstellationState[];
|
states: ConstellationState[];
|
||||||
|
suggestedHardwareId?: string;
|
||||||
onNameChange: (value: string) => void;
|
onNameChange: (value: string) => void;
|
||||||
onModeChange: (value: TangibleMode) => void;
|
onModeChange: (value: TangibleMode) => void;
|
||||||
onDescriptionChange: (value: string) => void;
|
onDescriptionChange: (value: string) => void;
|
||||||
|
|
@ -39,6 +40,7 @@ const TangibleForm = ({
|
||||||
nodeTypes,
|
nodeTypes,
|
||||||
edgeTypes,
|
edgeTypes,
|
||||||
states,
|
states,
|
||||||
|
suggestedHardwareId,
|
||||||
onNameChange,
|
onNameChange,
|
||||||
onModeChange,
|
onModeChange,
|
||||||
onDescriptionChange,
|
onDescriptionChange,
|
||||||
|
|
@ -63,9 +65,21 @@ const TangibleForm = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
Hardware ID (optional)
|
<label className="text-xs font-medium text-gray-700">
|
||||||
</label>
|
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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={hardwareId}
|
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 { useSettingsStore } from '../stores/settingsStore';
|
||||||
import { useGraphStore } from '../stores/graphStore';
|
import { useGraphStore } from '../stores/graphStore';
|
||||||
import { useTimelineStore } from '../stores/timelineStore';
|
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 { TuioTangibleInfo } from '../lib/tuio/types';
|
||||||
import type { TangibleConfig } from '../types';
|
import type { TangibleConfig } from '../types';
|
||||||
import { migrateTangibleConfig } from '../utils/tangibleMigration';
|
import { migrateTangibleConfig } from '../utils/tangibleMigration';
|
||||||
|
|
@ -20,54 +19,14 @@ import { migrateTangibleConfig } from '../utils/tangibleMigration';
|
||||||
* - State mode: Switches timeline state
|
* - State mode: Switches timeline state
|
||||||
*/
|
*/
|
||||||
export function useTuioIntegration() {
|
export function useTuioIntegration() {
|
||||||
const clientRef = useRef<TuioClientManager | null>(null);
|
|
||||||
const { presentationMode } = useSettingsStore();
|
const { presentationMode } = useSettingsStore();
|
||||||
const { websocketUrl, protocolVersion } = useTuioStore();
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Use the shared TUIO connection hook with presentation mode callbacks
|
||||||
// Only connect in presentation mode
|
useTuioConnection(presentationMode, {
|
||||||
if (!presentationMode) {
|
onTangibleAdd: handleTangibleAdd,
|
||||||
// Disconnect if we're leaving presentation mode
|
onTangibleUpdate: handleTangibleUpdate,
|
||||||
if (clientRef.current) {
|
onTangibleRemove: handleTangibleRemove,
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue