UNPKG

@shapediver/sdk.sdtf-v1

Version:
317 lines (264 loc) 13.2 kB
import { ISdtfIntegration, ISdtfWriteableAsset, ISdtfWriteableAttribute, ISdtfWriteableBuffer, ISdtfWriteableBufferView, ISdtfWriteableComponentFactory, ISdtfWriteableDataItem, ISdtfWriteableTypeHint, } from '@shapediver/sdk.sdtf-core'; import { SdtfWriteableAsset } from './components/SdtfWriteableAsset'; import { SdtfWriteableBuffer } from './components/SdtfWriteableBuffer'; import { ISdtfWriteableComponentList, writeableComponentListFromAsset, } from './ISdtfWriteableComponentList'; import { ISdtfWriteableComponentPostProcessor } from './ISdtfWriteableComponentPostProcessor'; import { SdtfWriteableComponentFactory } from './SdtfWriteableComponentFactory'; export class SdtfWriteableComponentPostProcessor implements ISdtfWriteableComponentPostProcessor { private readonly factory: ISdtfWriteableComponentFactory; constructor(private readonly integrations: ISdtfIntegration[]) { this.factory = new SdtfWriteableComponentFactory(); } optimize(asset: ISdtfWriteableAsset): ISdtfWriteableComponentList { // The optimization step will most likely change the given writeable objects. // Without cloning, this function would consume the given asset and might make it's inner structure invalid. const clonedAsset = SdtfWriteableAsset.clone(asset); let componentList = writeableComponentListFromAsset(clonedAsset); // Apply integration writers on data components this.processDataComponents( componentList.attributes.flatMap((a) => Object.values(a.entries)) ); this.processDataComponents(componentList.items); // Apply integration post-processor on data components this.postProcessDataComponents([ ...componentList.attributes.flatMap((a) => Object.values(a.entries)), ...componentList.items, ]); // `processDataComponents` might create new components. // Thus, we generate the component list again to include those new components as well. componentList = writeableComponentListFromAsset(clonedAsset); this.complementTypeHints(componentList); this.removeDuplicatedTypeHints(componentList); this.resolveBuffers(componentList); return componentList; } /** * Tries to find a suitable registered integration for each given component and runs the integration's writer for * each individual component. * @private */ processDataComponents(components: (ISdtfWriteableAttribute | ISdtfWriteableDataItem)[]): void { components.forEach((component) => { // Get the first integration that is supporting the given type hint const integration = this.integrations.find((i) => i.isTypeHintSupported(component.typeHint?.name ?? '') ); // Stop when no integration was found for this type hint if (!integration) return; // Post-process single component integration.getWriter(this.factory).writeComponent(component); }); } /** * Tries to find a suitable registered integration for all given components and runs the integration's * post-processor for all supported component (grouped). * @private */ postProcessDataComponents( components: (ISdtfWriteableAttribute | ISdtfWriteableDataItem)[] ): void { this.integrations.forEach((integration) => { // Find all components that are supported by this integration const supportedComponents = components.filter((component) => integration.isTypeHintSupported(component.typeHint?.name ?? '') ); // Stop when no components were found for this integration if (supportedComponents.length === 0) return; // Post-process collective component integration.getWriter(this.factory).postProcessComponents(supportedComponents); }); } /** * Bottom-up approach to complement a missing type hint in node and chunk components. * When all data items of a node are of a similar type hint, the respective type hint will be added to the node. * When all nodes of a chunk are of a similar type hint, the respective type hint will be added to the chunk. * @private */ complementTypeHints(componentList: ISdtfWriteableComponentList): void { // Helper function to complement const complement = ( base: { typeHint?: ISdtfWriteableTypeHint }, list: { typeHint?: ISdtfWriteableTypeHint }[] ): void => { // Stop if the base has already a type hint with a name assigned if (base.typeHint?.name !== undefined) return; // Stop if the list is empty if (list.length === 0) return; let typeHintName = list[0].typeHint?.name; // Stop if the type hint is undefined if (typeHintName === undefined) return; // Set type hint and update component list. if (list.every((i) => i.typeHint?.name === typeHintName)) { base.typeHint = this.factory.createTypeHint(typeHintName); componentList.typeHints.push(base.typeHint); } }; // Check every node whether their respective data items are of similar a type hint componentList.nodes.forEach((node) => complement(node, node.items)); // Check every chunk whether their respective nodes are of similar a type hint componentList.chunks.forEach((chunk) => complement(chunk, chunk.nodes)); } /** * Generates a list of unique type hints for all components in the list and resets the component references accordingly. * @private */ removeDuplicatedTypeHints(componentList: ISdtfWriteableComponentList): void { const uniqueTypeHints: ISdtfWriteableTypeHint[] = []; // Find all different type hint names componentList.typeHints.forEach((typeHint) => { if (!uniqueTypeHints.find(this.areTypeHintsSimilar.bind(this, typeHint))) uniqueTypeHints.push(typeHint); }); // Replace the type hint of all components with the unique ones const replaceTypeHints = (component: { typeHint?: ISdtfWriteableTypeHint }): void => { // Stop if no type hint or type hint already in unique-list if ( !component.typeHint || uniqueTypeHints.find( (typeHint) => typeHint.componentId === component.typeHint!.componentId ) ) return; // We know that there is a unique type hint for this component, so just search for it component.typeHint = uniqueTypeHints.find((typeHint) => this.areTypeHintsSimilar(typeHint, component.typeHint!) ); }; componentList.attributes.forEach((a) => Object.values(a.entries).forEach((e) => replaceTypeHints(e)) ); componentList.chunks.forEach((c) => replaceTypeHints(c)); componentList.items.forEach((i) => replaceTypeHints(i)); componentList.nodes.forEach((n) => replaceTypeHints(n)); // Update type hints in component list componentList.typeHints = Object.values(uniqueTypeHints); } /** * Helper function to compare the two given type hints. * Returns true, if both type hints have the same `name` property and share the same `additionalProperties`. * However, the order of additional properties is not considered. * @private */ areTypeHintsSimilar(t1: ISdtfWriteableTypeHint, t2: ISdtfWriteableTypeHint): boolean { const nAdditionalPropertiesT1 = Object.entries(t1.additionalProperties ?? {}), nAdditionalPropertiesT2 = Object.entries(t2.additionalProperties ?? {}); if ( t1.name !== t2.name || nAdditionalPropertiesT1.length !== nAdditionalPropertiesT2.length ) return false; for (let i = 0; i < nAdditionalPropertiesT1.length; i++) { const [key, value] = nAdditionalPropertiesT1[i]; if (t2.additionalProperties[key] !== value) return false; } return true; } /** * Merges all individual buffers by their URI. * @private */ resolveBuffers(componentList: ISdtfWriteableComponentList): void { // List that sorts buffers/bufferViews by the buffer uri const bufferViewsPerUri: Record<string, ISdtfWriteableBufferView[]> = {}; // Add buffer view to list componentList.bufferViews.forEach((bufferView) => { // We are not interested in buffer views that are not linked to a buffer if (!bufferView.buffer) return; const uri = bufferView.buffer.uri ?? ''; if (!bufferViewsPerUri[uri]) bufferViewsPerUri[uri] = []; bufferViewsPerUri[uri].push(bufferView); }); // We want to create a new buffer per uri that contains all buffer data of our list const mergedBuffers: ISdtfWriteableBuffer[] = []; Object.entries(bufferViewsPerUri).forEach(([uri, bufferViews]) => { const [mergedBuffer, offsets] = this.mergeBuffers( uri, bufferViews.map((bv) => bv.buffer!) ); // Update buffer information in buffer view bufferViews.forEach((bufferView, i) => { bufferView.byteOffset = offsets[i]; bufferView.byteLength = bufferView.buffer!.data!.byteLength ?? 0; bufferView.buffer = mergedBuffer; }); mergedBuffers.push(mergedBuffer); }); // Update buffers in component list componentList.buffers = mergedBuffers; } /** * Creates a new writeable buffer that holds the data of all given buffers. * It returns the newly created buffer and a list of `byte offsets` corresponding to the given buffer. * This allows to track down the individual buffers in the merged one. * @private * @returns - [ merged buffer, byte offset for each buffer ] */ mergeBuffers(uri: string, buffers: ISdtfWriteableBuffer[]): [ISdtfWriteableBuffer, number[]] { const merged = new SdtfWriteableBuffer(); merged.uri = uri; // merged.additionalProperties = { ...buffers.map(b => b.additionalProperties ?? {}) } // Merge their additional properties merged.additionalProperties = {}; // Merge additional properties Object.assign(merged.additionalProperties, ...buffers.map((b) => b.additionalProperties)); // Merge buffer data const [mergedData, offsetsPerBuffer] = this.mergeBufferData(buffers); merged.byteLength = mergedData.byteLength; merged.data = mergedData; return [merged, offsetsPerBuffer]; } /** * Creates a new writeable buffer that holds the data of all given buffers (hard copy!). * It returns the newly created buffer and each buffer's `byte offset`. * @private * @returns - [ merged buffer, byte offset for each buffer ] */ mergeBufferData(buffers: ISdtfWriteableBuffer[]): [ArrayBuffer, number[]] { /* * According to the sdTF v1 specification, when concatenating two buffers, the size of the first buffer must be * of a multiple of 4. This helper function rounds the given byte size up meet that criteria. */ const roundToNextMultipleOfFour = (value: number): number => { const diff = value % 4; return diff === 0 ? value : value + 4 - diff; }; // The first buffer start at the beginning let offsets: number[] = [0], lastBufferLength = buffers[0].data?.byteLength ?? 0; // Calculate the offsets of each buffer if (buffers.length > 0) { // All buffers, except for the last one, must have a size that is a multiple of four lastBufferLength = roundToNextMultipleOfFour(lastBufferLength); for (let i = 1; i < buffers.length; i++) { let bufferLength = 0; const data = buffers[i].data; if (data) { // All buffers, except for the last one, must have a size that is a multiple of four if (i === buffers.length - 1) bufferLength = data.byteLength; else bufferLength = roundToNextMultipleOfFour(data.byteLength); } offsets.push(offsets[i - 1] + lastBufferLength); lastBufferLength = bufferLength; } } // Create a new buffer and add the data of all the given buffers, according to the calculated offsets const merged = new Uint8Array(offsets[offsets.length - 1] + lastBufferLength); // Add content data to new buffer buffers.forEach((buffer, i) => { const data = buffer.data ? new Uint8Array(buffer.data) : new Uint8Array(0); merged.set(data, offsets[i]); }); return [merged, offsets]; } }