export function node( id: string, parentId: string | null = null, components: Record = {}, ) { return { id, name: id, parentId, enabled: true, transform: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }, components, }; } export function project(nodes: any[] = []) { return { format: "forma", version: 1, id: "test_project", name: "Regression fixture", revision: 0, activeSceneId: "scene_main", scenes: [{ id: "scene_main", name: "Main", entities: nodes }], assets: [], scripts: [], settings: { background: "#f3f0eb", ambient: 0.8, shadows: false, renderScale: 1, }, } as any; } export function scene(p: any) { return p.scenes.find((s: any) => s.id === p.activeSceneId); } export function findNode(p: any, id: string) { return scene(p).entities.find((e: any) => e.id === id); } /** A generated triangle, optionally skinned and animated; no game assets. */ export function triangleGlb(animated = false, configure?: (document: any, add: (data: Float32Array | Uint16Array, type: string, count: number, bounds?: Record) => number) => void): Buffer { const chunks: Buffer[] = []; const views: any[] = []; const accessors: any[] = []; let length = 0; const add = ( data: Float32Array | Uint16Array, type: string, count: number, bounds: Record = {}, ) => { const bytes = Buffer.from(data.buffer, data.byteOffset, data.byteLength); views.push({ buffer: 0, byteOffset: length, byteLength: bytes.length }); chunks.push(bytes); length += bytes.length; const padding = (4 - (length % 4)) % 4; if (padding) { chunks.push(Buffer.alloc(padding)); length += padding; } accessors.push({ bufferView: views.length - 1, componentType: data instanceof Float32Array ? 5126 : 5123, count, type, ...bounds, }); return accessors.length - 1; }; const position = add( new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), "VEC3", 3, { min: [0, 0, 0], max: [1, 1, 0] }, ); const normal = add(new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1]), "VEC3", 3); const indices = add(new Uint16Array([0, 1, 2]), "SCALAR", 3); const attributes: Record = { POSITION: position, NORMAL: normal, }; const document: any = { asset: { version: "2.0", generator: "Forma test fixture" }, scene: 0, scenes: [{ nodes: animated ? [0, 1] : [0] }], nodes: [{ name: "Triangle", mesh: 0 }], meshes: [{ primitives: [{ attributes, indices }] }], }; if (animated) { attributes.JOINTS_0 = add(new Uint16Array(12), "VEC4", 3); attributes.WEIGHTS_0 = add( new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]), "VEC4", 3, ); const inverseBindMatrices = add( new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]), "MAT4", 1, ); const input = add(new Float32Array([0, 1]), "SCALAR", 2, { min: [0], max: [1], }); const output = add(new Float32Array([0, 0, 0, 0, 0.2, 0]), "VEC3", 2); document.nodes[0].skin = 0; document.nodes.push({ name: "Joint" }); document.skins = [{ joints: [1], inverseBindMatrices }]; document.animations = [ { name: "Translate", samplers: [{ input, output, interpolation: "LINEAR" }], channels: [{ sampler: 0, target: { node: 1, path: "translation" } }], }, ]; } configure?.(document, add); Object.assign(document, { buffers: [{ byteLength: length }], bufferViews: views, accessors, }); const json = Buffer.from(JSON.stringify(document)); const jsonPadding = Buffer.alloc((4 - (json.length % 4)) % 4, 0x20); const body = Buffer.concat(chunks); const header = Buffer.alloc(12); header.writeUInt32LE(0x46546c67, 0); header.writeUInt32LE(2, 4); header.writeUInt32LE( 12 + 8 + json.length + jsonPadding.length + 8 + body.length, 8, ); const jsonHeader = Buffer.alloc(8); jsonHeader.writeUInt32LE(json.length + jsonPadding.length, 0); jsonHeader.writeUInt32LE(0x4e4f534a, 4); const bodyHeader = Buffer.alloc(8); bodyHeader.writeUInt32LE(body.length, 0); bodyHeader.writeUInt32LE(0x004e4942, 4); return Buffer.concat([ header, jsonHeader, json, jsonPadding, bodyHeader, body, ]); } export const triangleAsset = (animated = false) => ({ id: "asset_triangle", name: "Triangle.glb", kind: "model" as const, uri: "data:model/gltf-binary;base64," + triangleGlb(animated).toString("base64"), }); /** Material, camera, light and morph animation in one real glTF clip. */ export function sceneAnimationGlb() { return triangleGlb(false, (doc, add) => { doc.materials = [{ name: 'AnimatedPBR', pbrMetallicRoughness: { baseColorFactor: [1, 0, 0, 1], metallicFactor: .1, roughnessFactor: .2 } }]; doc.meshes[0].primitives[0].material = 0; doc.meshes[0].primitives[0].targets = [{ POSITION: add(new Float32Array([0,0,0, 0,0,0, 0,1,0]), 'VEC3', 3) }]; doc.meshes[0].weights = [0]; doc.cameras = [{ name: 'ImportedCamera', type: 'perspective', perspective: { yfov: .7, znear: .1 } }]; doc.nodes.push({ name: 'ImportedCamera', camera: 0, translation: [0, 0, 5] }, { name: 'ImportedLight', extensions: { KHR_lights_punctual: { light: 0 } } }); doc.scenes[0].nodes.push(1, 2); doc.cameras.push({ name: 'Ortho', type: 'orthographic', orthographic: { xmag: 2, ymag: 1, znear: .1, zfar: 100 } }); doc.nodes.push({ name: 'Ortho', camera: 1 });doc.scenes[0].nodes.push(3); doc.extensions = { KHR_lights_punctual: { lights: [{ name: 'ImportedLight', type: 'point', intensity: 10 }] } }; doc.extensionsUsed = ['KHR_lights_punctual', 'KHR_animation_pointer']; const input = add(new Float32Array([0, 1]), 'SCALAR', 2, { min: [0], max: [1] }); const animation: any = { name: 'Scene', channels: [], samplers: [] }; for (const [pointer, type, values] of [ ['/materials/0/pbrMetallicRoughness/baseColorFactor', 'VEC4', [1,0,0,1, 0,0,1,.5]], ['/materials/0/pbrMetallicRoughness/metallicFactor', 'SCALAR', [.1,.9]], ['/materials/0/pbrMetallicRoughness/roughnessFactor', 'SCALAR', [.2,.8]], ['/cameras/0/perspective/yfov', 'SCALAR', [.7,1.1]], ['/cameras/1/orthographic/xmag', 'SCALAR', [2,4]], ['/cameras/1/orthographic/ymag', 'SCALAR', [1,3]], ['/extensions/KHR_lights_punctual/lights/0/intensity', 'SCALAR', [10,20]], ] as const) { animation.channels.push({ sampler: animation.samplers.length, target: { path: 'pointer', extensions: { KHR_animation_pointer: { pointer } } } }); animation.samplers.push({ input, output: add(new Float32Array(values), type, 2), interpolation: 'LINEAR' }); } animation.channels.push({ sampler: animation.samplers.length, target: { node: 0, path: 'weights' } }); animation.samplers.push({ input, output: add(new Float32Array([0,1]), 'SCALAR', 2), interpolation: 'LINEAR' }); doc.animations = [animation]; }); }