feat(web-platform): release V0.2.1
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import {editableSourcePaths,exportedFileName,mergeCachedFiles,readCachedText,updateCachedText,upsertCachedMjcf} from './cachedFiles';
|
||||
import type {ProjectManifest} from './types';
|
||||
|
||||
const encoder=new TextEncoder();
|
||||
function fixture():ProjectManifest{const xml=encoder.encode('<mujoco/>'),png=new Uint8Array([1,2]);return {id:'p',name:'测试 工程.zip',files:[{path:'model.xml',data:xml,size:xml.byteLength,source:'zip',mimeType:'text/xml'},{path:'texture.png',data:png,size:png.byteLength,source:'zip',mimeType:'image/png'}],entries:[{path:'model.xml',format:'mjcf',label:'model'}],selectedEntry:'model.xml',totalBytes:xml.byteLength+png.byteLength};}
|
||||
|
||||
describe('cached source files',()=>{
|
||||
it('只列出可编辑文本并读取缓存',()=>{const manifest=fixture();expect(editableSourcePaths(manifest)).toEqual(['model.xml']);expect(readCachedText(manifest,'model.xml')).toBe('<mujoco/>');expect(()=>readCachedText(manifest,'texture.png')).toThrow('二进制');});
|
||||
it('以不可变方式更新会话缓存和大小',()=>{const original=fixture(),updated=updateCachedText(original,'model.xml','<mujoco model="edited"/>');expect(readCachedText(updated,'model.xml')).toContain('edited');expect(readCachedText(original,'model.xml')).toBe('<mujoco/>');expect(updated.totalBytes).toBe(updated.files.reduce((sum,file)=>sum+file.size,0));});
|
||||
it('合并转换生成的支持资源',()=>{const original=fixture(),obj={path:'mesh.mujoco.obj',data:encoder.encode('v 0 0 0'),size:7,source:'file' as const,mimeType:'text/plain'},updated=mergeCachedFiles(original,[obj]);expect(updated.files.map(file=>file.path)).toContain('mesh.mujoco.obj');expect(original.files.map(file=>file.path)).not.toContain('mesh.mujoco.obj');});
|
||||
it('创建可重新载入的 MJCF 缓存文件和入口',()=>{const updated=upsertCachedMjcf(fixture(),'.__converted_mjcf_cache__.xml','<mujoco model="cached"/>');expect(readCachedText(updated,'.__converted_mjcf_cache__.xml')).toContain('cached');expect(updated.entries.at(-1)).toMatchObject({path:'.__converted_mjcf_cache__.xml',format:'mjcf'});});
|
||||
it('生成安全的导出文件名',()=>{expect(exportedFileName('测试 工程.zip','urdf')).toBe('测试_工程.urdf');expect(exportedFileName('robot.xml','xml')).toBe('robot.xml');});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user