From 5362536fb15ee8683e642229437c01941e0d5861 Mon Sep 17 00:00:00 2001 From: sl-webui Date: Mon, 2 Mar 2026 13:24:34 -0500 Subject: [PATCH] feat(webui): waypoint editor with click-to-navigate (Issue #261) --- ui/social-bot/src/App.jsx | 3 - .../src/components/WaypointEditor.jsx | 84 +++++++++++++++---- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/ui/social-bot/src/App.jsx b/ui/social-bot/src/App.jsx index cdf0f5f..d64246b 100644 --- a/ui/social-bot/src/App.jsx +++ b/ui/social-bot/src/App.jsx @@ -61,9 +61,6 @@ import { NetworkPanel } from './components/NetworkPanel.jsx'; // Waypoint editor (issue #261) import { WaypointEditor } from './components/WaypointEditor.jsx'; -// Status header (issue #269) -import { StatusHeader } from './components/StatusHeader.jsx'; - const TAB_GROUPS = [ { label: 'SOCIAL', diff --git a/ui/social-bot/src/components/WaypointEditor.jsx b/ui/social-bot/src/components/WaypointEditor.jsx index ffbf571..3069d68 100644 --- a/ui/social-bot/src/components/WaypointEditor.jsx +++ b/ui/social-bot/src/components/WaypointEditor.jsx @@ -10,11 +10,13 @@ * - Execute waypoint sequence with automatic progression * - Clear all waypoints button * - Visual feedback for active waypoint (executing) + * - Imports map display from MapViewer for coordinate system */ import { useEffect, useRef, useState } from 'react'; function WaypointEditor({ subscribe, publish, callService }) { + // Waypoint storage const [waypoints, setWaypoints] = useState([]); const [selectedWaypoint, setSelectedWaypoint] = useState(null); const [isDragging, setIsDragging] = useState(false); @@ -22,16 +24,20 @@ function WaypointEditor({ subscribe, publish, callService }) { const [activeWaypoint, setActiveWaypoint] = useState(null); const [executing, setExecuting] = useState(false); + // Map context const [mapData, setMapData] = useState(null); const [robotPose, setRobotPose] = useState({ x: 0, y: 0, theta: 0 }); + // Canvas reference + const canvasRef = useRef(null); const containerRef = useRef(null); + // Refs for ROS integration const mapDataRef = useRef(null); const robotPoseRef = useRef({ x: 0, y: 0, theta: 0 }); const waypointsRef = useRef([]); - // Subscribe to map data + // Subscribe to map data (for coordinate reference) useEffect(() => { const unsubMap = subscribe( '/map', @@ -54,7 +60,7 @@ function WaypointEditor({ subscribe, publish, callService }) { return unsubMap; }, [subscribe]); - // Subscribe to robot odometry + // Subscribe to robot odometry (for current position reference) useEffect(() => { const unsubOdom = subscribe( '/odom', @@ -79,22 +85,29 @@ function WaypointEditor({ subscribe, publish, callService }) { return unsubOdom; }, [subscribe]); + // Canvas event handlers const handleCanvasClick = (e) => { - if (!mapDataRef.current || !containerRef.current) return; + if (!mapDataRef.current || !canvasRef.current) return; - const rect = containerRef.current.getBoundingClientRect(); + const canvas = canvasRef.current; + const rect = canvas.getBoundingClientRect(); const clickX = e.clientX - rect.left; const clickY = e.clientY - rect.top; + // Convert canvas coordinates to world coordinates + // This assumes the map is centered on the robot + const map = mapDataRef.current; const robot = robotPoseRef.current; - const zoom = 1; + const zoom = 1; // Would need to track zoom if map has zoom controls - const centerX = containerRef.current.clientWidth / 2; - const centerY = containerRef.current.clientHeight / 2; + // Inverse of map rendering calculation + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; const worldX = robot.x + (clickX - centerX) / zoom; const worldY = robot.y - (clickY - centerY) / zoom; + // Create new waypoint const newWaypoint = { id: Date.now(), x: parseFloat(worldX.toFixed(2)), @@ -106,6 +119,12 @@ function WaypointEditor({ subscribe, publish, callService }) { waypointsRef.current = [...waypointsRef.current, newWaypoint]; }; + const handleCanvasContextMenu = (e) => { + e.preventDefault(); + // Right-click handled by waypoint list + }; + + // Waypoint list handlers const handleDeleteWaypoint = (id) => { setWaypoints((prev) => prev.filter((wp) => wp.id !== id)); waypointsRef.current = waypointsRef.current.filter((wp) => wp.id !== id); @@ -139,12 +158,18 @@ function WaypointEditor({ subscribe, publish, callService }) { setDragIndex(null); }; + // Execute waypoints const sendNavGoal = async (waypoint) => { if (!callService) return; try { + // Create quaternion from heading (default to 0 if no heading) const heading = waypoint.theta || 0; const halfHeading = heading / 2; + const qx = 0; + const qy = 0; + const qz = Math.sin(halfHeading); + const qw = Math.cos(halfHeading); const goal = { pose: { @@ -154,14 +179,15 @@ function WaypointEditor({ subscribe, publish, callService }) { z: 0, }, orientation: { - x: 0, - y: 0, - z: Math.sin(halfHeading), - w: Math.cos(halfHeading), + x: qx, + y: qy, + z: qz, + w: qw, }, }, }; + // Send to Nav2 navigate_to_pose action await callService( '/navigate_to_pose', 'nav2_msgs/NavigateToPose', @@ -182,7 +208,11 @@ function WaypointEditor({ subscribe, publish, callService }) { setExecuting(true); for (const waypoint of waypoints) { const success = await sendNavGoal(waypoint); - if (!success) break; + if (!success) { + console.error('Failed to send goal for waypoint:', waypoint); + break; + } + // Wait a bit before sending next goal await new Promise((resolve) => setTimeout(resolve, 500)); } setExecuting(false); @@ -207,16 +237,21 @@ function WaypointEditor({ subscribe, publish, callService }) { return (
- {/* Map area */} + {/* Map area with click handlers */}
e.preventDefault()} + onContextMenu={handleCanvasContextMenu} > - + {/* Virtual map display - waypoints overlaid */} + + {/* Waypoint markers */} {waypoints.map((wp, idx) => { if (!mapDataRef.current) return null; @@ -233,6 +268,7 @@ function WaypointEditor({ subscribe, publish, callService }) { return ( + {/* Waypoint circle */} + {/* Waypoint number */} {idx + 1} + {/* Line to next waypoint */} {idx < waypoints.length - 1 && ( ); })} + + {/* Robot position marker */} {waypoints.length === 0 ? ( -
Click map to add waypoints
+
+ Click map to add waypoints +
) : ( waypoints.map((wp, idx) => (