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