Files
Mujoco_WASM/wasm/web_platform/src/project/ProjectTree.tsx
T
chenlin 1eb05132f1
build / setup (compute matrix) (pull_request) Has been cancelled
build / ${{ matrix.label }} (pull_request) Has been cancelled
build / macos-15-arm64-studio (pull_request) Has been cancelled
build / ubuntu-24.04-clang-18-studio (pull_request) Has been cancelled
build / ubuntu-24.04-gcc-14-studio (pull_request) Has been cancelled
build / windows-2025-ninja-studio (pull_request) Has been cancelled
build / ubuntu-24.04-clang-18-wasm (pull_request) Has been cancelled
build / ubuntu-24.04-clang-18-mjx (pull_request) Has been cancelled
lint / pre-commit (pull_request) Has been cancelled
feat: add browser-based MuJoCo simulation platform
2026-08-20 15:12:08 +08:00

73 lines
3.6 KiB
TypeScript

import type {ModelEntry} from './types';
export interface ProjectTreeFile {path:string;size:number;}
export interface ProjectTreeNode {
name:string;
path:string;
kind:'directory'|'file';
size?:number;
children?:ProjectTreeNode[];
}
interface MutableDirectory {
name:string;
path:string;
directories:Map<string,MutableDirectory>;
files:ProjectTreeNode[];
}
function compareNodes(a:ProjectTreeNode,b:ProjectTreeNode):number {
if(a.kind!==b.kind)return a.kind==='directory'?-1:1;
return a.name.localeCompare(b.name,'zh-CN',{numeric:true,sensitivity:'base'});
}
/** 将规范化后的工程路径转换为“目录优先、名称排序”的资源树。 */
// 同文件导出纯函数是为了让资源树的数据转换可独立测试。
// eslint-disable-next-line react-refresh/only-export-components
export function buildProjectTree(files:ProjectTreeFile[]):ProjectTreeNode[] {
const root:MutableDirectory={name:'',path:'',directories:new Map(),files:[]};
for(const file of files){
const parts=file.path.split('/').filter(Boolean);
if(!parts.length)continue;
let parent=root;
for(const part of parts.slice(0,-1)){
const path=parent.path?`${parent.path}/${part}`:part;
let directory=parent.directories.get(part);
if(!directory){directory={name:part,path,directories:new Map(),files:[]};parent.directories.set(part,directory);}
parent=directory;
}
parent.files.push({name:parts.at(-1)!,path:file.path,kind:'file',size:file.size});
}
const finish=(directory:MutableDirectory):ProjectTreeNode[]=>[
...Array.from(directory.directories.values(),child=>({name:child.name,path:child.path,kind:'directory' as const,children:finish(child)})),
...directory.files,
].sort(compareNodes);
return finish(root);
}
function formatSize(bytes:number):string {
if(bytes<1024)return `${bytes} B`;
if(bytes<1024*1024)return `${(bytes/1024).toFixed(bytes<10*1024?1:0)} KB`;
return `${(bytes/(1024*1024)).toFixed(1)} MB`;
}
function TreeNodes({nodes,entryFormats,selectedEntry}:{nodes:ProjectTreeNode[];entryFormats:Map<string,ModelEntry['format']>;selectedEntry?:string}){
return <ul role="group" className="ml-3 border-l border-slate-800 pl-1">
{nodes.map(node=>node.kind==='directory'?<li key={`d:${node.path}`}>
<details open>
<summary title={node.path} className="cursor-pointer select-none truncate rounded px-1.5 py-1 text-xs text-slate-300 hover:bg-slate-800"><span aria-hidden="true" className="mr-1">📁</span>{node.name}</summary>
<TreeNodes nodes={node.children??[]} entryFormats={entryFormats} selectedEntry={selectedEntry}/>
</details>
</li>:<li key={`f:${node.path}`} title={node.path} className={`flex min-w-0 items-center gap-1 rounded px-1.5 py-1 text-xs ${selectedEntry===node.path?'bg-green-900/40 text-green-200':'text-slate-300 hover:bg-slate-800'}`}>
<span aria-hidden="true">{entryFormats.has(node.path)?'◇':'▧'}</span><span className="min-w-0 flex-1 truncate">{node.name}</span>{entryFormats.has(node.path)&&<span className="shrink-0 text-[10px] uppercase text-green-400">{entryFormats.get(node.path)}</span>}<span className="shrink-0 text-[10px] text-slate-500">{formatSize(node.size??0)}</span>
</li>)}
</ul>;
}
export function ProjectTree({files,entries,selectedEntry}:{files:ProjectTreeFile[];entries:ModelEntry[];selectedEntry?:string}){
const nodes=buildProjectTree(files);
const entryFormats=new Map(entries.map(entry=>[entry.path,entry.format]));
return <nav aria-label="工程文件树"><TreeNodes nodes={nodes} entryFormats={entryFormats} selectedEntry={selectedEntry}/></nav>;
}