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/styles/parsers.ts b/packages/model-viewer/src/styles/parsers.ts index aa9cbc7164..79a7ebb273 100644 --- a/packages/model-viewer/src/styles/parsers.ts +++ b/packages/model-viewer/src/styles/parsers.ts @@ -258,8 +258,8 @@ const parseNumber = (() => { * Parses a hexadecimal-encoded color in 3, 6 or 8 digit form. */ const parseHex = (() => { - // TODO(cdata): right now we don't actually enforce the number of digits - const HEX_RE = /^[a-f0-9]*/i; + const HEX_RE = + /^([a-f0-9]{8}|[a-f0-9]{6}|[a-f0-9]{4}|[a-f0-9]{3})(?![a-f0-9])/i; return (inputString: string): ParseResult => { inputString = inputString.slice(1).trim(); diff --git a/packages/model-viewer/src/test/styles/parsers-spec.ts b/packages/model-viewer/src/test/styles/parsers-spec.ts index 4ef003a75c..aff69a6b73 100644 --- a/packages/model-viewer/src/test/styles/parsers-spec.ts +++ b/packages/model-viewer/src/test/styles/parsers-spec.ts @@ -50,10 +50,25 @@ suite('parsers', () => { test('parses hex colors', () => { expect(parseExpressions('#fff')).to.be.eql([expressionNode( [hexNode('fff')])]); + expect(parseExpressions('#abcd')).to.be.eql([expressionNode( + [hexNode('abcd')])]); expect(parseExpressions('#abc123')).to.be.eql([expressionNode( [hexNode('abc123')])]); expect(parseExpressions('#daf012ee')).to.be.eql([expressionNode( [hexNode('daf012ee')])]); + + // Invalid hex digit counts should fail to parse + expect(parseExpressions('#')).to.be.eql([]); + expect(parseExpressions('#1')).to.be.eql([]); + expect(parseExpressions('#12')).to.be.eql([]); + expect(parseExpressions('#12345')).to.be.eql([]); + expect(parseExpressions('#1234567')).to.be.eql([]); + expect(parseExpressions('#123456789')).to.be.eql([]); + expect(parseExpressions('#123456fg')).to.be.eql([]); + + // Correctly parses valid hex followed by non-hex characters + expect(parseExpressions('#123456g')).to.be.eql([expressionNode( + [hexNode('123456'), identNode('g')])]); }); test('parses functions', () => { 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); }