From 9a443bf6c8304362b5b4c4a02e4124b98545f429 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 19 Apr 2024 17:56:45 +0200 Subject: [PATCH 01/37] Try to upgrade CMPT to GLB when target version is 1.1 --- src/tools/contentProcessing/GltfTransform.ts | 58 +++++++++++++ src/tools/migration/TileFormatsMigration.ts | 21 +++++ .../migration/TileFormatsMigrationCmpt.ts | 83 +++++++++++++++++++ .../tilesetProcessing/TilesetUpgrader.ts | 61 +++++++++++++- .../upgrade/TilesetUpgradeOptions.ts | 3 + 5 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 src/tools/migration/TileFormatsMigrationCmpt.ts diff --git a/src/tools/contentProcessing/GltfTransform.ts b/src/tools/contentProcessing/GltfTransform.ts index 1487df3a..1dbc2e44 100644 --- a/src/tools/contentProcessing/GltfTransform.ts +++ b/src/tools/contentProcessing/GltfTransform.ts @@ -3,9 +3,14 @@ import draco3d from "draco3d"; import { MeshoptDecoder } from "meshoptimizer"; import { MeshoptEncoder } from "meshoptimizer"; +import { Document } from "@gltf-transform/core"; +import { Logger } from "@gltf-transform/core"; import { Transform } from "@gltf-transform/core"; import { NodeIO } from "@gltf-transform/core"; +import { prune } from "@gltf-transform/functions"; +import { unpartition } from "@gltf-transform/functions"; + import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; import { EXTStructuralMetadata } from "../../gltf-extensions"; @@ -79,4 +84,57 @@ export class GltfTransform { const outputGlb = await io.writeBinary(document); return Buffer.from(outputGlb); } + + /** + * Combine all scenes in the given document into one. + * + * This will take the first scene, declare it as the default scene, + * attach the nodes from all other scenes to this one, and dispose + * the other scenes. + * + * @param document - The glTF-Transform document + */ + private static async combineScenes(document: Document) { + const root = document.getRoot(); + const scenes = root.listScenes(); + if (scenes.length > 0) { + const combinedScene = scenes[0]; + root.setDefaultScene(combinedScene); + for (let s = 1; s < scenes.length; s++) { + const otherScene = scenes[s]; + const children = otherScene.listChildren(); + for (const child of children) { + combinedScene.addChild(child); + otherScene.removeChild(child); + } + otherScene.dispose(); + } + } + document.setLogger(new Logger(Logger.Verbosity.WARN)); + await document.transform(prune()); + } + + /** + * Creates a single glTF Transform document from the given GLB buffers. + * + * This will create a document with a single scene that contains all + * nodes that have been children of any scene in the given input + * GLBs. + * + * @param inputGlbBuffers - The buffers containing GLB data + * @returns The merged document + */ + static async merge(inputGlbBuffers: Buffer[]): Promise { + // Create one document from each buffer and merge them + const io = await GltfTransform.getIO(); + const mergedDocument = new Document(); + for (const inputGlbBuffer of inputGlbBuffers) { + const inputDocument = await io.readBinary(inputGlbBuffer); + mergedDocument.merge(inputDocument); + } + // Combine all scenes into one + await GltfTransform.combineScenes(mergedDocument); + await mergedDocument.transform(unpartition()); + return mergedDocument; + } } diff --git a/src/tools/migration/TileFormatsMigration.ts b/src/tools/migration/TileFormatsMigration.ts index 27664870..58080139 100644 --- a/src/tools/migration/TileFormatsMigration.ts +++ b/src/tools/migration/TileFormatsMigration.ts @@ -3,6 +3,7 @@ import { Document } from "@gltf-transform/core"; import { TileFormatsMigrationPnts } from "./TileFormatsMigrationPnts"; import { TileFormatsMigrationB3dm } from "./TileFormatsMigrationB3dm"; import { TileFormatsMigrationI3dm } from "./TileFormatsMigrationI3dm"; +import { TileFormatsMigrationCmpt } from "./TileFormatsMigrationCmpt"; /** * Methods for converting "legacy" tile formats into glTF assets @@ -57,6 +58,26 @@ export class TileFormatsMigration { ); } + /** + * Convert the given CMPT data into a single glTF asset + * + * @param cmptBuffer - The CMPT buffer + * @param externalGlbResolver - A function that will be used to resolve + * external GLB data if the CMPT contains I3DM that use + * `header.gltfFormat=0` (meaning that the payload is not GLB data, + * but only a GLB URI). + * @returns The GLB buffer + */ + static async convertCmptToGlb( + cmptBuffer: Buffer, + externalGlbResolver: (uri: string) => Promise + ): Promise { + return await TileFormatsMigrationCmpt.convertCmptToGlb( + cmptBuffer, + externalGlbResolver + ); + } + /** * Apply the given RTC_CENTER to the given glTF-Transform document, * by inserting a new root node that carries the given RTC_CENTER diff --git a/src/tools/migration/TileFormatsMigrationCmpt.ts b/src/tools/migration/TileFormatsMigrationCmpt.ts new file mode 100644 index 00000000..2b6fc726 --- /dev/null +++ b/src/tools/migration/TileFormatsMigrationCmpt.ts @@ -0,0 +1,83 @@ +import { ContentDataTypeRegistry } from "../../base"; +import { ContentDataTypes } from "../../base"; + +import { TileFormats } from "../../tilesets"; + +import { GltfTransform } from "../contentProcessing/GltfTransform"; + +import { TileFormatsMigration } from "./TileFormatsMigration"; + +import { Loggers } from "../../base"; +const logger = Loggers.get("migration"); + +/** + * Methods for converting CMPT tile data into GLB + * + * @internal + */ +export class TileFormatsMigrationCmpt { + /** + * Convert the given CMPT data into a glTF asset + * + * @param cmptBuffer - The CMPT buffer + * @param externalGlbResolver - A function that will be used to resolve + * external GLB data if the CMPT contains I3DM that use + * `header.gltfFormat=0` (meaning that the payload is not GLB data, + * but only a GLB URI). + * @returns The GLB buffer + * @throws TileFormatError If the CMPT contained I3DM with an external + * GLB URI * that could not resolved by the given resolver + */ + static async convertCmptToGlb( + cmptBuffer: Buffer, + externalGlbResolver: (uri: string) => Promise + ): Promise { + const compositeTileData = TileFormats.readCompositeTileData(cmptBuffer); + const innerTileBuffers = compositeTileData.innerTileBuffers; + + const innerTileGlbs: Buffer[] = []; + for (const innerTileBuffer of innerTileBuffers) { + const innerTileType = await ContentDataTypeRegistry.findType( + "", + innerTileBuffer + ); + if (innerTileType === ContentDataTypes.CONTENT_TYPE_PNTS) { + logger.trace("Converting inner PNTS tile to GLB..."); + const innerTileGlb = await TileFormatsMigration.convertPntsToGlb( + innerTileBuffer + ); + innerTileGlbs.push(innerTileGlb); + } else if (innerTileType === ContentDataTypes.CONTENT_TYPE_B3DM) { + logger.trace("Converting inner B3DM tile to GLB..."); + const innerTileGlb = await TileFormatsMigration.convertB3dmToGlb( + innerTileBuffer + ); + innerTileGlbs.push(innerTileGlb); + } else if (innerTileType === ContentDataTypes.CONTENT_TYPE_I3DM) { + logger.trace("Converting inner I3DM tile to GLB..."); + const innerTileGlb = await TileFormatsMigration.convertI3dmToGlb( + innerTileBuffer, + externalGlbResolver + ); + innerTileGlbs.push(innerTileGlb); + } else if (innerTileType === ContentDataTypes.CONTENT_TYPE_CMPT) { + logger.trace("Converting inner CMPT tile to GLB..."); + const innerTileGlb = await TileFormatsMigration.convertCmptToGlb( + innerTileBuffer, + externalGlbResolver + ); + innerTileGlbs.push(innerTileGlb); + } else { + logger.warn( + "Unknown type for inner tile of CMPT: " + + innerTileType + + " - ignoring" + ); + } + } + const document = await GltfTransform.merge(innerTileGlbs); + const io = await GltfTransform.getIO(); + const glb = await io.writeBinary(document); + return Buffer.from(glb); + } +} diff --git a/src/tools/tilesetProcessing/TilesetUpgrader.ts b/src/tools/tilesetProcessing/TilesetUpgrader.ts index 2d435aa3..ffe564eb 100644 --- a/src/tools/tilesetProcessing/TilesetUpgrader.ts +++ b/src/tools/tilesetProcessing/TilesetUpgrader.ts @@ -83,6 +83,7 @@ export class TilesetUpgrader { upgradePntsToGlb: false, upgradeB3dmToGlb: false, upgradeI3dmToGlb: false, + upgradeCmptToGlb: false, }; return options; } @@ -103,6 +104,7 @@ export class TilesetUpgrader { upgradePntsToGlb: true, upgradeB3dmToGlb: true, upgradeI3dmToGlb: true, + upgradeCmptToGlb: true, }; return options; } @@ -216,6 +218,11 @@ export class TilesetUpgrader { return Paths.replaceExtension(uri, ".glb"); } } + if (this.upgradeOptions.upgradeCmptToGlb) { + if (Paths.hasExtension(uri, ".cmpt")) { + return Paths.replaceExtension(uri, ".glb"); + } + } return uri; }; @@ -263,11 +270,12 @@ export class TilesetUpgrader { ): Promise => { if (type === ContentDataTypes.CONTENT_TYPE_PNTS) { return this.processEntryPnts(sourceEntry); - } - if (type === ContentDataTypes.CONTENT_TYPE_B3DM) { + } else if (type === ContentDataTypes.CONTENT_TYPE_B3DM) { return this.processEntryB3dm(sourceEntry); } else if (type === ContentDataTypes.CONTENT_TYPE_I3DM) { return this.processEntryI3dm(sourceEntry); + } else if (type === ContentDataTypes.CONTENT_TYPE_CMPT) { + return this.processEntryCmpt(sourceEntry); } else if (type == ContentDataTypes.CONTENT_TYPE_TILESET) { return this.processEntryTileset(sourceEntry); } @@ -394,6 +402,55 @@ export class TilesetUpgrader { return targetEntry; }; + /** + * Process the given tileset (content) entry that contains CMPT, + * and return the result. + * + * @param sourceEntry - The source entry + * @returns The processed entry + */ + private processEntryCmpt = async ( + sourceEntry: TilesetEntry + ): Promise => { + const sourceKey = sourceEntry.key; + const sourceValue = sourceEntry.value; + let targetKey = sourceKey; + let targetValue = sourceValue; + if (this.upgradeOptions.upgradeCmptToGlb) { + logger.debug(` Upgrading CMPT to GLB for ${sourceKey}`); + + targetKey = this.processContentUri(sourceKey); + + // Define the resolver for external GLB files in CMPT files: + // It will look up the entry using the 'tilesetProcessor' + const externalGlbResolver = async ( + uri: string + ): Promise => { + if (!this.tilesetProcessor) { + return undefined; + } + const externalGlbEntry = await this.tilesetProcessor.fetchSourceEntry( + uri + ); + if (!externalGlbEntry) { + return undefined; + } + return externalGlbEntry.value; + }; + targetValue = await TileFormatsMigration.convertCmptToGlb( + sourceValue, + externalGlbResolver + ); + } else { + logger.debug(` Not upgrading ${sourceKey} (disabled via option)`); + } + const targetEntry = { + key: targetKey, + value: targetValue, + }; + return targetEntry; + }; + /** * Process the given tileset (content) entry that contains the * JSON of an external tileset, and return the result. diff --git a/src/tools/tilesetProcessing/upgrade/TilesetUpgradeOptions.ts b/src/tools/tilesetProcessing/upgrade/TilesetUpgradeOptions.ts index 48a09632..4bad5315 100644 --- a/src/tools/tilesetProcessing/upgrade/TilesetUpgradeOptions.ts +++ b/src/tools/tilesetProcessing/upgrade/TilesetUpgradeOptions.ts @@ -47,4 +47,7 @@ export type TilesetUpgradeOptions = { // Whether attempts should be made to convert I3DM files to GLB // with metadata upgradeI3dmToGlb: boolean; + + // Whether attempts should be made to convert CMPT files to GLB + upgradeCmptToGlb: boolean; }; From 200ac7df87c1bb8a7a1b7c5f593b7420e0aa37a4 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 20 Apr 2024 17:35:45 +0200 Subject: [PATCH 02/37] Add spec files for CMPT upgrade --- .../Composite/Composite/composite.gltf | 299 ++++++++++++++++ .../compositeOfComposite.gltf | 298 ++++++++++++++++ .../compositeOfInstanced.gltf | 324 ++++++++++++++++++ .../input/Composite/Composite/composite.cmpt | Bin 0 -> 13472 bytes .../input/Composite/Composite/tileset.json | 41 +++ .../compositeOfComposite.cmpt | Bin 0 -> 13488 bytes .../CompositeOfComposite/tileset.json | 41 +++ .../Composite/CompositeOfInstanced/box.glb | Bin 0 -> 3284 bytes .../compositeOfInstanced.cmpt | Bin 0 -> 1024 bytes .../CompositeOfInstanced/tileset.json | 29 ++ .../migration/TileFormatsMigrationSpec.ts | 39 +++ 11 files changed, 1071 insertions(+) create mode 100644 specs/data/migration/golden_gltf/Composite/Composite/composite.gltf create mode 100644 specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf create mode 100644 specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf create mode 100644 specs/data/migration/input/Composite/Composite/composite.cmpt create mode 100644 specs/data/migration/input/Composite/Composite/tileset.json create mode 100644 specs/data/migration/input/Composite/CompositeOfComposite/compositeOfComposite.cmpt create mode 100644 specs/data/migration/input/Composite/CompositeOfComposite/tileset.json create mode 100644 specs/data/migration/input/Composite/CompositeOfInstanced/box.glb create mode 100644 specs/data/migration/input/Composite/CompositeOfInstanced/compositeOfInstanced.cmpt create mode 100644 specs/data/migration/input/Composite/CompositeOfInstanced/tileset.json diff --git a/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf b/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf new file mode 100644 index 00000000..fbe489c3 --- /dev/null +++ b/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf @@ -0,0 +1,299 @@ +{ + "asset": { + "generator": "glTF-Transform v3.10.0", + "version": "2.0" + }, + "accessors": [ + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 0 + }, + { + "type": "VEC4", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 300 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 700 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 25, + "bufferView": 0, + "byteOffset": 1000 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 240, + "max": [ + 108.69153594970703, + 74.8784408569336, + 66.49319458007812 + ], + "min": [ + -90.89249420166016, + -86.16458892822266, + -78.88909149169922 + ], + "bufferView": 1, + "byteOffset": 0 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 240, + "bufferView": 1, + "byteOffset": 12 + }, + { + "type": "SCALAR", + "componentType": 5126, + "count": 240, + "bufferView": 1, + "byteOffset": 24 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "max": [ + 10.000004768371582, + 10, + 10.000005722045898 + ], + "min": [ + -10.000003814697266, + -10, + -10.000003814697266 + ], + "bufferView": 2, + "byteOffset": 0 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "bufferView": 2, + "byteOffset": 12 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 360, + "bufferView": 3, + "byteOffset": 0 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 36, + "bufferView": 3, + "byteOffset": 720 + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 1052 + }, + { + "buffer": 0, + "byteOffset": 1052, + "byteLength": 6720, + "byteStride": 28, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 7772, + "byteLength": 576, + "byteStride": 24, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 8348, + "byteLength": 792, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 9140, + "byteLength": 25 + } + ], + "buffers": [ + { + "name": "buffer", + "byteLength": 9165 + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "metallicFactor": 0 + } + }, + { + "pbrMetallicRoughness": { + "metallicFactor": 0 + } + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "POSITION": 4, + "NORMAL": 5, + "_FEATURE_ID_0": 6 + }, + "mode": 4, + "material": 0, + "indices": 9, + "extensions": { + "EXT_mesh_features": { + "featureIds": [ + { + "featureCount": 10, + "attribute": 0, + "propertyTable": -1 + } + ] + } + } + } + ] + }, + { + "primitives": [ + { + "attributes": { + "POSITION": 7, + "NORMAL": 8 + }, + "mode": 4, + "material": 1, + "indices": 10 + } + ] + } + ], + "nodes": [ + { + "rotation": [ + -0.7071067811865475, + 0, + 0, + 0.7071067811865476 + ], + "mesh": 0 + }, + { + "translation": [ + 1215012.8988049095, + 4081604.3368623317, + 4736313.0423059845 + ], + "children": [ + 0 + ] + }, + { + "mesh": 1, + "extensions": { + "EXT_mesh_gpu_instancing": { + "attributes": { + "TRANSLATION": 0, + "ROTATION": 1, + "SCALE": 2, + "_FEATURE_ID_0": 3 + } + }, + "EXT_instance_features": { + "featureIds": [ + { + "featureCount": 25, + "attribute": 0, + "propertyTable": 0 + } + ] + } + } + }, + { + "translation": [ + 1215013.84, + 4081608.45, + 4736316.76 + ], + "children": [ + 2 + ] + } + ], + "scenes": [ + { + "nodes": [ + 1, + 3 + ] + } + ], + "scene": 0, + "extensionsUsed": [ + "EXT_structural_metadata", + "EXT_mesh_features", + "EXT_mesh_gpu_instancing", + "EXT_instance_features" + ], + "extensionsRequired": [ + "EXT_mesh_gpu_instancing" + ], + "extensions": { + "EXT_structural_metadata": { + "schema": { + "id": "ID_batch_table", + "name": "Generated from batch_table", + "classes": { + "class_batch_table": { + "name": "Generated from batch_table", + "properties": { + "Height": { + "name": "Height", + "description": "Generated from Height", + "type": "SCALAR", + "componentType": "UINT8", + "required": true + } + } + } + } + }, + "propertyTables": [ + { + "class": "class_batch_table", + "count": 25, + "properties": { + "Height": { + "values": 4 + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf b/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf new file mode 100644 index 00000000..116279cc --- /dev/null +++ b/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf @@ -0,0 +1,298 @@ +{ + "asset": { + "generator": "glTF-Transform v3.10.0", + "version": "2.0" + }, + "accessors": [ + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 0 + }, + { + "type": "VEC4", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 300 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 700 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 25, + "bufferView": 0, + "byteOffset": 1000 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 240, + "max": [ + 108.69153594970703, + 74.8784408569336, + 66.49319458007812 + ], + "min": [ + -90.89249420166016, + -86.16458892822266, + -78.88909149169922 + ], + "bufferView": 1, + "byteOffset": 0 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 240, + "bufferView": 1, + "byteOffset": 12 + }, + { + "type": "SCALAR", + "componentType": 5126, + "count": 240, + "bufferView": 1, + "byteOffset": 24 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "max": [ + 10.000004768371582, + 10, + 10.000005722045898 + ], + "min": [ + -10.000003814697266, + -10, + -10.000003814697266 + ], + "bufferView": 2, + "byteOffset": 0 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "bufferView": 2, + "byteOffset": 12 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 360, + "bufferView": 3, + "byteOffset": 0 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 36, + "bufferView": 3, + "byteOffset": 720 + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 1052 + }, + { + "buffer": 0, + "byteOffset": 1052, + "byteLength": 6720, + "byteStride": 28, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 7772, + "byteLength": 576, + "byteStride": 24, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 8348, + "byteLength": 792, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 9140, + "byteLength": 25 + } + ], + "buffers": [ + { + "name": "buffer", + "byteLength": 9165 + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "metallicFactor": 0 + } + }, + { + "pbrMetallicRoughness": { + "metallicFactor": 0 + } + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "POSITION": 4, + "NORMAL": 5, + "_FEATURE_ID_0": 6 + }, + "mode": 4, + "material": 0, + "indices": 9, + "extensions": { + "EXT_mesh_features": { + "featureIds": [ + { + "featureCount": 10, + "attribute": 0 + } + ] + } + } + } + ] + }, + { + "primitives": [ + { + "attributes": { + "POSITION": 7, + "NORMAL": 8 + }, + "mode": 4, + "material": 1, + "indices": 10 + } + ] + } + ], + "nodes": [ + { + "rotation": [ + -0.7071067811865475, + 0, + 0, + 0.7071067811865476 + ], + "mesh": 0 + }, + { + "translation": [ + 1215012.8988049095, + 4081604.3368623317, + 4736313.0423059845 + ], + "children": [ + 0 + ] + }, + { + "mesh": 1, + "extensions": { + "EXT_mesh_gpu_instancing": { + "attributes": { + "TRANSLATION": 0, + "ROTATION": 1, + "SCALE": 2, + "_FEATURE_ID_0": 3 + } + }, + "EXT_instance_features": { + "featureIds": [ + { + "featureCount": 25, + "attribute": 0, + "propertyTable": 0 + } + ] + } + } + }, + { + "translation": [ + 1215013.84, + 4081608.45, + 4736316.76 + ], + "children": [ + 2 + ] + } + ], + "scenes": [ + { + "nodes": [ + 1, + 3 + ] + } + ], + "scene": 0, + "extensionsUsed": [ + "EXT_mesh_gpu_instancing", + "EXT_structural_metadata", + "EXT_mesh_features", + "EXT_instance_features" + ], + "extensionsRequired": [ + "EXT_mesh_gpu_instancing" + ], + "extensions": { + "EXT_structural_metadata": { + "schema": { + "id": "ID_batch_table", + "name": "Generated from batch_table", + "classes": { + "class_batch_table": { + "name": "Generated from batch_table", + "properties": { + "Height": { + "name": "Height", + "description": "Generated from Height", + "type": "SCALAR", + "componentType": "UINT8", + "required": true + } + } + } + } + }, + "propertyTables": [ + { + "class": "class_batch_table", + "count": 25, + "properties": { + "Height": { + "values": 4 + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf b/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf new file mode 100644 index 00000000..92c46ec5 --- /dev/null +++ b/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf @@ -0,0 +1,324 @@ +{ + "asset": { + "generator": "glTF-Transform v3.10.0", + "version": "2.0" + }, + "accessors": [ + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 0 + }, + { + "type": "VEC4", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 300 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 700 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 25, + "bufferView": 0, + "byteOffset": 1000 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 1, + "byteOffset": 0 + }, + { + "type": "VEC4", + "componentType": 5126, + "count": 25, + "bufferView": 1, + "byteOffset": 300 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 1, + "byteOffset": 700 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 25, + "bufferView": 1, + "byteOffset": 1000 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 36, + "bufferView": 2, + "byteOffset": 0 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 36, + "bufferView": 2, + "byteOffset": 72 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "max": [ + 10.000004768371582, + 10, + 10.000005722045898 + ], + "min": [ + -10.000003814697266, + -10, + -10.000003814697266 + ], + "bufferView": 3, + "byteOffset": 0 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "bufferView": 3, + "byteOffset": 12 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "max": [ + 10.000004768371582, + 10, + 10.000005722045898 + ], + "min": [ + -10.000003814697266, + -10, + -10.000003814697266 + ], + "bufferView": 4, + "byteOffset": 0 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "bufferView": 4, + "byteOffset": 12 + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 1052 + }, + { + "buffer": 0, + "byteOffset": 1052, + "byteLength": 1052 + }, + { + "buffer": 0, + "byteOffset": 2104, + "byteLength": 144, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 2248, + "byteLength": 576, + "byteStride": 24, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 2824, + "byteLength": 576, + "byteStride": 24, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 3400, + "byteLength": 25 + } + ], + "buffers": [ + { + "byteLength": 3425 + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "metallicFactor": 0 + } + }, + { + "pbrMetallicRoughness": { + "metallicFactor": 0 + } + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "POSITION": 10, + "NORMAL": 11 + }, + "mode": 4, + "material": 0, + "indices": 8 + } + ] + }, + { + "primitives": [ + { + "attributes": { + "POSITION": 12, + "NORMAL": 13 + }, + "mode": 4, + "material": 1, + "indices": 9 + } + ] + } + ], + "nodes": [ + { + "mesh": 0, + "extensions": { + "EXT_mesh_gpu_instancing": { + "attributes": { + "TRANSLATION": 0, + "ROTATION": 1, + "SCALE": 2, + "_FEATURE_ID_0": 3 + } + }, + "EXT_instance_features": { + "featureIds": [ + { + "featureCount": 25, + "attribute": 0, + "propertyTable": -1 + } + ] + } + } + }, + { + "translation": [ + 1215013.84, + 4081608.45, + 4736316.76 + ], + "children": [ + 0 + ] + }, + { + "mesh": 1, + "extensions": { + "EXT_mesh_gpu_instancing": { + "attributes": { + "TRANSLATION": 4, + "ROTATION": 5, + "SCALE": 6, + "_FEATURE_ID_0": 7 + } + }, + "EXT_instance_features": { + "featureIds": [ + { + "featureCount": 25, + "attribute": 0, + "propertyTable": 0 + } + ] + } + } + }, + { + "translation": [ + 1215013.84, + 4081608.45, + 4736316.76 + ], + "children": [ + 2 + ] + } + ], + "scenes": [ + { + "nodes": [ + 1, + 3 + ] + } + ], + "scene": 0, + "extensionsUsed": [ + "EXT_mesh_gpu_instancing", + "EXT_structural_metadata", + "EXT_instance_features" + ], + "extensionsRequired": [ + "EXT_mesh_gpu_instancing" + ], + "extensions": { + "EXT_structural_metadata": { + "schema": { + "id": "ID_batch_table", + "name": "Generated from batch_table", + "classes": { + "class_batch_table": { + "name": "Generated from batch_table", + "properties": { + "Height": { + "name": "Height", + "description": "Generated from Height", + "type": "SCALAR", + "componentType": "UINT8", + "required": true + } + } + } + } + }, + "propertyTables": [ + { + "class": "class_batch_table", + "count": 25, + "properties": { + "Height": { + "values": 5 + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/specs/data/migration/input/Composite/Composite/composite.cmpt b/specs/data/migration/input/Composite/Composite/composite.cmpt new file mode 100644 index 0000000000000000000000000000000000000000..70d6d1a5b3d26ff4b42cda257a71701cd7c836f4 GIT binary patch literal 13472 zcmeHNdwdghwm%@cTG0!x@4HHFWVOgN&MT8i5!y^C(o$M%i!Z=#+NNzVO=^-BO07IZ zL_kC=polEUT}5Hn)kW}erOEIR#r6GI6;?#=Rl#LcObGRn;C6WRI#U zs8$tI^e9Sni;BRztszv#yLmgn!av zco$xXii9#`yy8@kSK_j8^JL5ow_9;z%q7{lDsc?ATJe|*!o)Ey26H{fp*Rl1aR@rE zN&L}kD$DZ~`ASn;#HX}*y97?^#UyBBWdUBmsVwMr5>diwa7urSC02{GHV_U({qacD zneCLSv*N)}AeI&L*Ebk_`XuLU3PfYU2xgU2bn(tsl*aPa%-yiR9+T1Sop zjTqA_6YnZU)Vr4_pFo{Q?8K(95y~bRk*oK|1JR&AWX7$bGCDpG_lH8ks?tbfZCx1e zAjo+CyC?mi*1NFgJEZ7vVo$;#_-z>H21F^b*B{dqX5602V{OFJ4 zo>ex+jgKwP2_)NeR1^$X2k|b6Tc3^t^QdPk zX*61ju%TuUEV%6muuPBZr~SQaOPgHeOit~$;&;q&XxG-AGU8b@Q$Tr ziJIpRFlQ4N&;+N?>5*ITCR@64&xA95Vd+4pUf;ajpoIm#!MdRd}acRsi{ zad}N0!$;Ki#BVoE&|jD{N3VGE!uG^f;}~8ReWU$?i%00+YPaf*BlabJ^2=O?yR)JE zbY99&d8h`enc7edl%M)ieyWXXp!}S>O`-g2+@ z#ba9-?#_nt(|IXB<)Ip=W@%l6?BuoY)S%{gqA)S| zfS2KnGgl?_rRCb(qjR;-=hh_8+Fi)-$LIVadHNlrwM!OWuN}Vp-o%TA#SH&#&yGa> z*L4~ah*Jn#V5EKHa!lpS*Yl!`<0XemXDZr#w^x)l6-u2Fg!;DL>UlHBkPU@4lps zzWZQu|B#qIxbg$7?5+coAjsk$?5qM^~GDZYej#4p5dXbJG5bc-& zjQ2J5+f5AbJovWen>;ruI|I60p=;vJ%Ng#@hVs*SDL>_*8mMM!Lp4x->Pz{lHmZU0 zf3x!@J->XT?jN{VKl%u#OTkAOmUc<{mn{$IU+rtwhkxbQkJPMYxa{0&{S|Sg?s@59 z{ieUT^=GeXV|eL#BlXBl^Yo$(i}VHFTlCraix}?ChVs*SDL>_*8mMM!Lp4x->Pz{l zHmZU0KlODexjE+yJz2LY`P}BUiBFopPZ~J+(cZ+~J=>DEzjj0N;ydn4*6x3w;obM$ zoeU4&m0Wn_o8+AH4<$Z3a}&cmE;%o`b#FA8ad>0$tcM;*2IKcI+?@^Or}I*N%0o3! z&D4f!p#0RA@>6Y81LZHi@hLqNe$qGp3%~Z+`bYKhn&%n5N?5PwjC;%X=CTHDq1Qk8jcFVl8LMAaMm@=y&_Gqs@_C_nY3{8Ss&K>2fC)kZCUZ)E$?r-$bB5^qKO9JFso`yTP^zfa5g z-{|e5jQ>aMC2mLit(JX{c+u}J+j`&pAvyBVyqsR*QE1hHrN7rFcLn1%d(d8|SHzC04h-Tvm;P`HoCc@D0Qd>^1K@Nx12VA9fHUE2I12{CzhOTR&Vj)& z2!4wFAUGG!gY&UHA1;6k;Uc&g+l%4f;ZnE+eg;FZ{~0*oKfsBt6E1_x;pZ?EuE2gM zTnWP<6NY1-2^?gB3tJal1w05KVk?3K3b;WA5B4&s;Du~#vta~`f{}1F17o283b8GOaZn87p$JN_FMVExY&xEb4)}dtnLO2TQSC3ircuSO#sd0{b?203L*u*sg?!U^P4ptKgT|uYyNl zEv$ic*sp>AgkQm<*ggu6!FqTcp1}4A*Z{wQU&BUt68nwtTX+hd#`bCW9c+eYU=wV? zeiJ+o&%v|s0`||sf5D5e72B=Q4hcwtj;#*cU?=Q=?eG%z+hG^H46k7O3haj0;P>z< zypH{=@D{uYZ@}BwzX9*SyYL>i@4@@<2lxO!#P&n@2>u9rVGn$a{T|o{pTK@>_rs^~ zKk(o18GMfYXK(<%gfHMA_FuqP@FzHg?IHLx1noMmvjKM>fbR#mqvi`C)^ZgTmzDd9 z^YY75^<1KxbLRWX$}8}<-}12)6DK&c>`nFrR5`8yV_yP1? zVd2-x+6v}ccpLhzMeH2XT=*SoxW~d*q3;X}2hg|1!VjTusfCxIZv^ow^xcWRlP!D{ zHH@{efWESYYth$X;mPPb3h@Pw=E9}ud!>bUq3`(?{vI{-x3G-9hwe7*jp(h7V<8gO@VT+}4LxiJw+9Q=u*S-(q600=N*9BebzYq$V{uAPm0lhB z(n~Q-_Z`MD>SLTU{Lt3-9UcZg&9XIk3|s1ZJR8HuIv5wVq5M=E#ngss z>rTHtmtjk7sIU1gI;-96jkZ@9|9-;<9QHE)jyn-%=S@@gH=hN&#f)nfQrf&dV5S#s z48PuBv}CVdtrO#tewH49&(g+o^lA7!O`oL);QuTCKU0p>W?jxVL?crJRdJ*_ zj)8;(VI=d1?31aII7d^UN^$GB0q0LMi(!Ww2Mz_ zI1;SJ1q8#HO^Aj$S~464t?adrIoT&{} zOt8c=&d!lQQnWTXOkvE7#sf|KR)9uiv}W|MBWwkY75@6lAl5~vcfuO44%XB(#sXc& z&C*39!uHl$4zY$nRb$8>O`l*XfZ>VN1;at3&a&&Nc+?+`HTa`}a8+}c_~UxnW*U3e zX*+w^8p)9gkqLq`0+~%#Y|M=?_Rm@)3uJ1&Y;Ps|T#->jj|R9%DH9)PIEUTXG?K$! zIpeWExT3X{Y`2WWvoEZpII6JB4VgHb%6MOU8=XIi@D-h-|V zR>M=l5RG7%O`0);({>7$o%~ja&B<&UJjpriW*DY0J|SjSN8*{LbS^Z^m?h0$hy3YP zg&e~j)^eGNnR0fx3&Qb07;~r@eGFRO#o3jg&!^GP5T_8yhld9gOUx=`d&~4c;TnD?a6icb+j( ke=RG1HkHbm=7cFBE6zcm?4BfqL_f{_qaQZR z@AsYGob&yCf9H2*c0x`?ZC$(%06ZxJ7=49xz6Q_x;(57LS!;~%`Z>TP?2S*IaREQg zPR(DMGp4K{xA2&yqTuEQ(WQD-m6yG|*Ue?g9!Ze| z$;Hc}#Jjz!?B--%6%<}}Ns^)}q9h5P$t}&!P$e?(oWO~k#L1kSQ#cQ&a^A_DvmhF& z4#gWPgJ_W@xFo@=sGJQXQBftY>=lsN8dD^XqKLf8EAI4|8)bMzfmdb4w)1#JMNz%H zY6vxXy^=@p;71lxGbp@ARz#0S6(libCi8-TgaYqPv8j?GBa>J4dQxU?FR$>D$1Nyq z9f7#L4Bn++kUTPH0!463q9BVHHN_gjxM3^>l@}y?NL3_W5mj7=*K1j+iY%!fjH+8z z?IA(-sH%c$RWU`6qC~f-2)x@GLS?+0S5yzL2(}gOnB*1((Jf0hms|GAxFkgsW!#7{ z!BBNg9Ctx+2{C{YB*EBv#pRJj+3WQh zsp2uVM{%h}BviM8E5Wr$oWQ$0xM}vzlkO1=+GY7Lp@?dl@9|X=LyNnD#LRIkK&MAsZ_ToK)Nry?n^Wx+d zuKi+!f6-!i7v6}9gfe8j<5Z7V;<9k_WXuh>TXAE|CE2(uaSXRw@t6z3#4&CLb3MnQ zI1a;c2s*Aw{LyPF$?+HXi&I;~r?h!H1y1V0Bxqx00p7r=Ea-9)QNn3(%0N9!tY&9* zFcORf;?bCMm{Y3Eiig6%`mFjuZJp7lFLKVtV5~kA#jJ9QF5cOK(pbKlxf==8Vp3W| zxHcns3xbjAc#U(IN0xbG%5;9yd{5=NZ7yOGby{~k5UVzB1PLElR_IIOv5@hI7R2=4 z^nlEy|FL{tBgXX3#HWf8^{(Z~Cs5}RJFqEigtAFS{hWx8N#h3@Vz!}u4e!ZXi^Lw{aZdx`x|+ba zs1a9Z(Rly06LN7gE29nN;a~|y8^1HFP)3k*25Uq0^`XX~?TlZg_`_1GHdtR1w4}yD zwV^ngnWup`?pb+5+&FD^jxQ?7E6pn^WSdr4R6NdKfV;^#%h=a`-Y9&op`uWvGK5b_ z-1>AJm`5#BDYvjWBY0xYfw78(*rUr3Wc_5}cMkrTtvP5!#ZZb|Mu_Vx@IGL?44GUu ztFbQgNj~5b04=W&TGyPOSp71W*zscV$jjTtWOL0M+YJ1|3(o8dwommrl$`8i>s*PS z{$cx81MgT`lBjzAkZ*r}UE;t4M|{J-9G7k2bMJe?cks-ovZwbAweicF5})t5gW;}h zC_kN-@>3qFfoi5UR0HLwzLcM8qZ%mxwOJDruQyNA-k_p^%v&M(aYYvuq|=bScaFy-e|kv;^F$Y+O2xS@O_D& z{4$&2u52hjotN@c9;$(ArZ!Xq<)^-spK7BTC_m?J)mDtG@XtKu>csx(>$EGz&Sp4V zv{v&u*7^Ik49MPm#!{^^ejmec?Y~!h@>y$^qsn4h^;+c4u-44?CVtF>( z@NV0rE%#_&Ji3M9u52hjotN@c9;$(ArZ!Xq<)^-spK7BTDF3JH3$^~2Mzk;6Dl~Qb zRBhE`(-cfn$9_ro!5XX!kKw>&#V z+m>9WEiRa*l|SUyjx;Y}xGNjVPv@oll!t1dnyC%dK>4XJ<)_-H2Fib6$k^m0C$D{{ zhBU|H`H6uCybNcYxhkPAE!E~8ovVF5w<>wo?tF$nKIa$7({CT8U9#|c?eOLIBwoxf zWccrUb|h-QuF<%$)3iOc&B^D6mNDFw4dtivQhv%qHBimehH9Yv)R*#8ZBzs0Km5^h z{feoz*%y@#)H44%NB{lIT@3H|%{+bPX*c?-1h=+g6Y8 z1LdFj?n~OJyACGz531J(mVcm?-1z~+j#2Mv6aTzEIW2dBzIe-at>7=uGd#Fuhc@IN zJCj8-X6u{JcwbY$-Nf+DgKum8Npq93GpNgDx+dPdoZ+r)C_kN-@>3qFfoi5UR0HLw zzLcM8qZ%mxH#=|Ab4xesf&Pp2qYrbs6ncbVX_usb*?hnL)xIWu=vM*#NY!eFOU|v- zUlCX8o|hidZ~CiSfA*SIhL@f3qFfoi5U zR0HLwzLcM8qZ%mxQ(uRZn|){K$(l{c=QgiReA4uN(!j}&_9pi3*_NE~+6~EzZ@(j1 zz5jiNci($gGBR*ia^aD0l5@^Kl=$q-O$_h2v}S2mQN z&P(|z57j_5QyZ#*@>5^RPqk4El)v!Cr}S{-3IF^r0@`QmAJI#zo@e+fVZH7f`WkdPtyp*5vPz_WwwV@g)KlP>jR2$Vm`F*cyBbUE7qV4F@gMB^3ThZQ!_HAh2 zEuQ_4slNY>%@}F?KVlDY8`^KR?7PK_etX&0d*=`G$wza1J;Wo?z76f$(7s#T!pGCtNj(vSLnKbb!# z&YuHcf1G+gcZ1pU8EsO}=M&-Hem)!f+Pj}wK7iglARG1f;*pKqy?M++{@y&+Ab)Qj z3FPn1V;}PO<^g_y-aG~%e{UXrSp7?1I0a6FQ=uRH1p9t)I-CI+*k-_)a5kI;{oy~b z?+@p|Ko|f&#eM*s3+KW4*q#p;z=d!TT#W6-@SkugTmnCXLD>Hc9PnS@#MTLy!R7FC z7z|fnKNzlrA&?0}vCjk!vcQF{3$6kl1Q4+mK>`KbAcF^c8C3AXFl>jxa2N?A;A-$; ze>G?r`fSL-J{v|sE{w)@G+YB?AP@4f&4;m22;-mtim)$$@o+5^V_OU*a2-s5Qn()b zQn(3jgd1QY_BUYt&t$k6+nb>begOd}$F>|QpbCOe3DwwFLJfpq3bs?=7KlJCgdvK3 z7-DcM)ImM=br6RJXvDS=rouF6g6Y^!hh~@oEpQvm#Qrvz1+!rewsYWimh)S$F~aXW@77B5cKWE3`oZlAvR&!#3CnJ77D!g#C8d1uw%Z*uDb0 z;WhX@yb7;l|0=u%Z^9e!Hui78JMb>NhwXdtKKubbfDf_#5I%xG!d}<|A7j4<_Q5Bx zAKU%#Df|!o2|k0*vHuJXz?bj^9K`+$_zM0Ehp;^ae}Rx)$8|Q~?gQ|B0C&{8;NvfVpbaT#Je@ST>{_VGPOxc9-&SCLb1O9C}{fA-vqc*LAX=xc!)S7qd z!X>X{UD}jiHoi6QZwpMk?Aq46y%yeG+?w~Yh5MAY<~?KKZwp)V-bVZ=`f}*oX5n)5 z-C*JS(RYP~Un^om!j{L7T$%v=Ue!D)X>+$GWs66%d|J5 z-<+mk+6o(jkRy+VLP}zSg3|IR$dhyXkk{m7{sje>L3}5 zQ@X44YR{Km0;$4h!?r z%V(t)lwH8$t@fogSUiX&DRwDYCkL#&X>WhxI-C=h33)n5W!yiq45w3B&GD;mx~|q~ z>9V?}$Mrd^Xy^x*;b85JU53-H@&*HIS0J%n=&JJO2UgR}Ki#aQ0dV;7NFF;jC~0G( z?}KB;`1i*K`Hz~{lKt@fRMXyn7{{oOan0~UTmN@>82B{H*5EO0sqgV@3?J)YT-1j0 zQ*9Je8>+1<{q|gjEw!P(=C|mqHnTU{9%20b4PS8B$M`+&M3`MSP1)~!7VHu;u31QF z^LB%oUbHd%dVY*TsSrA*006QG;d2b&jfVG!P%2(GUq$MPs!Nyai-OLz$8MVdLRFjuo19x?UHH zP6<}Tk>)rC5)wp^ED*LYrb^-*jll}Z5h$yR)?>w7G%`G+F%WACMXJpUjj0Q{Im0tL zd6}f0d{V>FP$e!P6v=EvG{n)I;V@`r2hQdg(Ro#lE7|4Dv>Z-w@k53iZbS&WB7tjK z({$rXb+~N2C7yA1j0BRRwaH-$V`elSXcDjjG(4jvqnjOJD`+SS)Ru>^E;_vv)_7&8 zs;Z$r*lFA>T{I$WZ>{A}Ul*)s2nS;63oHdNJoPo9NXV$O?0hL63qSamtuEh91Q!Je?$0cLFWNjo-c%b0eJy& zOSjy+(bdjsXet<@5e&0QGlp^64#BdM-wLrgnN5QiIfva0!4$?f#LUWQJkylUg@+ik zr1@)*KfS83W2nPgE;BJx&JK58Bp!@l4mF{VLCd>1yosh}c9_CZW6r_RTphyrY^H8B z+h(Z=Q#)T_loWRk2__NaH!ou>tk@X)h%um%GN@8!_C#o7Lnf`Ak)1T{2JNK5XM}mj qr~L5AGddQiX2s8@QaRI{FeStl;A~B<0M6Y|MsHkPNlS+DmGe*4YCuE) literal 0 HcmV?d00001 diff --git a/specs/data/migration/input/Composite/CompositeOfComposite/tileset.json b/specs/data/migration/input/Composite/CompositeOfComposite/tileset.json new file mode 100644 index 00000000..c6066895 --- /dev/null +++ b/specs/data/migration/input/Composite/CompositeOfComposite/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "1.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196959060375169, + "maximum": -1.3196607462778132 + }, + "Latitude": { + "minimum": 0.6988590050687061, + "maximum": 0.6988864387845588 + }, + "Height": { + "minimum": 6.1022464875131845, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "ADD", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "uri": "compositeOfComposite.cmpt" + } + } +} diff --git a/specs/data/migration/input/Composite/CompositeOfInstanced/box.glb b/specs/data/migration/input/Composite/CompositeOfInstanced/box.glb new file mode 100644 index 0000000000000000000000000000000000000000..66ca853f998ebadb865f8ce683b8168c41b4cb3b GIT binary patch literal 3284 zcma)8+iu%N5Z$DXo2qHrrfvEdVxQ!O5qObhSw>MPu3b2=E!TGP5*WscT1hM@QXnbC zj$j~<{j0+N(4T2h)IGb)r6|dbESuEsT+Ym#nWfkZPIvDULjHV2h_ynb(_Nd$C@f)ANqYc-j?aP$6y4{4Qv|*Xliw46ea3;*jB|7AGdB-YnvNR zt!6?du6cPLUaP7bwYs}uuh$(D9$LD-;W$=xy}nt8LsrR~oo$ycN!h8_Z)0XQ=_1~= z1Z0c>`*e3LmhP$36>G*~4~xz*80kzIPmrdi@Zi zPa6|D$b`GhlUyUMSnIXSM&>Nx+ffk3TJ{zVQ%y4|ANWb)k9n@MO#F#v2E#r(h(tlO zb=3Uz^$t4NiAL>!pP9Utdec1oA7HZneDL4*mhQLqC9gkA^}53?q14R$_^5**bi+$xP( zeCdZhr5NXmo$amCf|mka@X5uI-$4O>SRP}wW=u+kfUO#cX8gEVmGOgAm#68NcEwuT z5^jY=;0l1s#pQdU=>_LUS@IHdo(X`$T4AWfq*;*71sXxi86U#_D-a zh9k&Ow<%(PuybVT;m;^m!88n%rlD_Z7-77imOD{eR;io8nus#R?*o5YRbV_fvT~(~ z8aYk(UYPO_Gjs_b0c%xE9HMvSxe!KQ#W0Jrx+d{7(vqCDSspO&yeD2#7Ae6HNqnUS z7a2FAkKmxllz__3bRgL1rA%$cNDs}7P!A2B5$eR}et7ci#;hk_r&OlZvI>|dQemdf QslpI@bY48TsN|~jA5kyFCIA2c literal 0 HcmV?d00001 diff --git a/specs/data/migration/input/Composite/CompositeOfInstanced/compositeOfInstanced.cmpt b/specs/data/migration/input/Composite/CompositeOfInstanced/compositeOfInstanced.cmpt new file mode 100644 index 0000000000000000000000000000000000000000..99f84f85f092cce17a9f2d608cba0ed0b9b52d88 GIT binary patch literal 1024 zcmeIvKTE?v7{~Fp=v&ZD!);2`SnHBOC=$#CNn#Q~uo8$Sv0$la1Y^kNq9PPIQP9Q7 z!P(hC5dVOSvu{Cg5d{%{rH}8xO^=Z06UY~iyT!(8QY>I~6$&Mc`q)yV{r!%Yj;UbI zVg9U)antpx=A`EO)0#Qu=}aw;%S@}fo^M)?r~9)uQwida0I_27d^o*$#16MPw+Wf8SNE0Kzl(x zMteklbOqlgU!y&PS$}GLU=RD`5wu(65N$+0LF Date: Sat, 20 Apr 2024 18:24:42 +0200 Subject: [PATCH 03/37] Removed fit for unit tests --- specs/tools/migration/TileFormatsMigrationSpec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/tools/migration/TileFormatsMigrationSpec.ts b/specs/tools/migration/TileFormatsMigrationSpec.ts index f34b42de..713f52bf 100644 --- a/specs/tools/migration/TileFormatsMigrationSpec.ts +++ b/specs/tools/migration/TileFormatsMigrationSpec.ts @@ -837,7 +837,7 @@ describe("TileFormatsMigration", function () { //========================================================================== // CMPT - fit("converts Composite to the expected output", async function () { + it("converts Composite to the expected output", async function () { const subDir = "Composite/"; const name = "Composite"; const fileNameWithoutExtension = "composite"; @@ -849,7 +849,7 @@ describe("TileFormatsMigration", function () { expect(jsonStrings.outputJsonString).toEqual(jsonStrings.goldenJsonString); }); - fit("converts CompositeOfComposite to the expected output", async function () { + it("converts CompositeOfComposite to the expected output", async function () { const subDir = "Composite/"; const name = "CompositeOfComposite"; const fileNameWithoutExtension = "compositeOfComposite"; @@ -861,7 +861,7 @@ describe("TileFormatsMigration", function () { expect(jsonStrings.outputJsonString).toEqual(jsonStrings.goldenJsonString); }); - fit("converts CompositeOfInstanced to the expected output", async function () { + it("converts CompositeOfInstanced to the expected output", async function () { const subDir = "Composite/"; const name = "CompositeOfInstanced"; const fileNameWithoutExtension = "compositeOfInstanced"; From 83c920d1120abbb49c69895f7ef03bbd9568a970 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 20 Apr 2024 18:30:56 +0200 Subject: [PATCH 04/37] Remove invalid spec files --- .../Composite/Composite/composite.gltf | 299 ---------------- .../compositeOfComposite.gltf | 298 ---------------- .../compositeOfInstanced.gltf | 324 ------------------ 3 files changed, 921 deletions(-) delete mode 100644 specs/data/migration/golden_gltf/Composite/Composite/composite.gltf delete mode 100644 specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf delete mode 100644 specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf diff --git a/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf b/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf deleted file mode 100644 index fbe489c3..00000000 --- a/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf +++ /dev/null @@ -1,299 +0,0 @@ -{ - "asset": { - "generator": "glTF-Transform v3.10.0", - "version": "2.0" - }, - "accessors": [ - { - "type": "VEC3", - "componentType": 5126, - "count": 25, - "bufferView": 0, - "byteOffset": 0 - }, - { - "type": "VEC4", - "componentType": 5126, - "count": 25, - "bufferView": 0, - "byteOffset": 300 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 25, - "bufferView": 0, - "byteOffset": 700 - }, - { - "type": "SCALAR", - "componentType": 5123, - "count": 25, - "bufferView": 0, - "byteOffset": 1000 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 240, - "max": [ - 108.69153594970703, - 74.8784408569336, - 66.49319458007812 - ], - "min": [ - -90.89249420166016, - -86.16458892822266, - -78.88909149169922 - ], - "bufferView": 1, - "byteOffset": 0 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 240, - "bufferView": 1, - "byteOffset": 12 - }, - { - "type": "SCALAR", - "componentType": 5126, - "count": 240, - "bufferView": 1, - "byteOffset": 24 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 24, - "max": [ - 10.000004768371582, - 10, - 10.000005722045898 - ], - "min": [ - -10.000003814697266, - -10, - -10.000003814697266 - ], - "bufferView": 2, - "byteOffset": 0 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 24, - "bufferView": 2, - "byteOffset": 12 - }, - { - "type": "SCALAR", - "componentType": 5123, - "count": 360, - "bufferView": 3, - "byteOffset": 0 - }, - { - "type": "SCALAR", - "componentType": 5123, - "count": 36, - "bufferView": 3, - "byteOffset": 720 - } - ], - "bufferViews": [ - { - "buffer": 0, - "byteOffset": 0, - "byteLength": 1052 - }, - { - "buffer": 0, - "byteOffset": 1052, - "byteLength": 6720, - "byteStride": 28, - "target": 34962 - }, - { - "buffer": 0, - "byteOffset": 7772, - "byteLength": 576, - "byteStride": 24, - "target": 34962 - }, - { - "buffer": 0, - "byteOffset": 8348, - "byteLength": 792, - "target": 34963 - }, - { - "buffer": 0, - "byteOffset": 9140, - "byteLength": 25 - } - ], - "buffers": [ - { - "name": "buffer", - "byteLength": 9165 - } - ], - "materials": [ - { - "pbrMetallicRoughness": { - "metallicFactor": 0 - } - }, - { - "pbrMetallicRoughness": { - "metallicFactor": 0 - } - } - ], - "meshes": [ - { - "primitives": [ - { - "attributes": { - "POSITION": 4, - "NORMAL": 5, - "_FEATURE_ID_0": 6 - }, - "mode": 4, - "material": 0, - "indices": 9, - "extensions": { - "EXT_mesh_features": { - "featureIds": [ - { - "featureCount": 10, - "attribute": 0, - "propertyTable": -1 - } - ] - } - } - } - ] - }, - { - "primitives": [ - { - "attributes": { - "POSITION": 7, - "NORMAL": 8 - }, - "mode": 4, - "material": 1, - "indices": 10 - } - ] - } - ], - "nodes": [ - { - "rotation": [ - -0.7071067811865475, - 0, - 0, - 0.7071067811865476 - ], - "mesh": 0 - }, - { - "translation": [ - 1215012.8988049095, - 4081604.3368623317, - 4736313.0423059845 - ], - "children": [ - 0 - ] - }, - { - "mesh": 1, - "extensions": { - "EXT_mesh_gpu_instancing": { - "attributes": { - "TRANSLATION": 0, - "ROTATION": 1, - "SCALE": 2, - "_FEATURE_ID_0": 3 - } - }, - "EXT_instance_features": { - "featureIds": [ - { - "featureCount": 25, - "attribute": 0, - "propertyTable": 0 - } - ] - } - } - }, - { - "translation": [ - 1215013.84, - 4081608.45, - 4736316.76 - ], - "children": [ - 2 - ] - } - ], - "scenes": [ - { - "nodes": [ - 1, - 3 - ] - } - ], - "scene": 0, - "extensionsUsed": [ - "EXT_structural_metadata", - "EXT_mesh_features", - "EXT_mesh_gpu_instancing", - "EXT_instance_features" - ], - "extensionsRequired": [ - "EXT_mesh_gpu_instancing" - ], - "extensions": { - "EXT_structural_metadata": { - "schema": { - "id": "ID_batch_table", - "name": "Generated from batch_table", - "classes": { - "class_batch_table": { - "name": "Generated from batch_table", - "properties": { - "Height": { - "name": "Height", - "description": "Generated from Height", - "type": "SCALAR", - "componentType": "UINT8", - "required": true - } - } - } - } - }, - "propertyTables": [ - { - "class": "class_batch_table", - "count": 25, - "properties": { - "Height": { - "values": 4 - } - } - } - ] - } - } -} \ No newline at end of file diff --git a/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf b/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf deleted file mode 100644 index 116279cc..00000000 --- a/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf +++ /dev/null @@ -1,298 +0,0 @@ -{ - "asset": { - "generator": "glTF-Transform v3.10.0", - "version": "2.0" - }, - "accessors": [ - { - "type": "VEC3", - "componentType": 5126, - "count": 25, - "bufferView": 0, - "byteOffset": 0 - }, - { - "type": "VEC4", - "componentType": 5126, - "count": 25, - "bufferView": 0, - "byteOffset": 300 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 25, - "bufferView": 0, - "byteOffset": 700 - }, - { - "type": "SCALAR", - "componentType": 5123, - "count": 25, - "bufferView": 0, - "byteOffset": 1000 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 240, - "max": [ - 108.69153594970703, - 74.8784408569336, - 66.49319458007812 - ], - "min": [ - -90.89249420166016, - -86.16458892822266, - -78.88909149169922 - ], - "bufferView": 1, - "byteOffset": 0 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 240, - "bufferView": 1, - "byteOffset": 12 - }, - { - "type": "SCALAR", - "componentType": 5126, - "count": 240, - "bufferView": 1, - "byteOffset": 24 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 24, - "max": [ - 10.000004768371582, - 10, - 10.000005722045898 - ], - "min": [ - -10.000003814697266, - -10, - -10.000003814697266 - ], - "bufferView": 2, - "byteOffset": 0 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 24, - "bufferView": 2, - "byteOffset": 12 - }, - { - "type": "SCALAR", - "componentType": 5123, - "count": 360, - "bufferView": 3, - "byteOffset": 0 - }, - { - "type": "SCALAR", - "componentType": 5123, - "count": 36, - "bufferView": 3, - "byteOffset": 720 - } - ], - "bufferViews": [ - { - "buffer": 0, - "byteOffset": 0, - "byteLength": 1052 - }, - { - "buffer": 0, - "byteOffset": 1052, - "byteLength": 6720, - "byteStride": 28, - "target": 34962 - }, - { - "buffer": 0, - "byteOffset": 7772, - "byteLength": 576, - "byteStride": 24, - "target": 34962 - }, - { - "buffer": 0, - "byteOffset": 8348, - "byteLength": 792, - "target": 34963 - }, - { - "buffer": 0, - "byteOffset": 9140, - "byteLength": 25 - } - ], - "buffers": [ - { - "name": "buffer", - "byteLength": 9165 - } - ], - "materials": [ - { - "pbrMetallicRoughness": { - "metallicFactor": 0 - } - }, - { - "pbrMetallicRoughness": { - "metallicFactor": 0 - } - } - ], - "meshes": [ - { - "primitives": [ - { - "attributes": { - "POSITION": 4, - "NORMAL": 5, - "_FEATURE_ID_0": 6 - }, - "mode": 4, - "material": 0, - "indices": 9, - "extensions": { - "EXT_mesh_features": { - "featureIds": [ - { - "featureCount": 10, - "attribute": 0 - } - ] - } - } - } - ] - }, - { - "primitives": [ - { - "attributes": { - "POSITION": 7, - "NORMAL": 8 - }, - "mode": 4, - "material": 1, - "indices": 10 - } - ] - } - ], - "nodes": [ - { - "rotation": [ - -0.7071067811865475, - 0, - 0, - 0.7071067811865476 - ], - "mesh": 0 - }, - { - "translation": [ - 1215012.8988049095, - 4081604.3368623317, - 4736313.0423059845 - ], - "children": [ - 0 - ] - }, - { - "mesh": 1, - "extensions": { - "EXT_mesh_gpu_instancing": { - "attributes": { - "TRANSLATION": 0, - "ROTATION": 1, - "SCALE": 2, - "_FEATURE_ID_0": 3 - } - }, - "EXT_instance_features": { - "featureIds": [ - { - "featureCount": 25, - "attribute": 0, - "propertyTable": 0 - } - ] - } - } - }, - { - "translation": [ - 1215013.84, - 4081608.45, - 4736316.76 - ], - "children": [ - 2 - ] - } - ], - "scenes": [ - { - "nodes": [ - 1, - 3 - ] - } - ], - "scene": 0, - "extensionsUsed": [ - "EXT_mesh_gpu_instancing", - "EXT_structural_metadata", - "EXT_mesh_features", - "EXT_instance_features" - ], - "extensionsRequired": [ - "EXT_mesh_gpu_instancing" - ], - "extensions": { - "EXT_structural_metadata": { - "schema": { - "id": "ID_batch_table", - "name": "Generated from batch_table", - "classes": { - "class_batch_table": { - "name": "Generated from batch_table", - "properties": { - "Height": { - "name": "Height", - "description": "Generated from Height", - "type": "SCALAR", - "componentType": "UINT8", - "required": true - } - } - } - } - }, - "propertyTables": [ - { - "class": "class_batch_table", - "count": 25, - "properties": { - "Height": { - "values": 4 - } - } - } - ] - } - } -} \ No newline at end of file diff --git a/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf b/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf deleted file mode 100644 index 92c46ec5..00000000 --- a/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf +++ /dev/null @@ -1,324 +0,0 @@ -{ - "asset": { - "generator": "glTF-Transform v3.10.0", - "version": "2.0" - }, - "accessors": [ - { - "type": "VEC3", - "componentType": 5126, - "count": 25, - "bufferView": 0, - "byteOffset": 0 - }, - { - "type": "VEC4", - "componentType": 5126, - "count": 25, - "bufferView": 0, - "byteOffset": 300 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 25, - "bufferView": 0, - "byteOffset": 700 - }, - { - "type": "SCALAR", - "componentType": 5123, - "count": 25, - "bufferView": 0, - "byteOffset": 1000 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 25, - "bufferView": 1, - "byteOffset": 0 - }, - { - "type": "VEC4", - "componentType": 5126, - "count": 25, - "bufferView": 1, - "byteOffset": 300 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 25, - "bufferView": 1, - "byteOffset": 700 - }, - { - "type": "SCALAR", - "componentType": 5123, - "count": 25, - "bufferView": 1, - "byteOffset": 1000 - }, - { - "type": "SCALAR", - "componentType": 5123, - "count": 36, - "bufferView": 2, - "byteOffset": 0 - }, - { - "type": "SCALAR", - "componentType": 5123, - "count": 36, - "bufferView": 2, - "byteOffset": 72 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 24, - "max": [ - 10.000004768371582, - 10, - 10.000005722045898 - ], - "min": [ - -10.000003814697266, - -10, - -10.000003814697266 - ], - "bufferView": 3, - "byteOffset": 0 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 24, - "bufferView": 3, - "byteOffset": 12 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 24, - "max": [ - 10.000004768371582, - 10, - 10.000005722045898 - ], - "min": [ - -10.000003814697266, - -10, - -10.000003814697266 - ], - "bufferView": 4, - "byteOffset": 0 - }, - { - "type": "VEC3", - "componentType": 5126, - "count": 24, - "bufferView": 4, - "byteOffset": 12 - } - ], - "bufferViews": [ - { - "buffer": 0, - "byteOffset": 0, - "byteLength": 1052 - }, - { - "buffer": 0, - "byteOffset": 1052, - "byteLength": 1052 - }, - { - "buffer": 0, - "byteOffset": 2104, - "byteLength": 144, - "target": 34963 - }, - { - "buffer": 0, - "byteOffset": 2248, - "byteLength": 576, - "byteStride": 24, - "target": 34962 - }, - { - "buffer": 0, - "byteOffset": 2824, - "byteLength": 576, - "byteStride": 24, - "target": 34962 - }, - { - "buffer": 0, - "byteOffset": 3400, - "byteLength": 25 - } - ], - "buffers": [ - { - "byteLength": 3425 - } - ], - "materials": [ - { - "pbrMetallicRoughness": { - "metallicFactor": 0 - } - }, - { - "pbrMetallicRoughness": { - "metallicFactor": 0 - } - } - ], - "meshes": [ - { - "primitives": [ - { - "attributes": { - "POSITION": 10, - "NORMAL": 11 - }, - "mode": 4, - "material": 0, - "indices": 8 - } - ] - }, - { - "primitives": [ - { - "attributes": { - "POSITION": 12, - "NORMAL": 13 - }, - "mode": 4, - "material": 1, - "indices": 9 - } - ] - } - ], - "nodes": [ - { - "mesh": 0, - "extensions": { - "EXT_mesh_gpu_instancing": { - "attributes": { - "TRANSLATION": 0, - "ROTATION": 1, - "SCALE": 2, - "_FEATURE_ID_0": 3 - } - }, - "EXT_instance_features": { - "featureIds": [ - { - "featureCount": 25, - "attribute": 0, - "propertyTable": -1 - } - ] - } - } - }, - { - "translation": [ - 1215013.84, - 4081608.45, - 4736316.76 - ], - "children": [ - 0 - ] - }, - { - "mesh": 1, - "extensions": { - "EXT_mesh_gpu_instancing": { - "attributes": { - "TRANSLATION": 4, - "ROTATION": 5, - "SCALE": 6, - "_FEATURE_ID_0": 7 - } - }, - "EXT_instance_features": { - "featureIds": [ - { - "featureCount": 25, - "attribute": 0, - "propertyTable": 0 - } - ] - } - } - }, - { - "translation": [ - 1215013.84, - 4081608.45, - 4736316.76 - ], - "children": [ - 2 - ] - } - ], - "scenes": [ - { - "nodes": [ - 1, - 3 - ] - } - ], - "scene": 0, - "extensionsUsed": [ - "EXT_mesh_gpu_instancing", - "EXT_structural_metadata", - "EXT_instance_features" - ], - "extensionsRequired": [ - "EXT_mesh_gpu_instancing" - ], - "extensions": { - "EXT_structural_metadata": { - "schema": { - "id": "ID_batch_table", - "name": "Generated from batch_table", - "classes": { - "class_batch_table": { - "name": "Generated from batch_table", - "properties": { - "Height": { - "name": "Height", - "description": "Generated from Height", - "type": "SCALAR", - "componentType": "UINT8", - "required": true - } - } - } - } - }, - "propertyTables": [ - { - "class": "class_batch_table", - "count": 25, - "properties": { - "Height": { - "values": 5 - } - } - } - ] - } - } -} \ No newline at end of file From 1b2778c72a1259f301fe0a0dd8e1afaf01f8d7e6 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 20 Apr 2024 18:31:32 +0200 Subject: [PATCH 05/37] Add proper spec files --- .../Composite/Composite/composite.gltf | 299 ++++++++++++++++ .../compositeOfComposite.gltf | 298 ++++++++++++++++ .../compositeOfInstanced.gltf | 324 ++++++++++++++++++ 3 files changed, 921 insertions(+) create mode 100644 specs/data/migration/golden_gltf/Composite/Composite/composite.gltf create mode 100644 specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf create mode 100644 specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf diff --git a/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf b/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf new file mode 100644 index 00000000..fbe489c3 --- /dev/null +++ b/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf @@ -0,0 +1,299 @@ +{ + "asset": { + "generator": "glTF-Transform v3.10.0", + "version": "2.0" + }, + "accessors": [ + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 0 + }, + { + "type": "VEC4", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 300 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 700 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 25, + "bufferView": 0, + "byteOffset": 1000 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 240, + "max": [ + 108.69153594970703, + 74.8784408569336, + 66.49319458007812 + ], + "min": [ + -90.89249420166016, + -86.16458892822266, + -78.88909149169922 + ], + "bufferView": 1, + "byteOffset": 0 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 240, + "bufferView": 1, + "byteOffset": 12 + }, + { + "type": "SCALAR", + "componentType": 5126, + "count": 240, + "bufferView": 1, + "byteOffset": 24 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "max": [ + 10.000004768371582, + 10, + 10.000005722045898 + ], + "min": [ + -10.000003814697266, + -10, + -10.000003814697266 + ], + "bufferView": 2, + "byteOffset": 0 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "bufferView": 2, + "byteOffset": 12 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 360, + "bufferView": 3, + "byteOffset": 0 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 36, + "bufferView": 3, + "byteOffset": 720 + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 1052 + }, + { + "buffer": 0, + "byteOffset": 1052, + "byteLength": 6720, + "byteStride": 28, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 7772, + "byteLength": 576, + "byteStride": 24, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 8348, + "byteLength": 792, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 9140, + "byteLength": 25 + } + ], + "buffers": [ + { + "name": "buffer", + "byteLength": 9165 + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "metallicFactor": 0 + } + }, + { + "pbrMetallicRoughness": { + "metallicFactor": 0 + } + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "POSITION": 4, + "NORMAL": 5, + "_FEATURE_ID_0": 6 + }, + "mode": 4, + "material": 0, + "indices": 9, + "extensions": { + "EXT_mesh_features": { + "featureIds": [ + { + "featureCount": 10, + "attribute": 0, + "propertyTable": -1 + } + ] + } + } + } + ] + }, + { + "primitives": [ + { + "attributes": { + "POSITION": 7, + "NORMAL": 8 + }, + "mode": 4, + "material": 1, + "indices": 10 + } + ] + } + ], + "nodes": [ + { + "rotation": [ + -0.7071067811865475, + 0, + 0, + 0.7071067811865476 + ], + "mesh": 0 + }, + { + "translation": [ + 1215012.8988049095, + 4081604.3368623317, + 4736313.0423059845 + ], + "children": [ + 0 + ] + }, + { + "mesh": 1, + "extensions": { + "EXT_mesh_gpu_instancing": { + "attributes": { + "TRANSLATION": 0, + "ROTATION": 1, + "SCALE": 2, + "_FEATURE_ID_0": 3 + } + }, + "EXT_instance_features": { + "featureIds": [ + { + "featureCount": 25, + "attribute": 0, + "propertyTable": 0 + } + ] + } + } + }, + { + "translation": [ + 1215013.84, + 4081608.45, + 4736316.76 + ], + "children": [ + 2 + ] + } + ], + "scenes": [ + { + "nodes": [ + 1, + 3 + ] + } + ], + "scene": 0, + "extensionsUsed": [ + "EXT_structural_metadata", + "EXT_mesh_features", + "EXT_mesh_gpu_instancing", + "EXT_instance_features" + ], + "extensionsRequired": [ + "EXT_mesh_gpu_instancing" + ], + "extensions": { + "EXT_structural_metadata": { + "schema": { + "id": "ID_batch_table", + "name": "Generated from batch_table", + "classes": { + "class_batch_table": { + "name": "Generated from batch_table", + "properties": { + "Height": { + "name": "Height", + "description": "Generated from Height", + "type": "SCALAR", + "componentType": "UINT8", + "required": true + } + } + } + } + }, + "propertyTables": [ + { + "class": "class_batch_table", + "count": 25, + "properties": { + "Height": { + "values": 4 + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf b/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf new file mode 100644 index 00000000..116279cc --- /dev/null +++ b/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf @@ -0,0 +1,298 @@ +{ + "asset": { + "generator": "glTF-Transform v3.10.0", + "version": "2.0" + }, + "accessors": [ + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 0 + }, + { + "type": "VEC4", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 300 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 700 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 25, + "bufferView": 0, + "byteOffset": 1000 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 240, + "max": [ + 108.69153594970703, + 74.8784408569336, + 66.49319458007812 + ], + "min": [ + -90.89249420166016, + -86.16458892822266, + -78.88909149169922 + ], + "bufferView": 1, + "byteOffset": 0 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 240, + "bufferView": 1, + "byteOffset": 12 + }, + { + "type": "SCALAR", + "componentType": 5126, + "count": 240, + "bufferView": 1, + "byteOffset": 24 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "max": [ + 10.000004768371582, + 10, + 10.000005722045898 + ], + "min": [ + -10.000003814697266, + -10, + -10.000003814697266 + ], + "bufferView": 2, + "byteOffset": 0 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "bufferView": 2, + "byteOffset": 12 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 360, + "bufferView": 3, + "byteOffset": 0 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 36, + "bufferView": 3, + "byteOffset": 720 + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 1052 + }, + { + "buffer": 0, + "byteOffset": 1052, + "byteLength": 6720, + "byteStride": 28, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 7772, + "byteLength": 576, + "byteStride": 24, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 8348, + "byteLength": 792, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 9140, + "byteLength": 25 + } + ], + "buffers": [ + { + "name": "buffer", + "byteLength": 9165 + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "metallicFactor": 0 + } + }, + { + "pbrMetallicRoughness": { + "metallicFactor": 0 + } + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "POSITION": 4, + "NORMAL": 5, + "_FEATURE_ID_0": 6 + }, + "mode": 4, + "material": 0, + "indices": 9, + "extensions": { + "EXT_mesh_features": { + "featureIds": [ + { + "featureCount": 10, + "attribute": 0 + } + ] + } + } + } + ] + }, + { + "primitives": [ + { + "attributes": { + "POSITION": 7, + "NORMAL": 8 + }, + "mode": 4, + "material": 1, + "indices": 10 + } + ] + } + ], + "nodes": [ + { + "rotation": [ + -0.7071067811865475, + 0, + 0, + 0.7071067811865476 + ], + "mesh": 0 + }, + { + "translation": [ + 1215012.8988049095, + 4081604.3368623317, + 4736313.0423059845 + ], + "children": [ + 0 + ] + }, + { + "mesh": 1, + "extensions": { + "EXT_mesh_gpu_instancing": { + "attributes": { + "TRANSLATION": 0, + "ROTATION": 1, + "SCALE": 2, + "_FEATURE_ID_0": 3 + } + }, + "EXT_instance_features": { + "featureIds": [ + { + "featureCount": 25, + "attribute": 0, + "propertyTable": 0 + } + ] + } + } + }, + { + "translation": [ + 1215013.84, + 4081608.45, + 4736316.76 + ], + "children": [ + 2 + ] + } + ], + "scenes": [ + { + "nodes": [ + 1, + 3 + ] + } + ], + "scene": 0, + "extensionsUsed": [ + "EXT_mesh_gpu_instancing", + "EXT_structural_metadata", + "EXT_mesh_features", + "EXT_instance_features" + ], + "extensionsRequired": [ + "EXT_mesh_gpu_instancing" + ], + "extensions": { + "EXT_structural_metadata": { + "schema": { + "id": "ID_batch_table", + "name": "Generated from batch_table", + "classes": { + "class_batch_table": { + "name": "Generated from batch_table", + "properties": { + "Height": { + "name": "Height", + "description": "Generated from Height", + "type": "SCALAR", + "componentType": "UINT8", + "required": true + } + } + } + } + }, + "propertyTables": [ + { + "class": "class_batch_table", + "count": 25, + "properties": { + "Height": { + "values": 4 + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf b/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf new file mode 100644 index 00000000..92c46ec5 --- /dev/null +++ b/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf @@ -0,0 +1,324 @@ +{ + "asset": { + "generator": "glTF-Transform v3.10.0", + "version": "2.0" + }, + "accessors": [ + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 0 + }, + { + "type": "VEC4", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 300 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 0, + "byteOffset": 700 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 25, + "bufferView": 0, + "byteOffset": 1000 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 1, + "byteOffset": 0 + }, + { + "type": "VEC4", + "componentType": 5126, + "count": 25, + "bufferView": 1, + "byteOffset": 300 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 25, + "bufferView": 1, + "byteOffset": 700 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 25, + "bufferView": 1, + "byteOffset": 1000 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 36, + "bufferView": 2, + "byteOffset": 0 + }, + { + "type": "SCALAR", + "componentType": 5123, + "count": 36, + "bufferView": 2, + "byteOffset": 72 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "max": [ + 10.000004768371582, + 10, + 10.000005722045898 + ], + "min": [ + -10.000003814697266, + -10, + -10.000003814697266 + ], + "bufferView": 3, + "byteOffset": 0 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "bufferView": 3, + "byteOffset": 12 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "max": [ + 10.000004768371582, + 10, + 10.000005722045898 + ], + "min": [ + -10.000003814697266, + -10, + -10.000003814697266 + ], + "bufferView": 4, + "byteOffset": 0 + }, + { + "type": "VEC3", + "componentType": 5126, + "count": 24, + "bufferView": 4, + "byteOffset": 12 + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 1052 + }, + { + "buffer": 0, + "byteOffset": 1052, + "byteLength": 1052 + }, + { + "buffer": 0, + "byteOffset": 2104, + "byteLength": 144, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 2248, + "byteLength": 576, + "byteStride": 24, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 2824, + "byteLength": 576, + "byteStride": 24, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 3400, + "byteLength": 25 + } + ], + "buffers": [ + { + "byteLength": 3425 + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "metallicFactor": 0 + } + }, + { + "pbrMetallicRoughness": { + "metallicFactor": 0 + } + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "POSITION": 10, + "NORMAL": 11 + }, + "mode": 4, + "material": 0, + "indices": 8 + } + ] + }, + { + "primitives": [ + { + "attributes": { + "POSITION": 12, + "NORMAL": 13 + }, + "mode": 4, + "material": 1, + "indices": 9 + } + ] + } + ], + "nodes": [ + { + "mesh": 0, + "extensions": { + "EXT_mesh_gpu_instancing": { + "attributes": { + "TRANSLATION": 0, + "ROTATION": 1, + "SCALE": 2, + "_FEATURE_ID_0": 3 + } + }, + "EXT_instance_features": { + "featureIds": [ + { + "featureCount": 25, + "attribute": 0, + "propertyTable": -1 + } + ] + } + } + }, + { + "translation": [ + 1215013.84, + 4081608.45, + 4736316.76 + ], + "children": [ + 0 + ] + }, + { + "mesh": 1, + "extensions": { + "EXT_mesh_gpu_instancing": { + "attributes": { + "TRANSLATION": 4, + "ROTATION": 5, + "SCALE": 6, + "_FEATURE_ID_0": 7 + } + }, + "EXT_instance_features": { + "featureIds": [ + { + "featureCount": 25, + "attribute": 0, + "propertyTable": 0 + } + ] + } + } + }, + { + "translation": [ + 1215013.84, + 4081608.45, + 4736316.76 + ], + "children": [ + 2 + ] + } + ], + "scenes": [ + { + "nodes": [ + 1, + 3 + ] + } + ], + "scene": 0, + "extensionsUsed": [ + "EXT_mesh_gpu_instancing", + "EXT_structural_metadata", + "EXT_instance_features" + ], + "extensionsRequired": [ + "EXT_mesh_gpu_instancing" + ], + "extensions": { + "EXT_structural_metadata": { + "schema": { + "id": "ID_batch_table", + "name": "Generated from batch_table", + "classes": { + "class_batch_table": { + "name": "Generated from batch_table", + "properties": { + "Height": { + "name": "Height", + "description": "Generated from Height", + "type": "SCALAR", + "componentType": "UINT8", + "required": true + } + } + } + } + }, + "propertyTables": [ + { + "class": "class_batch_table", + "count": 25, + "properties": { + "Height": { + "values": 5 + } + } + } + ] + } + } +} \ No newline at end of file From b1facbcc65924ce4eaf611102ceaedae73f036bc Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 20 Apr 2024 18:37:12 +0200 Subject: [PATCH 06/37] Set proper generator for merged document --- src/tools/contentProcessing/GltfTransform.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tools/contentProcessing/GltfTransform.ts b/src/tools/contentProcessing/GltfTransform.ts index 1dbc2e44..8dfe822f 100644 --- a/src/tools/contentProcessing/GltfTransform.ts +++ b/src/tools/contentProcessing/GltfTransform.ts @@ -128,6 +128,9 @@ export class GltfTransform { // Create one document from each buffer and merge them const io = await GltfTransform.getIO(); const mergedDocument = new Document(); + const root = mergedDocument.getRoot(); + const asset = root.getAsset(); + asset.generator = "glTF-Transform"; for (const inputGlbBuffer of inputGlbBuffers) { const inputDocument = await io.readBinary(inputGlbBuffer); mergedDocument.merge(inputDocument); From 091a73737177a16d97b51df2300b69a408216bb1 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 20 Apr 2024 18:38:08 +0200 Subject: [PATCH 07/37] Update spec files --- .../migration/golden_gltf/Composite/Composite/composite.gltf | 2 +- .../Composite/CompositeOfComposite/compositeOfComposite.gltf | 2 +- .../Composite/CompositeOfInstanced/compositeOfInstanced.gltf | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf b/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf index fbe489c3..6822da8e 100644 --- a/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf +++ b/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf @@ -1,6 +1,6 @@ { "asset": { - "generator": "glTF-Transform v3.10.0", + "generator": "glTF-Transform", "version": "2.0" }, "accessors": [ diff --git a/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf b/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf index 116279cc..072934b7 100644 --- a/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf +++ b/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf @@ -1,6 +1,6 @@ { "asset": { - "generator": "glTF-Transform v3.10.0", + "generator": "glTF-Transform", "version": "2.0" }, "accessors": [ diff --git a/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf b/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf index 92c46ec5..b181233b 100644 --- a/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf +++ b/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf @@ -1,6 +1,6 @@ { "asset": { - "generator": "glTF-Transform v3.10.0", + "generator": "glTF-Transform", "version": "2.0" }, "accessors": [ From 55e05964c28e80c25e3f0d0ceb35a5a8fd253f90 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 20 Apr 2024 18:48:58 +0200 Subject: [PATCH 08/37] Set document generator last --- src/tools/contentProcessing/GltfTransform.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tools/contentProcessing/GltfTransform.ts b/src/tools/contentProcessing/GltfTransform.ts index 8dfe822f..563b20ed 100644 --- a/src/tools/contentProcessing/GltfTransform.ts +++ b/src/tools/contentProcessing/GltfTransform.ts @@ -128,9 +128,6 @@ export class GltfTransform { // Create one document from each buffer and merge them const io = await GltfTransform.getIO(); const mergedDocument = new Document(); - const root = mergedDocument.getRoot(); - const asset = root.getAsset(); - asset.generator = "glTF-Transform"; for (const inputGlbBuffer of inputGlbBuffers) { const inputDocument = await io.readBinary(inputGlbBuffer); mergedDocument.merge(inputDocument); @@ -138,6 +135,9 @@ export class GltfTransform { // Combine all scenes into one await GltfTransform.combineScenes(mergedDocument); await mergedDocument.transform(unpartition()); + const root = mergedDocument.getRoot(); + const asset = root.getAsset(); + asset.generator = "glTF-Transform"; return mergedDocument; } } From 8a9c46831c103c65b7ca390fc70ef90f819f41ff Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 1 May 2024 17:13:07 +0200 Subject: [PATCH 09/37] First draft for merging structural metadata --- .../gltfExtensions/EXTStructuralMetadata.ts | 2 +- .../gltfExtensions/StructuralMetadata.ts | 10 +- .../StructuralMetadataMergeUtilities.ts | 108 +++ .../StructuralMetadataMerger.ts | 719 ++++++++++++++++++ src/tools/index.ts | 1 + 5 files changed, 835 insertions(+), 5 deletions(-) create mode 100644 src/tools/gltfExtensionsUtils/StructuralMetadataMergeUtilities.ts create mode 100644 src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts diff --git a/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts b/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts index a452f977..5f07cd9f 100644 --- a/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts +++ b/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts @@ -995,7 +995,7 @@ export class EXTStructuralMetadata extends Extension { structuralMetadataDef.schema = schemaDef; } const schemaUri = structuralMetadata.getSchemaUri(); - if (schemaUri !== undefined) { + if (schemaUri !== null) { structuralMetadataDef.schemaUri = schemaUri; } diff --git a/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts b/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts index 3da627cd..cc133976 100644 --- a/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts +++ b/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts @@ -25,7 +25,7 @@ const NAME = "EXT_structural_metadata"; interface IStructuralMetadata extends IProperty { schema: Schema; - schemaUri: string; + schemaUri: string | null; propertyTables: PropertyTable[]; propertyTextures: PropertyTexture[]; propertyAttributes: PropertyAttribute[]; @@ -179,6 +179,8 @@ export class StructuralMetadata extends ExtensionProperty { protected override getDefaults() { return Object.assign(super.getDefaults(), { + schema: null, + schemaUri: null, propertyTables: [], propertyTextures: [], propertyAttributes: [], @@ -192,11 +194,11 @@ export class StructuralMetadata extends ExtensionProperty { return this.setRef("schema", schema); } - getSchemaUri(): string { + getSchemaUri(): string | null { return this.get("schemaUri"); } - setSchemaUri(name: string) { - return this.set("schemaUri", name); + setSchemaUri(schemaUri: string | null) { + return this.set("schemaUri", schemaUri); } listPropertyTables(): PropertyTable[] { diff --git a/src/tools/gltfExtensionsUtils/StructuralMetadataMergeUtilities.ts b/src/tools/gltfExtensionsUtils/StructuralMetadataMergeUtilities.ts new file mode 100644 index 00000000..df574612 --- /dev/null +++ b/src/tools/gltfExtensionsUtils/StructuralMetadataMergeUtilities.ts @@ -0,0 +1,108 @@ +// NOTE: The functions in this class are "ported" from +// https://github.com/donmccurdy/glTF-Transform/pull/1375/files +// +// The only exported function is "copyToDocument", which will be replaced +// by the `copyToDocument` functionality of glTF-Transform 4.0 + +import { Document } from "@gltf-transform/core"; +import { Graph } from "@gltf-transform/core"; +import { Property } from "@gltf-transform/core"; +import { PropertyType } from "@gltf-transform/core"; +import { PropertyResolver } from "@gltf-transform/core/dist/properties"; + +const { TEXTURE_INFO, ROOT } = PropertyType; +type PropertyConstructor = new (g: Graph) => Property; +const NO_TRANSFER_TYPES = new Set([TEXTURE_INFO, ROOT]); + +export function copyToDocument( + target: Document, + source: Document, + sourceProperties: Property[], + resolve?: PropertyResolver +): Map { + const sourcePropertyDependencies = new Set(); + for (const property of sourceProperties) { + if (NO_TRANSFER_TYPES.has(property.propertyType)) { + throw new Error(`Type "${property.propertyType}" cannot be transferred.`); + } + listPropertyDependencies(property, sourcePropertyDependencies); + } + return _copyToDocument( + target, + source, + Array.from(sourcePropertyDependencies), + resolve + ); +} + +function _copyToDocument( + target: Document, + source: Document, + sourceProperties: Property[], + resolve?: PropertyResolver +): Map { + resolve ||= createDefaultPropertyResolver(target, source); + + // Create stub classes for every Property in other Document. + const propertyMap = new Map(); + for (const sourceProp of sourceProperties) { + // TextureInfo copy handled by Material or ExtensionProperty. + if ( + !propertyMap.has(sourceProp) && + sourceProp.propertyType !== TEXTURE_INFO + ) { + propertyMap.set(sourceProp, resolve(sourceProp)); + } + } + + // Assemble relationships between Properties. + for (const [sourceProp, targetProp] of propertyMap.entries()) { + targetProp.copy(sourceProp, resolve); + } + + return propertyMap; +} + +function createDefaultPropertyResolver( + target: Document, + source: Document +): PropertyResolver { + const propertyMap = new Map([ + [source.getRoot(), target.getRoot()], + ]); + + return (sourceProp: Property): Property => { + // TextureInfo lifecycle is bound to a Material or ExtensionProperty. + if (sourceProp.propertyType === TEXTURE_INFO) return sourceProp; + + let targetProp = propertyMap.get(sourceProp); + if (!targetProp) { + // Create stub class, defer copying properties. + const PropertyClass = sourceProp.constructor as PropertyConstructor; + targetProp = new PropertyClass(target.getGraph()); + propertyMap.set(sourceProp, targetProp); + } + + return targetProp; + }; +} + +function listPropertyDependencies( + parent: Property, + visited: Set +): Set { + const graph = parent.getGraph(); + const queue: Property[] = [parent]; + + let next: Property | undefined = undefined; + while ((next = queue.pop())) { + visited.add(next); + for (const child of graph.listChildren(next)) { + if (!visited.has(child)) { + queue.push(child); + } + } + } + + return visited; +} diff --git a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts new file mode 100644 index 00000000..e3d994c2 --- /dev/null +++ b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts @@ -0,0 +1,719 @@ +import { Document } from "@gltf-transform/core"; +import { Property } from "@gltf-transform/core"; +import { IProperty } from "@gltf-transform/core"; + +import { StructuralMetadata } from "../../gltf-extensions/"; +import { StructuralMetadataSchema as Schema } from "../../gltf-extensions/"; +import { StructuralMetadataClass as Class } from "../../gltf-extensions/"; +import { EXTStructuralMetadata } from "../../gltf-extensions/"; + +import { copyToDocument } from "./StructuralMetadataMergeUtilities"; + +import { MetadataError } from "../../metadata"; +import { Loggers } from "../../base/"; + +const logger = Loggers.get("gltfExtensionsUtils"); +logger.level = "debug"; + +// TODO This logging is for first drafts/experiments only! +function log(object: any) { + //logger.info(object); + console.log(object); +} + +/** + * A class for merging two glTF-Transform documents that may contain + * the `EXT_structural_metadata` extension. + */ +export class StructuralMetadataMerger { + /** + * Merge two glTF-Transform documents, taking into account that + * they might contain the `EXT_structural_metadata` extension. + * + * This will perform a default `document.merge` operation, but apply + * special treatment for the case that either or both of the documents + * contain the extension: It will merge the top-level extension + * object, and assign the merged one to the root of the target + * document. + * + * @param targetDocument - The target document + * @param sourceDocument - The source document + * @param schemaUriResolver - A function that can resolve the `schemaUri` + * and return the metadata schema JSON object + * @returns A promise that resolves when the operation is finished + */ + static async mergeDocumentsWithStructuralMetadata( + targetDocument: Document, + sourceDocument: Document, + schemaUriResolver: (schemaUri: string) => Promise + ) { + const targetRoot = targetDocument.getRoot(); + const sourceRoot = sourceDocument.getRoot(); + const targetStructuralMetadata = + targetRoot.getExtension("EXT_structural_metadata"); + const sourceStructuralMetadata = + sourceRoot.getExtension("EXT_structural_metadata"); + targetDocument.merge(sourceDocument); + + // Early bailout for the cases where NOT BOTH of the documents + // contain the extension + if ( + targetStructuralMetadata === null && + sourceStructuralMetadata === null + ) { + log( + "Neither source nor target contain structural metadata - nothing to do" + ); + return; + } + + if ( + targetStructuralMetadata !== null && + sourceStructuralMetadata === null + ) { + log("Only target contains structural metadata - nothing to do"); + return; + } + + if ( + targetStructuralMetadata === null && + sourceStructuralMetadata !== null + ) { + log( + "Only source contains structural metadata - copying source to target" + ); + copyToDocument(targetDocument, sourceDocument, [ + sourceStructuralMetadata, + ]); + return; + } + + // The only case where special treatment is required is when + // both documents contain the extension: + if ( + targetStructuralMetadata !== null && + sourceStructuralMetadata !== null + ) { + log("Source and target contain structural metadata - merging"); + await StructuralMetadataMerger.mergeStructuralMetadata( + targetDocument, + targetStructuralMetadata, + sourceDocument, + sourceStructuralMetadata, + schemaUriResolver + ); + } + } + + /** + * Merge the given `EXT_structural_metadata` extension objects from + * the given documents, and assign the result to the given + * target document. + * + * @param targetDocument - The target document + * @param targetStructuralMetadata - The target extension object + * @param sourceDocument - The source document + * @param sourceStructuralMetadata - The source extension object + * @param schemaUriResolver - A function that can resolve the `schemaUri` + * and return the metadata schema JSON object + * @returns A promise that resolves when the operation is finished + */ + private static async mergeStructuralMetadata( + targetDocument: Document, + targetStructuralMetadata: StructuralMetadata, + sourceDocument: Document, + sourceStructuralMetadata: StructuralMetadata, + schemaUriResolver: (schemaUri: string) => Promise + ) { + const targetExtStructuralMetadata = targetDocument.createExtension( + EXTStructuralMetadata + ); + + // Obtain the Schema objects from the source and the target, + // creating the objects from the results of resolving the + // schemaUri where necessary. + + let sourceSchema: Schema | null; + const sourceSchemaUri = sourceStructuralMetadata.getSchemaUri(); + if (sourceSchemaUri !== null) { + const sourceSchemaJson = await schemaUriResolver(sourceSchemaUri); + // TODO Will the fact that the TARGET extension object + // is used here cause trouble when running copyToDocument? + // In any case, the object should NOT be created in the + // source (because the source should not be modified!) + sourceSchema = + targetExtStructuralMetadata.createSchemaFrom(sourceSchemaJson); + } else { + sourceSchema = sourceStructuralMetadata.getSchema(); + } + + let targetSchema: Schema | null; + const targetSchemaUri = targetStructuralMetadata.getSchemaUri(); + if (targetSchemaUri !== null) { + const targetSchemaJson = await schemaUriResolver(targetSchemaUri); + targetSchema = + targetExtStructuralMetadata.createSchemaFrom(targetSchemaJson); + + // If the target schema was resolved from a URI, then assign + // the newly created object to the target, and set the + // schemaUri of the target to null + targetStructuralMetadata.setSchema(targetSchema); + targetStructuralMetadata.setSchemaUri(null); + } else { + targetSchema = targetStructuralMetadata.getSchema(); + } + + if (sourceSchema === null) { + throw new MetadataError("Source schema could not be loaded"); + } + if (targetSchema === null) { + throw new MetadataError("Target schema could not be loaded"); + } + + log("Merging schemas..."); + + const sourceClassNamesInTarget = StructuralMetadataMerger.mergeSchemas( + targetDocument, + targetSchema, + sourceDocument, + sourceSchema + ); + + log("Merging property tables..."); + StructuralMetadataMerger.mergePropertyTables( + targetDocument, + targetStructuralMetadata, + sourceDocument, + sourceStructuralMetadata, + sourceClassNamesInTarget + ); + + log("Merging property textures..."); + StructuralMetadataMerger.mergePropertyTextures( + targetDocument, + targetStructuralMetadata, + sourceDocument, + sourceStructuralMetadata, + sourceClassNamesInTarget + ); + + log("Merging property attributes..."); + StructuralMetadataMerger.mergePropertyAttributes( + targetDocument, + targetStructuralMetadata, + sourceDocument, + sourceStructuralMetadata, + sourceClassNamesInTarget + ); + + const targetRoot = targetDocument.getRoot(); + targetRoot.setExtension( + "EXT_structural_metadata", + targetStructuralMetadata + ); + } + + /** + * Merge the property tables from the given source to the given targets. + * + * This will update the 'class' of the copied property tables according + * to the given name mapping. + * + * @param targetDocument - The target document + * @param targetStructuralMetadata - The target extension object + * @param sourceDocument - The source document + * @param sourceStructuralMetadata - The source extension object + * @param sourceClassNamesInTarget - The mapping from class names in + * the source schema to the names that they have in the target schema. + */ + private static mergePropertyTables( + targetDocument: Document, + targetStructuralMetadata: StructuralMetadata, + sourceDocument: Document, + sourceStructuralMetadata: StructuralMetadata, + sourceClassNamesInTarget: { [key: string]: string } + ) { + const sourcePropertyTables = sourceStructuralMetadata.listPropertyTables(); + const targetPropertyTables = StructuralMetadataMerger.copyArray( + targetDocument, + sourceDocument, + sourcePropertyTables + ); + for (const targetPropertyTable of targetPropertyTables) { + const sourceClassName = targetPropertyTable.getClass(); + const targetClassName = sourceClassNamesInTarget[sourceClassName]; + log( + "Property table referred to class " + + sourceClassName + + " and now refers to class " + + targetClassName + ); + targetPropertyTable.setClass(targetClassName); + targetStructuralMetadata.addPropertyTable(targetPropertyTable); + } + } + + /** + * Merge the property textures from the given source to the given targets. + * + * This will update the 'class' of the copied property textures according + * to the given name mapping. + * + * @param targetDocument - The target document + * @param targetStructuralMetadata - The target extension object + * @param sourceDocument - The source document + * @param sourceStructuralMetadata - The source extension object + * @param sourceClassNamesInTarget - The mapping from class names in + * the source schema to the names that they have in the target schema. + */ + private static mergePropertyTextures( + targetDocument: Document, + targetStructuralMetadata: StructuralMetadata, + sourceDocument: Document, + sourceStructuralMetadata: StructuralMetadata, + sourceClassNamesInTarget: { [key: string]: string } + ) { + const sourcePropertyTextures = + sourceStructuralMetadata.listPropertyTextures(); + const targetPropertyTextures = StructuralMetadataMerger.copyArray( + targetDocument, + sourceDocument, + sourcePropertyTextures + ); + for (const targetPropertyTexture of targetPropertyTextures) { + const sourceClassName = targetPropertyTexture.getClass(); + const targetClassName = sourceClassNamesInTarget[sourceClassName]; + log( + "Property texture referred to class " + + sourceClassName + + " and now refers to class " + + targetClassName + ); + targetPropertyTexture.setClass(targetClassName); + targetStructuralMetadata.addPropertyTexture(targetPropertyTexture); + } + } + + /** + * Merge the property attributes from the given source to the given targets. + * + * This will update the 'class' of the copied property attributes according + * to the given name mapping. + * + * @param targetDocument - The target document + * @param targetStructuralMetadata - The target extension object + * @param sourceDocument - The source document + * @param sourceStructuralMetadata - The source extension object + * @param sourceClassNamesInTarget - The mapping from class names in + * the source schema to the names that they have in the target schema. + */ + private static mergePropertyAttributes( + targetDocument: Document, + targetStructuralMetadata: StructuralMetadata, + sourceDocument: Document, + sourceStructuralMetadata: StructuralMetadata, + sourceClassNamesInTarget: { [key: string]: string } + ) { + const sourcePropertyAttributes = + sourceStructuralMetadata.listPropertyAttributes(); + const targetPropertyAttributes = StructuralMetadataMerger.copyArray( + targetDocument, + sourceDocument, + sourcePropertyAttributes + ); + for (const targetPropertyAttribute of targetPropertyAttributes) { + const sourceClassName = targetPropertyAttribute.getClass(); + const targetClassName = sourceClassNamesInTarget[sourceClassName]; + log( + "Property attribute referred to class " + + sourceClassName + + " and now refers to class " + + targetClassName + ); + targetPropertyAttribute.setClass(targetClassName); + targetStructuralMetadata.addPropertyAttribute(targetPropertyAttribute); + } + } + + /** + * Merge the given `EXT_structural_metadata` schema objects from + * the given documents. + * + * This will merge the enums and classes from the source schema + * into the target schema, performing renaming operations for + * disambiguation as necessary. + * + * The method will return a mapping from class names in the source + * schema to the names that they have in the target schema. + * + * @param targetDocument - The target document + * @param targetSchema - The target schema + * @param sourceDocument - The source document + * @param sourceSchema - The source schema + * @returns A mapping from class names in the source schema to the + * names that they have in the target schema. + */ + private static mergeSchemas( + targetDocument: Document, + targetSchema: Schema, + sourceDocument: Document, + sourceSchema: Schema + ): { [key: string]: string } { + const sourceEnumNamesInTarget = StructuralMetadataMerger.mergeSchemaEnums( + targetDocument, + targetSchema, + sourceDocument, + sourceSchema + ); + const sourceClassNamesInTarget = + StructuralMetadataMerger.mergeSchemaClasses( + targetDocument, + targetSchema, + sourceDocument, + sourceSchema, + sourceEnumNamesInTarget + ); + return sourceClassNamesInTarget; + } + + /** + * Merge the set of enums from given source schema into the given target. + * + * This will perform renaming operations for disambiguation as + * necessary. The method will return a mapping from enum names in + * the source schema to the names that they have in the target schema. + * + * @param targetDocument - The target document + * @param targetSchema - The target schema + * @param sourceDocument - The source document + * @param sourceSchema - The source schema + * @returns A mapping from enum names in the source schema to the + * names that they have in the target schema. + */ + private static mergeSchemaEnums( + targetDocument: Document, + targetSchema: Schema, + sourceDocument: Document, + sourceSchema: Schema + ) { + const sourceEnumNamesInTarget: { [key: string]: string } = {}; + + const sourceEnumKeys = sourceSchema.listEnumKeys(); + const targetEnumKeys = targetSchema.listEnumKeys(); + const allEnumKeys = [...targetEnumKeys]; + for (const sourceEnumKey of sourceEnumKeys) { + const sourceEnum = sourceSchema.getEnum(sourceEnumKey); + if (sourceEnum === null) { + throw new MetadataError("Source Enum " + sourceEnumKey + " not found"); + } + let targetEnum = targetSchema.getEnum(sourceEnumKey); + if (targetEnum === null) { + // The target schema does not yet contain an enum that has + // the same name as the source enum. Copy the source enum + // to the target + targetEnum = StructuralMetadataMerger.copySingle( + targetDocument, + sourceDocument, + sourceEnum + ); + targetSchema.setEnum(sourceEnumKey, targetEnum); + log("Source enum " + sourceEnumKey + " is directly copied to target"); + sourceEnumNamesInTarget[sourceEnumKey] = sourceEnumKey; + } else if (sourceEnum.equals(targetEnum)) { + // The target schema already contains an enum that has the + // same name as the source enum, but it is structurally + // equal. Nothing has to be done here + log( + "Source enum " + + sourceEnumKey + + " is equal to the one that already exists in the target" + ); + sourceEnumNamesInTarget[sourceEnumKey] = sourceEnumKey; + } else { + // The target schema already contains an enum that has the + // same name as the source enum, AND that has a different + // structure. Copy the source enum for the target, but + // store it under a different name + targetEnum = StructuralMetadataMerger.copySingle( + targetDocument, + sourceDocument, + sourceEnum + ); + const targetEnumKey = StructuralMetadataMerger.disambiguate( + sourceEnumKey, + allEnumKeys + ); + allEnumKeys.push(targetEnumKey); + targetSchema.setEnum(targetEnumKey, targetEnum); + log( + "Source enum " + + sourceEnumKey + + " is stored as " + + targetEnumKey + + " in the target" + ); + sourceEnumNamesInTarget[sourceEnumKey] = targetEnumKey; + } + } + return sourceEnumNamesInTarget; + } + + /** + * Merge the set of classes from given source schema into the given target. + * + * This will perform renaming operations for disambiguation as + * necessary. This will include any renamings that are caused by + * enums that have already been renamed: When a source class refers + * to an enum that has been renamed, then the corresponding enumName + * properties will be updated accordingly, and it will be added as + * a new class to the target. + * + * The method will return a mapping from class names in + * the source schema to the names that they have in the target schema. + * + * @param targetDocument - The target document + * @param targetSchema - The target schema + * @param sourceDocument - The source document + * @param sourceSchema - The source schema + * @param sourceEnumNamesInTarget - A mapping from enum names in the + * source schema to the names that they have in the target schema. + * @returns A mapping from class names in the source schema to the + * names that they have in the target schema. + */ + private static mergeSchemaClasses( + targetDocument: Document, + targetSchema: Schema, + sourceDocument: Document, + sourceSchema: Schema, + sourceEnumNamesInTarget: { [key: string]: string } + ) { + const sourceClassNamesInTarget: { [key: string]: string } = {}; + const sourceClassKeys = sourceSchema.listClassKeys(); + const targetClassKeys = targetSchema.listClassKeys(); + const allClassKeys = [...targetClassKeys]; + for (const sourceClassKey of sourceClassKeys) { + const sourceClass = sourceSchema.getClass(sourceClassKey); + if (sourceClass === null) { + throw new MetadataError( + "Source class " + sourceClassKey + " not found" + ); + } + const existingTargetClass = targetSchema.getClass(sourceClassKey); + if (existingTargetClass === null) { + // The target schema does not yet contain a class that has + // the same name as the source class. Copy the source class + // to the target + const targetClass = StructuralMetadataMerger.copySingle( + targetDocument, + sourceDocument, + sourceClass + ); + StructuralMetadataMerger.updateEnumTypes( + targetClass, + sourceEnumNamesInTarget + ); + targetSchema.setClass(sourceClassKey, targetClass); + log( + "Source class " + + sourceClassKey + + " is directly copied to target (possibly with updated enum types)" + ); + sourceClassNamesInTarget[sourceClassKey] = sourceClassKey; + } else { + // The target schema already contains a class that has the + // same name as the source class. + + // If the source class contains a property that has a "enumType" + // that was renamed, then the the target class is created by + // copying the source to the target, updating the respective + // enumType values, and storing it under a new name + if ( + StructuralMetadataMerger.containsRenamedEnumType( + sourceClass, + sourceEnumNamesInTarget + ) + ) { + const targetClass = StructuralMetadataMerger.copySingle( + targetDocument, + sourceDocument, + sourceClass + ); + const targetClassKey = StructuralMetadataMerger.disambiguate( + sourceClassKey, + allClassKeys + ); + allClassKeys.push(targetClassKey); + StructuralMetadataMerger.updateEnumTypes( + targetClass, + sourceEnumNamesInTarget + ); + targetSchema.setClass(targetClassKey, targetClass); + log( + "Source class " + + sourceClassKey + + " is copied to target due to renamed enum types, and stored as " + + targetClassKey + ); + sourceClassNamesInTarget[sourceClassKey] = targetClassKey; + } else { + // The target schema already contains a class that has the + // same name as the source class, and does not involve any + // renamed enumType + if (sourceClass.equals(existingTargetClass)) { + // When the source class and the existing target class + // are equal, then nothing has to be done + log("Source class " + sourceClassKey + " already exists "); + sourceClassNamesInTarget[sourceClassKey] = sourceClassKey; + } else { + // Otherwise, the source class is copied to the target and + // stored under a different name + const targetClass = StructuralMetadataMerger.copySingle( + targetDocument, + sourceDocument, + sourceClass + ); + const targetClassKey = StructuralMetadataMerger.disambiguate( + sourceClassKey, + allClassKeys + ); + allClassKeys.push(targetClassKey); + StructuralMetadataMerger.updateEnumTypes( + targetClass, + sourceEnumNamesInTarget + ); + targetSchema.setClass(targetClassKey, targetClass); + log( + "Source class " + + sourceClassKey + + " is copied to target and stored as " + + targetClassKey + ); + sourceClassNamesInTarget[sourceClassKey] = targetClassKey; + } + } + } + } + return sourceClassNamesInTarget; + } + + /** + * Returns whether the given source class contains an enum-typed + * property where the enum was renamed, according to the given + * name mapping + * + * @param sourceClass - The source class + * @param sourceEnumNamesInTarget - A mapping from enum names in the + * source schema to the names that they have in the target schema. + * @returns Whether the class contains a renamed enum type + */ + private static containsRenamedEnumType( + sourceClass: Class, + sourceEnumNamesInTarget: { [key: string]: string } + ) { + const sourcePropertyKeys = sourceClass.listPropertyKeys(); + for (const sourcePropertyKey of sourcePropertyKeys) { + const sourceProperty = sourceClass.getProperty(sourcePropertyKey); + if (sourceProperty) { + const sourceEnumType = sourceProperty.getEnumType(); + if (sourceEnumType !== undefined) { + const targetEnumType = sourceEnumNamesInTarget[sourceEnumType]; + if (targetEnumType !== sourceEnumType) { + return true; + } + } + } + } + return false; + } + + /** + * Updates the enumType of all enum-typed properties in the given + * class, based on the given name mapping. + * + * @param targetClass - The target class + * @param sourceEnumNamesInTarget - A mapping from enum names in the + * source schema to the names that they have in the target schema. + */ + private static updateEnumTypes( + targetClass: Class, + sourceEnumNamesInTarget: { [key: string]: string } + ) { + const targetPropertyKeys = targetClass.listPropertyKeys(); + for (const targetPropertyKey of targetPropertyKeys) { + const targetProperty = targetClass.getProperty(targetPropertyKey); + if (targetProperty) { + const sourceEnumType = targetProperty.getEnumType(); + if (sourceEnumType !== undefined) { + const targetEnumType = sourceEnumNamesInTarget[sourceEnumType]; + targetProperty.setEnumType(targetEnumType); + } + } + } + } + + /** + * Copy a single object from the given source document to the + * given target document, and return the copy. + * + * @param targetDocument - The target document + * @param sourceDocument - The source document + * @param sourceElement - The source object + * @returns The target object + */ + private static copySingle>( + targetDocument: Document, + sourceDocument: Document, + sourceElement: T + ): T { + const mapping = copyToDocument(targetDocument, sourceDocument, [ + sourceElement, + ]); + const targetElement = mapping.get(sourceElement) as T; + return targetElement; + } + + /** + * Copy an array of objects from the given source document to the + * given target document, and return the copies. + * + * @param targetDocument - The target document + * @param sourceDocument - The source document + * @param sourceElement - The source objects + * @returns The target objects + */ + private static copyArray>( + targetDocument: Document, + sourceDocument: Document, + sourceElements: T[] + ): T[] { + const mapping = copyToDocument( + targetDocument, + sourceDocument, + sourceElements + ); + const targetElements = sourceElements.map((e: T) => mapping.get(e) as T); + return targetElements; + } + + /** + * Disambiguate the given name against the existing names. + * + * This will return a name that is based on the given input + * name, but that does NOT yet appear in the given existing + * names. It will NOT add the resulting name to the given + * set. The exact disambiguation strategy is not specified. + * + * @param s - The input name + * @param existing - The existing name + * @returns - The disambiguated name + */ + private static disambiguate(s: string, existing: string[]) { + let result = s; + let counter = 0; + while (existing.includes(result)) { + result = s + "_" + counter; + counter++; + } + return result; + } +} diff --git a/src/tools/index.ts b/src/tools/index.ts index 23dc24f6..29d28cc7 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -76,3 +76,4 @@ export * from "./gltfExtensionsUtils/MeshFeaturesUtils"; export * from "./gltfExtensionsUtils/StringBuilder"; export * from "./gltfExtensionsUtils/StructuralMetadataPropertyTables"; export * from "./gltfExtensionsUtils/StructuralMetadataUtils"; +export * from "./gltfExtensionsUtils/StructuralMetadataMerger"; From 0aef5e74c880d0eaec6c3cba961f622eb2ee6535 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 2 May 2024 17:16:18 +0200 Subject: [PATCH 10/37] Minor formatting fix for debug output --- src/tools/gltfExtensionsUtils/StructuralMetadataUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/gltfExtensionsUtils/StructuralMetadataUtils.ts b/src/tools/gltfExtensionsUtils/StructuralMetadataUtils.ts index 7960f65f..e7559851 100644 --- a/src/tools/gltfExtensionsUtils/StructuralMetadataUtils.ts +++ b/src/tools/gltfExtensionsUtils/StructuralMetadataUtils.ts @@ -238,7 +238,7 @@ export class StructuralMetadataUtils { StructuralMetadataUtils.createClassString(sb, classObject); sb.decreaseIndent(); } - sb.increaseIndent(); + sb.decreaseIndent(); } } From b67053a4abb3f6faa7dcdbf071d62cdf08925b2e Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 2 May 2024 17:16:53 +0200 Subject: [PATCH 11/37] Assign new schema ID when schema was modified --- .../gltfExtensionsUtils/StructuralMetadataMerger.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts index e3d994c2..64404448 100644 --- a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts +++ b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts @@ -1,3 +1,5 @@ +import crypto from "crypto"; + import { Document } from "@gltf-transform/core"; import { Property } from "@gltf-transform/core"; import { IProperty } from "@gltf-transform/core"; @@ -171,6 +173,8 @@ export class StructuralMetadataMerger { } log("Merging schemas..."); + const oldClassKeys = targetSchema.listClassKeys(); + const oldEnumKeys = targetSchema.listEnumKeys(); const sourceClassNamesInTarget = StructuralMetadataMerger.mergeSchemas( targetDocument, @@ -179,6 +183,15 @@ export class StructuralMetadataMerger { sourceSchema ); + const newClassKeys = targetSchema.listClassKeys(); + const newEnumKeys = targetSchema.listEnumKeys(); + if (oldClassKeys != newClassKeys || oldEnumKeys != newEnumKeys) { + const newId = "SCHEMA-ID-" + crypto.randomUUID(); + log("Target schema was modified - assigning ID "+newId); + targetSchema.setId(newId) + } + + log("Merging property tables..."); StructuralMetadataMerger.mergePropertyTables( targetDocument, From 236a9a3f748783585eb7ed7e8f9fe30dbb0f3cde Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 2 May 2024 17:39:19 +0200 Subject: [PATCH 12/37] Formatting --- src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts index 64404448..906ca83a 100644 --- a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts +++ b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts @@ -187,11 +187,10 @@ export class StructuralMetadataMerger { const newEnumKeys = targetSchema.listEnumKeys(); if (oldClassKeys != newClassKeys || oldEnumKeys != newEnumKeys) { const newId = "SCHEMA-ID-" + crypto.randomUUID(); - log("Target schema was modified - assigning ID "+newId); - targetSchema.setId(newId) + log("Target schema was modified - assigning ID " + newId); + targetSchema.setId(newId); } - log("Merging property tables..."); StructuralMetadataMerger.mergePropertyTables( targetDocument, From f65be20d8c5fcf0e2b512e500435d2fa3d55d582 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 2 May 2024 17:39:45 +0200 Subject: [PATCH 13/37] First pass of specs for metadata merging --- .../StructuralMetadataMergerSpec.ts | 608 ++++++++++++++++++ 1 file changed, 608 insertions(+) create mode 100644 specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts diff --git a/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts b/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts new file mode 100644 index 00000000..fa53b307 --- /dev/null +++ b/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts @@ -0,0 +1,608 @@ +import { Document } from "@gltf-transform/core"; + +import { EXTStructuralMetadata } from "../../../src/gltf-extensions"; +import { StructuralMetadata } from "../../../src/gltf-extensions"; +import { StructuralMetadataSchema as Schema } from "../../../src/gltf-extensions"; +import { StructuralMetadataClass as Class } from "../../../src/gltf-extensions"; + +import { BinaryPropertyTableBuilder } from "../../../src/metadata/"; + +import { StructuralMetadataPropertyTables } from "../../../src/tools/"; +import { StructuralMetadataMerger } from "../../../src/tools/"; + +// A dummy resolver for the `schemaUri` that may be found in metadata +const specSchemaUriResolver = async (schemaUri: string) => { + console.error("The specSchemaUriResolver should not be called!"); + const schema = { + id: "SPEC_SCHEMA_FROM_URI_" + schemaUri, + }; + return schema; +}; + +/** + * Returns the Schema from the `EXT_structural_metadata` extension in + * the given document, or `null` if the extension does not exist or + * it does not contain a Schema. + * + * @param document - The glTF-Transform document + * @returns The metadata schema + */ +function getMetadataSchema(document: Document): Schema | null { + const root = document.getRoot(); + const structuralMetadata = root.getExtension( + "EXT_structural_metadata" + ); + if (structuralMetadata === null) { + return null; + } + const schema = structuralMetadata.getSchema(); + return schema; +} + +/** + * Returns the names of the classes in the Schema of the + * `EXT_structural_metadata` extension in the given document, + * or an empty array if the extension or schema do not + * exist + * + * @param document - The glTF-Transform document + * @returns The metadata class names + */ +function getMetadataClassNames(document: Document): string[] { + const schema = getMetadataSchema(document); + if (schema === null) { + return []; + } + const classKeys = schema.listClassKeys(); + return classKeys; +} + +/** + * Returns the names of the enums in the Schema of the + * `EXT_structural_metadata` extension in the given document, + * or an empty array if the extension or schema do not + * exist + * + * @param document - The glTF-Transform document + * @returns The metadata enum names + */ +function getMetadataEnumNames(document: Document): string[] { + const schema = getMetadataSchema(document); + if (schema === null) { + return []; + } + const enumKeys = schema.listEnumKeys(); + return enumKeys; +} + +/** + * Returns the class with the specified name from the schema + * of the structural metadata extension, throwing up if it + * does not exist + * + * @param document - The glTF-Transform document + * @returns The metadata class + */ +function getMetadataClass(document: Document, className: string): Class { + const schema = getMetadataSchema(document); + if (schema === null) { + throw new Error("Document does not contain metadata with schema"); + } + const classObject = schema.getClass(className); + if (classObject == null) { + throw new Error("Document does not contain metadata class " + className); + } + return classObject; +} + +/** + * Returns the number of property tables of the `EXT_structural_metadata` + * extension in the given document, or -1 if the extension does not + * exist. + * + * @param document - The glTF-Transform document + * @returns The number of property tables + */ +function getNumPropertyTables(document: Document): number { + const root = document.getRoot(); + const structuralMetadata = root.getExtension( + "EXT_structural_metadata" + ); + if (structuralMetadata === null) { + return -1; + } + const propertyTables = structuralMetadata.listPropertyTables(); + return propertyTables.length; +} + +/** + * Obtain the StructuralMetadata object from the given document, creating + * it if it did not exist yet. + * + * @param document - The document + * @returns The StructuralMetadata object + */ +function obtainStructuralMetadata(document: Document): StructuralMetadata { + const extStructuralMetadata = document.createExtension(EXTStructuralMetadata); + const root = document.getRoot(); + let structuralMetadata = root.getExtension( + "EXT_structural_metadata" + ); + if (structuralMetadata === null) { + structuralMetadata = extStructuralMetadata.createStructuralMetadata(); + root.setExtension( + "EXT_structural_metadata", + structuralMetadata + ); + } + return structuralMetadata; +} + +/** + * Assign a Schema to the StructuralMetadata in the given document, + * creating the StructuralMetadata if it did not exist yet + * + * @param document - The document + * @param schemaJson - The schema JSON + */ +function assignMetadataSchema(document: Document, schemaJson: any) { + const extStructuralMetadata = document.createExtension(EXTStructuralMetadata); + const structuralMetadata = obtainStructuralMetadata(document); + const metadataSchema = extStructuralMetadata.createSchemaFrom(schemaJson); + structuralMetadata.setSchema(metadataSchema); +} + +/** + * Add a property table to the StructuralMetadata in the given document. + * + * The property table JSON is assumed to be an object that has the following + * properties: + * + * - `className`: The name of a class from the schema + * - `properties`: A dictionary that maps names of class properties to + * arrays of values that should be put into the table column + * + * @param document - The document + * @param schemaJson - The schema JSON + * @param propertyTableJson - The property table JSON + */ +function addPropertyTable( + document: Document, + schemaJson: any, + propertyTableJson: any +) { + const extStructuralMetadata = document.createExtension(EXTStructuralMetadata); + const structuralMetadata = obtainStructuralMetadata(document); + const className = propertyTableJson.class; + const b = BinaryPropertyTableBuilder.create( + schemaJson, + className, + "Property Table" + ); + for (const p of Object.keys(propertyTableJson.properties)) { + const v = propertyTableJson.properties[p]; + b.addProperty(p, v); + } + const binaryPropertyTable = b.build(); + const propertyTable = StructuralMetadataPropertyTables.create( + extStructuralMetadata, + binaryPropertyTable + ); + structuralMetadata.addPropertyTable(propertyTable); +} + +fdescribe("StructuralMetadataMerger", function () { + //========================================================================== + // Basic class merging + + it("creates one class if the input classes have the same name/key and they are structurally equal", async function () { + // The classes have the same name/key + // The classes are structurally equal + // The result should be: + // One class + + const schemaA = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_STRING: { + name: "Example STRING property", + type: "STRING", + }, + }, + }, + }, + }; + + const schemaB = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_STRING: { + name: "Example STRING property", + type: "STRING", + }, + }, + }, + }, + }; + + const documentA = new Document(); + documentA.createBuffer(); + assignMetadataSchema(documentA, schemaA); + + const documentB = new Document(); + documentB.createBuffer(); + assignMetadataSchema(documentB, schemaB); + + const document = new Document(); + StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentA, + specSchemaUriResolver + ); + + // After merging in the first document: + // There should be one class + expect(getMetadataClassNames(document).length).toBe(1); + + StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentB, + specSchemaUriResolver + ); + + // After merging in the second document: + // There should still be one class + expect(getMetadataClassNames(document).length).toBe(1); + }); + + it("creates two classes if the input classes have different keys/names (even though they are structurally equal)", async function () { + // The classes have different names/keys + // The classes are structurally equal + // The result should be: + // Two classes + + const schemaA = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_STRING: { + name: "Example STRING property", + type: "STRING", + }, + }, + }, + }, + }; + + const schemaB = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + // Different name here! + exampleClassButWithDifferentName: { + name: "Example Class", + properties: { + example_STRING: { + name: "Example STRING property", + type: "STRING", + }, + }, + }, + }, + }; + + const documentA = new Document(); + documentA.createBuffer(); + assignMetadataSchema(documentA, schemaA); + + const documentB = new Document(); + documentB.createBuffer(); + assignMetadataSchema(documentB, schemaB); + + const document = new Document(); + StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentA, + specSchemaUriResolver + ); + + // After merging in the first document: + // There should be one class + expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); + + StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentB, + specSchemaUriResolver + ); + + // After merging in the second document: + // There should be two classes + expect(getMetadataClassNames(document).sort()).toEqual( + ["exampleClass", "exampleClassButWithDifferentName"].sort() + ); + }); + + it("creates two classes if the input classes have the same keys/names but are NOT structurally equal", async function () { + // The classes have the same name/key + // The classes are structurally different + // The result should be: + // Two classes (one disambiguated) + + const schemaA = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_STRING: { + name: "Example STRING property", + type: "STRING", + }, + }, + }, + }, + }; + + const schemaB = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class but with a difference", // Structural difference! + properties: { + example_STRING: { + name: "Example STRING property", + type: "STRING", + }, + }, + }, + }, + }; + + const documentA = new Document(); + documentA.createBuffer(); + assignMetadataSchema(documentA, schemaA); + + const documentB = new Document(); + documentB.createBuffer(); + assignMetadataSchema(documentB, schemaB); + + const document = new Document(); + StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentA, + specSchemaUriResolver + ); + + // After merging in the first document: + // There should be one class + expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); + + StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentB, + specSchemaUriResolver + ); + + // After merging in the second document: + // There should be two classes, with the second one have a disambiguated name/key + expect(getMetadataClassNames(document).sort()).toEqual( + ["exampleClass", "exampleClass_0"].sort() + ); + }); + + it("creates two classes if the input classes have the same keys/names and appear to be structurally equal, but are no longer equal after disambiguating enums", async function () { + // The enums have the same name/key + // The enums are structurally different + // The classes have the same name/key + // The classes are structurally equal INITIALLY, + // but are different after disambiguating the enums + // The result should be: + // Two enums (one disambiguated) + // Two classes (one disambiguated) + + const schemaA = { + id: "EXAMPLE_SCHEMA_ID", + enums: { + exampleEnum: { + valueType: "UINT8", + values: [ + { + name: "EXAMPLE_ENUM_VALUE_A", + value: 12, + }, + { + name: "EXAMPLE_ENUM_VALUE_B", + value: 34, + }, + ], + }, + }, + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_ENUM: { + name: "Example ENUM property", + type: "ENUM", + enumType: "exampleEnum", + }, + }, + }, + }, + }; + + const schemaB = { + id: "EXAMPLE_SCHEMA_ID", + enums: { + exampleEnum: { + valueType: "UINT8", + values: [ + { + name: "EXAMPLE_ENUM_VALUE_A", + value: 0, + }, + { + name: "EXAMPLE_ENUM_VALUE_B", + value: 1, + }, + ], + }, + }, + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_ENUM: { + name: "Example ENUM property", + type: "ENUM", + enumType: "exampleEnum", + }, + }, + }, + }, + }; + + const documentA = new Document(); + documentA.createBuffer(); + assignMetadataSchema(documentA, schemaA); + + const documentB = new Document(); + documentB.createBuffer(); + assignMetadataSchema(documentB, schemaB); + + const document = new Document(); + StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentA, + specSchemaUriResolver + ); + + // After merging in the first document: + // There should be one enum and one class + expect(getMetadataEnumNames(document)).toEqual(["exampleEnum"]); + expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); + + StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentB, + specSchemaUriResolver + ); + + // After merging in the second document: + // There should be two enums and two classes, + // with the second enum and class each having a disambiguated name/key + expect(getMetadataEnumNames(document).sort()).toEqual( + ["exampleEnum", "exampleEnum_0"].sort() + ); + expect(getMetadataClassNames(document).sort()).toEqual( + ["exampleClass", "exampleClass_0"].sort() + ); + + // Expect the enumType of the property to be updated for + // the disambiguated class, according to the disambiguated + // enum name + const exampleClass_0 = getMetadataClass(document, "exampleClass_0"); + const property = exampleClass_0.getProperty("example_ENUM"); + expect(property?.getEnumType()).toEqual("exampleEnum_0"); + }); + + //========================================================================== + // Basic property table merging + + // TODO: + it("works", async function () { + // One schema (with one class) for each document. + // The schemas (i.e. their classes) are structurally equal. + // One property table for each document. + // The result should be + // - one schema with one class + // - two property tables + + const schemaA = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_STRING: { + name: "Example STRING property", + type: "STRING", + }, + }, + }, + }, + }; + + const schemaB = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_STRING: { + name: "Example STRING property", + type: "STRING", + }, + }, + }, + }, + }; + + const propertyTableA = { + class: "exampleClass", + properties: { + example_STRING: ["This", "is", "an", "example"], + }, + }; + + const propertyTableB = { + class: "exampleClass", + properties: { + example_STRING: ["Yet", "another", "example", "table"], + }, + }; + + const documentA = new Document(); + documentA.createBuffer(); + assignMetadataSchema(documentA, schemaA); + addPropertyTable(documentA, schemaA, propertyTableA); + + const documentB = new Document(); + documentB.createBuffer(); + assignMetadataSchema(documentB, schemaB); + addPropertyTable(documentB, schemaB, propertyTableB); + + const document = new Document(); + StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentA, + specSchemaUriResolver + ); + + // After merging in the first document: + // There should be one class + // There should be one property table + expect(getMetadataClassNames(document).length).toBe(1); + expect(getNumPropertyTables(document)).toBe(1); + + StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentB, + specSchemaUriResolver + ); + + // After merging in the second document: + // There should still be one class + // There should be two property tables + expect(getMetadataClassNames(document).length).toBe(1); + expect(getNumPropertyTables(document)).toBe(2); + }); +}); From 70fa8b606187c8d0732ba686fe56c1e76547e5a2 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 3 May 2024 15:52:55 +0200 Subject: [PATCH 14/37] First spec for merging property tables --- .../StructuralMetadataMergerSpec.ts | 136 ++++++++++++++++-- 1 file changed, 126 insertions(+), 10 deletions(-) diff --git a/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts b/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts index fa53b307..044ea2fc 100644 --- a/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts +++ b/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts @@ -4,6 +4,7 @@ import { EXTStructuralMetadata } from "../../../src/gltf-extensions"; import { StructuralMetadata } from "../../../src/gltf-extensions"; import { StructuralMetadataSchema as Schema } from "../../../src/gltf-extensions"; import { StructuralMetadataClass as Class } from "../../../src/gltf-extensions"; +import { StructuralMetadataPropertyTable as PropertyTable } from "../../../src/gltf-extensions"; import { BinaryPropertyTableBuilder } from "../../../src/metadata/"; @@ -115,6 +116,27 @@ function getNumPropertyTables(document: Document): number { return propertyTables.length; } +/** + * Returns the property table with the given index from the + * `EXT_structural_metadata`, throwing up if this does not + * exist. + * + * @param document - The glTF-Transform document + * @param index The index of the property table + * @returns The property table + */ +function getPropertyTable(document: Document, index: number): PropertyTable { + const root = document.getRoot(); + const structuralMetadata = root.getExtension( + "EXT_structural_metadata" + ); + if (structuralMetadata === null) { + throw new Error("Document does not contain metadata"); + } + const propertyTables = structuralMetadata.listPropertyTables(); + return propertyTables[index]; +} + /** * Obtain the StructuralMetadata object from the given document, creating * it if it did not exist yet. @@ -517,14 +539,13 @@ fdescribe("StructuralMetadataMerger", function () { //========================================================================== // Basic property table merging - // TODO: - it("works", async function () { - // One schema (with one class) for each document. - // The schemas (i.e. their classes) are structurally equal. - // One property table for each document. - // The result should be - // - one schema with one class - // - two property tables + it("merges the set of property tables for one resulting class", async function () { + // The classes have the same name/key + // The classes are structurally equal + // The property tables refer to the respective class (which turns out to be equal) + // The result should be: + // One class + // Two property tables const schemaA = { id: "EXAMPLE_SCHEMA_ID", @@ -590,7 +611,7 @@ fdescribe("StructuralMetadataMerger", function () { // After merging in the first document: // There should be one class // There should be one property table - expect(getMetadataClassNames(document).length).toBe(1); + expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); expect(getNumPropertyTables(document)).toBe(1); StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( @@ -602,7 +623,102 @@ fdescribe("StructuralMetadataMerger", function () { // After merging in the second document: // There should still be one class // There should be two property tables - expect(getMetadataClassNames(document).length).toBe(1); + expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); + expect(getNumPropertyTables(document)).toBe(2); + }); + + it("merges the set of property tables and updates their class for disambiguated classes", async function () { + // The classes have the same name/key + // The classes are structurally equal + // The property tables refer to the respective class (which turns out to be equal) + // The result should be: + // One class + // Two property tables + + const schemaA = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_STRING: { + name: "Example STRING property", + type: "STRING", + }, + }, + }, + }, + }; + + const schemaB = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class but with a difference", // Structural difference! + properties: { + example_STRING: { + name: "Example STRING property", + type: "STRING", + }, + }, + }, + }, + }; + + const propertyTableA = { + class: "exampleClass", + properties: { + example_STRING: ["This", "is", "an", "example"], + }, + }; + + const propertyTableB = { + class: "exampleClass", + properties: { + example_STRING: ["Yet", "another", "example", "table"], + }, + }; + + const documentA = new Document(); + documentA.createBuffer(); + assignMetadataSchema(documentA, schemaA); + addPropertyTable(documentA, schemaA, propertyTableA); + + const documentB = new Document(); + documentB.createBuffer(); + assignMetadataSchema(documentB, schemaB); + addPropertyTable(documentB, schemaB, propertyTableB); + + const document = new Document(); + StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentA, + specSchemaUriResolver + ); + + // After merging in the first document: + // There should be one class + // There should be one property table + expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); + expect(getNumPropertyTables(document)).toBe(1); + + StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentB, + specSchemaUriResolver + ); + + // After merging in the second document: + // There should be two classes + // There should be two property tables + expect(getMetadataClassNames(document).sort()).toEqual( + ["exampleClass", "exampleClass_0"].sort() + ); expect(getNumPropertyTables(document)).toBe(2); + + // Expect the second property table to refer to the + // renamed class + const propertyTable1 = getPropertyTable(document, 1); + expect(propertyTable1.getClass()).toBe("exampleClass_0"); }); }); From 70ca742bfd08ea083f1b003e4f950dc2abdafd24 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 3 May 2024 17:06:48 +0200 Subject: [PATCH 15/37] Fix handling of optionals in instance features --- .../gltfExtensions/EXTInstanceFeatures.ts | 6 ++--- .../gltfExtensions/InstanceFeatures.ts | 25 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/gltf-extensions/gltfExtensions/EXTInstanceFeatures.ts b/src/gltf-extensions/gltfExtensions/EXTInstanceFeatures.ts index f894b33e..461f5059 100644 --- a/src/gltf-extensions/gltfExtensions/EXTInstanceFeatures.ts +++ b/src/gltf-extensions/gltfExtensions/EXTInstanceFeatures.ts @@ -159,9 +159,9 @@ export class EXTInstanceFeatures extends Extension { } const featureIdDef: FeatureIdDef = { featureCount: featureId.getFeatureCount(), - nullFeatureId: featureId.getNullFeatureId(), - label: featureId.getLabel(), - attribute: featureId.getAttribute(), + nullFeatureId: featureId.getNullFeatureId() ?? undefined, + label: featureId.getLabel() ?? undefined, + attribute: featureId.getAttribute() ?? undefined, propertyTable: propertyTableDef, }; return featureIdDef; diff --git a/src/gltf-extensions/gltfExtensions/InstanceFeatures.ts b/src/gltf-extensions/gltfExtensions/InstanceFeatures.ts index a37d41ae..fe29e1a7 100644 --- a/src/gltf-extensions/gltfExtensions/InstanceFeatures.ts +++ b/src/gltf-extensions/gltfExtensions/InstanceFeatures.ts @@ -15,9 +15,9 @@ interface IInstanceFeatures extends IProperty { } interface IFeatureId extends IProperty { featureCount: number; - nullFeatureId: number; - label: string; - attribute: FeatureIdAttribute; + nullFeatureId: number | null; + label: string | null; + attribute: FeatureIdAttribute | null; propertyTable: PropertyTable; } type FeatureIdAttribute = number; @@ -80,7 +80,12 @@ export class FeatureId extends ExtensionProperty { } protected override getDefaults() { - return Object.assign(super.getDefaults(), {}); + return Object.assign(super.getDefaults(), { + nullFeatureId: null, + label: null, + attribute: null, + propertyTable: null + }); } getFeatureCount(): number { @@ -90,24 +95,24 @@ export class FeatureId extends ExtensionProperty { return this.set("featureCount", featureCount); } - getNullFeatureId(): number { + getNullFeatureId(): number | null { return this.get("nullFeatureId"); } - setNullFeatureId(nullFeatureId: number) { + setNullFeatureId(nullFeatureId: number | null) { return this.set("nullFeatureId", nullFeatureId); } - getLabel(): string { + getLabel(): string | null { return this.get("label"); } - setLabel(label: string) { + setLabel(label: string | null) { return this.set("label", label); } - getAttribute(): FeatureIdAttribute { + getAttribute(): FeatureIdAttribute | null { return this.get("attribute"); } - setAttribute(attribute: FeatureIdAttribute) { + setAttribute(attribute: FeatureIdAttribute | null) { return this.set("attribute", attribute); } From b77bc432468b33e39ba9dd8e6374d8c8d4455a7b Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 3 May 2024 17:07:07 +0200 Subject: [PATCH 16/37] Fix handling of optionals in mesh features. Comments. --- .../gltfExtensions/EXTMeshFeatures.ts | 6 +-- .../gltfExtensions/MeshFeatures.ts | 39 +++++++++++++------ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/gltf-extensions/gltfExtensions/EXTMeshFeatures.ts b/src/gltf-extensions/gltfExtensions/EXTMeshFeatures.ts index 545aa133..522019fa 100644 --- a/src/gltf-extensions/gltfExtensions/EXTMeshFeatures.ts +++ b/src/gltf-extensions/gltfExtensions/EXTMeshFeatures.ts @@ -311,9 +311,9 @@ export class EXTMeshFeatures extends Extension { const featureIdDef: FeatureIdDef = { featureCount: featureId.getFeatureCount(), - nullFeatureId: featureId.getNullFeatureId(), - label: featureId.getLabel(), - attribute: featureId.getAttribute(), + nullFeatureId: featureId.getNullFeatureId() ?? undefined, + label: featureId.getLabel() ?? undefined, + attribute: featureId.getAttribute() ?? undefined, texture: textureDef, propertyTable: propertyTableDef, }; diff --git a/src/gltf-extensions/gltfExtensions/MeshFeatures.ts b/src/gltf-extensions/gltfExtensions/MeshFeatures.ts index 0e31faf7..cf0610bd 100644 --- a/src/gltf-extensions/gltfExtensions/MeshFeatures.ts +++ b/src/gltf-extensions/gltfExtensions/MeshFeatures.ts @@ -32,14 +32,25 @@ const NAME = "EXT_mesh_features"; // These interfaces are NOT publicly visible. They only serve as the type // pararameter for the `ExtensionProperty` class, which is the base // for the actual "model" classes that are exposed to the user. +// +// In these interfaces, optional properties are generally represented +// with the type being `... | null` when they have primitive types. +// In the model classes, the respective getters and setters will +// return/accept the `... null` type accordingly. +// +// For "reference typed" properties (i.e. references to other model +// classes), the fact that they are optional is built-in into the +// reference type itself: The corresponding methods of the model +// classes will return or accept the `...|null` type automatically. + interface IMeshFeatures extends IProperty { featureIds: FeatureId[]; } interface IFeatureId extends IProperty { featureCount: number; - nullFeatureId: number; - label: string; - attribute: FeatureIdAttribute; + nullFeatureId: number | null; + label: string | null; + attribute: FeatureIdAttribute | null; texture: FeatureIdTexture; propertyTable: PropertyTable; } @@ -74,7 +85,7 @@ interface IFeatureIdTexture extends IProperty { // Each texture in these classes is modeled as a property with the // type `Texture`, and an associated `TextureInfo`. The `TextureInfo` // can only be accessed with a `get` method, but not explicitly -// set: It is managed internally by glTF-Transform. So the for +// set: It is managed internally by glTF-Transform. So for // an `exampleTextureInfo: TextureInfo` property, there will only // be a getter, implemented as // ``` @@ -136,7 +147,13 @@ export class FeatureId extends ExtensionProperty { } protected override getDefaults() { - return Object.assign(super.getDefaults(), {}); + return Object.assign(super.getDefaults(), { + nullFeatureId: null, + label: null, + attribute: null, + texture: null, + propertyTable: null + }); } getFeatureCount(): number { @@ -146,24 +163,24 @@ export class FeatureId extends ExtensionProperty { return this.set("featureCount", featureCount); } - getNullFeatureId(): number { + getNullFeatureId(): number | null { return this.get("nullFeatureId"); } - setNullFeatureId(nullFeatureId: number) { + setNullFeatureId(nullFeatureId: number | null) { return this.set("nullFeatureId", nullFeatureId); } - getLabel(): string { + getLabel(): string | null { return this.get("label"); } - setLabel(label: string) { + setLabel(label: string | null) { return this.set("label", label); } - getAttribute(): FeatureIdAttribute { + getAttribute(): FeatureIdAttribute | null{ return this.get("attribute"); } - setAttribute(attribute: FeatureIdAttribute) { + setAttribute(attribute: FeatureIdAttribute | null) { return this.set("attribute", attribute); } From 3ba324a6aff68bc3537eff5dbd40228ab7f6008c Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 3 May 2024 17:16:40 +0200 Subject: [PATCH 17/37] Handling of optionals in structural metadata - WIP --- .../gltfExtensions/EXTStructuralMetadata.ts | 38 +++---- .../gltfExtensions/StructuralMetadata.ts | 100 ++++++++++-------- 2 files changed, 77 insertions(+), 61 deletions(-) diff --git a/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts b/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts index 5f07cd9f..ac9fc8ed 100644 --- a/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts +++ b/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts @@ -28,16 +28,16 @@ const NAME = "EXT_structural_metadata"; * Returns the given value if it is **NOT** equal to the given * default value. * - * Returns `undefined` if the given value **IS** equal to the + * Returns `null` if the given value **IS** equal to the * given default value. * * @param value - The value * @param defaultValue - The default value * @returns The result */ -function ifNot(value: T, defaultValue: T): T | undefined { +function ifNot(value: T, defaultValue: T): T | null { if (value === defaultValue) { - return undefined; + return null; } return value; } @@ -1069,9 +1069,9 @@ export class EXTStructuralMetadata extends Extension { const schemaDef: SchemaDef = { id: schema.getId(), - name: schema.getObjectName(), - description: schema.getDescription(), - version: schema.getVersion(), + name: schema.getObjectName() ?? undefined, + description: schema.getDescription() ?? undefined, + version: schema.getVersion() ?? undefined, classes: classes, enums: enums, }; @@ -1095,8 +1095,8 @@ export class EXTStructuralMetadata extends Extension { } const classDef: ClassDef = { - name: classObject.getObjectName(), - description: classObject.getDescription(), + name: classObject.getObjectName() ?? undefined, + description: classObject.getDescription() ?? undefined, properties: properties, }; return classDef; @@ -1106,19 +1106,19 @@ export class EXTStructuralMetadata extends Extension { classProperty: ClassProperty ): ClassPropertyDef { const classPropertyDef: ClassPropertyDef = { - name: classProperty.getObjectName(), - description: classProperty.getDescription(), + name: classProperty.getObjectName() ?? undefined, + description: classProperty.getDescription() ?? undefined, type: classProperty.getType(), - componentType: classProperty.getComponentType(), - enumType: classProperty.getEnumType(), - array: ifNot(classProperty.getArray(), false), - count: classProperty.getCount(), - normalized: ifNot(classProperty.getNormalized(), false), + componentType: classProperty.getComponentType() ?? undefined, + enumType: classProperty.getEnumType() ?? undefined, + array: ifNot(classProperty.getArray(), false) ?? undefined, + count: classProperty.getCount() ?? undefined, + normalized: ifNot(classProperty.getNormalized(), false) ?? undefined, offset: classProperty.getOffset(), scale: classProperty.getScale(), max: classProperty.getMax(), min: classProperty.getMin(), - required: ifNot(classProperty.getRequired(), false), + required: ifNot(classProperty.getRequired(), false) ?? undefined, noData: classProperty.getNoData(), default: classProperty.getDefault(), }; @@ -1137,7 +1137,7 @@ export class EXTStructuralMetadata extends Extension { const enumDef: EnumDef = { name: enumObject.getObjectName(), description: enumObject.getDescription(), - valueType: ifNot(enumObject.getValueType(), "UINT16"), + valueType: ifNot(enumObject.getValueType(), "UINT16") ?? undefined, values: valueDefs, }; return enumDef; @@ -1225,11 +1225,11 @@ export class EXTStructuralMetadata extends Extension { arrayOffsetType: ifNot( propertyTableProperty.getArrayOffsetType(), "UINT32" - ), + ) ?? undefined, stringOffsetType: ifNot( propertyTableProperty.getStringOffsetType(), "UINT32" - ), + ) ?? undefined, offset: propertyTableProperty.getOffset(), scale: propertyTableProperty.getScale(), max: propertyTableProperty.getMax(), diff --git a/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts b/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts index cc133976..5616b3c5 100644 --- a/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts +++ b/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts @@ -32,15 +32,15 @@ interface IStructuralMetadata extends IProperty { } interface ISchema extends IProperty { id: string; - objectName: string; - description: string; - version: string; + objectName: string | null; + description: string | null; + version: string | null; classes: { [key: string]: Class }; enums: { [key: string]: Enum }; } interface IClass extends IProperty { - objectName: string; - description: string; + objectName: string | null; + description: string | null; properties: { [key: string]: ClassProperty }; } @@ -58,19 +58,19 @@ type AnyValue = */ interface IClassProperty extends IProperty { - objectName: string; - description: string; + objectName: string | null; + description: string | null; type: ClassPropertyType; - componentType: ClassPropertyComponentType; - enumType: string; - array: boolean; - count: number; - normalized: boolean; + componentType: ClassPropertyComponentType | null; + enumType: string | null; + array: boolean | null; + count: number | null; + normalized: boolean | null; offset: any; scale: any; max: any; min: any; - required: boolean; + required: boolean | null; noData: any; default: any; } @@ -251,6 +251,9 @@ export class Schema extends ExtensionProperty { protected override getDefaults() { return Object.assign(super.getDefaults(), { + objectName: null, + description: null, + version: null, classes: {}, enums: {}, }); @@ -263,24 +266,24 @@ export class Schema extends ExtensionProperty { return this.set("id", name); } - getObjectName(): string { + getObjectName(): string | null { return this.get("objectName"); } - setObjectName(name: string) { + setObjectName(name: string | null) { return this.set("objectName", name); } - getDescription(): string { + getDescription(): string | null { return this.get("description"); } - setDescription(description: string) { + setDescription(description: string | null) { return this.set("description", description); } - getVersion(): string { + getVersion(): string | null { return this.get("version"); } - setVersion(version: string) { + setVersion(version: string | null) { return this.set("version", version); } @@ -330,21 +333,23 @@ export class Class extends ExtensionProperty { protected override getDefaults() { return Object.assign(super.getDefaults(), { + objectName: null, + description: null, properties: {}, }); } - getObjectName(): string { + getObjectName(): string | null { return this.get("objectName"); } - setObjectName(name: string) { + setObjectName(name: string | null) { return this.set("objectName", name); } - getDescription(): string { + getDescription(): string | null { return this.get("description"); } - setDescription(description: string) { + setDescription(description: string | null) { return this.set("description", description); } @@ -381,23 +386,34 @@ export class ClassProperty extends ExtensionProperty { protected override getDefaults() { return Object.assign(super.getDefaults(), { - array: false, - normalized: false, - required: false, + objectName: null, + description: null, + componentType: null, + enumType: null, + array: null, + count: null, + normalized: null, + offset: null, + scale: null, + max: null, + min: null, + required: null, + noData: null, + default: null }); } - getObjectName(): string { + getObjectName(): string | null { return this.get("objectName"); } - setObjectName(name: string) { + setObjectName(name: string | null) { return this.set("objectName", name); } - getDescription(): string { + getDescription(): string | null { return this.get("description"); } - setDescription(description: string) { + setDescription(description: string | null) { return this.set("description", description); } @@ -408,38 +424,38 @@ export class ClassProperty extends ExtensionProperty { return this.set("type", type); } - getComponentType(): ClassPropertyComponentType { + getComponentType(): ClassPropertyComponentType | null { return this.get("componentType"); } - setComponentType(componentType: ClassPropertyComponentType) { + setComponentType(componentType: ClassPropertyComponentType | null) { return this.set("componentType", componentType); } - getEnumType(): string { + getEnumType(): string | null { return this.get("enumType"); } - setEnumType(enumType: string) { + setEnumType(enumType: string | null) { return this.set("enumType", enumType); } - getArray(): boolean { + getArray(): boolean | null{ return this.get("array"); } - setArray(array: boolean) { + setArray(array: boolean | null) { return this.set("array", array); } - getCount(): number { + getCount(): number | null { return this.get("count"); } - setCount(count: number) { + setCount(count: number | null) { return this.set("count", count); } - getNormalized(): boolean { + getNormalized(): boolean | null { return this.get("normalized"); } - setNormalized(normalized: boolean) { + setNormalized(normalized: boolean | null) { return this.set("normalized", normalized); } @@ -471,10 +487,10 @@ export class ClassProperty extends ExtensionProperty { return this.set("min", min); } - getRequired(): boolean { + getRequired(): boolean | null { return this.get("required"); } - setRequired(required: boolean) { + setRequired(required: boolean | null) { return this.set("required", required); } From c7278195ff4b1834e84c651d1975be54c5b4509f Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 3 May 2024 17:30:11 +0200 Subject: [PATCH 18/37] Further handling of optionals in structural metadata --- .../gltfExtensions/EXTStructuralMetadata.ts | 16 +-- .../gltfExtensions/StructuralMetadata.ts | 100 +++++++++++------- 2 files changed, 69 insertions(+), 47 deletions(-) diff --git a/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts b/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts index ac9fc8ed..271b8dee 100644 --- a/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts +++ b/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts @@ -973,7 +973,7 @@ export class EXTStructuralMetadata extends Extension { if (propertyTableIndex >= 0) { const elementStructuralMetadataDef: ElementStructuralMetadataDef = { propertyTable: propertyTableIndex, - index: elementStructuralMetadata.getIndex(), + index: elementStructuralMetadata.getIndex() ?? undefined, }; nodeDef.extensions = nodeDef.extensions || {}; nodeDef.extensions[NAME] = elementStructuralMetadataDef; @@ -1135,8 +1135,8 @@ export class EXTStructuralMetadata extends Extension { } const enumDef: EnumDef = { - name: enumObject.getObjectName(), - description: enumObject.getDescription(), + name: enumObject.getObjectName() ?? undefined, + description: enumObject.getDescription() ?? undefined, valueType: ifNot(enumObject.getValueType(), "UINT16") ?? undefined, values: valueDefs, }; @@ -1146,7 +1146,7 @@ export class EXTStructuralMetadata extends Extension { private createEnumValueDef(enumValue: EnumValue): EnumValueDef { const enumValueDef: EnumValueDef = { name: enumValue.getObjectName(), - description: enumValue.getDescription(), + description: enumValue.getDescription() ?? undefined, value: enumValue.getValue(), }; return enumValueDef; @@ -1175,7 +1175,7 @@ export class EXTStructuralMetadata extends Extension { } const propertyTableDef: PropertyTableDef = { - name: propertyTable.getObjectName(), + name: propertyTable.getObjectName() ?? undefined, class: propertyTable.getClass(), count: propertyTable.getCount(), properties: propertyDefs, @@ -1264,7 +1264,7 @@ export class EXTStructuralMetadata extends Extension { } const propertyTextureDef: PropertyTextureDef = { - name: propertyTexture.getObjectName(), + name: propertyTexture.getObjectName() ?? undefined, class: propertyTexture.getClass(), properties: propertyDefs, }; @@ -1290,7 +1290,7 @@ export class EXTStructuralMetadata extends Extension { } const basicTextureDef = context.createTextureInfoDef(texture, textureInfo); const propertyTexturePropertyDef: PropertyTexturePropertyDef = { - channels: propertyTextureProperty.getChannels(), + channels: propertyTextureProperty.getChannels() ?? undefined, index: basicTextureDef.index, texCoord: basicTextureDef.texCoord, offset: propertyTextureProperty.getOffset(), @@ -1322,7 +1322,7 @@ export class EXTStructuralMetadata extends Extension { } const propertyAttributeDef: PropertyAttributeDef = { - name: propertyAttribute.getObjectName(), + name: propertyAttribute.getObjectName() ?? undefined, class: propertyAttribute.getClass(), properties: propertyDefs, }; diff --git a/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts b/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts index 5616b3c5..8294fd11 100644 --- a/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts +++ b/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts @@ -76,20 +76,20 @@ interface IClassProperty extends IProperty { } interface IEnum extends IProperty { - objectName: string; - description: string; - valueType: EnumValueType; + objectName: string | null; + description: string | null; + valueType: EnumValueType | null; values: EnumValue[]; } interface IEnumValue extends IProperty { objectName: string; - description: string; + description: string | null; value: number; } interface IPropertyTable extends IProperty { - objectName: string; + objectName: string | null; class: string; count: number; properties: { [key: string]: PropertyTableProperty }; @@ -99,8 +99,8 @@ interface IPropertyTableProperty extends IProperty { values: Uint8Array; arrayOffsets: Uint8Array; stringOffsets: Uint8Array; - arrayOffsetType: PropertyTablePropertyOffsetType; - stringOffsetType: PropertyTablePropertyOffsetType; + arrayOffsetType: PropertyTablePropertyOffsetType | null; + stringOffsetType: PropertyTablePropertyOffsetType | null; offset: any; scale: any; max: any; @@ -108,13 +108,13 @@ interface IPropertyTableProperty extends IProperty { } interface IPropertyTexture extends IProperty { - objectName: string; + objectName: string | null; class: string; properties: { [key: string]: PropertyTextureProperty }; } interface IPropertyTextureProperty extends IProperty { - channels: number[]; + channels: number[] | null; offset: any; scale: any; max: any; @@ -124,7 +124,7 @@ interface IPropertyTextureProperty extends IProperty { } interface IPropertyAttribute extends IProperty { - objectName: string; + objectName: string | null; class: string; properties: { [key: string]: PropertyAttributeProperty }; } @@ -143,7 +143,7 @@ interface IPropertyAttributeProperty extends IProperty { // the specification text for now interface IElementStructuralMetadata extends IProperty { propertyTable: PropertyTable; - index: number; + index: number | null; } // This corresponds to the mesh.primitive.EXT_structural_metadata.schema.json @@ -528,29 +528,31 @@ export class Enum extends ExtensionProperty { protected override getDefaults() { return Object.assign(super.getDefaults(), { - valueType: "UINT16", + objectName: null, + description: null, + valueType: null, values: [], }); } - getObjectName(): string { + getObjectName(): string | null { return this.get("objectName"); } - setObjectName(name: string) { + setObjectName(name: string | null) { return this.set("objectName", name); } - getDescription(): string { + getDescription(): string | null{ return this.get("description"); } - setDescription(description: string) { + setDescription(description: string | null) { return this.set("description", description); } - getValueType(): EnumValueType { + getValueType(): EnumValueType | null{ return this.get("valueType"); } - setValueType(valueType: EnumValueType) { + setValueType(valueType: EnumValueType | null) { return this.set("valueType", valueType); } @@ -583,7 +585,9 @@ export class EnumValue extends ExtensionProperty { } protected override getDefaults() { - return Object.assign(super.getDefaults(), {}); + return Object.assign(super.getDefaults(), { + description: null + }); } getObjectName(): string { @@ -593,10 +597,10 @@ export class EnumValue extends ExtensionProperty { return this.set("objectName", name); } - getDescription(): string { + getDescription(): string | null{ return this.get("description"); } - setDescription(description: string) { + setDescription(description: string | null) { return this.set("description", description); } @@ -627,14 +631,15 @@ export class PropertyTable extends ExtensionProperty { protected override getDefaults() { return Object.assign(super.getDefaults(), { + objectName: null, properties: {}, }); } - getObjectName(): string { + getObjectName(): string | null { return this.get("objectName"); } - setObjectName(name: string) { + setObjectName(name: string | null) { return this.set("objectName", name); } @@ -685,8 +690,14 @@ export class PropertyTableProperty extends ExtensionProperty { protected override getDefaults() { return Object.assign(super.getDefaults(), { + objectName: null, properties: {}, }); } - getObjectName(): string { + getObjectName(): string | null { return this.get("objectName"); } - setObjectName(name: string) { + setObjectName(name: string | null) { return this.set("objectName", name); } @@ -827,16 +839,20 @@ export class PropertyTextureProperty extends ExtensionProperty { protected override getDefaults() { return Object.assign(super.getDefaults(), { + objectName: null, properties: {}, }); } - getObjectName(): string { + getObjectName(): string | null{ return this.get("objectName"); } - setObjectName(name: string) { + setObjectName(name: string | null) { return this.set("objectName", name); } @@ -949,7 +966,12 @@ export class PropertyAttributeProperty extends ExtensionProperty Date: Fri, 3 May 2024 18:47:35 +0200 Subject: [PATCH 19/37] Hopefully proper initialization of defaults --- .../gltfExtensions/EXTMeshFeatures.ts | 20 +++++++- .../gltfExtensions/EXTStructuralMetadata.ts | 4 +- .../gltfExtensions/StructuralMetadata.ts | 46 +++++++++---------- 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/gltf-extensions/gltfExtensions/EXTMeshFeatures.ts b/src/gltf-extensions/gltfExtensions/EXTMeshFeatures.ts index 522019fa..80470a4a 100644 --- a/src/gltf-extensions/gltfExtensions/EXTMeshFeatures.ts +++ b/src/gltf-extensions/gltfExtensions/EXTMeshFeatures.ts @@ -11,6 +11,24 @@ import { StructuralMetadata } from "./StructuralMetadata"; const NAME = "EXT_mesh_features"; +/** + * Returns the given value if it is **NOT** equal to the given + * default value. + * + * Returns `null` if the given value **IS** equal to the + * given default value. + * + * @param value - The value + * @param defaultValue - The default value + * @returns The result + */ +function ifNot(value: T, defaultValue: T): T | null { + if (value == defaultValue) { + return null; + } + return value; +} + //============================================================================ // Interfaces for the JSON structure // @@ -285,7 +303,7 @@ export class EXTMeshFeatures extends Extension { textureInfo ); textureDef = { - channels: featureIdTexture.getChannels(), + channels: ifNot(featureIdTexture.getChannels(), [0]) ?? undefined, index: basicTextureDef.index, texCoord: basicTextureDef.texCoord, }; diff --git a/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts b/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts index 271b8dee..fa67fa78 100644 --- a/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts +++ b/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts @@ -36,7 +36,7 @@ const NAME = "EXT_structural_metadata"; * @returns The result */ function ifNot(value: T, defaultValue: T): T | null { - if (value === defaultValue) { + if (value == defaultValue) { return null; } return value; @@ -1290,7 +1290,7 @@ export class EXTStructuralMetadata extends Extension { } const basicTextureDef = context.createTextureInfoDef(texture, textureInfo); const propertyTexturePropertyDef: PropertyTexturePropertyDef = { - channels: propertyTextureProperty.getChannels() ?? undefined, + channels: ifNot(propertyTextureProperty.getChannels(), [0]) ?? undefined, index: basicTextureDef.index, texCoord: basicTextureDef.texCoord, offset: propertyTextureProperty.getOffset(), diff --git a/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts b/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts index 8294fd11..2870689f 100644 --- a/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts +++ b/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts @@ -63,14 +63,14 @@ interface IClassProperty extends IProperty { type: ClassPropertyType; componentType: ClassPropertyComponentType | null; enumType: string | null; - array: boolean | null; + array: boolean; count: number | null; - normalized: boolean | null; + normalized: boolean; offset: any; scale: any; max: any; min: any; - required: boolean | null; + required: boolean; noData: any; default: any; } @@ -78,7 +78,7 @@ interface IClassProperty extends IProperty { interface IEnum extends IProperty { objectName: string | null; description: string | null; - valueType: EnumValueType | null; + valueType: EnumValueType; values: EnumValue[]; } @@ -99,8 +99,8 @@ interface IPropertyTableProperty extends IProperty { values: Uint8Array; arrayOffsets: Uint8Array; stringOffsets: Uint8Array; - arrayOffsetType: PropertyTablePropertyOffsetType | null; - stringOffsetType: PropertyTablePropertyOffsetType | null; + arrayOffsetType: PropertyTablePropertyOffsetType; + stringOffsetType: PropertyTablePropertyOffsetType; offset: any; scale: any; max: any; @@ -114,7 +114,7 @@ interface IPropertyTexture extends IProperty { } interface IPropertyTextureProperty extends IProperty { - channels: number[] | null; + channels: number[]; offset: any; scale: any; max: any; @@ -438,10 +438,10 @@ export class ClassProperty extends ExtensionProperty { return this.set("enumType", enumType); } - getArray(): boolean | null{ + getArray(): boolean { return this.get("array"); } - setArray(array: boolean | null) { + setArray(array: boolean) { return this.set("array", array); } @@ -452,10 +452,10 @@ export class ClassProperty extends ExtensionProperty { return this.set("count", count); } - getNormalized(): boolean | null { + getNormalized(): boolean { return this.get("normalized"); } - setNormalized(normalized: boolean | null) { + setNormalized(normalized: boolean) { return this.set("normalized", normalized); } @@ -487,10 +487,10 @@ export class ClassProperty extends ExtensionProperty { return this.set("min", min); } - getRequired(): boolean | null { + getRequired(): boolean { return this.get("required"); } - setRequired(required: boolean | null) { + setRequired(required: boolean) { return this.set("required", required); } @@ -530,7 +530,7 @@ export class Enum extends ExtensionProperty { return Object.assign(super.getDefaults(), { objectName: null, description: null, - valueType: null, + valueType: "UINT16", values: [], }); } @@ -549,10 +549,10 @@ export class Enum extends ExtensionProperty { return this.set("description", description); } - getValueType(): EnumValueType | null{ + getValueType(): EnumValueType { return this.get("valueType"); } - setValueType(valueType: EnumValueType | null) { + setValueType(valueType: EnumValueType) { return this.set("valueType", valueType); } @@ -722,17 +722,17 @@ export class PropertyTableProperty extends ExtensionProperty Date: Fri, 3 May 2024 18:48:15 +0200 Subject: [PATCH 20/37] Formatting --- .../gltfExtensions/EXTStructuralMetadata.ts | 14 ++++++------- .../gltfExtensions/InstanceFeatures.ts | 2 +- .../gltfExtensions/MeshFeatures.ts | 4 ++-- .../gltfExtensions/StructuralMetadata.ts | 20 +++++++++---------- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts b/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts index fa67fa78..35a1b47d 100644 --- a/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts +++ b/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts @@ -1222,14 +1222,12 @@ export class EXTStructuralMetadata extends Extension { values: values, arrayOffsets: arrayOffsets, stringOffsets: stringOffsets, - arrayOffsetType: ifNot( - propertyTableProperty.getArrayOffsetType(), - "UINT32" - ) ?? undefined, - stringOffsetType: ifNot( - propertyTableProperty.getStringOffsetType(), - "UINT32" - ) ?? undefined, + arrayOffsetType: + ifNot(propertyTableProperty.getArrayOffsetType(), "UINT32") ?? + undefined, + stringOffsetType: + ifNot(propertyTableProperty.getStringOffsetType(), "UINT32") ?? + undefined, offset: propertyTableProperty.getOffset(), scale: propertyTableProperty.getScale(), max: propertyTableProperty.getMax(), diff --git a/src/gltf-extensions/gltfExtensions/InstanceFeatures.ts b/src/gltf-extensions/gltfExtensions/InstanceFeatures.ts index fe29e1a7..334d943c 100644 --- a/src/gltf-extensions/gltfExtensions/InstanceFeatures.ts +++ b/src/gltf-extensions/gltfExtensions/InstanceFeatures.ts @@ -84,7 +84,7 @@ export class FeatureId extends ExtensionProperty { nullFeatureId: null, label: null, attribute: null, - propertyTable: null + propertyTable: null, }); } diff --git a/src/gltf-extensions/gltfExtensions/MeshFeatures.ts b/src/gltf-extensions/gltfExtensions/MeshFeatures.ts index cf0610bd..ea7371cf 100644 --- a/src/gltf-extensions/gltfExtensions/MeshFeatures.ts +++ b/src/gltf-extensions/gltfExtensions/MeshFeatures.ts @@ -152,7 +152,7 @@ export class FeatureId extends ExtensionProperty { label: null, attribute: null, texture: null, - propertyTable: null + propertyTable: null, }); } @@ -177,7 +177,7 @@ export class FeatureId extends ExtensionProperty { return this.set("label", label); } - getAttribute(): FeatureIdAttribute | null{ + getAttribute(): FeatureIdAttribute | null { return this.get("attribute"); } setAttribute(attribute: FeatureIdAttribute | null) { diff --git a/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts b/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts index 2870689f..168dc244 100644 --- a/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts +++ b/src/gltf-extensions/gltfExtensions/StructuralMetadata.ts @@ -399,7 +399,7 @@ export class ClassProperty extends ExtensionProperty { min: null, required: null, noData: null, - default: null + default: null, }); } @@ -542,7 +542,7 @@ export class Enum extends ExtensionProperty { return this.set("objectName", name); } - getDescription(): string | null{ + getDescription(): string | null { return this.get("description"); } setDescription(description: string | null) { @@ -586,7 +586,7 @@ export class EnumValue extends ExtensionProperty { protected override getDefaults() { return Object.assign(super.getDefaults(), { - description: null + description: null, }); } @@ -597,7 +597,7 @@ export class EnumValue extends ExtensionProperty { return this.set("objectName", name); } - getDescription(): string | null{ + getDescription(): string | null { return this.get("description"); } setDescription(description: string | null) { @@ -697,7 +697,7 @@ export class PropertyTableProperty extends ExtensionProperty { }); } - getObjectName(): string | null{ + getObjectName(): string | null { return this.get("objectName"); } setObjectName(name: string | null) { @@ -967,10 +967,10 @@ export class PropertyAttributeProperty extends ExtensionProperty Date: Fri, 3 May 2024 18:50:00 +0200 Subject: [PATCH 21/37] Update for null vs. undefined --- src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts index 906ca83a..e7fe3585 100644 --- a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts +++ b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts @@ -627,7 +627,7 @@ export class StructuralMetadataMerger { const sourceProperty = sourceClass.getProperty(sourcePropertyKey); if (sourceProperty) { const sourceEnumType = sourceProperty.getEnumType(); - if (sourceEnumType !== undefined) { + if (sourceEnumType !== null) { const targetEnumType = sourceEnumNamesInTarget[sourceEnumType]; if (targetEnumType !== sourceEnumType) { return true; @@ -655,7 +655,7 @@ export class StructuralMetadataMerger { const targetProperty = targetClass.getProperty(targetPropertyKey); if (targetProperty) { const sourceEnumType = targetProperty.getEnumType(); - if (sourceEnumType !== undefined) { + if (sourceEnumType !== null) { const targetEnumType = sourceEnumNamesInTarget[sourceEnumType]; targetProperty.setEnumType(targetEnumType); } From ddfc6905e3d02d0d992e567e8e8f7a50fd9adf51 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 3 May 2024 18:51:19 +0200 Subject: [PATCH 22/37] Update for null vs. undefined --- src/tools/gltfExtensionsUtils/StructuralMetadataUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tools/gltfExtensionsUtils/StructuralMetadataUtils.ts b/src/tools/gltfExtensionsUtils/StructuralMetadataUtils.ts index e7559851..2e632710 100644 --- a/src/tools/gltfExtensionsUtils/StructuralMetadataUtils.ts +++ b/src/tools/gltfExtensionsUtils/StructuralMetadataUtils.ts @@ -165,7 +165,7 @@ export class StructuralMetadataUtils { let enumValueType: string | undefined = undefined; const enumType = classProperty.getEnumType(); - if (enumType !== undefined) { + if (enumType !== null) { const enumObject = schema.getEnum(enumType); if (!enumObject) { sb.addLine(`decoded values: (no enum '${enumType}' in schema)`); @@ -176,9 +176,9 @@ export class StructuralMetadataUtils { const propertyModel = BinaryPropertyModels.createPropertyModelInternal( propertyName, type, - componentType, + componentType ?? undefined, isArray, - count, + count ?? undefined, valuesBufferViewData, arrayOffsetsBufferViewData, arrayOffsetType, From a7c229648d1a5b2c0dac6351b07672264dcca8e6 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 3 May 2024 19:29:09 +0200 Subject: [PATCH 23/37] Remove unnecessary null coalescing --- src/tools/gltfExtensionsUtils/StructuralMetadataUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/gltfExtensionsUtils/StructuralMetadataUtils.ts b/src/tools/gltfExtensionsUtils/StructuralMetadataUtils.ts index 2e632710..7a9d2857 100644 --- a/src/tools/gltfExtensionsUtils/StructuralMetadataUtils.ts +++ b/src/tools/gltfExtensionsUtils/StructuralMetadataUtils.ts @@ -171,7 +171,7 @@ export class StructuralMetadataUtils { sb.addLine(`decoded values: (no enum '${enumType}' in schema)`); return; } - enumValueType = enumObject.getValueType() ?? "UINT16"; + enumValueType = enumObject.getValueType(); } const propertyModel = BinaryPropertyModels.createPropertyModelInternal( propertyName, From 0b363af06f942663a3fef8a2166b17e21f8feaa9 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 7 May 2024 13:33:45 +0200 Subject: [PATCH 24/37] Use undefined instead of writing null --- .../gltfExtensions/EXTStructuralMetadata.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts b/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts index 35a1b47d..4b552ff9 100644 --- a/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts +++ b/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts @@ -1114,13 +1114,13 @@ export class EXTStructuralMetadata extends Extension { array: ifNot(classProperty.getArray(), false) ?? undefined, count: classProperty.getCount() ?? undefined, normalized: ifNot(classProperty.getNormalized(), false) ?? undefined, - offset: classProperty.getOffset(), - scale: classProperty.getScale(), - max: classProperty.getMax(), - min: classProperty.getMin(), + offset: classProperty.getOffset() ?? undefined, + scale: classProperty.getScale() ?? undefined, + max: classProperty.getMax() ?? undefined, + min: classProperty.getMin() ?? undefined, required: ifNot(classProperty.getRequired(), false) ?? undefined, - noData: classProperty.getNoData(), - default: classProperty.getDefault(), + noData: classProperty.getNoData() ?? undefined, + default: classProperty.getDefault() ?? undefined, }; return classPropertyDef; } @@ -1228,10 +1228,10 @@ export class EXTStructuralMetadata extends Extension { stringOffsetType: ifNot(propertyTableProperty.getStringOffsetType(), "UINT32") ?? undefined, - offset: propertyTableProperty.getOffset(), - scale: propertyTableProperty.getScale(), - max: propertyTableProperty.getMax(), - min: propertyTableProperty.getMin(), + offset: propertyTableProperty.getOffset() ?? undefined, + scale: propertyTableProperty.getScale() ?? undefined, + max: propertyTableProperty.getMax() ?? undefined, + min: propertyTableProperty.getMin() ?? undefined, }; return propertyTablePropertyDef; } @@ -1291,10 +1291,10 @@ export class EXTStructuralMetadata extends Extension { channels: ifNot(propertyTextureProperty.getChannels(), [0]) ?? undefined, index: basicTextureDef.index, texCoord: basicTextureDef.texCoord, - offset: propertyTextureProperty.getOffset(), - scale: propertyTextureProperty.getScale(), - max: propertyTextureProperty.getMax(), - min: propertyTextureProperty.getMin(), + offset: propertyTextureProperty.getOffset() ?? undefined, + scale: propertyTextureProperty.getScale() ?? undefined, + max: propertyTextureProperty.getMax() ?? undefined, + min: propertyTextureProperty.getMin() ?? undefined, }; return propertyTexturePropertyDef; } @@ -1332,10 +1332,10 @@ export class EXTStructuralMetadata extends Extension { ) { const propertyAttributePropertyDef: PropertyAttributePropertyDef = { attribute: propertyAttributeProperty.getAttribute(), - offset: propertyAttributeProperty.getOffset(), - scale: propertyAttributeProperty.getScale(), - max: propertyAttributeProperty.getMax(), - min: propertyAttributeProperty.getMin(), + offset: propertyAttributeProperty.getOffset() ?? undefined, + scale: propertyAttributeProperty.getScale() ?? undefined, + max: propertyAttributeProperty.getMax() ?? undefined, + min: propertyAttributeProperty.getMin() ?? undefined, }; return propertyAttributePropertyDef; } From 2b878d325a0b4e66dd0dcbda0cf8bdef1d12a7fc Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 7 May 2024 14:58:01 +0200 Subject: [PATCH 25/37] Basic handling of attributes and textures, basic specs --- .../StructuralMetadataMergerSpec.ts | 680 +++++++++++++++++- .../StructuralMetadataMergeUtilities.ts | 37 +- .../StructuralMetadataMerger.ts | 275 +++++-- 3 files changed, 902 insertions(+), 90 deletions(-) diff --git a/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts b/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts index 044ea2fc..62e0bced 100644 --- a/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts +++ b/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts @@ -1,14 +1,23 @@ import { Document } from "@gltf-transform/core"; +import NdArray from "ndarray"; +import { savePixels } from "ndarray-pixels"; + import { EXTStructuralMetadata } from "../../../src/gltf-extensions"; +import { MeshPrimitiveStructuralMetadata } from "../../../src/gltf-extensions"; import { StructuralMetadata } from "../../../src/gltf-extensions"; import { StructuralMetadataSchema as Schema } from "../../../src/gltf-extensions"; import { StructuralMetadataClass as Class } from "../../../src/gltf-extensions"; import { StructuralMetadataPropertyTable as PropertyTable } from "../../../src/gltf-extensions"; +import { StructuralMetadataPropertyAttribute as PropertyAttribute } from "../../../src/gltf-extensions"; +import { StructuralMetadataPropertyTexture as PropertyTexture } from "../../../src/gltf-extensions"; import { BinaryPropertyTableBuilder } from "../../../src/metadata/"; -import { StructuralMetadataPropertyTables } from "../../../src/tools/"; +import { + GltfTransform, + StructuralMetadataPropertyTables, +} from "../../../src/tools/"; import { StructuralMetadataMerger } from "../../../src/tools/"; // A dummy resolver for the `schemaUri` that may be found in metadata @@ -116,6 +125,46 @@ function getNumPropertyTables(document: Document): number { return propertyTables.length; } +/** + * Returns the number of property attributes of the `EXT_structural_metadata` + * extension in the given document, or -1 if the extension does not + * exist. + * + * @param document - The glTF-Transform document + * @returns The number of property attributes + */ +function getNumPropertyAttributes(document: Document): number { + const root = document.getRoot(); + const structuralMetadata = root.getExtension( + "EXT_structural_metadata" + ); + if (structuralMetadata === null) { + return -1; + } + const propertyAttributes = structuralMetadata.listPropertyAttributes(); + return propertyAttributes.length; +} + +/** + * Returns the number of property textures of the `EXT_structural_metadata` + * extension in the given document, or -1 if the extension does not + * exist. + * + * @param document - The glTF-Transform document + * @returns The number of property textures + */ +function getNumPropertyTextures(document: Document): number { + const root = document.getRoot(); + const structuralMetadata = root.getExtension( + "EXT_structural_metadata" + ); + if (structuralMetadata === null) { + return -1; + } + const propertyTextures = structuralMetadata.listPropertyTextures(); + return propertyTextures.length; +} + /** * Returns the property table with the given index from the * `EXT_structural_metadata`, throwing up if this does not @@ -137,6 +186,54 @@ function getPropertyTable(document: Document, index: number): PropertyTable { return propertyTables[index]; } +/** + * Returns the property attribute with the given index from the + * `EXT_structural_metadata`, throwing up if this does not + * exist. + * + * @param document - The glTF-Transform document + * @param index The index of the property attribute + * @returns The property attribute + */ +function getPropertyAttribute( + document: Document, + index: number +): PropertyAttribute { + const root = document.getRoot(); + const structuralMetadata = root.getExtension( + "EXT_structural_metadata" + ); + if (structuralMetadata === null) { + throw new Error("Document does not contain metadata"); + } + const propertyAttributes = structuralMetadata.listPropertyAttributes(); + return propertyAttributes[index]; +} + +/** + * Returns the property texture with the given index from the + * `EXT_structural_metadata`, throwing up if this does not + * exist. + * + * @param document - The glTF-Transform document + * @param index The index of the property texture + * @returns The property texture + */ +function getPropertyTexture( + document: Document, + index: number +): PropertyTexture { + const root = document.getRoot(); + const structuralMetadata = root.getExtension( + "EXT_structural_metadata" + ); + if (structuralMetadata === null) { + throw new Error("Document does not contain metadata"); + } + const propertyTextures = structuralMetadata.listPropertyTextures(); + return propertyTextures[index]; +} + /** * Obtain the StructuralMetadata object from the given document, creating * it if it did not exist yet. @@ -213,7 +310,154 @@ function addPropertyTable( structuralMetadata.addPropertyTable(propertyTable); } -fdescribe("StructuralMetadataMerger", function () { +/** + * Add a property attribute to the StructuralMetadata in the given document. + * + * This method does name assumptions about the structure of the + * metadata class. It assumes that the class contains a property + * that is called "example_INT8_SCALAR" (with UINT8 scalar type) + * and it will create... + * + * - a property attribute in the top-level metadata extension object for + * such a property. It will refer to a mesh primitive attribute that + * will be called "_EXAMPLE_ATTRIBUTE" + * - a mesh with a primitive that contains such a "_EXAMPLE_ATTRIBUTE" + * containing "dummy" data for such an attribute + * + * @param document - The document + * @param className - The metadata class name + */ +function addSpecPropertyAttribute(document: Document, className: string) { + const propertyName = "example_INT8_SCALAR"; + const attributeName = "_EXAMPLE_ATTRIBUTE"; + + // Obtain the top-level extension object + const extStructuralMetadata = document.createExtension(EXTStructuralMetadata); + const structuralMetadata = obtainStructuralMetadata(document); + + // Create and add a property attribute with the following structure: + // { + // className: className + // properties: { + // example_INT8_SCALAR : { + // attribute: "_EXAMPLE_ATTRIBUTE" + // } + // } + // } + const propertyAttribute = extStructuralMetadata.createPropertyAttribute(); + propertyAttribute.setClass(className); + const propertyAttributeProperty = + extStructuralMetadata.createPropertyAttributeProperty(); + propertyAttributeProperty.setAttribute(attributeName); + propertyAttribute.setProperty(propertyName, propertyAttributeProperty); + structuralMetadata.addPropertyAttribute(propertyAttribute); + + // Create a dummy mesh with one primitive + const mesh = document.createMesh(); + const primitive = document.createPrimitive(); + primitive.setExtension; + mesh.addPrimitive(primitive); + + // Create an accessor with dummy UINT8 scalar data + // to hold the property attribute values, and store + // it as the "_EXAMPLE_ATTRIBUTE" in the primitive + const accessor = document.createAccessor(); + const buffer = document.getRoot().listBuffers()[0]; + accessor.setBuffer(buffer); + accessor.setType("SCALAR"); + const array = new Int8Array([0, 1, 2, 3]); + accessor.setArray(array); + primitive.setAttribute(attributeName, accessor); + + // Define the metadata for the mesh primitive, and let it + // refer to the property attribute that was created above + const meshPrimitiveStructuralMetadata = + extStructuralMetadata.createMeshPrimitiveStructuralMetadata(); + meshPrimitiveStructuralMetadata.addPropertyAttribute(propertyAttribute); + primitive.setExtension( + "EXT_structural_metadata", + meshPrimitiveStructuralMetadata + ); +} + +/** + * Add a property texture to the StructuralMetadata in the given document. + * + * This method does name assumptions about the structure of the + * metadata class. It assumes that the class contains a property + * that is called "example_INT8_SCALAR" (with UINT8 scalar type) + * and it will create... + * + * - a property texture in the top-level metadata extension object for + * such a property. It will refer to the texture with index 0, + * texture coordinate set 0, and channels [0] + * - a texture that contains "dummy" data in channel 0 + * + * @param document - The document + * @param className - The metadata class name + */ +async function addSpecPropertyTexture(document: Document, className: string) { + const propertyName = "example_INT8_SCALAR"; + + // Obtain the top-level extension object + const extStructuralMetadata = document.createExtension(EXTStructuralMetadata); + const structuralMetadata = obtainStructuralMetadata(document); + + // Create an image with integer values in channel 0, + // and a texture that refers to this image + const sizeX = 2; + const sizeY = 2; + const pixels = NdArray(new Uint8Array(sizeX * sizeY), [sizeX, sizeY, 4]); + for (let x = 0; x < pixels.shape[0]; x++) { + for (let y = 0; y < pixels.shape[1]; y++) { + pixels.set(x, y, 0, x * sizeY + y); + } + } + const image = await savePixels(pixels, "image/png"); + const texture = document.createTexture(); + texture.setImage(image); + texture.setURI("propertyTextuer.png"); + + // Create and add a property texture with the following structure: + // { + // className: className + // properties: { + // example_INT8_SCALAR : { + // index: 0, + // texCoord: 0, + // channels: [0] + // } + // } + // } + // (The texture and texCoord will be filled in by glTF-Transform, + // due to the reference to the texture that was created above) + const propertyTexture = extStructuralMetadata.createPropertyTexture(); + propertyTexture.setClass(className); + const propertyTextureProperty = + extStructuralMetadata.createPropertyTextureProperty(); + propertyTextureProperty.setTexture(texture); + propertyTextureProperty.setChannels([0]); + propertyTexture.setProperty(propertyName, propertyTextureProperty); + structuralMetadata.addPropertyTexture(propertyTexture); + + // Create a dummy mesh with one primitive + const mesh = document.createMesh(); + const primitive = document.createPrimitive(); + primitive.setExtension; + mesh.addPrimitive(primitive); + + // Define the metadata for the mesh primitive, and let it + // refer to the property texture that was created above + const meshPrimitiveStructuralMetadata = + extStructuralMetadata.createMeshPrimitiveStructuralMetadata(); + meshPrimitiveStructuralMetadata.addPropertyTexture(propertyTexture); + primitive.setExtension( + "EXT_structural_metadata", + meshPrimitiveStructuralMetadata + ); +} + +describe("StructuralMetadataMerger", function () { //========================================================================== // Basic class merging @@ -262,7 +506,7 @@ fdescribe("StructuralMetadataMerger", function () { assignMetadataSchema(documentB, schemaB); const document = new Document(); - StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( document, documentA, specSchemaUriResolver @@ -272,7 +516,7 @@ fdescribe("StructuralMetadataMerger", function () { // There should be one class expect(getMetadataClassNames(document).length).toBe(1); - StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( document, documentB, specSchemaUriResolver @@ -329,7 +573,7 @@ fdescribe("StructuralMetadataMerger", function () { assignMetadataSchema(documentB, schemaB); const document = new Document(); - StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( document, documentA, specSchemaUriResolver @@ -339,7 +583,7 @@ fdescribe("StructuralMetadataMerger", function () { // There should be one class expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); - StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( document, documentB, specSchemaUriResolver @@ -397,7 +641,7 @@ fdescribe("StructuralMetadataMerger", function () { assignMetadataSchema(documentB, schemaB); const document = new Document(); - StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( document, documentA, specSchemaUriResolver @@ -407,7 +651,7 @@ fdescribe("StructuralMetadataMerger", function () { // There should be one class expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); - StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( document, documentB, specSchemaUriResolver @@ -501,7 +745,7 @@ fdescribe("StructuralMetadataMerger", function () { assignMetadataSchema(documentB, schemaB); const document = new Document(); - StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( document, documentA, specSchemaUriResolver @@ -512,7 +756,7 @@ fdescribe("StructuralMetadataMerger", function () { expect(getMetadataEnumNames(document)).toEqual(["exampleEnum"]); expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); - StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( document, documentB, specSchemaUriResolver @@ -539,7 +783,7 @@ fdescribe("StructuralMetadataMerger", function () { //========================================================================== // Basic property table merging - it("merges the set of property tables for one resulting class", async function () { + fit("merges the set of property tables for one resulting class", async function () { // The classes have the same name/key // The classes are structurally equal // The property tables refer to the respective class (which turns out to be equal) @@ -602,7 +846,7 @@ fdescribe("StructuralMetadataMerger", function () { addPropertyTable(documentB, schemaB, propertyTableB); const document = new Document(); - StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( document, documentA, specSchemaUriResolver @@ -614,7 +858,7 @@ fdescribe("StructuralMetadataMerger", function () { expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); expect(getNumPropertyTables(document)).toBe(1); - StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( document, documentB, specSchemaUriResolver @@ -625,15 +869,23 @@ fdescribe("StructuralMetadataMerger", function () { // There should be two property tables expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); expect(getNumPropertyTables(document)).toBe(2); + + // XXX Try to write the document as a basic consistency check: + { + const io = await GltfTransform.getIO(); + const jsonDocument = await io.writeJSON(document); + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } }); - it("merges the set of property tables and updates their class for disambiguated classes", async function () { + fit("merges the set of property tables and updates their class for disambiguated classes", async function () { // The classes have the same name/key - // The classes are structurally equal - // The property tables refer to the respective class (which turns out to be equal) + // The classes are structurally different + // The property tables refer to the respective class // The result should be: // One class // Two property tables + // One referring to the disambiguated class const schemaA = { id: "EXAMPLE_SCHEMA_ID", @@ -690,7 +942,7 @@ fdescribe("StructuralMetadataMerger", function () { addPropertyTable(documentB, schemaB, propertyTableB); const document = new Document(); - StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( document, documentA, specSchemaUriResolver @@ -702,7 +954,7 @@ fdescribe("StructuralMetadataMerger", function () { expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); expect(getNumPropertyTables(document)).toBe(1); - StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( document, documentB, specSchemaUriResolver @@ -720,5 +972,397 @@ fdescribe("StructuralMetadataMerger", function () { // renamed class const propertyTable1 = getPropertyTable(document, 1); expect(propertyTable1.getClass()).toBe("exampleClass_0"); + + // XXX Try to write the document as a basic consistency check: + { + const io = await GltfTransform.getIO(); + const jsonDocument = await io.writeJSON(document); + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } + }); + + //========================================================================== + // Basic property attribute merging + + fit("merges the set of property attributes for one resulting class", async function () { + // The classes have the same name/key + // The classes are structurally equal + // The property attributes refer to the respective class (which turns out to be equal) + // The result should be: + // One class + // Two property attributes + + const schemaA = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_INT8_SCALAR: { + name: "Example SCALAR property with INT8 components", + description: + "An example property, with type SCALAR, with component type INT8", + type: "SCALAR", + componentType: "INT8", + array: false, + normalized: false, + }, + }, + }, + }, + }; + + const schemaB = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_INT8_SCALAR: { + name: "Example SCALAR property with INT8 components", + description: + "An example property, with type SCALAR, with component type INT8", + type: "SCALAR", + componentType: "INT8", + array: false, + normalized: false, + }, + }, + }, + }, + }; + + const documentA = new Document(); + documentA.createBuffer(); + assignMetadataSchema(documentA, schemaA); + addSpecPropertyAttribute(documentA, "exampleClass"); + + const documentB = new Document(); + documentB.createBuffer(); + assignMetadataSchema(documentB, schemaB); + addSpecPropertyAttribute(documentB, "exampleClass"); + + const document = new Document(); + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentA, + specSchemaUriResolver + ); + + // After merging in the first document: + // There should be one class + // There should be one property attribute + expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); + expect(getNumPropertyAttributes(document)).toBe(1); + + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentB, + specSchemaUriResolver + ); + + // After merging in the second document: + // There should still be one class + // There should be two property attributes + expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); + expect(getNumPropertyAttributes(document)).toBe(2); + + // XXX Try to write the document as a basic consistency check: + { + const io = await GltfTransform.getIO(); + const jsonDocument = await io.writeJSON(document); + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } + }); + + fit("merges the set of property attributes and updates their class for disambiguated classes", async function () { + // The classes have the same name/key + // The classes are structurally different + // The property attributes refer to the respective class + // The result should be: + // Two classes + // Two property attributes + + const schemaA = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_INT8_SCALAR: { + name: "Example SCALAR property with INT8 components", + description: + "An example property, with type SCALAR, with component type INT8", + type: "SCALAR", + componentType: "INT8", + array: false, + normalized: false, + }, + }, + }, + }, + }; + + const schemaB = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class but with a difference", // Structural difference! + properties: { + example_INT8_SCALAR: { + name: "Example SCALAR property with INT8 components", + description: + "An example property, with type SCALAR, with component type INT8", + type: "SCALAR", + componentType: "INT8", + array: false, + normalized: false, + }, + }, + }, + }, + }; + + const documentA = new Document(); + documentA.createBuffer(); + assignMetadataSchema(documentA, schemaA); + addSpecPropertyAttribute(documentA, "exampleClass"); + + const documentB = new Document(); + documentB.createBuffer(); + assignMetadataSchema(documentB, schemaB); + addSpecPropertyAttribute(documentB, "exampleClass"); + + const document = new Document(); + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentA, + specSchemaUriResolver + ); + + // After merging in the first document: + // There should be one class + // There should be one property attribute + expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); + expect(getNumPropertyAttributes(document)).toBe(1); + + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentB, + specSchemaUriResolver + ); + + // After merging in the second document: + // There should be two classes + // There should be two property attributes + expect(getMetadataClassNames(document).sort()).toEqual( + ["exampleClass", "exampleClass_0"].sort() + ); + expect(getNumPropertyAttributes(document)).toBe(2); + + // Expect the second property attribute to refer to the + // renamed class + const propertyAttribute1 = getPropertyAttribute(document, 1); + expect(propertyAttribute1.getClass()).toBe("exampleClass_0"); + + // XXX Try to write the document as a basic consistency check: + { + const io = await GltfTransform.getIO(); + const jsonDocument = await io.writeJSON(document); + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } + }); + + //========================================================================== + // Basic property texture merging + + fit("merges the set of property textures for one resulting class", async function () { + // The classes have the same name/key + // The classes are structurally equal + // The property textures refer to the respective class (which turns out to be equal) + // The result should be: + // One class + // Two property textures + + const schemaA = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_INT8_SCALAR: { + name: "Example SCALAR property with INT8 components", + description: + "An example property, with type SCALAR, with component type INT8", + type: "SCALAR", + componentType: "INT8", + array: false, + normalized: false, + }, + }, + }, + }, + }; + + const schemaB = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_INT8_SCALAR: { + name: "Example SCALAR property with INT8 components", + description: + "An example property, with type SCALAR, with component type INT8", + type: "SCALAR", + componentType: "INT8", + array: false, + normalized: false, + }, + }, + }, + }, + }; + + const documentA = new Document(); + documentA.createBuffer(); + assignMetadataSchema(documentA, schemaA); + await addSpecPropertyTexture(documentA, "exampleClass"); + + const documentB = new Document(); + documentB.createBuffer(); + assignMetadataSchema(documentB, schemaB); + await addSpecPropertyTexture(documentB, "exampleClass"); + + const document = new Document(); + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentA, + specSchemaUriResolver + ); + + // After merging in the first document: + // There should be one class + // There should be one property texture + expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); + expect(getNumPropertyTextures(document)).toBe(1); + + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentB, + specSchemaUriResolver + ); + + // After merging in the second document: + // There should still be one class + // There should be two property textures + expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); + expect(getNumPropertyTextures(document)).toBe(2); + + // XXX Try to write the document as a basic consistency check: + { + const io = await GltfTransform.getIO(); + const jsonDocument = await io.writeJSON(document); + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } + }); + + fit("merges the set of property textures and updates their class for disambiguated classes", async function () { + // The classes have the same name/key + // The classes are structurally different + // The property textures refer to the respective class + // The result should be: + // One class + // Two property textures + // One referring to the disambiguated class + + const schemaA = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_INT8_SCALAR: { + name: "Example SCALAR property with INT8 components", + description: + "An example property, with type SCALAR, with component type INT8", + type: "SCALAR", + componentType: "INT8", + array: false, + normalized: false, + }, + }, + }, + }, + }; + + const schemaB = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class but with a difference", // Structural difference! + properties: { + example_INT8_SCALAR: { + name: "Example SCALAR property with INT8 components", + description: + "An example property, with type SCALAR, with component type INT8", + type: "SCALAR", + componentType: "INT8", + array: false, + normalized: false, + }, + }, + }, + }, + }; + + const documentA = new Document(); + documentA.createBuffer(); + assignMetadataSchema(documentA, schemaA); + await addSpecPropertyTexture(documentA, "exampleClass"); + + const documentB = new Document(); + documentB.createBuffer(); + assignMetadataSchema(documentB, schemaB); + await addSpecPropertyTexture(documentB, "exampleClass"); + + const document = new Document(); + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentA, + specSchemaUriResolver + ); + + // After merging in the first document: + // There should be one class + // There should be one property texture + expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); + expect(getNumPropertyTextures(document)).toBe(1); + + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentB, + specSchemaUriResolver + ); + + // After merging in the second document: + // There should be two classes + // There should be two property textures + expect(getMetadataClassNames(document).sort()).toEqual( + ["exampleClass", "exampleClass_0"].sort() + ); + expect(getNumPropertyTextures(document)).toBe(2); + + // Expect the second property texture to refer to the + // renamed class + const propertyTexture1 = getPropertyTexture(document, 1); + expect(propertyTexture1.getClass()).toBe("exampleClass_0"); + + // XXX Try to write the document as a basic consistency check: + { + const io = await GltfTransform.getIO(); + const jsonDocument = await io.writeJSON(document); + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } }); }); diff --git a/src/tools/gltfExtensionsUtils/StructuralMetadataMergeUtilities.ts b/src/tools/gltfExtensionsUtils/StructuralMetadataMergeUtilities.ts index df574612..e8955d23 100644 --- a/src/tools/gltfExtensionsUtils/StructuralMetadataMergeUtilities.ts +++ b/src/tools/gltfExtensionsUtils/StructuralMetadataMergeUtilities.ts @@ -1,10 +1,10 @@ // NOTE: The functions in this class are "ported" from // https://github.com/donmccurdy/glTF-Transform/pull/1375/files // -// The only exported function is "copyToDocument", which will be replaced -// by the `copyToDocument` functionality of glTF-Transform 4.0 +// The only exported functions are "mergeDocuments" and "copyToDocument", +// which will be replaced by the functions from glTF-Transform 4.0 -import { Document } from "@gltf-transform/core"; +import { Document, Extension } from "@gltf-transform/core"; import { Graph } from "@gltf-transform/core"; import { Property } from "@gltf-transform/core"; import { PropertyType } from "@gltf-transform/core"; @@ -14,6 +14,37 @@ const { TEXTURE_INFO, ROOT } = PropertyType; type PropertyConstructor = new (g: Graph) => Property; const NO_TRANSFER_TYPES = new Set([TEXTURE_INFO, ROOT]); +function listNonRootProperties(document: Document): Property[] { + const visited = new Set(); + for (const edge of document.getGraph().listEdges()) { + visited.add(edge.getChild()); + } + return Array.from(visited); +} + +export function mergeDocuments( + target: Document, + source: Document, + resolve?: PropertyResolver +): Map { + resolve ||= createDefaultPropertyResolver(target, source); + + for (const sourceExtension of source.getRoot().listExtensionsUsed()) { + const targetExtension = target.createExtension( + sourceExtension.constructor as new (doc: Document) => Extension + ); + if (sourceExtension.isRequired()) targetExtension.setRequired(true); + } + + // Root properties (name, asset, default scene, extras) are not overwritten. + return _copyToDocument( + target, + source, + listNonRootProperties(source), + resolve + ); +} + export function copyToDocument( target: Document, source: Document, diff --git a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts index e7fe3585..17885155 100644 --- a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts +++ b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts @@ -1,14 +1,19 @@ import crypto from "crypto"; -import { Document } from "@gltf-transform/core"; +import { Document, Primitive } from "@gltf-transform/core"; import { Property } from "@gltf-transform/core"; import { IProperty } from "@gltf-transform/core"; +import { EXTStructuralMetadata } from "../../gltf-extensions/"; import { StructuralMetadata } from "../../gltf-extensions/"; +import { MeshPrimitiveStructuralMetadata } from "../../gltf-extensions/"; import { StructuralMetadataSchema as Schema } from "../../gltf-extensions/"; import { StructuralMetadataClass as Class } from "../../gltf-extensions/"; -import { EXTStructuralMetadata } from "../../gltf-extensions/"; +import { StructuralMetadataPropertyTable as PropertyTable } from "../../gltf-extensions/"; +import { StructuralMetadataPropertyTexture as PropertyTexture } from "../../gltf-extensions/"; +import { StructuralMetadataPropertyAttribute as PropertyAttribute } from "../../gltf-extensions/"; +import { mergeDocuments } from "./StructuralMetadataMergeUtilities"; import { copyToDocument } from "./StructuralMetadataMergeUtilities"; import { MetadataError } from "../../metadata"; @@ -32,7 +37,7 @@ export class StructuralMetadataMerger { * Merge two glTF-Transform documents, taking into account that * they might contain the `EXT_structural_metadata` extension. * - * This will perform a default `document.merge` operation, but apply + * This will perform a default merge operation, but apply * special treatment for the case that either or both of the documents * contain the extension: It will merge the top-level extension * object, and assign the merged one to the root of the target @@ -55,7 +60,7 @@ export class StructuralMetadataMerger { targetRoot.getExtension("EXT_structural_metadata"); const sourceStructuralMetadata = sourceRoot.getExtension("EXT_structural_metadata"); - targetDocument.merge(sourceDocument); + const mainMergeMap = mergeDocuments(targetDocument, sourceDocument); // Early bailout for the cases where NOT BOTH of the documents // contain the extension @@ -84,9 +89,20 @@ export class StructuralMetadataMerger { log( "Only source contains structural metadata - copying source to target" ); - copyToDocument(targetDocument, sourceDocument, [ - sourceStructuralMetadata, - ]); + + // The default glTF-Transform merge operation will copy the + // structural metadata (and store the mapping from the source + // to the target object), but it will not assign the extension + // object to the target root. So here, the copied object is + // obtained from the merge mapping, and assigned to the target + const targetRoot = targetDocument.getRoot(); + const copiedStructuralMetadata = mainMergeMap.get( + sourceStructuralMetadata + ) as StructuralMetadata; + targetRoot.setExtension( + "EXT_structural_metadata", + copiedStructuralMetadata + ); return; } @@ -102,6 +118,7 @@ export class StructuralMetadataMerger { targetStructuralMetadata, sourceDocument, sourceStructuralMetadata, + mainMergeMap, schemaUriResolver ); } @@ -116,6 +133,7 @@ export class StructuralMetadataMerger { * @param targetStructuralMetadata - The target extension object * @param sourceDocument - The source document * @param sourceStructuralMetadata - The source extension object + * @param mainMergeMap - The map from `mergeDocuments` * @param schemaUriResolver - A function that can resolve the `schemaUri` * and return the metadata schema JSON object * @returns A promise that resolves when the operation is finished @@ -125,6 +143,7 @@ export class StructuralMetadataMerger { targetStructuralMetadata: StructuralMetadata, sourceDocument: Document, sourceStructuralMetadata: StructuralMetadata, + mainMergeMap: Map, schemaUriResolver: (schemaUri: string) => Promise ) { const targetExtStructuralMetadata = targetDocument.createExtension( @@ -173,6 +192,10 @@ export class StructuralMetadataMerger { } log("Merging schemas..."); + + // Store the names/keys of the classes and enums in the + // target, as a baseline way of identifying whether + // the target schema changes during the merge const oldClassKeys = targetSchema.listClassKeys(); const oldEnumKeys = targetSchema.listEnumKeys(); @@ -183,6 +206,8 @@ export class StructuralMetadataMerger { sourceSchema ); + // If new classes or enums have been added, then assign + // a new ID to the schema const newClassKeys = targetSchema.listClassKeys(); const newEnumKeys = targetSchema.listEnumKeys(); if (oldClassKeys != newClassKeys || oldEnumKeys != newEnumKeys) { @@ -193,30 +218,37 @@ export class StructuralMetadataMerger { log("Merging property tables..."); StructuralMetadataMerger.mergePropertyTables( - targetDocument, targetStructuralMetadata, - sourceDocument, sourceStructuralMetadata, + mainMergeMap, sourceClassNamesInTarget ); log("Merging property textures..."); StructuralMetadataMerger.mergePropertyTextures( - targetDocument, targetStructuralMetadata, - sourceDocument, sourceStructuralMetadata, + mainMergeMap, sourceClassNamesInTarget ); + log("Updating mesh primitive property textures..."); + StructuralMetadataMerger.updateMeshPrimitivePropertyTextures( + sourceDocument, + mainMergeMap + ); log("Merging property attributes..."); StructuralMetadataMerger.mergePropertyAttributes( - targetDocument, targetStructuralMetadata, - sourceDocument, sourceStructuralMetadata, + mainMergeMap, sourceClassNamesInTarget ); + log("Updating mesh primitive property attributes..."); + StructuralMetadataMerger.updateMeshPrimitivePropertyAttributes( + sourceDocument, + mainMergeMap + ); const targetRoot = targetDocument.getRoot(); targetRoot.setExtension( @@ -231,28 +263,21 @@ export class StructuralMetadataMerger { * This will update the 'class' of the copied property tables according * to the given name mapping. * - * @param targetDocument - The target document * @param targetStructuralMetadata - The target extension object - * @param sourceDocument - The source document * @param sourceStructuralMetadata - The source extension object + * @param mainMergeMap - The mapping from `mergeDocuments` * @param sourceClassNamesInTarget - The mapping from class names in * the source schema to the names that they have in the target schema. */ private static mergePropertyTables( - targetDocument: Document, targetStructuralMetadata: StructuralMetadata, - sourceDocument: Document, sourceStructuralMetadata: StructuralMetadata, + mainMergeMap: Map, sourceClassNamesInTarget: { [key: string]: string } ) { const sourcePropertyTables = sourceStructuralMetadata.listPropertyTables(); - const targetPropertyTables = StructuralMetadataMerger.copyArray( - targetDocument, - sourceDocument, - sourcePropertyTables - ); - for (const targetPropertyTable of targetPropertyTables) { - const sourceClassName = targetPropertyTable.getClass(); + for (const sourcePropertyTable of sourcePropertyTables) { + const sourceClassName = sourcePropertyTable.getClass(); const targetClassName = sourceClassNamesInTarget[sourceClassName]; log( "Property table referred to class " + @@ -260,6 +285,9 @@ export class StructuralMetadataMerger { " and now refers to class " + targetClassName ); + const targetPropertyTable = mainMergeMap.get( + sourcePropertyTable + ) as PropertyTable; targetPropertyTable.setClass(targetClassName); targetStructuralMetadata.addPropertyTable(targetPropertyTable); } @@ -271,29 +299,22 @@ export class StructuralMetadataMerger { * This will update the 'class' of the copied property textures according * to the given name mapping. * - * @param targetDocument - The target document * @param targetStructuralMetadata - The target extension object - * @param sourceDocument - The source document * @param sourceStructuralMetadata - The source extension object + * @param mainMergeMap - The mapping from `mergeDocuments` * @param sourceClassNamesInTarget - The mapping from class names in * the source schema to the names that they have in the target schema. */ private static mergePropertyTextures( - targetDocument: Document, targetStructuralMetadata: StructuralMetadata, - sourceDocument: Document, sourceStructuralMetadata: StructuralMetadata, + mainMergeMap: Map, sourceClassNamesInTarget: { [key: string]: string } ) { const sourcePropertyTextures = sourceStructuralMetadata.listPropertyTextures(); - const targetPropertyTextures = StructuralMetadataMerger.copyArray( - targetDocument, - sourceDocument, - sourcePropertyTextures - ); - for (const targetPropertyTexture of targetPropertyTextures) { - const sourceClassName = targetPropertyTexture.getClass(); + for (const sourcePropertyTexture of sourcePropertyTextures) { + const sourceClassName = sourcePropertyTexture.getClass(); const targetClassName = sourceClassNamesInTarget[sourceClassName]; log( "Property texture referred to class " + @@ -301,40 +322,106 @@ export class StructuralMetadataMerger { " and now refers to class " + targetClassName ); + const targetPropertyTexture = mainMergeMap.get( + sourcePropertyTexture + ) as PropertyTexture; targetPropertyTexture.setClass(targetClassName); targetStructuralMetadata.addPropertyTexture(targetPropertyTexture); } } + /** + * Updates all mesh primitives to refer to the merged property textures. + * + * This will examine all mesh primitives in the given source document, + * and check whether they (and their counterpart in the target document) + * contain the `EXT_structural_metadata` extension object with + * property textures. + * + * If the extension object is found, then the property textures in + * the target will be cleared, and replaced by the property textures + * that have been created during the `mergeDocuments` call, to + * ensure that the property textures in the target are THE actual + * PropertyTexture objects that also appear in the top-level + * extension object. + * + * @param sourceDocument - The source document + * @param mainMergeMap - The mapping from `mergeDocuments` + */ + private static updateMeshPrimitivePropertyTextures( + sourceDocument: Document, + mainMergeMap: Map + ) { + const sourceRoot = sourceDocument.getRoot(); + const sourceMeshes = sourceRoot.listMeshes(); + for (const sourceMesh of sourceMeshes) { + const sourcePrimitives = sourceMesh.listPrimitives(); + for (const sourcePrimitive of sourcePrimitives) { + const targetPrimitive = mainMergeMap.get(sourcePrimitive) as Primitive; + + // Obtain the extension object from the source- and target + // mesh primitive + const sourceMeshPrimitiveStructuralMetadata = + sourcePrimitive.getExtension( + "EXT_structural_metadata" + ); + const targetMeshPrimitiveStructuralMetadata = + targetPrimitive.getExtension( + "EXT_structural_metadata" + ); + if ( + sourceMeshPrimitiveStructuralMetadata && + targetMeshPrimitiveStructuralMetadata + ) { + // Clear the property textures from the target mesh primitive + const oldTargetPropertyTextures = + targetMeshPrimitiveStructuralMetadata.listPropertyTextures(); + for (const oldTargetPropertyTexture of oldTargetPropertyTextures) { + targetMeshPrimitiveStructuralMetadata.removePropertyTexture( + oldTargetPropertyTexture + ); + } + + // Replace the property textures from the target mesh + // primitive with the ones that have been created during + // the `mergeDocuments` operation + const sourcePropertyTextures = + sourceMeshPrimitiveStructuralMetadata.listPropertyTextures(); + for (const sourcePropertyTexture of sourcePropertyTextures) { + const targetPropertyTexture = mainMergeMap.get( + sourcePropertyTexture + ) as PropertyTexture; + targetMeshPrimitiveStructuralMetadata.addPropertyTexture( + targetPropertyTexture + ); + } + } + } + } + } + /** * Merge the property attributes from the given source to the given targets. * * This will update the 'class' of the copied property attributes according * to the given name mapping. * - * @param targetDocument - The target document * @param targetStructuralMetadata - The target extension object - * @param sourceDocument - The source document * @param sourceStructuralMetadata - The source extension object + * @param mainMergeMap - The mapping from `mergeDocuments` * @param sourceClassNamesInTarget - The mapping from class names in * the source schema to the names that they have in the target schema. */ private static mergePropertyAttributes( - targetDocument: Document, targetStructuralMetadata: StructuralMetadata, - sourceDocument: Document, sourceStructuralMetadata: StructuralMetadata, + mainMergeMap: Map, sourceClassNamesInTarget: { [key: string]: string } ) { const sourcePropertyAttributes = sourceStructuralMetadata.listPropertyAttributes(); - const targetPropertyAttributes = StructuralMetadataMerger.copyArray( - targetDocument, - sourceDocument, - sourcePropertyAttributes - ); - for (const targetPropertyAttribute of targetPropertyAttributes) { - const sourceClassName = targetPropertyAttribute.getClass(); + for (const sourcePropertyAttribute of sourcePropertyAttributes) { + const sourceClassName = sourcePropertyAttribute.getClass(); const targetClassName = sourceClassNamesInTarget[sourceClassName]; log( "Property attribute referred to class " + @@ -342,11 +429,84 @@ export class StructuralMetadataMerger { " and now refers to class " + targetClassName ); + const targetPropertyAttribute = mainMergeMap.get( + sourcePropertyAttribute + ) as PropertyAttribute; targetPropertyAttribute.setClass(targetClassName); targetStructuralMetadata.addPropertyAttribute(targetPropertyAttribute); } } + /** + * Updates all mesh primitives to refer to the merged property attributes. + * + * This will examine all mesh primitives in the given source document, + * and check whether they (and their counterpart in the target document) + * contain the `EXT_structural_metadata` extension object with + * property attributes. + * + * If the extension object is found, then the property attributes in + * the target will be cleared, and replaced by the property attributes + * that have been created during the `mergeDocuments` call, to + * ensure that the property attributes in the target are THE actual + * PropertyAttribute objects that also appear in the top-level + * extension object. + * + * @param sourceDocument - The source document + * @param mainMergeMap - The mapping from `mergeDocuments` + */ + private static updateMeshPrimitivePropertyAttributes( + sourceDocument: Document, + mainMergeMap: Map + ) { + const sourceRoot = sourceDocument.getRoot(); + const sourceMeshes = sourceRoot.listMeshes(); + for (const sourceMesh of sourceMeshes) { + const sourcePrimitives = sourceMesh.listPrimitives(); + for (const sourcePrimitive of sourcePrimitives) { + const targetPrimitive = mainMergeMap.get(sourcePrimitive) as Primitive; + + // Obtain the extension object from the source- and target + // mesh primitive + const sourceMeshPrimitiveStructuralMetadata = + sourcePrimitive.getExtension( + "EXT_structural_metadata" + ); + const targetMeshPrimitiveStructuralMetadata = + targetPrimitive.getExtension( + "EXT_structural_metadata" + ); + if ( + sourceMeshPrimitiveStructuralMetadata && + targetMeshPrimitiveStructuralMetadata + ) { + // Clear the property attributes from the target mesh primitive + const oldTargetPropertyAttributes = + targetMeshPrimitiveStructuralMetadata.listPropertyAttributes(); + for (const oldTargetPropertyAttribute of oldTargetPropertyAttributes) { + targetMeshPrimitiveStructuralMetadata.removePropertyAttribute( + oldTargetPropertyAttribute + ); + } + + // Replace the property attributes from the target mesh + // primitive with the ones that have been created during + // the `mergeDocuments` operation + const sourcePropertyAttributes = + sourceMeshPrimitiveStructuralMetadata.listPropertyAttributes(); + for (const sourcePropertyAttribute of sourcePropertyAttributes) { + const targetPropertyAttribute = mainMergeMap.get( + sourcePropertyAttribute + ) as PropertyAttribute; + targetMeshPrimitiveStructuralMetadata.addPropertyAttribute( + targetPropertyAttribute + ); + } + } + } + } + } + /** * Merge the given `EXT_structural_metadata` schema objects from * the given documents. @@ -684,29 +844,6 @@ export class StructuralMetadataMerger { return targetElement; } - /** - * Copy an array of objects from the given source document to the - * given target document, and return the copies. - * - * @param targetDocument - The target document - * @param sourceDocument - The source document - * @param sourceElement - The source objects - * @returns The target objects - */ - private static copyArray>( - targetDocument: Document, - sourceDocument: Document, - sourceElements: T[] - ): T[] { - const mapping = copyToDocument( - targetDocument, - sourceDocument, - sourceElements - ); - const targetElements = sourceElements.map((e: T) => mapping.get(e) as T); - return targetElements; - } - /** * Disambiguate the given name against the existing names. * From e9a85253a8aa310698be51e4b3fe1b9fb37e95dc Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 7 May 2024 15:43:55 +0200 Subject: [PATCH 26/37] Unpartition after merge. Fix typo. --- .../tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts | 2 +- src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts b/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts index 62e0bced..ccca083a 100644 --- a/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts +++ b/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts @@ -416,7 +416,7 @@ async function addSpecPropertyTexture(document: Document, className: string) { const image = await savePixels(pixels, "image/png"); const texture = document.createTexture(); texture.setImage(image); - texture.setURI("propertyTextuer.png"); + texture.setURI("propertyTexture.png"); // Create and add a property texture with the following structure: // { diff --git a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts index 17885155..62e8d8b7 100644 --- a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts +++ b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts @@ -4,6 +4,8 @@ import { Document, Primitive } from "@gltf-transform/core"; import { Property } from "@gltf-transform/core"; import { IProperty } from "@gltf-transform/core"; +import { unpartition } from "@gltf-transform/functions"; + import { EXTStructuralMetadata } from "../../gltf-extensions/"; import { StructuralMetadata } from "../../gltf-extensions/"; import { MeshPrimitiveStructuralMetadata } from "../../gltf-extensions/"; @@ -103,6 +105,7 @@ export class StructuralMetadataMerger { "EXT_structural_metadata", copiedStructuralMetadata ); + await targetDocument.transform(unpartition()); return; } @@ -121,6 +124,7 @@ export class StructuralMetadataMerger { mainMergeMap, schemaUriResolver ); + await targetDocument.transform(unpartition()); } } From fa57ab7c269300891ca8dbfe555ab7e7916ecc52 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 8 May 2024 15:59:06 +0200 Subject: [PATCH 27/37] Deduplicating property attributes and textures --- .../StructuralMetadataMergerSpec.ts | 195 +++++++--- .../StructuralMetadataMerger.ts | 336 ++++++++++++------ 2 files changed, 379 insertions(+), 152 deletions(-) diff --git a/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts b/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts index ccca083a..dcf30994 100644 --- a/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts +++ b/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts @@ -14,12 +14,21 @@ import { StructuralMetadataPropertyTexture as PropertyTexture } from "../../../s import { BinaryPropertyTableBuilder } from "../../../src/metadata/"; -import { - GltfTransform, - StructuralMetadataPropertyTables, -} from "../../../src/tools/"; +import { GltfTransform } from "../../../src/tools/"; +import { StructuralMetadataPropertyTables } from "../../../src/tools/"; import { StructuralMetadataMerger } from "../../../src/tools/"; +// A compile-time flag that indicates that the documents that result +// from the merge operation should be serialized with glTF-Transform. +// This does impose an overhead, but should usually be `true`, because +// it is a very thorough consistency check. +const SERIALIZE_DOCUMENTS = true; + +// A compile-time flag that indicates that the documents that are +// created due to the SERIALIZE_DOCUMENTS flag should be logged +// to the console. This should be `true` ONLY for debugging! +const LOG_SERIALIZED_DOCUMENTS = true; + // A dummy resolver for the `schemaUri` that may be found in metadata const specSchemaUriResolver = async (schemaUri: string) => { console.error("The specSchemaUriResolver should not be called!"); @@ -391,12 +400,18 @@ function addSpecPropertyAttribute(document: Document, className: string) { * - a property texture in the top-level metadata extension object for * such a property. It will refer to the texture with index 0, * texture coordinate set 0, and channels [0] - * - a texture that contains "dummy" data in channel 0 + * - a texture that contains dummy data in channel 0 * * @param document - The document * @param className - The metadata class name + * @param redPixelValue - The value of the 'red' component of the + * pixels, i.e. the values in channel 0 */ -async function addSpecPropertyTexture(document: Document, className: string) { +async function addSpecPropertyTexture( + document: Document, + className: string, + redPixelValue: number +) { const propertyName = "example_INT8_SCALAR"; // Obtain the top-level extension object @@ -410,7 +425,7 @@ async function addSpecPropertyTexture(document: Document, className: string) { const pixels = NdArray(new Uint8Array(sizeX * sizeY), [sizeX, sizeY, 4]); for (let x = 0; x < pixels.shape[0]; x++) { for (let y = 0; y < pixels.shape[1]; y++) { - pixels.set(x, y, 0, x * sizeY + y); + pixels.set(x, y, 0, redPixelValue); } } const image = await savePixels(pixels, "image/png"); @@ -457,7 +472,7 @@ async function addSpecPropertyTexture(document: Document, className: string) { ); } -describe("StructuralMetadataMerger", function () { +fdescribe("StructuralMetadataMerger", function () { //========================================================================== // Basic class merging @@ -783,7 +798,7 @@ describe("StructuralMetadataMerger", function () { //========================================================================== // Basic property table merging - fit("merges the set of property tables for one resulting class", async function () { + it("merges the set of property tables for one resulting class", async function () { // The classes have the same name/key // The classes are structurally equal // The property tables refer to the respective class (which turns out to be equal) @@ -870,15 +885,16 @@ describe("StructuralMetadataMerger", function () { expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); expect(getNumPropertyTables(document)).toBe(2); - // XXX Try to write the document as a basic consistency check: - { + if (SERIALIZE_DOCUMENTS) { const io = await GltfTransform.getIO(); const jsonDocument = await io.writeJSON(document); - console.log(JSON.stringify(jsonDocument.json, null, 2)); + if (LOG_SERIALIZED_DOCUMENTS) { + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } } }); - fit("merges the set of property tables and updates their class for disambiguated classes", async function () { + it("merges the set of property tables and updates their class for disambiguated classes", async function () { // The classes have the same name/key // The classes are structurally different // The property tables refer to the respective class @@ -973,24 +989,25 @@ describe("StructuralMetadataMerger", function () { const propertyTable1 = getPropertyTable(document, 1); expect(propertyTable1.getClass()).toBe("exampleClass_0"); - // XXX Try to write the document as a basic consistency check: - { + if (SERIALIZE_DOCUMENTS) { const io = await GltfTransform.getIO(); const jsonDocument = await io.writeJSON(document); - console.log(JSON.stringify(jsonDocument.json, null, 2)); + if (LOG_SERIALIZED_DOCUMENTS) { + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } } }); //========================================================================== // Basic property attribute merging - fit("merges the set of property attributes for one resulting class", async function () { + it("merges the set of property attributes for one resulting class", async function () { // The classes have the same name/key // The classes are structurally equal // The property attributes refer to the respective class (which turns out to be equal) // The result should be: // One class - // Two property attributes + // ONE property attribute (because the property attributes turn out to be equal as well!) const schemaA = { id: "EXAMPLE_SCHEMA_ID", @@ -1063,25 +1080,26 @@ describe("StructuralMetadataMerger", function () { // After merging in the second document: // There should still be one class - // There should be two property attributes + // There should be ONE property attribute (because they are equal) expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); - expect(getNumPropertyAttributes(document)).toBe(2); + expect(getNumPropertyAttributes(document)).toBe(1); - // XXX Try to write the document as a basic consistency check: - { + if (SERIALIZE_DOCUMENTS) { const io = await GltfTransform.getIO(); const jsonDocument = await io.writeJSON(document); - console.log(JSON.stringify(jsonDocument.json, null, 2)); + if (LOG_SERIALIZED_DOCUMENTS) { + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } } }); - fit("merges the set of property attributes and updates their class for disambiguated classes", async function () { + it("merges the set of property attributes and updates their class for disambiguated classes", async function () { // The classes have the same name/key // The classes are structurally different // The property attributes refer to the respective class // The result should be: // Two classes - // Two property attributes + // Two property attributes (because the second one refers to a different class now!) const schemaA = { id: "EXAMPLE_SCHEMA_ID", @@ -1165,21 +1183,116 @@ describe("StructuralMetadataMerger", function () { const propertyAttribute1 = getPropertyAttribute(document, 1); expect(propertyAttribute1.getClass()).toBe("exampleClass_0"); - // XXX Try to write the document as a basic consistency check: - { + if (SERIALIZE_DOCUMENTS) { const io = await GltfTransform.getIO(); const jsonDocument = await io.writeJSON(document); - console.log(JSON.stringify(jsonDocument.json, null, 2)); + if (LOG_SERIALIZED_DOCUMENTS) { + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } } }); //========================================================================== // Basic property texture merging - fit("merges the set of property textures for one resulting class", async function () { + fit("merges the set of equal property textures for one resulting class", async function () { + // The classes have the same name/key + // The classes are structurally equal + // The property textures refer to the respective class (which turns out to be equal) + // The property textures have the same content (i.e. they are equal) + // The result should be: + // One class + // ONE property texture (because they turn out to be equal) + + const schemaA = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_INT8_SCALAR: { + name: "Example SCALAR property with INT8 components", + description: + "An example property, with type SCALAR, with component type INT8", + type: "SCALAR", + componentType: "INT8", + array: false, + normalized: false, + }, + }, + }, + }, + }; + + const schemaB = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_INT8_SCALAR: { + name: "Example SCALAR property with INT8 components", + description: + "An example property, with type SCALAR, with component type INT8", + type: "SCALAR", + componentType: "INT8", + array: false, + normalized: false, + }, + }, + }, + }, + }; + + const documentA = new Document(); + documentA.createBuffer(); + assignMetadataSchema(documentA, schemaA); + await addSpecPropertyTexture(documentA, "exampleClass", 12); // Same as below + + const documentB = new Document(); + documentB.createBuffer(); + assignMetadataSchema(documentB, schemaB); + await addSpecPropertyTexture(documentB, "exampleClass", 12); // Same as above + + const document = new Document(); + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentA, + specSchemaUriResolver + ); + + // After merging in the first document: + // There should be one class + // There should be one property texture + expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); + expect(getNumPropertyTextures(document)).toBe(1); + + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentB, + specSchemaUriResolver + ); + + // After merging in the second document: + // There should still be one class + // There should be ONE property texture (because the inputs are equal) + expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); + expect(getNumPropertyTextures(document)).toBe(1); + + if (SERIALIZE_DOCUMENTS) { + const io = await GltfTransform.getIO(); + const jsonDocument = await io.writeJSON(document); + if (LOG_SERIALIZED_DOCUMENTS) { + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } + } + }); + + fit("merges the set of different property textures for one resulting class", async function () { // The classes have the same name/key // The classes are structurally equal // The property textures refer to the respective class (which turns out to be equal) + // The property textures have different content (i.e. they are not equal) // The result should be: // One class // Two property textures @@ -1227,12 +1340,12 @@ describe("StructuralMetadataMerger", function () { const documentA = new Document(); documentA.createBuffer(); assignMetadataSchema(documentA, schemaA); - await addSpecPropertyTexture(documentA, "exampleClass"); + await addSpecPropertyTexture(documentA, "exampleClass", 12); // Other than below const documentB = new Document(); documentB.createBuffer(); assignMetadataSchema(documentB, schemaB); - await addSpecPropertyTexture(documentB, "exampleClass"); + await addSpecPropertyTexture(documentB, "exampleClass", 23); // Other than above const document = new Document(); await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( @@ -1259,15 +1372,16 @@ describe("StructuralMetadataMerger", function () { expect(getMetadataClassNames(document)).toEqual(["exampleClass"]); expect(getNumPropertyTextures(document)).toBe(2); - // XXX Try to write the document as a basic consistency check: - { + if (SERIALIZE_DOCUMENTS) { const io = await GltfTransform.getIO(); const jsonDocument = await io.writeJSON(document); - console.log(JSON.stringify(jsonDocument.json, null, 2)); + if (LOG_SERIALIZED_DOCUMENTS) { + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } } }); - fit("merges the set of property textures and updates their class for disambiguated classes", async function () { + it("merges the set of property textures and updates their class for disambiguated classes", async function () { // The classes have the same name/key // The classes are structurally different // The property textures refer to the respective class @@ -1319,12 +1433,12 @@ describe("StructuralMetadataMerger", function () { const documentA = new Document(); documentA.createBuffer(); assignMetadataSchema(documentA, schemaA); - await addSpecPropertyTexture(documentA, "exampleClass"); + await addSpecPropertyTexture(documentA, "exampleClass", 12); const documentB = new Document(); documentB.createBuffer(); assignMetadataSchema(documentB, schemaB); - await addSpecPropertyTexture(documentB, "exampleClass"); + await addSpecPropertyTexture(documentB, "exampleClass", 23); const document = new Document(); await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( @@ -1358,11 +1472,12 @@ describe("StructuralMetadataMerger", function () { const propertyTexture1 = getPropertyTexture(document, 1); expect(propertyTexture1.getClass()).toBe("exampleClass_0"); - // XXX Try to write the document as a basic consistency check: - { + if (SERIALIZE_DOCUMENTS) { const io = await GltfTransform.getIO(); const jsonDocument = await io.writeJSON(document); - console.log(JSON.stringify(jsonDocument.json, null, 2)); + if (LOG_SERIALIZED_DOCUMENTS) { + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } } }); }); diff --git a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts index 62e8d8b7..29506401 100644 --- a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts +++ b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts @@ -236,9 +236,9 @@ export class StructuralMetadataMerger { sourceClassNamesInTarget ); log("Updating mesh primitive property textures..."); - StructuralMetadataMerger.updateMeshPrimitivePropertyTextures( - sourceDocument, - mainMergeMap + StructuralMetadataMerger.updateMeshPrimitivesPropertyTextures( + targetDocument, + targetStructuralMetadata ); log("Merging property attributes..."); @@ -249,9 +249,9 @@ export class StructuralMetadataMerger { sourceClassNamesInTarget ); log("Updating mesh primitive property attributes..."); - StructuralMetadataMerger.updateMeshPrimitivePropertyAttributes( - sourceDocument, - mainMergeMap + StructuralMetadataMerger.updateMeshPrimitivesPropertyAttributes( + targetDocument, + targetStructuralMetadata ); const targetRoot = targetDocument.getRoot(); @@ -303,6 +303,12 @@ export class StructuralMetadataMerger { * This will update the 'class' of the copied property textures according * to the given name mapping. * + * The given target structural metadata will afterwards contain the set + * of unique textures. This means that when there are two textures + * that are `equal` according to the glTF-Transform `Property.equal` + * implementation, then the texture will only be added once to the + * target. + * * @param targetStructuralMetadata - The target extension object * @param sourceStructuralMetadata - The source extension object * @param mainMergeMap - The mapping from `mergeDocuments` @@ -317,6 +323,8 @@ export class StructuralMetadataMerger { ) { const sourcePropertyTextures = sourceStructuralMetadata.listPropertyTextures(); + const targetPropertyTextures = + targetStructuralMetadata.listPropertyTextures(); for (const sourcePropertyTexture of sourcePropertyTextures) { const sourceClassName = sourcePropertyTexture.getClass(); const targetClassName = sourceClassNamesInTarget[sourceClassName]; @@ -330,77 +338,113 @@ export class StructuralMetadataMerger { sourcePropertyTexture ) as PropertyTexture; targetPropertyTexture.setClass(targetClassName); - targetStructuralMetadata.addPropertyTexture(targetPropertyTexture); + const alreadyExists = StructuralMetadataMerger.containsEqual( + targetPropertyTextures, + targetPropertyTexture + ); + if (!alreadyExists) { + targetPropertyTextures.push(targetPropertyTexture); + targetStructuralMetadata.addPropertyTexture(targetPropertyTexture); + } } } /** * Updates all mesh primitives to refer to the merged property textures. * - * This will examine all mesh primitives in the given source document, - * and check whether they (and their counterpart in the target document) - * contain the `EXT_structural_metadata` extension object with - * property textures. + * This will examine all mesh primitives in the given target document, + * and check whether they contain the `EXT_structural_metadata` extension + * object with property textures. * * If the extension object is found, then the property textures in - * the target will be cleared, and replaced by the property textures - * that have been created during the `mergeDocuments` call, to - * ensure that the property textures in the target are THE actual - * PropertyTexture objects that also appear in the top-level - * extension object. + * the target mesh primitive will be updated, using + * `updateMeshPrimitivePropertyTextures`. * - * @param sourceDocument - The source document - * @param mainMergeMap - The mapping from `mergeDocuments` + * @param targetDocument - The target document + * @param targetStructuralMetadata - The target root extension object */ - private static updateMeshPrimitivePropertyTextures( - sourceDocument: Document, - mainMergeMap: Map + private static updateMeshPrimitivesPropertyTextures( + targetDocument: Document, + targetStructuralMetadata: StructuralMetadata ) { - const sourceRoot = sourceDocument.getRoot(); - const sourceMeshes = sourceRoot.listMeshes(); - for (const sourceMesh of sourceMeshes) { - const sourcePrimitives = sourceMesh.listPrimitives(); - for (const sourcePrimitive of sourcePrimitives) { - const targetPrimitive = mainMergeMap.get(sourcePrimitive) as Primitive; - - // Obtain the extension object from the source- and target - // mesh primitive - const sourceMeshPrimitiveStructuralMetadata = - sourcePrimitive.getExtension( - "EXT_structural_metadata" - ); + const targetRootPropertyTextures = + targetStructuralMetadata.listPropertyTextures(); + + const targetRoot = targetDocument.getRoot(); + const targetMeshes = targetRoot.listMeshes(); + for (const targetMesh of targetMeshes) { + const targetPrimitives = targetMesh.listPrimitives(); + for (const targetPrimitive of targetPrimitives) { const targetMeshPrimitiveStructuralMetadata = targetPrimitive.getExtension( "EXT_structural_metadata" ); + if (targetMeshPrimitiveStructuralMetadata) { + StructuralMetadataMerger.updateMeshPrimitivePropertyTextures( + targetRootPropertyTextures, + targetMeshPrimitiveStructuralMetadata + ); + } + } + } + } + + /** + * Updates the extension object of a mesh primitive to refer to the + * merged property textures. + * + * The property textures in the given extension object will be + * cleared, and replaced by the property textures that have been + * created in the root extension object, to ensure that the property + * textures in the mesh primitive extension object are THE actual + * PropertyTexture objects that also appear in the top-level + * extension object. + * + * @param targetRootPropertyTextures - The property textures from + * the root extension object of the target + * @param targetMeshPrimitiveStructuralMetadata - The extension object + * from a mesh primitive + */ + private static updateMeshPrimitivePropertyTextures( + targetRootPropertyTextures: PropertyTexture[], + targetMeshPrimitiveStructuralMetadata: MeshPrimitiveStructuralMetadata + ) { + // Clear the property textures from the target mesh primitive + const oldTargetMeshPrimitivePropertyTextures = + targetMeshPrimitiveStructuralMetadata.listPropertyTextures(); + for (const oldTargetMeshPrimitivePropertyTexture of oldTargetMeshPrimitivePropertyTextures) { + targetMeshPrimitiveStructuralMetadata.removePropertyTexture( + oldTargetMeshPrimitivePropertyTexture + ); + } + + // Go through the list of old property texture objects, + // and replace them with the ones from the root extension + // object that are 'equal' to the old ones. (They will + // not be 'identical', as in '==' or '===' - what counts + // here is equality in the sense of the glTF-Transform + // `Property.equals` function) + for (const oldTargetMeshPrimitivePropertyTexture of oldTargetMeshPrimitivePropertyTextures) { + let newTargetMeshPrimitivePropertyTexture: PropertyTexture | null = null; + for (let i = 0; i < targetRootPropertyTextures.length; i++) { if ( - sourceMeshPrimitiveStructuralMetadata && - targetMeshPrimitiveStructuralMetadata + oldTargetMeshPrimitivePropertyTexture.equals( + targetRootPropertyTextures[i] + ) ) { - // Clear the property textures from the target mesh primitive - const oldTargetPropertyTextures = - targetMeshPrimitiveStructuralMetadata.listPropertyTextures(); - for (const oldTargetPropertyTexture of oldTargetPropertyTextures) { - targetMeshPrimitiveStructuralMetadata.removePropertyTexture( - oldTargetPropertyTexture - ); - } - - // Replace the property textures from the target mesh - // primitive with the ones that have been created during - // the `mergeDocuments` operation - const sourcePropertyTextures = - sourceMeshPrimitiveStructuralMetadata.listPropertyTextures(); - for (const sourcePropertyTexture of sourcePropertyTextures) { - const targetPropertyTexture = mainMergeMap.get( - sourcePropertyTexture - ) as PropertyTexture; - targetMeshPrimitiveStructuralMetadata.addPropertyTexture( - targetPropertyTexture - ); - } + newTargetMeshPrimitivePropertyTexture = targetRootPropertyTextures[i]; + break; } } + if (newTargetMeshPrimitivePropertyTexture === null) { + throw new MetadataError( + "Could not find mesh primitive property texture in root extension object" + ); + } else { + targetMeshPrimitiveStructuralMetadata.addPropertyTexture( + newTargetMeshPrimitivePropertyTexture + ); + } } } @@ -410,6 +454,12 @@ export class StructuralMetadataMerger { * This will update the 'class' of the copied property attributes according * to the given name mapping. * + * The given target structural metadata will afterwards contain the set + * of unique attributes. This means that when there are two attributes + * that are `equal` according to the glTF-Transform `Property.equal` + * implementation, then the attribute will only be added once to the + * target. + * * @param targetStructuralMetadata - The target extension object * @param sourceStructuralMetadata - The source extension object * @param mainMergeMap - The mapping from `mergeDocuments` @@ -424,6 +474,8 @@ export class StructuralMetadataMerger { ) { const sourcePropertyAttributes = sourceStructuralMetadata.listPropertyAttributes(); + const targetPropertyAttributes = + targetStructuralMetadata.listPropertyAttributes(); for (const sourcePropertyAttribute of sourcePropertyAttributes) { const sourceClassName = sourcePropertyAttribute.getClass(); const targetClassName = sourceClassNamesInTarget[sourceClassName]; @@ -437,77 +489,115 @@ export class StructuralMetadataMerger { sourcePropertyAttribute ) as PropertyAttribute; targetPropertyAttribute.setClass(targetClassName); - targetStructuralMetadata.addPropertyAttribute(targetPropertyAttribute); + const alreadyExists = StructuralMetadataMerger.containsEqual( + targetPropertyAttributes, + targetPropertyAttribute + ); + if (!alreadyExists) { + targetPropertyAttributes.push(targetPropertyAttribute); + targetStructuralMetadata.addPropertyAttribute(targetPropertyAttribute); + } } } /** * Updates all mesh primitives to refer to the merged property attributes. * - * This will examine all mesh primitives in the given source document, - * and check whether they (and their counterpart in the target document) - * contain the `EXT_structural_metadata` extension object with - * property attributes. + * This will examine all mesh primitives in the given target document, + * and check whether they contain the `EXT_structural_metadata` extension + * object with property attributes. * * If the extension object is found, then the property attributes in - * the target will be cleared, and replaced by the property attributes - * that have been created during the `mergeDocuments` call, to - * ensure that the property attributes in the target are THE actual - * PropertyAttribute objects that also appear in the top-level - * extension object. + * the target mesh primitive will be updated, using + * `updateMeshPrimitivePropertyAttributes`. * - * @param sourceDocument - The source document - * @param mainMergeMap - The mapping from `mergeDocuments` + * @param targetDocument - The target document + * @param targetStructuralMetadata - The target root extension object */ - private static updateMeshPrimitivePropertyAttributes( - sourceDocument: Document, - mainMergeMap: Map + private static updateMeshPrimitivesPropertyAttributes( + targetDocument: Document, + targetStructuralMetadata: StructuralMetadata ) { - const sourceRoot = sourceDocument.getRoot(); - const sourceMeshes = sourceRoot.listMeshes(); - for (const sourceMesh of sourceMeshes) { - const sourcePrimitives = sourceMesh.listPrimitives(); - for (const sourcePrimitive of sourcePrimitives) { - const targetPrimitive = mainMergeMap.get(sourcePrimitive) as Primitive; - - // Obtain the extension object from the source- and target - // mesh primitive - const sourceMeshPrimitiveStructuralMetadata = - sourcePrimitive.getExtension( - "EXT_structural_metadata" - ); + const targetRootPropertyAttributes = + targetStructuralMetadata.listPropertyAttributes(); + + const targetRoot = targetDocument.getRoot(); + const targetMeshes = targetRoot.listMeshes(); + for (const targetMesh of targetMeshes) { + const targetPrimitives = targetMesh.listPrimitives(); + for (const targetPrimitive of targetPrimitives) { const targetMeshPrimitiveStructuralMetadata = targetPrimitive.getExtension( "EXT_structural_metadata" ); + if (targetMeshPrimitiveStructuralMetadata) { + StructuralMetadataMerger.updateMeshPrimitivePropertyAttributes( + targetRootPropertyAttributes, + targetMeshPrimitiveStructuralMetadata + ); + } + } + } + } + + /** + * Updates the extension object of a mesh primitive to refer to the + * merged property attributes. + * + * The property attributes in the given extension object will be + * cleared, and replaced by the property attributes that have been + * created in the root extension object, to ensure that the property + * attributes in the mesh primitive extension object are THE actual + * PropertyAttribute objects that also appear in the top-level + * extension object. + * + * @param targetRootPropertyAttributes - The property attributes from + * the root extension object of the target + * @param targetMeshPrimitiveStructuralMetadata - The extension object + * from a mesh primitive + */ + private static updateMeshPrimitivePropertyAttributes( + targetRootPropertyAttributes: PropertyAttribute[], + targetMeshPrimitiveStructuralMetadata: MeshPrimitiveStructuralMetadata + ) { + // Clear the property attributes from the target mesh primitive + const oldTargetMeshPrimitivePropertyAttributes = + targetMeshPrimitiveStructuralMetadata.listPropertyAttributes(); + for (const oldTargetMeshPrimitivePropertyAttribute of oldTargetMeshPrimitivePropertyAttributes) { + targetMeshPrimitiveStructuralMetadata.removePropertyAttribute( + oldTargetMeshPrimitivePropertyAttribute + ); + } + + // Go through the list of old property attribute objects, + // and replace them with the ones from the root extension + // object that are 'equal' to the old ones. (They will + // not be 'identical', as in '==' or '===' - what counts + // here is equality in the sense of the glTF-Transform + // `Property.equals` function) + for (const oldTargetMeshPrimitivePropertyAttribute of oldTargetMeshPrimitivePropertyAttributes) { + let newTargetMeshPrimitivePropertyAttribute: PropertyAttribute | null = + null; + for (let i = 0; i < targetRootPropertyAttributes.length; i++) { if ( - sourceMeshPrimitiveStructuralMetadata && - targetMeshPrimitiveStructuralMetadata + oldTargetMeshPrimitivePropertyAttribute.equals( + targetRootPropertyAttributes[i] + ) ) { - // Clear the property attributes from the target mesh primitive - const oldTargetPropertyAttributes = - targetMeshPrimitiveStructuralMetadata.listPropertyAttributes(); - for (const oldTargetPropertyAttribute of oldTargetPropertyAttributes) { - targetMeshPrimitiveStructuralMetadata.removePropertyAttribute( - oldTargetPropertyAttribute - ); - } - - // Replace the property attributes from the target mesh - // primitive with the ones that have been created during - // the `mergeDocuments` operation - const sourcePropertyAttributes = - sourceMeshPrimitiveStructuralMetadata.listPropertyAttributes(); - for (const sourcePropertyAttribute of sourcePropertyAttributes) { - const targetPropertyAttribute = mainMergeMap.get( - sourcePropertyAttribute - ) as PropertyAttribute; - targetMeshPrimitiveStructuralMetadata.addPropertyAttribute( - targetPropertyAttribute - ); - } + newTargetMeshPrimitivePropertyAttribute = + targetRootPropertyAttributes[i]; + break; } } + if (newTargetMeshPrimitivePropertyAttribute === null) { + throw new MetadataError( + "Could not find mesh primitive property attribute in root extension object" + ); + } else { + targetMeshPrimitiveStructuralMetadata.addPropertyAttribute( + newTargetMeshPrimitivePropertyAttribute + ); + } } } @@ -624,7 +714,7 @@ export class StructuralMetadataMerger { log( "Source enum " + sourceEnumKey + - " is stored as " + + " is copied and stored as " + targetEnumKey + " in the target" ); @@ -738,7 +828,11 @@ export class StructuralMetadataMerger { if (sourceClass.equals(existingTargetClass)) { // When the source class and the existing target class // are equal, then nothing has to be done - log("Source class " + sourceClassKey + " already exists "); + log( + "Source class " + + sourceClassKey + + " is equal to one that already exists in the target" + ); sourceClassNamesInTarget[sourceClassKey] = sourceClassKey; } else { // Otherwise, the source class is copied to the target and @@ -827,6 +921,24 @@ export class StructuralMetadataMerger { } } + /** + * Returns whether the given array of glTF-Transform `Property` objects + * contains an objec that is equal to the given one, based on the + * glTF-Transform `Property.equal` implementation. + * + * @param properties - The properties + * @param property - The property + * @returns The result + */ + private static containsEqual(properties: Property[], property: Property) { + for (const p of properties) { + if (p.equals(property)) { + return true; + } + } + return false; + } + /** * Copy a single object from the given source document to the * given target document, and return the copy. From 39ed3cc407dc781aa3875c0e5da844c714951f7d Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 8 May 2024 19:31:23 +0200 Subject: [PATCH 28/37] More rigorous error check in writer --- src/gltf-extensions/gltfExtensions/EXTInstanceFeatures.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gltf-extensions/gltfExtensions/EXTInstanceFeatures.ts b/src/gltf-extensions/gltfExtensions/EXTInstanceFeatures.ts index 461f5059..720eb276 100644 --- a/src/gltf-extensions/gltfExtensions/EXTInstanceFeatures.ts +++ b/src/gltf-extensions/gltfExtensions/EXTInstanceFeatures.ts @@ -151,6 +151,9 @@ export class EXTInstanceFeatures extends Extension { if (structuralMetadata) { const propertyTables = structuralMetadata.listPropertyTables(); propertyTableDef = propertyTables.indexOf(propertyTable); + if (propertyTableDef < 0) { + throw new Error(`${NAME}: Invalid property table in feature ID`); + } } else { throw new Error( `${NAME}: No EXT_structural_metadata definition for looking up property table index` From d094c07a8f0a2d8c469cc302fa807a012abcb0a2 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 8 May 2024 19:33:03 +0200 Subject: [PATCH 29/37] Use new merge function. Generalize resolver name. --- src/tools/contentProcessing/GltfTransform.ts | 15 ++++++-- src/tools/migration/TileFormatsMigration.ts | 18 +++++----- .../migration/TileFormatsMigrationCmpt.ts | 34 +++++++++++++++---- .../migration/TileFormatsMigrationI3dm.ts | 10 +++--- .../tilesetProcessing/TilesetUpgrader.ts | 9 ++--- 5 files changed, 60 insertions(+), 26 deletions(-) diff --git a/src/tools/contentProcessing/GltfTransform.ts b/src/tools/contentProcessing/GltfTransform.ts index 563b20ed..a7db29e2 100644 --- a/src/tools/contentProcessing/GltfTransform.ts +++ b/src/tools/contentProcessing/GltfTransform.ts @@ -17,6 +17,8 @@ import { EXTStructuralMetadata } from "../../gltf-extensions"; import { EXTMeshFeatures } from "../../gltf-extensions"; import { EXTInstanceFeatures } from "../../gltf-extensions"; +import { StructuralMetadataMerger } from "../gltfExtensionsUtils/StructuralMetadataMerger"; + /** * Utilities for using glTF-Transform in the 3D Tiles tools * @@ -122,15 +124,24 @@ export class GltfTransform { * GLBs. * * @param inputGlbBuffers - The buffers containing GLB data + * @param schemaUriResolver - A function that can resolve the `schemaUri` + * and return the metadata schema JSON object * @returns The merged document */ - static async merge(inputGlbBuffers: Buffer[]): Promise { + static async merge( + inputGlbBuffers: Buffer[], + schemaUriResolver: (schemaUri: string) => Promise + ): Promise { // Create one document from each buffer and merge them const io = await GltfTransform.getIO(); const mergedDocument = new Document(); for (const inputGlbBuffer of inputGlbBuffers) { const inputDocument = await io.readBinary(inputGlbBuffer); - mergedDocument.merge(inputDocument); + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + mergedDocument, + inputDocument, + schemaUriResolver + ); } // Combine all scenes into one await GltfTransform.combineScenes(mergedDocument); diff --git a/src/tools/migration/TileFormatsMigration.ts b/src/tools/migration/TileFormatsMigration.ts index 58080139..f18e7172 100644 --- a/src/tools/migration/TileFormatsMigration.ts +++ b/src/tools/migration/TileFormatsMigration.ts @@ -43,18 +43,18 @@ export class TileFormatsMigration { * Convert the given I3DM data into a glTF asset * * @param i3dmBuffer - The I3DM buffer - * @param externalGlbResolver - A function that will be used to resolve - * external GLB data if the I3DM uses `header.gltfFormat=0` (meaning - * that the payload is not GLB data, but only a GLB URI). + * @param externalResourceResolver - A function that will be used to resolve + * external resources, like GLB data if the I3DM uses `header.gltfFormat=0` + * (meaning that the payload is not GLB data, but only a GLB URI). * @returns The GLB buffer */ static async convertI3dmToGlb( i3dmBuffer: Buffer, - externalGlbResolver: (uri: string) => Promise + externalResourceResolver: (uri: string) => Promise ): Promise { return await TileFormatsMigrationI3dm.convertI3dmToGlb( i3dmBuffer, - externalGlbResolver + externalResourceResolver ); } @@ -62,19 +62,19 @@ export class TileFormatsMigration { * Convert the given CMPT data into a single glTF asset * * @param cmptBuffer - The CMPT buffer - * @param externalGlbResolver - A function that will be used to resolve - * external GLB data if the CMPT contains I3DM that use + * @param externalResourceResolver - A function that will be used to resolve + * external resources, like GLB data if the CMPT contains I3DM that use * `header.gltfFormat=0` (meaning that the payload is not GLB data, * but only a GLB URI). * @returns The GLB buffer */ static async convertCmptToGlb( cmptBuffer: Buffer, - externalGlbResolver: (uri: string) => Promise + externalResourceResolver: (uri: string) => Promise ): Promise { return await TileFormatsMigrationCmpt.convertCmptToGlb( cmptBuffer, - externalGlbResolver + externalResourceResolver ); } diff --git a/src/tools/migration/TileFormatsMigrationCmpt.ts b/src/tools/migration/TileFormatsMigrationCmpt.ts index 2b6fc726..0efb39f9 100644 --- a/src/tools/migration/TileFormatsMigrationCmpt.ts +++ b/src/tools/migration/TileFormatsMigrationCmpt.ts @@ -20,8 +20,8 @@ export class TileFormatsMigrationCmpt { * Convert the given CMPT data into a glTF asset * * @param cmptBuffer - The CMPT buffer - * @param externalGlbResolver - A function that will be used to resolve - * external GLB data if the CMPT contains I3DM that use + * @param externalResourceResolver - A function that will be used to resolve + * external resources, like GLB data if the CMPT contains I3DM that use * `header.gltfFormat=0` (meaning that the payload is not GLB data, * but only a GLB URI). * @returns The GLB buffer @@ -30,7 +30,7 @@ export class TileFormatsMigrationCmpt { */ static async convertCmptToGlb( cmptBuffer: Buffer, - externalGlbResolver: (uri: string) => Promise + externalResourceResolver: (uri: string) => Promise ): Promise { const compositeTileData = TileFormats.readCompositeTileData(cmptBuffer); const innerTileBuffers = compositeTileData.innerTileBuffers; @@ -57,14 +57,14 @@ export class TileFormatsMigrationCmpt { logger.trace("Converting inner I3DM tile to GLB..."); const innerTileGlb = await TileFormatsMigration.convertI3dmToGlb( innerTileBuffer, - externalGlbResolver + externalResourceResolver ); innerTileGlbs.push(innerTileGlb); } else if (innerTileType === ContentDataTypes.CONTENT_TYPE_CMPT) { logger.trace("Converting inner CMPT tile to GLB..."); const innerTileGlb = await TileFormatsMigration.convertCmptToGlb( innerTileBuffer, - externalGlbResolver + externalResourceResolver ); innerTileGlbs.push(innerTileGlb); } else { @@ -75,7 +75,29 @@ export class TileFormatsMigrationCmpt { ); } } - const document = await GltfTransform.merge(innerTileGlbs); + + // Create a resolver for the "schemaUri" strings that could + // be contained in the resulting GLBs. (There won't be any + // schema URIs, because the migration will only ever create + // schemas that are inlined, but the case has to be handled + // by the generic glTF merging process nevertheless...) + const schemaUriResolver = async (schemaUri: string) => { + const schemaJsonBuffer = await externalResourceResolver(schemaUri); + if (schemaJsonBuffer === undefined) { + return undefined; + } + try { + const schemaJson = JSON.parse(schemaJsonBuffer.toString()); + return schemaJson; + } catch (e) { + console.error("Could not parse schema from " + schemaUri); + return undefined; + } + }; + const document = await GltfTransform.merge( + innerTileGlbs, + schemaUriResolver + ); const io = await GltfTransform.getIO(); const glb = await io.writeBinary(document); return Buffer.from(glb); diff --git a/src/tools/migration/TileFormatsMigrationI3dm.ts b/src/tools/migration/TileFormatsMigrationI3dm.ts index 70ade0dc..ffbd5ca7 100644 --- a/src/tools/migration/TileFormatsMigrationI3dm.ts +++ b/src/tools/migration/TileFormatsMigrationI3dm.ts @@ -43,16 +43,16 @@ export class TileFormatsMigrationI3dm { * Convert the given I3DM data into a glTF asset * * @param i3dmBuffer - The I3DM buffer - * @param externalGlbResolver - A function that will be used to resolve - * external GLB data if the I3DM uses `header.gltfFormat=0` (meaning - * that the payload is not GLB data, but only a GLB URI). + * @param externalResourceResolver - A function that will be used to resolve + * external resources, like GLB data if the I3DM uses `header.gltfFormat=0` + * (meaning that the payload is not GLB data, but only a GLB URI). * @returns The GLB buffer * @throws TileFormatError If the I3DM contained an external GLB URI * that could not resolved by the given resolver */ static async convertI3dmToGlb( i3dmBuffer: Buffer, - externalGlbResolver: (uri: string) => Promise + externalResourceResolver: (uri: string) => Promise ): Promise { const tileData = TileFormats.readTileData(i3dmBuffer); @@ -77,7 +77,7 @@ export class TileFormatsMigrationI3dm { // the payload is a URI that has to be resolved. const glbBuffer = await TileFormats.obtainGlbPayload( tileData, - externalGlbResolver + externalResourceResolver ); if (!glbBuffer) { throw new TileFormatError( diff --git a/src/tools/tilesetProcessing/TilesetUpgrader.ts b/src/tools/tilesetProcessing/TilesetUpgrader.ts index ffe564eb..935103d4 100644 --- a/src/tools/tilesetProcessing/TilesetUpgrader.ts +++ b/src/tools/tilesetProcessing/TilesetUpgrader.ts @@ -421,9 +421,10 @@ export class TilesetUpgrader { targetKey = this.processContentUri(sourceKey); - // Define the resolver for external GLB files in CMPT files: - // It will look up the entry using the 'tilesetProcessor' - const externalGlbResolver = async ( + // Define the resolver for resources like external GLB files + // in CMPT files: It will look up the entry using the + // 'tilesetProcessor' + const externalResourceResolver = async ( uri: string ): Promise => { if (!this.tilesetProcessor) { @@ -439,7 +440,7 @@ export class TilesetUpgrader { }; targetValue = await TileFormatsMigration.convertCmptToGlb( sourceValue, - externalGlbResolver + externalResourceResolver ); } else { logger.debug(` Not upgrading ${sourceKey} (disabled via option)`); From 70bb0e51688457b08a1a65a50d8473962b80557c Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 8 May 2024 19:34:40 +0200 Subject: [PATCH 30/37] Improve handling for schema merging. --- .../StructuralMetadataMerger.ts | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts index 29506401..90214c33 100644 --- a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts +++ b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts @@ -1,6 +1,6 @@ import crypto from "crypto"; -import { Document, Primitive } from "@gltf-transform/core"; +import { Document } from "@gltf-transform/core"; import { Property } from "@gltf-transform/core"; import { IProperty } from "@gltf-transform/core"; @@ -22,12 +22,8 @@ import { MetadataError } from "../../metadata"; import { Loggers } from "../../base/"; const logger = Loggers.get("gltfExtensionsUtils"); -logger.level = "debug"; - -// TODO This logging is for first drafts/experiments only! function log(object: any) { - //logger.info(object); - console.log(object); + logger.debug(object); } /** @@ -35,6 +31,23 @@ function log(object: any) { * the `EXT_structural_metadata` extension. */ export class StructuralMetadataMerger { + /** + * A suffix that will be appended to an unspecified schema ID + * for merged schemas. This is ONLY used for unit tests. + */ + private static mergedSchemaIdSuffix: string | undefined = undefined; + + /** + * Set a suffix that will be appended to an unspecified schema ID + * for merged schemas. This is ONLY used for unit tests. Clients + * should never call this function. + * + * @param mergedSchemaIdSuffix - The suffix for merged schema IDs + */ + static setMergedSchemaIdSuffix(mergedSchemaIdSuffix: string | undefined) { + StructuralMetadataMerger.mergedSchemaIdSuffix = mergedSchemaIdSuffix; + } + /** * Merge two glTF-Transform documents, taking into account that * they might contain the `EXT_structural_metadata` extension. @@ -105,6 +118,21 @@ export class StructuralMetadataMerger { "EXT_structural_metadata", copiedStructuralMetadata ); + + // For the copied metadata, ensure that it does not use a schemaUri + // any more, but instead, contains the inlined schema directly + const copiedSchemaUri = copiedStructuralMetadata.getSchemaUri(); + if (copiedSchemaUri !== null) { + const targetExtStructuralMetadata = targetDocument.createExtension( + EXTStructuralMetadata + ); + const copiedSchemaJson = await schemaUriResolver(copiedSchemaUri); + const copiedSchema = + targetExtStructuralMetadata.createSchemaFrom(copiedSchemaJson); + copiedStructuralMetadata.setSchema(copiedSchema); + copiedStructuralMetadata.setSchemaUri(null); + } + await targetDocument.transform(unpartition()); return; } @@ -215,7 +243,11 @@ export class StructuralMetadataMerger { const newClassKeys = targetSchema.listClassKeys(); const newEnumKeys = targetSchema.listEnumKeys(); if (oldClassKeys != newClassKeys || oldEnumKeys != newEnumKeys) { - const newId = "SCHEMA-ID-" + crypto.randomUUID(); + let schemaIdSuffix = StructuralMetadataMerger.mergedSchemaIdSuffix; + if (schemaIdSuffix === undefined) { + schemaIdSuffix = crypto.randomUUID(); + } + const newId = "SCHEMA-ID-" + schemaIdSuffix; log("Target schema was modified - assigning ID " + newId); targetSchema.setId(newId); } From 380b7875d3af866b4dac225d9205e2634c6b9c8a Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 8 May 2024 19:35:09 +0200 Subject: [PATCH 31/37] Add specs for schema merging. Cleanups. --- .../StructuralMetadataMergerSpec.ts | 141 +++++++++++++++++- .../migration/TileFormatsMigrationSpec.ts | 4 + 2 files changed, 141 insertions(+), 4 deletions(-) diff --git a/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts b/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts index dcf30994..55f5d42f 100644 --- a/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts +++ b/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts @@ -27,7 +27,7 @@ const SERIALIZE_DOCUMENTS = true; // A compile-time flag that indicates that the documents that are // created due to the SERIALIZE_DOCUMENTS flag should be logged // to the console. This should be `true` ONLY for debugging! -const LOG_SERIALIZED_DOCUMENTS = true; +const LOG_SERIALIZED_DOCUMENTS = false; // A dummy resolver for the `schemaUri` that may be found in metadata const specSchemaUriResolver = async (schemaUri: string) => { @@ -472,7 +472,7 @@ async function addSpecPropertyTexture( ); } -fdescribe("StructuralMetadataMerger", function () { +describe("StructuralMetadataMerger", function () { //========================================================================== // Basic class merging @@ -540,6 +540,14 @@ fdescribe("StructuralMetadataMerger", function () { // After merging in the second document: // There should still be one class expect(getMetadataClassNames(document).length).toBe(1); + + if (SERIALIZE_DOCUMENTS) { + const io = await GltfTransform.getIO(); + const jsonDocument = await io.writeJSON(document); + if (LOG_SERIALIZED_DOCUMENTS) { + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } + } }); it("creates two classes if the input classes have different keys/names (even though they are structurally equal)", async function () { @@ -609,6 +617,14 @@ fdescribe("StructuralMetadataMerger", function () { expect(getMetadataClassNames(document).sort()).toEqual( ["exampleClass", "exampleClassButWithDifferentName"].sort() ); + + if (SERIALIZE_DOCUMENTS) { + const io = await GltfTransform.getIO(); + const jsonDocument = await io.writeJSON(document); + if (LOG_SERIALIZED_DOCUMENTS) { + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } + } }); it("creates two classes if the input classes have the same keys/names but are NOT structurally equal", async function () { @@ -677,6 +693,14 @@ fdescribe("StructuralMetadataMerger", function () { expect(getMetadataClassNames(document).sort()).toEqual( ["exampleClass", "exampleClass_0"].sort() ); + + if (SERIALIZE_DOCUMENTS) { + const io = await GltfTransform.getIO(); + const jsonDocument = await io.writeJSON(document); + if (LOG_SERIALIZED_DOCUMENTS) { + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } + } }); it("creates two classes if the input classes have the same keys/names and appear to be structurally equal, but are no longer equal after disambiguating enums", async function () { @@ -793,6 +817,115 @@ fdescribe("StructuralMetadataMerger", function () { const exampleClass_0 = getMetadataClass(document, "exampleClass_0"); const property = exampleClass_0.getProperty("example_ENUM"); expect(property?.getEnumType()).toEqual("exampleEnum_0"); + + if (SERIALIZE_DOCUMENTS) { + const io = await GltfTransform.getIO(); + const jsonDocument = await io.writeJSON(document); + if (LOG_SERIALIZED_DOCUMENTS) { + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } + } + }); + + //========================================================================== + // Handling of schemaUri + + it("resolve schemas from schemaUri and merges them into one that is inlined", async function () { + // These objects will be returned by the "localSchemaUriResolver" + // that is defined below, emulating the schemas that are resolved + // from schema URIs + const schemaA = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class", + properties: { + example_STRING: { + name: "Example STRING property", + type: "STRING", + }, + }, + }, + }, + }; + + const schemaB = { + id: "EXAMPLE_SCHEMA_ID", + classes: { + exampleClass: { + name: "Example Class but with a difference", // Structural difference! + properties: { + example_STRING: { + name: "Example STRING property", + type: "STRING", + }, + }, + }, + }, + }; + + const localSchemaUriResolver = async (schemaUri: string) => { + if (schemaUri === "schemaA.json") { + return schemaA; + } + if (schemaUri === "schemaB.json") { + return schemaB; + } + console.error("Unexpected schema URI: " + schemaUri); + const schema = { + id: "SPEC_SCHEMA_FROM_URI_" + schemaUri, + }; + return schema; + }; + + const documentA = new Document(); + documentA.createBuffer(); + const structuralMetadataA = obtainStructuralMetadata(documentA); + structuralMetadataA.setSchemaUri("schemaA.json"); + + const documentB = new Document(); + documentB.createBuffer(); + const structuralMetadataB = obtainStructuralMetadata(documentB); + structuralMetadataB.setSchemaUri("schemaB.json"); + + const document = new Document(); + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentA, + localSchemaUriResolver + ); + + // After merging in the first document: + // There should be one class + expect(getMetadataClassNames(document).length).toBe(1); + + // The schemaUri in the result should be "null" + const root = document.getRoot(); + const structuralMetadata = root.getExtension( + "EXT_structural_metadata" + ); + expect(structuralMetadata?.getSchemaUri()).toBe(null); + + await StructuralMetadataMerger.mergeDocumentsWithStructuralMetadata( + document, + documentB, + localSchemaUriResolver + ); + + // After merging in the second document: + // There should be two classes + expect(getMetadataClassNames(document).length).toBe(2); + + // The schemaUri in the result should still be "null" + expect(structuralMetadata?.getSchemaUri()).toBe(null); + + if (SERIALIZE_DOCUMENTS) { + const io = await GltfTransform.getIO(); + const jsonDocument = await io.writeJSON(document); + if (LOG_SERIALIZED_DOCUMENTS) { + console.log(JSON.stringify(jsonDocument.json, null, 2)); + } + } }); //========================================================================== @@ -1195,7 +1328,7 @@ fdescribe("StructuralMetadataMerger", function () { //========================================================================== // Basic property texture merging - fit("merges the set of equal property textures for one resulting class", async function () { + it("merges the set of equal property textures for one resulting class", async function () { // The classes have the same name/key // The classes are structurally equal // The property textures refer to the respective class (which turns out to be equal) @@ -1288,7 +1421,7 @@ fdescribe("StructuralMetadataMerger", function () { } }); - fit("merges the set of different property textures for one resulting class", async function () { + it("merges the set of different property textures for one resulting class", async function () { // The classes have the same name/key // The classes are structurally equal // The property textures refer to the respective class (which turns out to be equal) diff --git a/specs/tools/migration/TileFormatsMigrationSpec.ts b/specs/tools/migration/TileFormatsMigrationSpec.ts index 713f52bf..37a3dd5e 100644 --- a/specs/tools/migration/TileFormatsMigrationSpec.ts +++ b/specs/tools/migration/TileFormatsMigrationSpec.ts @@ -2,6 +2,7 @@ import fs from "fs"; import { Paths } from "../../../src/base"; +import { StructuralMetadataMerger } from "../../../src/tools"; import { TilesetOperations } from "../../../src/tools"; import { GltfUtilities } from "../../../src/tools"; @@ -86,6 +87,9 @@ describe("TileFormatsMigration", function () { afterEach(function () { //SpecHelpers.forceDeleteDirectory(outputDir); }); + beforeEach(function () { + StructuralMetadataMerger.setMergedSchemaIdSuffix("SPEC-SCHEMA-ID-SUFFIX"); + }); //========================================================================== // PNTS From 6b91c16ba531dee640a4a518a89487cd4a6498f1 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 8 May 2024 19:35:26 +0200 Subject: [PATCH 32/37] Update golden spec files --- .../Composite/Composite/composite.gltf | 81 ++++++++++++++++-- .../compositeOfComposite.gltf | 82 +++++++++++++++++-- .../compositeOfInstanced.gltf | 22 ++++- 3 files changed, 171 insertions(+), 14 deletions(-) diff --git a/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf b/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf index 6822da8e..23795f07 100644 --- a/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf +++ b/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf @@ -131,13 +131,33 @@ { "buffer": 0, "byteOffset": 9140, + "byteLength": 10 + }, + { + "buffer": 0, + "byteOffset": 9150, + "byteLength": 40 + }, + { + "buffer": 0, + "byteOffset": 9190, + "byteLength": 40 + }, + { + "buffer": 0, + "byteOffset": 9230, + "byteLength": 40 + }, + { + "buffer": 0, + "byteOffset": 9270, "byteLength": 25 } ], "buffers": [ { "name": "buffer", - "byteLength": 9165 + "byteLength": 9295 } ], "materials": [ @@ -170,7 +190,7 @@ { "featureCount": 10, "attribute": 0, - "propertyTable": -1 + "propertyTable": 0 } ] } @@ -228,7 +248,7 @@ { "featureCount": 25, "attribute": 0, - "propertyTable": 0 + "propertyTable": 1 } ] } @@ -266,10 +286,43 @@ "extensions": { "EXT_structural_metadata": { "schema": { - "id": "ID_batch_table", + "id": "SCHEMA-ID-SPEC-SCHEMA-ID-SUFFIX", "name": "Generated from batch_table", "classes": { "class_batch_table": { + "name": "Generated from batch_table", + "properties": { + "id": { + "name": "id", + "description": "Generated from id", + "type": "SCALAR", + "componentType": "UINT8", + "required": true + }, + "Longitude": { + "name": "Longitude", + "description": "Generated from Longitude", + "type": "SCALAR", + "componentType": "FLOAT32", + "required": true + }, + "Latitude": { + "name": "Latitude", + "description": "Generated from Latitude", + "type": "SCALAR", + "componentType": "FLOAT32", + "required": true + }, + "Height": { + "name": "Height", + "description": "Generated from Height", + "type": "SCALAR", + "componentType": "FLOAT32", + "required": true + } + } + }, + "class_batch_table_0": { "name": "Generated from batch_table", "properties": { "Height": { @@ -286,10 +339,28 @@ "propertyTables": [ { "class": "class_batch_table", + "count": 10, + "properties": { + "id": { + "values": 4 + }, + "Longitude": { + "values": 5 + }, + "Latitude": { + "values": 6 + }, + "Height": { + "values": 7 + } + } + }, + { + "class": "class_batch_table_0", "count": 25, "properties": { "Height": { - "values": 4 + "values": 8 } } } diff --git a/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf b/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf index 072934b7..109e9180 100644 --- a/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf +++ b/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf @@ -131,13 +131,33 @@ { "buffer": 0, "byteOffset": 9140, + "byteLength": 10 + }, + { + "buffer": 0, + "byteOffset": 9150, + "byteLength": 40 + }, + { + "buffer": 0, + "byteOffset": 9190, + "byteLength": 40 + }, + { + "buffer": 0, + "byteOffset": 9230, + "byteLength": 40 + }, + { + "buffer": 0, + "byteOffset": 9270, "byteLength": 25 } ], "buffers": [ { "name": "buffer", - "byteLength": 9165 + "byteLength": 9295 } ], "materials": [ @@ -169,7 +189,8 @@ "featureIds": [ { "featureCount": 10, - "attribute": 0 + "attribute": 0, + "propertyTable": 0 } ] } @@ -227,7 +248,7 @@ { "featureCount": 25, "attribute": 0, - "propertyTable": 0 + "propertyTable": 1 } ] } @@ -265,10 +286,43 @@ "extensions": { "EXT_structural_metadata": { "schema": { - "id": "ID_batch_table", + "id": "SCHEMA-ID-SPEC-SCHEMA-ID-SUFFIX", "name": "Generated from batch_table", "classes": { "class_batch_table": { + "name": "Generated from batch_table", + "properties": { + "id": { + "name": "id", + "description": "Generated from id", + "type": "SCALAR", + "componentType": "UINT8", + "required": true + }, + "Longitude": { + "name": "Longitude", + "description": "Generated from Longitude", + "type": "SCALAR", + "componentType": "FLOAT32", + "required": true + }, + "Latitude": { + "name": "Latitude", + "description": "Generated from Latitude", + "type": "SCALAR", + "componentType": "FLOAT32", + "required": true + }, + "Height": { + "name": "Height", + "description": "Generated from Height", + "type": "SCALAR", + "componentType": "FLOAT32", + "required": true + } + } + }, + "class_batch_table_0": { "name": "Generated from batch_table", "properties": { "Height": { @@ -285,10 +339,28 @@ "propertyTables": [ { "class": "class_batch_table", + "count": 10, + "properties": { + "id": { + "values": 4 + }, + "Longitude": { + "values": 5 + }, + "Latitude": { + "values": 6 + }, + "Height": { + "values": 7 + } + } + }, + { + "class": "class_batch_table_0", "count": 25, "properties": { "Height": { - "values": 4 + "values": 8 } } } diff --git a/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf b/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf index b181233b..3fe241fd 100644 --- a/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf +++ b/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf @@ -158,11 +158,16 @@ "buffer": 0, "byteOffset": 3400, "byteLength": 25 + }, + { + "buffer": 0, + "byteOffset": 3425, + "byteLength": 25 } ], "buffers": [ { - "byteLength": 3425 + "byteLength": 3450 } ], "materials": [ @@ -222,7 +227,7 @@ { "featureCount": 25, "attribute": 0, - "propertyTable": -1 + "propertyTable": 0 } ] } @@ -254,7 +259,7 @@ { "featureCount": 25, "attribute": 0, - "propertyTable": 0 + "propertyTable": 1 } ] } @@ -291,7 +296,7 @@ "extensions": { "EXT_structural_metadata": { "schema": { - "id": "ID_batch_table", + "id": "SCHEMA-ID-SPEC-SCHEMA-ID-SUFFIX", "name": "Generated from batch_table", "classes": { "class_batch_table": { @@ -317,6 +322,15 @@ "values": 5 } } + }, + { + "class": "class_batch_table", + "count": 25, + "properties": { + "Height": { + "values": 6 + } + } } ] } From e5f09bcbe778ae8e061b77a721aedbee63967ae2 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 8 May 2024 19:35:37 +0200 Subject: [PATCH 33/37] Update migration test sandcastle --- .../migration/TileFormatsMigrationTestSandcastle.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/specs/data/migration/TileFormatsMigrationTestSandcastle.js b/specs/data/migration/TileFormatsMigrationTestSandcastle.js index e21353e8..8b9c3360 100644 --- a/specs/data/migration/TileFormatsMigrationTestSandcastle.js +++ b/specs/data/migration/TileFormatsMigrationTestSandcastle.js @@ -26,7 +26,7 @@ async function recreateTileset() { currentTileset.style = new Cesium.Cesium3DTileStyle({ pointSize: "10" }); - + // Special handling for tilesets that are "at the origin": // Move them to a certain position on the globe let distance = 500; @@ -43,7 +43,7 @@ async function recreateTileset() { currentTileset.modelMatrix = modelMatrix; distance = 10; } - + if (doZoom) { const offset = new Cesium.HeadingPitchRange( Cesium.Math.toRadians(-22.5), @@ -156,7 +156,7 @@ function createOptions() { createOption("InstancedAxes/InstancedAxesSimple"), createOption("InstancedAxes/InstancedAxesRotated"), createOption("InstancedAxes/InstancedAxesScaled"), - + createOption("Instanced/InstancedAnimated"), createOption("Instanced/InstancedGltfExternal"), createOption("Instanced/InstancedOct32POrientation"), @@ -216,7 +216,12 @@ function createOptions() { createOption("Batched/BatchedWithTransformBox"), createOption("Batched/BatchedWithTransformRegion"), createOption("Batched/BatchedWithTransformSphere"), - createOption("Batched/BatchedWithVertexColors") + createOption("Batched/BatchedWithVertexColors"), + + createOption("Composite/Composite"), + createOption("Composite/CompositeOfComposite"), + createOption("Composite/CompositeOfInstanced") + ]; return options; } From eebfd8a6dc12edcf40c09431e92a25b82814272e Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 12 May 2024 16:22:44 +0200 Subject: [PATCH 34/37] Fix typo in comments --- src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts b/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts index 4b552ff9..1553d3d1 100644 --- a/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts +++ b/src/gltf-extensions/gltfExtensions/EXTStructuralMetadata.ts @@ -1360,13 +1360,13 @@ export class EXTStructuralMetadata extends Extension { } /** - * Prepares writing a document that contains this extension. + * Prepares writing a document that contains this extension. * * This will collect all buffer views that are referred to by the * property tables, and store them as "otherBufferViews" of * the writer context (for the main buffer), to make sure * that they are part of the buffer when it is eventually - * writenn in Writer.ts. + * written in Writer.ts. * * @param context - The writer context * @returns The deep void of space From 14e6dbfcf1cb6eb375ff2b31ffcbb1f8c769bc89 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 12 May 2024 16:23:14 +0200 Subject: [PATCH 35/37] Remove obsolete statements (from some copy-and-paste) --- specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts b/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts index 55f5d42f..c430861d 100644 --- a/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts +++ b/specs/tools/gltfExtensionsUtils/StructuralMetadataMergerSpec.ts @@ -364,7 +364,6 @@ function addSpecPropertyAttribute(document: Document, className: string) { // Create a dummy mesh with one primitive const mesh = document.createMesh(); const primitive = document.createPrimitive(); - primitive.setExtension; mesh.addPrimitive(primitive); // Create an accessor with dummy UINT8 scalar data @@ -458,7 +457,6 @@ async function addSpecPropertyTexture( // Create a dummy mesh with one primitive const mesh = document.createMesh(); const primitive = document.createPrimitive(); - primitive.setExtension; mesh.addPrimitive(primitive); // Define the metadata for the mesh primitive, and let it From 646f04bb42da359eca93a2a66b2fb6fe79b7636d Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 12 May 2024 16:23:40 +0200 Subject: [PATCH 36/37] Use underscore in auto-generated schema IDs --- specs/tools/migration/TileFormatsMigrationSpec.ts | 2 +- src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/specs/tools/migration/TileFormatsMigrationSpec.ts b/specs/tools/migration/TileFormatsMigrationSpec.ts index 37a3dd5e..9bda8697 100644 --- a/specs/tools/migration/TileFormatsMigrationSpec.ts +++ b/specs/tools/migration/TileFormatsMigrationSpec.ts @@ -88,7 +88,7 @@ describe("TileFormatsMigration", function () { //SpecHelpers.forceDeleteDirectory(outputDir); }); beforeEach(function () { - StructuralMetadataMerger.setMergedSchemaIdSuffix("SPEC-SCHEMA-ID-SUFFIX"); + StructuralMetadataMerger.setMergedSchemaIdSuffix("SPEC_SCHEMA_ID_SUFFIX"); }); //========================================================================== diff --git a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts index 90214c33..f5c94f57 100644 --- a/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts +++ b/src/tools/gltfExtensionsUtils/StructuralMetadataMerger.ts @@ -247,7 +247,8 @@ export class StructuralMetadataMerger { if (schemaIdSuffix === undefined) { schemaIdSuffix = crypto.randomUUID(); } - const newId = "SCHEMA-ID-" + schemaIdSuffix; + schemaIdSuffix = schemaIdSuffix.replace(/-/g, "_"); + const newId = "SCHEMA_ID_" + schemaIdSuffix; log("Target schema was modified - assigning ID " + newId); targetSchema.setId(newId); } From b8bad9ee568d5570b7c711411147d932e6b0c5df Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 12 May 2024 16:30:21 +0200 Subject: [PATCH 37/37] Update golden reference files --- .../migration/golden_gltf/Composite/Composite/composite.gltf | 2 +- .../Composite/CompositeOfComposite/compositeOfComposite.gltf | 2 +- .../Composite/CompositeOfInstanced/compositeOfInstanced.gltf | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf b/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf index 23795f07..69bab869 100644 --- a/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf +++ b/specs/data/migration/golden_gltf/Composite/Composite/composite.gltf @@ -286,7 +286,7 @@ "extensions": { "EXT_structural_metadata": { "schema": { - "id": "SCHEMA-ID-SPEC-SCHEMA-ID-SUFFIX", + "id": "SCHEMA_ID_SPEC_SCHEMA_ID_SUFFIX", "name": "Generated from batch_table", "classes": { "class_batch_table": { diff --git a/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf b/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf index 109e9180..656ca639 100644 --- a/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf +++ b/specs/data/migration/golden_gltf/Composite/CompositeOfComposite/compositeOfComposite.gltf @@ -286,7 +286,7 @@ "extensions": { "EXT_structural_metadata": { "schema": { - "id": "SCHEMA-ID-SPEC-SCHEMA-ID-SUFFIX", + "id": "SCHEMA_ID_SPEC_SCHEMA_ID_SUFFIX", "name": "Generated from batch_table", "classes": { "class_batch_table": { diff --git a/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf b/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf index 3fe241fd..0f2ea18e 100644 --- a/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf +++ b/specs/data/migration/golden_gltf/Composite/CompositeOfInstanced/compositeOfInstanced.gltf @@ -296,7 +296,7 @@ "extensions": { "EXT_structural_metadata": { "schema": { - "id": "SCHEMA-ID-SPEC-SCHEMA-ID-SUFFIX", + "id": "SCHEMA_ID_SPEC_SCHEMA_ID_SUFFIX", "name": "Generated from batch_table", "classes": { "class_batch_table": {