first commit
This commit is contained in:
43
CMakeLists.txt
Normal file
43
CMakeLists.txt
Normal file
@@ -0,0 +1,43 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(lidar_manager_web LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
include(FetchContent)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
FetchContent_Declare(
|
||||
cpp_httplib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git
|
||||
GIT_TAG v0.44.0
|
||||
)
|
||||
FetchContent_GetProperties(cpp_httplib)
|
||||
if(NOT cpp_httplib_POPULATED)
|
||||
FetchContent_Populate(cpp_httplib)
|
||||
endif()
|
||||
|
||||
FetchContent_Declare(
|
||||
nlohmann_json
|
||||
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
|
||||
)
|
||||
FetchContent_GetProperties(nlohmann_json)
|
||||
if(NOT nlohmann_json_POPULATED)
|
||||
FetchContent_Populate(nlohmann_json)
|
||||
endif()
|
||||
|
||||
add_executable(lidar_manager_web
|
||||
src/main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(lidar_manager_web PRIVATE Threads::Threads)
|
||||
|
||||
target_include_directories(lidar_manager_web SYSTEM PRIVATE
|
||||
"${cpp_httplib_SOURCE_DIR}"
|
||||
"${nlohmann_json_SOURCE_DIR}/single_include"
|
||||
)
|
||||
|
||||
target_compile_definitions(lidar_manager_web PRIVATE
|
||||
_DEFAULT_SOURCE
|
||||
)
|
||||
31
README.md
31
README.md
@@ -1,2 +1,31 @@
|
||||
# App
|
||||
# LiDAR Manager Web (Test3)
|
||||
|
||||
Chức năng:
|
||||
- Đăng ký danh sách cảm biến LiDAR (tên, ip, port)
|
||||
- Kéo thả icon LiDAR trên canvas để set vị trí tương đối (theo px)
|
||||
- Lưu cấu hình xuống file JSON
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cd /home/robotics/RD/Test3
|
||||
cmake -S . -B build
|
||||
cmake --build build -j
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
Chạy mặc định port 8080, phục vụ static từ `www/`, dữ liệu ở `data/state.json`:
|
||||
|
||||
```bash
|
||||
./build/lidar_manager_web
|
||||
```
|
||||
|
||||
Hoặc chỉ định:
|
||||
|
||||
```bash
|
||||
./build/lidar_manager_web 8080 ./www ./data/state.json
|
||||
```
|
||||
|
||||
Mở trình duyệt: `http://localhost:8080/`
|
||||
|
||||
|
||||
204
data/models/a07ab938d9029ef1.json
Normal file
204
data/models/a07ab938d9029ef1.json
Normal file
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"created_at": "2026-05-29T08:27:25Z",
|
||||
"id": "a07ab938d9029ef1",
|
||||
"layout": {
|
||||
"lidarPoses": {
|
||||
"02c4b7f4de7bd639": {
|
||||
"theta_deg": 45,
|
||||
"x": 215,
|
||||
"y": 40
|
||||
},
|
||||
"1e591c93c581f705": {
|
||||
"theta_deg": -45,
|
||||
"x": 215.39984362180326,
|
||||
"y": -40
|
||||
},
|
||||
"242be6d6e782ecdf": {
|
||||
"theta_deg": 180,
|
||||
"x": 145,
|
||||
"y": -0.3738614899159438
|
||||
}
|
||||
},
|
||||
"lidarPosesFrame": "robot",
|
||||
"lidarPositions": {},
|
||||
"map": {
|
||||
"height": 600,
|
||||
"width": 800
|
||||
},
|
||||
"robot": {
|
||||
"bicycle": {
|
||||
"display": {
|
||||
"L_px": 240.0,
|
||||
"r_px": 60.0,
|
||||
"scale_m_per_px": 0.005
|
||||
},
|
||||
"drive": {
|
||||
"joint_name": "rear_wheel_joint"
|
||||
},
|
||||
"limits": {
|
||||
"cmd_vel_timeout_s": 0.25,
|
||||
"linear": {
|
||||
"max_acceleration": 0.8,
|
||||
"max_velocity": 1
|
||||
}
|
||||
},
|
||||
"steer": {
|
||||
"joint_name": "front_steer_joint",
|
||||
"max_angle_deg": 60,
|
||||
"preview_deg": 15
|
||||
},
|
||||
"wheel_radius_m": 0.15,
|
||||
"wheelbase_m": 1.2,
|
||||
"wheels": [
|
||||
{
|
||||
"id": "rear",
|
||||
"joint_name": "rear_wheel_joint",
|
||||
"motor": {
|
||||
"gear_ratio": 20,
|
||||
"invert": false,
|
||||
"model": "m2dc10a",
|
||||
"vendor": "moons"
|
||||
},
|
||||
"role": "drive",
|
||||
"x_m": 0,
|
||||
"y_m": 0
|
||||
},
|
||||
{
|
||||
"id": "front",
|
||||
"joint_name": "front_steer_joint",
|
||||
"motor": {
|
||||
"gear_ratio": 20,
|
||||
"invert": false,
|
||||
"model": "m2dc10a",
|
||||
"vendor": "moons"
|
||||
},
|
||||
"role": "steer",
|
||||
"x_m": 1.2,
|
||||
"y_m": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"diff": {
|
||||
"b": 200.0,
|
||||
"d": 120.0,
|
||||
"display": {
|
||||
"b_px": 200.0,
|
||||
"d_px": 120.0,
|
||||
"scale_m_per_px": 0.005
|
||||
},
|
||||
"limits": {
|
||||
"angular": {
|
||||
"max_acceleration": 1.5,
|
||||
"max_velocity": 1.7
|
||||
},
|
||||
"cmd_vel_timeout_s": 0.25,
|
||||
"linear": {
|
||||
"max_acceleration": 0.8,
|
||||
"max_velocity": 1,
|
||||
"min_acceleration": -0.4,
|
||||
"min_velocity": -0.5
|
||||
}
|
||||
},
|
||||
"wheel_radius_m": 0.3,
|
||||
"wheel_radius_multiplier": 1,
|
||||
"wheel_separation_m": 1,
|
||||
"wheel_separation_multiplier": 1,
|
||||
"wheels": [
|
||||
{
|
||||
"id": "left",
|
||||
"joint_name": "wheel_left_joint",
|
||||
"motor": {
|
||||
"gear_ratio": 20,
|
||||
"invert": false,
|
||||
"model": "m2dc10a",
|
||||
"vendor": "moons"
|
||||
},
|
||||
"side": "left",
|
||||
"y_m": 0.5
|
||||
},
|
||||
{
|
||||
"id": "right",
|
||||
"joint_name": "wheel_right_joint",
|
||||
"motor": {
|
||||
"gear_ratio": 20,
|
||||
"invert": false,
|
||||
"model": "m2dc10a",
|
||||
"vendor": "moons"
|
||||
},
|
||||
"side": "right",
|
||||
"y_m": -0.5
|
||||
}
|
||||
]
|
||||
},
|
||||
"footprint": [
|
||||
{
|
||||
"x": 249.49596246923238,
|
||||
"y": 76.53128468019501
|
||||
},
|
||||
{
|
||||
"x": 252.05138984920825,
|
||||
"y": -73.40426803273583
|
||||
},
|
||||
{
|
||||
"x": 146.0988213814129,
|
||||
"y": -73.14624094113161
|
||||
},
|
||||
{
|
||||
"x": 146.4579317148541,
|
||||
"y": -36.76005121552378
|
||||
},
|
||||
{
|
||||
"x": -24.190052366845578,
|
||||
"y": -36.232153738354725
|
||||
},
|
||||
{
|
||||
"x": -23.18092513013994,
|
||||
"y": 31.895774646867324
|
||||
},
|
||||
{
|
||||
"x": 149.1507088069675,
|
||||
"y": 31.363038836025066
|
||||
},
|
||||
{
|
||||
"x": 148.2973527630072,
|
||||
"y": 77.68471811183447
|
||||
}
|
||||
],
|
||||
"footprint_params": {
|
||||
"length_m": 1.69,
|
||||
"radius_m": 0.8432486399759678,
|
||||
"segments": 32,
|
||||
"sides": 6,
|
||||
"width_m": 1.28
|
||||
},
|
||||
"footprint_shape": "custom",
|
||||
"frame_id": "base_footprint",
|
||||
"model": "bicycle",
|
||||
"x": 400,
|
||||
"y": 300,
|
||||
"yaw_deg": 0
|
||||
}
|
||||
},
|
||||
"lidars": [
|
||||
{
|
||||
"id": "02c4b7f4de7bd639",
|
||||
"ip": "192.168.1.11",
|
||||
"name": "Front",
|
||||
"port": 2112
|
||||
},
|
||||
{
|
||||
"id": "1e591c93c581f705",
|
||||
"ip": "192.168.1.12",
|
||||
"name": "Back",
|
||||
"port": 2112
|
||||
},
|
||||
{
|
||||
"id": "242be6d6e782ecdf",
|
||||
"ip": "192.168.1.15",
|
||||
"name": "Oile",
|
||||
"port": 2112
|
||||
}
|
||||
],
|
||||
"name": "Mặc định",
|
||||
"updated_at": "2026-05-29T08:39:03Z"
|
||||
}
|
||||
177
data/models/ea89e39c835c0557.json
Normal file
177
data/models/ea89e39c835c0557.json
Normal file
@@ -0,0 +1,177 @@
|
||||
{
|
||||
"created_at": "2026-05-29T08:40:51Z",
|
||||
"id": "ea89e39c835c0557",
|
||||
"layout": {
|
||||
"lidarPoses": {
|
||||
"40235913b52d8101": {
|
||||
"theta_deg": -135,
|
||||
"x": -120,
|
||||
"y": -90
|
||||
},
|
||||
"f4504deeb605e6ed": {
|
||||
"theta_deg": 45,
|
||||
"x": 120,
|
||||
"y": 90
|
||||
}
|
||||
},
|
||||
"lidarPosesFrame": "robot",
|
||||
"lidarPositions": {},
|
||||
"map": {
|
||||
"height": 600,
|
||||
"width": 800
|
||||
},
|
||||
"robot": {
|
||||
"bicycle": {
|
||||
"display": {
|
||||
"L_px": 240.0,
|
||||
"r_px": 60.0,
|
||||
"scale_m_per_px": 0.005
|
||||
},
|
||||
"drive": {
|
||||
"joint_name": "rear_wheel_joint"
|
||||
},
|
||||
"limits": {
|
||||
"cmd_vel_timeout_s": 0.25,
|
||||
"linear": {
|
||||
"max_acceleration": 0.8,
|
||||
"max_velocity": 1
|
||||
}
|
||||
},
|
||||
"steer": {
|
||||
"joint_name": "front_steer_joint",
|
||||
"max_angle_deg": 60,
|
||||
"preview_deg": 15
|
||||
},
|
||||
"wheel_radius_m": 0.15,
|
||||
"wheelbase_m": 1.2,
|
||||
"wheels": [
|
||||
{
|
||||
"id": "rear",
|
||||
"joint_name": "rear_wheel_joint",
|
||||
"motor": {
|
||||
"gear_ratio": 20,
|
||||
"invert": false,
|
||||
"model": "m2dc10a",
|
||||
"vendor": "moons"
|
||||
},
|
||||
"role": "drive",
|
||||
"x_m": 0,
|
||||
"y_m": 0
|
||||
},
|
||||
{
|
||||
"id": "front",
|
||||
"joint_name": "front_steer_joint",
|
||||
"motor": {
|
||||
"gear_ratio": 20,
|
||||
"invert": false,
|
||||
"model": "m2dc10a",
|
||||
"vendor": "moons"
|
||||
},
|
||||
"role": "steer",
|
||||
"x_m": 1.2,
|
||||
"y_m": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"diff": {
|
||||
"b": 220.0,
|
||||
"d": 120.0,
|
||||
"display": {
|
||||
"b_px": 220.0,
|
||||
"d_px": 120.0,
|
||||
"scale_m_per_px": 0.005
|
||||
},
|
||||
"limits": {
|
||||
"angular": {
|
||||
"max_acceleration": 1.5,
|
||||
"max_velocity": 1.7
|
||||
},
|
||||
"cmd_vel_timeout_s": 0.25,
|
||||
"linear": {
|
||||
"max_acceleration": 0.8,
|
||||
"max_velocity": 1,
|
||||
"min_acceleration": -0.8,
|
||||
"min_velocity": -0.5
|
||||
}
|
||||
},
|
||||
"wheel_radius_m": 0.3,
|
||||
"wheel_radius_multiplier": 1,
|
||||
"wheel_separation_m": 1.1,
|
||||
"wheel_separation_multiplier": 1,
|
||||
"wheels": [
|
||||
{
|
||||
"id": "left",
|
||||
"joint_name": "wheel_left_joint",
|
||||
"motor": {
|
||||
"gear_ratio": 10,
|
||||
"invert": true,
|
||||
"model": "m2dc10a",
|
||||
"vendor": "moons"
|
||||
},
|
||||
"side": "left",
|
||||
"y_m": 0.55
|
||||
},
|
||||
{
|
||||
"id": "right",
|
||||
"joint_name": "wheel_right_joint",
|
||||
"motor": {
|
||||
"gear_ratio": 10,
|
||||
"invert": false,
|
||||
"model": "m2dc10a",
|
||||
"vendor": "moons"
|
||||
},
|
||||
"side": "right",
|
||||
"y_m": -0.55
|
||||
}
|
||||
]
|
||||
},
|
||||
"footprint": [
|
||||
{
|
||||
"x": 150,
|
||||
"y": 120
|
||||
},
|
||||
{
|
||||
"x": 150,
|
||||
"y": -120
|
||||
},
|
||||
{
|
||||
"x": -150,
|
||||
"y": -120
|
||||
},
|
||||
{
|
||||
"x": -150,
|
||||
"y": 120
|
||||
}
|
||||
],
|
||||
"footprint_params": {
|
||||
"length_m": 1.5,
|
||||
"radius_m": 1,
|
||||
"segments": 32,
|
||||
"sides": 6,
|
||||
"width_m": 1.2
|
||||
},
|
||||
"footprint_shape": "rectangle",
|
||||
"frame_id": "base_footprint",
|
||||
"model": "diff",
|
||||
"x": 400,
|
||||
"y": 300,
|
||||
"yaw_deg": 0
|
||||
}
|
||||
},
|
||||
"lidars": [
|
||||
{
|
||||
"id": "f4504deeb605e6ed",
|
||||
"ip": "192.168.1.11",
|
||||
"name": "Front",
|
||||
"port": 2112
|
||||
},
|
||||
{
|
||||
"id": "40235913b52d8101",
|
||||
"ip": "192.168.1.11",
|
||||
"name": "Back",
|
||||
"port": 2112
|
||||
}
|
||||
],
|
||||
"name": "T800",
|
||||
"updated_at": "2026-05-29T08:44:03Z"
|
||||
}
|
||||
22
data/state.json
Normal file
22
data/state.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"active_layout_id": "a07ab938d9029ef1",
|
||||
"layouts": [
|
||||
{
|
||||
"created_at": "2026-05-29T08:27:25Z",
|
||||
"id": "a07ab938d9029ef1",
|
||||
"lidar_count": 3,
|
||||
"model": "bicycle",
|
||||
"name": "Mặc định",
|
||||
"updated_at": "2026-05-29T08:39:03Z"
|
||||
},
|
||||
{
|
||||
"created_at": "2026-05-29T08:40:51Z",
|
||||
"id": "ea89e39c835c0557",
|
||||
"lidar_count": 2,
|
||||
"model": "diff",
|
||||
"name": "T800",
|
||||
"updated_at": "2026-05-29T08:44:03Z"
|
||||
}
|
||||
],
|
||||
"version": 3
|
||||
}
|
||||
1252
src/main.cpp
Normal file
1252
src/main.cpp
Normal file
File diff suppressed because it is too large
Load Diff
2799
www/app.js
Normal file
2799
www/app.js
Normal file
File diff suppressed because it is too large
Load Diff
92
www/data/motor_catalog.json
Normal file
92
www/data/motor_catalog.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"vendors": {
|
||||
"moons": {
|
||||
"label": "Moons",
|
||||
"models": {
|
||||
"m2dc10a": {
|
||||
"label": "M2DC-10A (CANopen)",
|
||||
"interface": "canopen",
|
||||
"max_rpm": 3000,
|
||||
"rated_torque_nm": 2.5,
|
||||
"gear_ratio_default": 20
|
||||
},
|
||||
"m2dc15a": {
|
||||
"label": "M2DC-15A (CANopen)",
|
||||
"interface": "canopen",
|
||||
"max_rpm": 3000,
|
||||
"rated_torque_nm": 4.0,
|
||||
"gear_ratio_default": 16
|
||||
}
|
||||
}
|
||||
},
|
||||
"veichi": {
|
||||
"label": "Veichi",
|
||||
"models": {
|
||||
"sd700": {
|
||||
"label": "SD700 servo",
|
||||
"interface": "modbus",
|
||||
"max_rpm": 3500,
|
||||
"rated_torque_nm": 2.2,
|
||||
"gear_ratio_default": 15
|
||||
},
|
||||
"sd710": {
|
||||
"label": "SD710 servo",
|
||||
"interface": "modbus",
|
||||
"max_rpm": 3000,
|
||||
"rated_torque_nm": 3.5,
|
||||
"gear_ratio_default": 12
|
||||
}
|
||||
}
|
||||
},
|
||||
"md": {
|
||||
"label": "MD / Leadshine",
|
||||
"models": {
|
||||
"t3d": {
|
||||
"label": "T3D stepper driver",
|
||||
"interface": "pulse_dir",
|
||||
"max_rpm": 1200,
|
||||
"rated_torque_nm": 1.2,
|
||||
"gear_ratio_default": 10
|
||||
},
|
||||
"elm": {
|
||||
"label": "ELM servo",
|
||||
"interface": "pulse_dir",
|
||||
"max_rpm": 2500,
|
||||
"rated_torque_nm": 1.8,
|
||||
"gear_ratio_default": 18
|
||||
}
|
||||
}
|
||||
},
|
||||
"oriental": {
|
||||
"label": "Oriental Motor",
|
||||
"models": {
|
||||
"ble2": {
|
||||
"label": "BLE2 series",
|
||||
"interface": "network",
|
||||
"max_rpm": 3000,
|
||||
"rated_torque_nm": 1.5,
|
||||
"gear_ratio_default": 25
|
||||
},
|
||||
"az_series": {
|
||||
"label": "AZ series",
|
||||
"interface": "pulse_dir",
|
||||
"max_rpm": 2800,
|
||||
"rated_torque_nm": 2.0,
|
||||
"gear_ratio_default": 20
|
||||
}
|
||||
}
|
||||
},
|
||||
"custom": {
|
||||
"label": "Tùy chỉnh",
|
||||
"models": {
|
||||
"custom": {
|
||||
"label": "Motor tùy chỉnh",
|
||||
"interface": "other",
|
||||
"max_rpm": 3000,
|
||||
"rated_torque_nm": 1.0,
|
||||
"gear_ratio_default": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
433
www/index.html
Normal file
433
www/index.html
Normal file
@@ -0,0 +1,433 @@
|
||||
<!doctype html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>LiDAR Manager</title>
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<div class="brandIcon">R</div>
|
||||
<div class="brandText">
|
||||
<div class="brandTitle">PhenikaaX</div>
|
||||
<div class="brandSub">RobotApp</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navTitle">WORKSPACE</div>
|
||||
<nav class="nav">
|
||||
<a class="navItem active" href="#" data-page="overview" aria-current="page">
|
||||
<span class="navDot"></span>
|
||||
Tổng quan
|
||||
</a>
|
||||
<a class="navItem" href="#" data-page="config">
|
||||
<span class="navDot"></span>
|
||||
Cấu hình
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="sidebarFooter">
|
||||
<div class="statusBadge">
|
||||
<span class="statusLed"></span>
|
||||
<span id="status" class="statusText">…</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="body">
|
||||
<header class="topbar">
|
||||
<div class="topbarTitle">
|
||||
<div class="kicker">PhenikaaX Robotics</div>
|
||||
<div class="pageTitle">Cấu Hình</div>
|
||||
</div>
|
||||
<div class="topbarActions">
|
||||
<button id="refreshBtn" type="button" class="btn subtle">Tải lại</button>
|
||||
<button id="saveLayoutBtn" class="btn primary" type="button">Lưu layout</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="content">
|
||||
<div class="contentLeft">
|
||||
<section class="card" id="layoutManagerCard">
|
||||
<div class="cardHeader">
|
||||
<div>
|
||||
<div class="cardTitle">Quản lý layout</div>
|
||||
<div class="cardSub">Nhiều cấu hình robot — mỗi layout có LiDAR và model riêng.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cardBody">
|
||||
<div class="row rowWide">
|
||||
<label>Layout hiện tại</label>
|
||||
<select id="layoutSelect"></select>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Tên layout mới</label>
|
||||
<input id="layoutNewName" type="text" placeholder="VD: AGV kho A" />
|
||||
</div>
|
||||
<div class="checkRow">
|
||||
<label>
|
||||
<input id="layoutCloneCurrent" type="checkbox" />
|
||||
Sao chép từ layout đang mở
|
||||
</label>
|
||||
</div>
|
||||
<div class="layoutManagerActions">
|
||||
<button id="layoutCreateBtn" type="button" class="btn subtle">Tạo layout</button>
|
||||
<button id="layoutDeleteBtn" type="button" class="btn subtle danger">Xóa</button>
|
||||
</div>
|
||||
<p id="layoutActiveHint" class="mutedNote">—</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card collapsible" id="lidarListCard">
|
||||
<div
|
||||
class="cardHeader cardHeaderToggle"
|
||||
id="lidarListCardToggle"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-expanded="true"
|
||||
aria-controls="lidarListCardBody"
|
||||
>
|
||||
<div>
|
||||
<div class="cardTitle">LiDARs</div>
|
||||
<div class="cardSub">Đăng ký tên, IP, port và chỉnh pose theo robot frame.</div>
|
||||
</div>
|
||||
<span class="cardChevron" aria-hidden="true"></span>
|
||||
</div>
|
||||
|
||||
<div class="cardBody" id="lidarListCardBody">
|
||||
<form id="lidarForm" class="form">
|
||||
<div class="row">
|
||||
<label>Tên</label>
|
||||
<input id="name" placeholder="Lidar trước" required />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>IP</label>
|
||||
<input id="ip" placeholder="192.168.0.10" required />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Port</label>
|
||||
<input id="port" type="number" min="1" max="65535" value="2112" required />
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="addLidarBtn" class="btn primary" type="button">Thêm</button>
|
||||
</div>
|
||||
<p id="lidarFormHint" class="formHint" hidden></p>
|
||||
</form>
|
||||
|
||||
<div id="lidarList" class="list"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card collapsible" id="robotModelCard">
|
||||
<div
|
||||
class="cardHeader cardHeaderToggle"
|
||||
id="robotModelCardToggle"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-expanded="true"
|
||||
aria-controls="robotModelCardBody"
|
||||
>
|
||||
<div>
|
||||
<div class="cardTitle">Model robot</div>
|
||||
<div class="cardSub">Kinematic differential — bánh, động cơ và giới hạn vận tốc.</div>
|
||||
</div>
|
||||
<span class="cardChevron" aria-hidden="true"></span>
|
||||
</div>
|
||||
<div class="cardBody" id="robotModelCardBody">
|
||||
<div class="modelForm">
|
||||
<div class="row rowWide">
|
||||
<label>Model</label>
|
||||
<select id="robotModel">
|
||||
<option value="diff">Differential (2 bánh)</option>
|
||||
<option value="bicycle">Bicycle</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="diffParams" class="modelParams">
|
||||
<details class="acc" open>
|
||||
<summary>Hình học bánh</summary>
|
||||
<div class="accBody">
|
||||
<div class="row rowWide">
|
||||
<label>Khoảng cách 2 bánh</label>
|
||||
<div class="inputUnit">
|
||||
<input id="wheelSeparationM" type="number" min="0.05" max="5" step="0.01" value="1.0" />
|
||||
<span class="unit">m</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Bán kính bánh</label>
|
||||
<div class="inputUnit">
|
||||
<input id="wheelRadiusM" type="number" min="0.02" max="1" step="0.01" value="0.3" />
|
||||
<span class="unit">m</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Tỷ lệ hiển thị</label>
|
||||
<div class="inputUnit">
|
||||
<input id="scaleMPerPx" type="number" min="0.001" max="0.1" step="0.001" value="0.005" />
|
||||
<span class="unit">m/px</span>
|
||||
</div>
|
||||
</div>
|
||||
<details class="acc accNested">
|
||||
<summary>Hiệu chỉnh (nâng cao)</summary>
|
||||
<div class="accBody">
|
||||
<div class="row rowWide">
|
||||
<label>b multiplier</label>
|
||||
<input id="wheelSeparationMult" type="number" min="0.5" max="2" step="0.01" value="1.0" />
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>r multiplier</label>
|
||||
<input id="wheelRadiusMult" type="number" min="0.5" max="2" step="0.01" value="1.0" />
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="acc">
|
||||
<summary>Động cơ</summary>
|
||||
<div class="accBody">
|
||||
<p class="mutedNote">Mỗi bánh gán một động cơ — chọn hãng và model.</p>
|
||||
<div id="motorWheels" class="motorWheels"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="acc">
|
||||
<summary>Giới hạn vận tốc</summary>
|
||||
<div class="accBody">
|
||||
<div class="row rowWide">
|
||||
<label>cmd_vel timeout</label>
|
||||
<div class="inputUnit">
|
||||
<input id="cmdVelTimeout" type="number" min="0.05" max="5" step="0.05" value="0.25" />
|
||||
<span class="unit">s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Linear max</label>
|
||||
<div class="inputUnit">
|
||||
<input id="linearMaxVel" type="number" min="0.01" max="5" step="0.05" value="1.0" />
|
||||
<span class="unit">m/s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Linear min</label>
|
||||
<div class="inputUnit">
|
||||
<input id="linearMinVel" type="number" min="-5" max="0" step="0.05" value="-0.5" />
|
||||
<span class="unit">m/s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Linear accel max</label>
|
||||
<div class="inputUnit">
|
||||
<input id="linearMaxAccel" type="number" min="0.01" max="10" step="0.05" value="0.8" />
|
||||
<span class="unit">m/s²</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Angular max</label>
|
||||
<div class="inputUnit">
|
||||
<input id="angularMaxVel" type="number" min="0.01" max="10" step="0.05" value="1.7" />
|
||||
<span class="unit">rad/s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Angular accel max</label>
|
||||
<div class="inputUnit">
|
||||
<input id="angularMaxAccel" type="number" min="0.01" max="10" step="0.05" value="1.5" />
|
||||
<span class="unit">rad/s²</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<p id="diffValidation" class="validation" hidden></p>
|
||||
</div>
|
||||
|
||||
<div id="bicycleParams" class="modelParams" hidden>
|
||||
<p class="mutedNote">
|
||||
Kinematic bicycle — tham chiếu trục sau, quan hệ
|
||||
<span class="mono">ω = v·tan(δ)/L</span>
|
||||
(<a href="https://thomasfermi.github.io/Algorithms-for-Automated-Driving/Control/BicycleModel.html" target="_blank" rel="noopener">mô hình</a>).
|
||||
</p>
|
||||
<details class="acc" open>
|
||||
<summary>Hình học (wheelbase)</summary>
|
||||
<div class="accBody">
|
||||
<div class="row rowWide">
|
||||
<label>Wheelbase L</label>
|
||||
<div class="inputUnit">
|
||||
<input id="bicycleWheelbaseM" type="number" min="0.2" max="5" step="0.05" value="1.2" />
|
||||
<span class="unit">m</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Bán kính bánh</label>
|
||||
<div class="inputUnit">
|
||||
<input id="bicycleWheelRadiusM" type="number" min="0.02" max="1" step="0.01" value="0.15" />
|
||||
<span class="unit">m</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Tỷ lệ hiển thị</label>
|
||||
<div class="inputUnit">
|
||||
<input id="bicycleScaleMPerPx" type="number" min="0.001" max="0.1" step="0.001" value="0.005" />
|
||||
<span class="unit">m/px</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Góc lái xem trước</label>
|
||||
<div class="inputUnit">
|
||||
<input id="bicycleSteerPreviewDeg" type="number" min="-45" max="45" step="1" value="15" />
|
||||
<span class="unit">°</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details class="acc">
|
||||
<summary>Động cơ</summary>
|
||||
<div class="accBody">
|
||||
<p class="mutedNote">Bánh sau (drive) và bánh trước (steer).</p>
|
||||
<div id="bicycleMotorWheels" class="motorWheels"></div>
|
||||
</div>
|
||||
</details>
|
||||
<details class="acc">
|
||||
<summary>Giới hạn</summary>
|
||||
<div class="accBody">
|
||||
<div class="row rowWide">
|
||||
<label>δ max (lái)</label>
|
||||
<div class="inputUnit">
|
||||
<input id="bicycleSteerMaxDeg" type="number" min="5" max="60" step="1" value="35" />
|
||||
<span class="unit">°</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>cmd_vel timeout</label>
|
||||
<div class="inputUnit">
|
||||
<input id="bicycleCmdVelTimeout" type="number" min="0.05" max="5" step="0.05" value="0.25" />
|
||||
<span class="unit">s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Linear max</label>
|
||||
<div class="inputUnit">
|
||||
<input id="bicycleLinearMaxVel" type="number" min="0.01" max="5" step="0.05" value="1.0" />
|
||||
<span class="unit">m/s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Linear accel max</label>
|
||||
<div class="inputUnit">
|
||||
<input id="bicycleLinearMaxAccel" type="number" min="0.01" max="10" step="0.05" value="0.8" />
|
||||
<span class="unit">m/s²</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<p id="bicycleValidation" class="validation" hidden></p>
|
||||
</div>
|
||||
|
||||
<details class="acc">
|
||||
<summary>Footprint</summary>
|
||||
<div class="accBody">
|
||||
<p class="mutedNote">Hình dạng robot (ROS polygon) — tọa độ theo robot frame.</p>
|
||||
<div class="row rowWide">
|
||||
<label>Hình dạng</label>
|
||||
<select id="footprintShape">
|
||||
<option value="rectangle">Hình chữ nhật</option>
|
||||
<option value="circle">Hình tròn</option>
|
||||
<option value="regular_polygon">Đa giác đều</option>
|
||||
<option value="custom">Tùy chỉnh (đa giác)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="footprintPresetPanel" class="footprintPresetPanel">
|
||||
<div id="fpRectParams" class="fpShapeParams">
|
||||
<div class="row rowWide">
|
||||
<label>Chiều dài</label>
|
||||
<div class="inputUnit">
|
||||
<input id="fpLengthM" type="number" min="0.1" max="10" step="0.05" value="1.4" />
|
||||
<span class="unit">m</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Chiều rộng</label>
|
||||
<div class="inputUnit">
|
||||
<input id="fpWidthM" type="number" min="0.1" max="10" step="0.05" value="1.1" />
|
||||
<span class="unit">m</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fpCircleParams" class="fpShapeParams" hidden>
|
||||
<div class="row rowWide">
|
||||
<label>Bán kính</label>
|
||||
<div class="inputUnit">
|
||||
<input id="fpRadiusM" type="number" min="0.1" max="5" step="0.05" value="0.55" />
|
||||
<span class="unit">m</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Độ mịn (cạnh)</label>
|
||||
<input id="fpCircleSegments" type="number" min="8" max="64" step="1" value="32" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="fpPolyParams" class="fpShapeParams" hidden>
|
||||
<div class="row rowWide">
|
||||
<label>Bán kính</label>
|
||||
<div class="inputUnit">
|
||||
<input id="fpPolyRadiusM" type="number" min="0.1" max="5" step="0.05" value="0.6" />
|
||||
<span class="unit">m</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>Số cạnh</label>
|
||||
<input id="fpPolySides" type="number" min="3" max="32" step="1" value="6" />
|
||||
</div>
|
||||
</div>
|
||||
<button id="applyFootprintPresetBtn" type="button" class="btn subtle btnBlock">Áp dụng hình dạng</button>
|
||||
</div>
|
||||
<div id="footprintCustomPanel" class="footprintCustomPanel hidden">
|
||||
<div class="fpVertexRow">
|
||||
<span class="fpVertexLabel">Số đỉnh: <strong id="fpVertexCount">0</strong></span>
|
||||
<span id="fpSelectedVertexText" class="fpSelectedVertex mutedNote">—</span>
|
||||
</div>
|
||||
<div class="fpVertexActions">
|
||||
<button id="fpAddVertexBtn" type="button" class="btn subtle">Thêm đỉnh</button>
|
||||
<button id="fpRemoveVertexBtn" type="button" class="btn subtle" disabled>Xóa đỉnh</button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="editFootprintBtn" type="button" class="btn subtle btnBlock">Sửa footprint</button>
|
||||
<div id="footprintEditHint" class="fpHint" hidden></div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<div class="cardHeader">
|
||||
<div>
|
||||
<div class="cardTitle">Bố trí trên robot</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cardBody">
|
||||
<div class="canvasWrap" id="canvasWrap">
|
||||
<canvas id="canvas"></canvas>
|
||||
</div>
|
||||
<div class="metaBar">
|
||||
<div class="viewHint">Cuộn chuột: zoom • Shift + kéo: di chuyển vùng nhìn</div>
|
||||
<div id="robotDiffSummary" class="robotDiffSummary">—</div>
|
||||
<div>Robot center: <span id="robotCenterText"></span></div>
|
||||
<div>Selected: <span id="selectedText">none</span></div>
|
||||
<div>Pose: <span id="selectedRelText">—</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
524
www/style.css
Normal file
524
www/style.css
Normal file
@@ -0,0 +1,524 @@
|
||||
:root {
|
||||
--bg: #f6f8fb;
|
||||
--panel: #ffffff;
|
||||
--panel2: #f2f5fb;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--border: rgba(15, 23, 42, 0.12);
|
||||
--accent: #2563eb;
|
||||
--accent2: #10b981;
|
||||
--danger: #ef4444;
|
||||
--shadow: 0 8px 20px rgba(15, 23, 42, 0.08);
|
||||
--shadow2: 0 14px 36px rgba(15, 23, 42, 0.10);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji",
|
||||
"Segoe UI Emoji";
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
background: linear-gradient(180deg, #0b1220, #0b1220);
|
||||
color: #e8eefc;
|
||||
padding: 16px 14px;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.brand {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.brandIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(37, 99, 235, 0.22);
|
||||
border: 1px solid rgba(37, 99, 235, 0.35);
|
||||
font-weight: 800;
|
||||
}
|
||||
.brandTitle { font-weight: 800; font-size: 13px; letter-spacing: 0.2px; }
|
||||
.brandSub { color: rgba(232,238,252,0.75); font-size: 12px; margin-top: 2px; }
|
||||
|
||||
.navTitle {
|
||||
margin-top: 16px;
|
||||
padding: 0 10px;
|
||||
font-size: 11px;
|
||||
color: rgba(232,238,252,0.65);
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
.nav { margin-top: 8px; display: grid; gap: 6px; }
|
||||
.navItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 10px;
|
||||
border-radius: 12px;
|
||||
color: rgba(232,238,252,0.85);
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.navItem:hover { background: rgba(255,255,255,0.05); }
|
||||
.navItem.active {
|
||||
background: rgba(37, 99, 235, 0.22);
|
||||
border-color: rgba(37, 99, 235, 0.30);
|
||||
color: #ffffff;
|
||||
}
|
||||
.navDot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.25);
|
||||
}
|
||||
.navItem.active .navDot { background: rgba(37, 99, 235, 1); }
|
||||
|
||||
.sidebarFooter {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
right: 14px;
|
||||
bottom: 14px;
|
||||
}
|
||||
.statusBadge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.statusLed {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(16, 185, 129, 0.85);
|
||||
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.12);
|
||||
}
|
||||
.statusText { color: rgba(232,238,252,0.85); font-size: 12px; }
|
||||
|
||||
.body {
|
||||
display: grid;
|
||||
grid-template-rows: 72px 1fr;
|
||||
min-width: 0;
|
||||
}
|
||||
.topbar {
|
||||
background: var(--panel);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 14px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
.kicker { font-size: 12px; color: var(--muted); }
|
||||
.pageTitle { font-size: 16px; font-weight: 800; letter-spacing: 0.2px; margin-top: 2px; }
|
||||
.topbarActions { display: flex; gap: 10px; align-items: center; }
|
||||
|
||||
.content {
|
||||
padding: 18px;
|
||||
display: grid;
|
||||
grid-template-columns: min(460px, 100%) 1fr;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
.contentLeft {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
.modelForm { display: grid; gap: 10px; }
|
||||
.modelParams { display: grid; gap: 10px; }
|
||||
.modelParams[hidden] { display: none; }
|
||||
.modelForm .rowWide { grid-template-columns: 1fr; gap: 6px; align-items: stretch; }
|
||||
.modelForm .rowWide label { line-height: 1.3; }
|
||||
.inputUnit {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.inputUnit input { min-width: 0; }
|
||||
.inputUnit .unit {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.acc {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--panel2);
|
||||
overflow: hidden;
|
||||
}
|
||||
.acc > summary {
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
}
|
||||
.acc > summary::-webkit-details-marker { display: none; }
|
||||
.acc > summary::after {
|
||||
content: "";
|
||||
float: right;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-top: 4px;
|
||||
border-right: 2px solid var(--muted);
|
||||
border-bottom: 2px solid var(--muted);
|
||||
transform: rotate(45deg);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.acc[open] > summary::after { transform: rotate(-135deg); margin-top: 7px; }
|
||||
.accBody { padding: 0 12px 12px; display: grid; gap: 10px; }
|
||||
.accNested { margin-top: 4px; }
|
||||
.validation {
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
background: rgba(234, 179, 8, 0.12);
|
||||
border: 1px solid rgba(234, 179, 8, 0.35);
|
||||
color: #92400e;
|
||||
}
|
||||
.validation.error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.35);
|
||||
color: #b91c1c;
|
||||
}
|
||||
.yamlPreview {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.btnBlock { width: 100%; justify-content: center; }
|
||||
.mutedNote { margin: 0; font-size: 12px; color: var(--muted); }
|
||||
.mutedNote .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 11px; }
|
||||
.mutedNote a { color: rgba(37, 99, 235, 0.9); }
|
||||
.layoutManagerActions { display: flex; gap: 8px; margin-top: 4px; }
|
||||
.layoutManagerActions .btn { flex: 1; justify-content: center; }
|
||||
#layoutSelect { width: 100%; }
|
||||
.motorWheels { display: grid; gap: 12px; }
|
||||
.wheelMotorBlock {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
background: #fff;
|
||||
}
|
||||
.wheelMotorTitle {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text);
|
||||
}
|
||||
.wheelMotorMeta {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin-top: 6px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.wheelMotorBlock .rowWide { margin-bottom: 6px; }
|
||||
.wheelMotorBlock .rowWide:last-child { margin-bottom: 0; }
|
||||
.wheelMotorBlock input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.checkRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.robotDiffSummary {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
color: rgba(37, 99, 235, 0.95);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: linear-gradient(180deg, rgba(37, 99, 235, 0.06), transparent 70%);
|
||||
}
|
||||
.cardHeaderToggle {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.cardHeaderToggle:hover { background: linear-gradient(180deg, rgba(37, 99, 235, 0.10), transparent 70%); }
|
||||
.cardChevron {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel2);
|
||||
position: relative;
|
||||
margin-top: 2px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.cardChevron::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-right: 2px solid var(--muted);
|
||||
border-bottom: 2px solid var(--muted);
|
||||
transform: translate(-50%, -65%) rotate(45deg);
|
||||
}
|
||||
.card.collapsed .cardChevron { transform: rotate(-90deg); }
|
||||
.card.collapsed .cardBody { display: none; }
|
||||
.card.collapsed .cardHeader { border-bottom: none; }
|
||||
|
||||
.cardTitle { font-size: 14px; font-weight: 800; letter-spacing: 0.2px; }
|
||||
.cardSub { margin-top: 4px; color: var(--muted); font-size: 12px; line-height: 1.35; }
|
||||
.cardBody { padding: 14px 16px; }
|
||||
|
||||
.form { display: grid; gap: 10px; margin-bottom: 12px; }
|
||||
.formHint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
color: #b91c1c;
|
||||
}
|
||||
.formHint[hidden] { display: none; }
|
||||
.row { display: grid; grid-template-columns: 90px 1fr; gap: 10px; align-items: center; }
|
||||
label { color: var(--muted); font-size: 12px; }
|
||||
input, select {
|
||||
width: 100%;
|
||||
padding: 10px 10px;
|
||||
background: var(--panel2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
}
|
||||
input:focus, select:focus { border-color: rgba(37, 99, 235, 0.55); box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.12); }
|
||||
|
||||
.actions { display: flex; gap: 10px; }
|
||||
.btn, button {
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel2);
|
||||
color: var(--text);
|
||||
padding: 9px 12px;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn.primary { background: rgba(37, 99, 235, 0.92); border-color: rgba(37, 99, 235, 0.92); color: #fff; }
|
||||
.btn.primary:hover { background: rgba(37, 99, 235, 1); }
|
||||
.btn.subtle { background: var(--panel2); }
|
||||
.btn.danger { color: var(--danger); border-color: rgba(239, 68, 68, 0.35); }
|
||||
.btn:active, button:active { transform: translateY(1px); }
|
||||
.btn.active, button.active { box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.12); border-color: rgba(37, 99, 235, 0.55); }
|
||||
|
||||
.list { display: grid; gap: 10px; }
|
||||
.item {
|
||||
border: 1px solid var(--border);
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
.itemTop { display: flex; justify-content: space-between; gap: 10px; align-items: flex-start; }
|
||||
.itemMain { flex: 1; min-width: 0; }
|
||||
.itemToggle {
|
||||
flex-shrink: 0;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-top: 2px;
|
||||
margin-right: 8px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel2);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.itemToggle::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-right: 2px solid var(--muted);
|
||||
border-bottom: 2px solid var(--muted);
|
||||
transform: translate(-50%, -65%) rotate(45deg);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.item.collapsed .itemToggle::before { transform: translate(-50%, -35%) rotate(-135deg); }
|
||||
.item.collapsed .itemBody { display: none; }
|
||||
.itemTopRow { display: flex; align-items: flex-start; }
|
||||
.itemName { font-weight: 700; }
|
||||
.itemMeta { color: var(--muted); font-size: 12px; margin-top: 4px; }
|
||||
.itemBtns { display: flex; gap: 8px; }
|
||||
.poseRow {
|
||||
margin-top: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.poseField {
|
||||
display: grid;
|
||||
grid-template-columns: 18px 1fr;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.poseLabel {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
.poseInput {
|
||||
padding: 8px 9px;
|
||||
border-radius: 10px;
|
||||
background: var(--panel2);
|
||||
}
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(16, 185, 129, 0.10);
|
||||
border: 1px solid rgba(16, 185, 129, 0.22);
|
||||
color: rgba(16, 185, 129, 1);
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.tool { display: grid; gap: 6px; }
|
||||
.tool label { font-size: 12px; }
|
||||
.tool input { width: 120px; }
|
||||
.toolGroup {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
}
|
||||
.toolGroup[hidden] { display: none; }
|
||||
.tool select { width: 170px; }
|
||||
.toolGroup .tool input { width: 110px; }
|
||||
|
||||
.canvasWrap {
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
height: min(72vh, 680px);
|
||||
min-height: 420px;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
background: #0b1220;
|
||||
touch-action: none;
|
||||
}
|
||||
.canvasWrap.panning canvas { cursor: grabbing; }
|
||||
.canvasWrap.shift-pan canvas { cursor: grab; }
|
||||
|
||||
.metaBar {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 14px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.fpHint { color: rgba(37, 99, 235, 0.95); margin-top: 8px; font-size: 12px; line-height: 1.4; }
|
||||
.footprintPresetPanel {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: #f8fafc;
|
||||
}
|
||||
.footprintPresetPanel .fpShapeParams { display: grid; gap: 6px; }
|
||||
.footprintPresetPanel.hidden { display: none; }
|
||||
.footprintCustomPanel {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: #f8fafc;
|
||||
}
|
||||
.footprintCustomPanel.hidden { display: none; }
|
||||
.fpVertexRow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.fpVertexLabel strong { color: var(--text); }
|
||||
.fpSelectedVertex { font-size: 11px; }
|
||||
.fpVertexActions { display: flex; gap: 8px; }
|
||||
.fpVertexActions .btn { flex: 1; justify-content: center; font-size: 12px; padding: 6px 10px; }
|
||||
.viewHint { color: var(--muted); font-size: 12px; width: 100%; }
|
||||
.canvasWrap canvas.edit-footprint { cursor: crosshair; }
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.shell { grid-template-columns: 1fr; }
|
||||
.sidebar { position: relative; height: auto; }
|
||||
.body { grid-template-rows: auto 1fr; }
|
||||
.content { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user