feat(lekiwi): release V0.10.1 初步集成 LeKiwi,优化碰撞模型
集成通用机器人数值接口、本机控制桥、LeRobot 插件和统一键盘遥操作。采用离线 CoACD 全臂碰撞配方 revision 4、局部装配区切分与结构自接触,限制直接关节位姿写入并保留安全看门狗。同步版本号、变更记录、来源许可证和兼容性验证。
This commit is contained in:
@@ -5,6 +5,7 @@ export function SimulationControls({
|
||||
ready,
|
||||
speed,
|
||||
loading,
|
||||
externalControl = false,
|
||||
onTogglePause,
|
||||
onStep,
|
||||
onReset,
|
||||
@@ -14,6 +15,7 @@ export function SimulationControls({
|
||||
ready: boolean;
|
||||
speed: number;
|
||||
loading: boolean;
|
||||
externalControl?: boolean;
|
||||
onTogglePause: () => void;
|
||||
onStep: () => void;
|
||||
onReset: () => void;
|
||||
@@ -34,7 +36,7 @@ export function SimulationControls({
|
||||
tooltip="单步(暂停时可用)"
|
||||
aria-label="单步"
|
||||
onClick={onStep}
|
||||
disabled={!ready || !paused}
|
||||
disabled={!ready || !paused || externalControl}
|
||||
>
|
||||
<StepForward className="h-3.5 w-3.5" />
|
||||
</IconButton>
|
||||
@@ -49,7 +51,7 @@ export function SimulationControls({
|
||||
<Select
|
||||
aria-label="仿真速度"
|
||||
value={speed}
|
||||
disabled={loading}
|
||||
disabled={loading || externalControl}
|
||||
onChange={(event) => onSpeed(Number(event.target.value))}
|
||||
className="w-[70px]"
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, type ReactNode } from 'react';
|
||||
import { Camera, Settings2 } from 'lucide-react';
|
||||
import type { CameraDirection, UrdfEnhancementOptions } from '../../project/urdfToMjcf';
|
||||
import { Button, Dialog, Select, Tooltip } from '../../components/ui';
|
||||
import { ROBOT_PROFILES } from '../../robot/registry';
|
||||
|
||||
function OptionCard({
|
||||
checked,
|
||||
@@ -44,15 +45,18 @@ export function UrdfImportOptionsDialog({
|
||||
open,
|
||||
path,
|
||||
mountBodies = [],
|
||||
initialProfileId = '',
|
||||
onConfirm,
|
||||
onSkip,
|
||||
}: {
|
||||
open: boolean;
|
||||
path?: string;
|
||||
mountBodies?: string[];
|
||||
onConfirm: (options: UrdfEnhancementOptions) => void;
|
||||
initialProfileId?: string;
|
||||
onConfirm: (options: UrdfEnhancementOptions, profileId?: string) => void;
|
||||
onSkip: () => void;
|
||||
}) {
|
||||
const [profileId, setProfileId] = useState(initialProfileId);
|
||||
const [options, setOptions] = useState<UrdfEnhancementOptions>(() => ({
|
||||
addActuators: true,
|
||||
addSensors: true,
|
||||
@@ -77,8 +81,20 @@ export function UrdfImportOptionsDialog({
|
||||
className="max-w-xl"
|
||||
footer={
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button onClick={onSkip}>不添加,直接加载</Button>
|
||||
<Button variant="primary" onClick={() => onConfirm(options)}>
|
||||
<Button onClick={onSkip} disabled={Boolean(profileId)}>
|
||||
不添加,直接加载
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() =>
|
||||
profileId
|
||||
? onConfirm(
|
||||
{ addActuators: false, addSensors: false, sensorType: 'camera' },
|
||||
profileId,
|
||||
)
|
||||
: onConfirm(options)
|
||||
}
|
||||
>
|
||||
转换并加载
|
||||
</Button>
|
||||
</div>
|
||||
@@ -88,90 +104,116 @@ export function UrdfImportOptionsDialog({
|
||||
导入 <strong className="text-text-primary">{path}</strong> 的仿真组件
|
||||
</p>
|
||||
<div className="mt-4 space-y-3">
|
||||
<OptionCard
|
||||
checked={options.addActuators}
|
||||
onChange={(addActuators) => setOptions((value) => ({ ...value, addActuators }))}
|
||||
icon={<Settings2 className="h-4 w-4" />}
|
||||
title="为关节添加驱动器"
|
||||
description="生成不限幅 motor 控制输入:hinge 为 N·m,slide 为 N。已有驱动器不重复添加。"
|
||||
/>
|
||||
<OptionCard
|
||||
checked={options.addSensors}
|
||||
onChange={(addSensors) => setOptions((value) => ({ ...value, addSensors }))}
|
||||
icon={<Camera className="h-4 w-4" />}
|
||||
title="添加传感器"
|
||||
description="在浮动基座添加三轴陀螺仪和三轴加速度计(6轴 IMU),并添加一台 640×480 固定摄像头。"
|
||||
/>
|
||||
{options.addSensors && (
|
||||
<div className="border-t border-border pt-3">
|
||||
<div className="mb-2 text-xs font-medium text-text-primary">摄像头安装参数</div>
|
||||
<label className="block text-xs text-text-secondary">
|
||||
<span className="mb-1 block">固连 Body</span>
|
||||
<Select
|
||||
aria-label="摄像头固连 Body"
|
||||
className="w-full"
|
||||
value={options.cameraMountBody ?? ''}
|
||||
onChange={(event) =>
|
||||
setOptions((value) => ({
|
||||
...value,
|
||||
cameraMountBody: event.target.value || undefined,
|
||||
}))
|
||||
}
|
||||
>
|
||||
{mountBodies.length ? (
|
||||
mountBodies.map((name) => (
|
||||
<option key={name} value={name}>
|
||||
{name}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="">自动选择头部/末端 Body</option>
|
||||
)}
|
||||
</Select>
|
||||
</label>
|
||||
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||
{(['X', 'Y', 'Z'] as const).map((axis, index) => (
|
||||
<label key={axis} className="text-xs text-text-secondary">
|
||||
<span className="mb-0.5 flex items-center gap-1">
|
||||
<span className={`axis-badge axis-${axis.toLowerCase()}`}>{axis}</span>
|
||||
位置(m)
|
||||
</span>
|
||||
<input
|
||||
aria-label={`摄像头位置 ${axis}`}
|
||||
className="field technical-value h-8 w-full px-1.5 text-xs"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={(options.cameraPosition ?? [0.1, 0, 0.05])[index]}
|
||||
onChange={(event) => setPosition(index, Number(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<label className="mt-3 block text-xs text-text-secondary">
|
||||
<span className="mb-1 block">镜头朝向(Body 局部轴)</span>
|
||||
<Select
|
||||
aria-label="摄像头朝向"
|
||||
className="w-full"
|
||||
value={options.cameraDirection ?? '+X'}
|
||||
onChange={(event) =>
|
||||
setOptions((value) => ({
|
||||
...value,
|
||||
cameraDirection: event.target.value as CameraDirection,
|
||||
}))
|
||||
}
|
||||
>
|
||||
{(['+X', '-X', '+Y', '-Y', '+Z', '-Z'] as CameraDirection[]).map((direction) => (
|
||||
<option key={direction}>{direction}</option>
|
||||
))}
|
||||
</Select>
|
||||
</label>
|
||||
<Tooltip content="位置和朝向相对于所选 Body;ROS 摄像头通常 +X 朝前、+Z 朝上">
|
||||
<span tabIndex={0} className="mt-2 inline-block text-xs text-text-tertiary">
|
||||
安装坐标说明
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<label className="block text-sm">
|
||||
机器人 profile(可选)
|
||||
<Select
|
||||
aria-label="URDF 机器人 profile"
|
||||
value={profileId}
|
||||
className="mt-1 w-full"
|
||||
onChange={(e) => setProfileId(e.target.value)}
|
||||
>
|
||||
<option value="">不启用(通用转换)</option>
|
||||
{ROBOT_PROFILES.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</label>
|
||||
{profileId && (
|
||||
<p className="text-xs text-warning">
|
||||
将校验固定模型版本,采用浮动基座、简化被动滚子/碰撞和九路伺服;不使用下方通用组件,不启用相机。
|
||||
</p>
|
||||
)}
|
||||
<fieldset
|
||||
disabled={Boolean(profileId)}
|
||||
className={profileId ? 'space-y-3 opacity-50' : 'space-y-3'}
|
||||
>
|
||||
<OptionCard
|
||||
checked={options.addActuators}
|
||||
onChange={(addActuators) => setOptions((value) => ({ ...value, addActuators }))}
|
||||
icon={<Settings2 className="h-4 w-4" />}
|
||||
title="为关节添加驱动器"
|
||||
description="生成不限幅 motor 控制输入:hinge 为 N·m,slide 为 N。已有驱动器不重复添加。"
|
||||
/>
|
||||
<OptionCard
|
||||
checked={options.addSensors}
|
||||
onChange={(addSensors) => setOptions((value) => ({ ...value, addSensors }))}
|
||||
icon={<Camera className="h-4 w-4" />}
|
||||
title="添加传感器"
|
||||
description="在浮动基座添加三轴陀螺仪和三轴加速度计(6轴 IMU),并添加一台 640×480 固定摄像头。"
|
||||
/>
|
||||
{options.addSensors && (
|
||||
<div className="border-t border-border pt-3">
|
||||
<div className="mb-2 text-xs font-medium text-text-primary">摄像头安装参数</div>
|
||||
<label className="block text-xs text-text-secondary">
|
||||
<span className="mb-1 block">固连 Body</span>
|
||||
<Select
|
||||
aria-label="摄像头固连 Body"
|
||||
className="w-full"
|
||||
value={options.cameraMountBody ?? ''}
|
||||
onChange={(event) =>
|
||||
setOptions((value) => ({
|
||||
...value,
|
||||
cameraMountBody: event.target.value || undefined,
|
||||
}))
|
||||
}
|
||||
>
|
||||
{mountBodies.length ? (
|
||||
mountBodies.map((name) => (
|
||||
<option key={name} value={name}>
|
||||
{name}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="">自动选择头部/末端 Body</option>
|
||||
)}
|
||||
</Select>
|
||||
</label>
|
||||
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||
{(['X', 'Y', 'Z'] as const).map((axis, index) => (
|
||||
<label key={axis} className="text-xs text-text-secondary">
|
||||
<span className="mb-0.5 flex items-center gap-1">
|
||||
<span className={`axis-badge axis-${axis.toLowerCase()}`}>{axis}</span>
|
||||
位置(m)
|
||||
</span>
|
||||
<input
|
||||
aria-label={`摄像头位置 ${axis}`}
|
||||
className="field technical-value h-8 w-full px-1.5 text-xs"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={(options.cameraPosition ?? [0.1, 0, 0.05])[index]}
|
||||
onChange={(event) => setPosition(index, Number(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<label className="mt-3 block text-xs text-text-secondary">
|
||||
<span className="mb-1 block">镜头朝向(Body 局部轴)</span>
|
||||
<Select
|
||||
aria-label="摄像头朝向"
|
||||
className="w-full"
|
||||
value={options.cameraDirection ?? '+X'}
|
||||
onChange={(event) =>
|
||||
setOptions((value) => ({
|
||||
...value,
|
||||
cameraDirection: event.target.value as CameraDirection,
|
||||
}))
|
||||
}
|
||||
>
|
||||
{(['+X', '-X', '+Y', '-Y', '+Z', '-Z'] as CameraDirection[]).map((direction) => (
|
||||
<option key={direction}>{direction}</option>
|
||||
))}
|
||||
</Select>
|
||||
</label>
|
||||
<Tooltip content="位置和朝向相对于所选 Body;ROS 摄像头通常 +X 朝前、+Z 朝上">
|
||||
<span tabIndex={0} className="mt-2 inline-block text-xs text-text-tertiary">
|
||||
安装坐标说明
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
</div>
|
||||
<p className="mt-4 text-xs text-text-tertiary">
|
||||
自动组件只写入浏览器内生成的 MJCF,不会修改本地 URDF 文件;使用“原生
|
||||
|
||||
@@ -9,11 +9,18 @@ const tools: ToolbarItem<InteractionMode>[] = [
|
||||
export function ViewerToolDock({
|
||||
mode,
|
||||
onModeChange,
|
||||
jointDisabled = false,
|
||||
}: {
|
||||
mode: InteractionMode;
|
||||
jointDisabled?: boolean;
|
||||
onModeChange: (mode: InteractionMode) => void;
|
||||
}) {
|
||||
return (
|
||||
<ToolbarToggleGroup items={tools} value={mode} onChange={onModeChange} label="视口交互模式" />
|
||||
<ToolbarToggleGroup
|
||||
items={tools.map((tool) => ({ ...tool, disabled: tool.value === 'joint' && jointDisabled }))}
|
||||
value={mode}
|
||||
onChange={onModeChange}
|
||||
label="视口交互模式"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import type { TrainingSceneCompiler } from '../../map/trainingMap';
|
||||
import type { PolicyDeployment } from '../../rl/deployment';
|
||||
import type { PlacedMapAsset } from '../../map/types';
|
||||
@@ -37,6 +37,7 @@ export function WorkspaceToolsPanel({
|
||||
active,
|
||||
snapshot,
|
||||
loading,
|
||||
externalControl,
|
||||
ignoreJointLimits,
|
||||
jointAdvanced,
|
||||
angleUnit,
|
||||
@@ -82,6 +83,7 @@ export function WorkspaceToolsPanel({
|
||||
active: WorkspaceTool;
|
||||
snapshot?: SimulationSnapshot;
|
||||
loading: boolean;
|
||||
externalControl?: ReactNode;
|
||||
ignoreJointLimits: boolean;
|
||||
jointAdvanced: boolean;
|
||||
angleUnit: 'rad' | 'deg';
|
||||
@@ -125,10 +127,21 @@ export function WorkspaceToolsPanel({
|
||||
onDataRecordingExport: (format: 'csv' | 'json') => void;
|
||||
}) {
|
||||
const [trainingSummary, setTrainingSummary] = useState('');
|
||||
const manualDisabled =
|
||||
loading || Boolean(snapshot?.controlOwner && snapshot.controlOwner !== 'manual');
|
||||
const resolvedControllerStatus = controllerStatus ?? snapshot?.controller;
|
||||
const resolvedPolicyStatus = policyStatus ?? snapshot?.rlPolicy;
|
||||
const controls = snapshot ? (
|
||||
<>
|
||||
{externalControl && (
|
||||
<CollapsibleSection
|
||||
title="开源项目 / 外部控制"
|
||||
defaultOpen={Boolean(snapshot.robot)}
|
||||
keepMounted
|
||||
>
|
||||
{externalControl}
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
<CollapsibleSection
|
||||
title="执行器实时控制"
|
||||
defaultOpen={false}
|
||||
@@ -143,6 +156,8 @@ export function WorkspaceToolsPanel({
|
||||
<ActuatorControl
|
||||
key={actuator.id}
|
||||
showParameterNotice={false}
|
||||
disabled={manualDisabled}
|
||||
parametersDisabled={Boolean(snapshot.robot)}
|
||||
actuator={actuator}
|
||||
onControl={(value) => onActuator(actuator.id, value)}
|
||||
onParameters={(parameters) => onActuatorParameters(actuator.id, parameters)}
|
||||
@@ -159,10 +174,13 @@ export function WorkspaceToolsPanel({
|
||||
badge={<ConsoleSectionBadges count={snapshot.joints.length} />}
|
||||
>
|
||||
<div className="mb-4 grid grid-cols-2 gap-2">
|
||||
<Button onClick={onResetJoints}>重置关节</Button>
|
||||
<Button onClick={onResetJoints} disabled={manualDisabled}>
|
||||
重置关节
|
||||
</Button>
|
||||
<Button
|
||||
variant={ignoreJointLimits ? 'primary' : 'secondary'}
|
||||
aria-pressed={ignoreJointLimits}
|
||||
disabled={manualDisabled}
|
||||
onClick={onToggleJointLimits}
|
||||
>
|
||||
忽略关节限位
|
||||
@@ -302,13 +320,19 @@ export function WorkspaceToolsPanel({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<LocalTrainingPanel
|
||||
onStatusChange={setTrainingSummary}
|
||||
onPolicyReady={onImportPolicy}
|
||||
compileScene={compileTrainingScene}
|
||||
sceneMaps={trainingSceneMaps}
|
||||
sceneDirty={trainingSceneDirty}
|
||||
/>
|
||||
{snapshot.robot && !snapshot.robot.capabilities.training ? (
|
||||
<p className="text-xs text-text-secondary">
|
||||
此机器人接口 V1 不支持训练。既有 Go2 训练任务不受影响,请使用对应模型。
|
||||
</p>
|
||||
) : (
|
||||
<LocalTrainingPanel
|
||||
onStatusChange={setTrainingSummary}
|
||||
onPolicyReady={onImportPolicy}
|
||||
compileScene={compileTrainingScene}
|
||||
sceneMaps={trainingSceneMaps}
|
||||
sceneDirty={trainingSceneDirty}
|
||||
/>
|
||||
)}
|
||||
</CollapsibleSection>
|
||||
</>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user