@shapediver/sdk.sdtf-v1
Version:
Official sdTF SDK for TypeScript
317 lines (264 loc) • 13.2 kB
text/typescript
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];
}
}