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();