Compare commits
No commits in common. "main" and "feat/pty" have entirely different histories.
56 changed files with 2021 additions and 8217 deletions
2
.env
2
.env
|
|
@ -1,2 +0,0 @@
|
||||||
git remote | grep codeberg || git remote add codeberg git@codeberg.org:xrsh/xrsh-com.git
|
|
||||||
git remote | grep c-frame || git remote add c-frame git@github.com:c-frame/xrsh-com.git
|
|
||||||
|
|
@ -13,7 +13,7 @@ jobs:
|
||||||
- run: "echo \"${{ secrets.SSHKEY_APPS }}\" > ~/.ssh/id_rsa"
|
- run: "echo \"${{ secrets.SSHKEY_APPS }}\" > ~/.ssh/id_rsa"
|
||||||
- run: ssh-keyscan github.com >> ~/.ssh/known_hosts # see https://gist.github.com/vikpe/34454d69fe03a9617f2b009cc3ba200b
|
- run: ssh-keyscan github.com >> ~/.ssh/known_hosts # see https://gist.github.com/vikpe/34454d69fe03a9617f2b009cc3ba200b
|
||||||
- run: chmod 600 -R ~/.ssh
|
- run: chmod 600 -R ~/.ssh
|
||||||
- run: git remote add github git@github.com:c-frame/xrsh-com.git
|
- run: git remote add github git@github.com:coderofsalvation/xrsh-apps
|
||||||
- run: git push github main
|
- run: git push github main
|
||||||
# *todo* trigger deploy at website
|
# *todo* trigger deploy at website
|
||||||
#- run: git clone git@github.com:coderofsalvation/xrsh xrsh.github # now push empty commit to deploy website
|
#- run: git clone git@github.com:coderofsalvation/xrsh xrsh.github # now push empty commit to deploy website
|
||||||
41
README.awk
41
README.awk
|
|
@ -1,41 +0,0 @@
|
||||||
#!/usr/bin/env -S awk -f
|
|
||||||
# a no-nonsense source-to-markdown generator which scans for:
|
|
||||||
#
|
|
||||||
# /**
|
|
||||||
# * # foo
|
|
||||||
# *
|
|
||||||
# * this is markdown $(cat bar.md)
|
|
||||||
# */
|
|
||||||
#
|
|
||||||
# var foo; // comment with 2 leading spaces is markdown too $(date)
|
|
||||||
#
|
|
||||||
# easily refactorable to hash-based languages (py/bash/perl/lua e.g.)
|
|
||||||
# by changing the regexes
|
|
||||||
#
|
|
||||||
|
|
||||||
BEGIN{
|
|
||||||
# printf README.md until '# Component List'
|
|
||||||
system("grep -B9999 '# Component List' README.md")
|
|
||||||
print ""
|
|
||||||
}
|
|
||||||
|
|
||||||
/\$(?![\(|"])/ { cmd=$0;
|
|
||||||
gsub(/^.*\$\(/,"",cmd);
|
|
||||||
gsub(/\).*/,"",cmd);
|
|
||||||
cmd | getline stdout; close(cmd);
|
|
||||||
sub(/\$\(.*\)/,stdout);
|
|
||||||
}
|
|
||||||
/\/\*\*/ { doc=1; sub(/^.*\/\*/,""); }
|
|
||||||
doc && /\*\// { doc=0;
|
|
||||||
sub(/[[:space:]]*\*\/.*/,"");
|
|
||||||
sub(/^[[:space:]]*\*[[:space:]]?/,"");
|
|
||||||
print
|
|
||||||
}
|
|
||||||
doc && /^[[:space:]]*\*/ { sub(/^[[:space:]]*\*[[:space:]]?/,"");
|
|
||||||
print
|
|
||||||
}
|
|
||||||
#!doc && /\/\/ / { sub(".*// ","");
|
|
||||||
# sub("# ","\n# ");
|
|
||||||
# sub("> ","\n> ");
|
|
||||||
# print
|
|
||||||
# }
|
|
||||||
245
README.md
245
README.md
|
|
@ -1,256 +1,21 @@
|
||||||
# XRshell apps & components
|
# XRshell apps & components
|
||||||
|
|
||||||
<img src='https://codeberg.org/xrsh/xrsh/media/branch/main/xrsh.svg' width="25%"/>
|
<img src='https://github.com/coderofsalvation/xrshell/raw/main/src/assets/logo.svg' width="25%"/>
|
||||||
|
|
||||||
This is a library of useful AFRAME components used in [XRSH](https://xrsh.isvery.ninja) [or in any AFRAME app].<br>
|
This is a library of useful AFRAME components which can be used in any AFRAME app, which are higher-level than usual (and used in [XRSH](https://coderofsalvation.github.io/xrsh):
|
||||||
Characteristics:
|
|
||||||
|
|
||||||
* selfcontained
|
|
||||||
* auto-loading of dependencies (via [AFRAME.utils.require()](com/require.js) see this [example](com/example/helloworld.js))
|
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script src="https://codeberg.org/xrsh/xrsh-com/com/require.js"/>
|
<script src="https://coderofsalvation.github.io/xrsh/src/com/example/helloworld.js"/>
|
||||||
<script src="https://codeberg.org/xrsh/xrsh-com/com/example/helloworld.js"/>
|
|
||||||
|
|
||||||
<a-entity helloworld="foo:1" class="cubes" name="box">
|
<a-entity helloworld="foo:1" class="cubes" name="box">
|
||||||
```
|
```
|
||||||
|
|
||||||
See component list below
|
## Funding
|
||||||
|
|
||||||
> this README.md is generated by running `echo "$(./README.awk com/*.js)" > README.md`
|
This project is funded through [NGI0 Entrust](https://nlnet.nl/entrust), a fund established by [NLnet](https://nlnet.nl) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu) program. Learn more at the [NLnet project page](https://nlnet.nl/project/xrsh).
|
||||||
|
|
||||||
## Credits
|
|
||||||
|
|
||||||
This project is partially funded through [NGI0 Entrust](https://nlnet.nl/entrust), a fund established by [NLnet](https://nlnet.nl) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu) program. Learn more at the [NLnet project page](https://nlnet.nl/project/xrsh).
|
|
||||||
|
|
||||||
[<img src="https://nlnet.nl/logo/banner.png" alt="NLnet foundation logo" width="20%" />](https://nlnet.nl)
|
[<img src="https://nlnet.nl/logo/banner.png" alt="NLnet foundation logo" width="20%" />](https://nlnet.nl)
|
||||||
[<img src="https://nlnet.nl/image/logos/NGI0_tag.svg" alt="NGI Zero Logo" width="20%" />](https://nlnet.nl/entrust)
|
[<img src="https://nlnet.nl/image/logos/NGI0_tag.svg" alt="NGI Zero Logo" width="20%" />](https://nlnet.nl/entrust)
|
||||||
|
|
||||||
# Component List
|
|
||||||
|
|
||||||
|
|
||||||
## [data_events](com/data_events.js)
|
|
||||||
|
|
||||||
allows components to react to data changes
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script>
|
|
||||||
AFRAME.registerComponent('mycom',{
|
|
||||||
init: function(){ this.data.foo = 1 },
|
|
||||||
event: {
|
|
||||||
foo: (e) => alert("I was updated!")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<a-entity mycom data_events/>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [html-as-texture-in-xr](com/html-as-texture-in-xr.js)
|
|
||||||
|
|
||||||
shows domid **only** in immersive mode
|
|
||||||
(wrapper around [aframe-htmlmesh](https://ada.is/aframe-htmlmesh/)
|
|
||||||
|
|
||||||
It also sets class 'XR' to the (HTML) body-element in immersive mode.
|
|
||||||
This allows CSS (in [dom component](com/dom.js)) to visually update accordingly.
|
|
||||||
|
|
||||||
> depends on [AFRAME.utils.require](com/require.js)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<style type="text/css">
|
|
||||||
.XR #foo { color:red; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<a-entity html-as-texture-in-xr="domid: #foo">
|
|
||||||
<b id="foo">hello</b>
|
|
||||||
</a-entitiy>
|
|
||||||
```
|
|
||||||
|
|
||||||
| property | type |
|
|
||||||
|--------------|--------------------|
|
|
||||||
| `domid` | `string` |
|
|
||||||
|
|
||||||
| event | target | info |
|
|
||||||
|--------------|------------|--------------------------------------|
|
|
||||||
| `3D` | a-scene | fired when going into immersive mode |
|
|
||||||
| `2D` | a-scene | fired when leaving immersive mode |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [isoterminal](com/isoterminal.js)
|
|
||||||
|
|
||||||
Renders a windowed terminal in both (non)immersive mode.
|
|
||||||
It displays an interactive javascript console or boots into
|
|
||||||
a Linux ISO image (via WASM).
|
|
||||||
|
|
||||||
```html
|
|
||||||
<a-entity isoterminal="iso: xrsh.iso" position="0 1.6 -0.3"></a-entity>
|
|
||||||
```
|
|
||||||
|
|
||||||
> depends on [AFRAME.utils.require](com/require.js)
|
|
||||||
|
|
||||||
| property | type | default | info |
|
|
||||||
|-------------------|-----------|------------------------|------|
|
|
||||||
| `iso` | `string` | https`//forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" | |
|
|
||||||
| `overlayfs` | `string` | '' | zip URL/file to autoextract on top of filesystem |
|
|
||||||
| `width` | `number` | 800 ||
|
|
||||||
| `height` | `number` | 600 ||
|
|
||||||
| `depth` | `number` | 0.03 ||
|
|
||||||
| `lineHeight` | `number` | 18 ||
|
|
||||||
| `bootMenu` | `string` | "" | character to auto-enter in bootMenu |
|
|
||||||
| `bootMenuURL` | `string` | "" | character to auto-enter in bootMenu when URL has fragment (#foo.zip e.g.) |
|
|
||||||
| `padding` | `number` | 18 | |
|
|
||||||
| `maximized` | `boolean` | false | |
|
|
||||||
| `minimized` | `boolean` | false | |
|
|
||||||
| `muteUntilPrompt` | `boolean` | true | mute stdout until a prompt is detected in ISO |
|
|
||||||
| `HUD` | `boolean` | false | link to camera movement |
|
|
||||||
| `transparent` | `boolean` | false | heavy, needs good gpu |
|
|
||||||
| `memory` | `number` | 60 | VM memory (in MB) [NOTE` quest or smartphone webworker might crash > 40mb ] |
|
|
||||||
| `bufferLatency` | `number` | 1 | in ms` bufferlatency from webworker to term (batch-update every char to texture) |
|
|
||||||
| `debug` | `boolean` | false | |
|
|
||||||
| `emulator` | `string` | fbterm | terminal emulator |
|
|
||||||
|
|
||||||
> for more info see [xrsh.isvery.ninja](https://xrsh.isvery.ninja)
|
|
||||||
|
|
||||||
Component design:
|
|
||||||
```
|
|
||||||
css/html template
|
|
||||||
|
|
||||||
┌─────────┐ ┌────────────┐ exit-AR
|
|
||||||
┌───────►│ com/dom ┼──►│ com/window ├───────────────── exit-VR ◄─┐
|
|
||||||
│ └─────────┘ └───────────┬┘ │
|
|
||||||
│ │ │
|
|
||||||
┌──────────┴────────┐ │ ┌───────────┐ ┌─────────────────────────────┐
|
|
||||||
│ com/isoterminal ├────────────────────────────►│com/term.js│ │com/html-as-texture-in-XR.js │
|
|
||||||
└────────┬─┬────────┘ │ └──┬─────┬▲─┘ └─────────────────────────────┘
|
|
||||||
│ │ ┌────────┐ ┌──▼──────▼──────┐ ││ │
|
|
||||||
│ └───────►│ plane ├─────►text───┼►div#isoterminal│◄────────────────── enter-VR │
|
|
||||||
│ └────────┘ └────────────────┘ enter-AR ◄─┘
|
|
||||||
│ │
|
|
||||||
│ │
|
|
||||||
│ ISOTerminal.js
|
|
||||||
│ ┌───────────────────────────┐
|
|
||||||
│ │ com/isoterminal/worker.js ├
|
|
||||||
│ └──────────────┌────────────┤
|
|
||||||
│ │ │ v86.js │
|
|
||||||
│ │ │ feat/*.js │
|
|
||||||
│ │ │ libv86.js │
|
|
||||||
│ │ └────────────┘
|
|
||||||
│ │
|
|
||||||
└─────────────────────┘
|
|
||||||
|
|
||||||
NOTE: For convenience reasons, events are forwarded between com/isoterminal.js, worker.js and ISOTerminal
|
|
||||||
Instead of a melting pot of different functionnames, events are flowing through everything (ISOTerminal.emit())
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## [launcher](com/launcher.js)
|
|
||||||
|
|
||||||
displays app (icons) in 2D and 3D handmenu (enduser can launch desktop-like 'apps')
|
|
||||||
|
|
||||||
```html
|
|
||||||
<a-entity launcher>
|
|
||||||
<a-entity launch="component: helloworld; foo: bar"><a-entity>
|
|
||||||
</a-entity>
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
| property | type | example |
|
|
||||||
|--------------|--------------------|----------------------------------------------------------------------------------------|
|
|
||||||
| `attach` | `selector` | hand or object to attach menu to |
|
|
||||||
| `registries` | `array` of strings | `<a-entity launcher="registers: https://foo.com/index.json, ./index.json"/>` |
|
|
||||||
|
|
||||||
| event | target | info |
|
|
||||||
|--------------|-------------------------------------------------------------------------------------------------------------|
|
|
||||||
| `launcher` | an app | when pressing an app icon, `launcher` event will be send to the respective app |
|
|
||||||
|
|
||||||
There a multiple ways of letting the launcher know that an app can be launched:
|
|
||||||
|
|
||||||
1. any AFRAME component with an `launcher`-event + manifest is automatically added:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
AFRAME.registerComponent('foo',{
|
|
||||||
events:{
|
|
||||||
launcher: function(){ ...launch something... }
|
|
||||||
},
|
|
||||||
manifest:{ // HTML5 manifesto JSON object
|
|
||||||
// https://www.w3.org/TR/appmanifest/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. dynamically in javascript
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
window.launcher.register({
|
|
||||||
name:"foo",
|
|
||||||
icon: "https://.../optional_icon.png"
|
|
||||||
description: "lorem ipsum",
|
|
||||||
cb: () => alert("foo")
|
|
||||||
})
|
|
||||||
//window.launcher.unregister('foo')
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Restore the pose.
|
|
||||||
|
|
||||||
|
|
||||||
## [pastedrop](com/pastedrop.js)
|
|
||||||
|
|
||||||
detects user copy/paste and file dragdrop action
|
|
||||||
and clipboard functions
|
|
||||||
|
|
||||||
```html
|
|
||||||
<a-entity pastedrop/>
|
|
||||||
```
|
|
||||||
|
|
||||||
| event | target | info |
|
|
||||||
|--------------|--------|------------------------------------------|
|
|
||||||
| `pasteFile` | self | always translates input to a File object |
|
|
||||||
|
|
||||||
|
|
||||||
Restore the pose.
|
|
||||||
|
|
||||||
|
|
||||||
## [require](com/require('').js)
|
|
||||||
|
|
||||||
automatically requires dependencies or missing components
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
await AFRAME.utils.require( this.dependencies ) (*) autoload missing components
|
|
||||||
await AFRAME.utils.require( this.el.getAttributeNames() ) (*) autoload missing components
|
|
||||||
await AFRAME.utils.require({foo: "https://foo.com/aframe/components/foo.js"},this)
|
|
||||||
await AFRAME.utils.require(["./app/foo.js","foo.css"],this)
|
|
||||||
```
|
|
||||||
|
|
||||||
> (*) = prefixes baseURL AFRAME.utils.require.baseURL ('./com/' e.g.)
|
|
||||||
|
|
||||||
|
|
||||||
## [window](com/window.js)
|
|
||||||
|
|
||||||
wraps a draggable window around a dom id or [dom](com/dom.js) component.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<a-entity window="dom: #mydiv"/>
|
|
||||||
```
|
|
||||||
|
|
||||||
> depends on [AFRAME.utils.require](com/require.js)
|
|
||||||
|
|
||||||
| property | type | default | info |
|
|
||||||
|------------------|-----------|------------------------|------|
|
|
||||||
| `title` |`string` | "" | |
|
|
||||||
| `width` |`string` | | |
|
|
||||||
| `height` |`string` | 260px | |
|
|
||||||
| `uid` |`string` | | |
|
|
||||||
| `attach` |`selector` | | |
|
|
||||||
| `dom` |`selector` | | |
|
|
||||||
| `max` |`boolean` | false | |
|
|
||||||
| `min` |`boolean` | false | |
|
|
||||||
| `x` |`string` | "center" | |
|
|
||||||
| `y` |`string` | "center" | |
|
|
||||||
| `class` |`array` | [] | |
|
|
||||||
|
|
|
||||||
109
com/cast.js
109
com/cast.js
|
|
@ -4,8 +4,11 @@ AFRAME.registerComponent('cast', {
|
||||||
},
|
},
|
||||||
|
|
||||||
requires: {
|
requires: {
|
||||||
dom: "com/dom.js",
|
dom: "./com/dom.js", // interpret .dom object
|
||||||
window: "com/window.js",
|
xd: "./com/xd.js", // allow switching between 2D/3D
|
||||||
|
html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
|
||||||
|
winboxjs: "https://unpkg.com/winbox@0.2.82/dist/winbox.bundle.min.js", // deadsimple windows: https://nextapps-de.github.io/winbox
|
||||||
|
winboxcss: "https://unpkg.com/winbox@0.2.82/dist/css/winbox.min.css", //
|
||||||
},
|
},
|
||||||
|
|
||||||
dom: {
|
dom: {
|
||||||
|
|
@ -20,7 +23,7 @@ AFRAME.registerComponent('cast', {
|
||||||
|
|
||||||
init: function () { },
|
init: function () { },
|
||||||
|
|
||||||
etInstallables: function(){
|
getInstallables: function(){
|
||||||
const installed = document.querySelector('[launcher]').components.launcher.system.getLaunchables()
|
const installed = document.querySelector('[launcher]').components.launcher.system.getLaunchables()
|
||||||
return this.data.comps.map( (c) => {
|
return this.data.comps.map( (c) => {
|
||||||
return installed[c] ? null : c
|
return installed[c] ? null : c
|
||||||
|
|
@ -34,54 +37,77 @@ AFRAME.registerComponent('cast', {
|
||||||
if( this.el.sceneEl.renderer.xr.isPresenting ){
|
if( this.el.sceneEl.renderer.xr.isPresenting ){
|
||||||
this.el.sceneEl.exitVR() // *FIXME* we need a gui
|
this.el.sceneEl.exitVR() // *FIXME* we need a gui
|
||||||
}
|
}
|
||||||
this.el.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
|
const el = document.querySelector('body');
|
||||||
// instance this component
|
const cropTarget = await CropTarget.fromElement(el);
|
||||||
this.el.setAttribute("dom","")
|
const stream = await navigator.mediaDevices.getDisplayMedia();
|
||||||
},
|
const [track] = stream.getVideoTracks();
|
||||||
|
this.track = track
|
||||||
DOMready: function(){
|
this.stream = stream
|
||||||
this.setupCast();
|
this.createWindow()
|
||||||
this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.uid}; faceuser: true`)
|
}
|
||||||
},
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setupCast: async function(){
|
createWindow: async function(){
|
||||||
let s = await AFRAME.utils.require(this.requires)
|
let s = await AFRAME.utils.require(this.requires)
|
||||||
|
|
||||||
const video = this.el.dom.querySelector('video')
|
// instance this component
|
||||||
|
const instance = this.el.cloneNode(false)
|
||||||
|
this.el.sceneEl.appendChild( instance )
|
||||||
|
instance.setAttribute("dom", "")
|
||||||
|
instance.setAttribute("xd", "") // allows flipping between DOM/WebGL when toggling XD-button
|
||||||
|
instance.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
|
||||||
|
instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
|
||||||
|
instance.setAttribute("grabbable","")
|
||||||
|
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
|
||||||
|
instance.track = this.track
|
||||||
|
instance.stream = this.stream
|
||||||
|
|
||||||
video.addEventListener( "loadedmetadata", () => {
|
const setupWindow = () => {
|
||||||
|
instance.dom.style.display = 'none'
|
||||||
|
|
||||||
let width = Math.round(window.innerWidth*0.4)
|
const video = instance.dom.querySelector('video')
|
||||||
let factor = width / video.videoWidth
|
video.addEventListener( "loadedmetadata", function () {
|
||||||
let height = Math.round( video.videoHeight * factor)
|
let width = Math.round(window.innerWidth*0.4)
|
||||||
|
let factor = width / this.videoWidth
|
||||||
|
let height = Math.round(this.videoHeight * factor)
|
||||||
|
new WinBox("Casting Tab",{
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
x:"center",
|
||||||
|
y:"center",
|
||||||
|
id: instance.uid, // important hint for html-mesh
|
||||||
|
root: document.querySelector("#overlay"),
|
||||||
|
mount: instance.dom,
|
||||||
|
onclose: () => { instance.dom.style.display = 'none'; return false; },
|
||||||
|
oncreate: () => {
|
||||||
|
|
||||||
const createVideoTexture = () => {
|
// instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`)
|
||||||
const texture = new THREE.VideoTexture( video );
|
}
|
||||||
texture.colorSpace = THREE.SRGBColorSpace;
|
});
|
||||||
const geometry = new THREE.PlaneGeometry( 16, 9 );
|
instance.dom.style.display = '' // show
|
||||||
geometry.scale( 0.2, 0.2, 0.2 );
|
|
||||||
const material = new THREE.MeshBasicMaterial( { map: texture } );
|
|
||||||
const mesh = new THREE.Mesh( geometry, material );
|
|
||||||
mesh.lookAt( AFRAME.scenes[0].camera.position );
|
|
||||||
this.el.object3D.add(mesh)
|
|
||||||
}
|
|
||||||
|
|
||||||
createVideoTexture.apply(this)
|
// hint grabbable's obb-collider to track the window-object
|
||||||
//this.el.setAttribute("window", `title: casting tab; uid: ${this.el.uid}; attach: #overlay; dom: #${this.el.dom.id}; width:${width}; height: ${height}`)
|
instance.components['obb-collider'].data.trackedObject3D = 'components.html.el.object3D.children.0'
|
||||||
})
|
instance.components['obb-collider'].update()
|
||||||
|
})
|
||||||
|
video.srcObject = instance.stream
|
||||||
|
video.play()
|
||||||
|
|
||||||
const el = document.querySelector('body');
|
this.createVideoTexture.apply(instance)
|
||||||
const cropTarget = await CropTarget.fromElement(el);
|
}
|
||||||
const stream = await navigator.mediaDevices.getDisplayMedia();
|
setTimeout( () => setupWindow(), 10 ) // give new components time to init
|
||||||
const [track] = stream.getVideoTracks();
|
},
|
||||||
this.track = track
|
|
||||||
this.stream = stream
|
|
||||||
|
|
||||||
video.srcObject = this.stream
|
|
||||||
video.play()
|
|
||||||
|
|
||||||
|
createVideoTexture: function(){
|
||||||
|
console.dir(this)
|
||||||
|
const texture = new THREE.VideoTexture( video );
|
||||||
|
texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
const geometry = new THREE.PlaneGeometry( 16, 9 );
|
||||||
|
geometry.scale( 0.2, 0.2, 0.2 );
|
||||||
|
const material = new THREE.MeshBasicMaterial( { map: texture } );
|
||||||
|
const mesh = new THREE.Mesh( geometry, material );
|
||||||
|
mesh.lookAt( this.sceneEl.camera.position );
|
||||||
|
this.object3D.add(mesh)
|
||||||
},
|
},
|
||||||
|
|
||||||
manifest: { // HTML5 manifest to identify app to xrsh
|
manifest: { // HTML5 manifest to identify app to xrsh
|
||||||
|
|
@ -90,7 +116,6 @@ AFRAME.registerComponent('cast', {
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "https://css.gg/cast.svg",
|
"src": "https://css.gg/cast.svg",
|
||||||
"src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNMjAgNkg0VjhIMlY2QzIgNC44OTU0MyAyLjg5NTQzIDQgNCA0SDIwQzIxLjEwNDYgNCAyMiA0Ljg5NTQzIDIyIDZWMThDMjIgMTkuMTA0NiAyMS4xMDQ2IDIwIDIwIDIwSDE1VjE4SDIwVjZaIgogICAgZmlsbD0iY3VycmVudENvbG9yIgogIC8+CiAgPHBhdGgKICAgIGQ9Ik0yIDEzQzUuODY1OTkgMTMgOSAxNi4xMzQgOSAyMEg3QzcgMTcuMjM4NiA0Ljc2MTQyIDE1IDIgMTVWMTNaIgogICAgZmlsbD0iY3VycmVudENvbG9yIgogIC8+CiAgPHBhdGggZD0iTTIgMTdDMy42NTY4NSAxNyA1IDE4LjM0MzEgNSAyMEgyVjE3WiIgZmlsbD0iY3VycmVudENvbG9yIiAvPgogIDxwYXRoCiAgICBkPSJNMiA5QzguMDc1MTMgOSAxMyAxMy45MjQ5IDEzIDIwSDExQzExIDE1LjAyOTQgNi45NzA1NiAxMSAyIDExVjlaIgogICAgZmlsbD0iY3VycmVudENvbG9yIgogIC8+Cjwvc3ZnPg==",
|
|
||||||
"type": "image/svg+xml",
|
"type": "image/svg+xml",
|
||||||
"sizes": "512x512"
|
"sizes": "512x512"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
159
com/codemirror.js
Normal file
159
com/codemirror.js
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
if( AFRAME.components.codemirror ) delete AFRAME.components.codemirror
|
||||||
|
|
||||||
|
AFRAME.registerComponent('codemirror', {
|
||||||
|
schema: {
|
||||||
|
file: { type:"string"},
|
||||||
|
term: { type:"selector", default: "[isoterminal]" },
|
||||||
|
},
|
||||||
|
|
||||||
|
init: function () {
|
||||||
|
this.el.object3D.visible = false
|
||||||
|
if( !this.data.term || !this.data.term.components ) throw 'codemirror cannot get isoterminal'
|
||||||
|
if( this.data.file && this.data.file[0] != '/'){
|
||||||
|
this.data.file = "root/"+this.data.file
|
||||||
|
}
|
||||||
|
this.isoterminal = this.data.term.components.isoterminal.isoterminal
|
||||||
|
//this.el.innerHTML = ` `
|
||||||
|
this.requireAll()
|
||||||
|
},
|
||||||
|
|
||||||
|
requireAll: async function(){
|
||||||
|
let s = await AFRAME.utils.require(this.requires)
|
||||||
|
setTimeout( () => this.el.setAttribute("dom",""), 300 )
|
||||||
|
},
|
||||||
|
|
||||||
|
requires:{
|
||||||
|
window: "com/window.js"
|
||||||
|
},
|
||||||
|
|
||||||
|
dom: {
|
||||||
|
scale: 0.5,
|
||||||
|
events: ['click','keydown'],
|
||||||
|
html: (me) => `<div class="codemirror">
|
||||||
|
</div>`,
|
||||||
|
|
||||||
|
css: (me) => `.codemirror{
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
.codemirror *{
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace;
|
||||||
|
font-weight:500 !important;
|
||||||
|
letter-spacing: 0 !important;
|
||||||
|
text-shadow: 0px 0px 10px #F075;
|
||||||
|
}
|
||||||
|
.wb-body + .codemirror{ overflow:hidden; }
|
||||||
|
.CodeMirror {
|
||||||
|
margin-top:18px;
|
||||||
|
}
|
||||||
|
.cm-s-shadowfox.CodeMirror {
|
||||||
|
background:transparent !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
createEditor: function(value){
|
||||||
|
this.el.setAttribute("window", `title: codemirror; uid: ${this.el.dom.id}; attach: #overlay; dom: #${this.el.dom.id};`)
|
||||||
|
this.editor = CodeMirror( this.el.dom, {
|
||||||
|
value,
|
||||||
|
mode: "htmlmixed",
|
||||||
|
lineNumbers: true,
|
||||||
|
styleActiveLine: true,
|
||||||
|
matchBrackets: true,
|
||||||
|
Tab: "indentMore",
|
||||||
|
defaultTab: function(cm) {
|
||||||
|
if (cm.somethingSelected()) cm.indentSelection("add");
|
||||||
|
else cm.replaceSelection(" ", "end");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.editor.setOption("theme", "shadowfox")
|
||||||
|
this.editor.updateFile = AFRAME.utils.throttleLeadingAndTrailing( (file,str) => {
|
||||||
|
this.updateFile(file,str),
|
||||||
|
2000
|
||||||
|
})
|
||||||
|
this.editor.on('change', (instance,changeObj) => {
|
||||||
|
this.editor.updateFile( this.data.file, instance.getValue() )
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout( () => {
|
||||||
|
this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`) // only show aframe-html in xr
|
||||||
|
},1500)
|
||||||
|
},
|
||||||
|
|
||||||
|
updateFile: async function(file,str){
|
||||||
|
// we don't do via shellcmd: isoterminal.exec(`echo '${str}' > ${file}`,1)
|
||||||
|
// as it would require all kindof ugly stringescaping
|
||||||
|
console.log("updating "+file)
|
||||||
|
await this.isoterminal.emulator.fs9p.update_file( file, str)
|
||||||
|
},
|
||||||
|
|
||||||
|
events:{
|
||||||
|
|
||||||
|
// component events
|
||||||
|
DOMready: function(e){
|
||||||
|
this.isoterminal.emulator.read_file( this.data.file )
|
||||||
|
.then( this.isoterminal.convert.Uint8ArrayToString )
|
||||||
|
.then( (str) => {
|
||||||
|
this.createEditor( str )
|
||||||
|
})
|
||||||
|
.catch( (e) => {
|
||||||
|
console.log("error opening "+this.data.file+", creating new one")
|
||||||
|
this.createEditor("")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
manifest: { // HTML5 manifest to identify app to xrsh
|
||||||
|
"short_name": "Paste",
|
||||||
|
"name": "Paste",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "https://css.gg/clipboard.svg",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "/?source=pwa",
|
||||||
|
"start_url": "/?source=pwa",
|
||||||
|
"background_color": "#3367D6",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": "/",
|
||||||
|
"theme_color": "#3367D6",
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "What is the latest news?",
|
||||||
|
"cli":{
|
||||||
|
"usage": "helloworld <type> [options]",
|
||||||
|
"example": "helloworld news",
|
||||||
|
"args":{
|
||||||
|
"--latest": {type:"string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"short_name": "Today",
|
||||||
|
"description": "View weather information for today",
|
||||||
|
"url": "/today?source=pwa",
|
||||||
|
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Paste the clipboard",
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "/images/screenshot1.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "540x720",
|
||||||
|
"form_factor": "narrow"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"help":`
|
||||||
|
Helloworld application
|
||||||
|
|
||||||
|
This is a help file which describes the application.
|
||||||
|
It will be rendered thru troika text, and will contain
|
||||||
|
headers based on non-punctualized lines separated by linebreaks,
|
||||||
|
in above's case "\nHelloworld application\n" will qualify as header.
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -17,9 +17,7 @@ window.AFRAME.registerComponent('pinch-to-teleport', {
|
||||||
pos.x += direction.x
|
pos.x += direction.x
|
||||||
pos.z += direction.z
|
pos.z += direction.z
|
||||||
// set the new position
|
// set the new position
|
||||||
if( !this.data.rig ){
|
this.data.rig.setAttribute("position", pos);
|
||||||
this.data.rig.setAttribute("position", pos);
|
|
||||||
}
|
|
||||||
// !!! NOTE - it would be more efficient to do the
|
// !!! NOTE - it would be more efficient to do the
|
||||||
// position change on the players THREE.Object:
|
// position change on the players THREE.Object:
|
||||||
// `player.object3D.position.add(direction)`
|
// `player.object3D.position.add(direction)`
|
||||||
72
com/control/pressable.js
Normal file
72
com/control/pressable.js
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
// this makes WebXR hand controls able to click things (by touching it)
|
||||||
|
|
||||||
|
AFRAME.registerComponent('pressable', {
|
||||||
|
schema: {
|
||||||
|
pressDistance: {
|
||||||
|
default: 0.01
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init: function() {
|
||||||
|
this.worldPosition = new THREE.Vector3();
|
||||||
|
this.fingerWorldPosition = new THREE.Vector3();
|
||||||
|
this.raycaster = new THREE.Raycaster()
|
||||||
|
this.handEls = document.querySelectorAll('[hand-tracking-controls]');
|
||||||
|
this.pressed = false;
|
||||||
|
this.distance = -1
|
||||||
|
// we throttle by distance, to support scenes with loads of clickable objects (far away)
|
||||||
|
this.tick = this.throttleByDistance( () => this.detectPress() )
|
||||||
|
},
|
||||||
|
throttleByDistance: function(f){
|
||||||
|
return function(){
|
||||||
|
if( this.distance < 0 ) return f() // first call
|
||||||
|
if( !f.tid ){
|
||||||
|
let x = this.distance
|
||||||
|
let y = x*(x*0.05)*1000 // parabolic curve
|
||||||
|
f.tid = setTimeout( function(){
|
||||||
|
f.tid = null
|
||||||
|
f()
|
||||||
|
}, y )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
detectPress: function(){
|
||||||
|
var handEls = this.handEls;
|
||||||
|
var handEl;
|
||||||
|
let minDistance = 5
|
||||||
|
|
||||||
|
// compensate for xrf-get AFRAME component (which references non-reparented buffergeometries from the 3D model)
|
||||||
|
let object3D = this.el.object3D.child || this.el.object3D
|
||||||
|
|
||||||
|
for (var i = 0; i < handEls.length; i++) {
|
||||||
|
handEl = handEls[i];
|
||||||
|
let indexTipPosition = handEl.components['hand-tracking-controls'].indexTipPosition
|
||||||
|
// Apply the relative position to the parent's world position
|
||||||
|
handEl.object3D.updateMatrixWorld();
|
||||||
|
handEl.object3D.getWorldPosition( this.fingerWorldPosition )
|
||||||
|
this.fingerWorldPosition.add( indexTipPosition )
|
||||||
|
|
||||||
|
this.raycaster.far = this.data.pressDistance
|
||||||
|
// Create a direction vector (doesnt matter because it is supershort for 'touch' purposes)
|
||||||
|
const direction = new THREE.Vector3(1.0,0,0);
|
||||||
|
this.raycaster.set(this.fingerWorldPosition, direction)
|
||||||
|
intersects = this.raycaster.intersectObjects([object3D],true)
|
||||||
|
|
||||||
|
object3D.getWorldPosition(this.worldPosition)
|
||||||
|
|
||||||
|
distance = this.fingerWorldPosition.distanceTo(this.worldPosition)
|
||||||
|
minDistance = distance < minDistance ? distance : minDistance
|
||||||
|
|
||||||
|
if (intersects.length ){
|
||||||
|
if( !this.pressed ){
|
||||||
|
this.el.emit('pressedstarted');
|
||||||
|
this.el.emit('click');
|
||||||
|
this.pressed = setTimeout( () => {
|
||||||
|
this.el.emit('pressedended');
|
||||||
|
this.pressed = null
|
||||||
|
},300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.distance = minDistance
|
||||||
|
}
|
||||||
|
});
|
||||||
26
com/control/wearable.js
Normal file
26
com/control/wearable.js
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
window.AFRAME.registerComponent('xrf-wear', {
|
||||||
|
schema:{
|
||||||
|
el: {type:"selector"},
|
||||||
|
position: {type:"vec3"},
|
||||||
|
rotation: {type:"vec3"}
|
||||||
|
},
|
||||||
|
init: function(){
|
||||||
|
$('a-scene').addEventListener('enter-vr', (e) => this.wear(e) )
|
||||||
|
$('a-scene').addEventListener('exit-vr', (e) => this.unwear(e) )
|
||||||
|
},
|
||||||
|
wear: function(){
|
||||||
|
if( !this.wearable ){
|
||||||
|
let d = this.data
|
||||||
|
this.wearable = new THREE.Group()
|
||||||
|
this.el.object3D.children.map( (c) => this.wearable.add(c) )
|
||||||
|
this.wearable.position.set( d.position.x, d.position.y, d.position.z)
|
||||||
|
this.wearable.rotation.set( d.rotation.x, d.rotation.y, d.rotation.z)
|
||||||
|
}
|
||||||
|
this.data.el.object3D.add(this.wearable)
|
||||||
|
},
|
||||||
|
unwear: function(){
|
||||||
|
this.data.el.remove(this.wearable)
|
||||||
|
this.wearable.children.map( (c) => this.el.object3D.add(c) )
|
||||||
|
delete this.wearable
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
/*
|
|
||||||
*Usage:
|
|
||||||
* <a-entity controlattach="el: #leftHand; class: foo"></a-entity>
|
|
||||||
* <a-entity id="leftHand">
|
|
||||||
* <a-entity class="foo" position="0 0 0" rotation="0 0 0" scale="0 0 0"></a-entity>
|
|
||||||
* <a-entity>
|
|
||||||
*
|
|
||||||
* NOTE: you can hint different position/rotation-offsets by adding controller-specific entities to the target:
|
|
||||||
*
|
|
||||||
* <a-entity id="leftHand">
|
|
||||||
* <a-entity class="foo" position="0 0 0" rotation="0 0 0" scale="0 0 0"></a-entity>
|
|
||||||
* <a-entity class="foo hand-tracking-controls" position="0 0 0" rotation="0 0 0" scale="0 0 0"></a-entity>
|
|
||||||
* <a-entity>
|
|
||||||
*
|
|
||||||
* The controllernames are hinted by the 'controllermodelready' event:
|
|
||||||
*
|
|
||||||
* 'hand-tracking-controls',
|
|
||||||
* 'meta-touch-controls',
|
|
||||||
* 'valve-index-controls',
|
|
||||||
* 'logitech-mx-ink-controls',
|
|
||||||
* 'windows-motion-controls',
|
|
||||||
* 'hp-mixed-reality-controls',
|
|
||||||
* 'generic-tracked-controller-controls',
|
|
||||||
* 'pico-controls',
|
|
||||||
* (and more in the future)
|
|
||||||
*/
|
|
||||||
|
|
||||||
AFRAME.registerComponent('controlattach', {
|
|
||||||
schema:{
|
|
||||||
el: {type:"selector"},
|
|
||||||
class: {type:"string"}
|
|
||||||
},
|
|
||||||
init: function(){
|
|
||||||
if( !this.data.el ) return console.warn(`controlattach.js: cannot find ${this.data.el}`)
|
|
||||||
this.controllers = {}
|
|
||||||
this.remember()
|
|
||||||
this.data.el.addEventListener('controllermodelready', this.bindPlaceHolders.bind(this,["controllermodelready"]) )
|
|
||||||
this.data.el.addEventListener('controllerconnected', this.bindPlaceHolders.bind(this,["controllerconnected"] ) )
|
|
||||||
},
|
|
||||||
|
|
||||||
bindPlaceHolders: function(type,e){
|
|
||||||
let controllerName = e.detail.name
|
|
||||||
let selector = `.${this.data.class}.${controllerName}`
|
|
||||||
let placeholder = this.data.el.querySelector(selector)
|
|
||||||
if( !placeholder ){
|
|
||||||
placeholder = this.data.el.querySelector(`.${this.data.class}`)
|
|
||||||
console.warn(`controlattach.js: '${selector}' not found, fallback to default`)
|
|
||||||
}
|
|
||||||
if( !placeholder ) return console.warn("controlattach.js: could not find placeholder to attach to")
|
|
||||||
this.attachPlaceHolder(type,e,placeholder, controllerName)
|
|
||||||
},
|
|
||||||
|
|
||||||
attachPlaceHolder: function(type, el, placeholder, controllerName){
|
|
||||||
this.el.object3DMap = {} // unsync THREE <-> AFRAME entity
|
|
||||||
// these are handled by the placeholder entity
|
|
||||||
this.obj.position.set(0,0,0);
|
|
||||||
this.obj.rotation.set(0,0,0);
|
|
||||||
this.obj.scale.set(1,1,1);
|
|
||||||
|
|
||||||
if( controllerName != 'hand-tracking-controls' ){
|
|
||||||
// re-add for controller-models which don't re-add children ('meta-touch-controls' e.g.)
|
|
||||||
if( this.data.el.getObject3D("mesh") ){
|
|
||||||
this.data.el.getObject3D("mesh").add(placeholder.object3D)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
placeholder.object3D.add( this.obj )
|
|
||||||
},
|
|
||||||
|
|
||||||
detach: function(){
|
|
||||||
this.el.setObject3D( this.obj.uuid, this.obj )
|
|
||||||
this.el.object3D.position.copy( this.position )
|
|
||||||
this.el.object3D.rotation.copy( this.rotation )
|
|
||||||
this.el.object3D.scale.copy( this.scale )
|
|
||||||
},
|
|
||||||
|
|
||||||
remember: function(){
|
|
||||||
this.obj = this.el.object3D
|
|
||||||
this.position = this.el.object3D.position.clone()
|
|
||||||
this.rotation = this.el.object3D.rotation.clone()
|
|
||||||
this.scale = this.el.object3D.scale.clone()
|
|
||||||
this.parent = this.el.object3D.parent
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/*
|
||||||
* ## [data_events](com/data_events.js)
|
* ## data_events
|
||||||
*
|
*
|
||||||
* allows components to react to data changes
|
* allows components to react to data changes
|
||||||
*
|
*
|
||||||
|
|
|
||||||
25
com/dom.js
25
com/dom.js
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* ## [dom](com/dom.js)
|
* ## dom
|
||||||
*
|
*
|
||||||
* instances reactive DOM component from AFRAME component's `dom` metadata
|
* instances reactive DOM component from AFRAME component's `dom` metadata
|
||||||
*
|
*
|
||||||
|
|
@ -36,6 +36,10 @@ if( !AFRAME.components.dom ){
|
||||||
|
|
||||||
AFRAME.registerComponent('dom',{
|
AFRAME.registerComponent('dom',{
|
||||||
|
|
||||||
|
requires: {
|
||||||
|
"requestAnimationFrameXR": "com/requestAnimationFrameXR.js"
|
||||||
|
},
|
||||||
|
|
||||||
init: function(){
|
init: function(){
|
||||||
Object.values(this.el.components)
|
Object.values(this.el.components)
|
||||||
.map( (c) => {
|
.map( (c) => {
|
||||||
|
|
@ -50,11 +54,12 @@ if( !AFRAME.components.dom ){
|
||||||
|
|
||||||
this
|
this
|
||||||
.ensureOverlay()
|
.ensureOverlay()
|
||||||
|
.addCSS()
|
||||||
.createReactiveDOMElement()
|
.createReactiveDOMElement()
|
||||||
.assignUniqueID()
|
.assignUniqueID()
|
||||||
.addCSS()
|
|
||||||
.scaleDOMvsXR()
|
.scaleDOMvsXR()
|
||||||
.triggerKeyboardForInputs()
|
.triggerKeyboardForInputs()
|
||||||
|
.stubRequestAnimationFrame()
|
||||||
|
|
||||||
document.querySelector('#overlay').appendChild(this.el.dom)
|
document.querySelector('#overlay').appendChild(this.el.dom)
|
||||||
this.el.emit('DOMready',{el: this.el.dom})
|
this.el.emit('DOMready',{el: this.el.dom})
|
||||||
|
|
@ -66,11 +71,8 @@ if( !AFRAME.components.dom ){
|
||||||
if( !overlay ){
|
if( !overlay ){
|
||||||
overlay = document.createElement('div')
|
overlay = document.createElement('div')
|
||||||
overlay.id = "overlay"
|
overlay.id = "overlay"
|
||||||
overlay.setAttribute('style','position:fixed;top:0px;left:0px;right:0px;bottom:0px;pointer-events:none')
|
overlay.setAttribute('style','position:fixed;top:0px;left:0px;right:0px;bottom:0px')
|
||||||
document.body.appendChild(overlay)
|
document.body.appendChild(overlay)
|
||||||
document.head.innerHTML += `
|
|
||||||
<style type="text/css">#overlay * { pointer-events:all }</style>
|
|
||||||
`
|
|
||||||
// sceneEl.setAttribute("webxr","overlayElement:#overlay")
|
// sceneEl.setAttribute("webxr","overlayElement:#overlay")
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
|
|
@ -94,12 +96,8 @@ if( !AFRAME.components.dom ){
|
||||||
this.el.dom.innerHTML = this.dom.html(this)
|
this.el.dom.innerHTML = this.dom.html(this)
|
||||||
this.el.dom.className = this.dom.attrName
|
this.el.dom.className = this.dom.attrName
|
||||||
this.com.data = this.reactify( this.el, this.com.data )
|
this.com.data = this.reactify( this.el, this.com.data )
|
||||||
|
if( this.dom.events ) this.dom.events.map( (e) => this.el.dom.addEventListener(e, (ev) => this.el.emit(e,ev) ) )
|
||||||
this.el.dom = this.el.dom.children[0]
|
this.el.dom = this.el.dom.children[0]
|
||||||
if( this.dom.events ){
|
|
||||||
this.dom.events.map( (e) => {
|
|
||||||
this.el.dom.addEventListener(e, (ev) => this.el.emit(e,ev) )
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -133,5 +131,10 @@ if( !AFRAME.components.dom ){
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
stubRequestAnimationFrame: async function(){
|
||||||
|
let s = await AFRAME.utils.require(this.requires)
|
||||||
|
this.el.setAttribute("requestAnimationFrameXR","")
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
107
com/example/helloworld-html.js
Normal file
107
com/example/helloworld-html.js
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
AFRAME.registerComponent('helloworld-html', {
|
||||||
|
schema: {
|
||||||
|
foo: { type:"string"}
|
||||||
|
},
|
||||||
|
|
||||||
|
init: function () {
|
||||||
|
|
||||||
|
this.el.addEventListener('ready', () => this.el.dom.style.display = 'none' )
|
||||||
|
},
|
||||||
|
|
||||||
|
requires:{
|
||||||
|
html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
|
||||||
|
},
|
||||||
|
|
||||||
|
dom: {
|
||||||
|
scale: 1,
|
||||||
|
events: ['click'],
|
||||||
|
html: (me) => `<div>
|
||||||
|
<div class="pad"> helloworld-html: ${me.data.foo} <b>${me.data.myvalue}</b></span>
|
||||||
|
</div>`,
|
||||||
|
|
||||||
|
css: (me) => `.helloworld-html {
|
||||||
|
color: var(--xrsh-light-gray); /* see index.css */
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
events:{
|
||||||
|
|
||||||
|
// combined AFRAME+DOM reactive events
|
||||||
|
keydown: function(e){ }, //
|
||||||
|
click: function(e){
|
||||||
|
console.dir(e)
|
||||||
|
}, //
|
||||||
|
|
||||||
|
// reactive events for this.data updates
|
||||||
|
myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
|
||||||
|
|
||||||
|
|
||||||
|
ready: function( ){
|
||||||
|
this.el.dom.style.display = 'none'
|
||||||
|
console.log("this.el.dom has been added to DOM")
|
||||||
|
this.el.dom.children[0].id = this.el.uid // important hint for html-mesh
|
||||||
|
this.data.myvalue = 1
|
||||||
|
setInterval( () => this.data.myvalue++, 100 )
|
||||||
|
},
|
||||||
|
|
||||||
|
launcher: function(){
|
||||||
|
this.el.dom.style.display = ''
|
||||||
|
console.log("launcher")
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
manifest: { // HTML5 manifest to identify app to xrsh
|
||||||
|
"short_name": "Hello world",
|
||||||
|
"name": "Hello world",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "https://css.gg/browser.svg",
|
||||||
|
"src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNNCA4QzQuNTUyMjggOCA1IDcuNTUyMjggNSA3QzUgNi40NDc3MiA0LjU1MjI4IDYgNCA2QzMuNDQ3NzIgNiAzIDYuNDQ3NzIgMyA3QzMgNy41NTIyOCAzLjQ0NzcyIDggNCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNOCA3QzggNy41NTIyOCA3LjU1MjI4IDggNyA4QzYuNDQ3NzIgOCA2IDcuNTUyMjggNiA3QzYgNi40NDc3MiA2LjQ0NzcyIDYgNyA2QzcuNTUyMjggNiA4IDYuNDQ3NzIgOCA3WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNMTAgOEMxMC41NTIzIDggMTEgNy41NTIyOCAxMSA3QzExIDYuNDQ3NzIgMTAuNTUyMyA2IDEwIDZDOS40NDc3MSA2IDkgNi40NDc3MiA5IDdDOSA3LjU1MjI4IDkuNDQ3NzEgOCAxMCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMyAzQzEuMzQzMTUgMyAwIDQuMzQzMTUgMCA2VjE4QzAgMTkuNjU2OSAxLjM0MzE1IDIxIDMgMjFIMjFDMjIuNjU2OSAyMSAyNCAxOS42NTY5IDI0IDE4VjZDMjQgNC4zNDMxNSAyMi42NTY5IDMgMjEgM0gzWk0yMSA1SDNDMi40NDc3MiA1IDIgNS40NDc3MiAyIDZWOUgyMlY2QzIyIDUuNDQ3NzIgMjEuNTUyMyA1IDIxIDVaTTIgMThWMTFIMjJWMThDMjIgMTguNTUyMyAyMS41NTIzIDE5IDIxIDE5SDNDMi40NDc3MiAxOSAyIDE4LjU1MjMgMiAxOFoiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "/?source=pwa",
|
||||||
|
"start_url": "/?source=pwa",
|
||||||
|
"background_color": "#3367D6",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": "/",
|
||||||
|
"theme_color": "#3367D6",
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "What is the latest news?",
|
||||||
|
"cli":{
|
||||||
|
"usage": "helloworld <type> [options]",
|
||||||
|
"example": "helloworld news",
|
||||||
|
"args":{
|
||||||
|
"--latest": {type:"string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"short_name": "Today",
|
||||||
|
"description": "View weather information for today",
|
||||||
|
"url": "/today?source=pwa",
|
||||||
|
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Hello world information",
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "/images/screenshot1.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "540x720",
|
||||||
|
"form_factor": "narrow"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"help":`
|
||||||
|
Helloworld application
|
||||||
|
|
||||||
|
This is a help file which describes the application.
|
||||||
|
It will be rendered thru troika text, and will contain
|
||||||
|
headers based on non-punctualized lines separated by linebreaks,
|
||||||
|
in above's case "\nHelloworld application\n" will qualify as header.
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -1,113 +1,104 @@
|
||||||
AFRAME.registerComponent('helloworld-window', {
|
AFRAME.registerComponent('helloworld-htmlform', {
|
||||||
schema: {
|
schema: {
|
||||||
foo: { type:"string"}
|
foo: { type:"string"}
|
||||||
},
|
},
|
||||||
|
|
||||||
init: function () {
|
init: function () {},
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
requires:{
|
requires:{
|
||||||
window: "com/window.js",
|
html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
|
||||||
reactive: "com/data2event.js"
|
winboxjs: "https://unpkg.com/winbox@0.2.82/dist/winbox.bundle.min.js", // deadsimple windows: https://nextapps-de.github.io/winbox
|
||||||
|
winboxcss: "https://unpkg.com/winbox@0.2.82/dist/css/winbox.min.css", // deadsimple windows: https://nextapps-de.github.io/winbox
|
||||||
},
|
},
|
||||||
|
|
||||||
dom: {
|
dom: {
|
||||||
scale: 0.66,
|
scale: 1,
|
||||||
events: ['click','input'],
|
events: ['click','input'],
|
||||||
html: (me) => `<div class="htmlform">
|
html: (me) => `<div class="htmlform">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Welcome to XR Shell</legend>
|
<legend>Colour</legend>
|
||||||
A free offline-first morphable<br>
|
<input type="radio" id="color-red" name="color" value="red" checked><label for="color-red"> Red</label><br>
|
||||||
environment which provides <br>
|
<input type="radio" id="color-blue" name="color" value="blue"><label for="color-blue"> Blue</label><br>
|
||||||
<span id="myvalue"></span> XR-friendly shells.<br>
|
|
||||||
<ol>
|
|
||||||
<li>check the <a href="/" target="_blank">website</a></li>
|
|
||||||
<li>check the <a href="https://forgejo.isvery.ninja/xrsh/xrsh-buildroot/src/branch/main/buildroot-v86/board/v86/rootfs_overlay/root/manual.md" target="_blank">manual</a></li>
|
|
||||||
</ol>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<br>
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Icons</legend>
|
<legend>Material:</legend>
|
||||||
<input type="radio" id="small" name="icons" value="0.8" checked style=""><label for="small" style="margin-right:15px;">Small</label>
|
<input id="material-wireframe" type="checkbox" name="wireframe"><label for="material-wireframe"> Wireframe</label><br>
|
||||||
<input type="radio" id="big" name="icons" value="1.5"><label for="big">Big</label><br>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<!--
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Size</legend>
|
<legend>Size</legend>
|
||||||
<input type="range" min="0.1" max="2" value="1" step="0.01" id="myRange" style="background-color: transparent;">
|
<input type="range" min="0.1" max="2" value="1" step="0.01" id="myRange" style="background-color: transparent;">
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<button>hello <span id="myvalue"></span></button>
|
<button>hello <span id="myvalue"></span></button>
|
||||||
-->
|
|
||||||
</div>`,
|
</div>`,
|
||||||
|
|
||||||
css: (me) => `.htmlform { padding:11px; }
|
css: (me) => `.htmlform { padding:11px; }`
|
||||||
`
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
events:{
|
events:{
|
||||||
|
|
||||||
// component events
|
// component events
|
||||||
window: function( ){ console.log("window component mounted") },
|
html: function( ){ console.log("html-mesh requirement mounted") },
|
||||||
|
|
||||||
// combined AFRAME+DOM reactive events
|
// combined AFRAME+DOM reactive events
|
||||||
click: function(e){ console.dir(e) }, //
|
click: function(e){ }, //
|
||||||
input: function(e){
|
input: function(e){
|
||||||
if( !e.detail.target ) return
|
if( !e.detail.target ) return
|
||||||
if( e.detail.target.id == 'myRange' ) this.data.myvalue = e.detail.target.value // reactive demonstration
|
if( e.detail.target.id == 'myRange' ) this.data.myvalue = e.detail.target.value // reactive demonstration
|
||||||
if( e.detail.target.name == 'cmenu' ) document.querySelector(".iconmenu").style.display = e.detail.target.value == 'on' ? '' : 'none';
|
|
||||||
if( e.detail.target.name == 'icons' ){
|
|
||||||
document.querySelector('[launcher]').object3D.getObjectByProperty("HTMLMesh").scale.setScalar( e.detail.target.value )
|
|
||||||
document.querySelector('.iconmenu').style.transform = e.detail.target.value == '0.8' ? 'scale(1)' : 'scale(1.33)'
|
|
||||||
}
|
|
||||||
console.dir(e.detail)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// reactive events for this.data updates
|
// reactive events for this.data updates
|
||||||
myvalue: function(e){ this.el.dom.querySelector('#myvalue').innerText = this.data.myvalue },
|
myvalue: function(e){ this.el.dom.querySelector('#myvalue').innerText = this.data.myvalue },
|
||||||
|
|
||||||
launcher: async function(){
|
launcher: async function(){
|
||||||
if( !this.el.getAttribute("dom") ){
|
let s = await AFRAME.utils.require(this.requires)
|
||||||
let s = await AFRAME.utils.require(this.requires)
|
|
||||||
|
|
||||||
// instance this component
|
// instance this component
|
||||||
this.el.setAttribute("dom", "")
|
const instance = this.el.cloneNode(false)
|
||||||
this.el.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
|
this.el.sceneEl.appendChild( instance )
|
||||||
}else{
|
instance.setAttribute("dom", "")
|
||||||
// toggle visibility
|
instance.setAttribute("show-texture-in-xr", "") // only show aframe-html in xr
|
||||||
this.el.winbox[ this.el.winbox.min ? 'restore' : 'minimize' ]()
|
instance.setAttribute("grabbable","")
|
||||||
|
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
|
||||||
|
|
||||||
|
const setupWindow = () => {
|
||||||
|
const com = instance.components['helloworld-htmlform']
|
||||||
|
instance.dom.style.display = 'none'
|
||||||
|
new WinBox("Hello World",{
|
||||||
|
width: 250,
|
||||||
|
height: 340,
|
||||||
|
x:"center",
|
||||||
|
y:"center",
|
||||||
|
id: instance.uid, // important hint for html-mesh
|
||||||
|
root: document.querySelector("#overlay"),
|
||||||
|
mount: instance.dom,
|
||||||
|
onclose: () => { instance.dom.style.display = 'none'; return false; },
|
||||||
|
oncreate: () => {
|
||||||
|
instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
|
||||||
|
instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`)
|
||||||
|
instance.setAttribute("show-texture-in-xr","") // only show aframe-html texture in xr mode
|
||||||
|
}
|
||||||
|
});
|
||||||
|
instance.dom.style.display = AFRAME.utils.XD() == '3D' ? 'none' : '' // show/hide
|
||||||
|
|
||||||
|
// hint grabbable's obb-collider to track the window-object
|
||||||
|
instance.components['obb-collider'].data.trackedObject3D = 'components.html.el.object3D.children.0'
|
||||||
|
instance.components['obb-collider'].update()
|
||||||
|
|
||||||
|
// data2event demo
|
||||||
|
instance.setAttribute("data2event","")
|
||||||
|
com.data.myvalue = 1
|
||||||
|
com.data.foo = `instance ${instance.uid}: `
|
||||||
|
setInterval( () => com.data.myvalue++, 500 )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimeout( () => setupWindow(), 10 ) // give new components time to init
|
||||||
},
|
},
|
||||||
|
|
||||||
DOMready: function(){
|
ready: function( ){
|
||||||
this.el.setAttribute("window", `title: XRSH; uid: ${this.el.uid}; attach: #overlay; dom: #${this.el.dom.id}; class: no-min, no-max; width:250; height: 360`)
|
this.el.dom.style.display = 'none'
|
||||||
|
console.log("this.el.dom has been added to DOM")
|
||||||
// data2event demo
|
this.data.myvalue = 1
|
||||||
this.el.setAttribute("data2event","")
|
|
||||||
this.data.myvalue = 1001
|
|
||||||
this.data.foo = `this.el ${this.el.uid}: `
|
|
||||||
setInterval( () => this.data.myvalue++, 500 )
|
|
||||||
|
|
||||||
// if you want your launch-icon to stick around after
|
|
||||||
// closing the window, then register it manually
|
|
||||||
window.launcher.register({
|
|
||||||
component: "helloworld-window",
|
|
||||||
...this.manifest,
|
|
||||||
icon: this.manifest.icons[0].src,
|
|
||||||
cb: () => {
|
|
||||||
const el = document.createElement("a-entity")
|
|
||||||
el.setAttribute("helloworld-window","")
|
|
||||||
el.setAttribute("pressable","")
|
|
||||||
el.setAttribute("position","0 1.6 0")
|
|
||||||
AFRAME.scenes[0].appendChild(el)
|
|
||||||
setTimeout( () => el.emit('launcher',null,false), 100 )
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
"window.oncreate": function(){
|
|
||||||
this.el.setAttribute("html-as-texture-in-xr", `domid: .winbox#${this.el.uid}; faceuser: true`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
156
com/example/helloworld-iframe.js
Normal file
156
com/example/helloworld-iframe.js
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
AFRAME.registerComponent('helloworld-iframe', {
|
||||||
|
schema: {
|
||||||
|
url: { type:"string"}
|
||||||
|
},
|
||||||
|
|
||||||
|
init: function(){},
|
||||||
|
|
||||||
|
requires:{
|
||||||
|
html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
|
||||||
|
winboxjs: "https://unpkg.com/winbox@0.2.82/dist/winbox.bundle.min.js", // deadsimple windows: https://nextapps-de.github.io/winbox
|
||||||
|
winboxcss: "https://unpkg.com/winbox@0.2.82/dist/css/winbox.min.css", //
|
||||||
|
},
|
||||||
|
|
||||||
|
dom: {
|
||||||
|
scale: 3,
|
||||||
|
events: ['click','keydown'],
|
||||||
|
html: (me) => `<div class="relative">
|
||||||
|
<span id="warning">
|
||||||
|
<b>Unfortunately,</b><br><br>
|
||||||
|
The browser does not allow IFRAME rendering<br>
|
||||||
|
in immersive mode (for security reasons).<br><br>
|
||||||
|
</span>
|
||||||
|
<iframe src=""></iframe>
|
||||||
|
</div>`,
|
||||||
|
|
||||||
|
css: (me) => `
|
||||||
|
.XR #overlay .winbox.iframe{
|
||||||
|
visibility: visible;
|
||||||
|
position:relative;
|
||||||
|
} /* don't hide in XR mode */
|
||||||
|
.winbox.iframe > .wb-body { background:#FFF !important;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.winbox.iframe iframe { z-index:10; }
|
||||||
|
.winbox.iframe #warning { position:absolute; z-index:9; top:100px; left:20px; width:100%; height:50px; color:black; display:none; }
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
events:{
|
||||||
|
|
||||||
|
// combined AFRAME+DOM reactive events
|
||||||
|
click: function(e){ }, //
|
||||||
|
keydown: function(e){ }, //
|
||||||
|
|
||||||
|
// reactive updates (data2event.js)
|
||||||
|
url: function(e){
|
||||||
|
this.el.dom.querySelector('iframe').src = this.data.url
|
||||||
|
console.dir(this.el.dom.querySelector('iframe'))
|
||||||
|
},
|
||||||
|
|
||||||
|
launcher: async function(){
|
||||||
|
let URL = this.data.url || prompt('enter URL to display','https://fabien.benetou.fr/Wiki/Wiki')
|
||||||
|
if( !URL ) return
|
||||||
|
|
||||||
|
this.createWindow()
|
||||||
|
},
|
||||||
|
|
||||||
|
createWindow: function(){
|
||||||
|
let s = await AFRAME.utils.require(this.requires)
|
||||||
|
|
||||||
|
// instance this component
|
||||||
|
const instance = this.el.cloneNode(false)
|
||||||
|
this.el.sceneEl.appendChild( instance )
|
||||||
|
instance.setAttribute("dom", "")
|
||||||
|
instance.setAttribute("data2event","")
|
||||||
|
instance.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
|
||||||
|
instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(1.39) )
|
||||||
|
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
|
||||||
|
|
||||||
|
this.el.sceneEl.addEventListener('3D', () => {
|
||||||
|
instance.dom.querySelector('#warning').style.display = 'block' // show warning
|
||||||
|
})
|
||||||
|
|
||||||
|
const setupWindow = () => {
|
||||||
|
const com = instance.components['helloworld-iframe']
|
||||||
|
instance.dom.style.display = 'none'
|
||||||
|
new WinBox( this.manifest.short_name+" "+URL,{
|
||||||
|
width: Math.round(window.innerWidth*0.6),
|
||||||
|
height: Math.round(window.innerHeight*0.6),
|
||||||
|
class:["iframe"],
|
||||||
|
x:"center",
|
||||||
|
y:"center",
|
||||||
|
id: instance.uid, // important hint for html-mesh
|
||||||
|
root: document.querySelector("#overlay"),
|
||||||
|
mount: instance.dom,
|
||||||
|
onclose: () => { instance.dom.style.display = 'none'; return false; },
|
||||||
|
oncreate: () => {
|
||||||
|
com.data.url = URL
|
||||||
|
instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
|
||||||
|
instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`)
|
||||||
|
instance.setAttribute("show-texture-in-xr","") // only show aframe-html texture in xr mode
|
||||||
|
}
|
||||||
|
});
|
||||||
|
instance.dom.style.display = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout( () => setupWindow(), 10 ) // give new components time to init
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
manifest: { // HTML5 manifest to identify app to xrsh
|
||||||
|
"short_name": "Iframe",
|
||||||
|
"name": "Hello world IFRAME window",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "https://css.gg/browse.svg",
|
||||||
|
"src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNNCA4QzQuNTUyMjggOCA1IDcuNTUyMjggNSA3QzUgNi40NDc3MiA0LjU1MjI4IDYgNCA2QzMuNDQ3NzIgNiAzIDYuNDQ3NzIgMyA3QzMgNy41NTIyOCAzLjQ0NzcyIDggNCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNOCA3QzggNy41NTIyOCA3LjU1MjI4IDggNyA4QzYuNDQ3NzIgOCA2IDcuNTUyMjggNiA3QzYgNi40NDc3MiA2LjQ0NzcyIDYgNyA2QzcuNTUyMjggNiA4IDYuNDQ3NzIgOCA3WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNMTAgOEMxMC41NTIzIDggMTEgNy41NTIyOCAxMSA3QzExIDYuNDQ3NzIgMTAuNTUyMyA2IDEwIDZDOS40NDc3MSA2IDkgNi40NDc3MiA5IDdDOSA3LjU1MjI4IDkuNDQ3NzEgOCAxMCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMyAzQzEuMzQzMTUgMyAwIDQuMzQzMTUgMCA2VjE4QzAgMTkuNjU2OSAxLjM0MzE1IDIxIDMgMjFIMjFDMjIuNjU2OSAyMSAyNCAxOS42NTY5IDI0IDE4VjZDMjQgNC4zNDMxNSAyMi42NTY5IDMgMjEgM0gzWk0yMSA1SDNDMi40NDc3MiA1IDIgNS40NDc3MiAyIDZWOUgyMlY2QzIyIDUuNDQ3NzIgMjEuNTUyMyA1IDIxIDVaTTIgMThWMTFIMjJWMThDMjIgMTguNTUyMyAyMS41NTIzIDE5IDIxIDE5SDNDMi40NDc3MiAxOSAyIDE4LjU1MjMgMiAxOFoiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "/?source=pwa",
|
||||||
|
"start_url": "/?source=pwa",
|
||||||
|
"background_color": "#3367D6",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": "/",
|
||||||
|
"theme_color": "#3367D6",
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "What is the latest news?",
|
||||||
|
"cli":{
|
||||||
|
"usage": "helloworld <type> [options]",
|
||||||
|
"example": "helloworld news",
|
||||||
|
"args":{
|
||||||
|
"--latest": {type:"string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"short_name": "Today",
|
||||||
|
"description": "View weather information for today",
|
||||||
|
"url": "/today?source=pwa",
|
||||||
|
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Hello world information",
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "/images/screenshot1.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "540x720",
|
||||||
|
"form_factor": "narrow"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"help":`
|
||||||
|
Helloworld application
|
||||||
|
|
||||||
|
This is a help file which describes the application.
|
||||||
|
It will be rendered thru troika text, and will contain
|
||||||
|
headers based on non-punctualized lines separated by linebreaks,
|
||||||
|
in above's case "\nHelloworld application\n" will qualify as header.
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
135
com/example/helloworld-window.js
Normal file
135
com/example/helloworld-window.js
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
AFRAME.registerComponent('helloworld-window', {
|
||||||
|
schema: {
|
||||||
|
foo: { type:"string", "default":"foo"}
|
||||||
|
},
|
||||||
|
|
||||||
|
requires: {
|
||||||
|
dom: "./com/dom.js", // interpret .dom object
|
||||||
|
xrtexture: "./com/html-as-texture-in-xr.js", // allow switching between 3D/3D
|
||||||
|
html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
|
||||||
|
winboxjs: "https://unpkg.com/winbox@0.2.82/dist/winbox.bundle.min.js", // deadsimple windows: https://nextapps-de.github.io/winbox
|
||||||
|
winboxcss: "https://unpkg.com/winbox@0.2.82/dist/css/winbox.min.css", //
|
||||||
|
},
|
||||||
|
|
||||||
|
init: function(){ },
|
||||||
|
|
||||||
|
dom: {
|
||||||
|
scale: 0.8,
|
||||||
|
events: ['click','keydown'],
|
||||||
|
html: (me) => `<div>
|
||||||
|
<div class="pad"> <span>${me.data.foo}</span> <b>${me.data.myvalue}</b></span>
|
||||||
|
</div>`,
|
||||||
|
|
||||||
|
css: (me) => `.helloworld-window div.pad { padding:11px; }`
|
||||||
|
},
|
||||||
|
|
||||||
|
events:{
|
||||||
|
|
||||||
|
// combined AFRAME+DOM reactive events
|
||||||
|
click: function(e){ }, //
|
||||||
|
keydown: function(e){ }, //
|
||||||
|
|
||||||
|
// reactive events for this.data updates (data2event.js)
|
||||||
|
myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
|
||||||
|
foo: function(e){ this.el.dom.querySelector('span').innerText = this.data.foo },
|
||||||
|
|
||||||
|
launcher: async function(){
|
||||||
|
let s = await AFRAME.utils.require(this.requires)
|
||||||
|
|
||||||
|
// instance this component
|
||||||
|
const instance = this.el.cloneNode(false)
|
||||||
|
this.el.sceneEl.appendChild( instance )
|
||||||
|
instance.setAttribute("dom", "")
|
||||||
|
instance.setAttribute("grabbable","")
|
||||||
|
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
|
||||||
|
|
||||||
|
const setupWindow = () => {
|
||||||
|
const com = instance.components['helloworld-window']
|
||||||
|
instance.dom.style.display = 'none'
|
||||||
|
|
||||||
|
new WinBox("Hello World",{
|
||||||
|
width: 250,
|
||||||
|
height: 150,
|
||||||
|
x:"center",
|
||||||
|
y:"center",
|
||||||
|
id: instance.uid, // important hint for html-mesh
|
||||||
|
root: document.querySelector("#overlay"),
|
||||||
|
mount: instance.dom,
|
||||||
|
onclose: () => { instance.dom.style.display = 'none'; return false; },
|
||||||
|
oncreate: () => {
|
||||||
|
instance.setAttribute("show-texture-in-xr",`domid: #${instance.uid}`) // only show aframe-html texture in xr mode
|
||||||
|
}
|
||||||
|
});
|
||||||
|
instance.dom.style.display = '' // show
|
||||||
|
|
||||||
|
// hint grabbable's obb-collider to track the window-object
|
||||||
|
instance.components['obb-collider'].data.trackedObject3D = 'components.html.el.object3D.children.0'
|
||||||
|
instance.components['obb-collider'].update()
|
||||||
|
|
||||||
|
// data2event demo
|
||||||
|
instance.setAttribute("data2event","")
|
||||||
|
com.data.myvalue = 1
|
||||||
|
com.data.foo = `instance ${instance.uid}: `
|
||||||
|
setInterval( () => com.data.myvalue++, 500 )
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout( () => setupWindow(), 10 ) // give new components time to init
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
manifest: { // HTML5 manifest to identify app to xrsh
|
||||||
|
"short_name": "window",
|
||||||
|
"name": "Hello world window",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "https://css.gg/browser.svg",
|
||||||
|
"src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNNCA4QzQuNTUyMjggOCA1IDcuNTUyMjggNSA3QzUgNi40NDc3MiA0LjU1MjI4IDYgNCA2QzMuNDQ3NzIgNiAzIDYuNDQ3NzIgMyA3QzMgNy41NTIyOCAzLjQ0NzcyIDggNCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNOCA3QzggNy41NTIyOCA3LjU1MjI4IDggNyA4QzYuNDQ3NzIgOCA2IDcuNTUyMjggNiA3QzYgNi40NDc3MiA2LjQ0NzcyIDYgNyA2QzcuNTUyMjggNiA4IDYuNDQ3NzIgOCA3WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNMTAgOEMxMC41NTIzIDggMTEgNy41NTIyOCAxMSA3QzExIDYuNDQ3NzIgMTAuNTUyMyA2IDEwIDZDOS40NDc3MSA2IDkgNi40NDc3MiA5IDdDOSA3LjU1MjI4IDkuNDQ3NzEgOCAxMCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMyAzQzEuMzQzMTUgMyAwIDQuMzQzMTUgMCA2VjE4QzAgMTkuNjU2OSAxLjM0MzE1IDIxIDMgMjFIMjFDMjIuNjU2OSAyMSAyNCAxOS42NTY5IDI0IDE4VjZDMjQgNC4zNDMxNSAyMi42NTY5IDMgMjEgM0gzWk0yMSA1SDNDMi40NDc3MiA1IDIgNS40NDc3MiAyIDZWOUgyMlY2QzIyIDUuNDQ3NzIgMjEuNTUyMyA1IDIxIDVaTTIgMThWMTFIMjJWMThDMjIgMTguNTUyMyAyMS41NTIzIDE5IDIxIDE5SDNDMi40NDc3MiAxOSAyIDE4LjU1MjMgMiAxOFoiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "/?source=pwa",
|
||||||
|
"start_url": "/?source=pwa",
|
||||||
|
"background_color": "#3367D6",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": "/",
|
||||||
|
"theme_color": "#3367D6",
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "What is the latest news?",
|
||||||
|
"cli":{
|
||||||
|
"usage": "helloworld <type> [options]",
|
||||||
|
"example": "helloworld news",
|
||||||
|
"args":{
|
||||||
|
"--latest": {type:"string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"short_name": "Today",
|
||||||
|
"description": "View weather information for today",
|
||||||
|
"url": "/today?source=pwa",
|
||||||
|
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Hello world information",
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "/images/screenshot1.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "540x720",
|
||||||
|
"form_factor": "narrow"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"help":`
|
||||||
|
Helloworld application
|
||||||
|
|
||||||
|
This is a help file which describes the application.
|
||||||
|
It will be rendered thru troika text, and will contain
|
||||||
|
headers based on non-punctualized lines separated by linebreaks,
|
||||||
|
in above's case "\nHelloworld application\n" will qualify as header.
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
94
com/example/helloworld.js
Normal file
94
com/example/helloworld.js
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
AFRAME.registerComponent('helloworld', {
|
||||||
|
schema: {
|
||||||
|
wireframe: { type:"boolean", "default":false},
|
||||||
|
text: {type:"string","default":"hello world"}
|
||||||
|
},
|
||||||
|
|
||||||
|
dependencies: ['data2event'],
|
||||||
|
|
||||||
|
init: async function() {
|
||||||
|
this.el.object3D.visible = false
|
||||||
|
|
||||||
|
await AFRAME.utils.require(this.dependencies)
|
||||||
|
this.el.setAttribute("data2event","")
|
||||||
|
this.el.setAttribute("grabbable","")
|
||||||
|
|
||||||
|
this.el.innerHTML = `
|
||||||
|
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
|
||||||
|
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
|
||||||
|
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
|
||||||
|
<a-entity position="0 1.8 -3" scale="10 10 10" text="value: ${this.data.text}; align:center; color:#888"></a-entity>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
events:{
|
||||||
|
|
||||||
|
launcher: function(e){
|
||||||
|
this.el.object3D.visible = !this.el.object3D.visible
|
||||||
|
clearInterval(this.interval)
|
||||||
|
this.interval = setInterval( () => {
|
||||||
|
this.data.wireframe = !this.data.wireframe
|
||||||
|
}, 500 )
|
||||||
|
},
|
||||||
|
|
||||||
|
// reactive this.data value demo
|
||||||
|
wireframe:function( ){
|
||||||
|
this.el.object3D.traverse( (obj) => obj.material && (obj.material.wireframe = this.data.wireframe) )
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
manifest: { // HTML5 manifest to identify app to xrsh
|
||||||
|
"short_name": "Hello world",
|
||||||
|
"name": "Hello world",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "https://css.gg/shape-hexagon.svg",
|
||||||
|
"src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNNiAxNS4yMzQ4TDEyIDE4LjU2ODFMMTggMTUuMjM0OFY4Ljc2NTIxTDEyIDUuNDMxODhMNiA4Ljc2NTIxVjE1LjIzNDhaTTEyIDJMMyA3VjE3TDEyIDIyTDIxIDE3VjdMMTIgMloiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "/?source=pwa",
|
||||||
|
"start_url": "/?source=pwa",
|
||||||
|
"background_color": "#3367D6",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": "/",
|
||||||
|
"theme_color": "#3367D6",
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "What is the latest news?",
|
||||||
|
"cli":{
|
||||||
|
"usage": "helloworld <type> [options]",
|
||||||
|
"example": "helloworld news",
|
||||||
|
"args":{
|
||||||
|
"--latest": {type:"string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"short_name": "Today",
|
||||||
|
"description": "View weather information for today",
|
||||||
|
"url": "/today?source=pwa",
|
||||||
|
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Hello world information",
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "/images/screenshot1.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "540x720",
|
||||||
|
"form_factor": "narrow"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"help":`
|
||||||
|
Helloworld application
|
||||||
|
|
||||||
|
This is a help file which describes the application.
|
||||||
|
It will be rendered thru troika text, and will contain
|
||||||
|
headers based on non-punctualized lines separated by linebreaks,
|
||||||
|
in above's case "\nHelloworld application\n" will qualify as header.
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -1,95 +1,67 @@
|
||||||
/**
|
|
||||||
* ## [html-as-texture-in-xr](com/html-as-texture-in-xr.js)
|
|
||||||
*
|
|
||||||
* shows domid **only** in immersive mode
|
|
||||||
* (wrapper around [aframe-htmlmesh](https://ada.is/aframe-htmlmesh/)
|
|
||||||
*
|
|
||||||
* It also sets class 'XR' to the (HTML) body-element in immersive mode.
|
|
||||||
* This allows CSS (in [dom component](com/dom.js)) to visually update accordingly.
|
|
||||||
*
|
|
||||||
* > depends on [AFRAME.utils.require](com/require.js)
|
|
||||||
*
|
|
||||||
* ```html
|
|
||||||
* <style type="text/css">
|
|
||||||
* .XR #foo { color:red; }
|
|
||||||
* </style>
|
|
||||||
*
|
|
||||||
* <a-entity html-as-texture-in-xr="domid: #foo">
|
|
||||||
* <b id="foo">hello</b>
|
|
||||||
* </a-entitiy>
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* | property | type |
|
|
||||||
* |--------------|--------------------|
|
|
||||||
* | `domid` | `string` |
|
|
||||||
*
|
|
||||||
* | event | target | info |
|
|
||||||
* |--------------|------------|--------------------------------------|
|
|
||||||
* | `3D` | a-scene | fired when going into immersive mode |
|
|
||||||
* | `2D` | a-scene | fired when leaving immersive mode |
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
if( !AFRAME.components['html-as-texture-in-xr'] ){
|
if( !AFRAME.components['html-as-texture-in-xr'] ){
|
||||||
|
|
||||||
AFRAME.registerComponent('html-as-texture-in-xr', {
|
AFRAME.registerComponent('html-as-texture-in-xr', {
|
||||||
schema: {
|
schema: {
|
||||||
domid: { type:"string"},
|
domid: { type:"string"}
|
||||||
doublesided: {type: "boolean", default: true},
|
|
||||||
faceuser: { type: "boolean", default: false}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
dependencies:{
|
dependencies:{
|
||||||
html: "com/lib/aframe-html.js"
|
html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
|
||||||
|
//html: "https://coderofsalvation.github.io/aframe-htmlmesh/build/aframe-html.js"
|
||||||
|
//html: "com/aframe-html.js"
|
||||||
},
|
},
|
||||||
|
|
||||||
init: async function () {
|
init: async function () {
|
||||||
let el = document.querySelector(this.data.domid)
|
let el = document.querySelector(this.data.domid)
|
||||||
if( ! el ){
|
if( ! el ){
|
||||||
return console.error("html-as-texture-in-xr: cannot get dom element "+this.data.domid)
|
return console.error("html-as-texture-in-xr: cannot get dom element "+this.data.dom.id)
|
||||||
}
|
}
|
||||||
let s = await AFRAME.utils.require(this.dependencies)
|
let s = await AFRAME.utils.require(this.dependencies)
|
||||||
|
this.el.setAttribute("html",`html: ${this.data.domid}; cursor:#cursor; xrlayer: true`)
|
||||||
this.forwardClickToMesh();
|
|
||||||
|
|
||||||
this.el.sceneEl.addEventListener('enter-vr', () => this.enableDoubleSided() )
|
|
||||||
|
|
||||||
this.el.setAttribute("html",`html: ${this.data.domid}; cursor:#cursor; `)
|
|
||||||
this.el.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
|
this.el.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
|
||||||
if( this.data.faceuser ){
|
this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
|
||||||
this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.4) )
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
forwardClickToMesh: function(){
|
manifest: { // HTML5 manifest to identify app to xrsh
|
||||||
// monkeypatch: forward click to mesh
|
"short_name": "show-texture-in-xr",
|
||||||
const handle = AFRAME.components['html'].Component.prototype.handle
|
"name": "2D/3D switcher",
|
||||||
AFRAME.components['html'].Component.prototype.handle = function(type,evt){
|
"icons": [],
|
||||||
|
"id": "/?source=pwa",
|
||||||
if( !this.el.sceneEl.renderer.xr.isPresenting ) return // ignore events in desktop mode
|
"start_url": "/?source=pwa",
|
||||||
if( this.el.sceneEl.renderer.xr.isPresenting && type.match(/^mouse/) ) return // ignore mouse-events in XR
|
"background_color": "#3367D6",
|
||||||
|
"display": "standalone",
|
||||||
if( type == 'click' && evt.detail.length && evt.detail[0].uv ){
|
"scope": "/",
|
||||||
const mesh = this.el.object3D.children[0]
|
"theme_color": "#3367D6",
|
||||||
const uv = evt.detail[0].uv;
|
"category":"system",
|
||||||
const _pointer = new THREE.Vector2();
|
"shortcuts": [
|
||||||
const _event = { type: '', data: _pointer };
|
{
|
||||||
_event.type = type;
|
"name": "What is the latest news?",
|
||||||
_event.data.set( uv.x, 1 - uv.y );
|
"cli":{
|
||||||
mesh.dispatchEvent( _event );
|
"usage": "helloworld <type> [options]",
|
||||||
|
"example": "helloworld news",
|
||||||
|
"args":{
|
||||||
|
"--latest": {type:"string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"short_name": "Today",
|
||||||
|
"description": "View weather information for today",
|
||||||
|
"url": "/today?source=pwa",
|
||||||
|
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
||||||
}
|
}
|
||||||
return handle.apply(this,[type,evt])
|
],
|
||||||
}
|
"description": "use ESC-key to toggle between 2D / 3D",
|
||||||
|
"screenshots": [
|
||||||
},
|
{
|
||||||
|
"src": "/images/screenshot1.png",
|
||||||
enableDoubleSided: function(){
|
"type": "image/png",
|
||||||
// enable doubleside
|
"sizes": "540x720",
|
||||||
this.el.object3D.traverse( (o) => {
|
"form_factor": "narrow"
|
||||||
if( o.constructor && String(o.constructor).match(/HTMLMesh/) ){
|
|
||||||
o.material.side = THREE.DoubleSide
|
|
||||||
}
|
}
|
||||||
})
|
],
|
||||||
|
"help":`
|
||||||
|
Helloworld application
|
||||||
|
|
||||||
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,109 +1,51 @@
|
||||||
/**
|
function ISOTerminal(instance,opts){
|
||||||
* ## [isoterminal](com/isoterminal.js)
|
// create a neutral isoterminal object which can be decorated
|
||||||
*
|
// with prototype functions and has addListener() and dispatchEvent()
|
||||||
* Renders a windowed terminal in both (non)immersive mode.
|
let obj = new EventTarget()
|
||||||
* It displays an interactive javascript console or boots into
|
obj.instance = instance
|
||||||
* a Linux ISO image (via WASM).
|
obj.opts = opts
|
||||||
*
|
// register default event listeners (enable file based features like isoterminal/jsconsole.js e.g.)
|
||||||
* ```html
|
for( let event in ISOTerminal.listener )
|
||||||
* <a-entity isoterminal="iso: xrsh.iso" position="0 1.6 -0.3"></a-entity>
|
for( let cb in ISOTerminal.listener[event] )
|
||||||
* ```
|
obj.addEventListener( event, ISOTerminal.listener[event][cb] )
|
||||||
*
|
// compose object with functions
|
||||||
* > depends on [AFRAME.utils.require](com/require.js)
|
for( let i in ISOTerminal.prototype ) obj[i] = ISOTerminal.prototype[i]
|
||||||
*
|
obj.emit('init')
|
||||||
* | property | type | default | info |
|
return obj
|
||||||
* |-------------------|-----------|------------------------|------|
|
}
|
||||||
* | `title` | `string` | 'xrsh.iso' | window title |
|
|
||||||
* | `iso` | `string` | https`//forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" | |
|
ISOTerminal.prototype.emit = function(event,data){
|
||||||
* | `overlayfs` | `string` | '' | zip URL/file to autoextract on top of filesystem |
|
data = data || false
|
||||||
* | `width` | `number` | 800 ||
|
this.dispatchEvent( new CustomEvent(event, {detail: data} ) )
|
||||||
* | `height` | `number` | 600 ||
|
}
|
||||||
* | `depth` | `number` | 0.03 ||
|
|
||||||
* | `lineHeight` | `number` | 18 ||
|
ISOTerminal.addEventListener = (event,cb) => {
|
||||||
* | `bootMenu` | `string` | "" | character to auto-enter in bootMenu |
|
ISOTerminal.listener = ISOTerminal.listener || {}
|
||||||
* | `bootMenuURL` | `string` | "" | character to auto-enter in bootMenu when URL has fragment (#foo.zip e.g.) |
|
ISOTerminal.listener[event] = ISOTerminal.listener[event] || []
|
||||||
* | `padding` | `number` | 18 | |
|
ISOTerminal.listener[event].push(cb)
|
||||||
* | `maximized` | `boolean` | false | |
|
}
|
||||||
* | `minimized` | `boolean` | false | |
|
|
||||||
* | `muteUntilPrompt` | `boolean` | true | mute stdout until a prompt is detected in ISO |
|
// ISOTerminal has defacto support for AFRAME
|
||||||
* | `HUD` | `boolean` | false | link to camera movement |
|
// but can be decorated to work without it as well
|
||||||
* | `transparent` | `boolean` | false | heavy, needs good gpu |
|
|
||||||
* | `memory` | `number` | 60 | VM memory (in MB) [NOTE` quest or smartphone webworker might crash > 40mb ] |
|
|
||||||
* | `bufferLatency` | `number` | 1 | in ms` bufferlatency from webworker to term (batch-update every char to texture) |
|
|
||||||
* | `debug` | `boolean` | false | |
|
|
||||||
* | `emulator` | `string` | fbterm | terminal emulator |
|
|
||||||
*
|
|
||||||
* > for more info see [xrsh.isvery.ninja](https://xrsh.isvery.ninja)
|
|
||||||
*
|
|
||||||
* Component design:
|
|
||||||
* ```
|
|
||||||
* css/html template
|
|
||||||
*
|
|
||||||
* ┌─────────┐ ┌────────────┐ exit-AR
|
|
||||||
* ┌───────►│ com/dom ┼──►│ com/window ├───────────────── exit-VR ◄─┐
|
|
||||||
* │ └─────────┘ └───────────┬┘ │
|
|
||||||
* │ │ │
|
|
||||||
* ┌──────────┴────────┐ │ ┌───────────┐ ┌─────────────────────────────┐
|
|
||||||
* │ com/isoterminal ├────────────────────────────►│com/term.js│ │com/html-as-texture-in-XR.js │
|
|
||||||
* └────────┬─┬────────┘ │ └──┬─────┬▲─┘ └─────────────────────────────┘
|
|
||||||
* │ │ ┌────────┐ ┌──▼──────▼──────┐ ││ │
|
|
||||||
* │ └───────►│ plane ├─────►text───┼►div#isoterminal│◄────────────────── enter-VR │
|
|
||||||
* │ └────────┘ └────────────────┘ enter-AR ◄─┘
|
|
||||||
* │ │
|
|
||||||
* │ │
|
|
||||||
* │ ISOTerminal.js
|
|
||||||
* │ ┌───────────────────────────┐
|
|
||||||
* │ │ com/isoterminal/worker.js ├
|
|
||||||
* │ └──────────────┌────────────┤
|
|
||||||
* │ │ │ v86.js │
|
|
||||||
* │ │ │ feat/*.js │
|
|
||||||
* │ │ │ libv86.js │
|
|
||||||
* │ │ └────────────┘
|
|
||||||
* │ │
|
|
||||||
* └─────────────────────┘
|
|
||||||
*
|
|
||||||
* NOTE: For convenience reasons, events are forwarded between com/isoterminal.js, worker.js and ISOTerminal
|
|
||||||
* Instead of a melting pot of different functionnames, events are flowing through everything (ISOTerminal.emit())
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
|
|
||||||
if( typeof AFRAME != 'undefined '){
|
if( typeof AFRAME != 'undefined '){
|
||||||
|
|
||||||
AFRAME.registerComponent('isoterminal', {
|
AFRAME.registerComponent('isoterminal', {
|
||||||
schema: {
|
schema: {
|
||||||
iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" },
|
iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" },
|
||||||
overlayfs: { type:"string"},
|
overlayfs: { type:"string"},
|
||||||
title: { type:"string", "default":"xrsh.iso"},
|
cols: { type: 'number',"default": 120 },
|
||||||
width: { type: 'number',"default": 800 },
|
rows: { type: 'number',"default": 30 },
|
||||||
height: { type: 'number',"default": 600 },
|
padding: { type: 'number',"default": 18 },
|
||||||
depth: { type: 'number',"default": 0.03 },
|
minimized: { type: 'boolean',"default":false},
|
||||||
lineHeight: { type: 'number',"default": 18 },
|
maximized: { type: 'boolean',"default":true},
|
||||||
bootOnFragment: { type: 'string',"default": "nothing" },
|
transparent: { type:'boolean', "default":false }, // need good gpu
|
||||||
bootMenu: { type: 'string', "default": '' }, // show bootMenu if empty, or autosend key to bootMenu (key '1' e.g.)
|
xterm: { type: 'boolean', "default":true }, // use xterm.js? (=slower)
|
||||||
bootMenuURL: { type: 'string', "default": '' }, // show bootMenu if empty, or autosend key to bootMenu (key '1' e.g.)
|
memory: { type: 'number', "default":48 } // VM memory (in MB)
|
||||||
padding: { type: 'number',"default": 18 },
|
|
||||||
maximized: { type: 'boolean',"default":false},
|
|
||||||
minimized: { type: 'boolean',"default":false},
|
|
||||||
muteUntilPrompt:{ type: 'boolean',"default":true}, // mute stdout until a prompt is detected in ISO
|
|
||||||
HUD: { type: 'boolean',"default":false}, // link to camera movement
|
|
||||||
transparent: { type:'boolean', "default":false }, // need good gpu
|
|
||||||
memory: { type: 'number', "default":60 }, // VM memory (in MB) [NOTE: quest or smartphone might crash > 40mb ]
|
|
||||||
bufferLatency: { type: 'number', "default":1 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture)
|
|
||||||
debug: { type: 'boolean', "default":false },
|
|
||||||
emulator: { type: 'string', "default": "fbterm" }// terminal emulator
|
|
||||||
},
|
},
|
||||||
|
|
||||||
init: function(){
|
init: async function(){
|
||||||
this.el.object3D.visible = false
|
this.el.object3D.visible = false
|
||||||
if( window.innerWidth < this.data.width ){
|
|
||||||
this.data.maximized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
this.calculateDimension()
|
|
||||||
this.initHud()
|
|
||||||
this.setupPasteDrop()
|
|
||||||
this.setupEvents()
|
|
||||||
|
|
||||||
fetch(this.data.iso,{method: 'HEAD'})
|
fetch(this.data.iso,{method: 'HEAD'})
|
||||||
.then( (res) => {
|
.then( (res) => {
|
||||||
if( res.status != 200 ) throw 'not found'
|
if( res.status != 200 ) throw 'not found'
|
||||||
|
|
@ -117,156 +59,74 @@ if( typeof AFRAME != 'undefined '){
|
||||||
},
|
},
|
||||||
|
|
||||||
requires:{
|
requires:{
|
||||||
com: "com/dom.js",
|
com: "com/dom.js",
|
||||||
window: "com/window.js",
|
window: "com/window.js",
|
||||||
pastedrop: "com/pastedrop.js",
|
v86: "com/isoterminal/libv86.js",
|
||||||
v86: "com/isoterminal/libv86.js",
|
|
||||||
// allow xrsh to selfcontain scene + itself
|
// allow xrsh to selfcontain scene + itself
|
||||||
xhook: "com/lib/xhook.min.js",
|
xhook: "https://jpillora.com/xhook/dist/xhook.min.js",
|
||||||
//selfcontain: "com/selfcontainer.js",
|
selfcontain: "com/selfcontainer.js",
|
||||||
// html to texture
|
// html to texture
|
||||||
htmlinxr: "com/html-as-texture-in-xr.js",
|
htmlinxr: "com/html-as-texture-in-xr.js",
|
||||||
// isoterminal global features
|
// isoterminal features
|
||||||
PromiseWorker: "com/isoterminal/PromiseWorker.js",
|
core: "com/isoterminal/core.js",
|
||||||
ISOTerminal: "com/isoterminal/ISOTerminal.js",
|
utils_9p: "com/isoterminal/feat/9pfs_utils.js",
|
||||||
localforage: "com/isoterminal/localforage.js",
|
boot: "com/isoterminal/feat/boot.js",
|
||||||
|
jsconsole: "com/isoterminal/feat/jsconsole.js",
|
||||||
|
javascript: "com/isoterminal/feat/javascript.js",
|
||||||
|
indexhtml: "com/isoterminal/feat/index.html.js",
|
||||||
|
indexjs: "com/isoterminal/feat/index.js.js",
|
||||||
|
autorestore: "com/isoterminal/feat/autorestore.js",
|
||||||
|
localforage: "https://cdn.rawgit.com/mozilla/localForage/master/dist/localforage.js"
|
||||||
},
|
},
|
||||||
|
|
||||||
dom: {
|
dom: {
|
||||||
scale: 0.66,
|
scale: 0.5,
|
||||||
events: ['click','keydown'],
|
events: ['click','keydown'],
|
||||||
html: (me) => `<div class="isoterminal">
|
html: (me) => `<div class="isoterminal">
|
||||||
<input type="file" id="pastedrop" style="position:absolute; left:-9999px;opacity:0"></input>
|
<div id="screen" style="white-space: pre; font: 14px monospace; "></div>
|
||||||
<div id="term" tabindex="0"></div>
|
<canvas style="display: none"></canvas>
|
||||||
|
<div id="serial"></div>
|
||||||
</div>`,
|
</div>`,
|
||||||
|
|
||||||
css: (me) => `
|
css: (me) => `.isoterminal{
|
||||||
|
|
||||||
.isoterminal{
|
|
||||||
padding: ${me.com.data.padding}px;
|
padding: ${me.com.data.padding}px;
|
||||||
margin-top:-60px;
|
|
||||||
padding-bottom:60px;
|
|
||||||
width:100%;
|
width:100%;
|
||||||
height:99%;
|
height:100%;
|
||||||
resize: both;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Cousine';
|
font-family: 'Cousine';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url(./com/isoterminal/assets/Cousine.ttf) format('truetype');
|
src: url(./assets/Cousine.ttf) format('truetype');
|
||||||
}
|
}
|
||||||
@font-face {
|
|
||||||
font-family: 'Cousine';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
src: url(./com/isoterminal/assets/CousineBold.ttf) format('truetype');
|
|
||||||
}
|
|
||||||
|
|
||||||
.isoterminal *{
|
.isoterminal *{
|
||||||
outline:none;
|
white-space: pre;
|
||||||
box-shadow:none;
|
line-height:16px;
|
||||||
|
display:inline;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.isoterminal *,
|
||||||
.term {
|
.xterm-dom-renderer-owner-1 .xterm-rows {
|
||||||
font-family: 'Cousine';
|
font-size: 14px;
|
||||||
line-height: ${me.com.data.lineHeight}px;
|
font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace;
|
||||||
font-weight: normal;
|
font-weight:500 !important;
|
||||||
font-variant-ligatures: none;
|
letter-spacing: 0 !important;
|
||||||
color: #f0f0f0;
|
text-shadow: 0px 0px 10px #F075;
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.term_content a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: underline;
|
|
||||||
color:#2AFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.term_content a span{
|
|
||||||
text-shadow: 0px 0px 10px #F07A;
|
|
||||||
}
|
|
||||||
|
|
||||||
.term_content a:hover {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: underline;
|
|
||||||
animation:fade 1000ms infinite;
|
|
||||||
-webkit-animation:fade 1000ms infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.term_cursor {
|
|
||||||
color: #000000;
|
|
||||||
background: #70f;
|
|
||||||
animation:fade 1000ms infinite;
|
|
||||||
-webkit-animation:fade 1000ms infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.term_char_size {
|
|
||||||
display: inline-block;
|
|
||||||
visibility: hidden;
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: -1000px;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.term_textarea {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
width: 0px;
|
|
||||||
height: 0px;
|
|
||||||
padding: 0px;
|
|
||||||
border: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
opacity: 0;
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.term_scrollbar { background: transparent url(images/bg-scrollbar-track-y.png) no-repeat 0 0; position: relative; background-position: 0 0; float: right; height: 100%; }
|
|
||||||
.term_track { background: transparent url(images/bg-scrollbar-trackend-y.png) no-repeat 0 100%; height: 100%; width:13px; position: relative; padding: 0 1px; }
|
|
||||||
.term_thumb { background: transparent url(images/bg-scrollbar-thumb-y.png) no-repeat 50% 100%; height: 20px; width: 25px; cursor: pointer; overflow: hidden; position: absolute; top: 0; left: -5px; }
|
|
||||||
.term_thumb .term_end { background: transparent url(images/bg-scrollbar-thumb-y.png) no-repeat 50% 0; overflow: hidden; height: 5px; width: 25px; }
|
|
||||||
.noSelect { user-select: none; -o-user-select: none; -moz-user-select: none; -khtml-user-select: none; -webkit-user-select: none; }
|
|
||||||
|
|
||||||
.isoterminal style{ display:none }
|
.isoterminal style{ display:none }
|
||||||
|
|
||||||
blink{
|
|
||||||
border:none;
|
|
||||||
padding:none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#overlay .winbox:has(> .isoterminal){
|
|
||||||
background:transparent;
|
|
||||||
box-shadow:none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.XR .cursor {
|
|
||||||
animation:none;
|
|
||||||
-webkit-animation:none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.winbox#${me.el.uid} .wb-header{
|
|
||||||
background: var(--xrsh-black) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-body:has(> .isoterminal){
|
.wb-body:has(> .isoterminal){
|
||||||
background: var(--xrsh-black);
|
background: #000C;
|
||||||
overflow:hidden;
|
overflow:hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.XR .isoterminal{
|
.XR .wb-body:has(> .isoterminal){
|
||||||
background: #000 !important;
|
background: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.isoterminal *{
|
.isoterminal div{ display:block; }
|
||||||
font-size: 14px;
|
.isoterminal span{ display: inline }
|
||||||
font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace;
|
|
||||||
font-weight:500 !important;
|
|
||||||
text-shadow: 0px 0px 10px #F075;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade {
|
@keyframes fade {
|
||||||
from { opacity: 1.0; }
|
from { opacity: 1.0; }
|
||||||
|
|
@ -280,49 +140,22 @@ if( typeof AFRAME != 'undefined '){
|
||||||
to { opacity: 1.0; }
|
to { opacity: 1.0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.blink{
|
.blink {
|
||||||
animation:fade 1000ms infinite;
|
animation:fade 1000ms infinite;
|
||||||
-webkit-animation:fade 1000ms infinite;
|
-webkit-animation:fade 1000ms infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
initTerminal: async function(singleton){
|
initTerminal: async function(singleton){
|
||||||
|
|
||||||
// why not latest xterm or v3.12 with builtin-canvas support?
|
if( this.data.xterm ){
|
||||||
// first versions used 1.5.4, a typescript rewrite which:
|
this.requires.xtermjs = "https://unpkg.com/@xterm/xterm@5.5.0/lib/xterm.js"
|
||||||
// * acts weird with oculus browser keyboard (does not repaint properly after typing)
|
this.requires.xtermcss = "https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css"
|
||||||
// * does not use canvas anymore [which would be ideal for THREE.js texture]
|
this.requires.xterm = "com/isoterminal/feat/xterm.js"
|
||||||
// * does not allow switching between dom/canvas
|
}
|
||||||
// * only allows a standalone WebGL addon (conflicts with THREE)
|
|
||||||
// * heavily dependent on requestAnimationFrame (conflicts with THREE)
|
|
||||||
// * typescript-rewrite results in ~300k lib (instead of 96k)
|
|
||||||
// * v3.12 had slightly better performance but still very heavy
|
|
||||||
//
|
|
||||||
await AFRAME.utils.require(this.requires)
|
|
||||||
|
|
||||||
let features = { // ISOTerminal plugins
|
let s = await AFRAME.utils.require(this.requires)
|
||||||
boot: "com/isoterminal/feat/boot.js",
|
|
||||||
javascript: "com/isoterminal/feat/javascript.js",
|
|
||||||
jsconsole: "com/isoterminal/feat/jsconsole.js",
|
|
||||||
remotekeyboard: "com/isoterminal/feat/remotekeyboard.js",
|
|
||||||
indexhtml: "com/isoterminal/feat/index.html.js",
|
|
||||||
indexjs: "com/isoterminal/feat/index.js.js",
|
|
||||||
pastedropFeat: "com/isoterminal/feat/pastedrop.js",
|
|
||||||
httpfs: "com/isoterminal/feat/httpfs.js",
|
|
||||||
autorestore: "com/isoterminal/feat/autorestore.js",
|
|
||||||
}
|
|
||||||
if( document.location.hash.match(/#test/) || this.data.debug ){
|
|
||||||
features['tests'] = "tests/index.js"
|
|
||||||
}
|
|
||||||
if( this.data.emulator == 'fbterm' ){
|
|
||||||
features['fbtermjs'] = "com/isoterminal/term.js"
|
|
||||||
features['fbterm'] = "com/isoterminal/feat/term.js"
|
|
||||||
}
|
|
||||||
await AFRAME.utils.require(features)
|
|
||||||
// this one extends autorestore.js
|
|
||||||
await AFRAME.utils.require({remotestorage: "com/isoterminal/feat/remotestorage.js"})
|
|
||||||
|
|
||||||
this.el.setAttribute("selfcontainer","")
|
this.el.setAttribute("selfcontainer","")
|
||||||
|
|
||||||
|
|
@ -341,43 +174,44 @@ if( typeof AFRAME != 'undefined '){
|
||||||
}
|
}
|
||||||
|
|
||||||
// init isoterminal
|
// init isoterminal
|
||||||
this.term = new ISOTerminal(instance,this.data)
|
this.isoterminal = new ISOTerminal(instance,this.data)
|
||||||
|
|
||||||
instance.addEventListener('DOMready', () => {
|
instance.addEventListener('DOMready', () => {
|
||||||
this.term.emit('term_init', {instance, aEntity:this})
|
|
||||||
//instance.winbox.resize(720,380)
|
//instance.winbox.resize(720,380)
|
||||||
let size = `width: ${this.data.width}; height: ${this.data.height}`
|
let size = this.data.xterm ? 'width: 1024px; height:600px'
|
||||||
instance.setAttribute("window", `title: ${this.data.title}; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}; class: no-full, no-min, no-close, no-max, no-resize; grabbable: components.html.el.object3D.children.${this.el.children.length}`)
|
: 'width: 720px; height:455px'
|
||||||
|
instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
instance.addEventListener('window.oncreate', (e) => {
|
instance.addEventListener('window.oncreate', (e) => {
|
||||||
instance.dom.classList.add('blink')
|
instance.dom.classList.add('blink')
|
||||||
// canvas to texture texture
|
|
||||||
instance.setAttribute("html-as-texture-in-xr", `domid: .winbox#${instance.uid}; faceuser: true`)
|
|
||||||
|
|
||||||
// run iso
|
// run iso
|
||||||
let opts = {dom:instance.dom}
|
let opts = {dom:instance.dom}
|
||||||
for( let i in this.data ) opts[i] = this.data[i]
|
for( let i in this.data ) opts[i] = this.data[i]
|
||||||
opts.cols = this.cols
|
this.isoterminal.runISO(opts)
|
||||||
opts.rows = this.rows
|
|
||||||
this.term.start(opts)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
instance.setAttribute("dom", "")
|
instance.setAttribute("dom", "")
|
||||||
instance.setAttribute("pastedrop", "")
|
|
||||||
|
|
||||||
|
|
||||||
// *REMOVE* make a boot-plugin mechanism in feat/term.js
|
this.isoterminal.addEventListener('postReady', (e)=>{
|
||||||
this.term.addEventListener('enable-console', () => {
|
// bugfix: send window dimensions to xterm (xterm.js does that from dom-sizechange to xterm via escape codes)
|
||||||
|
let wb = instance.winbox
|
||||||
|
if( this.data.maximized ){
|
||||||
|
wb.restore()
|
||||||
|
wb.maximize()
|
||||||
|
}else wb.resize()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.isoterminal.addEventListener('ready', (e)=>{
|
||||||
instance.dom.classList.remove('blink')
|
instance.dom.classList.remove('blink')
|
||||||
|
this.isoterminal.emit('status',"running")
|
||||||
|
setTimeout( () => { // important: after window maximize animation to get true size
|
||||||
|
instance.setAttribute("html-as-texture-in-xr", `domid: #${instance.uid}`) // only show aframe-html in xr
|
||||||
|
},1500)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.term.addEventListener('ready', (e) => {
|
this.isoterminal.addEventListener('status', function(e){
|
||||||
instance.dom.classList.remove('blink')
|
|
||||||
this.term.emit('status',"running")
|
|
||||||
})
|
|
||||||
|
|
||||||
this.term.addEventListener('status', function(e){
|
|
||||||
let msg = e.detail
|
let msg = e.detail
|
||||||
const w = instance.winbox
|
const w = instance.winbox
|
||||||
if(!w) return
|
if(!w) return
|
||||||
|
|
@ -394,75 +228,18 @@ if( typeof AFRAME != 'undefined '){
|
||||||
instance.addEventListener('window.onmaximize', resize )
|
instance.addEventListener('window.onmaximize', resize )
|
||||||
|
|
||||||
const focus = (e) => {
|
const focus = (e) => {
|
||||||
|
if( this.isoterminal?.emulator?.serial_adapter?.term ){
|
||||||
if( event.target == document.activeElement ){
|
this.isoterminal.emulator.serial_adapter.term.focus()
|
||||||
// if we're already focused, the keyboard is already triggerend
|
|
||||||
// therefore we blur() the element for correctness
|
|
||||||
// which allows the WebXR keyboard dissappear if implemented at all
|
|
||||||
// [Meta Quest 2 does not do this]
|
|
||||||
event.target.blur()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate distance between thumb and indexfinger to detect pinch
|
|
||||||
// which should prevent focus-event (annoying to have keyboard popping up during pinch)
|
|
||||||
if( e.detail?.withEl?.components['hand-tracking-controls'] ){
|
|
||||||
const hand = e.detail.withEl.components['hand-tracking-controls']
|
|
||||||
const thumb = hand.bones.find( (b) => b.name == 'thumb-tip' )
|
|
||||||
const diff = thumb.position.distanceTo(hand.indexTipPosition)
|
|
||||||
if( diff < 0.02) return // pinching! don't trigger keyboard (focus)
|
|
||||||
}
|
|
||||||
this.el.emit('focus',e.detail)
|
|
||||||
}
|
}
|
||||||
|
instance.addEventListener('obbcollisionstarted', focus )
|
||||||
|
|
||||||
this.el.addEventListener('obbcollisionstarted', focus )
|
this.el.sceneEl.addEventListener('enter-vr', focus )
|
||||||
this.el.sceneEl.addEventListener('exit-vr', focus )
|
this.el.sceneEl.addEventListener('enter-ar', focus )
|
||||||
this.el.sceneEl.addEventListener('exit-ar', focus )
|
|
||||||
|
|
||||||
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
|
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
|
||||||
},
|
},
|
||||||
|
|
||||||
initHud: function(){
|
|
||||||
if( AFRAME.utils.device.isMobile() ) this.data.HUD = true
|
|
||||||
if( this.data.HUD ){
|
|
||||||
document.querySelector('[camera]').appendChild( this.el )
|
|
||||||
this.el.setAttribute("position","0 -0.03 -0.4")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setupPasteDrop: function(){
|
|
||||||
this.el.addEventListener('pasteFile', (e) => {
|
|
||||||
e.preventDefault() // prevent bubbling up to window (which is triggering this initially)
|
|
||||||
if( !this.term.pasteFile ) return // skip if feat/pastedrop.js is not loaded
|
|
||||||
this.term.pasteFile(e.detail)
|
|
||||||
})
|
|
||||||
return this
|
|
||||||
},
|
|
||||||
|
|
||||||
calculateDimension: function(){
|
|
||||||
if( this.data.width == -1 ) this.data.width = document.body.offsetWidth;
|
|
||||||
if( this.data.height == -1 ) this.data.height = Math.floor( document.body.offsetHeight - 30 )
|
|
||||||
if( this.data.height > this.data.width ) this.data.height = this.data.width // mobile smartphone fix
|
|
||||||
this.data.width -= this.data.padding*2
|
|
||||||
this.data.height -= this.data.padding*2
|
|
||||||
this.cols = Math.floor(this.data.width/this.data.lineHeight*2)-1
|
|
||||||
this.rows = Math.floor( (this.data.height*0.93)/this.data.lineHeight)-1
|
|
||||||
},
|
|
||||||
|
|
||||||
setupEvents: function(){
|
|
||||||
this.el.addEventListener('exec', (e) => this.term.exec( e.detail ) )
|
|
||||||
this.el.addEventListener('hook', (e) => this.term.hook( e.detail[0], e.detail[1] ) )
|
|
||||||
this.el.addEventListener('send', (e) => this.term.send( e.detail[0], e.detail[1] || 0 ) )
|
|
||||||
this.el.addEventListener('create_file', async (e) => await this.term.worker.create_file( e.detail[0], e.detail[1] ) )
|
|
||||||
this.el.addEventListener('create_file_from_url', async (e) => await this.term.worker.create_file_from_url( e.detail[0], e.detail[1] ) )
|
|
||||||
this.el.addEventListener('update_file', async (e) => await this.term.worker.update_file( e.detail[0], e.detail[1] ) )
|
|
||||||
this.el.addEventListener('append_file', async (e) => await this.term.worker.append_file( e.detail[0], e.detail[1] ) )
|
|
||||||
this.el.addEventListener('read_file', async (e) => {
|
|
||||||
const buf = await this.term.worker.read_file( e.detail[0] )
|
|
||||||
const str = new TextDecoder().decode(buf)
|
|
||||||
if( typeof e.detail[1] == 'function' ) e.detail[1](str,buf)
|
|
||||||
else console.log(str)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
events:{
|
events:{
|
||||||
|
|
||||||
|
|
@ -473,13 +250,12 @@ if( typeof AFRAME != 'undefined '){
|
||||||
// reactive events for this.data updates
|
// reactive events for this.data updates
|
||||||
myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
|
myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
|
||||||
|
|
||||||
|
ready: function( ){
|
||||||
|
this.el.dom.style.display = 'none'
|
||||||
|
},
|
||||||
|
|
||||||
launcher: async function(){
|
launcher: async function(){
|
||||||
if( !this.term.instance ){
|
this.initTerminal()
|
||||||
this.initTerminal()
|
|
||||||
}else{
|
|
||||||
// toggle visibility
|
|
||||||
this.el.winbox[ this.el.winbox.min ? 'restore' : 'minimize' ]()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
@ -503,7 +279,6 @@ if( typeof AFRAME != 'undefined '){
|
||||||
"scope": "/",
|
"scope": "/",
|
||||||
"theme_color": "#3367D6",
|
"theme_color": "#3367D6",
|
||||||
"shortcuts": [
|
"shortcuts": [
|
||||||
/*
|
|
||||||
{
|
{
|
||||||
"name": "What is the latest news?",
|
"name": "What is the latest news?",
|
||||||
"cli":{
|
"cli":{
|
||||||
|
|
@ -518,9 +293,8 @@ if( typeof AFRAME != 'undefined '){
|
||||||
"url": "/today?source=pwa",
|
"url": "/today?source=pwa",
|
||||||
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
],
|
],
|
||||||
"description": "Runs an .iso file",
|
"description": "Hello world information",
|
||||||
"screenshots": [
|
"screenshots": [
|
||||||
{
|
{
|
||||||
"src": "/images/screenshot1.png",
|
"src": "/images/screenshot1.png",
|
||||||
|
|
@ -530,7 +304,7 @@ if( typeof AFRAME != 'undefined '){
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"help":`
|
"help":`
|
||||||
XRSH application
|
Helloworld application
|
||||||
|
|
||||||
This is a help file which describes the application.
|
This is a help file which describes the application.
|
||||||
It will be rendered thru troika text, and will contain
|
It will be rendered thru troika text, and will contain
|
||||||
|
|
|
||||||
|
|
@ -1,360 +0,0 @@
|
||||||
function ISOTerminal(instance,opts){
|
|
||||||
// create a neutral isoterminal object which can be decorated
|
|
||||||
// with prototype functions and has addListener() and dispatchEvent()
|
|
||||||
let obj = new EventTarget()
|
|
||||||
obj.instance = instance
|
|
||||||
obj.opts = opts
|
|
||||||
// register default event listeners (enable file based features like isoterminal/jsconsole.js e.g.)
|
|
||||||
for( let event in ISOTerminal.listener )
|
|
||||||
for( let cb in ISOTerminal.listener[event] )
|
|
||||||
obj.addEventListener( event, ISOTerminal.listener[event][cb] )
|
|
||||||
// compose object with functions
|
|
||||||
for( let i in ISOTerminal.prototype ) obj[i] = ISOTerminal.prototype[i]
|
|
||||||
obj.emit('init')
|
|
||||||
instance.sceneEl.emit("isoterminal_init",{})
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.emit = function(event,data,sender){
|
|
||||||
data = data || false
|
|
||||||
const evObj = new CustomEvent(event, {detail: data} )
|
|
||||||
// forward event to worker/instance/AFRAME element or component-function
|
|
||||||
// this feels complex, but actually keeps event- and function-names more concise in codebase
|
|
||||||
let fire = () => {
|
|
||||||
this.dispatchEvent( evObj )
|
|
||||||
if( sender != "instance" && this.instance ) this.instance.dispatchEvent(evObj)
|
|
||||||
if( sender != "worker" && this.worker ) this.worker.postMessage({event,data}, PromiseWorker.prototype.getTransferable(data) )
|
|
||||||
if( sender !== undefined && typeof this[event] == 'function' ) this[event].apply(this, data && data.push ? data : [data] )
|
|
||||||
}
|
|
||||||
if( event.match(/^serial/) ){
|
|
||||||
this.preventFrameDrop( fire )
|
|
||||||
}else fire()
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.addEventListener = (event,cb) => {
|
|
||||||
ISOTerminal.listener = ISOTerminal.listener || {}
|
|
||||||
ISOTerminal.listener[event] = ISOTerminal.listener[event] || []
|
|
||||||
ISOTerminal.listener[event].push(cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.exec = function(opts){
|
|
||||||
const shellscript = opts[0];
|
|
||||||
const cb = opts[1];
|
|
||||||
let cmd = `printf "\n\r"; { sh<<EOF\n ${shellscript}; \nEOF\n} &> /mnt/exec;\n`
|
|
||||||
console.log(cmd)
|
|
||||||
if( cb ){
|
|
||||||
window.cb = cb
|
|
||||||
cmd += `js 'document.querySelector("[isoterminal]").emit("read_file", ["exec", window.cb ])';\n`
|
|
||||||
}
|
|
||||||
this.send( cmd, 1 )
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.hook = function(hookname,args){
|
|
||||||
let cmd = `{ type hook || source /etc/profile.sh; }; hook ${hookname} "${args.join('" "')}"`
|
|
||||||
this.exec([cmd])
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.serial_input = 0; // can be set to 0,1,2,3 to define stdinput tty (xterm plugin)
|
|
||||||
|
|
||||||
ISOTerminal.prototype.send = function(str, ttyNr){
|
|
||||||
if( ttyNr == undefined) ttyNr = this.serial_input
|
|
||||||
if( (this.emulator || this.worker) && this.ready ){
|
|
||||||
if( ttyNr == undefined ){
|
|
||||||
if( this.emulator.serial_adapter ){
|
|
||||||
this.emulator.serial_adapter.term.paste(str)
|
|
||||||
}else this.emulator.keyboard_send_text(str) // vga screen
|
|
||||||
}else{
|
|
||||||
this.convert.toUint8Array( str ).map( (c) => {
|
|
||||||
this.preventFrameDrop(
|
|
||||||
() => {
|
|
||||||
this.worker.postMessage({event:`serial${ttyNr}-input`,data:c})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
this.emit('serial-output-string', str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.convert = {
|
|
||||||
|
|
||||||
arrayBufferToBase64: function(buffer){
|
|
||||||
let binary = '';
|
|
||||||
const bytes = new Uint8Array(buffer);
|
|
||||||
const len = bytes.byteLength;
|
|
||||||
for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
|
|
||||||
return btoa(binary);
|
|
||||||
},
|
|
||||||
|
|
||||||
base64ToArrayBuffer: function(base64) {
|
|
||||||
const binaryString = atob(base64);
|
|
||||||
const len = binaryString.length;
|
|
||||||
const bytes = new Uint8Array(len);
|
|
||||||
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return bytes.buffer;
|
|
||||||
},
|
|
||||||
|
|
||||||
toUint8Array: function(str) {
|
|
||||||
str = String(str) || String("")
|
|
||||||
// Create a new Uint8Array with the same length as the input string
|
|
||||||
const uint8Array = new Uint8Array(str.length);
|
|
||||||
|
|
||||||
// Iterate over the string and populate the Uint8Array
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
uint8Array[i] = str.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return uint8Array;
|
|
||||||
},
|
|
||||||
|
|
||||||
Uint8ArrayToString: function(arr){
|
|
||||||
const decoder = new TextDecoder('utf-8'); // Specify encoding
|
|
||||||
return decoder.decode(arr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.start = function(opts){
|
|
||||||
|
|
||||||
let me = this
|
|
||||||
this.opts = {...this.opts, ...opts}
|
|
||||||
let image = {}
|
|
||||||
if( opts.iso.match(/\.iso$/) ) image.cdrom = { url: opts.iso }
|
|
||||||
if( opts.iso.match(/\.bin$/) ) image.bzimage = { url: opts.iso }
|
|
||||||
|
|
||||||
opts = { ...image,
|
|
||||||
uart1:true, // /dev/ttyS1
|
|
||||||
uart2:true, // /dev/ttyS2
|
|
||||||
uart3:true, // /dev/ttyS3
|
|
||||||
wasm_path: "v86.wasm",
|
|
||||||
memory_size: opts.memory * 1024 * 1024,
|
|
||||||
vga_memory_size: 2 * 1024 * 1024,
|
|
||||||
//screen_container: opts.dom,
|
|
||||||
//serial_container: opts.dom,
|
|
||||||
bios: {
|
|
||||||
url: "bios/seabios.bin",
|
|
||||||
},
|
|
||||||
vga_bios: {
|
|
||||||
url: "bios/vgabios.bin",
|
|
||||||
//urg|: "com/isoterminal/bios/VGABIOS-lgpl-latest.bin",
|
|
||||||
},
|
|
||||||
cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable init_on_freg|=on vga=ask", //vga=0x122",
|
|
||||||
net_device:{
|
|
||||||
//type:"ne2k",
|
|
||||||
relay_url:"fetch"
|
|
||||||
//relay_url:"wss://relay.widgetry.org/"
|
|
||||||
|
|
||||||
//local_http: true,
|
|
||||||
//type:"virtio",
|
|
||||||
//relay_url:"fetch",
|
|
||||||
//cors_proxy: "https://corsproxy.io/"
|
|
||||||
},
|
|
||||||
//bzimage_initrd_from_filesystem: true,
|
|
||||||
//filesystem: {
|
|
||||||
// baseurl: "com/isoterminal/v86/images/alpine-rootfs-flat",
|
|
||||||
// basefs: "com/isoterminal/v86/images/alpine-fs.json",
|
|
||||||
// },
|
|
||||||
//screen_dummy: true,
|
|
||||||
//disable_jit: false,
|
|
||||||
overlayfs: this.opts.overlayfs,
|
|
||||||
filesystem: {},
|
|
||||||
autostart: true,
|
|
||||||
prompt: this.opts.prompt,
|
|
||||||
debug: this.opts.debug ? true : false
|
|
||||||
};
|
|
||||||
|
|
||||||
this
|
|
||||||
.setupWorker(opts)
|
|
||||||
.startVM(opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.setupWorker = function(opts){
|
|
||||||
|
|
||||||
if( typeof window.PromiseWorker == 'undefined' ) return this
|
|
||||||
|
|
||||||
/*
|
|
||||||
* the WebWorker (which runs v86)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
this.worker = new PromiseWorker( "com/isoterminal/worker.js", (cb,event,data) => {
|
|
||||||
if( !data.promiseId ) this.emit(event,data,"worker") // forward event to world
|
|
||||||
this.preventFrameDrop( cb(event,data) )
|
|
||||||
})
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.getLoaderMsg = function(){
|
|
||||||
|
|
||||||
const loading = [
|
|
||||||
'loading quantum bits and bytes',
|
|
||||||
'preparing quantum flux capacitors',
|
|
||||||
'crunching peanuts and chakras',
|
|
||||||
'preparing parallel universe',
|
|
||||||
'loading quantum state fluctuations',
|
|
||||||
'preparing godmode',
|
|
||||||
'loading cat pawns and cuteness',
|
|
||||||
'beaming up scotty',
|
|
||||||
'still faster than Windows update',
|
|
||||||
'loading a microlinux',
|
|
||||||
'figuring out meaning of life',
|
|
||||||
'Aligning your chakras now',
|
|
||||||
'Breathing in good vibes',
|
|
||||||
'Finding inner peace soon',
|
|
||||||
'Centering your Zen energy',
|
|
||||||
'Awakening third eye powers',
|
|
||||||
'Tuning into the universe',
|
|
||||||
'Balancing your cosmic karma',
|
|
||||||
'Stretching time and space',
|
|
||||||
'Recharging your soul battery',
|
|
||||||
'Transcending earthly limits'
|
|
||||||
]
|
|
||||||
|
|
||||||
const empower = [
|
|
||||||
"FOSS gives users control over their software, offering freedom to modify and share",
|
|
||||||
"Feeling powerless? FOSS escapes a mindset known as learned helplessness",
|
|
||||||
"FOSS breaks this cycle by showing that anyone can learn and contribute",
|
|
||||||
"Proprietary software can make users dependent, but FOSS offers real choices",
|
|
||||||
"FOSS communities provide support and encourage users to develop new skills",
|
|
||||||
"FOSS empowers users to customize and improve their tools",
|
|
||||||
"Engaging with FOSS helps build confidence and self-reliance in tech",
|
|
||||||
"FOSS shows that anyone can shape the digital world with curiosity and effort",
|
|
||||||
"Linux can revive old computers, extending their life and reduces e-waste",
|
|
||||||
"Many lightweight Linux distributions run smoothly on older hardware",
|
|
||||||
"Installing Linux on aging devices keeps them functional instead of sending them to the landfill",
|
|
||||||
"Linux uses fewer resources, making it ideal for reusing older machines",
|
|
||||||
"By using Linux, you can avoid buying new hardware, cutting down on tech waste",
|
|
||||||
"Instead of discarding slow devices, Linux can bring them back to life",
|
|
||||||
"Linux supports a wide range of devices, helping to prevent e-waste",
|
|
||||||
"Open-source drivers in Linux enable compatibility with old peripherals, reducing the need for replacements",
|
|
||||||
"Free Linux software helps users avoid planned obsolescence in commercial products",
|
|
||||||
"Switching to Linux promotes sustainability by reducing demand for new gadgets and lowering e-waste"
|
|
||||||
]
|
|
||||||
|
|
||||||
let motd = `
|
|
||||||
\r[38;5;57m. . ____ _____________ ________. ._. ._. . .
|
|
||||||
\r[38;5;93m. . .\\ \\/ /\\______ \\/ _____// | \\. .
|
|
||||||
\r[38;5;93m. . . \\ / | _/\\_____ \\/ ~ \\ .
|
|
||||||
\r[38;5;129m. . . / \\ | | \\/ \\ Y / .
|
|
||||||
\r[38;5;165m. . ./___/\\ \\ |____|_ /_______ /\\___|_ /. .
|
|
||||||
\r[38;5;201m. . . . . .\\_/. . . . \\/ . . . .\\/ . . _ \\/ . .
|
|
||||||
\r[38;5;165m▬▬▬▬▬▬▬▬ [38;5;51mhttps://xrsh.isvery.ninja[38;5;165m ▬▬▬▬▬▬▬▬▬▬▬▬
|
|
||||||
\r[38;5;165mlocal-first, polyglot, unixy WebXR IDE & runtime
|
|
||||||
\r[38;5;57m
|
|
||||||
\rcredits
|
|
||||||
\r-------
|
|
||||||
\r[38;5;51mhttps://www.w3.org/TR/webxr
|
|
||||||
\rhttps://xrfragment.org
|
|
||||||
\rhttps://threejs.org
|
|
||||||
\rhttps://aframe.org
|
|
||||||
\rhttps://busybox.net
|
|
||||||
\rhttps://buildroot.org
|
|
||||||
\rfediverse: @lvk@mastodon.online @utopiah@mastodon.pirateparty.be @nlnet@nlnet.nl
|
|
||||||
\r`
|
|
||||||
|
|
||||||
const text_color = "\r[38;5;129m"
|
|
||||||
const text_reset = "\033[0m"
|
|
||||||
const loadmsg = "\n\r"+loading[ Math.floor(Math.random()*1000) % loading.length ] + "..please wait \n\n\r"
|
|
||||||
const empowermsg = "\n\r"+text_reset+'"'+empower[ Math.floor(Math.random()*1000) % empower.length ] + '"\n\r'
|
|
||||||
return { motd, text_color, text_reset, loadmsg, empowermsg}
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.startVM = function(opts){
|
|
||||||
|
|
||||||
this.v86opts = opts
|
|
||||||
|
|
||||||
this.addEventListener('emulator-started', async (e) => {
|
|
||||||
if( this.boot.fromImage ) return this.emit('serial-output-string', "\r[!] downloading session...please wait\n\r[!] this could take a while depending on your connection..\n\r")
|
|
||||||
|
|
||||||
let line = ''
|
|
||||||
this.ready = false
|
|
||||||
|
|
||||||
this.addEventListener(`serial0-output-string`, async (e) => {
|
|
||||||
const str = e.detail
|
|
||||||
|
|
||||||
// lets scan for a prompt so we can send a 'ready' event to the world
|
|
||||||
if( !this.ready && str.match(/\n(\/ #|~ #|~%|\[.*\]>)/) ) this.postBoot()
|
|
||||||
|
|
||||||
if( this.ready || !this.opts.muteUntilPrompt ) this.emit('serial-output-string', e.detail )
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let msglib = this.getLoaderMsg()
|
|
||||||
let msg = msglib.motd
|
|
||||||
|
|
||||||
this.emit('serial-output-string', msg)
|
|
||||||
this.emit('bootMenu',{bootMenu: this.opts.bootMenu, bootMenuURL: this.opts.bootMenuURL })
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.bootISO = function(){
|
|
||||||
const getImage = (str) => decodeURIComponent(
|
|
||||||
str.match(/\&img=/) ? str.replace(/.*img=/,'').replace(/\&.*/,'') : ''
|
|
||||||
)
|
|
||||||
let msglib = this.getLoaderMsg()
|
|
||||||
this.emit('status',msglib.loadmsg)
|
|
||||||
let msg = "\n\r" + msglib.empowermsg + msglib.text_color + msglib.loadmsg + msglib.text_reset
|
|
||||||
this.emit('serial-output-string', msg)
|
|
||||||
if( getImage(this.boot.hash) ){
|
|
||||||
this.boot.fromImage = true
|
|
||||||
}
|
|
||||||
this.emit('runISO',{...this.v86opts, bufferLatency: this.opts.bufferLatency, img: getImage(this.boot.hash) })
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ISOTerminal.prototype.postBoot = function(cb){
|
|
||||||
this.emit('postReady',{})
|
|
||||||
this.ready = true
|
|
||||||
setTimeout( () => {
|
|
||||||
this.emit('ready',{})
|
|
||||||
if( cb ) cb()
|
|
||||||
}, 500 )
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is allows (unsophisticated) outputbuffering
|
|
||||||
ISOTerminal.prototype.bufferOutput = function(byte,cb,latency){
|
|
||||||
const resetBuffer = () => ({str:""})
|
|
||||||
this.buffer = this.buffer || resetBuffer()
|
|
||||||
this.buffer.str += String.fromCharCode(byte)
|
|
||||||
if( !this.buffer.id ){
|
|
||||||
cb(this.buffer.str) // send out leading call
|
|
||||||
this.buffer = resetBuffer()
|
|
||||||
this.buffer.id = setTimeout( () => { // accumulate succesive calls
|
|
||||||
if( this.buffer.str ) cb(this.buffer.str)
|
|
||||||
this.buffer = resetBuffer()
|
|
||||||
}, this.latency || 250)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//ISOTerminal.prototype.bufferOutput = function(byte, cb, latency, buffer) {
|
|
||||||
// const str = String.fromCharCode(byte);
|
|
||||||
// //if (str === '\r' || str === '\n' || str.charCodeAt(0) < 32 || str.charCodeAt(0) === 127) {
|
|
||||||
// // cb(str);
|
|
||||||
// //} else if (str === '\x1b') { // ESC
|
|
||||||
// // buffer.esc = true;
|
|
||||||
// //} else if (buffer.esc) {
|
|
||||||
// // cb('\x1b' + str);
|
|
||||||
// // buffer.esc = false;
|
|
||||||
// //} else {
|
|
||||||
// buffer.str = (buffer.str || '') + str;
|
|
||||||
// if (Date.now() - (buffer.timestamp || 0) >= latency) {
|
|
||||||
// console.log(buffer.str)
|
|
||||||
// cb(buffer.str);
|
|
||||||
// buffer.str = '';
|
|
||||||
// buffer.timestamp = Date.now();
|
|
||||||
// }
|
|
||||||
// //}
|
|
||||||
//}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.preventFrameDrop = function(cb){
|
|
||||||
// don't let workers cause framerate dropping
|
|
||||||
const xr = this.instance.sceneEl.renderer.xr
|
|
||||||
if( xr.isPresenting ){
|
|
||||||
xr.getSession().requestAnimationFrame(cb)
|
|
||||||
}else{
|
|
||||||
window.requestAnimationFrame(cb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
/*
|
|
||||||
* This is basically a Javascript Proxy for 'new Worker()'
|
|
||||||
* which allows calling worker-functions via promises.
|
|
||||||
*
|
|
||||||
* It's basically comlink without the fat.
|
|
||||||
*
|
|
||||||
* const w = new PromiseWorker("worker.js", (cb,event_or_fn,data) => {
|
|
||||||
* cb(event_or_fn,data) // decorate/ratelimit/hooks here
|
|
||||||
* })
|
|
||||||
* w.foo().then( console.dir )
|
|
||||||
*
|
|
||||||
* in worker.js define a function: this.foo = () => return {foo:"Bar"}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function PromiseWorker(file, onmessage){
|
|
||||||
|
|
||||||
let proxy
|
|
||||||
const worker = new Worker(file)
|
|
||||||
|
|
||||||
worker.onmessage = (e) => { // handle messages originating from worker
|
|
||||||
const {event,data} = e.data // this is worker.onmessage(...)
|
|
||||||
const cb = (event,data) => () => { //
|
|
||||||
if( data.promiseId ){ //
|
|
||||||
proxy.resolver(data) // forward to promise resolver
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onmessage(cb,event,data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxy = new Proxy(this,{
|
|
||||||
get(me,k){
|
|
||||||
if( k.match(/(postMessage|onmessage)/) ) return worker[k].bind(worker)
|
|
||||||
if( k.match(/(resolver|promise)/) ) return this[k].bind(this)
|
|
||||||
// promisify postMessage(...) call
|
|
||||||
return function(){
|
|
||||||
return this.promise(me,{event: k, data: [...arguments] })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
promise(me,msg){
|
|
||||||
if( typeof msg != 'object' || !msg.event || !msg.data ){
|
|
||||||
throw 'worker.promise({event:..,data:..}) did not receive correct msg : '+JSON.stringify(msg)
|
|
||||||
}
|
|
||||||
this.resolvers = this.resolvers || {last:1,pending:{}}
|
|
||||||
msg.data.promiseId = this.resolvers.last++
|
|
||||||
// Send id and task to WebWorker
|
|
||||||
let dataTransferable = PromiseWorker.prototype.getTransferable(msg.data)
|
|
||||||
worker.postMessage(msg, dataTransferable )
|
|
||||||
return new Promise( resolve => this.resolvers.pending[ msg.data.promiseId ] = resolve );
|
|
||||||
},
|
|
||||||
|
|
||||||
resolver(data){
|
|
||||||
if( !data || !data.promiseId ) throw 'promiseId not given'
|
|
||||||
this.resolvers.pending[ data.promiseId ](data.result);
|
|
||||||
delete this.resolvers.pending[ data.promiseId ]; // Prevent memory leak
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
PromiseWorker.prototype.getTransferable = function(data){
|
|
||||||
let objs = []
|
|
||||||
function isTransferable(obj) {
|
|
||||||
return obj instanceof ArrayBuffer ||
|
|
||||||
obj instanceof MessagePort ||
|
|
||||||
obj instanceof ImageBitmap ||
|
|
||||||
(typeof OffscreenCanvas !== 'undefined' && obj instanceof OffscreenCanvas) ||
|
|
||||||
(typeof ReadableStream !== 'undefined' && obj instanceof ReadableStream) ||
|
|
||||||
(typeof WritableStream !== 'undefined' && obj instanceof WritableStream) ||
|
|
||||||
(typeof TransformStream !== 'undefined' && obj instanceof TransformStream);
|
|
||||||
}
|
|
||||||
for( var i in data ){
|
|
||||||
if( isTransferable(data[i]) ) objs.push(data[i])
|
|
||||||
}
|
|
||||||
return objs.length ? objs : undefined
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
|
@ -1,16 +0,0 @@
|
||||||
--2024-09-23 17:49:04-- https://fonts.gstatic.com/s/cousine/v27/d6lNkaiiRdih4SpP9Z8K6T4.ttf
|
|
||||||
Resolving fonts.gstatic.com (fonts.gstatic.com)... 142.250.180.195, 2a00:1450:400d:806::2003
|
|
||||||
Connecting to fonts.gstatic.com (fonts.gstatic.com)|142.250.180.195|:443... connected.
|
|
||||||
HTTP request sent, awaiting response... 200 OK
|
|
||||||
Length: 287924 (281K) [font/ttf]
|
|
||||||
Saving to: ‘d6lNkaiiRdih4SpP9Z8K6T4.ttf’
|
|
||||||
|
|
||||||
0K .......... .......... .......... .......... .......... 17% 2.31M 0s
|
|
||||||
50K .......... .......... .......... .......... .......... 35% 1.91M 0s
|
|
||||||
100K .......... .......... .......... .......... .......... 53% 9.09M 0s
|
|
||||||
150K .......... .......... .......... .......... .......... 71% 3.03M 0s
|
|
||||||
200K .......... .......... .......... .......... .......... 88% 3.40M 0s
|
|
||||||
250K .......... .......... .......... . 100% 12.4M=0.09s
|
|
||||||
|
|
||||||
2024-09-23 17:49:05 (3.23 MB/s) - ‘d6lNkaiiRdih4SpP9Z8K6T4.ttf’ saved [287924/287924]
|
|
||||||
|
|
||||||
193
com/isoterminal/core.js
Normal file
193
com/isoterminal/core.js
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
//ISOTerminal.prototype.exec(cmd_array,stdin){
|
||||||
|
// // exec(['lua'] "print \"hello\") ---> cat /dev/browser/js/stdin | lua > /dev/browser/js/stdout
|
||||||
|
//}
|
||||||
|
|
||||||
|
ISOTerminal.prototype.serial_input = undefined; // can be set to 0,1,2,3 to define stdinput tty (xterm plugin)
|
||||||
|
|
||||||
|
ISOTerminal.prototype.exec = function(shellscript){
|
||||||
|
//let ts = String(Date.now())+".job"
|
||||||
|
//this.emulator.create_file(ts, this.toUint8Array(shellscript) )
|
||||||
|
this.send(shellscript+"\n",1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ISOTerminal.prototype.send = function(str, ttyNr){
|
||||||
|
if( !ttyNr ) ttyNr = this.serial_input
|
||||||
|
if( !ttyNr ){
|
||||||
|
if( this.emulator.serial_adapter ){
|
||||||
|
this.emulator.serial_adapter.term.paste(str)
|
||||||
|
}else this.emulator.keyboard_send_text(str) // vga screen
|
||||||
|
}else{
|
||||||
|
this.convert.toUint8Array( str ).map( (c) => this.emulator.bus.send(`serial${ttyNr}-input`, c ) )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ISOTerminal.prototype.convert = {
|
||||||
|
|
||||||
|
arrayBufferToBase64: function(buffer){
|
||||||
|
let binary = '';
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
const len = bytes.byteLength;
|
||||||
|
for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
|
||||||
|
return window.btoa(binary);
|
||||||
|
},
|
||||||
|
|
||||||
|
base64ToArrayBuffer: function(base64) {
|
||||||
|
const binaryString = window.atob(base64);
|
||||||
|
const len = binaryString.length;
|
||||||
|
const bytes = new Uint8Array(len);
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes.buffer;
|
||||||
|
},
|
||||||
|
|
||||||
|
toUint8Array: function(str) {
|
||||||
|
str = String(str) || String("")
|
||||||
|
// Create a new Uint8Array with the same length as the input string
|
||||||
|
const uint8Array = new Uint8Array(str.length);
|
||||||
|
|
||||||
|
// Iterate over the string and populate the Uint8Array
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
uint8Array[i] = str.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return uint8Array;
|
||||||
|
},
|
||||||
|
|
||||||
|
Uint8ArrayToString: function(arr){
|
||||||
|
const decoder = new TextDecoder('utf-8'); // Specify encoding
|
||||||
|
return decoder.decode(arr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ISOTerminal.prototype.runISO = function(opts){
|
||||||
|
|
||||||
|
let me = this
|
||||||
|
this.opts = {...this.opts, ...opts}
|
||||||
|
let image = {}
|
||||||
|
if( opts.iso.match(/\.iso$/) ) image.cdrom = { url: opts.iso }
|
||||||
|
if( opts.iso.match(/\.bin$/) ) image.bzimage = { url: opts.iso }
|
||||||
|
|
||||||
|
opts = { ...image,
|
||||||
|
uart1:true, // /dev/ttyS1
|
||||||
|
uart2:true, // /dev/ttyS2
|
||||||
|
uart3:true, // /dev/ttyS3
|
||||||
|
wasm_path: "com/isoterminal/v86.wasm",
|
||||||
|
memory_size: opts.memory * 1024 * 1024,
|
||||||
|
vga_memory_size: 2 * 1024 * 1024,
|
||||||
|
screen_container: opts.dom,
|
||||||
|
//serial_container: opts.dom,
|
||||||
|
bios: {
|
||||||
|
url: "com/isoterminal/bios/seabios.bin",
|
||||||
|
},
|
||||||
|
vga_bios: {
|
||||||
|
url: "com/isoterminal/bios/vgabios.bin",
|
||||||
|
//urg|: "com/isoterminal/bios/VGABIOS-lgpl-latest.bin",
|
||||||
|
},
|
||||||
|
network_relay_url: "wss://relay.widgetry.org/",
|
||||||
|
cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable init_on_freg|=on vga=ask", //vga=0x122",
|
||||||
|
//bzimage_initrd_from_filesystem: true,
|
||||||
|
//filesystem: {
|
||||||
|
// baseurl: "com/isoterminal/v86/images/alpine-rootfs-flat",
|
||||||
|
// basefs: "com/isoterminal/v86/images/alpine-fs.json",
|
||||||
|
// },
|
||||||
|
//screen_dummy: true,
|
||||||
|
//disable_jit: false,
|
||||||
|
filesystem: {},
|
||||||
|
autostart: true,
|
||||||
|
};
|
||||||
|
this.emit('runISO',opts)
|
||||||
|
let emulator = this.emulator = new V86(opts)
|
||||||
|
|
||||||
|
const loading = [
|
||||||
|
'loading quantum bits and bytes',
|
||||||
|
'preparing quantum flux capacitors',
|
||||||
|
'crunching peanuts and chakras',
|
||||||
|
'preparing parallel universe',
|
||||||
|
'loading quantum state fluctuations',
|
||||||
|
'preparing godmode',
|
||||||
|
'loading cat pawns and cuteness',
|
||||||
|
'beaming up scotty',
|
||||||
|
'still faster than Windows update',
|
||||||
|
'loading a microlinux',
|
||||||
|
'figuring out meaning of life',
|
||||||
|
'Aligning your chakras now',
|
||||||
|
'Breathing in good vibes',
|
||||||
|
'Finding inner peace soon',
|
||||||
|
'Centering your Zen energy',
|
||||||
|
'Awakening third eye powers',
|
||||||
|
'Tuning into the universe',
|
||||||
|
'Balancing your cosmic karma',
|
||||||
|
'Stretching time and space',
|
||||||
|
'Recharging your soul battery',
|
||||||
|
'Transcending earthly limits'
|
||||||
|
]
|
||||||
|
|
||||||
|
let loadmsg = loading[ Math.floor(Math.random()*1000) % loading.length ]
|
||||||
|
this.emit('status',loadmsg)
|
||||||
|
|
||||||
|
// replace welcome message https://github.com/copy/v86/blob/3c77b98bc4bc7a5d51a2056ea73d7666ca50fc9d/src/browser/serial.js#L231
|
||||||
|
let welcome = "This is the serial console. Whatever you type or paste here will be sent to COM1"
|
||||||
|
let motd = "\r[38;5;129m"
|
||||||
|
let msg = `${loadmsg}, please wait..`
|
||||||
|
while( msg.length < welcome.length ) msg += " "
|
||||||
|
msg += "\n"
|
||||||
|
motd += msg+"\033[0m"
|
||||||
|
|
||||||
|
emulator.bus.register("emulator-started", async (e) => {
|
||||||
|
this.emit('emulator-started',e)
|
||||||
|
|
||||||
|
if( emulator.serial_adapter ){
|
||||||
|
emulator.serial_adapter.term.clear()
|
||||||
|
emulator.serial_adapter.term.write(motd)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if( me.opts.overlayfs ){
|
||||||
|
fetch(me.opts.overlayfs)
|
||||||
|
.then( (f) => {
|
||||||
|
f.arrayBuffer().then( (buf) => {
|
||||||
|
emulator.create_file('overlayfs.zip', new Uint8Array(buf) )
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = ''
|
||||||
|
let ready = false
|
||||||
|
emulator.add_listener(`serial0-output-byte`, async (byte) => {
|
||||||
|
this.emit('${this.serial}-output-byte',byte)
|
||||||
|
var chr = String.fromCharCode(byte);
|
||||||
|
if(chr < " " && chr !== "\n" && chr !== "\t" || chr > "~")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(chr === "\n")
|
||||||
|
{
|
||||||
|
var new_line = line;
|
||||||
|
line = "";
|
||||||
|
}
|
||||||
|
else if(chr >= " " && chr <= "~")
|
||||||
|
{
|
||||||
|
line += chr;
|
||||||
|
}
|
||||||
|
if( !ready && line.match(/^(\/ #|~%|\[.*\]>)/) ){
|
||||||
|
this.emit('postReady',e)
|
||||||
|
setTimeout( () => this.emit('ready',e), 500 )
|
||||||
|
ready = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ISOTerminal.prototype.readFromPipe = function(filename,cb){
|
||||||
|
|
||||||
|
this.emulator.add_listener("9p-write-end", async (opts) => {
|
||||||
|
if ( opts[0] == filename.replace(/.*\//,'') ){
|
||||||
|
const buf = await this.emulator.read_file("console.tty")
|
||||||
|
cb( this.convert.Uint8ArrayToString(buf) )
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,109 +1,46 @@
|
||||||
let emulator = this.emulator
|
ISOTerminal.addEventListener('emulator-started', function(){
|
||||||
let me = this
|
let emulator = this.emulator
|
||||||
|
let isoterminal = this
|
||||||
|
|
||||||
emulator.fs9p.update_file = async function(file,data){
|
emulator.fs9p.update_file = async function(file,data){
|
||||||
const convert = ISOTerminal.prototype.convert
|
|
||||||
|
|
||||||
const p = this.SearchPath(file);
|
const p = this.SearchPath(file);
|
||||||
|
|
||||||
if(p.id === -1)
|
if(p.id === -1)
|
||||||
{
|
{
|
||||||
return emulator.create_file(file,data)
|
return emulator.create_file(file,data)
|
||||||
}
|
}
|
||||||
|
|
||||||
const inode = this.GetInode(p.id);
|
const inode = this.GetInode(p.id);
|
||||||
const buf = typeof data == 'string' ? convert.toUint8Array(data) : data || ""
|
const buf = typeof data == 'string' ? isoterminal.convert.toUint8Array(data) : data
|
||||||
if( buf.length == 0 ) return new Promise( (resolve,reject) => resolve(data) )
|
|
||||||
|
|
||||||
try{
|
|
||||||
await this.Write(p.id,0, buf.length, buf )
|
await this.Write(p.id,0, buf.length, buf )
|
||||||
// update inode
|
// update inode
|
||||||
inode.size = buf.length
|
inode.size = buf.length
|
||||||
const now = Math.round(Date.now() / 1000);
|
const now = Math.round(Date.now() / 1000);
|
||||||
inode.atime = inode.mtime = now;
|
inode.atime = inode.mtime = now;
|
||||||
|
isoterminal.exec(`touch ${file}`) // update inode
|
||||||
return new Promise( (resolve,reject) => resolve(buf) )
|
return new Promise( (resolve,reject) => resolve(buf) )
|
||||||
}catch(e){
|
|
||||||
console.error({file,data})
|
|
||||||
return new Promise( (resolve,reject) => reject(e) )
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emulator.fs9p.create_file_from_url = async function(file,url){
|
}
|
||||||
const convert = ISOTerminal.prototype.convert
|
|
||||||
return fetch(url)
|
|
||||||
.then( (res) => res.arrayBuffer() )
|
|
||||||
.then( (buf) => {
|
|
||||||
let arr = new Uint8Array(buf)
|
|
||||||
return emulator.create_file(file, arr )
|
|
||||||
})
|
|
||||||
.catch( (e) => {
|
|
||||||
emulator.create_file(file, new Uint8Array() ) // empty file so at least other processes can check for error (v86 has no retcodes for fs9p)
|
|
||||||
console.error(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
emulator.fs9p.append_file = async function(file,data){
|
emulator.fs9p.append_file = async function(file,data){
|
||||||
const convert = ISOTerminal.prototype.convert
|
|
||||||
|
|
||||||
const p = this.SearchPath(file);
|
const p = this.SearchPath(file);
|
||||||
|
|
||||||
if(p.id === -1)
|
if(p.id === -1)
|
||||||
{
|
{
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const inode = this.GetInode(p.id);
|
const inode = this.GetInode(p.id);
|
||||||
const buf = typeof data == 'string' ? convert.toUint8Array(data) : data
|
const buf = typeof data == 'string' ? isoterminal.convert.toUint8Array(data) : data
|
||||||
await this.Write(p.id, inode.size, buf.length, buf )
|
await this.Write(p.id, inode.size, buf.length, buf )
|
||||||
// update inode
|
// update inode
|
||||||
inode.size = inode.size + buf.length
|
inode.size = inode.size + buf.length
|
||||||
const now = Math.round(Date.now() / 1000);
|
const now = Math.round(Date.now() / 1000);
|
||||||
inode.atime = inode.mtime = now;
|
inode.atime = inode.mtime = now;
|
||||||
return new Promise( (resolve,reject) => resolve(buf) )
|
return new Promise( (resolve,reject) => resolve(buf) )
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
emulator.fs9p.read_file_world = async function(file){
|
})
|
||||||
const p = this.SearchPath(file);
|
|
||||||
|
|
||||||
if(p.id === -1)
|
|
||||||
{
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const inode = this.GetInode(p.id);
|
|
||||||
const perms = this.parseFilePermissions(inode.mode)
|
|
||||||
|
|
||||||
if( !perms.world.read ){
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.Read(p.id, 0, inode.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
emulator.fs9p.parseFilePermissions = function(permissionInt) {
|
|
||||||
// Convert the permission integer to octal
|
|
||||||
const octalPermissions = permissionInt.toString(8);
|
|
||||||
|
|
||||||
// Extract the permission bits (last 3 digits in octal)
|
|
||||||
const permissionBits = octalPermissions.slice(-3);
|
|
||||||
|
|
||||||
|
|
||||||
function parsePermission(digit) {
|
|
||||||
const num = parseInt(digit, 10);
|
|
||||||
|
|
||||||
return {
|
|
||||||
read: Boolean(num & 4), // 4 = read
|
|
||||||
write: Boolean(num & 2), // 2 = write
|
|
||||||
execute: Boolean(num & 1) // 1 = execute
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Decode the permissions
|
|
||||||
const permissions = {
|
|
||||||
owner: parsePermission(permissionBits[0]),
|
|
||||||
group: parsePermission(permissionBits[1]),
|
|
||||||
world: parsePermission(permissionBits[2]),
|
|
||||||
};
|
|
||||||
|
|
||||||
return permissions;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,106 +1,52 @@
|
||||||
// this is restoring state to/from the v86 emulator
|
ISOTerminal.addEventListener('emulator-started', function(e){
|
||||||
// however instead of passing the huge blob between webworker/browser
|
this.autorestore(e)
|
||||||
// we transfer it via localforage as a base64 string
|
})
|
||||||
|
|
||||||
if( typeof emulator != 'undefined' ){
|
ISOTerminal.prototype.autorestore = async function(e){
|
||||||
// inside worker-thread
|
|
||||||
importScripts("localforage.js") // we don't instance it again here (just use its functions)
|
|
||||||
|
|
||||||
this.restore_state = async function(data){
|
localforage.setDriver([
|
||||||
// fastforward instance state
|
localforage.INDEXEDDB,
|
||||||
this.opts.muteUntilPrompt = false
|
localforage.WEBSQL,
|
||||||
this.ready = true
|
localforage.LOCALSTORAGE
|
||||||
|
]).then( () => {
|
||||||
|
|
||||||
return new Promise( (resolve,reject) => {
|
localforage.getItem("state", async (err,stateBase64) => {
|
||||||
localforage.getItem("state", async (err,stateBase64) => {
|
if( !err && confirm('continue last session?') ){
|
||||||
if( stateBase64 && !err ){
|
|
||||||
state = ISOTerminal.prototype.convert.base64ToArrayBuffer( stateBase64 )
|
|
||||||
await emulator.restore_state(state)
|
|
||||||
console.log("restored state")
|
|
||||||
}else return reject("worker.js: emulator.restore_state (could not get state from localforage)")
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.save_state = async function(){
|
|
||||||
return new Promise( async (resolve,reject ) => {
|
|
||||||
console.log("saving session")
|
|
||||||
let state = await emulator.save_state()
|
|
||||||
localforage.setDriver([
|
|
||||||
localforage.INDEXEDDB,
|
|
||||||
localforage.WEBSQL,
|
|
||||||
localforage.LOCALSTORAGE
|
|
||||||
])
|
|
||||||
.then( () => {
|
|
||||||
localforage.setItem("state", ISOTerminal.prototype.convert.arrayBufferToBase64(state) )
|
|
||||||
console.log("state saved")
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
.catch( reject )
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}else{
|
|
||||||
// inside browser-thread
|
|
||||||
ISOTerminal.addEventListener('emulator-started', function(e){
|
|
||||||
this.autorestore(e)
|
|
||||||
this.emit("autorestore-installed")
|
|
||||||
})
|
|
||||||
|
|
||||||
ISOTerminal.prototype.restore = async function(e){
|
|
||||||
|
|
||||||
const onGetItem = (err,stateBase64) => {
|
|
||||||
const askConfirm = () => {
|
|
||||||
if( window.localStorage.getItem("restorestate") == "true" ) return true
|
|
||||||
try{
|
|
||||||
const scene = document.querySelector('a-scene');
|
|
||||||
if( scene.is('ar-mode') ) scene.exitAR()
|
|
||||||
if( scene.is('vr-mode') ) scene.exitVR()
|
|
||||||
}catch(e){}
|
|
||||||
return confirm( "Continue old session?" )
|
|
||||||
}
|
|
||||||
|
|
||||||
if( stateBase64 && !err && document.location.hash.length < 2 && askConfirm() ){
|
|
||||||
this.noboot = true // see feat/boot.js
|
this.noboot = true // see feat/boot.js
|
||||||
try{
|
state = this.convert.base64ToArrayBuffer( stateBase64 )
|
||||||
this.worker.restore_state()
|
this.emulator.restore_state(state)
|
||||||
.then( () => {
|
this.emit('postReady',e)
|
||||||
// simulate / fastforward boot events
|
setTimeout( () => {
|
||||||
this.postBoot( () => {
|
this.emit('ready',e)
|
||||||
// force redraw terminal issue
|
// press CTRL+a l (=gnu screen redisplay)
|
||||||
this.send("l")
|
setTimeout( () => this.send("l\n"),400 )
|
||||||
setTimeout( () => this.send("l"), 200 )
|
// reload index.js
|
||||||
//this.send("12")
|
this.emulator.read_file("root/index.js")
|
||||||
this.emit("exec",["source /etc/profile.sh; hook wakeup\n"])
|
.then( this.convert.Uint8ArrayToString )
|
||||||
this.emit("restored")
|
.then( this.runJavascript )
|
||||||
})
|
.catch( console.error )
|
||||||
})
|
// reload index.html
|
||||||
}catch(e){ console.error(e) }
|
this.emulator.read_file("root/index.html")
|
||||||
|
.then( this.convert.Uint8ArrayToString )
|
||||||
|
.then( this.runHTML )
|
||||||
|
.catch( console.error )
|
||||||
|
|
||||||
|
}, 500 )
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.save = async () => {
|
||||||
|
const state = await this.emulator.save_state()
|
||||||
|
console.log( String(this.convert.arrayBufferToBase64(state)).substr(0,5) )
|
||||||
|
localforage.setItem("state", this.convert.arrayBufferToBase64(state) )
|
||||||
}
|
}
|
||||||
|
|
||||||
const doRestore = () => {
|
window.addEventListener("beforeunload", function (e) {
|
||||||
|
var confirmationMessage = "Sure you want to leave?\nTIP: enter 'save' to continue this session later";
|
||||||
localforage.getItem("state", (err,stateBase64) => onGetItem(err,stateBase64) )
|
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
|
||||||
|
return confirmationMessage; //Webkit, Safari, Chrome
|
||||||
window.addEventListener("beforeunload", function (e) {
|
});
|
||||||
var confirmationMessage = "Sure you want to leave?\nTIP: enter 'save' to continue this session later";
|
|
||||||
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
|
|
||||||
return confirmationMessage; //Webkit, Safari, Chrome
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
localforage.setDriver([
|
|
||||||
localforage.INDEXEDDB,
|
|
||||||
localforage.WEBSQL,
|
|
||||||
localforage.LOCALSTORAGE
|
|
||||||
])
|
|
||||||
.then( () => doRestore() )
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.autorestore = ISOTerminal.prototype.restore // alias to launch during boot
|
|
||||||
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,86 +1,27 @@
|
||||||
ISOTerminal.addEventListener('ready', function(e){
|
ISOTerminal.addEventListener('ready', function(e){
|
||||||
setTimeout( () => this.boot(), 50 ) // allow other features/plugins to settle first (autorestore.js e.g.)
|
setTimeout( () => this.boot(), 50 ) // because of autorestore.js
|
||||||
})
|
})
|
||||||
|
|
||||||
ISOTerminal.prototype.bootMenu = function(e){
|
|
||||||
this.boot.menu.selected = false // reset
|
|
||||||
const autobootURL = e && e.detail.bootMenuURL && document.location.hash.length > 1
|
|
||||||
const autoboot = e && e.detail.bootMenu || autobootURL
|
|
||||||
if( !autoboot ){
|
|
||||||
|
|
||||||
let msg = '\n\r'
|
|
||||||
this.boot.menu.map( (m) => {
|
|
||||||
msg += `\r[36m${m.key})[0m ${m.title(this.opts)}\n`
|
|
||||||
})
|
|
||||||
msg += `\n\r[36menter choice>[0m `
|
|
||||||
this.send(msg)
|
|
||||||
|
|
||||||
}else{ // autoboot
|
|
||||||
if( this.term ){
|
|
||||||
this.term.handler( String(e.detail.bootMenu || e.detail.bootMenuURL).charAt(0) )
|
|
||||||
this.term.handler("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.addEventListener('bootMenu', function(e){this.bootMenu(e) })
|
|
||||||
|
|
||||||
ISOTerminal.prototype.boot = async function(e){
|
ISOTerminal.prototype.boot = async function(e){
|
||||||
// set environment
|
// set environment
|
||||||
let env = [
|
let env = ['export BROWSER=1']
|
||||||
`export LINES=${this.opts.rows}`,
|
|
||||||
`export COLUMNS=${this.opts.cols}`,
|
|
||||||
'export BROWSER=1',
|
|
||||||
]
|
|
||||||
for ( let i in document.location ){
|
for ( let i in document.location ){
|
||||||
if( typeof document.location[i] == 'string' && !String(i).match(/(hash|search)/) ){
|
if( typeof document.location[i] == 'string' )
|
||||||
env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') )
|
env.push( 'export '+String(i).toUpperCase()+'="'+document.location[i]+'"')
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
await this.emulator.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) )
|
||||||
// we export the cached hash/query (because they might be gone due to remotestorage plugin)
|
|
||||||
if( this.boot.hash.charAt(2) == '&' ){ // strip bootoption
|
|
||||||
this.boot.hashExBoot = `#` + this.boot.hash.substr(3)
|
|
||||||
}
|
|
||||||
env.push( 'export HASH="'+decodeURIComponent( this.boot.hashExBoot || this.boot.hash ) +'"' )
|
|
||||||
env.push( 'export QUERY="'+decodeURIComponent( this.boot.query ) +'"' )
|
|
||||||
await this.worker.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) )
|
|
||||||
|
|
||||||
if( this.serial_input == 0 ){
|
if( this.serial_input == 0 ){
|
||||||
if( !this.noboot ){
|
if( !this.noboot ){
|
||||||
this.send("source /etc/profile # \\o/ FOSS powa!\n")
|
let boot = "source /etc/profile\n"
|
||||||
|
this.send(boot+"\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
if( this.emulator.serial_adapter ) this.emulator.serial_adapter.term.focus()
|
||||||
|
else{
|
||||||
ISOTerminal.prototype.boot.fromImage = false
|
let els = [...document.querySelectorAll("div#screen")]
|
||||||
ISOTerminal.prototype.boot.menu = []
|
els.map( (el) => el.focus() )
|
||||||
ISOTerminal.prototype.boot.hash = document.location.hash
|
}
|
||||||
ISOTerminal.prototype.boot.query = document.location.search
|
|
||||||
|
|
||||||
// here REPL's can be defined
|
|
||||||
|
|
||||||
// REPL: iso
|
|
||||||
if( typeof window.PromiseWorker != 'undefined' ){ // if xrsh v86 is able to run in in worker
|
|
||||||
ISOTerminal.prototype.boot.menu.push(
|
|
||||||
{
|
|
||||||
key: "1",
|
|
||||||
title: (opts) => `boot [31m${String(opts.iso || "").replace(/.*\//,'')}[0m Linux ❤️ `,
|
|
||||||
init: function(){
|
|
||||||
|
|
||||||
// hack to notify href clicks
|
|
||||||
Term.prototype.href = (a) => {
|
|
||||||
if( a.href ){
|
|
||||||
this.exec(`source /etc/profile.sh; hook href "${a.href}"`)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
this.bootISO()
|
|
||||||
},
|
|
||||||
keyHandler: function(ch){ this.send(ch) } // send to v86 webworker
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
if( typeof emulator != 'undefined' ){
|
|
||||||
|
|
||||||
}else{
|
|
||||||
|
|
||||||
ISOTerminal.addEventListener('ready', function(e){
|
|
||||||
|
|
||||||
function getMimeType(file) {
|
|
||||||
const mimeTypes = {
|
|
||||||
jpg: 'image/jpeg',
|
|
||||||
jpeg: 'image/jpeg',
|
|
||||||
png: 'image/png',
|
|
||||||
mp4: 'video/mp4',
|
|
||||||
gif: 'image/gif',
|
|
||||||
};
|
|
||||||
|
|
||||||
const extension = file.split('.').pop().toLowerCase();
|
|
||||||
return mimeTypes[extension] || 'application/octet-stream'; // Fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
// listen for http request to the filesystem ( file://host/path )
|
|
||||||
xhook.before( (request,callback) => {
|
|
||||||
console.log(request.url)
|
|
||||||
if (request.url.match(/^\/mnt\/.*/) ){
|
|
||||||
let response
|
|
||||||
let file = request.url.replace(/^\/mnt\//,'')
|
|
||||||
let mimetype = getMimeType(file)
|
|
||||||
this.worker.read_file_world(file)
|
|
||||||
.then( (data) => {
|
|
||||||
if( data == null ) throw `/mnt/${file} does not exist in ISO filesystem`
|
|
||||||
let blob = new Blob( [data], {type: getMimeType(file) }) // wrap Uint8Array into array
|
|
||||||
response = {
|
|
||||||
headers: new Headers({ 'Content-Type': getMimeType(file) }),
|
|
||||||
data,
|
|
||||||
url: file,
|
|
||||||
status: 200,
|
|
||||||
blob: () => new Promise( (resolve,reject) => resolve(blob) ),
|
|
||||||
arrayBuffer: blob.arrayBuffer
|
|
||||||
}
|
|
||||||
console.log("serving from iso filesystem: "+file)
|
|
||||||
console.log("*TODO* large files being served partially")
|
|
||||||
callback(response)
|
|
||||||
})
|
|
||||||
.catch( (e) => {
|
|
||||||
console.error(e)
|
|
||||||
response = new Response()
|
|
||||||
response.status = 404
|
|
||||||
callback(response)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}else{
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +1,32 @@
|
||||||
|
ISOTerminal.addEventListener('init', function(){
|
||||||
|
|
||||||
if( typeof emulator != 'undefined' ){
|
this.addEventListener('emulator-started', function(e){
|
||||||
// inside worker-thread
|
|
||||||
|
|
||||||
this.listenIndexHTML = () => {
|
const emulator = this.emulator
|
||||||
const file = "dev/browser/html"
|
|
||||||
emulator.readFromPipe( file, async (data) => {
|
// unix to js device
|
||||||
const buf = await emulator.read_file( file )
|
this.readFromPipe( '/mnt/index.html', async (data) => {
|
||||||
|
const buf = await emulator.read_file("index.html")
|
||||||
const decoder = new TextDecoder('utf-8');
|
const decoder = new TextDecoder('utf-8');
|
||||||
const html = decoder.decode(buf).replace(/^#!\/bin\/html/,'') // remove leftover shebangs if any
|
const html = decoder.decode(buf)
|
||||||
try{
|
try{
|
||||||
this.postMessage({event:'runHTML',data:[html]})
|
this.runHTML(html)
|
||||||
}catch(e){
|
}catch(e){
|
||||||
console.error(file)
|
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}else{
|
|
||||||
// inside browser-thread
|
|
||||||
|
|
||||||
ISOTerminal.addEventListener('emulator-started', function(){
|
|
||||||
this.addEventListener('ready', async () => {
|
|
||||||
this.worker.listenIndexHTML()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ISOTerminal.prototype.runHTML = function(html){
|
})
|
||||||
let $scene = document.querySelector("a-scene")
|
|
||||||
let $root = document.querySelector("a-entity#root")
|
|
||||||
if( !$root ){
|
|
||||||
$root = document.createElement("a-entity")
|
|
||||||
$root.id = "root"
|
|
||||||
$scene.appendChild($root)
|
|
||||||
}
|
|
||||||
console.log(html)
|
|
||||||
$root.innerHTML = html
|
|
||||||
}
|
|
||||||
|
|
||||||
|
ISOTerminal.prototype.runHTML = function(html){
|
||||||
|
let $scene = document.querySelector("a-scene")
|
||||||
|
let $root = document.querySelector("a-entity#root")
|
||||||
|
if( !$root ){
|
||||||
|
$root = document.createElement("a-entity")
|
||||||
|
$root.id = "root"
|
||||||
|
$scene.appendChild($root)
|
||||||
|
}
|
||||||
|
$root.innerHTML = html
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,32 @@
|
||||||
if( typeof emulator != 'undefined' ){
|
ISOTerminal.addEventListener('init', function(){
|
||||||
// inside worker-thread
|
|
||||||
|
this.addEventListener('emulator-started', function(e){
|
||||||
|
|
||||||
|
const emulator = this.emulator
|
||||||
|
|
||||||
|
// unix to js device
|
||||||
|
this.readFromPipe( '/mnt/index.js', async (data) => {
|
||||||
|
const buf = await emulator.read_file("index.js")
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
const js = decoder.decode(buf)
|
||||||
|
try{
|
||||||
|
this.runJavascript(js)
|
||||||
|
}catch(e){
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// unix to js device
|
|
||||||
this.emulator.readFromPipe( 'dev/browser/js', async (data) => {
|
|
||||||
const buf = await emulator.read_file("dev/browser/js")
|
|
||||||
const decoder = new TextDecoder('utf-8');
|
|
||||||
const js = decoder.decode(buf).replace(/^#!\/bin\/js/,'') // remove leftover shebangs if any
|
|
||||||
try{
|
|
||||||
this.postMessage({event:'runJavascript',data:[js]})
|
|
||||||
}catch(e){
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
}else{
|
ISOTerminal.prototype.runJavascript = function(js){
|
||||||
// inside browser-thread
|
let $root = document.querySelector("script#root")
|
||||||
|
if( !$root ){
|
||||||
ISOTerminal.prototype.runJavascript = function(js){
|
$root = document.createElement("script")
|
||||||
let $root = document.querySelector("script#root")
|
$root.id = "root"
|
||||||
if( !$root ){
|
document.body.appendChild($root)
|
||||||
$root = document.createElement("script")
|
|
||||||
$root.id = "root"
|
|
||||||
document.body.appendChild($root)
|
|
||||||
}
|
|
||||||
$root.innerHTML = js
|
|
||||||
}
|
}
|
||||||
|
$root.innerHTML = js
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,29 @@
|
||||||
if( typeof emulator != 'undefined' ){
|
ISOTerminal.addEventListener('init', function(){
|
||||||
// inside worker-thread
|
|
||||||
|
|
||||||
// unix to js device
|
this.addEventListener('emulator-started', function(e){
|
||||||
emulator.readFromPipe( 'dev/browser/js', async (data) => {
|
|
||||||
const convert = ISOTerminal.prototype.convert
|
const emulator = this.emulator
|
||||||
const buf = await this.emulator.read_file("dev/browser/js")
|
|
||||||
const script = convert.Uint8ArrayToString(buf)
|
// unix to js device
|
||||||
let PID=null
|
this.readFromPipe( '/mnt/js', async (data) => {
|
||||||
try{
|
const buf = await emulator.read_file("js")
|
||||||
if( script.match(/^PID/) ){
|
const script = this.convert.Uint8ArrayToString(buf)
|
||||||
PID = script.match(/^PID=([0-9]+);/)[1]
|
let PID="?"
|
||||||
|
try{
|
||||||
|
if( script.match(/^PID/) ){
|
||||||
|
PID = script.match(/^PID=([0-9]+);/)[1]
|
||||||
|
}
|
||||||
|
let res = (new Function(`${script}`))()
|
||||||
|
if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2)
|
||||||
|
// write output to 9p with PID as filename
|
||||||
|
// *FIXME* not flexible / robust
|
||||||
|
emulator.create_file(PID, this.convert.toUint8Array(res) )
|
||||||
|
}catch(e){
|
||||||
|
console.error(e)
|
||||||
}
|
}
|
||||||
this.postMessage({event:'javascript-eval',data:{script,PID}})
|
})
|
||||||
}catch(e){
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
}else{
|
})
|
||||||
// inside browser-thread
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* here we're going to execute javascript in the browser,
|
|
||||||
* both a terrible and great idea depending on a corporate vs personal computing angle.
|
|
||||||
*
|
|
||||||
* if a function-string gets evaluated, and it returns a promise..then we assume async.
|
|
||||||
*/
|
|
||||||
ISOTerminal.addEventListener('javascript-eval', async function(e){
|
|
||||||
const {script,PID} = e.detail
|
|
||||||
let res;
|
|
||||||
let error = false;
|
|
||||||
|
|
||||||
const output = (res,PID) => {
|
|
||||||
if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2)
|
|
||||||
// update output to 9p with PID as filename (in /mnt/run)
|
|
||||||
if( PID ){
|
|
||||||
this.worker.update_file(`run/${PID}.exit`, error ? "1" : "0")
|
|
||||||
this.worker.update_file(`run/${PID}`, this.convert.toUint8Array(res) )
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try{
|
|
||||||
let f = new Function(`${script}`);
|
|
||||||
res = f();
|
|
||||||
if( res && typeof res.then == 'function' ){ // if we got a promise
|
|
||||||
res.then( (res) => output(res,PID) )
|
|
||||||
res.catch( (e) => output(e,PID) )
|
|
||||||
}else{ // normal sync function
|
|
||||||
output(res,PID)
|
|
||||||
}
|
|
||||||
}catch(err){
|
|
||||||
error = true
|
|
||||||
console.error(err)
|
|
||||||
console.dir(err)
|
|
||||||
res = "error: "+err.toString()
|
|
||||||
|
|
||||||
// try to figure out line *FIXME*
|
|
||||||
let line = err.stack.split("\n").find(e => e.includes("<anonymous>:") || e.includes("Function:"));
|
|
||||||
if( line ){
|
|
||||||
let lineIndex = (line.includes("<anonymous>:") && line.indexOf("<anonymous>:") + "<anonymous>:".length) || (line.includes("Function:") && line.indexOf("Function:") + "Function:".length);
|
|
||||||
let lnr = +line.substring(lineIndex, lineIndex + 1) - 2
|
|
||||||
res += script.split("\n")[lnr-1]
|
|
||||||
}else console.dir(script)
|
|
||||||
console.error(res)
|
|
||||||
output(res,PID)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,109 +1,56 @@
|
||||||
ISOTerminal.prototype.redirectConsole = function(handler){
|
ISOTerminal.prototype.redirectConsole = function(handler){
|
||||||
const log = console._log = console.log;
|
const log = console.log;
|
||||||
const dir = console._dir = console.dir;
|
const dir = console.dir;
|
||||||
const err = console._error = console.error;
|
const err = console.error;
|
||||||
const warn = console._warn = console.warn;
|
const warn = console.warn;
|
||||||
const addLineFeeds = (str) => typeof str == 'string' ? str.replace(/\n/g,"\r\n") : str
|
|
||||||
|
|
||||||
console.log = (...args)=>{
|
console.log = (...args)=>{
|
||||||
const textArg = args[0];
|
const textArg = args[0];
|
||||||
handler( addLineFeeds(textArg) );
|
handler(textArg+'\n');
|
||||||
log.apply(log, args);
|
log.apply(log, args);
|
||||||
};
|
};
|
||||||
console.error = (...args)=>{
|
console.error = (...args)=>{
|
||||||
const textArg = args[0]
|
const textArg = args[0].message?args[0].message:args[0];
|
||||||
handler( addLineFeeds(textArg), '\x1b[31merror\x1b[0m');
|
handler( textArg+'\n', '\x1b[31merror\x1b[0m');
|
||||||
err.apply(log, args);
|
err.apply(log, args);
|
||||||
};
|
};
|
||||||
console.dir = (...args)=>{
|
console.dir = (...args)=>{
|
||||||
const textArg = args[0]
|
const textArg = args[0].message?args[0].message:args[0];
|
||||||
let str = JSON.stringify(textArg,null,2)+'\n'
|
handler( JSON.stringify(textArg,null,2)+'\n');
|
||||||
handler( addLineFeeds(str) )
|
|
||||||
dir.apply(log, args);
|
dir.apply(log, args);
|
||||||
};
|
};
|
||||||
console.warn = (...args)=>{
|
console.warn = (...args)=>{
|
||||||
const textArg = args[0]
|
const textArg = args[0].message?args[0].message:args[0];
|
||||||
handler( addLineFeeds(textArg),'\x1b[38;5;208mwarn\x1b[0m');
|
handler(textArg+'\n','\x1b[38;5;208mwarn\x1b[0m');
|
||||||
err.apply(log, args);
|
err.apply(log, args);
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ISOTerminal.prototype.enableConsole = function(opts){
|
ISOTerminal.addEventListener('emulator-started', function(){
|
||||||
|
let emulator = this.emulator
|
||||||
opts = opts || {stdout:false}
|
|
||||||
|
|
||||||
this.redirectConsole( (str,prefix) => {
|
this.redirectConsole( (str,prefix) => {
|
||||||
let _str = typeof str == 'string' ? str : JSON.stringify(str)
|
let finalStr = ""
|
||||||
let finalStr = "";
|
prefix = prefix ? prefix+' ' : ' '
|
||||||
prefix = prefix ? prefix+' ' : ''
|
str.trim().split("\n").map( (line) => {
|
||||||
String(_str).trim().split("\n").map( (line) => {
|
finalStr += '\x1b[38;5;165m/dev/browser: \x1b[0m'+prefix+line+'\n'
|
||||||
finalStr += `${opts.stdout ? '' : "\x1b[38;5;165m/dev/browser: \x1b[0m"}`+prefix+line+'\n'
|
|
||||||
})
|
})
|
||||||
if( opts.stdout ){
|
emulator.fs9p.append_file( "console", finalStr )
|
||||||
this.emit('serial-output-string', finalStr, "worker")
|
|
||||||
}else this.emit('append_file', ["/dev/browser/console",finalStr])
|
|
||||||
this.lastStr = finalStr
|
|
||||||
})
|
})
|
||||||
|
|
||||||
window.addEventListener('error', function(event) {
|
window.addEventListener('error', function(event) {
|
||||||
if( event.filename ){
|
console.error(event.filename+":"+event.lineno+":"+event.colno)
|
||||||
console.error(event.filename+":"+event.lineno+":"+event.colno)
|
console.error(event.message);
|
||||||
console.error(event.message);
|
console.error(event.error);
|
||||||
console.error(event.error);
|
|
||||||
}else console.error(event)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('unhandledrejection', function(event) {
|
window.addEventListener('unhandledrejection', function(event) {
|
||||||
console.error(event);
|
console.error('Unhandled promise rejection:', event.reason);
|
||||||
});
|
});
|
||||||
|
|
||||||
if( opts.stdout ){
|
// enable/disable logging file (echo 1 > mnt/console.tty)
|
||||||
window.menu = () => this.bootMenu()
|
this.readFromPipe( '/mnt/console.tty', (data) => {
|
||||||
this.emit('serial-output-string', "\n\n\r☑ initialized javascript console\n");
|
emulator.log_to_tty = ( String(data).trim() == '1')
|
||||||
this.emit('serial-output-string', "\r☑ please use these functions to print:\n");
|
|
||||||
this.emit('serial-output-string', "\r└☑ console.log(\"foo\")\n");
|
|
||||||
this.emit('serial-output-string', "\r└☑ console.warn(\"foo\")\n");
|
|
||||||
this.emit('serial-output-string', "\r└☑ console.dir({foo:12})\n");
|
|
||||||
this.emit('serial-output-string', "\r└☑ console.error(\"foo\")\n");
|
|
||||||
this.emit('serial-output-string', "\r\n");
|
|
||||||
this.emit('serial-output-string', "\rtype 'menu()' to return to mainmenu");
|
|
||||||
this.emit('serial-output-string', "\r\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.addEventListener('emulator-started', function(){
|
|
||||||
this.enableConsole()
|
|
||||||
})
|
|
||||||
|
|
||||||
ISOTerminal.addEventListener('init', function(){
|
|
||||||
this.addEventListener('enable-console', function(opts){
|
|
||||||
this.enableConsole(opts.detail)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// REPL: jsconsole
|
|
||||||
ISOTerminal.prototype.boot.menu.push(
|
|
||||||
{
|
|
||||||
key: "j",
|
|
||||||
title: (opts) => "just give me an javascript-console in WebXR instantly",
|
|
||||||
init: function(){
|
|
||||||
this.prompt = "\r[36m>[0m "
|
|
||||||
this.emit('enable-console',{stdout:true})
|
|
||||||
this.emit('status',"javascript console")
|
|
||||||
this.console = ""
|
|
||||||
setTimeout( () => {
|
|
||||||
this.send(this.prompt)
|
|
||||||
}, 100 )
|
|
||||||
},
|
|
||||||
keyHandler: function(ch){
|
|
||||||
this.send(ch)
|
|
||||||
},
|
|
||||||
cmdHandler: function(cmd){
|
|
||||||
this.send("\n\r")
|
|
||||||
eval(cmd)
|
|
||||||
setTimeout( () => this.send(this.prompt) ,10) // because worker vs terminal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
if( typeof emulator != 'undefined' ){
|
|
||||||
// inside worker-thread
|
|
||||||
|
|
||||||
}else{
|
|
||||||
// inside browser-thread
|
|
||||||
//
|
|
||||||
ISOTerminal.prototype.pasteWriteFile = async function(data,type,filename){
|
|
||||||
this.pasteWriteFile.fileCount = this.pasteWriteFile.fileCount || 0
|
|
||||||
const file = `clipboard/`+ ( filename || `user-paste-${this.pasteWriteFile.fileCount}`)
|
|
||||||
await this.worker.create_file(file, data )
|
|
||||||
// run the xrsh hook
|
|
||||||
this.hook("clipboard", [ `/mnt/${file}`, type ] )
|
|
||||||
console.log("clipboard paste: /mnt/"+file)
|
|
||||||
this.pasteWriteFile.fileCount += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.pasteFile = async function(data){
|
|
||||||
const {type,item,pastedText} = data
|
|
||||||
if( pastedText){
|
|
||||||
// the terminal handles this (pastes text)
|
|
||||||
// this.pasteWriteFile( this.convert.toUint8Array(pastedText) ,type, null, true)
|
|
||||||
}else{
|
|
||||||
const file = item.getAsFile();
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const arr = new Uint8Array(e.target.result)
|
|
||||||
this.pasteWriteFile( arr, type, file.name ); // or use readAsDataURL for images
|
|
||||||
};
|
|
||||||
reader.readAsArrayBuffer(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.pasteInit = function(opts){
|
|
||||||
// bind upload input
|
|
||||||
const {instance, aEntity} = opts
|
|
||||||
const el = aEntity.el.dom.querySelector('#pastedrop') // upload input
|
|
||||||
el.addEventListener('change', (e) => {
|
|
||||||
const file = el.files[0];
|
|
||||||
const item = {...file, getAsFile: () => file } // pasteFile-event works with File objets
|
|
||||||
const data = { item, type: file.type }
|
|
||||||
this.emit( 'pasteFile', data, "worker" ) // impersonate as worker (as worker cannot handle File objet)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.addEventListener('init', function(){
|
|
||||||
this.addEventListener('term_init', (opts) => this.pasteInit(opts.detail) )
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
ISOTerminal.prototype.enableRemoteKeyboard = function(opts){
|
|
||||||
|
|
||||||
let service = {
|
|
||||||
ready: false,
|
|
||||||
reset: (service) => {
|
|
||||||
service.ip = localStorage.getItem("keyboardIP") || ""
|
|
||||||
service.ip = service.ip.trim()
|
|
||||||
service.state = "need-ip"
|
|
||||||
service.attempts = 0
|
|
||||||
},
|
|
||||||
init: function init( mainmenu ){
|
|
||||||
this.emit('status',"")
|
|
||||||
this.emit('enable-console',{stdout:true})
|
|
||||||
service.reset(service)
|
|
||||||
setTimeout( () => {
|
|
||||||
const clearScreen = "[1;1H[2J\r"
|
|
||||||
this.send(clearScreen);
|
|
||||||
this.send(`\n\rfor instructions\n\rsee ${document.location.origin}/index.html#Remote%20keyboard\n\n\r`)
|
|
||||||
this.send("enter 'm' for mainmenu\n\n\r")
|
|
||||||
this.send("[36mkeyboard ip-address> [0m")
|
|
||||||
// autofill ip
|
|
||||||
if( service.ip ){
|
|
||||||
for( let i = 0; i < service.ip.length; i++ ) this.send(service.ip.charAt(i))
|
|
||||||
}
|
|
||||||
}, 100 )
|
|
||||||
},
|
|
||||||
server: (term) => {
|
|
||||||
try{
|
|
||||||
service.addr = `wss://${service.ip}:9090/`;
|
|
||||||
service.ws = new WebSocket(service.addr)
|
|
||||||
service.ws.addEventListener("error", console.error )
|
|
||||||
service.ws.addEventListener("open", (e) => {
|
|
||||||
if( service.state == 'listening' ){
|
|
||||||
this.send(`\n\rconnected! \\o/\n\r`)
|
|
||||||
this.bootMenu()
|
|
||||||
}
|
|
||||||
service.state = 'receiving'
|
|
||||||
})
|
|
||||||
service.ws.addEventListener("close", () => {
|
|
||||||
service.attempts += 1
|
|
||||||
if( service.attempts > 3 && service.state == 'listening'){
|
|
||||||
service.reset(service)
|
|
||||||
this.send(`\n\roops..I did not detect any connection :/\n\r`)
|
|
||||||
localStorage.setItem("keyboardIP","") // reset ip
|
|
||||||
this.bootMenu()
|
|
||||||
}else setTimeout( () => service.server(term), 1000 ) // retry connection
|
|
||||||
}) // retry on EOF
|
|
||||||
service.ws.onmessage = function(event) {
|
|
||||||
if( !event.data ) return
|
|
||||||
event.data.arrayBuffer().then( (buf) => {
|
|
||||||
const arr = new Uint8Array(buf)
|
|
||||||
let string = Array.from(arr, byte => String.fromCharCode(byte)).join('')
|
|
||||||
term.term.handler(string)
|
|
||||||
service.state = 'receiving'
|
|
||||||
localStorage.setItem("keyboardIP",service.ip) // save ip for later
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}catch(e){
|
|
||||||
console.error(e)
|
|
||||||
service.reset(service)
|
|
||||||
localStorage.setItem("keyboardIP","") // reset ip
|
|
||||||
this.bootMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize REPL
|
|
||||||
ISOTerminal.prototype.boot.menu.push(
|
|
||||||
{
|
|
||||||
key: "k",
|
|
||||||
title: (opts) => "connect a remote keyboard",
|
|
||||||
init: service.init,
|
|
||||||
keyHandler: function(ch){
|
|
||||||
this.send(ch)
|
|
||||||
if( service.state == 'need-ip'){
|
|
||||||
if( ch == 'm'){
|
|
||||||
this.send("\n\r")
|
|
||||||
this.bootMenu()
|
|
||||||
}else if( ch == '\n' || ch == '\r'){
|
|
||||||
service.server(this)
|
|
||||||
service.state = 'listening'
|
|
||||||
this.send("\n\rconnecting to "+service.addr)
|
|
||||||
}else{
|
|
||||||
service.ip = ch == '\b' ? service.ip.substr(0,this.service.ip.length-1)
|
|
||||||
: service.ip + ch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.addEventListener('init', function(){
|
|
||||||
this.enableRemoteKeyboard()
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
@ -1,365 +0,0 @@
|
||||||
/* Remote storage feature
|
|
||||||
*
|
|
||||||
* NOTE: this feature extends the localstorage functions.
|
|
||||||
* this file can be excluded without crippling the
|
|
||||||
* core localstorage mechanism.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// see https://remotestorage.io/rs.js/docs/data-modules/
|
|
||||||
ISOTerminal.prototype.remoteStorageModule = {
|
|
||||||
name: 'xrsh',
|
|
||||||
builder: function(privateClient, publicClient) {
|
|
||||||
return {
|
|
||||||
exports: {
|
|
||||||
add: function(data,opts) {
|
|
||||||
if( !data || !opts.filename || !opts.mimetype) throw 'webxr.add() needs filedata + filename + mimetype'
|
|
||||||
const client = opts.client = opts.public ? publicClient : privateClient;
|
|
||||||
return client.storeFile(opts.mimetype, opts.filename,data)
|
|
||||||
},
|
|
||||||
|
|
||||||
getListing: function(a,opts){
|
|
||||||
opts = opts || {}
|
|
||||||
const client = opts.public ? publicClient : privateClient;
|
|
||||||
return client.getListing(a)
|
|
||||||
},
|
|
||||||
|
|
||||||
getFile: function(file,opts){
|
|
||||||
opts = opts || {}
|
|
||||||
const client = opts.public ? publicClient : privateClient;
|
|
||||||
return client.getFile(file)
|
|
||||||
},
|
|
||||||
|
|
||||||
remove: function(file,opts){
|
|
||||||
opts = opts || {}
|
|
||||||
const client = opts.public ? publicClient : privateClient;
|
|
||||||
return client.remove(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// this is the HTML which we append to the remotestorage widget:
|
|
||||||
// https://remotestorage.io/rs.js/docs/getting-started/connect-widget.html
|
|
||||||
const _rs_widget_html = `
|
|
||||||
<div class="form" id="remote">
|
|
||||||
<select class="remote" id="states"></select>
|
|
||||||
<button id="save">save</button>
|
|
||||||
|
|
||||||
<a class="btn remote" href="https://inspektor.5apps.com/?path=xrsh%2F" style="background:#888" target="_blank">filemanager</a>
|
|
||||||
<div id="result"></div>
|
|
||||||
</div>
|
|
||||||
<div class="form" id="local" style="position:relative">
|
|
||||||
<hr>
|
|
||||||
<div class="header">WebBrowser session:</div>
|
|
||||||
<div style="text-align:right">
|
|
||||||
<button id="saveLocal">save</button>
|
|
||||||
<button id="restoreLocal">restore</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- disabled for now -->
|
|
||||||
<div class="form" id="local" style="position:relative; display:none">
|
|
||||||
<hr>
|
|
||||||
<div class="header">File session:</div>
|
|
||||||
<div style="text-align:right">
|
|
||||||
<button id="saveLocalFile">save file</button>
|
|
||||||
<button id="restoreLocalFile">import file</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style type="text/css">
|
|
||||||
#remotestorage-widget{
|
|
||||||
position:fixed;
|
|
||||||
top:0;
|
|
||||||
right:0;
|
|
||||||
}
|
|
||||||
#remotestorage-widget h1,
|
|
||||||
#remotestorage-widget h2,
|
|
||||||
#remotestorage-widget h3,
|
|
||||||
#remotestorage-widget h4,
|
|
||||||
#remotestorage-widget h5{
|
|
||||||
color:#222;
|
|
||||||
}
|
|
||||||
#remotestorage-widget .rs-widget{
|
|
||||||
margin:25px;
|
|
||||||
}
|
|
||||||
#remotestorage-widget .rs-closed .form{
|
|
||||||
display:none !important;
|
|
||||||
}
|
|
||||||
#remotestorage-widget .form select#states{
|
|
||||||
padding: 5px;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-right: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
#remotestorage-widget .form #result{
|
|
||||||
padding-top:10px;
|
|
||||||
}
|
|
||||||
#remotestorage-widget .form select,
|
|
||||||
#remotestorage-widget .form button,
|
|
||||||
#remotestorage-widget .form a{
|
|
||||||
height:35px;
|
|
||||||
margin-bottom:0;
|
|
||||||
}
|
|
||||||
#remotestorage-widget *{
|
|
||||||
font-size:15px;
|
|
||||||
}
|
|
||||||
#remotestorage-widget div.header{
|
|
||||||
position:absolute;
|
|
||||||
display:inline;
|
|
||||||
top:15px;
|
|
||||||
left:0;
|
|
||||||
font-weight:bold;
|
|
||||||
}
|
|
||||||
#remotestorage-widget .form .btn {
|
|
||||||
text-decoration: none;
|
|
||||||
color: white;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
display: inline-block;
|
|
||||||
line-height: 15px;
|
|
||||||
}
|
|
||||||
#remotestorage-widget .form{
|
|
||||||
margin-top:10px;
|
|
||||||
}
|
|
||||||
#remotestorage-widget .btn:hover,
|
|
||||||
#remotestorage-widget button:hover {
|
|
||||||
background:#CCC;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
`
|
|
||||||
|
|
||||||
const widgetForm = function(opts){
|
|
||||||
let el = document.querySelector("#remotestorage-widget select#states")
|
|
||||||
const $widget = document.querySelector("#remotestorage-widget .rs-widget")
|
|
||||||
const $result = document.querySelector("#remotestorage-widget #result")
|
|
||||||
if( !el ){
|
|
||||||
|
|
||||||
let div = document.createElement('div')
|
|
||||||
div.innerHTML = _rs_widget_html
|
|
||||||
$widget.appendChild(div)
|
|
||||||
$widget.querySelector("h3").innerText = "Click here to connect your storage"
|
|
||||||
|
|
||||||
el = $widget.querySelector("select#states")
|
|
||||||
el.addEventListener('change', () => {
|
|
||||||
if( el.options.selectedIndex == 0 ) return; // ignore default option
|
|
||||||
this.remoteStorage.widget.filename = el.value
|
|
||||||
opts.status = "loading..."
|
|
||||||
$widget.classList.add(["blink"])
|
|
||||||
this.remoteStorage.xrsh.getFile(el.value)
|
|
||||||
.then( (data,err) => {
|
|
||||||
this.restore({remotestorage:true, data:data.data})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.setupStorageListeners($widget,opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateStorageForm(el,$widget,$result,opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.remoteStorageWidget = function(){
|
|
||||||
if( this.remoteStorageWidget.installed ) return // only do once
|
|
||||||
let apis = {
|
|
||||||
//dropbox: "ce876ce",
|
|
||||||
//googledrive: "c3983c3"
|
|
||||||
}
|
|
||||||
const modules = [ this.remoteStorageModule ]
|
|
||||||
const remoteStorage = this.remoteStorage = new RemoteStorage({logging: true, modules })
|
|
||||||
remoteStorage.setApiKeys(apis)
|
|
||||||
|
|
||||||
remoteStorage.on('not-connected', (e) => {
|
|
||||||
this.remoteStorage.connected = false
|
|
||||||
this.remoteStorage.widget.form({show:false})
|
|
||||||
})
|
|
||||||
remoteStorage.on('ready', (e) => { })
|
|
||||||
remoteStorage.on('connected', (e) => {
|
|
||||||
this.remoteStorage.connected = true
|
|
||||||
this.remoteStorage.widget.form({update:true,show:true})
|
|
||||||
})
|
|
||||||
|
|
||||||
remoteStorage.access.claim( `xrsh`, 'rw'); // our data dir
|
|
||||||
remoteStorage.caching.enable( `/xrsh/` ) // local-first, remotestorage-second
|
|
||||||
remoteStorage.caching.enable( `/public/xrsh/` ) // local-first, remotestorage-second
|
|
||||||
|
|
||||||
// create widget
|
|
||||||
let opts = {}
|
|
||||||
opts.modalBackdrop = false
|
|
||||||
opts.leaveOpen = false
|
|
||||||
const widget = remoteStorage.widget = new window.Widget(remoteStorage, opts)
|
|
||||||
widget.attach();
|
|
||||||
widget.form = widgetForm.bind(this)
|
|
||||||
|
|
||||||
this.remoteStorageWidget.installed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.setupStorageListeners = function($widget,opts){
|
|
||||||
|
|
||||||
this.addEventListener("ready", () => {
|
|
||||||
opts.loading = false
|
|
||||||
opts.status = "session loaded"
|
|
||||||
})
|
|
||||||
|
|
||||||
// setup events
|
|
||||||
$widget.querySelector("button#save").addEventListener("click", () => {
|
|
||||||
this.save({remotestorage:true})
|
|
||||||
})
|
|
||||||
$widget.querySelector("button#saveLocal").addEventListener("click", async () => {
|
|
||||||
if( confirm("save current session?") ){
|
|
||||||
$widget.classList.add(['blink'])
|
|
||||||
await this.worker.save_state()
|
|
||||||
$widget.classList.remove(['blink'])
|
|
||||||
alert("succesfully saved session")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
$widget.querySelector("button#restoreLocal").addEventListener("click", async () => {
|
|
||||||
this.restore({localstorage:true})
|
|
||||||
})
|
|
||||||
$widget.querySelector("button#saveLocalFile").addEventListener("click", () => {
|
|
||||||
alert("saveFile")
|
|
||||||
})
|
|
||||||
$widget.querySelector("button#restoreLocalFile").addEventListener("click", () => {
|
|
||||||
alert("saveFile")
|
|
||||||
})
|
|
||||||
$widget.addEventListener("click", () => this.remoteStorage.widget.open() )
|
|
||||||
this.addEventListener("restored", () => this.remoteStorage.widget.form({loading:false, status:"session restored"}) )
|
|
||||||
}
|
|
||||||
|
|
||||||
ISOTerminal.prototype.updateStorageForm = function(el,$widget,$result,opts){
|
|
||||||
|
|
||||||
if( typeof opts.show != 'undefined' ){
|
|
||||||
$widget.querySelector('.form#remote').style.display = opts.show ? 'block' : 'none'
|
|
||||||
}
|
|
||||||
|
|
||||||
if( typeof opts.status != 'undefined'){
|
|
||||||
$result.innerText = opts.status
|
|
||||||
}
|
|
||||||
|
|
||||||
if( opts.public ){
|
|
||||||
const link = this.remoteStorage.remote.href + '/public/xrsh/' + this.remoteStorage.widget.filename
|
|
||||||
const linkWebView = document.location.href.replace(/(\?|#).*/,'') + `#1&img=${link}`
|
|
||||||
$result.innerHTML += `<br><a class="btn" href="${linkWebView}" target="_blank">public weblink</a>`
|
|
||||||
}
|
|
||||||
|
|
||||||
if( typeof opts.loading != 'undefined' ){
|
|
||||||
$widget.classList[ opts.loading ? 'add' : 'remove' ](["blink"])
|
|
||||||
}
|
|
||||||
|
|
||||||
if( opts.update ){
|
|
||||||
|
|
||||||
const createOption = (opt) => {
|
|
||||||
let option = document.createElement("option")
|
|
||||||
option.value = opt.value
|
|
||||||
option.innerText = opt.name || opt.value
|
|
||||||
el.appendChild(option)
|
|
||||||
}
|
|
||||||
|
|
||||||
el.innerHTML = ''
|
|
||||||
createOption({value:"-- snapshots --"})
|
|
||||||
this.remoteStorage.xrsh.getListing()
|
|
||||||
.then( (data,err) => {
|
|
||||||
for( let file in data ){
|
|
||||||
createOption({value:file})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const requireFiles = () => AFRAME.utils.require({remotestorageWidget:"assets/aframe-remotestorage.min.js"})
|
|
||||||
const getForage = () => localforage.setDriver([
|
|
||||||
localforage.INDEXEDDB,
|
|
||||||
localforage.WEBSQL,
|
|
||||||
localforage.LOCALSTORAGE
|
|
||||||
])
|
|
||||||
|
|
||||||
// we extend the autorestore feature at the init-event
|
|
||||||
ISOTerminal.addEventListener('init', function(e){
|
|
||||||
|
|
||||||
const decorateSave = () => {
|
|
||||||
|
|
||||||
let localSave = this.save
|
|
||||||
|
|
||||||
this.save = async (opts) => {
|
|
||||||
requireFiles()
|
|
||||||
.then( getForage )
|
|
||||||
.then( async () => {
|
|
||||||
|
|
||||||
if( opts.localstorage ){
|
|
||||||
this.save.localstorage(opts) // default behaviour
|
|
||||||
}
|
|
||||||
|
|
||||||
this.remoteStorageWidget()
|
|
||||||
|
|
||||||
if( opts.remotestorage ){
|
|
||||||
this.remoteStorage.widget.open() // force open dialog
|
|
||||||
|
|
||||||
let filename = String(this.remoteStorage.widget.filename || "snapshot").replace(/\.bin/,'')
|
|
||||||
filename = prompt("please name your snapshot:", filename )
|
|
||||||
filename = filename.replace(/\.bin*/,'') + '.bin'
|
|
||||||
|
|
||||||
// make it public or not (disabled for now as it does not work flawlessly)
|
|
||||||
const public = false // confirm('create a public link?')
|
|
||||||
this.remoteStorage.widget.filename = filename
|
|
||||||
|
|
||||||
this.remoteStorage.widget.form({loading:true,status: "saving.."})
|
|
||||||
|
|
||||||
await this.worker.save_state()
|
|
||||||
localforage.getItem("state", async (err,stateBase64) => {
|
|
||||||
this.remoteStorage.xrsh.add( stateBase64, {filename, mimetype: 'application/x-v86-base64', public})
|
|
||||||
.then( () => {
|
|
||||||
console.log("saved to remotestorage")
|
|
||||||
setTimeout(
|
|
||||||
() => this.remoteStorage.widget.form({update:true, loading:false, status:"saved succesfully", public})
|
|
||||||
,500 )
|
|
||||||
})
|
|
||||||
.catch( console.error )
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.save.localstorage = localSave
|
|
||||||
}
|
|
||||||
|
|
||||||
const decorateRestore = () => {
|
|
||||||
|
|
||||||
let localRestore = this.restore
|
|
||||||
|
|
||||||
this.restore = async (opts) => {
|
|
||||||
requireFiles()
|
|
||||||
.then( getForage )
|
|
||||||
.then( async () => {
|
|
||||||
|
|
||||||
document.querySelector("#remotestorage-widget .rs-widget").classList.add(['blink'])
|
|
||||||
if( opts.localstorage ){
|
|
||||||
this.restore.localstorage.apply(this,opts) // default behaviour
|
|
||||||
}
|
|
||||||
if( opts.remotestorage ){
|
|
||||||
localforage.setItem( "state", opts.data )
|
|
||||||
.then( () => this.restore.localstorage.call(this,opts) ) // now trigger default behaviour
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.restore.localstorage = localRestore
|
|
||||||
this.autorestore = this.restore // reroute automapping
|
|
||||||
}
|
|
||||||
|
|
||||||
// decorate save() and restore() of autorestore.js after it's declared (hence the setTimeout)
|
|
||||||
this.addEventListener('autorestore-installed', () => {
|
|
||||||
decorateSave()
|
|
||||||
decorateRestore()
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
// decorate autorestore
|
|
||||||
const autorestoreLocalStorage = ISOTerminal.prototype.autorestore;
|
|
||||||
ISOTerminal.prototype.autorestore = function(data){
|
|
||||||
requireFiles().then( async () => {
|
|
||||||
this.remoteStorageWidget()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
ISOTerminal.addEventListener('init', function(){
|
|
||||||
this.TermInit()
|
|
||||||
})
|
|
||||||
|
|
||||||
ISOTerminal.prototype.TermInit = function(){
|
|
||||||
|
|
||||||
const setupTerm = (opts) => {
|
|
||||||
if( !opts ) return
|
|
||||||
const {instance, aEntity} = opts
|
|
||||||
const el = aEntity.el.dom.querySelector('#term')
|
|
||||||
opts.termOpts = {
|
|
||||||
cols: aEntity.cols,
|
|
||||||
rows: aEntity.rows,
|
|
||||||
el_or_id: el,
|
|
||||||
scrollback: aEntity.rows*3,
|
|
||||||
fontSize: null,
|
|
||||||
//rainbow: [Term.COLOR_MAGENTA, Term.COLOR_CYAN ],
|
|
||||||
isWebXRKeyboard: () => AFRAME.scenes[0].renderer.xr.isPresenting && AFRAME.scenes[0].renderer.xr.getSession().isSystemKeyboardSupported, // naive way
|
|
||||||
//map: {
|
|
||||||
// 'ArrowRight': { ch: false, ctrl: '\x1b\x66' }, // this triggers ash-shell forward-word
|
|
||||||
// 'ArrowLeft': { ch: false, ctrl: '\x1b\x62' } // backward-word
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
// patch Term-class
|
|
||||||
Term.prototype.move_textarea = function(){} /* *TODO* *FIXME* does not work in winbox */
|
|
||||||
|
|
||||||
Term.prototype.keyDownHandler = function(original){
|
|
||||||
return function (e){
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
|
|
||||||
return true; // bubble up to pasteHandler (see pastedrop.js)
|
|
||||||
}
|
|
||||||
original.apply(this,[e])
|
|
||||||
}
|
|
||||||
}( Term.prototype.keyDownHandler )
|
|
||||||
|
|
||||||
this.term = new Term( opts.termOpts )
|
|
||||||
Term.prototype.href = (a) => true
|
|
||||||
this.term.colors = [
|
|
||||||
/* normal */
|
|
||||||
"#000000",
|
|
||||||
"#2FA",
|
|
||||||
"#7700ff",
|
|
||||||
"#555555",
|
|
||||||
"#0000ff",
|
|
||||||
"#aa00aa",
|
|
||||||
"#ff00aa",
|
|
||||||
"#aaaaaa",
|
|
||||||
/* bright */
|
|
||||||
"#555555",
|
|
||||||
"#ff5555",
|
|
||||||
"#2CF",
|
|
||||||
"#aa00ff",
|
|
||||||
"#5555ff",
|
|
||||||
"#ff55ff",
|
|
||||||
"#55ffff",
|
|
||||||
"#ffffff"
|
|
||||||
];
|
|
||||||
this.term.open(el)
|
|
||||||
this.term.el = el
|
|
||||||
|
|
||||||
|
|
||||||
// this is the default REPL
|
|
||||||
// please do not edit or replace this
|
|
||||||
// but instead extend/override ISOTerminal.prototype.boot.menu
|
|
||||||
// as demonstrated in index.html
|
|
||||||
this.term.setKeyHandler( (ch) => {
|
|
||||||
let erase = false
|
|
||||||
const isEnter = ch == '\n' || ch == '\r'
|
|
||||||
if( ch == '\x7F' ){
|
|
||||||
ch = "\b \b" // why does write() not just support \x7F ?
|
|
||||||
erase = true
|
|
||||||
}
|
|
||||||
if( this.boot.menu.selected ){
|
|
||||||
this.boot.menu.selected.keyHandler.call(this,ch)
|
|
||||||
if( isEnter ){
|
|
||||||
if( this.boot.menu.selected.cmdHandler ){
|
|
||||||
this.boot.menu.selected.cmdHandler.call(this,this.lastCmd)
|
|
||||||
}
|
|
||||||
this.lastCmd = ""
|
|
||||||
}
|
|
||||||
}else if( isEnter ){
|
|
||||||
let menuitem = this.boot.menu.find( (m) => m.key == this.lastChar )
|
|
||||||
if( menuitem ){
|
|
||||||
this.boot.menu.selected = menuitem
|
|
||||||
this.lastCmd = ""
|
|
||||||
menuitem.init.call(this, () => {
|
|
||||||
this.term.write("\n\r")
|
|
||||||
this.bootMenu()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
ch.split("").map( (ch) => this.term.write( ch ) )
|
|
||||||
}
|
|
||||||
if( !erase ){
|
|
||||||
this.lastChar = ch
|
|
||||||
this.lastCmd = this.lastCmd ? this.lastCmd + ch : ch
|
|
||||||
}else this.lastCmd = this.lastCmd ? this.lastCmd.substr(0, this.lastCmd.length-1) : ""
|
|
||||||
})
|
|
||||||
|
|
||||||
aEntity.el.addEventListener('focus', () => {
|
|
||||||
let textarea = el.querySelector("textarea")
|
|
||||||
textarea.focus()
|
|
||||||
if( document.activeElement != textarea ) textarea.focus()
|
|
||||||
})
|
|
||||||
aEntity.el.addEventListener('serial-output-string', (e) => {
|
|
||||||
let msg = e.detail
|
|
||||||
this.term.write(msg)
|
|
||||||
})
|
|
||||||
//aEntity.term.emit('initTerm',this)
|
|
||||||
//aEntity.el.addEventListener('focus', () => this.vt100.focus() )
|
|
||||||
|
|
||||||
//aEntity.el.addEventListener('serial-output-string', (e) => {
|
|
||||||
// this.vt100.write(e.detail)
|
|
||||||
//})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addEventListener('term_init', (opts) => setupTerm(opts.detail) )
|
|
||||||
}
|
|
||||||
77
com/isoterminal/feat/xterm.js
Normal file
77
com/isoterminal/feat/xterm.js
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
ISOTerminal.addEventListener('init', function(){
|
||||||
|
if( typeof Terminal != 'undefined' ) this.xtermInit()
|
||||||
|
})
|
||||||
|
|
||||||
|
ISOTerminal.addEventListener('runISO', function(e){
|
||||||
|
let opts = e.detail
|
||||||
|
opts.serial_container_xtermjs = opts.screen_container
|
||||||
|
delete opts.screen_container
|
||||||
|
})
|
||||||
|
|
||||||
|
ISOTerminal.prototype.xtermInit = function(){
|
||||||
|
this.serial_input = 0 // set input to serial line 0
|
||||||
|
let isoterm = this
|
||||||
|
// monkeypatch Xterm (which V86 initializes) so we can add our own constructor args
|
||||||
|
window._Terminal = window.Terminal
|
||||||
|
window.Terminal = function(opts){
|
||||||
|
const term = new window._Terminal({ ...opts,
|
||||||
|
cursorBlink:true,
|
||||||
|
onSelectionChange: function(e){ console.log("selectchange") },
|
||||||
|
letterSpacing: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
term.onSelectionChange( () => {
|
||||||
|
document.execCommand('copy')
|
||||||
|
term.select(0, 0, 0)
|
||||||
|
isoterm.emit('status','copied to clipboard')
|
||||||
|
})
|
||||||
|
return term
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addEventListener('emulator-started', function(){
|
||||||
|
this.emulator.serial_adapter.term.element.querySelector('.xterm-viewport').style.background = 'transparent'
|
||||||
|
// toggle immersive with ESCAPE
|
||||||
|
//document.body.addEventListener('keydown', (e) => e.key == 'Escape' && this.emulator.serial_adapter.term.blur() )
|
||||||
|
})
|
||||||
|
|
||||||
|
const resize = (w,h) => {
|
||||||
|
setTimeout( () => {
|
||||||
|
isoterm.xtermAutoResize(isoterm.emulator.serial_adapter.term, isoterm.instance,-3)
|
||||||
|
},800) // wait for resize anim
|
||||||
|
}
|
||||||
|
isoterm.instance.addEventListener('window.onresize', resize )
|
||||||
|
isoterm.instance.addEventListener('window.onmaximize', resize )
|
||||||
|
}
|
||||||
|
|
||||||
|
ISOTerminal.prototype.xtermAutoResize = function(term,instance,rowoffset){
|
||||||
|
if( !term.element ) return
|
||||||
|
|
||||||
|
const defaultScrollWidth = 24;
|
||||||
|
const MINIMUM_COLS = 2;
|
||||||
|
const MINIMUM_ROWS = 2;
|
||||||
|
|
||||||
|
const dims = term._core._renderService.dimensions;
|
||||||
|
const scrollbarWidth = (term.options.scrollback === 0
|
||||||
|
? 0
|
||||||
|
: (term.options.overviewRuler?.width || defaultScrollWidth ));
|
||||||
|
|
||||||
|
const parentElementStyle = window.getComputedStyle(instance.dom);
|
||||||
|
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||||
|
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||||
|
const elementStyle = window.getComputedStyle(term.element);
|
||||||
|
const elementPadding = {
|
||||||
|
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
||||||
|
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||||
|
right: parseInt(elementStyle.getPropertyValue('padding-right')),
|
||||||
|
left: parseInt(elementStyle.getPropertyValue('padding-left'))
|
||||||
|
};
|
||||||
|
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||||
|
const elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||||
|
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||||
|
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
|
||||||
|
const geometry = {
|
||||||
|
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
|
||||||
|
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
|
||||||
|
};
|
||||||
|
term.resize(geometry.cols, geometry.rows + (rowoffset||0) );
|
||||||
|
}
|
||||||
BIN
com/isoterminal/images/buildroot-bzimage.bin
Normal file
BIN
com/isoterminal/images/buildroot-bzimage.bin
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,134 +0,0 @@
|
||||||
importScripts("libv86.js");
|
|
||||||
importScripts("ISOTerminal.js") // we don't instance it again here (just use its functions)
|
|
||||||
|
|
||||||
this.runISO = async function(opts){
|
|
||||||
this.opts = opts
|
|
||||||
if( opts.debug ) console.dir(opts)
|
|
||||||
|
|
||||||
if( opts.cdrom && !opts.cdrom.url.match(/^http/) ) opts.cdrom.url = "../../"+opts.cdrom.url
|
|
||||||
if( opts.bzimage && !opts.cdrom.url.match(/^http/) ) opts.bzimage.url = "../../"+opts.bzimage.url
|
|
||||||
|
|
||||||
let emulator = this.emulator = new V86(opts);
|
|
||||||
console.log("[worker.js] started emulator")
|
|
||||||
|
|
||||||
// event forwarding
|
|
||||||
emulator.buf0 = {}
|
|
||||||
emulator.buf1 = {}
|
|
||||||
emulator.buf2 = {}
|
|
||||||
|
|
||||||
emulator.add_listener("serial0-output-byte", function(byte){
|
|
||||||
ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping
|
|
||||||
if( !str ) return
|
|
||||||
this.postMessage({event:"serial0-output-string",data:str});
|
|
||||||
}, opts.bufferLatency, emulator.buf0 )
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
emulator.add_listener("serial1-output-byte", function(byte){
|
|
||||||
ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping
|
|
||||||
if( !str ) return
|
|
||||||
this.postMessage({event:"serial1-output-string",data:str});
|
|
||||||
}, opts.bufferLatency, emulator.buf1 )
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
emulator.add_listener("serial2-output-byte", function(byte){
|
|
||||||
ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping
|
|
||||||
if( !str ) return
|
|
||||||
this.postMessage({event:"serial2-output-string",data:str});
|
|
||||||
}, opts.bufferLatency, emulator.buf2 )
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
emulator.add_listener("emulator-started", function(){
|
|
||||||
importScripts("feat/9pfs_utils.js")
|
|
||||||
this.postMessage({event:"emulator-started",data:false});
|
|
||||||
if( opts.img ) this.restoreImage(opts)
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
/*
|
|
||||||
* forward events/functions so non-worker world can reach them
|
|
||||||
*/
|
|
||||||
|
|
||||||
// stripping '/mnt' is needed (the 9p mounted fs does not know about this)
|
|
||||||
const stripMountDir = (arr) => {
|
|
||||||
arr[0] = String(arr[0]).replace(/^\/mnt/,'')
|
|
||||||
return arr
|
|
||||||
}
|
|
||||||
this.create_file = async function(){ return emulator.create_file.apply(emulator, stripMountDir(arguments[0]) ) }
|
|
||||||
this.create_file_from_url = async function(){ return emulator.fs9p.create_file_from_url.apply(emulator, stripMountDir(arguments[0]) ) }
|
|
||||||
this.read_file = async function(){ return emulator.read_file.apply(emulator, stripMountDir(arguments[0]) ) }
|
|
||||||
this.read_file_world = async function(){ return emulator.fs9p.read_file_world.apply(emulator.fs9p, stripMountDir(arguments[0]) ) }
|
|
||||||
this.append_file = async function(){ emulator.fs9p.append_file.apply(emulator.fs9p, stripMountDir(arguments[0])) }
|
|
||||||
this.update_file = async function(){ emulator.fs9p.update_file.apply(emulator.fs9p, stripMountDir(arguments[0])) }
|
|
||||||
|
|
||||||
// filename will be read from 9pfs: "/mnt/"+filename
|
|
||||||
emulator.readFromPipe = function(filename,cb){
|
|
||||||
emulator.add_listener("9p-write-end", async (opts) => {
|
|
||||||
if ( opts[0] == filename.replace(/.*\//,'') ){
|
|
||||||
cb()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
importScripts("feat/javascript.js")
|
|
||||||
importScripts("feat/index.html.js")
|
|
||||||
importScripts("feat/autorestore.js")
|
|
||||||
|
|
||||||
if( opts.overlayfs ) await this.addOverlayFS(opts)
|
|
||||||
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* forward events/functions so non-worker world can reach them
|
|
||||||
*/
|
|
||||||
|
|
||||||
this['serial0-input'] = function(c){ emulator.bus.send( 'serial0-input', c) } // to /dev/ttyS0
|
|
||||||
this['serial1-input'] = function(c){ emulator.bus.send( 'serial1-input', c) } // to /dev/ttyS1
|
|
||||||
this['serial2-input'] = function(c){ emulator.bus.send( 'serial2-input', c) } // to /dev/ttyS2
|
|
||||||
|
|
||||||
this.onmessage = async function(e){
|
|
||||||
let {event,data} = e.data
|
|
||||||
if( this[event] ){
|
|
||||||
if( this.opts?.debug ) console.log(`[worker.js] this.${event}(${JSON.stringify(data).substr(0,60)})`)
|
|
||||||
try{
|
|
||||||
let result = await this[event](data)
|
|
||||||
if( data.promiseId ){ // auto-callback to ISOTerminal.worker.promise(...)
|
|
||||||
this.postMessage({event,data: {...data,result}})
|
|
||||||
}
|
|
||||||
}catch(e){
|
|
||||||
if( data.promiseId ){ // auto-callback to ISOTerminal.worker.promise(...)
|
|
||||||
this.postMessage({event,data: {...data,error:e}})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addOverlayFS = async function(opts){
|
|
||||||
return new Promise( (resolve,reject) => {
|
|
||||||
// OVERLAY FS *FIXME*
|
|
||||||
if( opts.overlayfs ){
|
|
||||||
fetch(opts.overlayfs)
|
|
||||||
.then( (f) => {
|
|
||||||
f.arrayBuffer().then( (buf) => {
|
|
||||||
let filename = opts.overlayfs.match(/\.zip$/) ? 'overlayfs.zip' : '.env'
|
|
||||||
this.emulator.create_file( filename, new Uint8Array(buf) )
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.restoreImage = function(opts){
|
|
||||||
|
|
||||||
this.postMessage({event:"serial0-output-string",data:`\n\r[!] loading session image:\n\r[!] ${opts.img}\n\r[!] please wait...internet speed matters here..\n`})
|
|
||||||
|
|
||||||
fetch( opts.img )
|
|
||||||
.then( (res) => {
|
|
||||||
this.postMessage({event:"serial0-output-string",data:`\r[v] image downloaded..restoring..\n\r`})
|
|
||||||
return res
|
|
||||||
})
|
|
||||||
.then( (res) => res.arrayBuffer() )
|
|
||||||
.then( async (buf) => {
|
|
||||||
await this.emulator.restore_state(buf)
|
|
||||||
this.postMessage({event:"serial0-output-string",data:`[v] restored\n\r`})
|
|
||||||
|
|
||||||
})
|
|
||||||
.catch( console.error )
|
|
||||||
}
|
|
||||||
325
com/launcher.js
325
com/launcher.js
|
|
@ -1,55 +1,20 @@
|
||||||
/**
|
/*
|
||||||
* ## [launcher](com/launcher.js)
|
* ## launcher
|
||||||
*
|
*
|
||||||
* displays app (icons) in 2D and 3D handmenu (enduser can launch desktop-like 'apps')
|
* displays app (icons) for enduser to launch
|
||||||
*
|
|
||||||
* They can be temporary processes (icon disappears after component is removed) or permanent icons
|
|
||||||
* when registered via .register({...})
|
|
||||||
*
|
|
||||||
* ```html
|
|
||||||
* <a-entity launcher>
|
|
||||||
* <a-entity launch="component: helloworld; foo: bar"><a-entity>
|
|
||||||
* </a-entity>
|
|
||||||
*
|
*
|
||||||
|
* ```javascript
|
||||||
|
* <a-entity app="app/launcher.js"/>
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* | property | type | example |
|
* | property | type | example |
|
||||||
* |--------------|--------------------|----------------------------------------------------------------------------------------|
|
* |--------------|--------------------|----------------------------------------------------------------------------------------|
|
||||||
* | `attach` | `selector` | hand or object to attach menu to |
|
* | `registries` | `array` of strings | <a-entity app="app/launcher.js; registers: https://foo.com/index.json, ./index.json"/> |
|
||||||
* | `registries` | `array` of strings | `<a-entity launcher="registers: https://foo.com/index.json, ./index.json"/>` |
|
|
||||||
*
|
*
|
||||||
* | event | target | info |
|
* | event | target | info |
|
||||||
* |--------------|-------------------------------------------------------------------------------------------------------------|
|
* |--------------|-------------------------------------------------------------------------------------------------------------|
|
||||||
* | `launcher` | an app | when pressing an app icon, `launcher` event will be send to the respective app |
|
* | `launcher` | an app | when pressing an app icon, `launcher` event will be send to the respective app |
|
||||||
*
|
*/
|
||||||
* There a multiple ways of letting the launcher know that an app can be launched:
|
|
||||||
*
|
|
||||||
* 1. any AFRAME component with an `launcher`-event + manifest is automatically added:
|
|
||||||
*
|
|
||||||
* ```javascript
|
|
||||||
* AFRAME.registerComponent('foo',{
|
|
||||||
* events:{
|
|
||||||
* launcher: function(){ ...launch something... }
|
|
||||||
* },
|
|
||||||
* manifest:{ // HTML5 manifesto JSON object
|
|
||||||
* // https://www.w3.org/TR/appmanifest/
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* 2. dynamically in javascript
|
|
||||||
*
|
|
||||||
* ```javascript
|
|
||||||
* window.launcher.register({
|
|
||||||
* name:"foo",
|
|
||||||
* icon: "https://.../optional_icon.png"
|
|
||||||
* description: "lorem ipsum",
|
|
||||||
* cb: () => alert("foo")
|
|
||||||
* })
|
|
||||||
* //window.launcher.unregister('foo')
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
AFRAME.registerComponent('launch', { // use this component to auto-launch component
|
AFRAME.registerComponent('launch', { // use this component to auto-launch component
|
||||||
init: function(){
|
init: function(){
|
||||||
|
|
@ -61,6 +26,12 @@ AFRAME.registerComponent('launch', { // use this component to auto-launch compon
|
||||||
|
|
||||||
AFRAME.registerComponent('launcher', {
|
AFRAME.registerComponent('launcher', {
|
||||||
schema: {
|
schema: {
|
||||||
|
attach: { type:"selector"},
|
||||||
|
padding: { type:"number","default":0.15},
|
||||||
|
fingerTip: {type:"selector"},
|
||||||
|
fingerDistance: {type:"number", "default":0.25},
|
||||||
|
rescale: {type:"number","default":0.4},
|
||||||
|
open: { type:"boolean", "default":true},
|
||||||
colors: { type:"array", "default": [
|
colors: { type:"array", "default": [
|
||||||
'#4C73FE',
|
'#4C73FE',
|
||||||
'#554CFE',
|
'#554CFE',
|
||||||
|
|
@ -71,40 +42,60 @@ AFRAME.registerComponent('launcher', {
|
||||||
'#333333',
|
'#333333',
|
||||||
]},
|
]},
|
||||||
paused: { type:"boolean","default":false},
|
paused: { type:"boolean","default":false},
|
||||||
|
cols: { type:"number", "default": 5 }
|
||||||
},
|
},
|
||||||
|
|
||||||
requires:{
|
dependencies:{
|
||||||
dom: "com/dom.js",
|
dom: "com/dom.js"
|
||||||
htmlinxr: "com/html-as-texture-in-xr.js",
|
|
||||||
data2events: "com/data2event.js"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
init: async function () {
|
init: async function () {
|
||||||
this.el.object3D.visible = false;
|
await AFRAME.utils.require(this.dependencies)
|
||||||
await AFRAME.utils.require(this.requires)
|
|
||||||
this.worldPosition = new THREE.Vector3()
|
this.worldPosition = new THREE.Vector3()
|
||||||
|
|
||||||
|
await AFRAME.utils.require({
|
||||||
|
html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
|
||||||
|
dom: "./com/dom.js",
|
||||||
|
data2events: "./com/data2event.js"
|
||||||
|
})
|
||||||
|
|
||||||
this.el.setAttribute("dom","")
|
this.el.setAttribute("dom","")
|
||||||
this.el.setAttribute("pressable","")
|
this.el.setAttribute("noxd","ignore") // hint to XD.js that we manage ourselve concerning 2D/3D switching
|
||||||
this.el.sceneEl.addEventListener('enter-vr', () => this.centerMenu() )
|
|
||||||
this.el.sceneEl.addEventListener('enter-ar', () => this.centerMenu() )
|
|
||||||
this.render()
|
this.render()
|
||||||
|
|
||||||
|
if( this.data.attach ){
|
||||||
|
this.el.object3D.visible = false
|
||||||
|
if( this.isHand(this.data.attach) ){
|
||||||
|
this.data.attach.addEventListener('model-loaded', () => {
|
||||||
|
this.ready = true
|
||||||
|
this.attachMenu()
|
||||||
|
})
|
||||||
|
// add button
|
||||||
|
this.menubutton = this.createMenuButton()
|
||||||
|
this.menubutton.object3D.visible = false
|
||||||
|
this.data.attach.appendChild( this.menubutton )
|
||||||
|
}else this.data.attach.appendChild(this.el)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
isHand: (el) => {
|
||||||
|
return el.getAttributeNames().filter( (n) => n.match(/^hand-tracking/) ? n : null ).length ? true : false
|
||||||
},
|
},
|
||||||
|
|
||||||
dom: {
|
dom: {
|
||||||
scale: 0.8,
|
scale: 3,
|
||||||
events: ['click'],
|
events: ['click'],
|
||||||
html: (me) => `<div class="iconmenu">loading components..</div>`,
|
html: (me) => `<div class="iconmenu">loading components..</div>`,
|
||||||
css: (me) => `
|
css: (me) => `.iconmenu {
|
||||||
.iconmenu {
|
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
width:764px;
|
|
||||||
overflow:hidden;
|
overflow:hidden;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
right: 162px;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
left:20px;
|
left:20px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
@ -125,8 +116,6 @@ AFRAME.registerComponent('launcher', {
|
||||||
border-top: 2px solid #BBB;
|
border-top: 2px solid #BBB;
|
||||||
border-bottom: 2px solid #BBB;
|
border-bottom: 2px solid #BBB;
|
||||||
font-size:18px;
|
font-size:18px;
|
||||||
color: #777;
|
|
||||||
line-height: 7px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconmenu > button:first-child {
|
.iconmenu > button:first-child {
|
||||||
|
|
@ -143,10 +132,6 @@ AFRAME.registerComponent('launcher', {
|
||||||
padding-top:13px;
|
padding-top:13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconmenu > button:only-child{
|
|
||||||
border-radius:5px 5px 5px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconmenu > button > img {
|
.iconmenu > button > img {
|
||||||
transform: translate(0px,-14px);
|
transform: translate(0px,-14px);
|
||||||
opacity:0.5;
|
opacity:0.5;
|
||||||
|
|
@ -161,11 +146,15 @@ AFRAME.registerComponent('launcher', {
|
||||||
},
|
},
|
||||||
|
|
||||||
events:{
|
events:{
|
||||||
|
open: function(){
|
||||||
click: function(e){ },
|
this.preventAccidentalButtonPresses()
|
||||||
|
if( this.data.open ){
|
||||||
DOMready: function(){
|
this.el.setAttribute("animation",`dur: 200; property: scale; from: 0 0 1; to: ${this.data.rescale} ${this.data.rescale} ${this.data.rescale}`)
|
||||||
this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}; faceuser: false`)
|
this.menubutton.object3D.visible = false
|
||||||
|
}else{
|
||||||
|
this.el.setAttribute("animation",`dur: 200; property: scale; from: ${this.data.rescale} ${this.data.rescale} ${this.data.rescale}; to: 0 0 1`)
|
||||||
|
this.menubutton.object3D.visible = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -174,70 +163,136 @@ AFRAME.registerComponent('launcher', {
|
||||||
setTimeout( () => this.data.paused = false, 500 ) // prevent menubutton press collide with animated buttons
|
setTimeout( () => this.data.paused = false, 500 ) // prevent menubutton press collide with animated buttons
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createMenuButton: function(colo){
|
||||||
|
let aentity = document.createElement('a-entity')
|
||||||
|
aentity.setAttribute("mixin","menubutton")
|
||||||
|
aentity.addEventListener('obbcollisionstarted', this.onpress )
|
||||||
|
aentity.addEventListener('obbcollisionended', this.onreleased )
|
||||||
|
return aentity
|
||||||
|
},
|
||||||
|
|
||||||
render: async function(els){
|
render: async function(els){
|
||||||
if( !this.el.dom ) return // too early (dom.js component not ready)
|
if( !this.el.dom ) return // too early (dom.js component not ready)
|
||||||
|
|
||||||
|
let requires = []
|
||||||
|
let i = 0
|
||||||
|
let j = 0
|
||||||
let colors = this.data.colors
|
let colors = this.data.colors
|
||||||
const add2D = (launchCom,manifest) => {
|
const add2D = (launchCom,el,manifest) => {
|
||||||
let btn = document.createElement('button')
|
let btn = document.createElement('button')
|
||||||
let iconDefault = "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMjAuMTcwMiAzTDIwLjE2NjMgMy4wMDQ1M0MyMS43NDU4IDMuMDkwODQgMjMgNC4zOTg5NiAyMyA2VjE4QzIzIDE5LjY1NjkgMjEuNjU2OSAyMSAyMCAyMUg0QzIuMzQzMTUgMjEgMSAxOS42NTY5IDEgMThWNkMxIDQuMzQzMTUgMi4zNDMxNSAzIDQgM0gyMC4xNzAyWk0xMC40NzY0IDVIMTYuNDc2NEwxMy4wODkgOUg3LjA4ODk5TDEwLjQ3NjQgNVpNNS4wODg5OSA5TDguNDc2NDQgNUg0QzMuNDQ3NzIgNSAzIDUuNDQ3NzIgMyA2VjlINS4wODg5OVpNMyAxMVYxOEMzIDE4LjU1MjMgMy40NDc3MiAxOSA0IDE5SDIwQzIwLjU1MjMgMTkgMjEgMTguNTUyMyAyMSAxOFYxMUgzWk0yMSA5VjZDMjEgNS40NDc3MSAyMC41NTIzIDUgMjAgNUgxOC40NzY0TDE1LjA4OSA5SDIxWiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgo8L3N2Zz4="
|
btn.innerHTML = `${ manifest?.icons?.length > 0
|
||||||
let html = manifest?.icons?.length > 0 || !manifest.name ? `<img src='${manifest.icons[0].src || iconDefault}' title='${manifest.name}: ${manifest.description}'/>` : ""
|
? `<img src='${manifest.icons[0].src}' title='${manifest.name}: ${manifest.description}'/>`
|
||||||
if( manifest.name && !html ) html = `${manifest.short_name || manifest.name}`
|
: `${manifest.short_name}`
|
||||||
btn.innerHTML = html
|
}`
|
||||||
|
btn.addEventListener('click', () => el.emit('launcher',{}) )
|
||||||
btn.addEventListener('click', (e) => {
|
|
||||||
launchCom.launcher()
|
|
||||||
e.stopPropagation()
|
|
||||||
// visual feedback to user
|
|
||||||
btn.style.filter = "brightness(0.5)"
|
|
||||||
this.el.components.html.rerender()
|
|
||||||
setTimeout( () => {
|
|
||||||
btn.style.filter = "brightness(1)"
|
|
||||||
this.el.components.html.rerender()
|
|
||||||
}, 500 )
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
this.el.dom.appendChild(btn)
|
this.el.dom.appendChild(btn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const add3D = (launchCom,el,manifest) => {
|
||||||
|
let aentity = document.createElement('a-entity')
|
||||||
|
let atext = document.createElement('a-entity')
|
||||||
|
let padding = this.data.padding
|
||||||
|
if( (i % this.data.cols) == 0 ) j++
|
||||||
|
aentity.setAttribute("mixin","menuitem")
|
||||||
|
aentity.setAttribute("position",`${padding+(i++ % this.data.cols) * padding} ${j*padding} 0`)
|
||||||
|
if( !aentity.getAttribute("material")){
|
||||||
|
aentity.setAttribute('material',`side: double; color: ${colors[ i % colors.length]}`)
|
||||||
|
}
|
||||||
|
aentity.addEventListener('obbcollisionstarted', this.onpress )
|
||||||
|
aentity.addEventListener('obbcollisionended', this.onreleased )
|
||||||
|
atext.setAttribute("text",`value: ${manifest.short_name}; align: baseline; anchor: align; align:center; wrapCount:7`)
|
||||||
|
atext.setAttribute("scale","0.1 0.1 0.1")
|
||||||
|
aentity.appendChild(atext)
|
||||||
|
this.el.appendChild(aentity)
|
||||||
|
aentity.launchCom = launchCom
|
||||||
|
return aentity
|
||||||
|
}
|
||||||
|
|
||||||
// finally render them!
|
// finally render them!
|
||||||
this.el.dom.innerHTML = '' // clear
|
this.el.dom.innerHTML = '' // clear
|
||||||
els = els || this.system.launchables
|
els = els || this.system.components
|
||||||
els.map( (c) => {
|
els.map( (c) => {
|
||||||
const manifest = c.manifest
|
const launchComponentKey = c.getAttributeNames().shift()
|
||||||
|
const launchCom = c.components[ launchComponentKey ]
|
||||||
|
if( !launchCom ) return console.warn(`could not find component '${launchComponentKey}' (forgot to include script-tag?)`)
|
||||||
|
const manifest = launchCom.manifest
|
||||||
if( manifest ){
|
if( manifest ){
|
||||||
add2D(c,manifest)
|
add2D(launchCom,c,manifest)
|
||||||
|
add3D(launchCom,c,manifest)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.centerMenu();
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
centerMenu: function(){
|
onpress: function(e){
|
||||||
// center along x-axis
|
const launcher = document.querySelector('[launcher]').components.launcher
|
||||||
this.el.object3D.traverse( (o) => {
|
if( launcher.data.paused ) return // prevent accidental pressed due to animation
|
||||||
if( o.constructor && String(o.constructor).match(/HTMLMesh/) ){
|
if( e.detail.withEl.computedMixinStr == 'menuitem' ) return // dont react to menuitems touching eachother
|
||||||
this.setOriginToMiddle(o, this.el.object3D, {x:true})
|
|
||||||
o.position.z = 0.012 // position a bit before the grab-line
|
// if user press menu button toggle menu
|
||||||
o.position.y = -0.01 // position a bit before the grab-line
|
if( launcher && !launcher.data.open && e.srcElement.computedMixinStr == 'menubutton' ){
|
||||||
|
return (launcher.data.open = true)
|
||||||
|
}
|
||||||
|
if( launcher && !launcher.data.open ) return // dont process menuitems when menu is closed
|
||||||
|
let el = e.srcElement
|
||||||
|
if(!el) return
|
||||||
|
el.object3D.traverse( (o) => {
|
||||||
|
if( o.material && o.material.color ){
|
||||||
|
if( !o.material.colorOriginal ) o.material.colorOriginal = o.material.color.clone()
|
||||||
|
o.material.color.r *= 0.3
|
||||||
|
o.material.color.g *= 0.3
|
||||||
|
o.material.color.b *= 0.3
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.el.object3D.visible = true // ensure visibility
|
if( el.launchCom ){
|
||||||
|
console.log("launcher.js: launching "+el.launchCom.el.getAttributeNames().shift())
|
||||||
|
launcher.preventAccidentalButtonPresses()
|
||||||
|
el.launchCom.el.emit('launcher') // launch component!
|
||||||
|
this.data.open = false // close to prevent infinite loop of clicks when leaving immersive mode
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setOriginToMiddle: function(fromObject, toObject, axis) {
|
onreleased: function(e){
|
||||||
var boxFrom = new THREE.Box3().setFromObject(fromObject);
|
if( e.detail.withEl.computedMixinStr == 'menuitem' ) return // dont react to menuitems touching eachother
|
||||||
var boxTo = new THREE.Box3().setFromObject(toObject);
|
let el = e.srcElement
|
||||||
var center = new THREE.Vector3();
|
el.object3D.traverse( (o) => {
|
||||||
if( !toObject.positionOriginal ) toObject.positionOriginal = toObject.position.clone()
|
if( o.material && o.material.color ){
|
||||||
center.x = axis.x ? - (boxFrom.max.x/2) : 0
|
if( o.material.colorOriginal ) o.material.color = o.material.colorOriginal.clone()
|
||||||
center.y = axis.y ? - (boxFrom.max.y/2) : 0
|
}
|
||||||
center.z = axis.z ? - (boxFrom.max.z/2) : 0
|
})
|
||||||
toObject.position.copy( toObject.positionOriginal )
|
|
||||||
toObject.position.sub(center);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
attachMenu: function(){
|
||||||
|
if( this.el.parentNode != this.data.attach ){
|
||||||
|
this.el.object3D.visible = true
|
||||||
|
let armature = this.data.attach.object3D.getObjectByName('Armature')
|
||||||
|
if( !armature ) return console.warn('cannot find armature')
|
||||||
|
this.data.attach.object3D.children[0].add(this.el.object3D)
|
||||||
|
this.el.object3D.scale.x = this.data.rescale
|
||||||
|
this.el.object3D.scale.y = this.data.rescale
|
||||||
|
this.el.object3D.scale.z = this.data.rescale
|
||||||
|
|
||||||
|
// add obb-collider to index finger-tip
|
||||||
|
let aentity = document.createElement('a-entity')
|
||||||
|
trackedObject3DVariable = 'parentNode.components.hand-tracking-controls.bones.9';
|
||||||
|
this.data.fingerTip.appendChild(aentity)
|
||||||
|
aentity.setAttribute('obb-collider', {trackedObject3D: trackedObject3DVariable, size: 0.015});
|
||||||
|
|
||||||
|
if( this.isHand(this.data.attach) ){
|
||||||
|
// shortly show and hide menu into palm (hint user)
|
||||||
|
setTimeout( () => { this.data.open = false }, 1500 )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
tick: function(){
|
||||||
|
if( this.ready && this.data.open ){
|
||||||
|
let indexTipPosition = document.querySelector('#right-hand[hand-tracking-controls]').components['hand-tracking-controls'].indexTipPosition
|
||||||
|
this.el.object3D.getWorldPosition(this.worldPosition)
|
||||||
|
const lookingAtPalm = this.data.attach.components['hand-tracking-controls'].wristObject3D.rotation.z > 2.0
|
||||||
|
if( !lookingAtPalm ){ this.data.open = false }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
manifest: { // HTML5 manifest to identify app to xrsh
|
manifest: { // HTML5 manifest to identify app to xrsh
|
||||||
"short_name": "launcher",
|
"short_name": "launcher",
|
||||||
|
|
@ -297,81 +352,35 @@ in above's case "\nHelloworld application\n" will qualify as header.
|
||||||
AFRAME.registerSystem('launcher',{
|
AFRAME.registerSystem('launcher',{
|
||||||
|
|
||||||
init: function(){
|
init: function(){
|
||||||
this.launchables = []
|
this.components = []
|
||||||
this.dom = []
|
|
||||||
this.registered = []
|
|
||||||
// observe HTML changes in <a-scene>
|
// observe HTML changes in <a-scene>
|
||||||
observer = new MutationObserver( (a,b) => this.getLaunchables(a,b) )
|
observer = new MutationObserver( (a,b) => this.getLaunchables(a,b) )
|
||||||
observer.observe( this.sceneEl, {characterData: false, childList: true, attributes: false});
|
observer.observe( this.sceneEl, {characterData: false, childList: true, attributes: false});
|
||||||
|
|
||||||
window.launcher = this
|
|
||||||
this.getLaunchables()
|
|
||||||
},
|
|
||||||
|
|
||||||
register: function(launchable){
|
|
||||||
try{
|
|
||||||
let {name, description, cb} = launchable
|
|
||||||
const exist = this.registered.find( (r) => r.manifest.name == name )
|
|
||||||
if( exist ) return // already registered
|
|
||||||
this.registered.push({
|
|
||||||
manifest: {name, description, icons: launchable.icon ? [{src:launchable.icon}] : [] },
|
|
||||||
launcher: cb
|
|
||||||
})
|
|
||||||
}catch(e){
|
|
||||||
console.error('AFRAME.systems.launcher.register({ name, description, icon, cb }) got invalid obj')
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getLaunchables()
|
|
||||||
},
|
|
||||||
|
|
||||||
unregister: function(launchableName){
|
|
||||||
this.registered = this.registered.filter( (l) => l.name != launchableName )
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getLaunchables: function(mutationsList,observer){
|
getLaunchables: function(mutationsList,observer){
|
||||||
let searchEvent = 'launcher'
|
let searchEvent = 'launcher'
|
||||||
let els = [...this.sceneEl.getElementsByTagName("*")]
|
let els = [...this.sceneEl.getElementsByTagName("*")]
|
||||||
let seen = {}
|
let seen = {}
|
||||||
this.launchables = [
|
|
||||||
/*
|
|
||||||
* {
|
|
||||||
* manifest: {...}
|
|
||||||
* launcher: () => ....
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
];
|
|
||||||
|
|
||||||
// collect manually registered launchables
|
this.components = els.filter( (el) => {
|
||||||
this.registered.map( (launchable) => {
|
|
||||||
if( launchable.component ) seen[ launchable.component ] = true
|
|
||||||
this.launchables.push(launchable)
|
|
||||||
})
|
|
||||||
|
|
||||||
// collect launchables in aframe dom elements
|
|
||||||
this.dom = els.filter( (el) => {
|
|
||||||
let hasEvent = false
|
let hasEvent = false
|
||||||
if( el.components ){
|
if( el.components ){
|
||||||
for( let i in el.components ){
|
for( let i in el.components ){
|
||||||
if( el.components[i].events && el.components[i].events[searchEvent] && !seen[i] ){
|
if( el.components[i].events && el.components[i].events[searchEvent] && !seen[i] ){
|
||||||
let com = hasEvent = seen[i] = el.components[i]
|
hasEvent = seen[i] = true
|
||||||
let alreadyAdded = this.launchables.find( (l) => l.manifest.name == com.manifest.name )
|
|
||||||
if( !alreadyAdded ){
|
|
||||||
com.launcher = () => com.el.emit('launcher',null,false) // important: no bubble
|
|
||||||
this.launchables.push({manifest: com.manifest, launcher: com.launcher})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hasEvent ? el : null
|
return hasEvent ? el : null
|
||||||
})
|
})
|
||||||
this.updateLauncher()
|
this.updateLauncher()
|
||||||
return this.launchables
|
return seen
|
||||||
},
|
},
|
||||||
|
|
||||||
updateLauncher: function(){
|
updateLauncher: function(){
|
||||||
let launcher = document.querySelector('[launcher]')
|
let launcher = document.querySelector('[launcher]')
|
||||||
if( launcher && launcher.components.launcher) launcher.components.launcher.render()
|
if( launcher ) launcher.components.launcher.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,682 +0,0 @@
|
||||||
|
|
||||||
;
|
|
||||||
(function (three) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// This is a copy of https://github.com/mrdoob/three.js/blob/0403020848c26a9605eb91c99a949111ad4a532e/examples/jsm/interactive/HTMLMesh.js
|
|
||||||
|
|
||||||
class HTMLMesh extends three.Mesh {
|
|
||||||
|
|
||||||
constructor( dom ) {
|
|
||||||
|
|
||||||
const texture = new HTMLTexture( dom );
|
|
||||||
|
|
||||||
const geometry = new three.PlaneGeometry( texture.image.width * 0.001, texture.image.height * 0.001 );
|
|
||||||
const material = new three.MeshBasicMaterial( { map: texture, toneMapped: false, transparent: true } );
|
|
||||||
|
|
||||||
super( geometry, material );
|
|
||||||
|
|
||||||
function onEvent( event ) {
|
|
||||||
|
|
||||||
material.map.dispatchDOMEvent( event );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addEventListener( 'mousedown', onEvent );
|
|
||||||
this.addEventListener( 'mousemove', onEvent );
|
|
||||||
this.addEventListener( 'mouseup', onEvent );
|
|
||||||
this.addEventListener( 'click', onEvent );
|
|
||||||
|
|
||||||
this.dispose = function () {
|
|
||||||
|
|
||||||
geometry.dispose();
|
|
||||||
material.dispose();
|
|
||||||
|
|
||||||
material.map.dispose();
|
|
||||||
|
|
||||||
canvases.delete( dom );
|
|
||||||
|
|
||||||
this.removeEventListener( 'mousedown', onEvent );
|
|
||||||
this.removeEventListener( 'mousemove', onEvent );
|
|
||||||
this.removeEventListener( 'mouseup', onEvent );
|
|
||||||
this.removeEventListener( 'click', onEvent );
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class HTMLTexture extends three.CanvasTexture {
|
|
||||||
|
|
||||||
constructor( dom ) {
|
|
||||||
|
|
||||||
super( html2canvas( dom ) );
|
|
||||||
|
|
||||||
this.dom = dom;
|
|
||||||
|
|
||||||
this.anisotropy = 16;
|
|
||||||
this.encoding = three.sRGBEncoding;
|
|
||||||
this.minFilter = three.LinearFilter;
|
|
||||||
this.magFilter = three.LinearFilter;
|
|
||||||
|
|
||||||
// Create an observer on the DOM, and run html2canvas update in the next loop
|
|
||||||
const observer = new MutationObserver( () => {
|
|
||||||
|
|
||||||
if ( ! this.scheduleUpdate ) {
|
|
||||||
|
|
||||||
// ideally should use xr.requestAnimationFrame, here setTimeout to avoid passing the renderer
|
|
||||||
this.scheduleUpdate = setTimeout( () => this.update(), 16 );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
const config = { attributes: true, childList: true, subtree: true, characterData: true };
|
|
||||||
observer.observe( dom, config );
|
|
||||||
|
|
||||||
this.observer = observer;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchDOMEvent( event ) {
|
|
||||||
|
|
||||||
if ( event.data ) {
|
|
||||||
|
|
||||||
htmlevent( this.dom, event.type, event.data.x, event.data.y );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
|
|
||||||
this.image = html2canvas( this.dom );
|
|
||||||
this.needsUpdate = true;
|
|
||||||
|
|
||||||
this.scheduleUpdate = null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
|
|
||||||
if ( this.observer ) {
|
|
||||||
|
|
||||||
this.observer.disconnect();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scheduleUpdate = clearTimeout( this.scheduleUpdate );
|
|
||||||
|
|
||||||
super.dispose();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
const canvases = new WeakMap();
|
|
||||||
|
|
||||||
function html2canvas( element ) {
|
|
||||||
|
|
||||||
const range = document.createRange();
|
|
||||||
const color = new three.Color();
|
|
||||||
|
|
||||||
function Clipper( context ) {
|
|
||||||
|
|
||||||
const clips = [];
|
|
||||||
let isClipping = false;
|
|
||||||
|
|
||||||
function doClip() {
|
|
||||||
|
|
||||||
if ( isClipping ) {
|
|
||||||
|
|
||||||
isClipping = false;
|
|
||||||
context.restore();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( clips.length === 0 ) return;
|
|
||||||
|
|
||||||
let minX = - Infinity, minY = - Infinity;
|
|
||||||
let maxX = Infinity, maxY = Infinity;
|
|
||||||
|
|
||||||
for ( let i = 0; i < clips.length; i ++ ) {
|
|
||||||
|
|
||||||
const clip = clips[ i ];
|
|
||||||
|
|
||||||
minX = Math.max( minX, clip.x );
|
|
||||||
minY = Math.max( minY, clip.y );
|
|
||||||
maxX = Math.min( maxX, clip.x + clip.width );
|
|
||||||
maxY = Math.min( maxY, clip.y + clip.height );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
context.save();
|
|
||||||
context.beginPath();
|
|
||||||
context.rect( minX, minY, maxX - minX, maxY - minY );
|
|
||||||
context.clip();
|
|
||||||
|
|
||||||
isClipping = true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
|
|
||||||
add: function ( clip ) {
|
|
||||||
|
|
||||||
clips.push( clip );
|
|
||||||
doClip();
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
remove: function () {
|
|
||||||
|
|
||||||
clips.pop();
|
|
||||||
doClip();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawText( style, x, y, string ) {
|
|
||||||
|
|
||||||
if ( string !== '' ) {
|
|
||||||
|
|
||||||
if ( style.textTransform === 'uppercase' ) {
|
|
||||||
|
|
||||||
string = string.toUpperCase();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
context.font = style.fontWeight + ' ' + style.fontSize + ' ' + style.fontFamily;
|
|
||||||
context.textBaseline = 'top';
|
|
||||||
context.fillStyle = style.color;
|
|
||||||
context.fillText( string, x, y + parseFloat( style.fontSize ) * 0.1 );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRectPath( x, y, w, h, r ) {
|
|
||||||
|
|
||||||
if ( w < 2 * r ) r = w / 2;
|
|
||||||
if ( h < 2 * r ) r = h / 2;
|
|
||||||
|
|
||||||
context.beginPath();
|
|
||||||
context.moveTo( x + r, y );
|
|
||||||
context.arcTo( x + w, y, x + w, y + h, r );
|
|
||||||
context.arcTo( x + w, y + h, x, y + h, r );
|
|
||||||
context.arcTo( x, y + h, x, y, r );
|
|
||||||
context.arcTo( x, y, x + w, y, r );
|
|
||||||
context.closePath();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawBorder( style, which, x, y, width, height ) {
|
|
||||||
|
|
||||||
const borderWidth = style[ which + 'Width' ];
|
|
||||||
const borderStyle = style[ which + 'Style' ];
|
|
||||||
const borderColor = style[ which + 'Color' ];
|
|
||||||
|
|
||||||
if ( borderWidth !== '0px' && borderStyle !== 'none' && borderColor !== 'transparent' && borderColor !== 'rgba(0, 0, 0, 0)' ) {
|
|
||||||
|
|
||||||
context.strokeStyle = borderColor;
|
|
||||||
context.lineWidth = parseFloat( borderWidth );
|
|
||||||
context.beginPath();
|
|
||||||
context.moveTo( x, y );
|
|
||||||
context.lineTo( x + width, y + height );
|
|
||||||
context.stroke();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawElement( element, style ) {
|
|
||||||
|
|
||||||
let x = 0, y = 0, width = 0, height = 0;
|
|
||||||
|
|
||||||
if ( element.nodeType === Node.TEXT_NODE ) {
|
|
||||||
|
|
||||||
// text
|
|
||||||
|
|
||||||
range.selectNode( element );
|
|
||||||
|
|
||||||
const rect = range.getBoundingClientRect();
|
|
||||||
|
|
||||||
x = rect.left - offset.left - 0.5;
|
|
||||||
y = rect.top - offset.top - 0.5;
|
|
||||||
width = rect.width;
|
|
||||||
height = rect.height;
|
|
||||||
|
|
||||||
drawText( style, x, y, element.nodeValue.trim() );
|
|
||||||
|
|
||||||
} else if ( element.nodeType === Node.COMMENT_NODE ) {
|
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
} else if ( element instanceof HTMLCanvasElement ) {
|
|
||||||
|
|
||||||
// Canvas element
|
|
||||||
if ( element.style.display === 'none' ) return;
|
|
||||||
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
|
|
||||||
x = rect.left - offset.left - 0.5;
|
|
||||||
y = rect.top - offset.top - 0.5;
|
|
||||||
|
|
||||||
context.save();
|
|
||||||
const dpr = window.devicePixelRatio;
|
|
||||||
context.scale( 1 / dpr, 1 / dpr );
|
|
||||||
context.drawImage( element, x, y );
|
|
||||||
context.restore();
|
|
||||||
|
|
||||||
} else if ( element instanceof HTMLImageElement ) {
|
|
||||||
|
|
||||||
if ( element.style.display === 'none' ) return;
|
|
||||||
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
|
|
||||||
x = rect.left - offset.left - 0.5;
|
|
||||||
y = rect.top - offset.top - 0.5;
|
|
||||||
width = rect.width;
|
|
||||||
height = rect.height;
|
|
||||||
|
|
||||||
context.drawImage( element, x, y, width, height );
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
if ( element.style.display === 'none' ) return;
|
|
||||||
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
|
|
||||||
x = rect.left - offset.left - 0.5;
|
|
||||||
y = rect.top - offset.top - 0.5;
|
|
||||||
width = rect.width;
|
|
||||||
height = rect.height;
|
|
||||||
|
|
||||||
style = window.getComputedStyle( element );
|
|
||||||
|
|
||||||
// Get the border of the element used for fill and border
|
|
||||||
|
|
||||||
buildRectPath( x, y, width, height, parseFloat( style.borderRadius ) );
|
|
||||||
|
|
||||||
const backgroundColor = style.backgroundColor;
|
|
||||||
|
|
||||||
if ( backgroundColor !== 'transparent' && backgroundColor !== 'rgba(0, 0, 0, 0)' ) {
|
|
||||||
|
|
||||||
context.fillStyle = backgroundColor;
|
|
||||||
context.fill();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// If all the borders match then stroke the round rectangle
|
|
||||||
|
|
||||||
const borders = [ 'borderTop', 'borderLeft', 'borderBottom', 'borderRight' ];
|
|
||||||
|
|
||||||
let match = true;
|
|
||||||
let prevBorder = null;
|
|
||||||
|
|
||||||
for ( const border of borders ) {
|
|
||||||
|
|
||||||
if ( prevBorder !== null ) {
|
|
||||||
|
|
||||||
match = ( style[ border + 'Width' ] === style[ prevBorder + 'Width' ] ) &&
|
|
||||||
( style[ border + 'Color' ] === style[ prevBorder + 'Color' ] ) &&
|
|
||||||
( style[ border + 'Style' ] === style[ prevBorder + 'Style' ] );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( match === false ) break;
|
|
||||||
|
|
||||||
prevBorder = border;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( match === true ) {
|
|
||||||
|
|
||||||
// They all match so stroke the rectangle from before allows for border-radius
|
|
||||||
|
|
||||||
const width = parseFloat( style.borderTopWidth );
|
|
||||||
|
|
||||||
if ( style.borderTopWidth !== '0px' && style.borderTopStyle !== 'none' && style.borderTopColor !== 'transparent' && style.borderTopColor !== 'rgba(0, 0, 0, 0)' ) {
|
|
||||||
|
|
||||||
context.strokeStyle = style.borderTopColor;
|
|
||||||
context.lineWidth = width;
|
|
||||||
context.stroke();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Otherwise draw individual borders
|
|
||||||
|
|
||||||
drawBorder( style, 'borderTop', x, y, width, 0 );
|
|
||||||
drawBorder( style, 'borderLeft', x, y, 0, height );
|
|
||||||
drawBorder( style, 'borderBottom', x, y + height, width, 0 );
|
|
||||||
drawBorder( style, 'borderRight', x + width, y, 0, height );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( element instanceof HTMLInputElement ) {
|
|
||||||
|
|
||||||
let accentColor = style.accentColor;
|
|
||||||
|
|
||||||
if ( accentColor === undefined || accentColor === 'auto' ) accentColor = style.color;
|
|
||||||
|
|
||||||
color.set( accentColor );
|
|
||||||
|
|
||||||
const luminance = Math.sqrt( 0.299 * ( color.r ** 2 ) + 0.587 * ( color.g ** 2 ) + 0.114 * ( color.b ** 2 ) );
|
|
||||||
const accentTextColor = luminance < 0.5 ? 'white' : '#111111';
|
|
||||||
|
|
||||||
if ( element.type === 'radio' ) {
|
|
||||||
|
|
||||||
buildRectPath( x, y, width, height, height );
|
|
||||||
|
|
||||||
context.fillStyle = 'white';
|
|
||||||
context.strokeStyle = accentColor;
|
|
||||||
context.lineWidth = 1;
|
|
||||||
context.fill();
|
|
||||||
context.stroke();
|
|
||||||
|
|
||||||
if ( element.checked ) {
|
|
||||||
|
|
||||||
buildRectPath( x + 2, y + 2, width - 4, height - 4, height );
|
|
||||||
|
|
||||||
context.fillStyle = accentColor;
|
|
||||||
context.strokeStyle = accentTextColor;
|
|
||||||
context.lineWidth = 2;
|
|
||||||
context.fill();
|
|
||||||
context.stroke();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( element.type === 'checkbox' ) {
|
|
||||||
|
|
||||||
buildRectPath( x, y, width, height, 2 );
|
|
||||||
|
|
||||||
context.fillStyle = element.checked ? accentColor : 'white';
|
|
||||||
context.strokeStyle = element.checked ? accentTextColor : accentColor;
|
|
||||||
context.lineWidth = 1;
|
|
||||||
context.stroke();
|
|
||||||
context.fill();
|
|
||||||
|
|
||||||
if ( element.checked ) {
|
|
||||||
|
|
||||||
const currentTextAlign = context.textAlign;
|
|
||||||
|
|
||||||
context.textAlign = 'center';
|
|
||||||
|
|
||||||
const properties = {
|
|
||||||
color: accentTextColor,
|
|
||||||
fontFamily: style.fontFamily,
|
|
||||||
fontSize: height + 'px',
|
|
||||||
fontWeight: 'bold'
|
|
||||||
};
|
|
||||||
|
|
||||||
drawText( properties, x + ( width / 2 ), y, '✔' );
|
|
||||||
|
|
||||||
context.textAlign = currentTextAlign;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( element.type === 'range' ) {
|
|
||||||
|
|
||||||
const [ min, max, value ] = [ 'min', 'max', 'value' ].map( property => parseFloat( element[ property ] ) );
|
|
||||||
const position = ( ( value - min ) / ( max - min ) ) * ( width - height );
|
|
||||||
|
|
||||||
buildRectPath( x, y + ( height / 4 ), width, height / 2, height / 4 );
|
|
||||||
context.fillStyle = accentTextColor;
|
|
||||||
context.strokeStyle = accentColor;
|
|
||||||
context.lineWidth = 1;
|
|
||||||
context.fill();
|
|
||||||
context.stroke();
|
|
||||||
|
|
||||||
buildRectPath( x, y + ( height / 4 ), position + ( height / 2 ), height / 2, height / 4 );
|
|
||||||
context.fillStyle = accentColor;
|
|
||||||
context.fill();
|
|
||||||
|
|
||||||
buildRectPath( x + position, y, height, height, height / 2 );
|
|
||||||
context.fillStyle = accentColor;
|
|
||||||
context.fill();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( element.type === 'color' || element.type === 'text' || element.type === 'number' ) {
|
|
||||||
|
|
||||||
clipper.add( { x: x, y: y, width: width, height: height } );
|
|
||||||
|
|
||||||
drawText( style, x + parseInt( style.paddingLeft ), y + parseInt( style.paddingTop ), element.value );
|
|
||||||
|
|
||||||
clipper.remove();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
// debug
|
|
||||||
context.strokeStyle = '#' + Math.random().toString( 16 ).slice( - 3 );
|
|
||||||
context.strokeRect( x - 0.5, y - 0.5, width + 1, height + 1 );
|
|
||||||
*/
|
|
||||||
|
|
||||||
const isClipping = style.overflow === 'auto' || style.overflow === 'hidden';
|
|
||||||
|
|
||||||
if ( isClipping ) clipper.add( { x: x, y: y, width: width, height: height } );
|
|
||||||
|
|
||||||
for ( let i = 0; i < element.childNodes.length; i ++ ) {
|
|
||||||
|
|
||||||
drawElement( element.childNodes[ i ], style );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( isClipping ) clipper.remove();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = element.getBoundingClientRect();
|
|
||||||
|
|
||||||
let canvas = canvases.get( element );
|
|
||||||
|
|
||||||
if ( canvas === undefined ) {
|
|
||||||
|
|
||||||
canvas = document.createElement( 'canvas' );
|
|
||||||
canvas.width = offset.width;
|
|
||||||
canvas.height = offset.height;
|
|
||||||
canvases.set( element, canvas );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = canvas.getContext( '2d'/*, { alpha: false }*/ );
|
|
||||||
|
|
||||||
const clipper = new Clipper( context );
|
|
||||||
|
|
||||||
// console.time( 'drawElement' );
|
|
||||||
|
|
||||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
drawElement( element );
|
|
||||||
|
|
||||||
// console.timeEnd( 'drawElement' );
|
|
||||||
|
|
||||||
return canvas;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function htmlevent( element, event, x, y ) {
|
|
||||||
|
|
||||||
const mouseEventInit = {
|
|
||||||
clientX: ( x * element.offsetWidth ) + element.offsetLeft,
|
|
||||||
clientY: ( y * element.offsetHeight ) + element.offsetTop,
|
|
||||||
view: element.ownerDocument.defaultView
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Find out why this is added. Keep commented out when this file is updated
|
|
||||||
// window.dispatchEvent( new MouseEvent( event, mouseEventInit ) );
|
|
||||||
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
|
|
||||||
x = x * rect.width + rect.left;
|
|
||||||
y = y * rect.height + rect.top;
|
|
||||||
|
|
||||||
function traverse( element ) {
|
|
||||||
|
|
||||||
if ( element.nodeType !== Node.TEXT_NODE && element.nodeType !== Node.COMMENT_NODE ) {
|
|
||||||
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
|
|
||||||
if ( x > rect.left && x < rect.right && y > rect.top && y < rect.bottom ) {
|
|
||||||
|
|
||||||
element.dispatchEvent( new MouseEvent( event, mouseEventInit ) );
|
|
||||||
|
|
||||||
if ( element instanceof HTMLInputElement && element.type === 'range' && ( event === 'mousedown' || event === 'click' ) ) {
|
|
||||||
|
|
||||||
const [ min, max ] = [ 'min', 'max' ].map( property => parseFloat( element[ property ] ) );
|
|
||||||
|
|
||||||
const width = rect.width;
|
|
||||||
const offsetX = x - rect.x;
|
|
||||||
const proportion = offsetX / width;
|
|
||||||
element.value = min + ( max - min ) * proportion;
|
|
||||||
element.dispatchEvent( new InputEvent( 'input', { bubbles: true } ) );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
for ( let i = 0; i < element.childNodes.length; i ++ ) {
|
|
||||||
|
|
||||||
traverse( element.childNodes[ i ] );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
traverse( element );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* jshint esversion: 9, -W097 */
|
|
||||||
|
|
||||||
const schemaHTML = {
|
|
||||||
html: {
|
|
||||||
type: 'selector',
|
|
||||||
},
|
|
||||||
cursor: {
|
|
||||||
type: 'selector',
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
schemaHTML.html.description = `HTML element to use.`;
|
|
||||||
schemaHTML.cursor.description = `Visual indicator for where the user is currently pointing`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const _pointer = new THREE.Vector2();
|
|
||||||
const _event = { type: '', data: _pointer };
|
|
||||||
AFRAME.registerComponent('html', {
|
|
||||||
schema: schemaHTML,
|
|
||||||
init() {
|
|
||||||
this.rerender = this.rerender.bind(this);
|
|
||||||
this.handle = this.handle.bind(this);
|
|
||||||
this.onClick = e => this.handle('click', e);
|
|
||||||
this.onMouseLeave = e => this.handle('mouseleave', e);
|
|
||||||
this.onMouseEnter = e => this.handle('mouseenter', e);
|
|
||||||
this.onMouseUp = e => this.handle('mouseup', e);
|
|
||||||
this.onMouseDown = e => this.handle('mousedown', e);
|
|
||||||
this.mouseMoveDetail = {
|
|
||||||
detail: {
|
|
||||||
cursorEl: null,
|
|
||||||
intersection: null
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
play() {
|
|
||||||
this.el.addEventListener('click', this.onClick);
|
|
||||||
this.el.addEventListener('mouseleave', this.onMouseLeave);
|
|
||||||
this.el.addEventListener('mouseenter', this.onMouseEnter);
|
|
||||||
this.el.addEventListener('mouseup', this.onMouseUp);
|
|
||||||
this.el.addEventListener('mousedown', this.onMouseDown);
|
|
||||||
},
|
|
||||||
pause() {
|
|
||||||
this.el.removeEventListener('click', this.onClick);
|
|
||||||
this.el.removeEventListener('mouseleave', this.onMouseLeave);
|
|
||||||
this.el.removeEventListener('mouseenter', this.onMouseEnter);
|
|
||||||
this.el.removeEventListener('mouseup', this.onMouseUp);
|
|
||||||
this.el.removeEventListener('mousedown', this.onMouseDown);
|
|
||||||
},
|
|
||||||
update() {
|
|
||||||
this.remove();
|
|
||||||
if (!this.data.html) return;
|
|
||||||
const mesh = new HTMLMesh(this.data.html);
|
|
||||||
this.el.setObject3D('html', mesh);
|
|
||||||
this.data.html.addEventListener('input', this.rerender);
|
|
||||||
this.data.html.addEventListener('change', this.rerender);
|
|
||||||
this.cursor = this.data.cursor ? this.data.cursor.object3D : null;
|
|
||||||
},
|
|
||||||
tick() {
|
|
||||||
if (this.activeRaycaster) {
|
|
||||||
const intersection = this.activeRaycaster.components.raycaster.getIntersection(this.el);
|
|
||||||
this.mouseMoveDetail.detail.cursorEl = this.activeRaycaster;
|
|
||||||
this.mouseMoveDetail.detail.intersection = intersection;
|
|
||||||
this.handle('mousemove', this.mouseMoveDetail);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handle(type, evt) {
|
|
||||||
const intersection = evt.detail.intersection;
|
|
||||||
const raycaster = evt.detail.cursorEl;
|
|
||||||
if (type === 'mouseenter') {
|
|
||||||
this.activeRaycaster = raycaster;
|
|
||||||
}
|
|
||||||
if (type === 'mouseleave' && this.activeRaycaster === raycaster) {
|
|
||||||
this.activeRaycaster = null;
|
|
||||||
}
|
|
||||||
if (this.cursor) this.cursor.visible = false;
|
|
||||||
if (intersection) {
|
|
||||||
const mesh = this.el.getObject3D('html');
|
|
||||||
const uv = intersection.uv;
|
|
||||||
_event.type = type;
|
|
||||||
_event.data.set( uv.x, 1 - uv.y );
|
|
||||||
mesh.dispatchEvent( _event );
|
|
||||||
|
|
||||||
if (this.cursor) {
|
|
||||||
this.cursor.visible = true;
|
|
||||||
this.cursor.parent.worldToLocal(this.cursor.position.copy(intersection.point));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rerender() {
|
|
||||||
const mesh = this.el.getObject3D('html');
|
|
||||||
if (mesh && !mesh.material.map.scheduleUpdate) {
|
|
||||||
mesh.material.map.scheduleUpdate = setTimeout( () => mesh.material.map.update(), 16 );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
remove() {
|
|
||||||
const mesh = this.el.getObject3D('html');
|
|
||||||
if (mesh) {
|
|
||||||
this.el.removeObject3D('html');
|
|
||||||
this.data.html.removeEventListener('input', this.rerender);
|
|
||||||
this.data.html.removeEventListener('change', this.rerender);
|
|
||||||
mesh.dispose();
|
|
||||||
}
|
|
||||||
this.activeRaycaster = null;
|
|
||||||
this.mouseMoveDetail.detail.cursorEl = null;
|
|
||||||
this.mouseMoveDetail.detail.intersection = null;
|
|
||||||
this.cursor = null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
})(THREE);
|
|
||||||
|
|
||||||
|
|
||||||
4
com/lib/xhook.min.js
vendored
4
com/lib/xhook.min.js
vendored
File diff suppressed because one or more lines are too long
179
com/paste.js
Normal file
179
com/paste.js
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
AFRAME.registerComponent('paste', {
|
||||||
|
schema: {
|
||||||
|
foo: { type:"string"}
|
||||||
|
},
|
||||||
|
|
||||||
|
init: function () {
|
||||||
|
this.el.object3D.visible = false
|
||||||
|
//this.el.innerHTML = ` `
|
||||||
|
},
|
||||||
|
|
||||||
|
requires:{
|
||||||
|
osbutton: "com/osbutton.js"
|
||||||
|
},
|
||||||
|
|
||||||
|
events:{
|
||||||
|
|
||||||
|
// component events
|
||||||
|
somecomponent: function( ){ console.log("component requirement mounted") },
|
||||||
|
ready: function(e){ console.log("requires are loaded") },
|
||||||
|
|
||||||
|
launcher: function(e){
|
||||||
|
const paste = () => {
|
||||||
|
navigator.clipboard.readText()
|
||||||
|
.then( (base64) => {
|
||||||
|
let mimetype = base64.replace(/;base64,.*/,'')
|
||||||
|
let data = base64.replace(/.*;base64,/,'')
|
||||||
|
let type = this.textHeuristic(data)
|
||||||
|
console.log("type="+type)
|
||||||
|
switch( this.textHeuristic(data) ){
|
||||||
|
case "aframe": this.insertAFRAME(data); break;
|
||||||
|
default: this.insertText(data); break;
|
||||||
|
}
|
||||||
|
this.count += 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.permissions.query({ name: 'clipboard-read' })
|
||||||
|
.then( (permission) => {
|
||||||
|
if( permission.state != 'granted' ){
|
||||||
|
this.el.sceneEl.exitVR()
|
||||||
|
setTimeout( () => paste(), 500 )
|
||||||
|
return
|
||||||
|
}else paste()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
textHeuristic: function(text){
|
||||||
|
// Script type identification clues
|
||||||
|
const bashClues = ["|", "if ", "fi", "cat"];
|
||||||
|
const htmlClues = ["/>", "href=", "src="];
|
||||||
|
const aframeClues = ["<a-entity", "/>", "position="];
|
||||||
|
const jsClues = ["var ", "let ", "function ", "setTimeout","console."];
|
||||||
|
// Count occurrences of clues for each script type
|
||||||
|
const bashCount = bashClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0);
|
||||||
|
const htmlCount = htmlClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0);
|
||||||
|
const aframeCount = aframeClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0);
|
||||||
|
const jsCount = jsClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0);
|
||||||
|
|
||||||
|
// Identify the script with the most clues or return unknown if inconclusive
|
||||||
|
const maxCount = Math.max(bashCount, htmlCount, jsCount, aframeCount);
|
||||||
|
if (maxCount === 0) {
|
||||||
|
return "unknown";
|
||||||
|
} else if (bashCount === maxCount) {
|
||||||
|
return "bash";
|
||||||
|
} else if (htmlCount === maxCount) {
|
||||||
|
return "html";
|
||||||
|
} else if (jsCount === maxCount) {
|
||||||
|
return "javascript";
|
||||||
|
} else {
|
||||||
|
return "aframe";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
insertAFRAME: function(data){
|
||||||
|
let scene = document.createElement('a-entity')
|
||||||
|
scene.id = "embedAframe"
|
||||||
|
scene.innerHTML = data
|
||||||
|
let el = document.createElement('a-text')
|
||||||
|
el.setAttribute("value",data)
|
||||||
|
el.setAttribute("color","white")
|
||||||
|
el.setAttribute("align","center")
|
||||||
|
el.setAttribute("anchor","align")
|
||||||
|
let osbutton = this.wrapOSButton(el,"aframe",data)
|
||||||
|
AFRAME.scenes[0].appendChild(osbutton)
|
||||||
|
console.log(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
insertText: function(data){
|
||||||
|
let el = document.createElement('a-text')
|
||||||
|
el.setAttribute("value",data)
|
||||||
|
el.setAttribute("color","white")
|
||||||
|
el.setAttribute("align","center")
|
||||||
|
el.setAttribute("anchor","align")
|
||||||
|
let osbutton = this.wrapOSButton(el,"text",data)
|
||||||
|
AFRAME.scenes[0].appendChild(osbutton)
|
||||||
|
console.log(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
wrapOSButton: function(el,type,data){
|
||||||
|
let osbutton = document.createElement('a-entity')
|
||||||
|
let height = type == 'aframe' ? 0.3 : 0.1
|
||||||
|
let depth = type == 'aframe' ? 0.3 : 0.05
|
||||||
|
osbutton.setAttribute("osbutton",`width:0.3; height: ${height}; depth: ${depth}; color:blue `)
|
||||||
|
osbutton.appendChild(el)
|
||||||
|
osbutton.object3D.position.copy( this.getPositionInFrontOfCamera() )
|
||||||
|
return osbutton
|
||||||
|
},
|
||||||
|
|
||||||
|
getPositionInFrontOfCamera: function(distance){
|
||||||
|
const camera = this.el.sceneEl.camera;
|
||||||
|
let pos = new THREE.Vector3()
|
||||||
|
let direction = new THREE.Vector3();
|
||||||
|
// Get camera's forward direction (without rotation)
|
||||||
|
camera.getWorldDirection(direction);
|
||||||
|
camera.getWorldPosition(pos)
|
||||||
|
direction.normalize();
|
||||||
|
// Scale the direction by 1 meter
|
||||||
|
if( !distance ) distance = 1.5
|
||||||
|
direction.multiplyScalar(distance);
|
||||||
|
// Add the camera's position to the scaled direction to get the target point
|
||||||
|
pos.add(direction);
|
||||||
|
return pos
|
||||||
|
},
|
||||||
|
|
||||||
|
manifest: { // HTML5 manifest to identify app to xrsh
|
||||||
|
"short_name": "Paste",
|
||||||
|
"name": "Paste",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "https://css.gg/clipboard.svg",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "/?source=pwa",
|
||||||
|
"start_url": "/?source=pwa",
|
||||||
|
"background_color": "#3367D6",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": "/",
|
||||||
|
"theme_color": "#3367D6",
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "What is the latest news?",
|
||||||
|
"cli":{
|
||||||
|
"usage": "helloworld <type> [options]",
|
||||||
|
"example": "helloworld news",
|
||||||
|
"args":{
|
||||||
|
"--latest": {type:"string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"short_name": "Today",
|
||||||
|
"description": "View weather information for today",
|
||||||
|
"url": "/today?source=pwa",
|
||||||
|
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Paste the clipboard",
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "/images/screenshot1.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "540x720",
|
||||||
|
"form_factor": "narrow"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"help":`
|
||||||
|
Helloworld application
|
||||||
|
|
||||||
|
This is a help file which describes the application.
|
||||||
|
It will be rendered thru troika text, and will contain
|
||||||
|
headers based on non-punctualized lines separated by linebreaks,
|
||||||
|
in above's case "\nHelloworld application\n" will qualify as header.
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
130
com/pastedrop.js
130
com/pastedrop.js
|
|
@ -1,130 +0,0 @@
|
||||||
/**
|
|
||||||
* ## [pastedrop](com/pastedrop.js)
|
|
||||||
*
|
|
||||||
* detects user copy/paste and file dragdrop action
|
|
||||||
* and clipboard functions
|
|
||||||
*
|
|
||||||
* ```html
|
|
||||||
* <a-entity pastedrop/>
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* | event | target | info |
|
|
||||||
* |--------------|--------|------------------------------------------|
|
|
||||||
* | `pasteFile` | self | always translates input to a File object |
|
|
||||||
*/
|
|
||||||
|
|
||||||
AFRAME.registerComponent('pastedrop', {
|
|
||||||
schema: {
|
|
||||||
foo: { type:"string"}
|
|
||||||
},
|
|
||||||
|
|
||||||
init: function () {
|
|
||||||
|
|
||||||
window.addEventListener('paste', this.onPaste.bind(this) )
|
|
||||||
|
|
||||||
document.body.addEventListener('dragover',(e) => e.preventDefault() )
|
|
||||||
document.body.addEventListener('drop', this.onDrop.bind(this) )
|
|
||||||
},
|
|
||||||
|
|
||||||
initClipboard: function(){
|
|
||||||
navigator.permissions.query({ name: 'clipboard-read' })
|
|
||||||
.then( (permission) => {
|
|
||||||
if( permission.state != 'granted' ){
|
|
||||||
this.el.sceneEl.exitVR()
|
|
||||||
setTimeout( () => this.paste(), 500 )
|
|
||||||
return
|
|
||||||
}else this.paste()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
getClipboard: function(){
|
|
||||||
navigator.clipboard.readText()
|
|
||||||
.then( async (base64) => {
|
|
||||||
let mimetype = base64.replace(/;base64,.*/,'')
|
|
||||||
let data = base64.replace(/.*;base64,/,'')
|
|
||||||
let type = this.textHeuristic(data)
|
|
||||||
const term = document.querySelector('[isoterminal]').components.isoterminal.term
|
|
||||||
this.el.emit('pasteFile',{}) /*TODO* data incompatible */
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
onDrop: function(e){
|
|
||||||
e.preventDefault()
|
|
||||||
this.onPaste({...e, type: "paste", clipboardData: e.dataTransfer})
|
|
||||||
},
|
|
||||||
|
|
||||||
onPaste: function(e){
|
|
||||||
if( e.type != "paste" ) return
|
|
||||||
|
|
||||||
const clipboardData = e.clipboardData || navigator.clipboard;
|
|
||||||
const items = clipboardData.items;
|
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const item = items[i];
|
|
||||||
const type = item.type;
|
|
||||||
|
|
||||||
// Check if the item is a file
|
|
||||||
if (item.kind === "file") {
|
|
||||||
this.el.emit('pasteFile',{item,type})
|
|
||||||
} else if (type === "text/plain") {
|
|
||||||
const pastedText = clipboardData.getData("text/plain");
|
|
||||||
const newType = "text" // let /root/hook.d/mimetype/text further decide whether this is text/plain (or something else)
|
|
||||||
this.el.emit('pasteFile',{item,type:newType,pastedText})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
manifest: { // HTML5 manifest to identify app to xrsh
|
|
||||||
"short_name": "Paste",
|
|
||||||
"name": "Paste",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "https://css.gg/clipboard.svg",
|
|
||||||
"type": "image/svg+xml",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"id": "/?source=pwa",
|
|
||||||
"start_url": "/?source=pwa",
|
|
||||||
"background_color": "#3367D6",
|
|
||||||
"display": "standalone",
|
|
||||||
"scope": "/",
|
|
||||||
"theme_color": "#3367D6",
|
|
||||||
"shortcuts": [
|
|
||||||
{
|
|
||||||
"name": "What is the latest news?",
|
|
||||||
"cli":{
|
|
||||||
"usage": "helloworld <type> [options]",
|
|
||||||
"example": "helloworld news",
|
|
||||||
"args":{
|
|
||||||
"--latest": {type:"string"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"short_name": "Today",
|
|
||||||
"description": "View weather information for today",
|
|
||||||
"url": "/today?source=pwa",
|
|
||||||
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Paste the clipboard",
|
|
||||||
"screenshots": [
|
|
||||||
{
|
|
||||||
"src": "/images/screenshot1.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "540x720",
|
|
||||||
"form_factor": "narrow"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"help":`
|
|
||||||
Helloworld application
|
|
||||||
|
|
||||||
This is a help file which describes the application.
|
|
||||||
It will be rendered thru troika text, and will contain
|
|
||||||
headers based on non-punctualized lines separated by linebreaks,
|
|
||||||
in above's case "\nHelloworld application\n" will qualify as header.
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
// this makes WebXR hand controls able to click things (by touching it)
|
|
||||||
|
|
||||||
AFRAME.registerComponent('pressable', {
|
|
||||||
schema: {
|
|
||||||
pressDistance: { default: 0.008 },
|
|
||||||
pressDuration: { default: 300 },
|
|
||||||
immersiveOnly: { default: true }
|
|
||||||
},
|
|
||||||
init: function() {
|
|
||||||
this.worldPosition = new THREE.Vector3();
|
|
||||||
this.raycaster = new THREE.Raycaster()
|
|
||||||
this.handEls = document.querySelectorAll('[hand-tracking-controls]');
|
|
||||||
this.pressed = false;
|
|
||||||
this.distance = -1
|
|
||||||
// we throttle by distance, to support scenes with loads of clickable objects (far away)
|
|
||||||
this.tick = this.throttleByDistance( () => this.detectPress() )
|
|
||||||
this.el.addEventListener("raycaster-intersected", (e) => {
|
|
||||||
if( !this.data || (this.data.immersiveOnly && !this.el.sceneEl.renderer.xr.isPresenting) ) return
|
|
||||||
this.el.emit('click', e.detail )
|
|
||||||
})
|
|
||||||
|
|
||||||
},
|
|
||||||
throttleByDistance: function(f){
|
|
||||||
return function(){
|
|
||||||
if( this.distance < 0 ) return f() // first call
|
|
||||||
if( !f.tid ){
|
|
||||||
let x = this.distance
|
|
||||||
let y = x*(x*0.05)*1000 // parabolic curve
|
|
||||||
f.tid = setTimeout( function(){
|
|
||||||
f.tid = null
|
|
||||||
f()
|
|
||||||
}, y )
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
detectPress: function(){
|
|
||||||
if( !this.el.sceneEl.renderer.xr.isPresenting ) return // ignore events in desktop mode
|
|
||||||
|
|
||||||
if( this.handEls.length == 0 ){
|
|
||||||
this.handEls = document.querySelectorAll('[hand-tracking-controls]');
|
|
||||||
}
|
|
||||||
var handEls = this.handEls;
|
|
||||||
var handEl;
|
|
||||||
let minDistance = 5
|
|
||||||
|
|
||||||
// compensate for an object inside a group
|
|
||||||
let object3D = this.el.object3D.type == "Group" ? this.el.object3D.children[0] : this.el.object3D
|
|
||||||
if( !object3D ) return
|
|
||||||
|
|
||||||
for (var i = 0; i < handEls.length; i++) {
|
|
||||||
handEl = handEls[i];
|
|
||||||
let indexTip = handEl.components['hand-tracking-controls'] ?
|
|
||||||
handEl.components['hand-tracking-controls'].indexTipPosition :
|
|
||||||
false
|
|
||||||
if( ! indexTip ) return // nothing to do here
|
|
||||||
|
|
||||||
this.raycaster.far = this.data.pressDistance
|
|
||||||
|
|
||||||
// Create a direction vector to negative Z
|
|
||||||
const direction = new THREE.Vector3(0,0,-1.0);
|
|
||||||
direction.normalize()
|
|
||||||
this.raycaster.set(indexTip, direction)
|
|
||||||
intersects = this.raycaster.intersectObjects([object3D],true)
|
|
||||||
|
|
||||||
object3D.getWorldPosition(this.worldPosition)
|
|
||||||
distance = indexTip.distanceTo(this.worldPosition)
|
|
||||||
minDistance = distance < minDistance ? distance : minDistance
|
|
||||||
|
|
||||||
if (intersects.length ){
|
|
||||||
this.i = this.i || 0;
|
|
||||||
if( !this.pressed ){
|
|
||||||
this.el.emit('pressedstarted', intersects);
|
|
||||||
this.el.emit('click', {intersection: intersects[0]});
|
|
||||||
this.pressed = setTimeout( () => {
|
|
||||||
this.el.emit('pressedended', intersects);
|
|
||||||
this.pressed = null
|
|
||||||
}, this.data.pressDuration )
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.distance = minDistance
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
50
com/requestAnimationFrameXR.js
Normal file
50
com/requestAnimationFrameXR.js
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* ## requestAnimationFrameXR
|
||||||
|
*
|
||||||
|
* reroutes requestAnimationFrame-calls to xrSession.requestAnimationFrame
|
||||||
|
* reason: in immersive mode this function behaves differently
|
||||||
|
* (causing HTML apps like xterm.js not getting updated due to relying
|
||||||
|
* on window.requestAnimationFrame)
|
||||||
|
*
|
||||||
|
* ```html
|
||||||
|
* <a-entity requestAnimationFrameXR dom/>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
if( !AFRAME.systems.requestAnimationFrameXR ){
|
||||||
|
|
||||||
|
AFRAME.registerSystem('requestAnimationFrameXR',{
|
||||||
|
|
||||||
|
init: function init(){
|
||||||
|
if( document.location.hostname.match(/localhost/) ) return // allow webxr polyfill during development (they hang in XR)
|
||||||
|
AFRAME.systems.requestAnimationFrameXR.q = []
|
||||||
|
this.sceneEl.addEventListener('enter-vr', this.enable )
|
||||||
|
this.sceneEl.addEventListener('enter-ar', this.enable )
|
||||||
|
this.sceneEl.addEventListener('exit-vr', this.disable )
|
||||||
|
this.sceneEl.addEventListener('exit-ar', this.disable )
|
||||||
|
},
|
||||||
|
|
||||||
|
enable: function enable(){
|
||||||
|
this.requestAnimationFrame = window.requestAnimationFrame
|
||||||
|
// NOTE: we don't call xrSession.requestAnimationFrame directly like this:
|
||||||
|
//
|
||||||
|
// window.requestAnimationFrame = AFRAME.utils.throttleTick( (cb) => this.sceneEl.xrSession.requestAnimationFrame(cb), 50 )
|
||||||
|
//
|
||||||
|
// as that breaks webxr polyfill (for in-browser testing)
|
||||||
|
// instead we defer calls to tick() (which is called both in XR and non-XR)
|
||||||
|
//
|
||||||
|
window.requestAnimationFrame = (cb) => AFRAME.systems.requestAnimationFrameXR.q.push(cb)
|
||||||
|
const q = AFRAME.systems.requestAnimationFrameXR.q
|
||||||
|
this.tick = AFRAME.utils.throttleTick( () => {
|
||||||
|
while( q.length != 0 ) (q.pop())()
|
||||||
|
},50)
|
||||||
|
},
|
||||||
|
|
||||||
|
disable: function disable(){
|
||||||
|
delete this.tick
|
||||||
|
window.requestAnimationFrame = this.requestAnimationFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,17 @@
|
||||||
/**
|
// usage:
|
||||||
* ## [require](com/require('').js)
|
//
|
||||||
*
|
// await AFRAME.utils.require( this.dependencies ) (*) autoload missing components
|
||||||
* automatically requires dependencies or missing components
|
// await AFRAME.utils.require( this.el.getAttributeNames() ) (*) autoload missing components
|
||||||
*
|
// await AFRAME.utils.require({foo: "https://foo.com/aframe/components/foo.js"},this)
|
||||||
* ```javascript
|
// await AFRAME.utils.require(["./app/foo.js","foo.css"],this)
|
||||||
* await AFRAME.utils.require( this.dependencies ) (*) autoload missing components
|
//
|
||||||
* await AFRAME.utils.require( this.el.getAttributeNames() ) (*) autoload missing components
|
// (*) = prefixes baseURL AFRAME.utils.require.baseURL ('./com/' e.g.)
|
||||||
* await AFRAME.utils.require({foo: "https://foo.com/aframe/components/foo.js"},this)
|
//
|
||||||
* await AFRAME.utils.require(["./app/foo.js","foo.css"],this)
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* > (*) = prefixes baseURL AFRAME.utils.require.baseURL ('./com/' e.g.)
|
|
||||||
*/
|
|
||||||
AFRAME.utils.require = function(arr_or_obj,opts){
|
AFRAME.utils.require = function(arr_or_obj,opts){
|
||||||
opts = opts || {}
|
opts = opts || {}
|
||||||
let i = 0
|
let i = 0
|
||||||
let deps = []
|
let deps = []
|
||||||
|
AFRAME.required = AFRAME.required || {}
|
||||||
let packagesArr = arr_or_obj.map ? arr_or_obj : Object.values(arr_or_obj)
|
let packagesArr = arr_or_obj.map ? arr_or_obj : Object.values(arr_or_obj)
|
||||||
|
|
||||||
const parseURI = function(uri){
|
const parseURI = function(uri){
|
||||||
|
|
@ -35,8 +31,8 @@ AFRAME.utils.require = function(arr_or_obj,opts){
|
||||||
}
|
}
|
||||||
|
|
||||||
// prevent duplicate requests
|
// prevent duplicate requests
|
||||||
if( AFRAME.utils.require.required[id] ) return // already loaded before
|
if( AFRAME.required[id] ) return // already loaded before
|
||||||
AFRAME.utils.require.required[id] = true
|
AFRAME.required[id] = true
|
||||||
|
|
||||||
if( !document.body.querySelector(`script#${id}`) &&
|
if( !document.body.querySelector(`script#${id}`) &&
|
||||||
!document.body.querySelector(`link#${id}`) ){
|
!document.body.querySelector(`link#${id}`) ){
|
||||||
|
|
@ -71,41 +67,4 @@ AFRAME.utils.require = function(arr_or_obj,opts){
|
||||||
return Promise.all(deps)
|
return Promise.all(deps)
|
||||||
}
|
}
|
||||||
|
|
||||||
AFRAME.utils.require.required = {}
|
|
||||||
|
|
||||||
AFRAME.utils.require.baseURL = './com/'
|
AFRAME.utils.require.baseURL = './com/'
|
||||||
|
|
||||||
|
|
||||||
//// this component will scan the DOM for missing components and lazy load them
|
|
||||||
//AFRAME.registerSystem('require',{
|
|
||||||
//
|
|
||||||
// init: function(){
|
|
||||||
// this.components = []
|
|
||||||
// // observe HTML changes in <a-scene>
|
|
||||||
// observer = new MutationObserver( (a,b) => this.getMissingComponents(a,b) )
|
|
||||||
// observer.observe( this.sceneEl, {characterData: false, childList: true, attributes: false});
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// getMissingComponents: function(mutationsList,observer){
|
|
||||||
// let els = [...this.sceneEl.getElementsByTagName("*")]
|
|
||||||
// let seen = []
|
|
||||||
//
|
|
||||||
// els.map( async (el) => {
|
|
||||||
// let attrs = el.getAttributeNames()
|
|
||||||
// .filter( (a) => a.match(/(^aframe-injected|^data-aframe|^id$|^class$|^on)/) ? null : a )
|
|
||||||
// for( let attr in attrs ){
|
|
||||||
// let component = attrs[attr]
|
|
||||||
// if( el.components && !el.components[component] ){
|
|
||||||
// console.info(`require.js: lazy-loading missing <${el.tagName.toLowerCase()} ${component} ... > (TODO: fix selectors in schema)`)
|
|
||||||
// // require && remount
|
|
||||||
// try{
|
|
||||||
// await AFRAME.utils.require([component])
|
|
||||||
// el.removeAttribute(component)
|
|
||||||
// el.setAttribute(component, el.getAttribute(component) )
|
|
||||||
// }catch(e){ } // give up, normal AFRAME behaviour follows
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//})
|
|
||||||
|
|
|
||||||
|
|
@ -34,23 +34,16 @@ AFRAME.registerComponent('selfcontainer', {
|
||||||
|
|
||||||
installProxyServer: function(){
|
installProxyServer: function(){
|
||||||
if( !window.store ) window.store = {}
|
if( !window.store ) window.store = {}
|
||||||
|
|
||||||
// selfcontain every webrequest to store (and serve if stored)
|
// selfcontain every webrequest to store (and serve if stored)
|
||||||
let curry = function(me){
|
let curry = function(me){
|
||||||
return function(request, response, cb){
|
return function(request, response, cb){
|
||||||
|
|
||||||
let data = request ? window.store[ request.url ] || false : false
|
let data = request ? window.store[ request.url ] || false : false
|
||||||
if( data ){ // return inline version
|
if( data ){ // return inline version
|
||||||
console.log('selfcontainer.js: serving '+request.url+' from cache')
|
console.log('selfcontained cache: '+request.url)
|
||||||
let res = new Response()
|
let res = new Response()
|
||||||
res[ data.binary ? 'data' : 'text' ] = data.binary ? () => me.convert.base64ToArrayBuffer(data.text) : data.text
|
res[ data.binary ? 'data' : 'text' ] = data.binary ? () => me.convert.base64ToArrayBuffer(data.text) : data.text
|
||||||
cb(res)
|
cb(res)
|
||||||
}else{
|
}else{
|
||||||
|
|
||||||
// never cache requests to filesystem or zip/sh packages
|
|
||||||
if( request.url.match(/(^\/mnt\/|\.zip$|\.sh$)/) ) return cb(response)
|
|
||||||
|
|
||||||
console.log("selfcontainer.js: caching "+request.url)
|
|
||||||
if( response.text ){
|
if( response.text ){
|
||||||
data = {text: response.text}
|
data = {text: response.text}
|
||||||
}else{
|
}else{
|
||||||
|
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
AFRAME.registerComponent('wearable', {
|
|
||||||
schema:{
|
|
||||||
el: {type:"selector"},
|
|
||||||
controlPos: {type:"vec3"},
|
|
||||||
controlRot: {type:"vec3"},
|
|
||||||
handPos: {type:"vec3"},
|
|
||||||
handRot: {type:"vec3"}
|
|
||||||
},
|
|
||||||
init: function(){
|
|
||||||
this.remember()
|
|
||||||
|
|
||||||
if( !this.data.el ) return console.warn(`wear.js: cannot find ${this.data.el}`)
|
|
||||||
this.data.el.object3D.name = 'wearable'
|
|
||||||
|
|
||||||
// hand vs controller multi attach-heuristics (intended to survived AFRAME updates)
|
|
||||||
this.data.el.addEventListener('controllermodelready', this.attachWhatEverController.bind(this) ) // downside: no model yet
|
|
||||||
this.data.el.addEventListener('model-loaded', this.attachWhatEverController.bind(this) ) // downside: only called once [model not added yet]
|
|
||||||
this.el.sceneEl.addEventListener('controllersupdated', this.attachWhatEverController.bind(this) ) // downside: only called when switching [no model yet]
|
|
||||||
},
|
|
||||||
|
|
||||||
attachWhatEverController: function(e){
|
|
||||||
setTimeout( () => { // needed because the events are called before the model was added via add())
|
|
||||||
let wrist = false
|
|
||||||
let hand = this.data.el.components['hand-tracking-controls']
|
|
||||||
if( hand && hand.controllerPresent ) wrist = hand.wristObject3D
|
|
||||||
this.attach( wrist || this.data.el.object3D)
|
|
||||||
this.update( wrist ? 'hand' : 'control')
|
|
||||||
},100)
|
|
||||||
},
|
|
||||||
|
|
||||||
attach: function(target){
|
|
||||||
if( this.target && target.uuid == this.target.uuid ) return// already attached
|
|
||||||
target.add(this.el.object3D )
|
|
||||||
this.target = target
|
|
||||||
},
|
|
||||||
|
|
||||||
detach: function(){
|
|
||||||
this.parent.add(this.el.object3D)
|
|
||||||
this.el.object3D.position.copy( this.position )
|
|
||||||
this.el.object3D.rotation.copy( this.rotation )
|
|
||||||
},
|
|
||||||
|
|
||||||
remember: function(){
|
|
||||||
this.position = this.el.object3D.position.clone()
|
|
||||||
this.rotation = this.el.object3D.rotation.clone()
|
|
||||||
this.parent = this.el.object3D.parent
|
|
||||||
},
|
|
||||||
|
|
||||||
update: function(type){
|
|
||||||
let position = type == 'hand' ? this.data.handPos : this.data.controlPos
|
|
||||||
let rotation = type == 'hand' ? this.data.handRot : this.data.controlRot
|
|
||||||
this.el.object3D.position.copy( position )
|
|
||||||
this.el.object3D.rotation.set( rotation.x, rotation.y, rotation.z )
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
120
com/window.js
120
com/window.js
|
|
@ -1,29 +1,3 @@
|
||||||
/**
|
|
||||||
* ## [window](com/window.js)
|
|
||||||
*
|
|
||||||
* wraps a draggable window around a dom id or [dom](com/dom.js) component.
|
|
||||||
*
|
|
||||||
* ```html
|
|
||||||
* <a-entity window="dom: #mydiv"/>
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* > depends on [AFRAME.utils.require](com/require.js)
|
|
||||||
*
|
|
||||||
* | property | type | default | info |
|
|
||||||
* |------------------|-----------|------------------------|------|
|
|
||||||
* | `title` |`string` | "" | |
|
|
||||||
* | `width` |`string` | | |
|
|
||||||
* | `height` |`string` | 260px | |
|
|
||||||
* | `uid` |`string` | | |
|
|
||||||
* | `attach` |`selector` | | |
|
|
||||||
* | `dom` |`selector` | | |
|
|
||||||
* | `max` |`boolean` | false | |
|
|
||||||
* | `min` |`boolean` | false | |
|
|
||||||
* | `x` |`string` | "center" | |
|
|
||||||
* | `y` |`string` | "center" | |
|
|
||||||
* | `class` |`array` | [] | |
|
|
||||||
*/
|
|
||||||
|
|
||||||
AFRAME.registerComponent('window', {
|
AFRAME.registerComponent('window', {
|
||||||
schema:{
|
schema:{
|
||||||
title: {type:'string',"default":"title"},
|
title: {type:'string',"default":"title"},
|
||||||
|
|
@ -31,19 +5,17 @@ AFRAME.registerComponent('window', {
|
||||||
height: {type:'string',"default":'260px'},
|
height: {type:'string',"default":'260px'},
|
||||||
uid: {type:'string'},
|
uid: {type:'string'},
|
||||||
attach: {type:'selector'},
|
attach: {type:'selector'},
|
||||||
grabbable: {type:'string', "default":"components.html.el.object3D.children.0"},
|
|
||||||
dom: {type:'selector'},
|
dom: {type:'selector'},
|
||||||
max: {type:'boolean',"default":false},
|
max: {type:'boolean',"default":false},
|
||||||
min: {type:'boolean',"default":false},
|
min: {type:'boolean',"default":false},
|
||||||
x: {type:'string',"default":"center"},
|
x: {type:'string',"default":"center"},
|
||||||
y: {type:'string',"default":"center"},
|
y: {type:'string',"default":"center"}
|
||||||
"class": {type:'array',"default":[]},
|
|
||||||
autoresize:{type:'bool', "default": false}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
dependencies:{
|
dependencies:{
|
||||||
dom: "com/dom.js",
|
dom: "com/dom.js",
|
||||||
winboxjs: "assets/winbox.bundle.min.js", // deadsimple windows: https://nextapps-de.github.io/winbox
|
winboxjs: "https://unpkg.com/winbox@0.2.82/dist/winbox.bundle.min.js", // deadsimple windows: https://nextapps-de.github.io/winbox
|
||||||
|
//winboxcss: "https://unpkg.com/winbox@0.2.82/dist/css/winbox.min.css", // main theme
|
||||||
},
|
},
|
||||||
|
|
||||||
init: function(){
|
init: function(){
|
||||||
|
|
@ -53,14 +25,9 @@ AFRAME.registerComponent('window', {
|
||||||
setupWindow: async function(){
|
setupWindow: async function(){
|
||||||
await AFRAME.utils.require(this.dependencies)
|
await AFRAME.utils.require(this.dependencies)
|
||||||
if( !this.el.dom ) return console.error('window element requires dom-component as dependency')
|
if( !this.el.dom ) return console.error('window element requires dom-component as dependency')
|
||||||
this.el.addEventListener('close', () => {
|
|
||||||
close()
|
|
||||||
this.el.winbox.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.el.dom.style.display = 'none'
|
this.el.dom.style.display = 'none'
|
||||||
let winbox = this.el.winbox = new WinBox( this.data.title, {
|
let winbox = this.el.winbox = new WinBox( this.data.title, {
|
||||||
class: this.data.class,
|
|
||||||
height:this.data.height,
|
height:this.data.height,
|
||||||
width:this.data.width,
|
width:this.data.width,
|
||||||
x: this.data.x,
|
x: this.data.x,
|
||||||
|
|
@ -76,84 +43,25 @@ AFRAME.registerComponent('window', {
|
||||||
this.el.emit('window.oncreate',{})
|
this.el.emit('window.oncreate',{})
|
||||||
// resize after the dom content has been rendered & updated
|
// resize after the dom content has been rendered & updated
|
||||||
setTimeout( () => {
|
setTimeout( () => {
|
||||||
if( !this.data.max && this.data.autoresize ) winbox.resize( this.el.dom.offsetWidth+'px', this.el.dom.offsetHeight+'px' )
|
if( !this.data.max ) winbox.resize( this.el.dom.offsetWidth+'px', this.el.dom.offsetHeight+'px' )
|
||||||
// hint grabbable's obb-collider to track the window-object
|
// hint grabbable's obb-collider to track the window-object
|
||||||
this.el.components['obb-collider'].data.trackedObject3D = this.data.grabbable
|
this.el.components['obb-collider'].data.trackedObject3D = 'components.html.el.object3D.children.0'
|
||||||
this.el.components['obb-collider'].update()
|
this.el.components['obb-collider'].update()
|
||||||
},1000)
|
},1000)
|
||||||
|
|
||||||
this.patchButtons(e)
|
|
||||||
},
|
},
|
||||||
onminimize: this.onminimize,
|
onclose: () => {
|
||||||
onrestore: this.onrestore,
|
let e = {halt:false}
|
||||||
onclose: this.onclose.bind(this),
|
this.el.emit('window.onclose',e)
|
||||||
|
if( e.halt ) return true
|
||||||
|
this.data.dom.style.display = 'none';
|
||||||
|
if( this.el.parentNode ) this.el.remove() //parentElement.remove( this.el )
|
||||||
|
this.data.dom.parentElement.remove()
|
||||||
|
return false
|
||||||
|
},
|
||||||
});
|
});
|
||||||
this.data.dom.style.display = '' // show
|
this.data.dom.style.display = '' // show
|
||||||
|
|
||||||
this.el.setAttribute("grabbable","")
|
this.el.setAttribute("grabbable","")
|
||||||
|
|
||||||
if( this.el.object3D.position.x == 0 &&
|
|
||||||
this.el.object3D.position.y == 0 &&
|
|
||||||
this.el.object3D.position.z == 0 ){ // position next to previous window
|
|
||||||
var els = [...document.querySelectorAll('[window]')]
|
|
||||||
if( els.length < 2 ) return
|
|
||||||
let current = els[ els.length-1 ]
|
|
||||||
let last = els[ els.length-2 ]
|
|
||||||
AFRAME.utils.positionObjectNextToNeighbor( current.object3D , last.object3D, 0.02 )
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
show: function(state){
|
|
||||||
this.el.dom.closest('.winbox').style.display = state ? '' : 'none'
|
|
||||||
},
|
|
||||||
|
|
||||||
onminimize: function(e){
|
|
||||||
if( AFRAME.scenes[0].renderer.xr.isPresenting ){
|
|
||||||
this.window.style.display = 'none'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onrestore: function(e){
|
|
||||||
if( AFRAME.scenes[0].renderer.xr.isPresenting ){
|
|
||||||
this.window.style.display = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onclose: function(){
|
|
||||||
let e = {halt:false}
|
|
||||||
this.el.emit('window.onclose',e)
|
|
||||||
if( e.halt ) return true
|
|
||||||
this.data.dom.style.display = 'none';
|
|
||||||
if( this.el.parentNode ) this.el.remove() //parentElement.remove( this.el )
|
|
||||||
this.data.dom.parentElement.remove()
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
// the buttons don't work in XR because HTMLMesh does not understand onclick on divs
|
|
||||||
patchButtons: function(e){
|
|
||||||
let wEl = e.mount;
|
|
||||||
let controls = [...wEl.closest(".winbox").querySelectorAll(".wb-control span")]
|
|
||||||
controls.map( (c) => {
|
|
||||||
if( c.className.match(/wb-(close|min)/) ){
|
|
||||||
let btn = document.createElement("button")
|
|
||||||
btn.className = `xr xr-${c.className}`
|
|
||||||
btn.innerText = c.className == "wb-close" ? "x" : "_"
|
|
||||||
btn.addEventListener("click", (e) => { } )// this will bubble up (to click ancestor in XR)
|
|
||||||
c.appendChild(btn)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
AFRAME.utils.positionObjectNextToNeighbor = function positionObjectNextToNeighbor(object, lastNeighbor = null, margin ){
|
|
||||||
if( lastNeighbor == null || object == null) return
|
|
||||||
// *FIXME* this could be more sophisticated :)
|
|
||||||
object.position.x = lastNeighbor.position.x + margin
|
|
||||||
object.position.y = lastNeighbor.position.y - margin
|
|
||||||
object.position.z = lastNeighbor.position.z + margin
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
81
com/xrfragments.js
Normal file
81
com/xrfragments.js
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
AFRAME.registerComponent('xrfragments', {
|
||||||
|
schema: {
|
||||||
|
url: { type:"string"}
|
||||||
|
},
|
||||||
|
|
||||||
|
init: function () {
|
||||||
|
},
|
||||||
|
|
||||||
|
events:{
|
||||||
|
|
||||||
|
launcher: async function(){
|
||||||
|
let url = prompt('enter URL to glb/fbx/json/obj/usdz asset', 'https://xrfragment.org/index.glb')
|
||||||
|
if( !url ) return
|
||||||
|
await AFRAME.utils.require({
|
||||||
|
xrfragments: "https://xrfragment.org/dist/xrfragment.aframe.js",
|
||||||
|
})
|
||||||
|
|
||||||
|
// remove objects which are marked to be removed from scene (with noxrf)
|
||||||
|
let els = [...document.querySelectorAll('[noxrf]') ]
|
||||||
|
els.map( (el) => el.remove() )
|
||||||
|
|
||||||
|
if( !this.el.getAttribute("xrf") ){
|
||||||
|
this.el.setAttribute("xrf", url )
|
||||||
|
let ARbutton = document.querySelector('.a-enter-ar-button')
|
||||||
|
if( ARbutton ){
|
||||||
|
ARbutton.addEventListener('click', () => {
|
||||||
|
AFRAME.XRF.reset()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}else AFRAME.XRF.navigator.to(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
manifest: { // HTML5 manifest to identify app to xrsh
|
||||||
|
"short_name": "XRF",
|
||||||
|
"name": "XR Fragment URL",
|
||||||
|
"icons": [ ],
|
||||||
|
"id": "/?source=pwa",
|
||||||
|
"start_url": "/?source=pwa",
|
||||||
|
"background_color": "#3367D6",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": "/",
|
||||||
|
"theme_color": "#3367D6",
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "What is the latest news?",
|
||||||
|
"cli":{
|
||||||
|
"usage": "helloworld <type> [options]",
|
||||||
|
"example": "helloworld news",
|
||||||
|
"args":{
|
||||||
|
"--latest": {type:"string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"short_name": "Today",
|
||||||
|
"description": "View weather information for today",
|
||||||
|
"url": "/today?source=pwa",
|
||||||
|
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Hello world information",
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "/images/screenshot1.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "540x720",
|
||||||
|
"form_factor": "narrow"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"help":`
|
||||||
|
Helloworld application
|
||||||
|
|
||||||
|
This is a help file which describes the application.
|
||||||
|
It will be rendered thru troika text, and will contain
|
||||||
|
headers based on non-punctualized lines separated by linebreaks,
|
||||||
|
in above's case "\nHelloworld application\n" will qualify as header.
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
Loading…
Add table
Reference in a new issue