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
集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
235 lines
9.0 KiB
TypeScript
235 lines
9.0 KiB
TypeScript
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>
|
||
);
|
||
}
|