diff --git a/.prettierignore b/.prettierignore index 1a64dfa16..9cbcfde0a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,5 @@ +packages/functions/src/** +packages/cli/src/** **/dist **/in **/out diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 38f650226..12cf0ec3c 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -1,6 +1,6 @@ /** @module core */ -export { Document, Transform } from './document'; +export { Document, Transform, TransformContext } from './document'; export { JSONDocument } from './json-document'; export { Extension } from './extension'; export { diff --git a/packages/core/src/document.ts b/packages/core/src/document.ts index 7a49bff4a..279144391 100644 --- a/packages/core/src/document.ts +++ b/packages/core/src/document.ts @@ -22,7 +22,11 @@ import { } from './properties'; import { Logger } from './utils'; -export type Transform = (doc: Document) => void; +export interface TransformContext { + stack: string[]; +} + +export type Transform = (doc: Document, context?: TransformContext) => void; /** * # Document @@ -190,8 +194,9 @@ export class Document { * @param transforms List of synchronous transformation functions to apply. */ public async transform(...transforms: Transform[]): Promise { + const stack = transforms.map((fn) => fn.name); for (const transform of transforms) { - await transform(this); + await transform(this, { stack }); } return this; } diff --git a/packages/functions/src/center.ts b/packages/functions/src/center.ts index 24ace76f8..8cf1fd299 100644 --- a/packages/functions/src/center.ts +++ b/packages/functions/src/center.ts @@ -1,5 +1,6 @@ import { Document, Transform, vec3 } from '@gltf-transform/core'; import { bounds } from '@gltf-transform/core'; +import { createTransform } from './utils'; const NAME = 'center'; @@ -24,7 +25,7 @@ const CENTER_DEFAULTS: Required = {pivot: 'center'}; export function center (_options: CenterOptions = CENTER_DEFAULTS): Transform { const options = {...CENTER_DEFAULTS, ..._options} as Required; - return (doc: Document): void => { + return createTransform(NAME, (doc: Document): void => { const logger = doc.getLogger(); const root = doc.getRoot(); const isAnimated = root.listAnimations().length > 0 || root.listSkins().length > 0; @@ -69,6 +70,6 @@ export function center (_options: CenterOptions = CENTER_DEFAULTS): Transform { }); logger.debug(`${NAME}: Complete.`); - }; + }); -} +}; diff --git a/packages/functions/src/colorspace.ts b/packages/functions/src/colorspace.ts index cc1c9d15f..69df1fa1e 100644 --- a/packages/functions/src/colorspace.ts +++ b/packages/functions/src/colorspace.ts @@ -1,4 +1,5 @@ import { Accessor, Document, Primitive, Transform, vec3 } from '@gltf-transform/core'; +import { createTransform } from './utils'; const NAME = 'colorspace'; @@ -15,7 +16,7 @@ export interface ColorspaceOptions { */ export function colorspace (options: ColorspaceOptions): Transform { - return (doc: Document): void => { + return createTransform(NAME, (doc: Document): void => { const logger = doc.getLogger(); @@ -67,6 +68,6 @@ export function colorspace (options: ColorspaceOptions): Transform { logger.debug(`${NAME}: Complete.`); - }; + }); -} +}; diff --git a/packages/functions/src/dedup.ts b/packages/functions/src/dedup.ts index 19cdbe841..bbbb6ae99 100755 --- a/packages/functions/src/dedup.ts +++ b/packages/functions/src/dedup.ts @@ -1,4 +1,5 @@ import { Accessor, BufferUtils, Document, Logger, Material, Mesh, Primitive, PrimitiveTarget, PropertyType, Root, Texture, Transform } from '@gltf-transform/core'; +import { createTransform } from './utils'; const NAME = 'dedup'; @@ -42,7 +43,7 @@ export const dedup = function (_options: DedupOptions = DEDUP_DEFAULTS): Transfo } } - return (doc: Document): void => { + return createTransform(NAME, (doc: Document): void => { const logger = doc.getLogger(); if (propertyTypes.has(PropertyType.ACCESSOR)) dedupAccessors(logger, doc); @@ -51,7 +52,7 @@ export const dedup = function (_options: DedupOptions = DEDUP_DEFAULTS): Transfo if (propertyTypes.has(PropertyType.MATERIAL)) dedupMaterials(logger, doc); logger.debug(`${NAME}: Complete.`); - }; + }); }; diff --git a/packages/functions/src/instance.ts b/packages/functions/src/instance.ts index c76f981f2..ca83f6eb0 100644 --- a/packages/functions/src/instance.ts +++ b/packages/functions/src/instance.ts @@ -1,5 +1,6 @@ import { Document, Logger, MathUtils, Mesh, Node, Transform, vec3, vec4 } from '@gltf-transform/core'; import { InstancedMesh, MeshGPUInstancing } from '@gltf-transform/extensions'; +import { createTransform } from './utils'; const NAME = 'instance'; @@ -16,7 +17,7 @@ export function instance (_options: InstanceOptions = INSTANCE_DEFAULTS): Transf // eslint-disable-next-line @typescript-eslint/no-unused-vars const options = {...INSTANCE_DEFAULTS, ..._options} as Required; - return (doc: Document): void => { + return createTransform(NAME, (doc: Document): void => { const logger = doc.getLogger(); const root = doc.getRoot(); const batchExtension = doc.createExtension(MeshGPUInstancing); @@ -97,7 +98,7 @@ export function instance (_options: InstanceOptions = INSTANCE_DEFAULTS): Transf } logger.debug(`${NAME}: Complete.`); - }; + }); } diff --git a/packages/functions/src/metal-rough.ts b/packages/functions/src/metal-rough.ts index ce2795ff2..ca586623b 100644 --- a/packages/functions/src/metal-rough.ts +++ b/packages/functions/src/metal-rough.ts @@ -1,6 +1,6 @@ import { Document, Texture, Transform } from '@gltf-transform/core'; import { MaterialsIOR, MaterialsPBRSpecularGlossiness, MaterialsSpecular, PBRSpecularGlossiness } from '@gltf-transform/extensions'; -import { rewriteTexture } from './utils'; +import { createTransform, rewriteTexture } from './utils'; const NAME = 'metalRough'; @@ -21,7 +21,7 @@ export function metalRough (_options: MetalRoughOptions = METALROUGH_DEFAULTS): // eslint-disable-next-line @typescript-eslint/no-unused-vars const options = {...METALROUGH_DEFAULTS, ..._options} as Required; - return async (doc: Document): Promise => { + return createTransform(NAME, async (doc: Document): Promise => { const logger = doc.getLogger(); @@ -117,6 +117,6 @@ export function metalRough (_options: MetalRoughOptions = METALROUGH_DEFAULTS): logger.debug(`${NAME}: Complete.`); - }; + }); } diff --git a/packages/functions/src/partition.ts b/packages/functions/src/partition.ts index a6d979878..89c87f20b 100755 --- a/packages/functions/src/partition.ts +++ b/packages/functions/src/partition.ts @@ -1,5 +1,6 @@ import { Document, Logger, PropertyType, Transform } from '@gltf-transform/core'; import { prune } from './prune'; +import { createTransform } from './utils'; const NAME = 'partition'; @@ -32,7 +33,7 @@ const partition = (_options: PartitionOptions = PARTITION_DEFAULTS): Transform = const options = {...PARTITION_DEFAULTS, ..._options} as Required; - return async (doc: Document): Promise => { + return createTransform(NAME, async (doc: Document): Promise => { const logger = doc.getLogger(); if (options.meshes !== false) partitionMeshes(doc, logger, options); @@ -45,7 +46,7 @@ const partition = (_options: PartitionOptions = PARTITION_DEFAULTS): Transform = await doc.transform(prune({propertyTypes: [PropertyType.BUFFER]})); logger.debug(`${NAME}: Complete.`); - }; + }); }; diff --git a/packages/functions/src/prune.ts b/packages/functions/src/prune.ts index 7d747893a..c81d02534 100644 --- a/packages/functions/src/prune.ts +++ b/packages/functions/src/prune.ts @@ -1,4 +1,5 @@ import { AnimationChannel, Document, Graph, Property, PropertyType, Root, Transform } from '@gltf-transform/core'; +import { createTransform } from './utils'; const NAME = 'prune'; @@ -44,7 +45,7 @@ export const prune = function (_options: PruneOptions = PRUNE_DEFAULTS): Transfo const options = {...PRUNE_DEFAULTS, ..._options} as Required; const propertyTypes = options.propertyTypes; - return (doc: Document): void => { + return createTransform(NAME, (doc: Document): void => { const logger = doc.getLogger(); const root = doc.getRoot(); const graph = doc.getGraph(); @@ -139,6 +140,6 @@ export const prune = function (_options: PruneOptions = PRUNE_DEFAULTS): Transfo disposed[prop.propertyType]++; } - }; + }); }; diff --git a/packages/functions/src/quantize.ts b/packages/functions/src/quantize.ts index 93d02f085..89a8010e0 100644 --- a/packages/functions/src/quantize.ts +++ b/packages/functions/src/quantize.ts @@ -21,6 +21,7 @@ import { fromRotationTranslationScale, fromScaling, invert, multiply as multiply import { max, min, scale, transformMat4 } from 'gl-matrix/vec3'; import { MeshQuantization } from '@gltf-transform/extensions'; import { prune } from './prune'; +import { createTransform } from './utils'; const NAME = 'quantize'; @@ -80,7 +81,7 @@ export const QUANTIZE_DEFAULTS: Required = { const quantize = (_options: QuantizeOptions = QUANTIZE_DEFAULTS): Transform => { const options = { ...QUANTIZE_DEFAULTS, ..._options } as Required; - return async (doc: Document): Promise => { + return createTransform(NAME, async (doc: Document): Promise => { const logger = doc.getLogger(); const root = doc.getRoot(); @@ -116,7 +117,7 @@ const quantize = (_options: QuantizeOptions = QUANTIZE_DEFAULTS): Transform => { ); logger.debug(`${NAME}: Complete.`); - }; + }); }; function quantizePrimitive( diff --git a/packages/functions/src/reorder.ts b/packages/functions/src/reorder.ts index e35645081..051fbe24a 100644 --- a/packages/functions/src/reorder.ts +++ b/packages/functions/src/reorder.ts @@ -1,6 +1,6 @@ import { Accessor, Document, GLTF, Primitive, PropertyType, Transform } from '@gltf-transform/core'; import { prune } from './prune'; -import { SetMap } from './utils'; +import { createTransform, SetMap } from './utils'; import type { MeshoptEncoder } from 'meshoptimizer'; const NAME = 'reorder'; @@ -46,7 +46,7 @@ export function reorder (_options: ReorderOptions = REORDER_DEFAULTS): Transform const options = {...REORDER_DEFAULTS, ..._options} as Required; const encoder = options.encoder; - return async (doc: Document): Promise => { + return createTransform(NAME, async (doc: Document): Promise => { const logger = doc.getLogger(); await encoder.ready; @@ -95,7 +95,7 @@ export function reorder (_options: ReorderOptions = REORDER_DEFAULTS): Transform } else { logger.debug(`${NAME}: Complete.`); } - }; + }); } function remapAttribute(attribute: Accessor, remap: Uint32Array, dstCount: number) { diff --git a/packages/functions/src/resample.ts b/packages/functions/src/resample.ts index 6cfd1d451..06f33ac99 100644 --- a/packages/functions/src/resample.ts +++ b/packages/functions/src/resample.ts @@ -1,4 +1,5 @@ -import { Accessor, AnimationSampler, Document, Root, Transform } from '@gltf-transform/core'; +import { Accessor, AnimationSampler, Document, Root, Transform, TransformContext } from '@gltf-transform/core'; +import { createTransform, isTransformPending } from './utils'; const NAME = 'resample'; @@ -17,7 +18,7 @@ export const resample = (_options: ResampleOptions = RESAMPLE_DEFAULTS): Transfo const options = {...RESAMPLE_DEFAULTS, ..._options} as Required; - return (doc: Document): void => { + return createTransform(NAME, (doc: Document, context?: TransformContext): void => { const accessorsVisited = new Set(); const accessorsCountPrev = doc.getRoot().listAccessors().length; const logger = doc.getLogger(); @@ -52,7 +53,7 @@ export const resample = (_options: ResampleOptions = RESAMPLE_DEFAULTS): Transfo if (!used) accessor.dispose(); } - if (doc.getRoot().listAccessors().length > accessorsCountPrev) { + if (doc.getRoot().listAccessors().length > accessorsCountPrev && !isTransformPending(context, NAME, 'dedup')) { logger.warn( `${NAME}: Resampling required copying accessors, some of which may be duplicates.` + ' Consider using "dedup" to consolidate any duplicates.' @@ -64,7 +65,7 @@ export const resample = (_options: ResampleOptions = RESAMPLE_DEFAULTS): Transfo } logger.debug(`${NAME}: Complete.`); - }; + }); }; diff --git a/packages/functions/src/sequence.ts b/packages/functions/src/sequence.ts index bd03b06f1..2a653f344 100644 --- a/packages/functions/src/sequence.ts +++ b/packages/functions/src/sequence.ts @@ -1,4 +1,5 @@ import { Accessor, AnimationChannel, AnimationSampler, Document, Transform } from '@gltf-transform/core'; +import { createTransform } from './utils'; const NAME = 'sequence'; @@ -26,7 +27,7 @@ const SEQUENCE_DEFAULTS: Required = { export function sequence (_options: SequenceOptions = SEQUENCE_DEFAULTS): Transform { const options = {...SEQUENCE_DEFAULTS, ..._options} as Required; - return (doc: Document): void => { + return createTransform(NAME, (doc: Document): void => { const logger = doc.getLogger(); const root = doc.getRoot(); @@ -80,6 +81,6 @@ export function sequence (_options: SequenceOptions = SEQUENCE_DEFAULTS): Transf logger.debug(`${NAME}: Complete.`); - }; + }); } diff --git a/packages/functions/src/tangents.ts b/packages/functions/src/tangents.ts index 3739dad39..9a19123e1 100644 --- a/packages/functions/src/tangents.ts +++ b/packages/functions/src/tangents.ts @@ -1,4 +1,5 @@ import { Accessor, Document, Logger, Primitive, Transform, TypedArray, uuid } from '@gltf-transform/core'; +import { createTransform } from './utils'; const NAME = 'tangents'; @@ -42,7 +43,7 @@ export function tangents (_options: TangentsOptions = TANGENTS_DEFAULTS): Transf const options = {...TANGENTS_DEFAULTS, ..._options} as Required; - return (doc: Document): void => { + return createTransform(NAME, (doc: Document): void => { const logger = doc.getLogger(); const attributeIDs = new Map(); const tangentCache = new Map(); @@ -117,7 +118,7 @@ export function tangents (_options: TangentsOptions = TANGENTS_DEFAULTS): Transf } else { logger.debug(`${NAME}: Complete.`); } - }; + }); } function getNormalTexcoord(prim: Primitive): string { diff --git a/packages/functions/src/texture-resize.ts b/packages/functions/src/texture-resize.ts index 3b8745259..775c63581 100644 --- a/packages/functions/src/texture-resize.ts +++ b/packages/functions/src/texture-resize.ts @@ -2,6 +2,7 @@ import ndarray from 'ndarray'; import { lanczos2, lanczos3 } from 'ndarray-lanczos'; import { getPixels, savePixels } from 'ndarray-pixels'; import { Document, Transform, vec2 } from '@gltf-transform/core'; +import { createTransform } from './utils'; const NAME = 'textureResize'; @@ -40,7 +41,7 @@ export const TEXTURE_RESIZE_DEFAULTS: TextureResizeOptions = { export function textureResize(_options: TextureResizeOptions = TEXTURE_RESIZE_DEFAULTS): Transform { const options = {...TEXTURE_RESIZE_DEFAULTS, ..._options} as Required; - return async (doc: Document): Promise => { + return createTransform(NAME, async (doc: Document): Promise => { const logger = doc.getLogger(); @@ -105,6 +106,6 @@ export function textureResize(_options: TextureResizeOptions = TEXTURE_RESIZE_DE logger.debug(`${NAME}: Complete.`); - }; + }); } diff --git a/packages/functions/src/unweld.ts b/packages/functions/src/unweld.ts index e342c4434..b674d6eec 100644 --- a/packages/functions/src/unweld.ts +++ b/packages/functions/src/unweld.ts @@ -1,4 +1,5 @@ import { Accessor, Document, Logger, Transform, TypedArray } from '@gltf-transform/core'; +import { createTransform } from './utils'; const NAME = 'unweld'; @@ -19,7 +20,7 @@ export function unweld (_options: UnweldOptions = UNWELD_DEFAULTS): Transform { // eslint-disable-next-line @typescript-eslint/no-unused-vars const options = {...UNWELD_DEFAULTS, ..._options} as Required; - return (doc: Document): void => { + return createTransform(NAME, (doc: Document): void => { const logger = doc.getLogger(); const visited = new Map>(); @@ -60,7 +61,7 @@ export function unweld (_options: UnweldOptions = UNWELD_DEFAULTS): Transform { } logger.debug(`${NAME}: Complete.`); - }; + }); } function unweldAttribute( diff --git a/packages/functions/src/utils.ts b/packages/functions/src/utils.ts index 6be76e9e3..b8c09a0c0 100644 --- a/packages/functions/src/utils.ts +++ b/packages/functions/src/utils.ts @@ -1,6 +1,23 @@ import { NdArray } from 'ndarray'; import { getPixels, savePixels } from 'ndarray-pixels'; -import { Primitive, Texture } from '@gltf-transform/core'; +import { Primitive, Texture, Transform, TransformContext } from '@gltf-transform/core'; + +/** + * Prepares a function used in an {@link Document.transform} pipeline. Use of this wrapper is + * optional, and plain functions may be used in transform pipelines just as well. The wrapper is + * used internally so earlier pipeline stages can detect and optimize based on later stages. + */ +export function createTransform(name: string, fn: Transform): Transform { + Object.defineProperty(fn, 'name', { value: name }); + return fn; +} + +export function isTransformPending(context: TransformContext | undefined, initial: string, pending: string): boolean { + if (!context) return false; + const initialIndex = context.stack.lastIndexOf(initial); + const pendingIndex = context.stack.lastIndexOf(pending); + return initialIndex < pendingIndex; +} /** Maps pixels from source to target textures, with a per-pixel callback. */ export async function rewriteTexture( diff --git a/packages/functions/src/weld.ts b/packages/functions/src/weld.ts index 404bd47f3..c2d59ba04 100644 --- a/packages/functions/src/weld.ts +++ b/packages/functions/src/weld.ts @@ -1,5 +1,5 @@ import { Accessor, Document, Primitive, PrimitiveTarget, Transform, TypedArray } from '@gltf-transform/core'; -import { getGLPrimitiveCount } from './utils'; +import { getGLPrimitiveCount, createTransform } from './utils'; const NAME = 'weld'; @@ -17,7 +17,7 @@ const WELD_DEFAULTS: Required = {tolerance: 1e-4}; export function weld (_options: WeldOptions = WELD_DEFAULTS): Transform { const options = {...WELD_DEFAULTS, ..._options} as Required; - return (doc: Document): void => { + return createTransform(NAME, (doc: Document): void => { const logger = doc.getLogger(); for (const mesh of doc.getRoot().listMeshes()) { @@ -31,7 +31,7 @@ export function weld (_options: WeldOptions = WELD_DEFAULTS): Transform { } logger.debug(`${NAME}: Complete.`); - }; + }); } /** In-place weld, adds indices without changing number of vertices. */ diff --git a/packages/functions/test/utils.test.ts b/packages/functions/test/utils.test.ts index 529b96028..f5f888682 100644 --- a/packages/functions/test/utils.test.ts +++ b/packages/functions/test/utils.test.ts @@ -1,8 +1,8 @@ require('source-map-support').install(); import test from 'tape'; -import { Accessor, Document, GLTF, Primitive } from '@gltf-transform/core'; -import { getGLPrimitiveCount } from '../src/utils'; +import { Accessor, Document, GLTF, Primitive, Transform, TransformContext } from '@gltf-transform/core'; +import { getGLPrimitiveCount, createTransform, isTransformPending } from '../src/utils'; test('@gltf-transform/functions::utils | getGLPrimitiveCount', async (t) => { const doc = new Document(); @@ -40,3 +40,24 @@ test('@gltf-transform/functions::utils | getGLPrimitiveCount', async (t) => { t.end(); }); + +test('@gltf-transform/functions::utils | transform pipeline', async (t) => { + const doc = new Document(); + const first = createTransform('first', (_: Document, context?: TransformContext) => { + if (!isTransformPending(context, 'first', 'second')) { + throw new Error('Out of order!'); + } + }); + const second: Transform = (_: Document) => undefined; + + t.ok(doc.transform(first, second), '[a, b] OK'); + + try { + await doc.transform(second, first); + t.fail('[b, a] NOT OK'); + } catch (e) { + t.match((e as Error).message, /out of order/i, '[b, a] NOT OK'); + } + + t.end(); +});