99 lines
5.4 KiB
TypeScript
99 lines
5.4 KiB
TypeScript
import { createFailure, type FacadeResponse } from "./facade-envelope.js";
|
|
import type { WorkbenchCommandRecord } from "./workbench-command-facade.js";
|
|
|
|
export interface MaterialValue {
|
|
uuid: string;
|
|
name: string;
|
|
description: string;
|
|
author: string;
|
|
license: string;
|
|
physical: Record<string, string>;
|
|
appearance: Record<string, string>;
|
|
physicalModels: string[];
|
|
appearanceModels: string[];
|
|
}
|
|
|
|
export interface MaterialDesktopState {
|
|
command: string;
|
|
cancelled?: boolean;
|
|
material?: MaterialValue;
|
|
sourceUuid?: string;
|
|
materials?: MaterialValue[];
|
|
materialLibraries?: string[];
|
|
modelLibraries?: string[];
|
|
migrationCompleted?: boolean;
|
|
savedPath?: string;
|
|
objects?: Array<{ object: string; label: string; property: string; material: MaterialValue }>;
|
|
file?: { name: string; base64: string };
|
|
}
|
|
|
|
export function prepareMaterialDesktopArgs(command: string, args: Record<string, unknown>, documentId: string): Record<string, unknown> {
|
|
const action = args.action === "accept" ? "apply"
|
|
: args.action === "read" ? (command === "Material_Edit" && !args.uuid ? "list" : "inspect")
|
|
: args.action ?? (command === "Material_Edit" ? "list" : "inspect");
|
|
const prepared = { ...args, action, documentId: args.documentId ?? args.document ?? documentId };
|
|
if (command === "Material_Edit") return { ...prepared, selection: [], object: "" };
|
|
return prepared;
|
|
}
|
|
|
|
export function materialDesktopState(response: FacadeResponse | null, command: string): MaterialDesktopState | undefined {
|
|
if (response?.kind !== "result" || !response.result || typeof response.result !== "object") return;
|
|
const records = (response.result as { records?: unknown }).records;
|
|
if (!Array.isArray(records)) return;
|
|
return records.find(item => item && typeof item === "object" && item.command === command
|
|
&& item.status === "exposed") as MaterialDesktopState | undefined;
|
|
}
|
|
|
|
export function downloadMaterialFile(file: { name: string; base64: string }): void {
|
|
const bytes = Uint8Array.from(atob(file.base64), character => character.charCodeAt(0));
|
|
const url = URL.createObjectURL(new Blob([bytes], { type: "application/octet-stream" }));
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = file.name.replace(/[\\/]/g, "_");
|
|
document.body.append(link);
|
|
try { link.click(); } finally { link.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); }
|
|
}
|
|
|
|
export async function persistMaterialDesktopResult(response: FacadeResponse, command: string, args: Record<string, unknown>): Promise<FacadeResponse> {
|
|
if (command !== "Material_Edit" || args.action !== "apply") return response;
|
|
const state = materialDesktopState(response, command);
|
|
if (!state || state.cancelled) return response;
|
|
if (!state.file) return createFailure("desktop-Material", "native_protocol_error", "Material save returned no FCMat file");
|
|
try {
|
|
const root = await navigator.storage.getDirectory();
|
|
const directory = await root.getDirectoryHandle("materials", { create: true });
|
|
const name = state.file.name.replace(/[\\/]/g, "_");
|
|
const handle = await directory.getFileHandle(name, { create: true });
|
|
const writer = await handle.createWritable();
|
|
try {
|
|
await writer.write(Uint8Array.from(atob(state.file.base64), character => character.charCodeAt(0)));
|
|
await writer.close();
|
|
} catch (error) {
|
|
await writer.abort().catch(() => undefined);
|
|
throw error;
|
|
}
|
|
state.savedPath = `materials/${name}`;
|
|
return response;
|
|
} catch (error) {
|
|
return createFailure("desktop-Material", "material_storage_failed", error instanceof Error ? error.message : String(error), { response });
|
|
}
|
|
}
|
|
|
|
/** Validates native Material desktop command records before they reach the UI. */
|
|
export function validateMaterialDesktopResponse(record: WorkbenchCommandRecord, response: FacadeResponse, capabilityId: string): FacadeResponse | undefined {
|
|
if (response.kind !== "result") return response;
|
|
if (!response.result || typeof response.result !== "object" || Array.isArray(response.result)) {
|
|
return createFailure("desktop-Material", "native_protocol_error", "Material command result is not an object", { capabilityId: record.id, response });
|
|
}
|
|
const batch = response.result as { schema?: string; module?: string; records?: Array<{ apiId?: string; status?: string; code?: string; message?: string }>; count?: number; resourcesReleased?: boolean };
|
|
if (batch.schema !== "freecad-web-material/python-family-batch/v1" || batch.module !== "Material"
|
|
|| !Array.isArray(batch.records) || batch.count !== batch.records.length || batch.resourcesReleased !== true) {
|
|
return createFailure("desktop-Material", "native_protocol_error", "Material command result has an invalid batch envelope", { capabilityId: record.id, response });
|
|
}
|
|
const native = batch.records.find((item) => item && typeof item === "object" && item.apiId === capabilityId);
|
|
if (!native) return createFailure("desktop-Material", "native_protocol_error", "Material command result is missing", { capabilityId: record.id });
|
|
if (native.status !== "exposed") return createFailure("desktop-Material", native.code || "native_desktop_command_failed", native.message || "Material command failed", { capabilityId: record.id, response });
|
|
if (!native.code || !native.message) return createFailure("desktop-Material", "native_protocol_error", "Material command result is incomplete", { capabilityId: record.id, response });
|
|
return undefined;
|
|
}
|