Skip to content

Commit

Permalink
Mobile Bridge (google#1746)
Browse files Browse the repository at this point in the history
* initial logic for UI for mobile view

* add posts

* add qr code capability via npm package qrious

* name changes

* add basic routing

* move mobile to own expandable section

* add todos

* env image, glb, poster, and snippet receive and updates

* remove router

* add catch statements and use an update object to initiate mobile view

* add modal for mobile view instead of in bar

* searching for the listener...

* fetching loops such that updates are constantly being asked for, and the editor knows when mobile is ready

* testing tests

* remove poster, fix async everything but applying material edits

* add random number for session

* add ar toggle

* initial test of ar modes

* add ar modes, dynamic refresh button if viewers out of sync

* get materials to update by sending packed model

* code cleanup

* moved open section into mobile view

* fix wording of modal

* cleaning up comments
  • Loading branch information
chrismgeorge authored Dec 16, 2020
1 parent 60a1164 commit aa2587c
Show file tree
Hide file tree
Showing 17 changed files with 1,546 additions and 667 deletions.
54 changes: 54 additions & 0 deletions packages/space-opera/editor/view/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<!--
@license
Copyright 2020 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.
-->

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<meta name="description" content="&lt;model-viewer&gt; editor">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Model Editor</title>
<link href="https://fonts.googleapis.com/css?family=Material+Icons&display=block" rel="stylesheet">
<link rel="shortcut icon" type="image/png" href="../../shared-assets/icons/favicon.png"/>
<!-- Web animations for paper-dropdown -->
<script src="../../node_modules/web-animations-js/web-animations-next-lite.min.js"></script>
<script src="../../node_modules/js-beautify/js/lib/beautify-html.js"></script>
<script src="../../node_modules/js-beautify/js/lib/beautify-css.js"></script>
<script>
// Necessary hack for Redux. See: https://github.com/reduxjs/redux/pull/2910
window.process = {
env: {
NODE_ENV :'production'
}
};
</script>
<script type="module" src="../../dist/space-opera.js"></script>
</head>
<style>
:root {
--expandable-section-background: #2b2d30;
--expandable-section-text: #EEEEEE;
--text-on-expandable-background: #F5F5F5;
--number-input-background: #212121;
}

body {
margin: 0
}
</style>
<body>
<mobile-view></mobile-view>
</body>
1,324 changes: 670 additions & 654 deletions packages/space-opera/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/space-opera/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@
"rollup": "^2.26.6",
"three": "^0.123.0",
"ts-closure-library": "^2019.11.1-1.10",
"typescript": "4.0.3"
"typescript": "4.0.3",
"qrious": "^4.0.2"
},
"dependencies": {
"js-beautify": "^1.11.0",
Expand Down
3 changes: 3 additions & 0 deletions packages/space-opera/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ import './components/model_viewer_preview/model_viewer_preview.js';
import './components/model_viewer_snippet/model_viewer_snippet.js';
import './components/inspector/inspector.js';
import './components/shared/tabs/tabs.js';
import './components/mobile_view/mobile_view.js';
import './components/mobile_view/open_mobile_view.js';
import './components/mobile_view/components/mobile_modal.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @license
* Copyright 2020 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 {customElement, html, internalProperty, property, query} from 'lit-element';
// @ts-ignore, the qrious package isn't typed
import QRious from 'qrious';

import {openModalStyles} from '../../../styles.css.js';
import {ConnectedLitElement} from '../../connected_lit_element/connected_lit_element.js';

/**
* Modal for displaying the QR Code & link
*/
@customElement('mobile-modal')
export class MobileModal extends ConnectedLitElement {
static styles = openModalStyles;

@property({type: Number}) pipeId = 0;
@internalProperty() isOpen: boolean = false;
@internalProperty() isNewQRCode = true;
@query('canvas#qr') canvasQR!: HTMLCanvasElement;

get viewableSite(): string {
const path = window.location.origin + window.location.pathname;
return `${path}view/?id=${this.pipeId}`;
}

open() {
if (this.isNewQRCode) {
new QRious({element: this.canvasQR, value: this.viewableSite, size: 200});
this.isNewQRCode = false
}
this.isOpen = true;
}

close() {
this.isOpen = false;
}

render() {
return html`
<paper-dialog id="file-modal" modal ?opened=${this.isOpen} class="dialog">
<div class="FileModalContainer">
<div class="FileModalHeader">
<div>Mobile View</div>
</div>
<div style="font-size: 14px; font-weight: 500; margin: 10px 0px; color: white; word-wrap: break-word; width: 100%;">
Use the QR Code to open your edited model, environment image, and &ltmodel-viewer&gt snippet on a mobile device to test out AR features.
After every subsequent change, click the "Refresh Mobile" button.
</div>
<canvas id="qr" style="display: block; margin-bottom: 20px;"></canvas>
<div style="margin: 10px 0px; overflow-wrap: break-word; word-wrap: break-word;">
<a href=${this.viewableSite} style="color: white;" target="_blank">
${this.viewableSite}
</a>
</div>
</div>
<div class="FileModalCancel">
<mwc-button unelevated icon="cancel"
@click=${this.close}>Close</mwc-button>
</div>
</paper-dialog>`;
}
}

declare global {
interface HTMLElementTagNameMap {
'mobile-modal': MobileModal;
}
}
216 changes: 216 additions & 0 deletions packages/space-opera/src/components/mobile_view/mobile_view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/**
* @license
* Copyright 2020 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 {GltfModel, ModelViewerConfig} from '@google/model-viewer-editing-adapter/lib/main';
import {customElement, html, internalProperty} from 'lit-element';
import {ifDefined} from 'lit-html/directives/if-defined';

import {reduxStore} from '../../space_opera_base.js';
import {ArConfigState, State} from '../../types.js';
import {applyCameraEdits, Camera, INITIAL_CAMERA} from '../camera_settings/camera_state.js';
import {dispatchSetCamera, getCamera} from '../camera_settings/reducer.js';
import {dispatchEnvrionmentImage, dispatchSetConfig, getConfig} from '../config/reducer.js';
import {ConnectedLitElement} from '../connected_lit_element/connected_lit_element.js';
import {dispatchSetHotspots, getHotspots} from '../hotspot_panel/reducer.js';
import {HotspotConfig} from '../hotspot_panel/types.js';
import {dispatchGltfUrl, getGltfModel, getGltfUrl} from '../model_viewer_preview/reducer.js';
import {renderHotspots} from '../utils/hotspot/render_hotspots.js';
import {dispatchArConfig, getArConfig} from './reducer.js';

import {styles} from './styles.css.js';

/**
* The view loaded at /editor/view/?id=xyz
*/
@customElement('mobile-view')
export class MobileView extends ConnectedLitElement {
static styles = styles;

@internalProperty() gltfUrl: string|undefined;
@internalProperty() config: ModelViewerConfig = {};
@internalProperty() arConfig: ArConfigState = {};
@internalProperty() camera: Camera = INITIAL_CAMERA;
@internalProperty() hotspots: HotspotConfig[] = [];
@internalProperty() gltf?: GltfModel;

@internalProperty() pipeId = window.location.search.replace('?id=', '');
@internalProperty() base = 'https://ppng.io/modelviewereditor';
@internalProperty() snippetPipeUrl = `${this.base}-state-${this.pipeId}`;
@internalProperty() updatesPipeUrl = `${this.base}-updates-${this.pipeId}`;
@internalProperty() mobilePingUrl = `${this.base}-ping-${this.pipeId}`;

stateChanged(state: State) {
this.gltfUrl = getGltfUrl(state);
this.config = getConfig(state);
this.arConfig = getArConfig(state);
this.hotspots = getHotspots(state);
this.camera = getCamera(state);
this.gltf = getGltfModel(state);
}

getSrcPipeUrl(srcType: string): string {
return `https://ppng.io/modelviewereditor-srcs-${srcType}-${this.pipeId}`;
}

// TODO: https://javascript.info/fetch-progress
async waitForModel() {
await fetch(this.getSrcPipeUrl('gltf'))
.then(response => response.blob())
.then(blob => {
const modelUrl = URL.createObjectURL(blob);
reduxStore.dispatch(dispatchGltfUrl(modelUrl));
reduxStore.dispatch(dispatchSetHotspots([]));
})
.catch((error) => {
console.error('Error:', error);
});
}

async waitForState(envChanged: boolean) {
let partialState: any = {};
await fetch(this.snippetPipeUrl)
.then((response) => {
if (response.ok) {
return response.json();
} else {
throw new Error('Something went wrong');
}
})
.then((responseJson) => {
partialState = responseJson;
})
.catch((error) => {
console.log('error', error);
});

// These links would be corresponding to the original editor's link.
if (envChanged) {
partialState.config.environmentImage = undefined;
} else if (this.config.environmentImage) {
partialState.config.environmentImage = this.config.environmentImage;
}
partialState.config.src = this.gltfUrl;

reduxStore.dispatch(dispatchSetHotspots(partialState.hotspots));
reduxStore.dispatch(dispatchSetCamera(partialState.camera));
reduxStore.dispatch(dispatchSetConfig(partialState.config));
reduxStore.dispatch(dispatchArConfig(partialState.arConfig));
}

async waitForEnv(envIsHdr: boolean) {
await fetch(this.getSrcPipeUrl('env'))
.then(response => response.blob())
.then(blob => {
// simulating createBlobUrlFromEnvironmentImage
const addOn = envIsHdr ? '#.hdr' : '';
const envUrl = URL.createObjectURL(blob) + addOn;
reduxStore.dispatch(dispatchEnvrionmentImage(envUrl));
})
.catch((error) => {
console.error('Error:', error);
});
}

async waitForData(json: any) {
if (json.gltfChanged) {
await this.waitForModel();
}
if (json.stateChanged) {
await this.waitForState(json.envChanged);
}
if (json.envChanged) {
await this.waitForEnv(json.envIsHdr);
}
}

async fetchLoop() {
await fetch(this.updatesPipeUrl)
.then(response => response.json())
.then(json => this.waitForData(json))
.catch((error) => {
console.error('Error:', error);
});
}

async triggerFetchLoop() {
await this.fetchLoop();
await this.triggerFetchLoop();
}

render() {
const config = {...this.config};
applyCameraEdits(config, this.camera);
const skyboxImage =
config.useEnvAsSkybox ? config.environmentImage : undefined;
const childElements = [...renderHotspots(this.hotspots)];
return html`
<div class="app">
<div class="mvContainer">
<model-viewer
src=${this.gltfUrl || ''}
?ar=${ifDefined(!!this.arConfig.ar)}
ar-modes=${ifDefined(this.arConfig!.arModes)}
?autoplay=${!!config.autoplay}
?auto-rotate=${!!config.autoRotate}
?camera-controls=${!!config.cameraControls}
environment-image=${ifDefined(config.environmentImage)}
skybox-image=${ifDefined(skyboxImage)}
exposure=${ifDefined(config.exposure)}
poster=${ifDefined(config.poster)}
reveal=${ifDefined(config.reveal)}
shadow-intensity=${ifDefined(config.shadowIntensity)}
shadow-softness=${ifDefined(config.shadowSoftness)}
camera-target=${ifDefined(config.cameraTarget)}
camera-orbit=${ifDefined(config.cameraOrbit)}
field-of-view=${ifDefined(config.fieldOfView)}
min-camera-orbit=${ifDefined(config.minCameraOrbit)}
max-camera-orbit=${ifDefined(config.maxCameraOrbit)}
min-field-of-view=${ifDefined(config.minFov)}
max-field-of-view=${ifDefined(config.maxFov)}
animation-name=${ifDefined(config.animationName)}
>${childElements}</model-viewer>
</div>
</div>`;
}

async ping() {
await fetch(this.mobilePingUrl, {
method: 'POST',
body: JSON.stringify({isPing: true}),
})
.then(response => {
console.log('Success:', response);
})
.catch((error) => {
console.log('Error:', error);
throw new Error(`Failed to post: ${this.mobilePingUrl}`);
});
}

// (Overriding default) Tell editor session that it is ready for data.
// @ts-ignore changedProperties is unused
firstUpdated(changedProperties: any) {
this.ping();
this.triggerFetchLoop();
}
}

declare global {
interface HTMLElementTagNameMap {
'mobile-view': MobileView;
}
}
Loading

0 comments on commit aa2587c

Please sign in to comment.