import { memo, useEffect, useRef, useState } from "react"; import { Copy, RotateCcw } from "lucide-react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "../ui/accordion"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Slider } from "../ui/slider"; import { Switch } from "../ui/switch"; import FileSheet from "./FileSheet"; const fieldLabelClasses = "block text-xs font-medium text-muted-foreground"; const compactInputClasses = "h-8 text-xs font-medium tabular-nums"; function formatJointValue(valueDeg) { const rounded = Math.round(Number(valueDeg) * 10) / 10; return `${Number.isFinite(rounded) ? rounded : 0}\u00b0`; } function formatJointInput(valueDeg) { const rounded = Math.round(Number(valueDeg) * 10) / 10; return Number.isFinite(rounded) ? String(rounded) : ""; } function formatAnimationSpeed(speed) { const rounded = Math.round(Number(speed) * 100) / 100; return `${Number.isFinite(rounded) ? rounded.toFixed(2).replace(/\.?0+$/, "") : "1"}x`; } function clampJointInputValue(valueDeg, minValueDeg, maxValueDeg, fallbackValueDeg) { const numericValue = Number.isFinite(Number(valueDeg)) ? Number(valueDeg) : fallbackValueDeg; return Math.min(Math.max(numericValue, minValueDeg), Math.max(minValueDeg, maxValueDeg)); } const UrdfJointRow = memo(function UrdfJointRow({ joint, valueDeg, onValueChange }) { const jointName = String(joint?.name || "").trim(); const minValueDeg = Number.isFinite(Number(joint?.minValueDeg)) ? Number(joint.minValueDeg) : -180; const maxValueDeg = Number.isFinite(Number(joint?.maxValueDeg)) ? Number(joint.maxValueDeg) : 180; const safeValueDeg = clampJointInputValue(valueDeg, minValueDeg, maxValueDeg, 0); const pendingFrameRef = useRef(0); const pendingValueRef = useRef(safeValueDeg); const [liveValueDeg, setLiveValueDeg] = useState(safeValueDeg); const [draftValue, setDraftValue] = useState(() => formatJointInput(safeValueDeg)); useEffect(() => { pendingValueRef.current = safeValueDeg; setLiveValueDeg(safeValueDeg); setDraftValue(formatJointInput(safeValueDeg)); }, [safeValueDeg]); useEffect(() => () => { if (pendingFrameRef.current && typeof cancelAnimationFrame === "function") { cancelAnimationFrame(pendingFrameRef.current); } }, []); const scheduleValueChange = (nextValueDeg) => { pendingValueRef.current = nextValueDeg; if (typeof requestAnimationFrame !== "function") { onValueChange(joint, nextValueDeg); return; } if (pendingFrameRef.current) { return; } pendingFrameRef.current = requestAnimationFrame(() => { pendingFrameRef.current = 0; onValueChange(joint, pendingValueRef.current); }); }; const commitValue = (nextValueDeg) => { const normalizedValueDeg = clampJointInputValue(nextValueDeg, minValueDeg, maxValueDeg, liveValueDeg); pendingValueRef.current = normalizedValueDeg; if (pendingFrameRef.current && typeof cancelAnimationFrame === "function") { cancelAnimationFrame(pendingFrameRef.current); pendingFrameRef.current = 0; } setLiveValueDeg(normalizedValueDeg); setDraftValue(formatJointInput(normalizedValueDeg)); onValueChange(joint, normalizedValueDeg); }; return (
{formatJointValue(minValueDeg)} {formatJointValue(maxValueDeg)}
); }); export default function UrdfFileSheet({ open, isDesktop, width, joints, poses, activePoseName, jointValues, onJointValueChange, onPoseSelect, onCopyJointAngles, introAnimationEnabled = false, animationSpeed = 1, onIntroAnimationEnabledChange, onReplayIntroAnimation, onAnimationSpeedChange, onResetPose }) { const movableJoints = Array.isArray(joints) ? joints : []; const posePresets = Array.isArray(poses) ? poses : []; const defaultSections = posePresets.length ? ["joints", "poses", "animation"] : ["joints", "animation"]; return ( Joints {movableJoints.length ? ( <> {movableJoints.map((joint) => ( ))}
) : (

No movable joints are available.

)}
{posePresets.length ? ( Poses
{posePresets.map((pose) => { const poseName = String(pose?.name || "").trim() || "Pose"; const active = poseName === activePoseName; const jointCount = Object.keys(pose?.jointValuesByName && typeof pose.jointValuesByName === "object" ? pose.jointValuesByName : {}).length; return ( ); })}
) : null} Animation
Entry animation
Joint motion speed {formatAnimationSpeed(animationSpeed)}
{ onAnimationSpeedChange?.(nextValue?.[0] ?? animationSpeed); }} aria-label="URDF joint motion speed" />
Slower Faster
); }