diff --git a/packages/model-viewer/src/extra-model.ts b/packages/model-viewer/src/extra-model.ts new file mode 100644 index 0000000000..a976bc6028 --- /dev/null +++ b/packages/model-viewer/src/extra-model.ts @@ -0,0 +1,67 @@ +/* @license + * Copyright 2019 Google LLC. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {html, ReactiveElement} from 'lit'; +import {property} from 'lit/decorators.js'; + +import {Model} from './features/scene-graph/model.js'; +import ModelViewerElementBase from './model-viewer-base.js'; + + +/** + * Definition for a basic element. + */ +// @customElement('extra-model') +export class ExtraModelElement extends ReactiveElement { + @property({type: String}) src: string|null = null; + @property({type: Boolean}) loaded: boolean|null = null; + @property({type: Model}) model: Object|null = null; + @property({type: Array}) availableVariants: string[]|null = null; + + render() { + console.log('extra model render') + return html` `; + } + constructor() { + super(); + this.loaded = false; + console.log('extra model constructor') + } + + updated(changedProperties: Map) { + super.updated(changedProperties); + console.log('updated is called'); + if (changedProperties.has('src')) { + this.loaded = true; + console.log('src has changed'); + } + if (changedProperties.has('availableVariants')) { + console.log('availableVariants has changed'); + } + } + + connectedCallback() { + super.connectedCallback(); + // Get the parent element + console.log('extra model connected callback') + const modelViewer = this.closest('model-viewer') as ModelViewerElementBase; + if (modelViewer) { + // Add this extra model to the scene + modelViewer.addExtraModel(this); + } else { + console.error(' must be a child of '); + } + } +} diff --git a/packages/model-viewer/src/model-viewer-base.ts b/packages/model-viewer/src/model-viewer-base.ts index 3077912a1d..45ae946140 100644 --- a/packages/model-viewer/src/model-viewer-base.ts +++ b/packages/model-viewer/src/model-viewer-base.ts @@ -18,6 +18,7 @@ import {property} from 'lit/decorators.js'; import {Camera as ThreeCamera, Event as ThreeEvent, Vector2, Vector3, WebGLRenderer} from 'three'; import {HAS_INTERSECTION_OBSERVER, HAS_RESIZE_OBSERVER} from './constants.js'; +import {ExtraModelElement} from './extra-model.js'; import {$updateEnvironment} from './features/environment.js'; import {makeTemplate} from './template.js'; import {$evictionPolicy, CachingGLTFLoader} from './three-components/CachingGLTFLoader.js'; @@ -214,6 +215,8 @@ export default class ModelViewerElementBase extends ReactiveElement { protected[$progressTracker]: ProgressTracker = new ProgressTracker(); + // private extraModels: ExtraModelElement[] = []; + /** @export */ get loaded() { return this[$getLoaded](); @@ -645,4 +648,9 @@ export default class ModelViewerElementBase extends ReactiveElement { updateSourceProgress(1.0); } } + + async addExtraModel(extraModel: ExtraModelElement) { + console.log('Adding a new extra model with src: ', extraModel.src); + // this.extraModels.add(extraModel); + } } diff --git a/packages/model-viewer/src/model-viewer.ts b/packages/model-viewer/src/model-viewer.ts index c2a353a617..a662af1c6e 100644 --- a/packages/model-viewer/src/model-viewer.ts +++ b/packages/model-viewer/src/model-viewer.ts @@ -13,6 +13,7 @@ * limitations under the License. */ +import {ExtraModelElement} from './extra-model.js'; import {AnimationMixin} from './features/animation.js'; import {AnnotationMixin} from './features/annotation.js'; import {ARMixin} from './features/ar.js'; @@ -28,7 +29,7 @@ import ModelViewerElementBase from './model-viewer-base.js'; export {CanvasTexture, FileLoader, Loader, NearestFilter} from 'three'; export const ModelViewerElement = -AnnotationMixin(SceneGraphMixin(StagingMixin(EnvironmentMixin(ControlsMixin( + AnnotationMixin(SceneGraphMixin(StagingMixin(EnvironmentMixin(ControlsMixin( ARMixin(LoadingMixin(AnimationMixin(ModelViewerElementBase)))))))); export type ModelViewerElement = InstanceType; @@ -36,9 +37,11 @@ export type ModelViewerElement = InstanceType; export type{RGB, RGBA} from './three-components/gltf-instance/gltf-2.0'; customElements.define('model-viewer', ModelViewerElement); +customElements.define('extra-model', ExtraModelElement); declare global { interface HTMLElementTagNameMap { 'model-viewer': ModelViewerElement; + 'extra-model': ExtraModelElement; } -} +} \ No newline at end of file diff --git a/packages/model-viewer/src/test/extra-model-spec.ts b/packages/model-viewer/src/test/extra-model-spec.ts new file mode 100644 index 0000000000..010aa1bbec --- /dev/null +++ b/packages/model-viewer/src/test/extra-model-spec.ts @@ -0,0 +1,103 @@ +/* @license + * Copyright 2024 Google LLC. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {expect} from 'chai'; + +import {ExtraModelElement} from '../extra-model.js'; +import {ModelViewerElement} from '../model-viewer.js'; +import {timePasses, waitForEvent} from '../utilities.js'; + +import {assetPath} from './helpers.js'; + +suite('ExtraModelElement', () => { + let extraElement: ExtraModelElement; + let modelViewer: ModelViewerElement; + + setup(() => { + modelViewer = new ModelViewerElement(); + extraElement = new ExtraModelElement(); + document.body.insertBefore(modelViewer, document.body.firstChild); + modelViewer.appendChild(extraElement); + }); + + teardown(() => { + if (modelViewer.parentNode != null) { + modelViewer.removeChild(extraElement); + modelViewer.parentNode.removeChild(modelViewer); + } + }); + + suite('with src property', () => { + test('set loaded to true at first when src is set', async () => { + extraElement.src = assetPath('models/Astronaut.glb'); + await extraElement.updateComplete; + expect(extraElement.loaded).to.be.true; + }); + test.skip('dispatches a load event when src is set', async () => { + const sourceLoads = waitForEvent(extraElement, 'load'); + extraElement.src = assetPath('models/Astronaut.glb'); + await sourceLoads; + expect(extraElement.loaded).to.be.true; + }); + + test.skip('dispatches an error event when src is invalid', async () => { + const sourceErrors = waitForEvent(extraElement, 'error'); + extraElement.src = './does-not-exist.glb'; + await sourceErrors; + expect(extraElement.loaded).to.be.false; + }); + }); + + suite('with availableVariants property', () => { + test('updates when availableVariants is changed', async () => { + const variants = ['variant1', 'variant2']; + extraElement.availableVariants = variants; + await timePasses(); + expect(extraElement.availableVariants).to.deep.equal(variants); + }); + }); + + suite('connectedCallback', () => { + test('adds itself to the parent model-viewer', () => { + const modelViewer = document.querySelector('model-viewer') as HTMLElement; + const firstExtraModel = modelViewer.querySelector('extra-model'); + expect(firstExtraModel).to.include(extraElement); + }); + }); + + suite( + 'updated', + () => { + /* test('reacts to src property changes', async () => { + const spy = sinon.spy(console, 'log'); + extraElement.src = assetPath('models/Astronaut.glb'); + await timePasses(); + expect(spy.calledWith('updated is called')).to.be.true; + expect(spy.calledWith('src has changed')).to.be.true; + spy.restore(); + }); + + test('reacts to availableVariants property changes', async () + => { const spy = sinon.spy(console, 'log'); + extraElement.availableVariants = ['variant1', 'variant2']; + await timePasses(); + expect(spy.calledWith('updated is called')).to.be.true; + expect(spy.calledWith('availableVariants has + changed')).to.be.true; spy.restore(); + });*/ + }); + + // ... Add more tests for other functionalities ... +}); \ No newline at end of file diff --git a/packages/modelviewer.dev/index.html b/packages/modelviewer.dev/index.html index db6c6b42d1..5699d7e31b 100644 --- a/packages/modelviewer.dev/index.html +++ b/packages/modelviewer.dev/index.html @@ -69,7 +69,16 @@

Quick Start

- + + +