Files
workinf_Blender_Wasm/web/app/src/fonts/external-vfont-import.ts
mes123456 0fe8d2bb56
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
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

131 lines
5.9 KiB
TypeScript

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<StorageAssetPutResult>;
}
export interface ExternalVFontReadStoragePort {
readAsset(projectId: string, sha256: string): Promise<StorageAssetReadResult>;
}
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<ImportExternalVFontResult> {
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<ReplaceExternalVFontStyleResult> {
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 };
}