From 10f2c9748fc7afe6a3e98f16fbf844e4346cea6b Mon Sep 17 00:00:00 2001 From: Iker Jamardo Date: Thu, 13 Feb 2020 13:14:42 -0800 Subject: [PATCH] 3dom and iframes Use case: A page contains a model-viewer instance but the functionality to control that instance through 3dom scripts is emedded in an iframe. * Modified ThreeDOMExecutionContext to create the worker using a factory method. * 2 new classes that inherit from ThreeDOMExecutionContext and override the worker creation method factory method. - HostThreeDOMExecutionContext: For the host page that includes the iframe. The returned worker is fake and just used to intercept the passing of the MessagePort. The port is transferred not to the worker in the host page but to a worker in the iframe page. This class handles all the handshake necessary with the iframe. - IFrameThreeDOMExecutionContext: For the iframe page. The returned worker is an actual worker but it waits to hand over the port until it is received from the host page. * The scene-graph feature exposes new method: setThreeDOMExecutionContext. * New demo to show the iframe functionality. --- packages/3dom/demo/iframe/iframe.html | 68 +++++++++++ packages/3dom/demo/iframe/iframe.js | 4 + packages/3dom/demo/iframe/index.html | 10 ++ packages/3dom/demo/iframe/index.js | 10 ++ packages/3dom/src/context-host.ts | 108 ++++++++++++++++++ packages/3dom/src/context-iframe.ts | 79 +++++++++++++ packages/3dom/src/context.ts | 6 +- .../model-viewer/src/features/scene-graph.ts | 11 ++ 8 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 packages/3dom/demo/iframe/iframe.html create mode 100644 packages/3dom/demo/iframe/iframe.js create mode 100644 packages/3dom/demo/iframe/index.html create mode 100644 packages/3dom/demo/iframe/index.js create mode 100644 packages/3dom/src/context-host.ts create mode 100644 packages/3dom/src/context-iframe.ts diff --git a/packages/3dom/demo/iframe/iframe.html b/packages/3dom/demo/iframe/iframe.html new file mode 100644 index 0000000000..e35df010f0 --- /dev/null +++ b/packages/3dom/demo/iframe/iframe.html @@ -0,0 +1,68 @@ + + + + + +
+ + + +
+ + + + + \ No newline at end of file diff --git a/packages/3dom/demo/iframe/iframe.js b/packages/3dom/demo/iframe/iframe.js new file mode 100644 index 0000000000..b814ba7e49 --- /dev/null +++ b/packages/3dom/demo/iframe/iframe.js @@ -0,0 +1,4 @@ +import { IFrameThreeDOMExecutionContext } from '../../lib/context-iframe.js'; + +const context = + new IFrameThreeDOMExecutionContext(['messaging', 'material-properties']); \ No newline at end of file diff --git a/packages/3dom/demo/iframe/index.html b/packages/3dom/demo/iframe/index.html new file mode 100644 index 0000000000..d75eaf60fc --- /dev/null +++ b/packages/3dom/demo/iframe/index.html @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/packages/3dom/demo/iframe/index.js b/packages/3dom/demo/iframe/index.js new file mode 100644 index 0000000000..d406992450 --- /dev/null +++ b/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); diff --git a/packages/3dom/src/context-host.ts b/packages/3dom/src/context-host.ts new file mode 100644 index 0000000000..4acdf98d26 --- /dev/null +++ b/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): void; + 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; + 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 { + protected[$iframe]: HTMLIFrameElement; + protected[$iframeLoaded] = false; + + constructor( + capabilities: Array, + iframe: HTMLIFrameElement|null = null) { + super(capabilities); + // Make sure there is an iframe to connect with + if (!iframe) { + iframe = document.querySelector('iframe'); + 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() { + // 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 + 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') { + // 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); + } +} diff --git a/packages/3dom/src/context-iframe.ts b/packages/3dom/src/context-iframe.ts new file mode 100644 index 0000000000..ceeff2a1d0 --- /dev/null +++ b/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): 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 { + constructor(capabilities: Array) { + super(capabilities); + const onMessageReceived = (event: MessageEvent) => { + switch (event.data.action) { + case 'handshakeRequest': { + 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); + } + + // Override + protected createWorker(url: string): Worker { + return new IFrameWorker(url); + } +} diff --git a/packages/3dom/src/context.ts b/packages/3dom/src/context.ts index ec75e00500..b17425625f 100644 --- a/packages/3dom/src/context.ts +++ b/packages/3dom/src/context.ts @@ -135,6 +135,10 @@ export class ThreeDOMExecutionContext extends EventTarget { protected[$workerInitializes]: Promise; protected[$modelGraftManipulator]: ModelGraftManipulator|null = null; + protected createWorker(url: string): Worker { + return new Worker(url); + } + constructor(capabilities: Array) { super(); @@ -142,7 +146,7 @@ export class ThreeDOMExecutionContext extends EventTarget { const url = URL.createObjectURL( new Blob([contextScriptSource], {type: 'text/javascript'})); - this[$worker] = new Worker(url); + this[$worker] = this.createWorker(url); this[$workerInitializes] = new Promise((resolve) => { const {port1, port2} = new MessageChannel(); const onMessageEvent = (event: MessageEvent) => { diff --git a/packages/model-viewer/src/features/scene-graph.ts b/packages/model-viewer/src/features/scene-graph.ts index a02c075d4d..d8d59438dc 100644 --- a/packages/model-viewer/src/features/scene-graph.ts +++ b/packages/model-viewer/src/features/scene-graph.ts @@ -40,6 +40,8 @@ const $modelGraftMutationHandler = Symbol('modelGraftMutationHandler'); export interface SceneGraphInterface { worklet: Worker|null; + setThreeDOMExecutionContext: + (executionContext: ThreeDOMExecutionContext) => void; } /** @@ -120,6 +122,15 @@ export const SceneGraphMixin = >( 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();