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; appearance: Record; 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, documentId: string): Record { 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): Promise { 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; }