import { createExternalVFontMainImport, ExternalVFontValidationError, validateExternalVFontImport, validateStoredExternalVFontAsset, type ExternalVFontImportRequestIR, } from "../../../protocol/external-vfont"; import type { NonMeshFontLinksIR, SceneSnapshotIR, VFontResourceIR } from "../../../protocol/scene-ir"; import type { StorageAssetPutResult, StorageAssetReadResult, StorageAssetRecord } from "../../../protocol/storage"; import type { WebEngineEditCommand } from "../../../protocol/web-engine"; export interface ExternalVFontStoragePort { putAsset(projectId: string, data: ArrayBuffer, mimeType: string, sourcePath?: string): Promise; } export interface ExternalVFontReadStoragePort { readAsset(projectId: string, sha256: string): Promise; } export interface ExternalVFontEnginePort { applyCommand(payload: WebEngineEditCommand): Promise<{ snapshot: SceneSnapshotIR }>; } export interface ImportExternalVFontOptions { projectId: string; request: ExternalVFontImportRequestIR; storage: ExternalVFontStoragePort; engine: ExternalVFontEnginePort; } export interface ImportExternalVFontResult { asset: StorageAssetPutResult; vfont: VFontResourceIR; snapshot: SceneSnapshotIR; } export type ExternalVFontStyleSlot = keyof NonMeshFontLinksIR; export interface ReplaceExternalVFontStyleOptions { projectId: string; sha256: string; dataId: string; vfontId: string; style: ExternalVFontStyleSlot; snapshot: SceneSnapshotIR; storage: ExternalVFontReadStoragePort; engine: ExternalVFontEnginePort; } export interface ReplaceExternalVFontStyleResult { asset: StorageAssetRecord; vfont: VFontResourceIR; previousLinks: NonMeshFontLinksIR; links: NonMeshFontLinksIR; snapshot: SceneSnapshotIR; } function encodeBase64(data: ArrayBuffer): string { const bytes = new Uint8Array(data); let binary = ""; for (let offset = 0; offset < bytes.length; offset += 0x8000) { binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000)); } return btoa(binary); } export async function importExternalVFontIntoMain(options: ImportExternalVFontOptions): Promise { const validated = await validateExternalVFontImport(options.request); const asset = await options.storage.putAsset( options.projectId, validated.data.slice(0), validated.mimeType, validated.sourcePath, ); const mainImport = createExternalVFontMainImport(validated, asset); const { data, ...proof } = mainImport; const applied = await options.engine.applyCommand({ type: "importVFont", ...proof, base64: encodeBase64(data) }); const vfont = applied.snapshot.vfonts?.find((candidate) => candidate.sourcePath === proof.sourcePath && candidate.name === proof.name && candidate.packed && candidate.packedByteLength === proof.byteLength && candidate.sha256 === proof.sha256); if (!vfont) throw new Error("NON_MESH_RESOURCE_MISSING: imported VFont was not published from Blender Main"); return { asset, vfont, snapshot: applied.snapshot }; } export async function replaceExternalVFontStyleInMain( options: ReplaceExternalVFontStyleOptions, ): Promise { if (!(["regular", "bold", "italic", "boldItalic"] as string[]).includes(options.style)) { throw new ExternalVFontValidationError("NON_MESH_PROPERTY_INVALID", "external VFont style slot is invalid"); } const data = options.snapshot.nonMeshData?.find((candidate) => candidate.id === options.dataId); if (!data || data.type !== "FONT" || !data.fontLinks) { throw new ExternalVFontValidationError("NON_MESH_DATA_UNSUPPORTED", "font data block or style links are unavailable"); } const vfont = options.snapshot.vfonts?.find((candidate) => candidate.id === options.vfontId); if (!vfont || vfont.builtin || !vfont.packed || vfont.sha256 !== options.sha256 || !Number.isSafeInteger(vfont.packedByteLength) || (vfont.packedByteLength as number) <= 0) { throw new ExternalVFontValidationError("NON_MESH_RESOURCE_MISSING", "replacement VFont is not a verified packed Main resource"); } let stored: StorageAssetReadResult; try { stored = await options.storage.readAsset(options.projectId, options.sha256); } catch (error) { if (error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "NON_MESH_RESOURCE_MISSING") { throw new ExternalVFontValidationError("NON_MESH_RESOURCE_MISSING", "replacement VFont project asset is missing"); } throw error; } const validated = await validateStoredExternalVFontAsset(options.projectId, options.sha256, stored); if (vfont.sourcePath !== validated.sourcePath || vfont.packedByteLength !== validated.byteLength || vfont.sha256 !== validated.sha256) { throw new ExternalVFontValidationError("ASSET_SOURCE_HASH_MISMATCH", "packed Main VFont does not match its project asset"); } const previousLinks = { ...data.fontLinks }; if (previousLinks[options.style] === vfont.id) { throw new ExternalVFontValidationError("NON_MESH_PROPERTY_INVALID", "font style already uses the requested VFont"); } const links = { ...previousLinks, [options.style]: vfont.id }; const applied = await options.engine.applyCommand({ type: "setFontLinks", dataId: data.id, links }); const appliedData = applied.snapshot.nonMeshData?.find((candidate) => candidate.id === data.id); const appliedVFont = applied.snapshot.vfonts?.find((candidate) => candidate.id === vfont.id); if (!appliedData?.fontLinks || appliedData.fontLinks[options.style] !== vfont.id || !appliedVFont?.packed || appliedVFont.sha256 !== validated.sha256 || appliedVFont.packedByteLength !== validated.byteLength) { throw new ExternalVFontValidationError("NON_MESH_RESOURCE_MISSING", "Blender Main did not publish the verified font replacement"); } return { asset: stored.asset, vfont: appliedVFont, previousLinks, links, snapshot: applied.snapshot }; }