Files
Mujoco_WASM/web_platform/src/app/components/UrdfImportOptionsDialog.tsx
T
chenlin f3a8a38acd
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
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
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
feat: release v1.0.1 CADWorld 网站与 LeKiwi 智能抓放
集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
2026-09-24 09:57:41 +08:00

235 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
onChange,
icon,
title,
description,
children,
}: {
checked: boolean;
onChange: (checked: boolean) => void;
icon: ReactNode;
title: string;
description: string;
children?: ReactNode;
}) {
return (
<label
className={`flex cursor-pointer gap-3 rounded-lg border p-3 transition-colors ${checked ? 'border-accent/60 bg-accent/10' : 'border-border bg-surface hover:bg-element-hover'}`}
>
<input
className="mt-0.5 h-4 w-4 accent-accent"
type="checkbox"
checked={checked}
onChange={(event) => onChange(event.target.checked)}
/>
<span className="mt-0.5 text-text-secondary" aria-hidden="true">
{icon}
</span>
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium text-text-primary">{title}</span>
<span className="mt-1 block text-xs leading-5 text-text-secondary">{description}</span>
{children}
</span>
</label>
);
}
export function UrdfImportOptionsDialog({
open,
path,
mountBodies = [],
initialProfileId = '',
onConfirm,
onSkip,
}: {
open: boolean;
path?: string;
mountBodies?: string[];
initialProfileId?: string;
onConfirm: (options: UrdfEnhancementOptions, profileId?: string) => void;
onSkip: () => void;
}) {
const [profileId, setProfileId] = useState(initialProfileId);
const [options, setOptions] = useState<UrdfEnhancementOptions>(() => ({
addActuators: true,
addSensors: true,
sensorType: 'camera',
cameraMountBody:
mountBodies.find((name) => /(head|camera|sensor|neck|头)/i.test(name)) ?? mountBodies.at(-1),
cameraPosition: [0.1, 0, 0.05],
cameraDirection: '+X',
}));
const setPosition = (axis: number, value: number) =>
setOptions((current) => {
const position: [number, number, number] = [...(current.cameraPosition ?? [0.1, 0, 0.05])];
position[axis] = Number.isFinite(value) ? value : 0;
return { ...current, cameraPosition: position };
});
return (
<Dialog
open={open}
onClose={onSkip}
closable={false}
title="配置 URDF 仿真组件"
className="max-w-xl"
footer={
<div className="flex justify-end gap-2">
<Button onClick={onSkip} disabled={Boolean(profileId)}>
不添加,直接加载
</Button>
<Button
variant="primary"
onClick={() =>
profileId
? onConfirm(
{ addActuators: false, addSensors: false, sensorType: 'camera' },
profileId,
)
: onConfirm(options)
}
>
转换并加载
</Button>
</div>
}
>
<p className="break-words text-sm text-text-secondary">
导入 <strong className="text-text-primary">{path}</strong> 的仿真组件
</p>
<div className="mt-4 space-y-3">
<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>
<option value="MobileManipulator-LeKiwi-v1">LeKiwi v1 · 移动操作训练场景</option>
<option value="MobileManipulator-LeKiwi-Bundle">
LeKiwi Bundle · 移动操作训练场景
</option>
{ROBOT_PROFILES.map((p) => (
<option key={p.id} value={p.id}>
{p.label}
</option>
))}
</Select>
</label>
{profileId.startsWith('MobileManipulator-') && (
<p className="text-xs text-warning">
加载抓取物体与搬运目标;进入控制台 →
强化学习任务即可一键训练。移动操作模式不启用外部控制桥。
</p>
)}
{profileId && !profileId.startsWith('MobileManipulator-') && (
<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 文件;使用“原生
URDF”加载模式时不会注入这些组件。
</p>
</Dialog>
);
}