feat(web-platform): release V0.7 地图模块
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled

This commit is contained in:
2026-09-01 14:23:49 +08:00
parent 60d3a6d68c
commit 3b4ced6437
58 changed files with 7287 additions and 269 deletions
+125 -12
View File
@@ -1,6 +1,6 @@
import { useState, type ReactNode } from 'react';
import { Box, FolderTree, Info, Settings2, SlidersHorizontal } from 'lucide-react';
import type { ModelEntry } from '../../project/types';
import { Box, FolderTree, Info, Map as MapIcon, Settings2, SlidersHorizontal } from 'lucide-react';
import type { MapEntry, ModelEntry } from '../../project/types';
import {
countProjectSearchResults,
ProjectTree,
@@ -34,6 +34,20 @@ import { ProjectBreadcrumb } from './ProjectBreadcrumb';
import { PythonControllerPanel } from './PythonControllerPanel';
import { RLPolicyPanel } from './RLPolicyPanel';
import { LocalTrainingPanel } from './LocalTrainingPanel';
import { PhysicalMapPanel } from './PhysicalMapPanel';
import { MapAssetLibrary } from './MapAssetLibrary';
import {
DEFAULT_PHYSICAL_MAP_CONFIG,
type MapSelection,
type SystemTerrainPreset,
} from '../../map/types';
import type {
EditableMapDocument,
EditableMapObjectType,
MapEditorInteractionCallbacks,
MapEditorTransformMode,
MapObjectPlacementMode,
} from '../../map/editor/types';
export function SidebarPanel({
title,
@@ -51,8 +65,10 @@ export function SidebarPanel({
<aside
className={`flex h-full w-full min-w-0 flex-col overflow-hidden bg-panel ${side === 'left' ? 'border-r' : 'border-l'} border-border`}
>
<h2 className="flex h-10 shrink-0 items-center gap-2 border-b border-border bg-panel px-3 text-sm font-semibold text-text-primary">
<Settings2 aria-hidden="true" className="h-4 w-4 text-accent" />
<h2 className="flex h-10 shrink-0 items-center gap-2 border-b border-border bg-panel/95 px-3 text-sm font-semibold tracking-tight text-text-primary">
<span className="grid h-6 w-6 place-items-center rounded-md bg-accent-soft text-accent">
<Settings2 aria-hidden="true" className="h-3.5 w-3.5" />
</span>
{title}
</h2>
{children}
@@ -69,9 +85,17 @@ export function ProjectSidebar({
snapshot,
loading,
visible = true,
nativeUrdf,
mapSelection,
editorDocument,
activeTab,
onActiveTabChange,
onRemove,
onSelectEntry,
onJointHover,
onAddMapAsset,
onSelectTerrain,
onSelectMapObject,
}: {
projectName?: string;
files: ProjectTreeFile[];
@@ -80,19 +104,31 @@ export function ProjectSidebar({
snapshot?: SimulationSnapshot;
loading: boolean;
visible?: boolean;
nativeUrdf: boolean;
mapSelection: MapSelection;
editorDocument: EditableMapDocument | null;
activeTab?: 'project' | 'structure' | 'assets';
onActiveTabChange?: (value: 'project' | 'structure' | 'assets') => void;
onRemove: () => void;
onSelectEntry: (path: string) => void;
onJointHover: (jointId: number | null) => void;
onAddMapAsset: (
type: EditableMapObjectType,
placementMode: MapObjectPlacementMode,
) => void | Promise<void>;
onSelectTerrain: (preset: SystemTerrainPreset) => void;
onSelectMapObject: (id: string) => void;
}) {
const [tab, setTab] = useState<'project' | 'structure'>('project'),
const [internalTab, setInternalTab] = useState<'project' | 'structure' | 'assets'>('project'),
[fileQuery, setFileQuery] = useState(''),
[structureQuery, setStructureQuery] = useState('');
const fileMatches = countProjectSearchResults(files, fileQuery),
const tab = activeTab ?? internalTab,
fileMatches = countProjectSearchResults(files, fileQuery),
structureMatches = snapshot
? countModelStructureSearchResults(snapshot.bodies, snapshot.joints, structureQuery)
: 0;
return (
<SidebarPanel title="工程资源" side="left" visible={visible}>
<SidebarPanel title="资产与结构" side="left" visible={visible}>
{projectName ? (
<>
<div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-2.5">
@@ -116,7 +152,10 @@ export function ProjectSidebar({
<Tabs
label="工程侧栏"
value={tab}
onValueChange={setTab}
onValueChange={(value) => {
setInternalTab(value);
onActiveTabChange?.(value);
}}
items={[
{
value: 'project',
@@ -170,6 +209,26 @@ export function ProjectSidebar({
<p className="p-4 text-center text-xs text-text-tertiary"></p>
),
},
{
value: 'assets',
label: '资产',
icon: <MapIcon className="h-3.5 w-3.5" />,
disabled: !snapshot,
content: (
<MapAssetLibrary
disabled={loading || nativeUrdf}
terrainSize={
mapSelection.kind === 'builtin'
? mapSelection.config.size
: DEFAULT_PHYSICAL_MAP_CONFIG.size
}
document={editorDocument}
onAdd={onAddMapAsset}
onSelectTerrain={onSelectTerrain}
onSelectObject={onSelectMapObject}
/>
),
},
]}
/>
</>
@@ -199,6 +258,11 @@ interface ModelControlsProps {
policyPaths: string[];
selectedPolicyPath?: string;
policyStatus?: RLPolicyStatus;
mapSelection: MapSelection;
maps: MapEntry[];
showVisualMap: boolean;
showMapCollision: boolean;
editorDocument: EditableMapDocument | null;
onUrdfMode: (value: UrdfLoadMode) => void;
onBaseMode: (value: UrdfBaseMode) => void;
onShowCollision: (value: boolean) => void;
@@ -222,13 +286,24 @@ interface ModelControlsProps {
onTogglePolicy: (enabled: boolean) => void;
onPolicyCommand: (command: RLCommand) => void;
onRemovePolicy: () => void;
onApplyMap: (value: MapSelection) => void;
onEditorPreview: (document: EditableMapDocument | null) => void;
onEditorApply: (document: EditableMapDocument) => Promise<boolean>;
onEditorExport: () => void;
onEditorConvert: () => Promise<boolean>;
onEditorBindInteraction: (callbacks: MapEditorInteractionCallbacks | null) => void;
onEditorSelect: (id: string | null) => void;
onEditorTransformMode: (mode: MapEditorTransformMode) => void;
onEditorSnapping: (translation: number | null, rotationDegrees: number | null) => void;
onMapDisplay: (visual: boolean, collision: boolean) => void;
onMapTabOpen?: () => void;
}
export function ModelControlsSidebar(props: ModelControlsProps) {
const [tab, setTab] = useState<'properties' | 'controls'>('properties'),
const [tab, setTab] = useState<'properties' | 'controls' | 'map'>('properties'),
s = props.snapshot;
if (!s)
return (
<SidebarPanel title="模型与控制" side="right" visible={props.visible}>
<SidebarPanel title="属性与参数" side="right" visible={props.visible}>
<div className="p-4 text-sm text-text-tertiary"></div>
</SidebarPanel>
);
@@ -439,11 +514,14 @@ export function ModelControlsSidebar(props: ModelControlsProps) {
</>
);
return (
<SidebarPanel title="模型与控制" side="right" visible={props.visible}>
<SidebarPanel title="属性与参数" side="right" visible={props.visible}>
<Tabs
label="模型控制侧栏"
value={tab}
onValueChange={setTab}
onValueChange={(value) => {
setTab(value);
if (value === 'map') props.onMapTabOpen?.();
}}
items={[
{
value: 'properties',
@@ -457,6 +535,41 @@ export function ModelControlsSidebar(props: ModelControlsProps) {
icon: <SlidersHorizontal className="h-3.5 w-3.5" />,
content: controls,
},
{
value: 'map',
label: '地图',
icon: <MapIcon className="h-3.5 w-3.5" />,
content: (
<PhysicalMapPanel
key={JSON.stringify(props.mapSelection)}
value={props.mapSelection}
maps={props.maps}
rootBodies={s.bodies
.filter(
(body) =>
body.id !== 0 &&
body.parentId === 0 &&
!body.name.startsWith('__platform_map_'),
)
.map((body) => body.name)}
loading={props.loading}
nativeUrdf={props.selectedFormat === 'urdf' && props.urdfMode === 'native'}
showVisualMap={props.showVisualMap}
showMapCollision={props.showMapCollision}
editorDocument={props.editorDocument}
onEditorPreview={props.onEditorPreview}
onEditorApply={props.onEditorApply}
onEditorExport={props.onEditorExport}
onEditorConvert={props.onEditorConvert}
onEditorBindInteraction={props.onEditorBindInteraction}
onEditorSelect={props.onEditorSelect}
onEditorTransformMode={props.onEditorTransformMode}
onEditorSnapping={props.onEditorSnapping}
onMapDisplay={props.onMapDisplay}
onApply={props.onApplyMap}
/>
),
},
]}
/>
</SidebarPanel>