import { getToolcraftRuntimeBackgroundColor, isToolcraftRuntimeBackgroundEnabled, } from "../state/canvas-background-state"; import { downloadToolcraftArtifact, type ToolcraftArtifactDownloadRequest, type ToolcraftArtifactDownloadResult, type ToolcraftArtifactFileExtension, } from "./artifact-download"; import type { ToolcraftArtifactExportRequest } from "./artifact-export-request"; import { resolveToolcraftImageExportSettings } from "./artifact-export-settings"; import { renderToolcraftArtifactFrame } from "./artifact-frame-renderer"; import { resolveToolcraftStillArtifactFrame } from "./artifact-scene-frame"; import { createToolcraftArtifactFrameState } from "./artifact-frame-state"; import { ToolcraftSceneExportError, validateToolcraftArtifactSize, } from "./export-frame"; import { ToolcraftArtifactExportError } from "./export-error"; import { getToolcraftImageExportSize } from "./export-sizing"; export type ToolcraftImageArtifactExportResult = Readonly<{ byteLength: number; extension: ".jpg" | ".png"; height: number; mediaType: "image/jpeg" | "image/png"; width: number; }>; export type ToolcraftImageArtifactExportRequest = ToolcraftArtifactExportRequest & Readonly<{ canvasFactory?: () => HTMLCanvasElement; downloadArtifact?: ( request: ToolcraftArtifactDownloadRequest, ) => ToolcraftArtifactDownloadResult; }>; function encodeCanvas( canvas: HTMLCanvasElement, mediaType: "image/jpeg" | "image/png", ): Promise { return new Promise((resolve, reject) => { try { canvas.toBlob( (blob) => { if (blob) { resolve(blob); } else { reject( new ToolcraftArtifactExportError({ code: "image-encode-failed", message: "Toolcraft could not encode the image export.", }), ); } }, mediaType, mediaType === "image/jpeg" ? 0.95 : undefined, ); } catch (error) { reject( new ToolcraftArtifactExportError( { code: "image-encode-failed", message: "Toolcraft could not encode the image export.", }, { cause: error }, ), ); } }); } export async function exportToolcraftImageArtifact( request: ToolcraftImageArtifactExportRequest, ): Promise { const settings = resolveToolcraftImageExportSettings(request.state); const frameState = createToolcraftArtifactFrameState( request.state, request.state.timeline.currentTimeSeconds, ); const frame = resolveToolcraftStillArtifactFrame({ boundsProvider: request.boundsProvider, productSceneRequired: request.productSceneRequired, state: frameState, visibility: request.visibility, }); const size = getToolcraftImageExportSize({ frame, resolution: settings.resolution, state: frameState, }); const sizeValidation = validateToolcraftArtifactSize(size); if (!sizeValidation.ok) { throw new ToolcraftSceneExportError(sizeValidation); } const canvas = (request.canvasFactory ?? (() => document.createElement("canvas")))(); const mediaType = settings.format === "jpg" ? "image/jpeg" : "image/png"; const extension: ToolcraftArtifactFileExtension = settings.format === "jpg" ? ".jpg" : ".png"; const includeBackground = mediaType === "image/jpeg" || isToolcraftRuntimeBackgroundEnabled(frameState); try { canvas.width = size.width; canvas.height = size.height; request.reportProgress(0.1); await renderToolcraftArtifactFrame({ backgroundColor: getToolcraftRuntimeBackgroundColor(frameState) ?? "#000000", canvas, frame, includeBackground, pixelRatio: size.pixelRatio, renderProductFrame: request.exportRenderer?.renderFrame ?? null, renderRuntimeScene: request.renderRuntimeScene, rendererPipeline: request.rendererPipeline, state: frameState, }); request.reportProgress(0.75); const blob = await encodeCanvas(canvas, mediaType); request.reportProgress(0.9); (request.downloadArtifact ?? downloadToolcraftArtifact)({ blob, extension, rawBaseFileName: request.exportRenderer?.baseFileName ?? "toolcraft-export", }); request.reportProgress(1); return { byteLength: blob.size, extension, height: size.height, mediaType, width: size.width, }; } finally { canvas.width = 1; canvas.height = 1; } }