feat(lekiwi): release V0.10.1 初步集成 LeKiwi,优化碰撞模型
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled

集成通用机器人数值接口、本机控制桥、LeRobot 插件和统一键盘遥操作。采用离线 CoACD 全臂碰撞配方 revision 4、局部装配区切分与结构自接触,限制直接关节位姿写入并保留安全看门狗。同步版本号、变更记录、来源许可证和兼容性验证。
This commit is contained in:
2026-09-20 14:42:30 +08:00
parent da4d59c31a
commit 3ad29356c9
103 changed files with 23055 additions and 280 deletions
@@ -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>
</>
) : (