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
3dom and iframes #1025
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { IFrameThreeDOMExecutionContext } from '../../lib/context-iframe.js'; | ||
|
||
const context = | ||
new IFrameThreeDOMExecutionContext(['messaging', 'material-properties']); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this[$iframe] = iframe ? iframe : document.querySelector('iframe'); |
||
if (!iframe) { | ||
iframe = document.querySelector('iframe'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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': { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this too |
||
} | ||
|
||
// Override | ||
protected createWorker(url: string): Worker { | ||
return new IFrameWorker(url); | ||
} | ||
} |
There was a problem hiding this comment.
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...