Compare commits
No commits in common. "b6104763c52eb5a2988038ad070e1eac8ab7a052" and "0776003dd31e5e5829efaee732499fbb8a36176b" have entirely different histories.
b6104763c5
...
0776003dd3
@ -5,10 +5,21 @@ from launch.actions import DeclareLaunchArgument
|
|||||||
import os
|
import os
|
||||||
from ament_index_python.packages import get_package_share_directory
|
from ament_index_python.packages import get_package_share_directory
|
||||||
|
|
||||||
|
|
||||||
def generate_launch_description():
|
def generate_launch_description():
|
||||||
pkg_dir = get_package_share_directory("saltybot_wheel_slip_detector")
|
pkg_dir = get_package_share_directory("saltybot_wheel_slip_detector")
|
||||||
config_file = os.path.join(pkg_dir, "config", "wheel_slip_config.yaml")
|
config_file = os.path.join(pkg_dir, "config", "wheel_slip_config.yaml")
|
||||||
return LaunchDescription([
|
return LaunchDescription([
|
||||||
DeclareLaunchArgument("config_file", default_value=config_file, description="Path to configuration YAML file"),
|
DeclareLaunchArgument(
|
||||||
Node(package="saltybot_wheel_slip_detector", executable="wheel_slip_detector_node", name="wheel_slip_detector", output="screen", parameters=[LaunchConfiguration("config_file")]),
|
"config_file",
|
||||||
|
default_value=config_file,
|
||||||
|
description="Path to configuration YAML file",
|
||||||
|
),
|
||||||
|
Node(
|
||||||
|
package="saltybot_wheel_slip_detector",
|
||||||
|
executable="wheel_slip_detector_node",
|
||||||
|
name="wheel_slip_detector",
|
||||||
|
output="screen",
|
||||||
|
parameters=[LaunchConfiguration("config_file")],
|
||||||
|
),
|
||||||
])
|
])
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
|
||||||
<package format="3">
|
|
||||||
<name>saltybot_wheel_slip_detector</name>
|
|
||||||
<version>0.1.0</version>
|
|
||||||
<description>Wheel slip detection by comparing commanded vs actual velocity.</description>
|
|
||||||
<maintainer email="seb@vayrette.com">Seb</maintainer>
|
|
||||||
<license>Apache-2.0</license>
|
|
||||||
<buildtool_depend>ament_python</buildtool_depend>
|
|
||||||
<depend>rclpy</depend>
|
|
||||||
<depend>geometry_msgs</depend>
|
|
||||||
<depend>std_msgs</depend>
|
|
||||||
<depend>nav_msgs</depend>
|
|
||||||
<test_depend>pytest</test_depend>
|
|
||||||
<export>
|
|
||||||
<build_type>ament_python</build_type>
|
|
||||||
</export>
|
|
||||||
</package>
|
|
||||||
@ -1,6 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
"""Wheel slip detector for SaltyBot.
|
||||||
|
|
||||||
|
Detects wheel slip by comparing commanded velocity vs actual encoder velocity.
|
||||||
|
Publishes Bool when slip is detected for >0.5s, enabling speed reduction response.
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import math
|
import math
|
||||||
|
|
||||||
import rclpy
|
import rclpy
|
||||||
from rclpy.node import Node
|
from rclpy.node import Node
|
||||||
from rclpy.timer import Timer
|
from rclpy.timer import Timer
|
||||||
@ -8,60 +15,82 @@ from geometry_msgs.msg import Twist
|
|||||||
from nav_msgs.msg import Odometry
|
from nav_msgs.msg import Odometry
|
||||||
from std_msgs.msg import Bool
|
from std_msgs.msg import Bool
|
||||||
|
|
||||||
|
|
||||||
class WheelSlipDetectorNode(Node):
|
class WheelSlipDetectorNode(Node):
|
||||||
|
"""ROS2 node for wheel slip detection."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("wheel_slip_detector")
|
super().__init__("wheel_slip_detector")
|
||||||
|
|
||||||
self.declare_parameter("frequency", 10)
|
self.declare_parameter("frequency", 10)
|
||||||
frequency = self.get_parameter("frequency").value
|
frequency = self.get_parameter("frequency").value
|
||||||
self.declare_parameter("slip_threshold", 0.1)
|
self.declare_parameter("slip_threshold", 0.1)
|
||||||
self.declare_parameter("slip_timeout", 0.5)
|
self.declare_parameter("slip_timeout", 0.5)
|
||||||
|
|
||||||
self.slip_threshold = self.get_parameter("slip_threshold").value
|
self.slip_threshold = self.get_parameter("slip_threshold").value
|
||||||
self.slip_timeout = self.get_parameter("slip_timeout").value
|
self.slip_timeout = self.get_parameter("slip_timeout").value
|
||||||
self.period = 1.0 / frequency
|
self.period = 1.0 / frequency
|
||||||
|
|
||||||
self.cmd_vel: Optional[Twist] = None
|
self.cmd_vel: Optional[Twist] = None
|
||||||
self.actual_vel: Optional[Twist] = None
|
self.actual_vel: Optional[Twist] = None
|
||||||
self.slip_duration = 0.0
|
self.slip_duration = 0.0
|
||||||
self.slip_detected = False
|
self.slip_detected = False
|
||||||
|
|
||||||
self.create_subscription(Twist, "/cmd_vel", self._on_cmd_vel, 10)
|
self.create_subscription(Twist, "/cmd_vel", self._on_cmd_vel, 10)
|
||||||
self.create_subscription(Odometry, "/odom", self._on_odom, 10)
|
self.create_subscription(Odometry, "/odom", self._on_odom, 10)
|
||||||
self.pub_slip = self.create_publisher(Bool, "/saltybot/wheel_slip_detected", 10)
|
self.pub_slip = self.create_publisher(Bool, "/saltybot/wheel_slip_detected", 10)
|
||||||
self.timer: Timer = self.create_timer(self.period, self._timer_callback)
|
self.timer: Timer = self.create_timer(self.period, self._timer_callback)
|
||||||
self.get_logger().info(f"Wheel slip detector initialized at {frequency}Hz. Threshold: {self.slip_threshold} m/s, Timeout: {self.slip_timeout}s")
|
|
||||||
|
self.get_logger().info(
|
||||||
|
f"Wheel slip detector initialized at {frequency}Hz. "
|
||||||
|
f"Threshold: {self.slip_threshold} m/s, Timeout: {self.slip_timeout}s"
|
||||||
|
)
|
||||||
|
|
||||||
def _on_cmd_vel(self, msg: Twist) -> None:
|
def _on_cmd_vel(self, msg: Twist) -> None:
|
||||||
|
"""Update commanded velocity from subscription."""
|
||||||
self.cmd_vel = msg
|
self.cmd_vel = msg
|
||||||
|
|
||||||
def _on_odom(self, msg: Odometry) -> None:
|
def _on_odom(self, msg: Odometry) -> None:
|
||||||
|
"""Update actual velocity from odometry subscription."""
|
||||||
self.actual_vel = msg.twist.twist
|
self.actual_vel = msg.twist.twist
|
||||||
|
|
||||||
def _timer_callback(self) -> None:
|
def _timer_callback(self) -> None:
|
||||||
|
"""Detect wheel slip and publish detection flag."""
|
||||||
if self.cmd_vel is None or self.actual_vel is None:
|
if self.cmd_vel is None or self.actual_vel is None:
|
||||||
slip_detected = False
|
slip_detected = False
|
||||||
else:
|
else:
|
||||||
slip_detected = self._check_slip()
|
slip_detected = self._check_slip()
|
||||||
|
|
||||||
if slip_detected:
|
if slip_detected:
|
||||||
self.slip_duration += self.period
|
self.slip_duration += self.period
|
||||||
else:
|
else:
|
||||||
self.slip_duration = 0.0
|
self.slip_duration = 0.0
|
||||||
|
|
||||||
is_slip = self.slip_duration > self.slip_timeout
|
is_slip = self.slip_duration > self.slip_timeout
|
||||||
|
|
||||||
if is_slip != self.slip_detected:
|
if is_slip != self.slip_detected:
|
||||||
self.slip_detected = is_slip
|
self.slip_detected = is_slip
|
||||||
if self.slip_detected:
|
if self.slip_detected:
|
||||||
self.get_logger().warn(f"WHEEL SLIP DETECTED: {self.slip_duration:.2f}s")
|
self.get_logger().warn(f"WHEEL SLIP DETECTED: {self.slip_duration:.2f}s")
|
||||||
else:
|
else:
|
||||||
self.get_logger().info("Wheel slip cleared")
|
self.get_logger().info("Wheel slip cleared")
|
||||||
|
|
||||||
slip_msg = Bool()
|
slip_msg = Bool()
|
||||||
slip_msg.data = is_slip
|
slip_msg.data = is_slip
|
||||||
self.pub_slip.publish(slip_msg)
|
self.pub_slip.publish(slip_msg)
|
||||||
|
|
||||||
def _check_slip(self) -> bool:
|
def _check_slip(self) -> bool:
|
||||||
|
"""Check if velocity difference indicates slip."""
|
||||||
cmd_speed = math.sqrt(self.cmd_vel.linear.x**2 + self.cmd_vel.linear.y**2)
|
cmd_speed = math.sqrt(self.cmd_vel.linear.x**2 + self.cmd_vel.linear.y**2)
|
||||||
actual_speed = math.sqrt(self.actual_vel.linear.x**2 + self.actual_vel.linear.y**2)
|
actual_speed = math.sqrt(self.actual_vel.linear.x**2 + self.actual_vel.linear.y**2)
|
||||||
vel_diff = abs(cmd_speed - actual_speed)
|
vel_diff = abs(cmd_speed - actual_speed)
|
||||||
|
|
||||||
if cmd_speed < 0.05 and actual_speed < 0.05:
|
if cmd_speed < 0.05 and actual_speed < 0.05:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return vel_diff > self.slip_threshold
|
return vel_diff > self.slip_threshold
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
def main(args=None):
|
||||||
rclpy.init(args=args)
|
rclpy.init(args=args)
|
||||||
node = WheelSlipDetectorNode()
|
node = WheelSlipDetectorNode()
|
||||||
@ -73,5 +102,6 @@ def main(args=None):
|
|||||||
node.destroy_node()
|
node.destroy_node()
|
||||||
rclpy.shutdown()
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
[develop]
|
|
||||||
script-dir=$base/lib/saltybot_wheel_slip_detector
|
|
||||||
[install]
|
|
||||||
install-scripts=$base/lib/saltybot_wheel_slip_detector
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
package_name = "saltybot_wheel_slip_detector"
|
package_name = "saltybot_wheel_slip_detector"
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name=package_name,
|
name=package_name,
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
@ -17,5 +19,9 @@ setup(
|
|||||||
description="Wheel slip detection from velocity command/actual mismatch",
|
description="Wheel slip detection from velocity command/actual mismatch",
|
||||||
license="Apache-2.0",
|
license="Apache-2.0",
|
||||||
tests_require=["pytest"],
|
tests_require=["pytest"],
|
||||||
entry_points={"console_scripts": ["wheel_slip_detector_node = saltybot_wheel_slip_detector.wheel_slip_detector_node:main"]},
|
entry_points={
|
||||||
|
"console_scripts": [
|
||||||
|
"wheel_slip_detector_node = saltybot_wheel_slip_detector.wheel_slip_detector_node:main",
|
||||||
|
],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
@ -61,6 +61,9 @@ import { NetworkPanel } from './components/NetworkPanel.jsx';
|
|||||||
// Waypoint editor (issue #261)
|
// Waypoint editor (issue #261)
|
||||||
import { WaypointEditor } from './components/WaypointEditor.jsx';
|
import { WaypointEditor } from './components/WaypointEditor.jsx';
|
||||||
|
|
||||||
|
// Status header (issue #269)
|
||||||
|
import { StatusHeader } from './components/StatusHeader.jsx';
|
||||||
|
|
||||||
const TAB_GROUPS = [
|
const TAB_GROUPS = [
|
||||||
{
|
{
|
||||||
label: 'SOCIAL',
|
label: 'SOCIAL',
|
||||||
|
|||||||
@ -10,13 +10,11 @@
|
|||||||
* - Execute waypoint sequence with automatic progression
|
* - Execute waypoint sequence with automatic progression
|
||||||
* - Clear all waypoints button
|
* - Clear all waypoints button
|
||||||
* - Visual feedback for active waypoint (executing)
|
* - Visual feedback for active waypoint (executing)
|
||||||
* - Imports map display from MapViewer for coordinate system
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
function WaypointEditor({ subscribe, publish, callService }) {
|
function WaypointEditor({ subscribe, publish, callService }) {
|
||||||
// Waypoint storage
|
|
||||||
const [waypoints, setWaypoints] = useState([]);
|
const [waypoints, setWaypoints] = useState([]);
|
||||||
const [selectedWaypoint, setSelectedWaypoint] = useState(null);
|
const [selectedWaypoint, setSelectedWaypoint] = useState(null);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
@ -24,20 +22,16 @@ function WaypointEditor({ subscribe, publish, callService }) {
|
|||||||
const [activeWaypoint, setActiveWaypoint] = useState(null);
|
const [activeWaypoint, setActiveWaypoint] = useState(null);
|
||||||
const [executing, setExecuting] = useState(false);
|
const [executing, setExecuting] = useState(false);
|
||||||
|
|
||||||
// Map context
|
|
||||||
const [mapData, setMapData] = useState(null);
|
const [mapData, setMapData] = useState(null);
|
||||||
const [robotPose, setRobotPose] = useState({ x: 0, y: 0, theta: 0 });
|
const [robotPose, setRobotPose] = useState({ x: 0, y: 0, theta: 0 });
|
||||||
|
|
||||||
// Canvas reference
|
|
||||||
const canvasRef = useRef(null);
|
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
// Refs for ROS integration
|
|
||||||
const mapDataRef = useRef(null);
|
const mapDataRef = useRef(null);
|
||||||
const robotPoseRef = useRef({ x: 0, y: 0, theta: 0 });
|
const robotPoseRef = useRef({ x: 0, y: 0, theta: 0 });
|
||||||
const waypointsRef = useRef([]);
|
const waypointsRef = useRef([]);
|
||||||
|
|
||||||
// Subscribe to map data (for coordinate reference)
|
// Subscribe to map data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubMap = subscribe(
|
const unsubMap = subscribe(
|
||||||
'/map',
|
'/map',
|
||||||
@ -60,7 +54,7 @@ function WaypointEditor({ subscribe, publish, callService }) {
|
|||||||
return unsubMap;
|
return unsubMap;
|
||||||
}, [subscribe]);
|
}, [subscribe]);
|
||||||
|
|
||||||
// Subscribe to robot odometry (for current position reference)
|
// Subscribe to robot odometry
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubOdom = subscribe(
|
const unsubOdom = subscribe(
|
||||||
'/odom',
|
'/odom',
|
||||||
@ -85,29 +79,22 @@ function WaypointEditor({ subscribe, publish, callService }) {
|
|||||||
return unsubOdom;
|
return unsubOdom;
|
||||||
}, [subscribe]);
|
}, [subscribe]);
|
||||||
|
|
||||||
// Canvas event handlers
|
|
||||||
const handleCanvasClick = (e) => {
|
const handleCanvasClick = (e) => {
|
||||||
if (!mapDataRef.current || !canvasRef.current) return;
|
if (!mapDataRef.current || !containerRef.current) return;
|
||||||
|
|
||||||
const canvas = canvasRef.current;
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
const clickX = e.clientX - rect.left;
|
const clickX = e.clientX - rect.left;
|
||||||
const clickY = e.clientY - rect.top;
|
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 robot = robotPoseRef.current;
|
||||||
const zoom = 1; // Would need to track zoom if map has zoom controls
|
const zoom = 1;
|
||||||
|
|
||||||
// Inverse of map rendering calculation
|
const centerX = containerRef.current.clientWidth / 2;
|
||||||
const centerX = canvas.width / 2;
|
const centerY = containerRef.current.clientHeight / 2;
|
||||||
const centerY = canvas.height / 2;
|
|
||||||
|
|
||||||
const worldX = robot.x + (clickX - centerX) / zoom;
|
const worldX = robot.x + (clickX - centerX) / zoom;
|
||||||
const worldY = robot.y - (clickY - centerY) / zoom;
|
const worldY = robot.y - (clickY - centerY) / zoom;
|
||||||
|
|
||||||
// Create new waypoint
|
|
||||||
const newWaypoint = {
|
const newWaypoint = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
x: parseFloat(worldX.toFixed(2)),
|
x: parseFloat(worldX.toFixed(2)),
|
||||||
@ -119,12 +106,6 @@ function WaypointEditor({ subscribe, publish, callService }) {
|
|||||||
waypointsRef.current = [...waypointsRef.current, newWaypoint];
|
waypointsRef.current = [...waypointsRef.current, newWaypoint];
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCanvasContextMenu = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
// Right-click handled by waypoint list
|
|
||||||
};
|
|
||||||
|
|
||||||
// Waypoint list handlers
|
|
||||||
const handleDeleteWaypoint = (id) => {
|
const handleDeleteWaypoint = (id) => {
|
||||||
setWaypoints((prev) => prev.filter((wp) => wp.id !== id));
|
setWaypoints((prev) => prev.filter((wp) => wp.id !== id));
|
||||||
waypointsRef.current = waypointsRef.current.filter((wp) => wp.id !== id);
|
waypointsRef.current = waypointsRef.current.filter((wp) => wp.id !== id);
|
||||||
@ -158,18 +139,12 @@ function WaypointEditor({ subscribe, publish, callService }) {
|
|||||||
setDragIndex(null);
|
setDragIndex(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Execute waypoints
|
|
||||||
const sendNavGoal = async (waypoint) => {
|
const sendNavGoal = async (waypoint) => {
|
||||||
if (!callService) return;
|
if (!callService) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create quaternion from heading (default to 0 if no heading)
|
|
||||||
const heading = waypoint.theta || 0;
|
const heading = waypoint.theta || 0;
|
||||||
const halfHeading = heading / 2;
|
const halfHeading = heading / 2;
|
||||||
const qx = 0;
|
|
||||||
const qy = 0;
|
|
||||||
const qz = Math.sin(halfHeading);
|
|
||||||
const qw = Math.cos(halfHeading);
|
|
||||||
|
|
||||||
const goal = {
|
const goal = {
|
||||||
pose: {
|
pose: {
|
||||||
@ -179,15 +154,14 @@ function WaypointEditor({ subscribe, publish, callService }) {
|
|||||||
z: 0,
|
z: 0,
|
||||||
},
|
},
|
||||||
orientation: {
|
orientation: {
|
||||||
x: qx,
|
x: 0,
|
||||||
y: qy,
|
y: 0,
|
||||||
z: qz,
|
z: Math.sin(halfHeading),
|
||||||
w: qw,
|
w: Math.cos(halfHeading),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send to Nav2 navigate_to_pose action
|
|
||||||
await callService(
|
await callService(
|
||||||
'/navigate_to_pose',
|
'/navigate_to_pose',
|
||||||
'nav2_msgs/NavigateToPose',
|
'nav2_msgs/NavigateToPose',
|
||||||
@ -208,11 +182,7 @@ function WaypointEditor({ subscribe, publish, callService }) {
|
|||||||
setExecuting(true);
|
setExecuting(true);
|
||||||
for (const waypoint of waypoints) {
|
for (const waypoint of waypoints) {
|
||||||
const success = await sendNavGoal(waypoint);
|
const success = await sendNavGoal(waypoint);
|
||||||
if (!success) {
|
if (!success) break;
|
||||||
console.error('Failed to send goal for waypoint:', waypoint);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Wait a bit before sending next goal
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
}
|
}
|
||||||
setExecuting(false);
|
setExecuting(false);
|
||||||
@ -237,21 +207,16 @@ function WaypointEditor({ subscribe, publish, callService }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-3">
|
<div className="flex h-full gap-3">
|
||||||
{/* Map area with click handlers */}
|
{/* Map area */}
|
||||||
<div className="flex-1 flex flex-col space-y-3">
|
<div className="flex-1 flex flex-col space-y-3">
|
||||||
<div className="flex-1 bg-gray-900 rounded-lg border border-cyan-950 overflow-hidden relative cursor-crosshair">
|
<div className="flex-1 bg-gray-900 rounded-lg border border-cyan-950 overflow-hidden relative cursor-crosshair">
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
onClick={handleCanvasClick}
|
onClick={handleCanvasClick}
|
||||||
onContextMenu={handleCanvasContextMenu}
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
{/* Virtual map display - waypoints overlaid */}
|
<svg className="absolute inset-0 w-full h-full pointer-events-none" id="waypoint-overlay">
|
||||||
<svg
|
|
||||||
className="absolute inset-0 w-full h-full pointer-events-none"
|
|
||||||
id="waypoint-overlay"
|
|
||||||
>
|
|
||||||
{/* Waypoint markers */}
|
|
||||||
{waypoints.map((wp, idx) => {
|
{waypoints.map((wp, idx) => {
|
||||||
if (!mapDataRef.current) return null;
|
if (!mapDataRef.current) return null;
|
||||||
|
|
||||||
@ -268,7 +233,6 @@ function WaypointEditor({ subscribe, publish, callService }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<g key={wp.id}>
|
<g key={wp.id}>
|
||||||
{/* Waypoint circle */}
|
|
||||||
<circle
|
<circle
|
||||||
cx={canvasX}
|
cx={canvasX}
|
||||||
cy={canvasY}
|
cy={canvasY}
|
||||||
@ -276,7 +240,6 @@ function WaypointEditor({ subscribe, publish, callService }) {
|
|||||||
fill={isActive ? '#ef4444' : isSelected ? '#fbbf24' : '#06b6d4'}
|
fill={isActive ? '#ef4444' : isSelected ? '#fbbf24' : '#06b6d4'}
|
||||||
opacity="0.8"
|
opacity="0.8"
|
||||||
/>
|
/>
|
||||||
{/* Waypoint number */}
|
|
||||||
<text
|
<text
|
||||||
x={canvasX}
|
x={canvasX}
|
||||||
y={canvasY}
|
y={canvasY}
|
||||||
@ -289,19 +252,12 @@ function WaypointEditor({ subscribe, publish, callService }) {
|
|||||||
>
|
>
|
||||||
{idx + 1}
|
{idx + 1}
|
||||||
</text>
|
</text>
|
||||||
{/* Line to next waypoint */}
|
|
||||||
{idx < waypoints.length - 1 && (
|
{idx < waypoints.length - 1 && (
|
||||||
<line
|
<line
|
||||||
x1={canvasX}
|
x1={canvasX}
|
||||||
y1={canvasY}
|
y1={canvasY}
|
||||||
x2={
|
x2={centerX + (waypoints[idx + 1].x - robot.x) * zoom}
|
||||||
centerX +
|
y2={centerY - (waypoints[idx + 1].y - robot.y) * zoom}
|
||||||
(waypoints[idx + 1].x - robot.x) * zoom
|
|
||||||
}
|
|
||||||
y2={
|
|
||||||
centerY -
|
|
||||||
(waypoints[idx + 1].y - robot.y) * zoom
|
|
||||||
}
|
|
||||||
stroke="#10b981"
|
stroke="#10b981"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
opacity="0.6"
|
opacity="0.6"
|
||||||
@ -310,8 +266,6 @@ function WaypointEditor({ subscribe, publish, callService }) {
|
|||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Robot position marker */}
|
|
||||||
<circle
|
<circle
|
||||||
cx={containerRef.current?.clientWidth / 2 || 400}
|
cx={containerRef.current?.clientWidth / 2 || 400}
|
||||||
cy={containerRef.current?.clientHeight / 2 || 300}
|
cy={containerRef.current?.clientHeight / 2 || 300}
|
||||||
@ -357,9 +311,7 @@ function WaypointEditor({ subscribe, publish, callService }) {
|
|||||||
{/* Waypoint list */}
|
{/* Waypoint list */}
|
||||||
<div className="flex-1 overflow-y-auto space-y-1">
|
<div className="flex-1 overflow-y-auto space-y-1">
|
||||||
{waypoints.length === 0 ? (
|
{waypoints.length === 0 ? (
|
||||||
<div className="text-center text-gray-700 text-xs py-4">
|
<div className="text-center text-gray-700 text-xs py-4">Click map to add waypoints</div>
|
||||||
Click map to add waypoints
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
waypoints.map((wp, idx) => (
|
waypoints.map((wp, idx) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user