import type { BlenderOperatorPropertyValue } from './blender-operator'; import type { EditorMode } from './editor-workflow'; export type NativeWorkflowKind = 'RENDER' | 'PYTHON' | 'DESKTOP_OPERATOR' | 'EXPORT'; export type NativeExportFormat = 'GLB' | 'GLTF' | 'FBX' | 'OBJ' | 'STL' | 'PLY' | 'USD' | 'ALEMBIC'; export interface NativeWorkflowContext { revision?: number; sceneName?: string; activeObject?: string; selectedObjects?: string[]; mode?: EditorMode; meshSelection?: { domain: 'VERT' | 'EDGE' | 'FACE'; indices: number[] }; nonMeshSelection?: { controlPoints: number[] }; nodeTreeType?: 'ShaderNodeTree' | 'GeometryNodeTree' | 'CompositorNodeTree'; nodeTreeName?: string; areaType?: 'VIEW_3D' | 'IMAGE_EDITOR' | 'NODE_EDITOR' | 'GRAPH_EDITOR' | 'DOPESHEET_EDITOR' | 'SEQUENCE_EDITOR' | 'TEXT_EDITOR' | 'NLA_EDITOR' | 'CLIP_EDITOR' | 'CONSOLE' | 'INFO' | 'OUTLINER' | 'PROPERTIES' | 'FILE_BROWSER' | 'SPREADSHEET' | 'PREFERENCES'; } export interface NativeWorkflowInput { kind: NativeWorkflowKind; context: NativeWorkflowContext; resources?: { token: string; sha256: string }; desktop?: { width: number; height: number; preserveLayout: boolean; useSavedStartup?: boolean; recoveryKey?: string; recoverySavedAt?: number }; render?: { engine?: 'SCENE' | 'CYCLES' | 'BLENDER_EEVEE' | 'BLENDER_WORKBENCH'; frameStart: number; frameEnd: number; step: number; format: 'PNG' | 'OPEN_EXR' | 'FFMPEG'; resolutionPercentage?: number }; python?: { source: string; textName: string; console: boolean }; operator?: { id: string; properties: Record }; export?: { format: NativeExportFormat; selectedOnly: boolean; properties: Record }; } export interface NativeSessionHandle { id: string; token: string; sourceRevision: number; sourceBlendSha256: string; width: number; height: number; } export type NativeSessionCommand = { kind: 'read-main' } | { kind: 'python'; source: string } | { kind: 'undo' } | { kind: 'redo' } | { kind: 'invoke'; operator: string; properties: Record } | { kind: 'commit' } | { kind: 'cancel-modal' } | { kind: 'preferences-save' } | { kind: 'editor'; areaType: NonNullable } | { kind: 'clipboard'; text: string } | { kind: 'clipboard-read' } | { kind: 'file-browser'; path: string; filename?: string } | { kind: 'open-blend'; path: string } | { kind: 'download-project' }; export type NativeDesktopEvent = { type: 'move'; x: number; y: number } | { type: 'button'; x: number; y: number; button: number; down: boolean } | { type: 'key'; code: string; key?: string; locks?:{caps:boolean;num:boolean}; down: boolean } | { type: 'wheel'; delta: number; horizontal?: boolean } | { type: 'release' } | { type:'pen'; x:number; y:number; action:'move'|'press'|'release'; button:number; pressure:number; tiltX:number; tiltY:number; eraser:boolean }; export interface NativeWorkflowRequest extends NativeWorkflowInput { schemaVersion: 1; jobId: string; sourceRevision: number; sourceBlendSha256: string; sourceBlendByteLength: number; timeoutMs: number; requestSha256: string; } export interface NativeWorkflowArtifact { id: string; name: string; mime: string; bytes: number; sha256: string; frame?: number; } export interface NativeMainReceipt { command: string; sequence: number; activeObject?: string | null; objects: { name: string; location: [number, number, number] }[]; } export interface NativeWorkflowResult { schemaVersion: 1; jobId: string; requestSha256: string; sourceBlendSha256: string; sourceRevision: number; kind: NativeWorkflowKind; status: 'RUNNING' | 'SUCCEEDED' | 'FAILED' | 'CANCELLED'; error?: string; output?: string; artifacts?: NativeWorkflowArtifact[]; blenderVersion?: string; progress?: { completed: number; total: number }; resources?: { token: string; sha256: string }; } export function canonicalNativeWorkflow(value: unknown): string { if (Array.isArray(value)) return `[${value.map(canonicalNativeWorkflow).join(',')}]`; if (value && typeof value === 'object') return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([a],[b]) => a < b ? -1 : a > b ? 1 : 0).map(([key,item]) => `${JSON.stringify(key)}:${canonicalNativeWorkflow(item)}`).join(',')}}`; if (typeof value === 'number' && !Number.isFinite(value)) throw new Error('NATIVE_WORKFLOW_NONFINITE'); return JSON.stringify(value); } export function encodeNativeWorkflow(request: NativeWorkflowRequest, source: ArrayBuffer): ArrayBuffer { const metadata = new TextEncoder().encode(JSON.stringify(request)); if (metadata.length > 256 * 1024 || source.byteLength > 512 * 1024 * 1024) throw new Error('NATIVE_WORKFLOW_BUDGET'); const bytes = new Uint8Array(12 + metadata.length + source.byteLength); bytes.set(new TextEncoder().encode('BWF1')); const view = new DataView(bytes.buffer); view.setUint32(4, metadata.length, true); view.setUint32(8, source.byteLength, true); bytes.set(metadata, 12); bytes.set(new Uint8Array(source), 12 + metadata.length); return bytes.buffer; }