import type { GLTF } from "@gltf-transform/core"; import type { ToolcraftModelSourceBundleTransfer } from "../model-import-types"; import { createGltfTransfer, type ModelFixture, } from "./model-fixtures"; export const GLTF_APPEARANCE_PNG = new Uint8Array([ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, ]); export const GLTF_APPEARANCE_JPEG = new Uint8Array([ 0xff, 0xd8, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x04, 0x00, 0x05, 0x03, 0x01, 0x11, 0x00, 0x02, 0x11, 0x00, 0x03, 0x11, 0x00, 0xff, 0xd9, ]); function align4(value: number): number { return (value + 3) & ~3; } function bytesFor(view: ArrayBufferView): Uint8Array { return new Uint8Array(view.buffer, view.byteOffset, view.byteLength); } function join( views: readonly ArrayBufferView[], ): Readonly<{ bin: Uint8Array; bufferViews: GLTF.IBufferView[] }> { const bufferViews: GLTF.IBufferView[] = []; let length = 0; for (const view of views) { length = align4(length); bufferViews.push({ buffer: 0, byteLength: view.byteLength, byteOffset: length, }); length += view.byteLength; } const bin = new Uint8Array(align4(length)); views.forEach((view, index) => { bin.set(bytesFor(view), bufferViews[index]!.byteOffset); }); return { bin, bufferViews }; } export function createEmbeddedGltfPbrFixture(): ModelFixture { const positions = new Float32Array([ 0, 0, 0, 1, 0, 0, 0, 1, 0, ]); const uv0 = new Float32Array([0, 0, 1, 0, 0, 1]); const uv1 = new Float32Array([0.1, 0.2, 0.9, 0.2, 0.1, 0.8]); const colors = new Uint8Array([ 255, 0, 0, 255, 0, 255, 0, 128, 0, 0, 255, 64, ]); const indices = new Uint16Array([0, 1, 2]); const joined = join([ positions, uv0, uv1, colors, indices, GLTF_APPEARANCE_PNG, GLTF_APPEARANCE_JPEG, ]); return { bin: joined.bin, json: { accessors: [ { bufferView: 0, componentType: 5126, count: 3, type: "VEC3" }, { bufferView: 1, componentType: 5126, count: 3, type: "VEC2" }, { bufferView: 2, componentType: 5126, count: 3, type: "VEC2" }, { bufferView: 3, componentType: 5121, count: 3, normalized: true, type: "VEC4", }, { bufferView: 4, componentType: 5123, count: 3, type: "SCALAR" }, ], asset: { version: "2.0" }, buffers: [{ byteLength: joined.bin.byteLength, uri: "mesh.bin" }], bufferViews: joined.bufferViews, images: [ { bufferView: 5, mimeType: "image/png", name: "Albedo" }, { bufferView: 6, mimeType: "image/jpeg", name: "Packed" }, ], materials: [{ alphaCutoff: 0.4, alphaMode: "MASK", doubleSided: true, emissiveFactor: [0.1, 0.2, 0.3], emissiveTexture: { index: 1, texCoord: 0 }, normalTexture: { index: 0, scale: 0.35, texCoord: 0 }, occlusionTexture: { index: 1, strength: 0.45, texCoord: 1 }, pbrMetallicRoughness: { baseColorFactor: [0.25, 0.5, 0.75, 0.6], baseColorTexture: { index: 0, texCoord: 0 }, metallicFactor: 0.7, metallicRoughnessTexture: { index: 1, texCoord: 1 }, roughnessFactor: 0.2, }, }], meshes: [{ primitives: [{ attributes: { COLOR_0: 3, POSITION: 0, TEXCOORD_0: 1, TEXCOORD_1: 2, }, indices: 4, material: 0, mode: 4, }], }], nodes: [{ mesh: 0, name: "PBR mesh" }], samplers: [ { magFilter: 9728, minFilter: 9984, wrapS: 33071, wrapT: 33648 }, { magFilter: 9729, minFilter: 9987, wrapS: 10497, wrapT: 10497 }, ], scene: 0, scenes: [{ nodes: [0] }], textures: [ { sampler: 0, source: 0 }, { sampler: 1, source: 1 }, ], }, }; } function resourceFile( path: string, bytes: Uint8Array, mimeType: string, ): ToolcraftModelSourceBundleTransfer["sourceFiles"][number] { return Object.freeze({ bytes: bytes.slice().buffer, contentDigest: `fixture-${path}`, mimeType, path, }); } export function createLocalImageGltfTransfer( missing: "jpeg" | "none" | "png" = "none", ): ToolcraftModelSourceBundleTransfer { const fixture = createEmbeddedGltfPbrFixture(); const json = JSON.parse(JSON.stringify(fixture.json)) as GLTF.IGLTF; json.images = [ { name: "Albedo", uri: "textures/albedo.png" }, { name: "Packed", uri: "textures/packed.jpg" }, ]; const base = createGltfTransfer( { bin: fixture.bin, json }, "models/scene.gltf", "models/mesh.bin", ); return Object.freeze({ ...base, sourceFiles: Object.freeze([ ...base.sourceFiles, ...(missing === "png" ? [] : [resourceFile( "models/textures/albedo.png", GLTF_APPEARANCE_PNG, "image/png", )]), ...(missing === "jpeg" ? [] : [resourceFile( "models/textures/packed.jpg", GLTF_APPEARANCE_JPEG, "image/jpeg", )]), ]), }); }