From f2098ea4a07cad4ffdbf1a8bd766c7d63c99ca18 Mon Sep 17 00:00:00 2001 From: Charan Raj Kandula Date: Fri, 5 Jun 2026 01:51:10 -0400 Subject: [PATCH] feat: define typescript interfaces for gltf accessor sparse property and update environment setup --- .gitmodules | 2 +- package-lock.json | 51 +++---------- package.json | 1 + .../src/test/effect-composer-spec.ts | 3 +- .../src/test/effects/color-grade-spec.ts | 3 +- packages/model-viewer/src/constants.ts | 3 +- packages/model-viewer/src/features/loading.ts | 5 +- .../test/three-components/ARRenderer-spec.ts | 74 +++++++++---------- .../three-components/TextureUtils-spec.ts | 39 +++++----- .../src/three-components/ARRenderer.ts | 19 +++-- .../src/three-components/TextureUtils.ts | 7 +- .../gltf-instance/gltf-2.0.ts | 26 ++++++- 12 files changed, 118 insertions(+), 115 deletions(-) diff --git a/.gitmodules b/.gitmodules index 5b42e5ace7..12058bb6ca 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "packages/shared-assets/models/glTF-Sample-Assets"] path = packages/shared-assets/models/glTF-Sample-Assets - url = git@github.com:KhronosGroup/glTF-Sample-Assets + url = https://github.com/KhronosGroup/glTF-Sample-Assets.git diff --git a/package-lock.json b/package-lock.json index 6526dc668f..8d17ab1a66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "puppeteer": "^24.4.0" }, "devDependencies": { + "@types/chai": "^4.3.20", "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", "clang-format": "^1.8.0", @@ -97,7 +98,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -4459,7 +4459,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.26" @@ -4794,15 +4793,11 @@ } }, "node_modules/@types/chai": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.0.1.tgz", - "integrity": "sha512-5T8ajsg3M/FOncpLYW7sdOcD6yf4+722sze/tc4KQV0P8Z2rAr3SAuHCIkYmYpt8VbcQlnz8SxlOlPQYefe4cA==", + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/deep-eql": "*" - } + "license": "MIT" }, "node_modules/@types/chai-subset": { "version": "1.3.6", @@ -4907,13 +4902,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/doctrine": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.1.tgz", @@ -5427,7 +5415,6 @@ "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", @@ -6088,7 +6075,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7314,7 +7300,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -8626,8 +8611,7 @@ "version": "0.0.1608973", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/diff": { "version": "5.2.2", @@ -9109,7 +9093,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -15625,8 +15608,7 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-devtools-core": { "version": "0.2.1", @@ -15831,17 +15813,6 @@ "socketcluster-client": "^14.2.1" } }, - "node_modules/remote-redux-devtools/node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, "node_modules/remote-redux-devtools/node_modules/redux-devtools-instrument": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/redux-devtools-instrument/-/redux-devtools-instrument-1.10.0.tgz", @@ -16123,7 +16094,6 @@ "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.9" }, @@ -17607,8 +17577,8 @@ "version": "0.183.2", "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/through2": { "version": "2.0.5", @@ -17921,9 +17891,8 @@ "version": "5.8.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index a608b6757f..e422e67ae2 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ }, "homepage": "https://github.com/google/model-viewer#readme", "devDependencies": { + "@types/chai": "^4.3.20", "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", "clang-format": "^1.8.0", diff --git a/packages/model-viewer-effects/src/test/effect-composer-spec.ts b/packages/model-viewer-effects/src/test/effect-composer-spec.ts index 9e930a5ada..47c5e9bfe4 100644 --- a/packages/model-viewer-effects/src/test/effect-composer-spec.ts +++ b/packages/model-viewer-effects/src/test/effect-composer-spec.ts @@ -14,12 +14,11 @@ */ import {ModelViewerElement} from '@google/model-viewer'; +import {Renderer} from '@google/model-viewer/lib/three-components/Renderer.js'; import {expect} from 'chai'; import {DotScreenEffect, Effect, EffectPass, GridEffect} from 'postprocessing'; import {Camera} from 'three'; -import {Renderer} from '@google/model-viewer/lib/three-components/Renderer.js'; - import {$effectComposer, $normalPass, $renderPass, $scene} from '../effect-composer.js'; import {EffectComposer} from '../model-viewer-effects.js'; diff --git a/packages/model-viewer-effects/src/test/effects/color-grade-spec.ts b/packages/model-viewer-effects/src/test/effects/color-grade-spec.ts index 07a3773200..ecd03060f3 100644 --- a/packages/model-viewer-effects/src/test/effects/color-grade-spec.ts +++ b/packages/model-viewer-effects/src/test/effects/color-grade-spec.ts @@ -14,9 +14,8 @@ */ import {ModelViewerElement} from '@google/model-viewer'; -import {expect} from 'chai'; - import {Renderer} from '@google/model-viewer/lib/three-components/Renderer.js'; +import {expect} from 'chai'; import {ColorGradeEffect, EffectComposer} from '../../model-viewer-effects.js'; import {ArraysAreEqual, assetPath, AverageHSL, CompareArrays, createModelViewerElement, rafPasses, screenshot, waitForEvent} from '../utilities.js'; diff --git a/packages/model-viewer/src/constants.ts b/packages/model-viewer/src/constants.ts index ad6f36c49c..972fe17dee 100644 --- a/packages/model-viewer/src/constants.ts +++ b/packages/model-viewer/src/constants.ts @@ -67,7 +67,8 @@ export const IS_FIREFOX = /firefox/i.test(navigator.userAgent); export const IS_OCULUS = /OculusBrowser/.test(navigator.userAgent); export const IS_IOS_CHROME = IS_IOS && /CriOS\//.test(navigator.userAgent); export const IS_IOS_SAFARI = IS_IOS && IS_SAFARI; -export const IS_IOS_THIRDPARTY = IS_IOS && /CriOS\/|EdgiOS\/|FxiOS\/|GSA\/|DuckDuckGo\//.test(navigator.userAgent); +export const IS_IOS_THIRDPARTY = IS_IOS && + /CriOS\/|EdgiOS\/|FxiOS\/|GSA\/|DuckDuckGo\//.test(navigator.userAgent); export const IS_IOS_GSA = IS_IOS && /GSA\//.test(navigator.userAgent); export const IS_SCENEVIEWER_CANDIDATE = IS_ANDROID && !IS_FIREFOX && !IS_OCULUS; diff --git a/packages/model-viewer/src/features/loading.ts b/packages/model-viewer/src/features/loading.ts index 49d5874326..de86d4aeed 100644 --- a/packages/model-viewer/src/features/loading.ts +++ b/packages/model-viewer/src/features/loading.ts @@ -205,8 +205,9 @@ export const LoadingMixin = >( * * The default value is "auto". The only supported alternative values are * "lazy" and "eager". Auto is equivalent to lazy, which loads the model - * when it is near the viewport for reveal = "auto", and when dismissPoster() - * is called for reveal = "manual". Eager loads the model immediately. + * when it is near the viewport for reveal = "auto", and when + * dismissPoster() is called for reveal = "manual". Eager loads the model + * immediately. */ @property({type: String}) loading: LoadingAttributeValue = LoadingStrategy.AUTO; diff --git a/packages/model-viewer/src/test/three-components/ARRenderer-spec.ts b/packages/model-viewer/src/test/three-components/ARRenderer-spec.ts index cbbdcead7f..63450c7d8d 100644 --- a/packages/model-viewer/src/test/three-components/ARRenderer-spec.ts +++ b/packages/model-viewer/src/test/three-components/ARRenderer-spec.ts @@ -241,43 +241,43 @@ suite('ARRenderer', () => { expect(scale.z).to.be.equal(1); }); - test('prevents default WebXR select when beforexrselect is triggered on interactive elements', () => { - const button = document.createElement('button'); - const event = new CustomEvent('beforexrselect', { - bubbles: true, - cancelable: true - }); - Object.defineProperty(event, 'composedPath', { - value: () => [button, (arRenderer as any).overlay] - }); - - let defaultPrevented = false; - event.preventDefault = () => { - defaultPrevented = true; - }; - - (arRenderer as any).overlay.dispatchEvent(event); - expect(defaultPrevented).to.be.equal(true); - }); - - test('does not prevent default WebXR select on non-interactive elements', () => { - const div = document.createElement('div'); - const event = new CustomEvent('beforexrselect', { - bubbles: true, - cancelable: true - }); - Object.defineProperty(event, 'composedPath', { - value: () => [div, (arRenderer as any).overlay] - }); - - let defaultPrevented = false; - event.preventDefault = () => { - defaultPrevented = true; - }; - - (arRenderer as any).overlay.dispatchEvent(event); - expect(defaultPrevented).to.be.equal(false); - }); + test( + 'prevents default WebXR select when beforexrselect is triggered on interactive elements', + () => { + const button = document.createElement('button'); + const event = new CustomEvent( + 'beforexrselect', {bubbles: true, cancelable: true}); + Object.defineProperty(event, 'composedPath', { + value: () => [button, (arRenderer as any).overlay] + }); + + let defaultPrevented = false; + event.preventDefault = () => { + defaultPrevented = true; + }; + + (arRenderer as any).overlay.dispatchEvent(event); + expect(defaultPrevented).to.be.equal(true); + }); + + test( + 'does not prevent default WebXR select on non-interactive elements', + () => { + const div = document.createElement('div'); + const event = new CustomEvent( + 'beforexrselect', {bubbles: true, cancelable: true}); + Object.defineProperty(event, 'composedPath', { + value: () => [div, (arRenderer as any).overlay] + }); + + let defaultPrevented = false; + event.preventDefault = () => { + defaultPrevented = true; + }; + + (arRenderer as any).overlay.dispatchEvent(event); + expect(defaultPrevented).to.be.equal(false); + }); suite('presentation ends', () => { setup(async () => { diff --git a/packages/model-viewer/src/test/three-components/TextureUtils-spec.ts b/packages/model-viewer/src/test/three-components/TextureUtils-spec.ts index 52fcb6f5e0..e94d4d851d 100644 --- a/packages/model-viewer/src/test/three-components/TextureUtils-spec.ts +++ b/packages/model-viewer/src/test/three-components/TextureUtils-spec.ts @@ -73,24 +73,27 @@ suite('TextureUtils', () => { expect(texture.name).to.be.eq(EQUI_URL); expect(texture.mapping).to.be.eq(EquirectangularReflectionMapping); }); - test('decodes a gainmap and disposes intermediate render targets', async () => { - const GAINMAP_URL = assetPath('environments/spruit_sunrise_1k_HDR.jpg'); - const THREE = await import('three'); - let disposeCount = 0; - const originalDispose = THREE.WebGLRenderTarget.prototype.dispose; - THREE.WebGLRenderTarget.prototype.dispose = function() { - disposeCount++; - return originalDispose.call(this); - }; - - try { - const texture = await textureUtils.loadEquirect(GAINMAP_URL); - texture.dispose(); - expect(disposeCount).to.be.greaterThan(0); - } finally { - THREE.WebGLRenderTarget.prototype.dispose = originalDispose; - } - }); + test( + 'decodes a gainmap and disposes intermediate render targets', + async () => { + const GAINMAP_URL = + assetPath('environments/spruit_sunrise_1k_HDR.jpg'); + const THREE = await import('three'); + let disposeCount = 0; + const originalDispose = THREE.WebGLRenderTarget.prototype.dispose; + THREE.WebGLRenderTarget.prototype.dispose = function() { + disposeCount++; + return originalDispose.call(this); + }; + + try { + const texture = await textureUtils.loadEquirect(GAINMAP_URL); + texture.dispose(); + expect(disposeCount).to.be.greaterThan(0); + } finally { + THREE.WebGLRenderTarget.prototype.dispose = originalDispose; + } + }); test('loads a valid KTX2 texture from URL', async () => { let texture = await textureUtils.loadImage(KTX2_URL, false); texture.dispose(); diff --git a/packages/model-viewer/src/three-components/ARRenderer.ts b/packages/model-viewer/src/three-components/ARRenderer.ts index 0ed26085c8..18ee8c7b68 100644 --- a/packages/model-viewer/src/three-components/ARRenderer.ts +++ b/packages/model-viewer/src/three-components/ARRenderer.ts @@ -459,7 +459,8 @@ export class ARRenderer extends EventDispatcher< this.selectedXRController.userData.line.visible = false; if (scene.canScale && this.isWorldSpaceReady()) { this.isTwoHandInteraction = true; - this.firstRatio = this.controllerSeparation() / scene.scenePivot.scale.x; + this.firstRatio = + this.controllerSeparation() / scene.scenePivot.scale.x; this.scaleLine.visible = true; } } else { @@ -478,7 +479,8 @@ export class ARRenderer extends EventDispatcher< this.xrController2?.userData.isSelected) { if (scene.canScale && this.isWorldSpaceReady()) { this.isTwoHandInteraction = true; - this.firstRatio = this.controllerSeparation() / scene.scenePivot.scale.x; + this.firstRatio = + this.controllerSeparation() / scene.scenePivot.scale.x; this.scaleLine.visible = true; } } else { @@ -516,7 +518,8 @@ export class ARRenderer extends EventDispatcher< scene.attach(scene.scenePivot); this.selectedXRController = null; this.goalYaw = Math.atan2( - scene.scenePivot.matrix.elements[8], scene.scenePivot.matrix.elements[10]); + scene.scenePivot.matrix.elements[8], + scene.scenePivot.matrix.elements[10]); this.goalPosition.x = scene.scenePivot.position.x; this.goalPosition.z = scene.scenePivot.position.z; @@ -957,8 +960,8 @@ export class ARRenderer extends EventDispatcher< if (element instanceof HTMLElement) { const tagName = element.tagName.toLowerCase(); if (tagName === 'input' || tagName === 'button' || - tagName === 'select' || tagName === 'textarea' || - tagName === 'a' || element.hasAttribute('data-pointer-coalesce') || + tagName === 'select' || tagName === 'textarea' || tagName === 'a' || + element.hasAttribute('data-pointer-coalesce') || element.classList.contains('interactive')) { event.preventDefault(); break; @@ -1094,14 +1097,16 @@ export class ARRenderer extends EventDispatcher< } } - private applyXRControllerRotation(controller: XRController, scenePivot: Object3D) { + private applyXRControllerRotation( + controller: XRController, scenePivot: Object3D) { if (!controller.userData.turning) { return; } const angle = (controller.position.x - controller.userData.initialX) * ROTATION_SENSIVITY; this.deltaRotation.setFromAxisAngle(AXIS_Y, angle); - scenePivot.quaternion.multiplyQuaternions(this.deltaRotation, scenePivot.quaternion); + scenePivot.quaternion.multiplyQuaternions( + this.deltaRotation, scenePivot.quaternion); } private handleScalingInXR(scene: ModelScene, delta: number) { diff --git a/packages/model-viewer/src/three-components/TextureUtils.ts b/packages/model-viewer/src/three-components/TextureUtils.ts index f62e03bc8b..30aa5c8895 100644 --- a/packages/model-viewer/src/three-components/TextureUtils.ts +++ b/packages/model-viewer/src/three-components/TextureUtils.ts @@ -157,12 +157,15 @@ export default class TextureUtils { texture = dataTexture; result.dispose(true); } else { - console.warn('Decoded DataTexture is completely black, falling back to render target texture.'); + console.warn( + 'Decoded DataTexture is completely black, falling back to render target texture.'); texture = renderTarget.texture; result.dispose(false); } } catch (e) { - console.warn('Failed to convert gainmap to DataTexture, falling back to render target texture:', e); + console.warn( + 'Failed to convert gainmap to DataTexture, falling back to render target texture:', + e); texture = renderTarget.texture; result.dispose(false); } diff --git a/packages/model-viewer/src/three-components/gltf-instance/gltf-2.0.ts b/packages/model-viewer/src/three-components/gltf-instance/gltf-2.0.ts index 26031161cb..172ed68dc6 100644 --- a/packages/model-viewer/src/three-components/gltf-instance/gltf-2.0.ts +++ b/packages/model-viewer/src/three-components/gltf-instance/gltf-2.0.ts @@ -224,6 +224,29 @@ export interface Scene { export type AccessorType = 'SCALAR'|'VEC2'|'VEC3'|'VEC4'|'MAT2'|'MAT3'|'MAT4'; +export interface AccessorSparseIndices { + bufferView: number; + byteOffset?: number; + componentType: number; + extensions?: ExtensionDictionary; + extras?: Extras; +} + +export interface AccessorSparseValues { + bufferView: number; + byteOffset?: number; + extensions?: ExtensionDictionary; + extras?: Extras; +} + +export interface AccessorSparse { + count: number; + indices: AccessorSparseIndices; + values: AccessorSparseValues; + extensions?: ExtensionDictionary; + extras?: Extras; +} + export interface Accessor { name?: string; bufferView?: number; @@ -237,9 +260,8 @@ export interface Accessor { extensions?: ExtensionDictionary; extras?: Extras; - // TODO: What is this? // @see https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#accessorsparse - sparse?: any; + sparse?: AccessorSparse; } export type ChannelTargetPath = 'rotation'|'scale'|'translation'|'weights';