feat(web): complete product workflow implementation
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 08:33:27 +08:00
parent 7e3f6da78b
commit 1184a51ddf
39 changed files with 3736 additions and 160 deletions

View File

@@ -51,6 +51,9 @@ const FORMAT_EXTENSIONS: Record<IOFormatMatrixFormat, readonly string[]> = {
ALEMBIC: [".abc"],
};
/** Formats with a browser file boundary and a native WebEngine command route. */
const LOCAL_SCENE_FORMATS: readonly IOFormatMatrixFormat[] = ["GLTF", "GLB", "OBJ", "STL", "PLY"];
function routeFor(
matrix: IOFormatCapabilityMatrixIR,
operation: IOFormatMatrixOperation,
@@ -60,7 +63,8 @@ function routeFor(
.filter((entry) => {
const route = entry.operations[operation][execution.toLowerCase() as "local" | "server"];
const runtimeStatus = operation === "IMPORT" ? entry.runtimeImportStatus : entry.runtimeExportStatus;
return runtimeStatus === "AVAILABLE" && route.status === "READY" && route.execution === execution;
return route.status === "READY" && route.execution === execution &&
(execution === "SERVER" || (runtimeStatus === "AVAILABLE" && LOCAL_SCENE_FORMATS.includes(entry.format)));
})
.map((entry) => ({
format: entry.format,
@@ -70,6 +74,15 @@ function routeFor(
}));
}
function browserSceneRoutes(operation: IOFormatMatrixOperation): IOFormatUIRouteIR[] {
return LOCAL_SCENE_FORMATS.map((format) => ({
format,
operation,
execution: "LOCAL" as const,
extensions: [...FORMAT_EXTENSIONS[format]],
}));
}
export function buildIOFormatUIRegistry(matrix: IOFormatCapabilityMatrixIR, parentMatrixSha256: string): IOFormatUIRegistryIR {
if (!SHA256.test(parentMatrixSha256)) throw new IOFormatUIGateError("parent matrix SHA-256 is invalid");
return {
@@ -77,8 +90,8 @@ export function buildIOFormatUIRegistry(matrix: IOFormatCapabilityMatrixIR, pare
task: IO_FORMAT_UI_TASK,
parentMatrixSha256,
projectFileAccept: IO_FORMAT_PROJECT_ACCEPT,
importRoutes: routeFor(matrix, "IMPORT", "LOCAL"),
exportRoutes: routeFor(matrix, "EXPORT", "LOCAL"),
importRoutes: [...browserSceneRoutes("IMPORT"), ...routeFor(matrix, "IMPORT", "SERVER")],
exportRoutes: [...browserSceneRoutes("EXPORT"), ...routeFor(matrix, "EXPORT", "SERVER")],
};
}
@@ -96,9 +109,9 @@ function record(value: unknown, path: string): Record<string, unknown> {
function parseRoute(value: unknown, path: string, operation: IOFormatMatrixOperation): IOFormatUIRouteIR {
const input = record(value, path);
exactKeys(input, ["format", "operation", "execution", "extensions"], path);
if (typeof input.format !== "string" || !(input.format in FORMAT_EXTENSIONS) || input.operation !== operation || input.execution !== "LOCAL") throw new IOFormatUIGateError(`${path} route identity is invalid`);
if (typeof input.format !== "string" || !(input.format in FORMAT_EXTENSIONS) || input.operation !== operation || !["LOCAL", "SERVER"].includes(input.execution as string)) throw new IOFormatUIGateError(`${path} route identity is invalid`);
if (!Array.isArray(input.extensions) || input.extensions.length !== FORMAT_EXTENSIONS[input.format as IOFormatMatrixFormat].length || input.extensions.some((extension, index) => extension !== FORMAT_EXTENSIONS[input.format as IOFormatMatrixFormat][index])) throw new IOFormatUIGateError(`${path}.extensions are invalid`);
return { format: input.format as IOFormatMatrixFormat, operation, execution: "LOCAL", extensions: [...input.extensions as string[]] };
return { format: input.format as IOFormatMatrixFormat, operation, execution: input.execution as "LOCAL" | "SERVER", extensions: [...input.extensions as string[]] };
}
export function parseIOFormatUIRegistry(value: unknown): IOFormatUIRegistryIR {
@@ -135,11 +148,11 @@ export function filterIOFormatOperatorCommands<T extends { ioFormat?: IOFormatUI
return commands.filter((command) => !command.ioFormat || routeMatches(registry, command.ioFormat));
}
export function gateIOFormatFileSelection(fileName: string, registry: IOFormatUIRegistryIR): { status: "READY"; kind: "BLEND" } | { status: "BLOCKED"; code: "IO_FORMAT_UNSUPPORTED"; extension: string } {
export function gateIOFormatFileSelection(fileName: string, registry: IOFormatUIRegistryIR): { status: "READY"; kind: "BLEND" } | { status: "READY"; kind: "SCENE"; route: IOFormatUIRouteIR } | { status: "BLOCKED"; code: "IO_FORMAT_UNSUPPORTED"; extension: string } {
const extension = fileName.trim().toLowerCase().match(/\.[a-z0-9]+$/)?.[0] ?? "";
if (extension === ".blend") return { status: "READY", kind: "BLEND" };
const route = registry.importRoutes.find((candidate) => candidate.extensions.includes(extension));
if (route) return { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", extension };
if (route) return { status: "READY", kind: "SCENE", route };
return { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", extension };
}