Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3dom and iframes #1025

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
68 changes: 68 additions & 0 deletions packages/3dom/demo/iframe/iframe.html
@@ -0,0 +1,68 @@
<html>
<head>
<style>
#controls {
position: absolute;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
bottom: 0;
left: 0;
width: 100%;
height: 64px;
}

button {
font-size: 2em;
margin: 0 0.25em;
}
</style>
</head>
<body>
<div id="controls">
<button data-color="1,0,0,1">Red</button>
<button data-color="0,1,0,1">Green</button>
<button data-color="0,0,1,1">Blue</button>
</div>
<script type="3DOM">
console.log('Hello from the 3DOM worker!');
self.addEventListener('model-change', (event) => {
const {model} = event;
const [material] = model.materials;
console.log('model-change received in the 3DOM worker!');


self.addEventListener('message', (event) => {
console.log('message from the main thread: ' + event.data);
switch(event.data.action) {
case 'change-color':
console.log('Changing color to:', event.data.payload);
material.pbrMetallicRoughness.setBaseColorFactor(event.data.payload);
break;
}
});
});
</script>
<script type="module" src="iframe.js"></script>
<script type="module">
function threeDOMReady(e) {
window.removeEventListener('3domready', threeDOMReady);
const executionContext = e.detail.executionContext;
document.querySelector('#controls').addEventListener('click', (event) => {
const colorString = event.target.dataset.color;
if (!colorString) {
return;
}
const color = colorString.split(',')
.map(numberString => parseFloat(numberString));
executionContext.worker.postMessage({
action: 'change-color',
payload: color
});
});
}
window.addEventListener('3domready', threeDOMReady);
</script>
</body>
</html>
4 changes: 4 additions & 0 deletions packages/3dom/demo/iframe/iframe.js
@@ -0,0 +1,4 @@
import { IFrameThreeDOMExecutionContext } from '../../lib/context-iframe.js';

const context =
new IFrameThreeDOMExecutionContext(['messaging', 'material-properties']);
10 changes: 10 additions & 0 deletions packages/3dom/demo/iframe/index.html
@@ -0,0 +1,10 @@
<html>
<body>
<!-- <script type="module" src="../../../../dist/model-viewer.js"></script> -->
<script type="module" src="../../../model-viewer/dist/model-viewer.js"></script>
<model-viewer id="mv" src="../../../shared-assets/models/Astronaut.glb" auto-rotate camera-controls>
</model-viewer>
<iframe id="iframe" src="iframe.html"></iframe>
<script type="module" src="index.js"></script>
</body>
</html>
10 changes: 10 additions & 0 deletions packages/3dom/demo/iframe/index.js
@@ -0,0 +1,10 @@
import { HostThreeDOMExecutionContext } from '../../lib/context-host.js';

const context =
new HostThreeDOMExecutionContext(['messaging', 'material-properties']);
function threeDOMReady() {
window.removeEventListener('3domReady', threeDOMReady);
const modelViewer = document.querySelector('model-viewer');
modelViewer.setThreeDOMExecutionContext(context);
}
window.addEventListener('3domready', threeDOMReady);
108 changes: 108 additions & 0 deletions packages/3dom/src/context-host.ts
@@ -0,0 +1,108 @@
import {ThreeDOMCapability} from './api.js';
import {ThreeDOMExecutionContext} from './context.js';
import {ThreeDOMMessageType} from './protocol.js';

// This fake worker class is just used to capture the transfer of the message
// port.
class FakeWorker extends EventTarget implements Worker {
onerror: EventListener|null = null;
onmessage: EventListener|null = null;
onmessageerror: EventListener|null = null;
port2: MessagePort|null = null;
hostExecutionContext: HostThreeDOMExecutionContext;

constructor(hostExecutionContext: HostThreeDOMExecutionContext) {
super();
this.hostExecutionContext = hostExecutionContext;
}

postMessage(message: any, transfer: Array<Transferable>): void;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what type is message? it seems like it is just an Object? Linter is complaining we're supposed to avoid any in general...

postMessage(message: any, options?: PostMessageOptions|undefined): void;
postMessage(message: any, secondArg: any): void {
// When the handshake message is sent, capture and store the port.
if (message.type == ThreeDOMMessageType.HANDSHAKE) {
const transfer = secondArg as Array<Transferable>;
this.port2 = transfer[0] as MessagePort;
// Indicate the host execution context that the port has been captured.
this.hostExecutionContext.portHasBeenSet();
}
}

terminate() {
this.port2 = null;
}
}

const $iframe = Symbol('iframe');
const $iframeLoaded = Symbol('iframeLoaded');

export class HostThreeDOMExecutionContext extends ThreeDOMExecutionContext {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring needed here

protected[$iframe]: HTMLIFrameElement;
protected[$iframeLoaded] = false;

constructor(
capabilities: Array<ThreeDOMCapability>,
iframe: HTMLIFrameElement|null = null) {
super(capabilities);
// Make sure there is an iframe to connect with

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this[$iframe] = iframe ? iframe : document.querySelector('iframe');
if (!this[$iframe]) {
throw new Error(...
}

if (!iframe) {
iframe = document.querySelector('iframe');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constant?

if (!iframe) {
throw new Error(
'Either provide an iframe or the page should contain and iframe.');
}
}
this[$iframe] = iframe;
// Wait for the iframe to load
const onIFrameLoaded = () => {
this[$iframeLoaded] = true;
this[$iframe].removeEventListener('load', onIFrameLoaded);
this.sendHandshakeToIFrame();
};
this[$iframe].addEventListener('load', onIFrameLoaded);
}

// Called from the FakeWorker when the postMessage is passed.
portHasBeenSet() {
this.sendHandshakeToIFrame();
}

protected sendHandshakeToIFrame() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring

// Wait until the iframe is loaded AND the port has been set
const fakeWorker = this.worker as FakeWorker;
const port2 = fakeWorker.port2;
if (!this[$iframeLoaded] || !port2) {
return;
}
// Listen for messages from the iframe

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason this isn't just a class method?

const onMessageReceived = (event: MessageEvent) => {
// If the iframe send the handshake response, it will contain the 3DOM
// script to be loaded, so evaluate it (it will actually be received and
// evaluated in the iframe's worker).
if (event.data.action === 'handshakeResponse') {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should all these literal strings be constants?

// No need to listen to more messages from the iframe
window.removeEventListener('message', onMessageReceived);
// Load the script passed from the iframe
this.eval(event.data.payload);
// Indicate the iframe that the host is ready
const contentWindow = this[$iframe].contentWindow;
if (contentWindow) {
contentWindow.postMessage({action: 'ready'}, '*');
}
window.dispatchEvent(
new CustomEvent('3domready', {detail: {executionContext: this}}));
}
};
window.addEventListener('message', onMessageReceived);
// Send the hadnshake to the iframe transferring the port
const contentWindow = this[$iframe].contentWindow;
if (contentWindow) {
contentWindow.postMessage({action: 'handshakeRequest'}, '*', [port2]);
}
}

// Override
protected createWorker(_url: string): Worker {
return new FakeWorker(this);
}
}
79 changes: 79 additions & 0 deletions packages/3dom/src/context-iframe.ts
@@ -0,0 +1,79 @@
import {ThreeDOMCapability} from './api.js';
import {ThreeDOMExecutionContext} from './context.js';
import {ThreeDOMMessageType} from './protocol.js';

// This semi fake worker is used to capture the post message of the handshake
// and delay it until the port is given.
class IFrameWorker extends Worker {
protected handshakeMessage: any;
protected port2: MessagePort|null = null;

constructor(url: string) {
super(url);
}

// Override
postMessage(message: any, transfer: Array<Transferable>): void;
postMessage(message: any, options?: PostMessageOptions|undefined): void;
postMessage(message: any, secondArg: any): void {
if (message.type == ThreeDOMMessageType.HANDSHAKE) {
this.handshakeMessage = message;
this.postHandshakeMessage();
} else {
super.postMessage(message, secondArg);
}
}

setPort2(port2: MessagePort): void {
this.port2 = port2;
this.postHandshakeMessage();
}

protected postHandshakeMessage() {
if (this.handshakeMessage && this.port2) {
super.postMessage(this.handshakeMessage, [this.port2]);
}
}
}

export class IFrameThreeDOMExecutionContext extends ThreeDOMExecutionContext {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring

constructor(capabilities: Array<ThreeDOMCapability>) {
super(capabilities);
const onMessageReceived = (event: MessageEvent) => {
switch (event.data.action) {
case 'handshakeRequest': {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

especially if used in multiple places, these string literals should be constants

if (!event.source) {
throw new Error('No event source to post message to.');
}
const script = document.querySelector('script[type="3DOM"]');
if (!script) {
throw new Error('No 3DOM script found in the page.');
}
const scriptText = script.textContent;
// TODO: Check is the scriptText has content?

// Pass the transferred port to the worker
const iframeWorker = this.worker as IFrameWorker;
iframeWorker.setPort2(event.ports[0]);

// Respond to the host so it can inject the 3DOM script
const source = event.source as WindowProxy;
source.postMessage(
{action: 'handshakeResponse', payload: scriptText}, event.origin);
break;
}
case 'ready':
window.removeEventListener('message', onMessageReceived);
window.dispatchEvent(
new CustomEvent('3domready', {detail: {executionContext: this}}));
break;
}
};
window.addEventListener('message', onMessageReceived);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this too

}

// Override
protected createWorker(url: string): Worker {
return new IFrameWorker(url);
}
}
6 changes: 5 additions & 1 deletion packages/3dom/src/context.ts
Expand Up @@ -135,14 +135,18 @@ export class ThreeDOMExecutionContext extends EventTarget {
protected[$workerInitializes]: Promise<MessagePort>;
protected[$modelGraftManipulator]: ModelGraftManipulator|null = null;

protected createWorker(url: string): Worker {
return new Worker(url);
}

constructor(capabilities: Array<ThreeDOMCapability>) {
super();

const contextScriptSource = generateContextScriptSource(capabilities);
const url = URL.createObjectURL(
new Blob([contextScriptSource], {type: 'text/javascript'}));

this[$worker] = new Worker(url);
this[$worker] = this.createWorker(url);
this[$workerInitializes] = new Promise<MessagePort>((resolve) => {
const {port1, port2} = new MessageChannel();
const onMessageEvent = (event: MessageEvent) => {
Expand Down
11 changes: 11 additions & 0 deletions packages/model-viewer/src/features/scene-graph.ts
Expand Up @@ -40,6 +40,8 @@ const $modelGraftMutationHandler = Symbol('modelGraftMutationHandler');

export interface SceneGraphInterface {
worklet: Worker|null;
setThreeDOMExecutionContext:
(executionContext: ThreeDOMExecutionContext) => void;
}

/**
Expand Down Expand Up @@ -120,6 +122,15 @@ export const SceneGraphMixin = <T extends Constructor<ModelViewerElementBase>>(
return executionContext != null ? executionContext.worker : null;
}

setThreeDOMExecutionContext(executionContext: ThreeDOMExecutionContext) {
if (this[$executionContext] != null) {
this[$executionContext]!.terminate();
this[$executionContext] = null;
}
this[$executionContext] = executionContext;
this[$updateExecutionContextModel]();
}

connectedCallback() {
super.connectedCallback();

Expand Down