feat(web): complete portable Blender feature gaps
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-09-06 19:33:50 +08:00
parent ed347098b4
commit 66d3f4e0f4
48 changed files with 58048 additions and 61 deletions

View File

@@ -171,10 +171,61 @@ export function gateIORequest(value: unknown): CapabilityGateResult {
}
export function gateLibraryMutation(operation: "CATALOG_EDIT" | "APPEND" | "LINK" | "OVERRIDE" | "RELOAD" | "RELOCATE"): CapabilityGateResult {
if (operation === "CATALOG_EDIT") return readyGate("N-023", operation);
if (["CATALOG_EDIT", "APPEND", "LINK", "OVERRIDE", "RELOAD"].includes(operation)) return readyGate("N-023", operation);
return blockedGate("N-023", operation, [capabilityIssue("LIBRARY_MUTATION_UNAVAILABLE", `${operation} requires a real Blender Main transaction`)]);
}
export type AssetLibraryCommandName = "assetMark" | "assetCatalog" | "libraryLink" | "libraryOverride" | "libraryReload";
export interface AssetLibraryCommandIR {
command: AssetLibraryCommandName;
params: Record<string, unknown>;
}
const ASSET_LIBRARY_COMMANDS = new Set<AssetLibraryCommandName>(["assetMark", "assetCatalog", "libraryLink", "libraryOverride", "libraryReload"]);
function boundedId(value: unknown, name: string): string {
return text(value, name, 256);
}
function boundedTags(value: unknown): string[] {
if (value === undefined) return [];
if (!Array.isArray(value) || value.length > ASSET_LIBRARY_BUDGET.maxTagsPerAsset) throw new AssetLibraryValidationError("ASSET_MANIFEST_INVALID", "tags are invalid");
return value.map((tag, index) => text(tag, `tags[${index}]`, 64));
}
/** Validate browser-originated asset/library commands before they cross into WASM. */
export function parseAssetLibraryCommand(command: string, value: unknown): AssetLibraryCommandIR {
if (!ASSET_LIBRARY_COMMANDS.has(command as AssetLibraryCommandName) || !record(value)) throw new AssetLibraryValidationError("ASSET_MANIFEST_INVALID", "Unsupported asset/library command");
const name = command as AssetLibraryCommandName;
const params: Record<string, unknown> = { ...value };
if (name === "assetMark") {
params.dataId = boundedId(params.dataId, "dataId");
params.catalogId = params.catalogId === undefined || params.catalogId === null ? null : boundedId(params.catalogId, "catalogId");
params.tags = boundedTags(params.tags);
params.marked = params.marked === undefined ? true : params.marked;
if (typeof params.marked !== "boolean") throw new AssetLibraryValidationError("ASSET_MANIFEST_INVALID", "marked is invalid");
}
else if (name === "assetCatalog") {
const operation = params.operation === undefined ? "LIST" : params.operation;
if (!["LIST", "UPSERT", "REMOVE"].includes(operation as string)) throw new AssetLibraryValidationError("ASSET_MANIFEST_INVALID", "catalog operation is invalid");
params.operation = operation;
if (operation !== "LIST") {
params.catalogId = boundedId(params.catalogId, "catalogId");
if (operation === "UPSERT") {
params.name = text(params.name, "name", 256);
params.parentId = params.parentId === undefined || params.parentId === null ? null : boundedId(params.parentId, "parentId");
}
}
}
else {
params.libraryId = boundedId(params.libraryId, "libraryId");
if (name === "libraryLink" || params.sourcePath !== undefined) params.sourcePath = projectPath(params.sourcePath, "sourcePath", "IO_EXTERNAL_URI_BLOCKED");
if (name === "libraryLink" || name === "libraryOverride") params.dataId = boundedId(params.dataId, "dataId");
}
return { command: name, params };
}
export function assetStorageCapabilities(scope: typeof globalThis = globalThis): { contentAddressedIndex: "LOCAL_BOUNDED"; opfs: "PROBE_REQUIRED" | "UNAVAILABLE" } {
const storage = (scope.navigator as Navigator & { storage?: { getDirectory?: unknown } } | undefined)?.storage;
return { contentAddressedIndex: "LOCAL_BOUNDED", opfs: storage && typeof storage.getDirectory === "function" ? "PROBE_REQUIRED" : "UNAVAILABLE" };