131 lines
5.9 KiB
TypeScript
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 };
|
|
}
|