import type {ProjectManifest} from './types'; const TEXT_EXTENSIONS=/\.(?:xml|urdf|txt|obj|mtl|csv|json|yaml|yml)$/i; const decoder=new TextDecoder('utf-8',{fatal:false}); const encoder=new TextEncoder(); export function isEditableSource(path:string):boolean{return TEXT_EXTENSIONS.test(path);} export function editableSourcePaths(manifest:ProjectManifest):string[]{ return manifest.files.filter(file=>isEditableSource(file.path)).map(file=>file.path).sort((a,b)=>a.localeCompare(b)); } export function readCachedText(manifest:ProjectManifest,path:string):string{ const file=manifest.files.find(candidate=>candidate.path===path); if(!file)throw new Error(`缓存中找不到文件:${path}`); if(!isEditableSource(path))throw new Error(`不支持编辑二进制文件:${path}`); return decoder.decode(file.data); } /** 返回只更新浏览器会话内存的新工程清单,不接触用户本地文件系统。 */ export function mergeCachedFiles(manifest:ProjectManifest,additional:ProjectManifest['files']):ProjectManifest{ if(!additional.length)return manifest; const byPath=new Map(manifest.files.map(file=>[file.path,file])); for(const file of additional)byPath.set(file.path,file); const files=Array.from(byPath.values()); return {...manifest,files,totalBytes:files.reduce((total,item)=>total+item.size,0)}; } export function upsertCachedMjcf(manifest:ProjectManifest,path:string,text:string):ProjectManifest{ const data=encoder.encode(text),index=manifest.files.findIndex(candidate=>candidate.path===path); const files=manifest.files.slice(); const file={path,data,size:data.byteLength,source:'file' as const,mimeType:'application/xml'}; if(index<0)files.push(file);else files[index]={...files[index],...file}; const entries=manifest.entries.some(entry=>entry.path===path)?manifest.entries:[...manifest.entries,{path,format:'mjcf' as const,label:`${path} (MJCF 缓存)`}]; return {...manifest,files,entries,totalBytes:files.reduce((total,item)=>total+item.size,0)}; } export function updateCachedText(manifest:ProjectManifest,path:string,text:string):ProjectManifest{ const index=manifest.files.findIndex(candidate=>candidate.path===path); if(index<0)throw new Error(`缓存中找不到文件:${path}`); if(!isEditableSource(path))throw new Error(`不支持编辑二进制文件:${path}`); const data=encoder.encode(text),files=manifest.files.slice(); files[index]={...files[index],data,size:data.byteLength,mimeType:files[index].mimeType||'text/plain'}; return {...manifest,files,totalBytes:files.reduce((total,file)=>total+file.size,0)}; } export function downloadBytes(data:Uint8Array,fileName:string,mimeType='application/xml'):void{ const blob=new Blob([data as BlobPart],{type:`${mimeType};charset=utf-8`}); const url=URL.createObjectURL(blob),anchor=document.createElement('a'); anchor.href=url;anchor.download=fileName;anchor.style.display='none';document.body.append(anchor);anchor.click();anchor.remove(); setTimeout(()=>URL.revokeObjectURL(url),0); } export function exportedFileName(projectName:string,extension:'urdf'|'xml'):string{ const stem=projectName.replace(/\.(?:zip|xml|urdf)$/i,'').replace(/[^\p{L}\p{N}._-]+/gu,'_')||'model'; return `${stem}.${extension}`; }