diff --git a/README.md b/README.md new file mode 100644 index 0000000..1730dce --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +

Exokit Studio

+

An interface for the Exokit Engine.

+ +

+ + + + + + +

+ +
+ Site + — + Docs + — + Discord + — + Twitter + — + Email List +
+ +## Examples + +Hands Reality Tab +Live Reload Magic Leap +Tutorial Reality Tab + +Reality Projection with HTC Vive and Magic Leap +Emukit +Various Exokit Apps + +*Find more examples on [YouTube](https://www.youtube.com/channel/UC87Q7_5ooY8FSLwOec52ZPQ).* + + +## Overview + +Exokit Studio **enables developers to build XR experiences with ease** by having a seamless interface to the Exokit Engine. Studio prefers using GUIs instead of command lines with difficult to remember arguments to use the functionality you want. + +Studio uses the [Exokit Engine](https://github.com/exokitxr/exokit), which is written on top of Node and emulates a web browser, providing native hooks for WebGL, WebGL2, WebVR, WebXR, WebAudio, and other APIs used in immersive experiences. + +:eyeglasses: **Exokit Engine currently targets the following platforms**: +* OpenVR Desktop VR (Steam compatible) +* HTC Vive +* Valve Index * +* Oculus Desktop (Oculus Rift, Oculus Rift S) +* Oculus Mobile (Oculus Quest, Oculus Go, GearVR) +* Magic Leap +* iOS ARKit * +* Android ARCore * +* Google VR (Daydream / Cardboard / Mirage Solo) * +* Hololens / Hololens 2 * +* any XR device, start a [pull request](https://github.com/exokitxr/exokit/compare) to the Exokit Engine with a native binding if it isn't listed here! * + +\* not supported yet + +:electric_plug: **Exokit Engine powers experiences built with**: +* Three.js +* Unity +* Pixi.js +* Babylon.js +* A-Frame +* Custom WebGL frameworks +* WebAssembly, TypeScript, and any language that transpiles to JavaScript +* Unity WebVR export * +* SteamVR * +* any 3d web framework, start a [pull request](https://github.com/exokitxr/exokit/compare) to the Exokit Engine if a 3d web framework isn't currently supported! * + +\* not supported yet + +## Quickstart + +### Desktop +

Download and install Studio for current OS

+ +### Local Development + +```sh +git clone https://github.com/exokitxr/studio.git +cd studio +npm install +npm build +``` + +## Stay in Touch + +- [Join our Discord](https://discord.gg/Apk6cZN) for discussions. +- [Follow @exokitxr on Twitter](https://twitter.com/exokitxr) for updates. diff --git a/exobot.html b/exobot.html new file mode 100644 index 0000000..1930185 --- /dev/null +++ b/exobot.html @@ -0,0 +1,149 @@ + + + + + + + exobot + + + + + +

exobot

+ + + + + diff --git a/graffiti_ml.html b/graffiti_ml.html index f18989e..c59404f 100644 --- a/graffiti_ml.html +++ b/graffiti_ml.html @@ -331,7 +331,7 @@

graffiti_ml

scene.add(controllerMesh); }); - const cubeGeometry = new THREE.BoxBufferGeometry(0.02, 0.02, 0.001); + const cubeGeometry = new THREE.BoxBufferGeometry(0.05, 0.05, 0.05); const hitMeshMaterial = new THREE.MeshPhongMaterial({ color: 0x673ab7, }); diff --git a/index.html b/index.html index 9011030..257717d 100644 --- a/index.html +++ b/index.html @@ -11,9 +11,14 @@ - - - - \ No newline at end of file diff --git a/ui/public/index.html b/ui/public/index.html index cbaaddb..0defbd8 100644 --- a/ui/public/index.html +++ b/ui/public/index.html @@ -12,7 +12,7 @@ - Exokit Launcher + Exokit Studio diff --git a/ui/public/manifest.json b/ui/public/manifest.json index fddfd1d..bc4df82 100644 --- a/ui/public/manifest.json +++ b/ui/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "Exokit Launcher", - "name": "Exokit Launcher", + "short_name": "Exokit Studio", + "name": "Exokit Studio", "icons": [ { "src": "favicon.ico", diff --git a/ui/src/components/Dom.jsx b/ui/src/components/Dom.jsx index ad1d815..1c0f0e5 100644 --- a/ui/src/components/Dom.jsx +++ b/ui/src/components/Dom.jsx @@ -65,6 +65,7 @@ class Dom extends React.Component { }; _decorateKeypaths(root); this.state = { + domView: this.props.domView, root, selectEl: null, epoch: 0, @@ -97,6 +98,20 @@ class Dom extends React.Component { }); } + componentDidUpdate(prevProps, prevState) { + if (this.state.domView !== prevState.domView) { + this.setState({ + domView: prevState.domView, + }); + } + } + + static getDerivedStateFromProps(nextProps, prevState) { + return { + domView: nextProps.domView, + }; + } + onClick(el) { if (this.state.selectEl !== el) { this.setState({ @@ -114,7 +129,7 @@ class Dom extends React.Component { render() { return (
- this.onClick(el)} /> + this.onClick(el)} /> {/*this.state.selectEl ? : null*/}
); @@ -127,6 +142,7 @@ class DomList extends React.Component { this.state = { hoverEl: null, + domView: this.props.domView, }; } @@ -144,10 +160,24 @@ class DomList extends React.Component { } } + componentDidUpdate(prevProps, prevState) { + if (this.state.domView !== prevState.domView) { + this.setState({ + domView: prevState.domView, + }); + } + } + + static getDerivedStateFromProps(nextProps, prevState) { + return { + domView: nextProps.domView, + }; + } + render() { return ( ); } @@ -162,13 +192,19 @@ class DomItem extends React.Component { this.state = { open: true, dropdownOpen: false, + domView: this.props.domView, }; } componentDidMount() { this.bindDomEl(); } - componentDidUpdate() { + componentDidUpdate(prevProps, prevState) { + if (this.state.domView !== prevState.domView) { + this.setState({ + domView: prevState.domView, + }); + } this.bindDomEl(); } @@ -192,6 +228,12 @@ class DomItem extends React.Component { return classNames.join(' '); } + static getDerivedStateFromProps(nextProps, prevState) { + return { + domView: nextProps.domView, + }; + } + getStyle() { return { paddingLeft: `${this.props.level * 10}px`, @@ -243,41 +285,69 @@ class DomItem extends React.Component { render() { const {el, level} = this.props; - if (el.nodeType === Node.ELEMENT_NODE && el.tagName === 'IFRAME') { - return ( -
  • -
    this.props.onClick(el)} onMouseEnter={() => this.props.onMouseEnter(el)} onMouseLeave={() => this.props.onMouseLeave(el)} ref={this.domElRef} tabIndex={-1}> -
    this.toggleDropdownOpen(e)}>...
    -
    {_el3Text(el)}
    -
    -
    {this.state.dropdownOpen ? -
    -
    this.cloneTab()}>Clone
    -
    this.deleteTab()}>Delete
    + if (this.state.domView) { + if (el.nodeType === Node.ELEMENT_NODE) { + return ( +
  • +
    this.props.onClick(el)} onMouseEnter={() => this.props.onMouseEnter(el)} onMouseLeave={() => this.props.onMouseLeave(el)} ref={this.domElRef} tabIndex={-1}> +
    this.toggleOpen(e)}>⮞
    + +
    +
    + {el.childNodes.map((childNode, i) => this.props.onClick(el)} onMouseEnter={this.props.onMouseEnter} onMouseLeave={this.props.onMouseLeave} key={i}/>)} +
    +
  • + ); + } else if (el.nodeType === Node.TEXT_NODE) { + if (/\S/.test(el.value)) { // has non-whitespace + return ( +
  • this.props.onClick(el)} onMouseEnter={() => this.props.onMouseEnter(el)} onMouseLeave={() => this.props.onMouseLeave(el)} ref={this.domElRef} tabIndex={-1}> +
    "{el.value}"
    +
  • + ); + } else { + return null; + } + } else { + return null; + } + } else if (!this.state.domView) { + if (el.nodeType === Node.ELEMENT_NODE && el.tagName === 'IFRAME') { + return ( +
  • +
    this.props.onClick(el)} onMouseEnter={() => this.props.onMouseEnter(el)} onMouseLeave={() => this.props.onMouseLeave(el)} ref={this.domElRef} tabIndex={-1}> +
    this.toggleDropdownOpen(e)}>...
    +
    {_el3Text(el)}
    - : null} -
  • - ); - } else if (el.nodeType === Node.ELEMENT_NODE) { - return ( -
  • -
    - {el.childNodes.map((childNode, i) => this.props.onClick(el)} onMouseEnter={this.props.onMouseEnter} onMouseLeave={this.props.onMouseLeave} key={i}/>)} -
    -
  • - ); - } else if (el.nodeType === Node.TEXT_NODE) { - if (/\S/.test(el.value)) { // has non-whitespace +
    {this.state.dropdownOpen ? +
    +
    this.cloneTab()}>Clone
    +
    this.deleteTab()}>Delete
    +
    + : null}
    + + ); + } else if (el.nodeType === Node.ELEMENT_NODE) { return ( -
  • this.props.onClick(el)} onMouseEnter={() => this.props.onMouseEnter(el)} onMouseLeave={() => this.props.onMouseLeave(el)} ref={this.domElRef} tabIndex={-1}> -
    +
  • +
    + {el.childNodes.map((childNode, i) => this.props.onClick(el)} onMouseEnter={this.props.onMouseEnter} onMouseLeave={this.props.onMouseLeave} key={i}/>)} +
  • ); + } else if (el.nodeType === Node.TEXT_NODE) { + if (/\S/.test(el.value)) { // has non-whitespace + return ( +
  • this.props.onClick(el)} onMouseEnter={() => this.props.onMouseEnter(el)} onMouseLeave={() => this.props.onMouseLeave(el)} ref={this.domElRef} tabIndex={-1}> +
    +
  • + ); + } else { + return null; + } } else { return null; } - } else { - return null; } } } diff --git a/ui/src/components/Engine.jsx b/ui/src/components/Engine.jsx index 57b3cf1..f96d1d7 100644 --- a/ui/src/components/Engine.jsx +++ b/ui/src/components/Engine.jsx @@ -22,6 +22,7 @@ class Engine extends React.Component { this.handleURLChange = this.handleURLChange.bind(this); this.state = { consoleOpen: false, + domView: false, flags: [], item: null, settings: null, @@ -219,6 +220,16 @@ class Engine extends React.Component { }); } + toggleDomView(e) { + this.setState({ + domView: !this.state.domView, + }); + setTimeout( + function() { + _postViewportMessage(); + }, 100); + } + blur() { this.setState({ item: null, @@ -284,9 +295,9 @@ class Engine extends React.Component {
    -
    this.addTemplate('kitchenSink')}> - -
    Kitchen sink
    +
    this.addTemplate('webXrSample')}> + +
    WebXR Sample
    this.addTemplate('exobot')}> @@ -321,6 +332,10 @@ class Engine extends React.Component {
    +
    this.toggleDomView(e)}> + +
    Dom View
    +
    this.toggleConsoleOpen(e)}>
    Console
    @@ -341,8 +356,7 @@ class Engine extends React.Component {
    this.onEngineRenderClick()} /> { _postViewportMessage(); }}> @@ -356,7 +370,7 @@ class Engine extends React.Component { _postViewportMessage(); }}>
    - +
    diff --git a/ui/src/css/console.css b/ui/src/css/console.css index 1b13cd4..ac1a362 100644 --- a/ui/src/css/console.css +++ b/ui/src/css/console.css @@ -1,14 +1,21 @@ .Console { position: relative; - height: 0px; + max-height: 0px; border-top: 2px solid #222; font-family: monospace; font-size: 11px; white-space: pre-wrap; - overflow-y: auto; + overflow-x: none; + overflow-y: scroll; + display: flex; + align-items: flex-end; + flex-direction:column; + justify-content:space-between; } .Console.open { - height: 100px; + height: 100%; + max-height: 100%; + min-height: 150px; } .Console .console-output { diff --git a/ui/src/css/dom.css b/ui/src/css/dom.css index 80284cd..ccd37f0 100644 --- a/ui/src/css/dom.css +++ b/ui/src/css/dom.css @@ -22,7 +22,13 @@ cursor: pointer; user-select: none; } -.Dom > .dom-list .dom-item.dropdownOpen > .dom-item-label .dom-item-arrow { +.Dom > .dom-list .dom-item.open > .dom-item-label .dom-item-arrow { + color: #888; + padding-top: 5px; + padding-bottom: 5px; + transform: rotatez(90deg); +} +.Dom > .dom-list .dom-item.dropdownOpen > .dom-item-label .dom-item-dots { color: #888; padding-top: 5px; padding-bottom: 5px; @@ -38,23 +44,36 @@ display: flex; } .Dom > .dom-list .dom-item.hover > .dom-item-label, -.Dom > .dom-list .dom-item.hover > .dom-item-text +.Dom > .dom-list .dom-item.hover > .dom-item-text, +.Dom > .dom-list .dom-item.hover > .dom-item-label .dom-item-name, +.Dom > .dom-list .dom-item.hover > .dom-item-label .dom-item-name:focus { background-color: #333; } .Dom > .dom-list .dom-item.select > .dom-item-label, -.Dom > .dom-list .dom-item.select > .dom-item-text +.Dom > .dom-list .dom-item.select > .dom-item-text, +.Dom > .dom-list .dom-item.select > .dom-item-label .dom-item-name, +.Dom > .dom-list .dom-item.hover > .dom-item-label .dom-item-name:focus { background-color: #333; } -.Dom > .dom-list .dom-item.select > .dom-item-label .dom-item-arrow { +.Dom > .dom-list .dom-item > .dom-item-label .dom-item-arrow { + padding-top: 5px; + padding-bottom: 5px; + color: #888; + transform: none; +} +.Dom > .dom-list .dom-item.select > .dom-item-label .dom-item-dots { color: #888; } .Dom > .dom-list .dom-item.hover > .dom-item-label .dom-item-arrow { color: #888; } -.Dom > .dom-list .dom-item > .dom-item-label .dom-item-arrow { +.Dom > .dom-list .dom-item.hover > .dom-item-label .dom-item-dots { + color: #888; +} +.Dom > .dom-list .dom-item > .dom-item-label .dom-item-dots { color: #444; transform: rotatez(90deg); padding-top: 5px; @@ -65,8 +84,17 @@ padding-top: 5px; padding-left: 3px; white-space: nowrap; + border: none; + padding: 2px; + background-color: #232526; + color: white; +} +.Dom > .dom-list .dom-item > .dom-item-label .dom-item-name:focus { + border: 1px solid black; + -moz-border-radius: 2px; + border-radius: 2px; + background-color: #333; } - .Dom > .dom-list .dom-item > .dom-item-children { display: none; flex-direction: column; @@ -89,6 +117,8 @@ padding-left: 10px; display: table-cell; vertical-align: middle; + position: absolute; + z-index: 1; } .Dom > .dom-detail > .dom-detail-name { font-family: monospace; diff --git a/ui/src/css/engine.css b/ui/src/css/engine.css index 2a065d1..7b414a7 100644 --- a/ui/src/css/engine.css +++ b/ui/src/css/engine.css @@ -160,7 +160,7 @@ bottom: 0; left: 0; right: 0; - background-color: rgba(0, 0, 0, 0.5); + /* background-color: rgba(0, 0, 0, 0.5); */ } #Engine .settings > .settings-foreground { position: relative; @@ -202,5 +202,5 @@ } #Engine .engine-render { flex: 1; - background-color: #000; + background-color: #0F0; }