106 lines
3.6 KiB
TypeScript
106 lines
3.6 KiB
TypeScript
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}`;
|
|
}
|