blanchon's picture
Update
6ce4ca6
<script lang="ts">
import { T } from "@threlte/core";
import { Billboard } from "@threlte/extras";
import type { Robot } from '../Robot.svelte.js';
import { interactivity, type IntersectionEvent, useCursor } from "@threlte/extras";
import { getRootLinks } from "@/components/3d/elements/robot/URDF/utils/UrdfParser";
import UrdfLink from "@/components/3d/elements/robot/URDF/primitives/UrdfLink.svelte";
import { ROBOT_CONFIG } from '../config.js';
interface Props {
robot: Robot;
onInteract: (robot: Robot, type: 'consumer' | 'producer' | 'manual') => void;
}
let { robot, onInteract }: Props = $props();
// Reactive values
const position = $derived(robot.position);
const hasConsumer = $derived(robot.hasConsumer);
const outputDriverCount = $derived(robot.outputDriverCount);
const isManualControl = $derived(robot.isManualControlEnabled);
const connectionStatus = $derived(robot.connectionStatus);
const jointArray = $derived(robot.jointArray);
// Use the robot's stored URDF state (loaded once when robot was created)
const urdfRobotState = $derived(robot.urdfRobotState);
let isHovered = $state(false);
let isSelected = $state(false);
let lastJointValues = $state<Record<string, number>>({});
// Sync joint values from simplified Robot to URDF joints with optimized updates
$effect(() => {
if (!urdfRobotState || jointArray.length === 0) return;
// Check if this is the initial sync (no previous values recorded)
const isInitialSync = Object.keys(lastJointValues).length === 0;
// Check if any joint values have actually changed (using config threshold)
const threshold = isInitialSync ? 0 : ROBOT_CONFIG.performance.uiUpdateThreshold;
const hasSignificantChanges = isInitialSync || jointArray.some(joint =>
Math.abs((lastJointValues[joint.name] || 0) - joint.value) > threshold
);
if (!hasSignificantChanges) return;
// Batch update all joints that have changed (or all joints on initial sync)
let updatedCount = 0;
jointArray.forEach(joint => {
if (isInitialSync || Math.abs((lastJointValues[joint.name] || 0) - joint.value) > threshold) {
lastJointValues[joint.name] = joint.value;
const urdfJoint = findUrdfJoint(urdfRobotState, joint.name);
if (urdfJoint) {
// Initialize rotation array if it doesn't exist
if (!urdfJoint.rotation) {
urdfJoint.rotation = [0, 0, 0];
}
// Use the Robot's conversion method for proper coordinate mapping
const radians = robot.convertNormalizedToUrdfRadians(joint.name, joint.value);
const axis = urdfJoint.axis_xyz || [0, 0, 1];
// Reset rotation and apply to the appropriate axis
urdfJoint.rotation = [0, 0, 0];
for (let i = 0; i < 3; i++) {
if (Math.abs(axis[i]) > 0.001) {
urdfJoint.rotation[i] = radians * axis[i];
}
}
updatedCount++;
}
}
});
if (updatedCount > 0) {
console.debug(`${isInitialSync ? 'Initial sync: ' : ''}Updated ${updatedCount} URDF joints for robot ${robot.id}`);
}
});
function findUrdfJoint(robot: any, jointName: string): any {
// Search through the robot's joints array
if (robot.joints && Array.isArray(robot.joints)) {
for (const joint of robot.joints) {
if (joint.name === jointName) {
return joint;
}
}
}
return null;
}
const { onPointerEnter, onPointerLeave } = useCursor();
interactivity();
</script>
<T.Group
position.x={position.x}
position.y={position.y}
position.z={position.z}
scale={[10, 10, 10]}
rotation={[-Math.PI / 2, 0, 0]}
>
{#if urdfRobotState}
<!-- URDF Robot representation -->
<T.Group
onclick={(event: IntersectionEvent<MouseEvent>) => {
event.stopPropagation();
isSelected = true;
onInteract(robot, 'manual');
}}
onpointerenter={(event: IntersectionEvent<PointerEvent>) => {
event.stopPropagation();
onPointerEnter();
isHovered = true;
}}
onpointerleave={(event: IntersectionEvent<PointerEvent>) => {
event.stopPropagation();
onPointerLeave();
isHovered = false;
}}
>
{#each getRootLinks(urdfRobotState) as link}
<UrdfLink
robot={urdfRobotState}
{link}
textScale={0.2}
showName={isHovered || isSelected}
showVisual={true}
showCollision={false}
visualColor={connectionStatus.isConnected ? "#10b981" : "#6b7280"}
visualOpacity={isHovered || isSelected ? 0.4 : 1.0}
collisionOpacity={1.0}
collisionColor="#813d9c"
jointNames={isHovered}
joints={isHovered}
jointColor="#62a0ea"
jointIndicatorColor="#f66151"
nameHeight={0.1}
showLine={isHovered || isSelected}
opacity={1}
isInteractive={false}
/>
{/each}
</T.Group>
{:else}
<!-- Fallback simple representation while URDF loads -->
<T.Mesh
onpointerenter={() => isHovered = true}
onpointerleave={() => isHovered = false}
onclick={() => onInteract(robot, 'manual')}
>
<T.BoxGeometry args={[0.1, 0.1, 0.1]} />
<T.MeshStandardMaterial
color={connectionStatus.isConnected ? "#10b981" : "#6b7280"}
opacity={isHovered ? 0.8 : 1.0}
transparent
/>
</T.Mesh>
{/if}
<!-- Status billboard when hovered -->
{#if isHovered}
<Billboard>
<T.Group position.y={1.5}>
<div class="bg-slate-800/90 rounded-lg p-3 text-white text-sm min-w-48 backdrop-blur">
<div class="font-semibold mb-2">{robot.id}</div>
<!-- Connection status boxes -->
<div class="flex gap-2 mb-2">
<!-- Consumer status -->
<button
onclick={() => onInteract(robot, 'consumer')}
class="flex-1 p-2 rounded border transition-colors {hasConsumer ? 'bg-green-600 border-green-500' : 'bg-gray-600 border-gray-500 hover:bg-gray-500'}"
>
<div class="text-xs">Consumer</div>
<div class="text-[10px] opacity-75">
{hasConsumer ? 'Connected' : 'None'}
</div>
</button>
<!-- Robot status -->
<div class="flex-1 p-2 rounded border border-yellow-500 bg-yellow-600">
<div class="text-xs">Robot</div>
<div class="text-[10px] opacity-75">{robot.jointArray.length} joints</div>
</div>
<!-- Producer status -->
<button
onclick={() => onInteract(robot, 'producer')}
class="flex-1 p-2 rounded border transition-colors {outputDriverCount > 0 ? 'bg-blue-600 border-blue-500' : 'bg-gray-600 border-gray-500 hover:bg-gray-500'}"
>
<div class="text-xs">Producer</div>
<div class="text-[10px] opacity-75">
{outputDriverCount} driver{outputDriverCount !== 1 ? 's' : ''}
</div>
</button>
</div>
<!-- Control status -->
<div class="text-xs text-center px-2 py-1 rounded {isManualControl ? 'bg-purple-600' : 'bg-orange-600'}">
{isManualControl ? 'Manual Control' : 'External Control'}
</div>
</div>
</T.Group>
</Billboard>
{/if}
</T.Group>