Compare commits
No commits in common. "main" and "feat/vt100" have entirely different histories.
main
...
feat/vt100
52 changed files with 3950 additions and 8073 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` | [] | |
|
|
||||||
|
|
|
||||||
111
com/cast.js
111
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
|
|
||||||
// instance this component
|
|
||||||
this.el.setAttribute("dom","")
|
|
||||||
},
|
|
||||||
|
|
||||||
DOMready: function(){
|
|
||||||
this.setupCast();
|
|
||||||
this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.uid}; faceuser: true`)
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
setupCast: async function(){
|
|
||||||
let s = await AFRAME.utils.require(this.requires)
|
|
||||||
|
|
||||||
const video = this.el.dom.querySelector('video')
|
|
||||||
|
|
||||||
video.addEventListener( "loadedmetadata", () => {
|
|
||||||
|
|
||||||
let width = Math.round(window.innerWidth*0.4)
|
|
||||||
let factor = width / video.videoWidth
|
|
||||||
let height = Math.round( video.videoHeight * factor)
|
|
||||||
|
|
||||||
const createVideoTexture = () => {
|
|
||||||
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( AFRAME.scenes[0].camera.position );
|
|
||||||
this.el.object3D.add(mesh)
|
|
||||||
}
|
|
||||||
|
|
||||||
createVideoTexture.apply(this)
|
|
||||||
//this.el.setAttribute("window", `title: casting tab; uid: ${this.el.uid}; attach: #overlay; dom: #${this.el.dom.id}; width:${width}; height: ${height}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
const el = document.querySelector('body');
|
const el = document.querySelector('body');
|
||||||
const cropTarget = await CropTarget.fromElement(el);
|
const cropTarget = await CropTarget.fromElement(el);
|
||||||
const stream = await navigator.mediaDevices.getDisplayMedia();
|
const stream = await navigator.mediaDevices.getDisplayMedia();
|
||||||
const [track] = stream.getVideoTracks();
|
const [track] = stream.getVideoTracks();
|
||||||
this.track = track
|
this.track = track
|
||||||
this.stream = stream
|
this.stream = stream
|
||||||
|
this.createWindow()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
video.srcObject = this.stream
|
createWindow: 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("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
|
||||||
|
|
||||||
|
const setupWindow = () => {
|
||||||
|
instance.dom.style.display = 'none'
|
||||||
|
|
||||||
|
const video = instance.dom.querySelector('video')
|
||||||
|
video.addEventListener( "loadedmetadata", function () {
|
||||||
|
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: () => {
|
||||||
|
|
||||||
|
// instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
video.srcObject = instance.stream
|
||||||
video.play()
|
video.play()
|
||||||
|
|
||||||
|
this.createVideoTexture.apply(instance)
|
||||||
|
}
|
||||||
|
setTimeout( () => setupWindow(), 10 ) // give new components time to init
|
||||||
|
},
|
||||||
|
|
||||||
|
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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
187
com/codemirror.js
Normal file
187
com/codemirror.js
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
if( AFRAME.components.codemirror ) delete AFRAME.components.codemirror
|
||||||
|
|
||||||
|
AFRAME.registerComponent('codemirror', {
|
||||||
|
schema: {
|
||||||
|
file: { type:"string"},
|
||||||
|
term: { type:"selector", default: "[isoterminal]" },
|
||||||
|
width: { type:"number", default:900},
|
||||||
|
height: { type:"number", default:700},
|
||||||
|
},
|
||||||
|
|
||||||
|
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.term
|
||||||
|
//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: ${me.com.data.width}px !important;
|
||||||
|
height: ${me.com.data.height-30}px !important;
|
||||||
|
}
|
||||||
|
.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:has(> .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}; width: ${this.data.width}px; height: ${this.data.height}px`)
|
||||||
|
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.throttle( (file,str) => {
|
||||||
|
this.updateFile(file,str)
|
||||||
|
}, 1500)
|
||||||
|
this.editor.on('change', (instance,changeObj) => {
|
||||||
|
this.editor.updateFile( this.data.file, instance.getValue() )
|
||||||
|
})
|
||||||
|
|
||||||
|
this
|
||||||
|
.handleFocus()
|
||||||
|
|
||||||
|
setTimeout( () => {
|
||||||
|
this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`) // only show aframe-html in xr
|
||||||
|
},1500)
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFocus: function(){
|
||||||
|
const focus = (showdom) => (e) => {
|
||||||
|
if( this.editor ){
|
||||||
|
this.editor.focus()
|
||||||
|
}
|
||||||
|
if( this.el.components.window && this.data.renderer == 'canvas'){
|
||||||
|
this.el.components.window.show( showdom )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.el.addEventListener('obbcollisionstarted', focus(false) )
|
||||||
|
this.el.sceneEl.addEventListener('enter-vr', focus(false) )
|
||||||
|
this.el.sceneEl.addEventListener('enter-ar', focus(false) )
|
||||||
|
this.el.sceneEl.addEventListener('exit-vr', focus(true) )
|
||||||
|
this.el.sceneEl.addEventListener('exit-ar', focus(true) )
|
||||||
|
},
|
||||||
|
|
||||||
|
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)
|
||||||
|
console.log(str)
|
||||||
|
await this.isoterminal.worker['emulator.update_file'](file, term.convert.toUint8Array(str) )
|
||||||
|
},
|
||||||
|
|
||||||
|
events:{
|
||||||
|
|
||||||
|
// component events
|
||||||
|
DOMready: function(e){
|
||||||
|
|
||||||
|
this.isoterminal.worker['emulator.read_file'](this.data.file)
|
||||||
|
.then( this.isoterminal.convert.Uint8ArrayToString )
|
||||||
|
.then( (str) => {
|
||||||
|
console.log("creating editor")
|
||||||
|
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
|
||||||
*
|
*
|
||||||
|
|
|
||||||
19
com/dom.js
19
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,9 +54,9 @@ if( !AFRAME.components.dom ){
|
||||||
|
|
||||||
this
|
this
|
||||||
.ensureOverlay()
|
.ensureOverlay()
|
||||||
|
.addCSS()
|
||||||
.createReactiveDOMElement()
|
.createReactiveDOMElement()
|
||||||
.assignUniqueID()
|
.assignUniqueID()
|
||||||
.addCSS()
|
|
||||||
.scaleDOMvsXR()
|
.scaleDOMvsXR()
|
||||||
.triggerKeyboardForInputs()
|
.triggerKeyboardForInputs()
|
||||||
|
|
||||||
|
|
@ -66,11 +70,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 +95,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
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
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
|
||||||
},
|
|
||||||
|
|
||||||
DOMready: function(){
|
const setupWindow = () => {
|
||||||
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`)
|
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
|
// data2event demo
|
||||||
this.el.setAttribute("data2event","")
|
instance.setAttribute("data2event","")
|
||||||
this.data.myvalue = 1001
|
com.data.myvalue = 1
|
||||||
this.data.foo = `this.el ${this.el.uid}: `
|
com.data.foo = `instance ${instance.uid}: `
|
||||||
setInterval( () => this.data.myvalue++, 500 )
|
setInterval( () => com.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 )
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
setTimeout( () => setupWindow(), 10 ) // give new components time to init
|
||||||
},
|
},
|
||||||
|
|
||||||
"window.oncreate": function(){
|
ready: function( ){
|
||||||
this.el.setAttribute("html-as-texture-in-xr", `domid: .winbox#${this.el.uid}; faceuser: true`)
|
this.el.dom.style.display = 'none'
|
||||||
|
console.log("this.el.dom has been added to DOM")
|
||||||
|
this.data.myvalue = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
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,70 @@
|
||||||
/**
|
|
||||||
* ## [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}
|
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 ){
|
if( this.data.faceuser ){
|
||||||
this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.4) )
|
this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.8) )
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
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"}
|
||||||
}
|
}
|
||||||
return handle.apply(this,[type,evt])
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
"short_name": "Today",
|
||||||
enableDoubleSided: function(){
|
"description": "View weather information for today",
|
||||||
// enable doubleside
|
"url": "/today?source=pwa",
|
||||||
this.el.object3D.traverse( (o) => {
|
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
||||||
if( o.constructor && String(o.constructor).match(/HTMLMesh/) ){
|
|
||||||
o.material.side = THREE.DoubleSide
|
|
||||||
}
|
}
|
||||||
})
|
],
|
||||||
|
"description": "use ESC-key to toggle between 2D / 3D",
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "/images/screenshot1.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "540x720",
|
||||||
|
"form_factor": "narrow"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"help":`
|
||||||
|
Helloworld application
|
||||||
|
|
||||||
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,32 @@
|
||||||
/**
|
/*
|
||||||
* ## [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 |
|
|
||||||
* |-------------------|-----------|------------------------|------|
|
|
||||||
* | `title` | `string` | 'xrsh.iso' | window title |
|
|
||||||
* | `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
|
* css/html template
|
||||||
*
|
*
|
||||||
* ┌─────────┐ ┌────────────┐ exit-AR
|
* ┌─────────┐ ┌────────────┐ ┌─────────────┐ exit-AR
|
||||||
* ┌───────►│ com/dom ┼──►│ com/window ├───────────────── exit-VR ◄─┐
|
* ┌───────►│ com/dom ┼──►│ com/window ├─►│ domrenderer │◄────────── exit-VR ◄─┐
|
||||||
* │ └─────────┘ └───────────┬┘ │
|
* │ └─────────┘ └────────────┘ └─────▲───────┘ │
|
||||||
* │ │ │
|
* │ │ ┌───────────────┐ │
|
||||||
* ┌──────────┴────────┐ │ ┌───────────┐ ┌─────────────────────────────┐
|
* ┌──────────┴────────┐ ┌─────┴──────┐ │ xterm.js │ ┌─────────────────────────────┐
|
||||||
* │ com/isoterminal ├────────────────────────────►│com/term.js│ │com/html-as-texture-in-XR.js │
|
* │ com/isoterminal ├────────────────────────────►│com/xterm.js│◄─┤ │ │com/html-as-texture-in-XR.js │
|
||||||
* └────────┬─┬────────┘ │ └──┬─────┬▲─┘ └─────────────────────────────┘
|
* └────────┬─┬────────┘ └──┬──────┬▲─┘ │ xterm.css │ └─────────────────────────────┘
|
||||||
* │ │ ┌────────┐ ┌──▼──────▼──────┐ ││ │
|
* │ │ ┌────────┐ ┌─────────▼──┐ ││ └───────────────┘ │ ▲
|
||||||
* │ └───────►│ plane ├─────►text───┼►div#isoterminal│◄────────────────── enter-VR │
|
* │ └───────►│ plane ├─────►text───┼►canvas │◄────────────────── enter-VR │ │
|
||||||
* │ └────────┘ └────────────────┘ enter-AR ◄─┘
|
* │ └────────┘ └────────────┘ ││ enter-AR ◄─┘ │
|
||||||
|
* │ renderer=canvas ││ │
|
||||||
|
* │ ││ │
|
||||||
|
* │ ISOTerminal.js ││ │
|
||||||
|
* │ ┌───────────────────────────┐◄────┘│ │
|
||||||
|
* │ │ com/isoterminal/worker.js ├──────┘ │
|
||||||
|
* │ └──────────────┌────────────┤ │
|
||||||
|
* │ │ v86.js │ │
|
||||||
|
* │ │ feat/*.js │ │
|
||||||
|
* │ │ libv86.js │ │
|
||||||
|
* │ └────────────┘ │
|
||||||
* │ │
|
* │ │
|
||||||
* │ │
|
* └────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
* │ 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
|
* 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())
|
* Instead of a melting pot of different functionnames, events are flowing through everything (ISOTerminal.emit())
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if( typeof AFRAME != 'undefined '){
|
if( typeof AFRAME != 'undefined '){
|
||||||
|
|
@ -73,36 +35,27 @@ if( typeof AFRAME != 'undefined '){
|
||||||
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"},
|
width: { type: 'number',"default": -1 },
|
||||||
width: { type: 'number',"default": 800 },
|
height: { type: 'number',"default": -1 },
|
||||||
height: { type: 'number',"default": 600 },
|
|
||||||
depth: { type: 'number',"default": 0.03 },
|
depth: { type: 'number',"default": 0.03 },
|
||||||
lineHeight: { type: 'number',"default": 18 },
|
lineHeight: { type: 'number',"default": 18 },
|
||||||
bootOnFragment: { type: 'string',"default": "nothing" },
|
|
||||||
bootMenu: { type: 'string', "default": '' }, // show bootMenu if empty, or autosend key to bootMenu (key '1' e.g.)
|
|
||||||
bootMenuURL: { type: 'string', "default": '' }, // show bootMenu if empty, or autosend key to bootMenu (key '1' e.g.)
|
|
||||||
padding: { type: 'number',"default": 18 },
|
padding: { type: 'number',"default": 18 },
|
||||||
maximized: { type: 'boolean',"default":false},
|
|
||||||
minimized: { type: 'boolean',"default":false},
|
minimized: { type: 'boolean',"default":false},
|
||||||
|
maximized: { type: 'boolean',"default":true},
|
||||||
muteUntilPrompt:{ type: 'boolean',"default":true}, // mute stdout until a prompt is detected in ISO
|
muteUntilPrompt:{ type: 'boolean',"default":true}, // mute stdout until a prompt is detected in ISO
|
||||||
HUD: { type: 'boolean',"default":false}, // link to camera movement
|
HUD: { type: 'boolean',"default":false}, // link to camera movement
|
||||||
transparent: { type:'boolean', "default":false }, // need good gpu
|
transparent: { type:'boolean', "default":false }, // need good gpu
|
||||||
memory: { type: 'number', "default":60 }, // VM memory (in MB) [NOTE: quest or smartphone might crash > 40mb ]
|
memory: { type: 'number', "default":40 }, // 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)
|
bufferLatency: { type: 'number', "default":1 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture)
|
||||||
debug: { type: 'boolean', "default":false },
|
debug: { type: 'boolean', "default":false }
|
||||||
emulator: { type: 'string', "default": "fbterm" }// terminal emulator
|
|
||||||
},
|
},
|
||||||
|
|
||||||
init: function(){
|
init: function(){
|
||||||
this.el.object3D.visible = false
|
this.el.object3D.visible = false
|
||||||
if( window.innerWidth < this.data.width ){
|
|
||||||
this.data.maximized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
this.calculateDimension()
|
this.calculateDimension()
|
||||||
this.initHud()
|
this.initHud()
|
||||||
this.setupPasteDrop()
|
this.setupBox()
|
||||||
this.setupEvents()
|
|
||||||
|
|
||||||
fetch(this.data.iso,{method: 'HEAD'})
|
fetch(this.data.iso,{method: 'HEAD'})
|
||||||
.then( (res) => {
|
.then( (res) => {
|
||||||
|
|
@ -119,11 +72,11 @@ 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",
|
||||||
|
vt100: "com/isoterminal/VT100.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 global features
|
||||||
|
|
@ -136,20 +89,24 @@ if( typeof AFRAME != 'undefined '){
|
||||||
scale: 0.66,
|
scale: 0.66,
|
||||||
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="term" tabindex="0">
|
||||||
<div id="term" tabindex="0"></div>
|
<pre></pre>
|
||||||
|
</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;
|
position:relative;
|
||||||
overflow: hidden;
|
}
|
||||||
|
.isoterminal div{
|
||||||
|
display:block;
|
||||||
|
position:relative;
|
||||||
|
line-height: ${me.com.data.lineHeight}px;
|
||||||
|
}
|
||||||
|
#term {
|
||||||
|
outline: none !important;
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Cousine';
|
font-family: 'Cousine';
|
||||||
|
|
@ -164,73 +121,6 @@ if( typeof AFRAME != 'undefined '){
|
||||||
src: url(./com/isoterminal/assets/CousineBold.ttf) format('truetype');
|
src: url(./com/isoterminal/assets/CousineBold.ttf) format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.isoterminal *{
|
|
||||||
outline:none;
|
|
||||||
box-shadow:none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.term {
|
|
||||||
font-family: 'Cousine';
|
|
||||||
line-height: ${me.com.data.lineHeight}px;
|
|
||||||
font-weight: normal;
|
|
||||||
font-variant-ligatures: none;
|
|
||||||
color: #f0f0f0;
|
|
||||||
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{
|
blink{
|
||||||
|
|
@ -243,24 +133,18 @@ if( typeof AFRAME != 'undefined '){
|
||||||
box-shadow:none;
|
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: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.XR .isoterminal{
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
.isoterminal *{
|
.isoterminal *{
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace;
|
font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace;
|
||||||
|
|
@ -299,30 +183,16 @@ if( typeof AFRAME != 'undefined '){
|
||||||
// * heavily dependent on requestAnimationFrame (conflicts with THREE)
|
// * heavily dependent on requestAnimationFrame (conflicts with THREE)
|
||||||
// * typescript-rewrite results in ~300k lib (instead of 96k)
|
// * typescript-rewrite results in ~300k lib (instead of 96k)
|
||||||
// * v3.12 had slightly better performance but still very heavy
|
// * v3.12 had slightly better performance but still very heavy
|
||||||
//
|
|
||||||
await AFRAME.utils.require(this.requires)
|
|
||||||
|
|
||||||
let features = { // ISOTerminal plugins
|
await AFRAME.utils.require(this.requires)
|
||||||
|
await AFRAME.utils.require({ // ISOTerminal plugins
|
||||||
boot: "com/isoterminal/feat/boot.js",
|
boot: "com/isoterminal/feat/boot.js",
|
||||||
javascript: "com/isoterminal/feat/javascript.js",
|
javascript: "com/isoterminal/feat/javascript.js",
|
||||||
jsconsole: "com/isoterminal/feat/jsconsole.js",
|
jsconsole: "com/isoterminal/feat/jsconsole.js",
|
||||||
remotekeyboard: "com/isoterminal/feat/remotekeyboard.js",
|
|
||||||
indexhtml: "com/isoterminal/feat/index.html.js",
|
indexhtml: "com/isoterminal/feat/index.html.js",
|
||||||
indexjs: "com/isoterminal/feat/index.js.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",
|
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","")
|
||||||
|
|
||||||
|
|
@ -344,16 +214,17 @@ if( typeof AFRAME != 'undefined '){
|
||||||
this.term = new ISOTerminal(instance,this.data)
|
this.term = new ISOTerminal(instance,this.data)
|
||||||
|
|
||||||
instance.addEventListener('DOMready', () => {
|
instance.addEventListener('DOMready', () => {
|
||||||
this.term.emit('term_init', {instance, aEntity:this})
|
this.setupVT100(instance)
|
||||||
|
setTimeout( () => {
|
||||||
|
instance.setAttribute("html-as-texture-in-xr", `domid: #term; faceuser: true`)
|
||||||
|
},100)
|
||||||
//instance.winbox.resize(720,380)
|
//instance.winbox.resize(720,380)
|
||||||
let size = `width: ${this.data.width}; height: ${this.data.height}`
|
let size = `width: ${this.data.width}; height: ${this.data.height}`
|
||||||
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}`)
|
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}
|
||||||
|
|
@ -364,17 +235,11 @@ if( typeof AFRAME != 'undefined '){
|
||||||
})
|
})
|
||||||
|
|
||||||
instance.setAttribute("dom", "")
|
instance.setAttribute("dom", "")
|
||||||
instance.setAttribute("pastedrop", "")
|
|
||||||
|
|
||||||
|
|
||||||
// *REMOVE* make a boot-plugin mechanism in feat/term.js
|
|
||||||
this.term.addEventListener('enable-console', () => {
|
|
||||||
instance.dom.classList.remove('blink')
|
|
||||||
})
|
|
||||||
|
|
||||||
this.term.addEventListener('ready', (e) => {
|
this.term.addEventListener('ready', (e) => {
|
||||||
instance.dom.classList.remove('blink')
|
instance.dom.classList.remove('blink')
|
||||||
this.term.emit('status',"running")
|
this.term.emit('status',"running")
|
||||||
|
if( this.data.debug ) this.runTests()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.term.addEventListener('status', function(e){
|
this.term.addEventListener('status', function(e){
|
||||||
|
|
@ -393,30 +258,18 @@ if( typeof AFRAME != 'undefined '){
|
||||||
instance.addEventListener('window.onresize', resize )
|
instance.addEventListener('window.onresize', resize )
|
||||||
instance.addEventListener('window.onmaximize', resize )
|
instance.addEventListener('window.onmaximize', resize )
|
||||||
|
|
||||||
const focus = (e) => {
|
const focus = (showdom) => (e) => {
|
||||||
|
if( this.el.components.window && this.data.renderer == 'canvas'){
|
||||||
if( event.target == document.activeElement ){
|
this.el.components.window.show( showdom )
|
||||||
// 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)
|
this.el.emit('focus',e.detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.el.addEventListener('obbcollisionstarted', focus )
|
this.el.addEventListener('obbcollisionstarted', focus(false) )
|
||||||
this.el.sceneEl.addEventListener('exit-vr', focus )
|
this.el.sceneEl.addEventListener('enter-vr', focus(false) )
|
||||||
this.el.sceneEl.addEventListener('exit-ar', focus )
|
this.el.sceneEl.addEventListener('enter-ar', focus(false) )
|
||||||
|
this.el.sceneEl.addEventListener('exit-vr', focus(true) )
|
||||||
|
this.el.sceneEl.addEventListener('exit-ar', focus(true) )
|
||||||
|
|
||||||
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
|
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
|
||||||
},
|
},
|
||||||
|
|
@ -429,39 +282,70 @@ if( typeof AFRAME != 'undefined '){
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setupPasteDrop: function(){
|
runTests: async function(){
|
||||||
this.el.addEventListener('pasteFile', (e) => {
|
await AFRAME.utils.require({
|
||||||
e.preventDefault() // prevent bubbling up to window (which is triggering this initially)
|
"test_util": "tests/util.js",
|
||||||
if( !this.term.pasteFile ) return // skip if feat/pastedrop.js is not loaded
|
"test_isoterminal":"tests/ISOTerminal.js"
|
||||||
this.term.pasteFile(e.detail)
|
|
||||||
})
|
})
|
||||||
return this
|
console.test.run()
|
||||||
|
},
|
||||||
|
|
||||||
|
setupVT100: function(instance){
|
||||||
|
const el = this.el.dom.querySelector('#term')
|
||||||
|
const opts = {
|
||||||
|
cols: this.cols,
|
||||||
|
rows: this.rows,
|
||||||
|
el_or_id: el,
|
||||||
|
max_scroll_lines: 100,
|
||||||
|
nodim: true
|
||||||
|
}
|
||||||
|
this.vt100 = new VT100( opts )
|
||||||
|
this.vt100.el = el
|
||||||
|
this.vt100.curs_set( 1, true)
|
||||||
|
el.focus()
|
||||||
|
this.el.addEventListener('focus', () => el.focus())
|
||||||
|
this.vt100.getch( (ch,t) => {
|
||||||
|
this.term.send( ch )
|
||||||
|
this.vt100.curs_set( 0, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.el.addEventListener('serial-output-byte', (e) => {
|
||||||
|
const byte = e.detail
|
||||||
|
var chr = String.fromCharCode(byte);
|
||||||
|
this.vt100.addchr(chr)
|
||||||
|
})
|
||||||
|
this.el.addEventListener('serial-output-string', (e) => {
|
||||||
|
this.vt100.write(e.detail)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
//this.el.dom.querySelector('input').addEventListener('keyup', (e) => {
|
||||||
|
// VT100.handle_onkeypress_( {charCode : e.charCode || e.keyCode, keyCode: e.keyCode}, (chars) => {
|
||||||
|
// debugger
|
||||||
|
// chars.map( (c) => this.term.send(str) )
|
||||||
|
// })
|
||||||
|
//})
|
||||||
|
},
|
||||||
|
|
||||||
|
setupBox: function(){
|
||||||
|
// setup slightly bigger black backdrop (this.el.getObject3D("mesh"))
|
||||||
|
const w = this.data.width/950;
|
||||||
|
const h = this.data.height/950;
|
||||||
|
this.el.box = document.createElement('a-entity')
|
||||||
|
this.el.box.setAttribute("geometry",`primitive: box; width:${w}; height:${h}; depth: -${this.data.depth}`)
|
||||||
|
this.el.box.setAttribute("material","shader:flat; color:black; opacity:0.9; transparent:true; ")
|
||||||
|
this.el.box.setAttribute("position",`0 0 ${(this.data.depth/2)-0.001}`)
|
||||||
|
this.el.appendChild(this.el.box)
|
||||||
},
|
},
|
||||||
|
|
||||||
calculateDimension: function(){
|
calculateDimension: function(){
|
||||||
if( this.data.width == -1 ) this.data.width = document.body.offsetWidth;
|
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 == -1 ) this.data.height = document.body.offsetHeight
|
||||||
if( this.data.height > this.data.width ) this.data.height = this.data.width // mobile smartphone fix
|
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.width -= this.data.padding*2
|
||||||
this.data.height -= this.data.padding*2
|
this.data.height -= this.data.padding*2
|
||||||
this.cols = Math.floor(this.data.width/this.data.lineHeight*2)-1
|
this.cols = Math.floor(this.data.width/this.data.lineHeight*1.9)
|
||||||
this.rows = Math.floor( (this.data.height*0.93)/this.data.lineHeight)-1
|
this.rows = Math.floor(this.data.height*0.5/this.data.lineHeight*1.7) // keep extra height for mobile browser bottom-bar (android)
|
||||||
},
|
|
||||||
|
|
||||||
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:{
|
||||||
|
|
@ -474,12 +358,7 @@ if( typeof AFRAME != 'undefined '){
|
||||||
myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
|
myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
|
||||||
|
|
||||||
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 +382,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 +396,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 +407,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
|
||||||
|
|
|
||||||
|
|
@ -11,24 +11,20 @@ function ISOTerminal(instance,opts){
|
||||||
// compose object with functions
|
// compose object with functions
|
||||||
for( let i in ISOTerminal.prototype ) obj[i] = ISOTerminal.prototype[i]
|
for( let i in ISOTerminal.prototype ) obj[i] = ISOTerminal.prototype[i]
|
||||||
obj.emit('init')
|
obj.emit('init')
|
||||||
instance.sceneEl.emit("isoterminal_init",{})
|
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
ISOTerminal.prototype.emit = function(event,data,sender){
|
ISOTerminal.prototype.emit = function(event,data,sender){
|
||||||
data = data || false
|
data = data || false
|
||||||
const evObj = new CustomEvent(event, {detail: data} )
|
const evObj = new CustomEvent(event, {detail: data} )
|
||||||
|
//this.preventFrameDrop( () => {
|
||||||
// forward event to worker/instance/AFRAME element or component-function
|
// forward event to worker/instance/AFRAME element or component-function
|
||||||
// this feels complex, but actually keeps event- and function-names more concise in codebase
|
// this feels complex, but actually keeps event- and function-names more concise in codebase
|
||||||
let fire = () => {
|
|
||||||
this.dispatchEvent( evObj )
|
this.dispatchEvent( evObj )
|
||||||
if( sender != "instance" && this.instance ) this.instance.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 != "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( 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.addEventListener = (event,cb) => {
|
||||||
|
|
@ -37,28 +33,14 @@ ISOTerminal.addEventListener = (event,cb) => {
|
||||||
ISOTerminal.listener[event].push(cb)
|
ISOTerminal.listener[event].push(cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
ISOTerminal.prototype.exec = function(opts){
|
ISOTerminal.prototype.exec = function(shellscript){
|
||||||
const shellscript = opts[0];
|
this.send(shellscript+"\n",1)
|
||||||
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.serial_input = 0; // can be set to 0,1,2,3 to define stdinput tty (xterm plugin)
|
||||||
|
|
||||||
ISOTerminal.prototype.send = function(str, ttyNr){
|
ISOTerminal.prototype.send = function(str, ttyNr){
|
||||||
if( ttyNr == undefined) ttyNr = this.serial_input
|
if( ttyNr == undefined) ttyNr = this.serial_input
|
||||||
if( (this.emulator || this.worker) && this.ready ){
|
|
||||||
if( ttyNr == undefined ){
|
if( ttyNr == undefined ){
|
||||||
if( this.emulator.serial_adapter ){
|
if( this.emulator.serial_adapter ){
|
||||||
this.emulator.serial_adapter.term.paste(str)
|
this.emulator.serial_adapter.term.paste(str)
|
||||||
|
|
@ -66,15 +48,10 @@ ISOTerminal.prototype.send = function(str, ttyNr){
|
||||||
}else{
|
}else{
|
||||||
this.convert.toUint8Array( str ).map( (c) => {
|
this.convert.toUint8Array( str ).map( (c) => {
|
||||||
this.preventFrameDrop(
|
this.preventFrameDrop(
|
||||||
() => {
|
() => this.worker.postMessage({event:`serial${ttyNr}-input`,data:c})
|
||||||
this.worker.postMessage({event:`serial${ttyNr}-input`,data:c})
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}else{
|
|
||||||
this.emit('serial-output-string', str)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ISOTerminal.prototype.convert = {
|
ISOTerminal.prototype.convert = {
|
||||||
|
|
@ -140,17 +117,8 @@ ISOTerminal.prototype.start = function(opts){
|
||||||
url: "bios/vgabios.bin",
|
url: "bios/vgabios.bin",
|
||||||
//urg|: "com/isoterminal/bios/VGABIOS-lgpl-latest.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",
|
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,
|
//bzimage_initrd_from_filesystem: true,
|
||||||
//filesystem: {
|
//filesystem: {
|
||||||
// baseurl: "com/isoterminal/v86/images/alpine-rootfs-flat",
|
// baseurl: "com/isoterminal/v86/images/alpine-rootfs-flat",
|
||||||
|
|
@ -158,10 +126,8 @@ ISOTerminal.prototype.start = function(opts){
|
||||||
// },
|
// },
|
||||||
//screen_dummy: true,
|
//screen_dummy: true,
|
||||||
//disable_jit: false,
|
//disable_jit: false,
|
||||||
overlayfs: this.opts.overlayfs,
|
|
||||||
filesystem: {},
|
filesystem: {},
|
||||||
autostart: true,
|
autostart: true,
|
||||||
prompt: this.opts.prompt,
|
|
||||||
debug: this.opts.debug ? true : false
|
debug: this.opts.debug ? true : false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -172,8 +138,6 @@ ISOTerminal.prototype.start = function(opts){
|
||||||
|
|
||||||
ISOTerminal.prototype.setupWorker = function(opts){
|
ISOTerminal.prototype.setupWorker = function(opts){
|
||||||
|
|
||||||
if( typeof window.PromiseWorker == 'undefined' ) return this
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* the WebWorker (which runs v86)
|
* the WebWorker (which runs v86)
|
||||||
*
|
*
|
||||||
|
|
@ -186,8 +150,10 @@ ISOTerminal.prototype.setupWorker = function(opts){
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
ISOTerminal.prototype.getLoaderMsg = function(){
|
|
||||||
|
|
||||||
|
ISOTerminal.prototype.startVM = function(opts){
|
||||||
|
|
||||||
|
this.emit('runISO',{...opts, bufferLatency: this.opts.bufferLatency })
|
||||||
const loading = [
|
const loading = [
|
||||||
'loading quantum bits and bytes',
|
'loading quantum bits and bytes',
|
||||||
'preparing quantum flux capacitors',
|
'preparing quantum flux capacitors',
|
||||||
|
|
@ -214,14 +180,16 @@ ISOTerminal.prototype.getLoaderMsg = function(){
|
||||||
|
|
||||||
const empower = [
|
const empower = [
|
||||||
"FOSS gives users control over their software, offering freedom to modify and share",
|
"FOSS gives users control over their software, offering freedom to modify and share",
|
||||||
"Feeling powerless? FOSS escapes a mindset known as learned helplessness",
|
"Feeling powerless with tech? FOSS escapes a mindset known as learned helplessness",
|
||||||
"FOSS breaks this cycle by showing that anyone can learn and contribute",
|
"FOSS breaks this cycle by showing that anyone can learn and contribute",
|
||||||
"Proprietary software can make users dependent, but FOSS offers real choices",
|
"Proprietary software can make users dependent, but FOSS offers real choices",
|
||||||
"FOSS communities provide support and encourage users to develop new skills",
|
"FOSS communities provide support and encourage users to develop new skills",
|
||||||
|
"Learned helplessness fades when we realize tech isn’t too complex to understand",
|
||||||
"FOSS empowers users to customize and improve their tools",
|
"FOSS empowers users to customize and improve their tools",
|
||||||
"Engaging with FOSS helps build confidence and self-reliance in tech",
|
"Engaging with FOSS helps build confidence and self-reliance in tech",
|
||||||
|
"FOSS tools are accessible and often better than closed alternatives",
|
||||||
"FOSS shows that anyone can shape the digital world with curiosity and effort",
|
"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",
|
"Linux can revive old computers, extending their life and reducing e-waste",
|
||||||
"Many lightweight Linux distributions run smoothly on older hardware",
|
"Many lightweight Linux distributions run smoothly on older hardware",
|
||||||
"Installing Linux on aging devices keeps them functional instead of sending them to the landfill",
|
"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",
|
"Linux uses fewer resources, making it ideal for reusing older machines",
|
||||||
|
|
@ -233,40 +201,44 @@ ISOTerminal.prototype.getLoaderMsg = function(){
|
||||||
"Switching to Linux promotes sustainability by reducing demand for new gadgets and lowering e-waste"
|
"Switching to Linux promotes sustainability by reducing demand for new gadgets and lowering e-waste"
|
||||||
]
|
]
|
||||||
|
|
||||||
let motd = `
|
const motd = `
|
||||||
\r[38;5;57m. . ____ _____________ ________. ._. ._. . .
|
\r[38;5;57m . . ____ _____________ ________. ._. ._. . .
|
||||||
\r[38;5;93m. . .\\ \\/ /\\______ \\/ _____// | \\. .
|
\r[38;5;93m . . .\\ \\/ /\\______ \\/ _____// | \\. .
|
||||||
\r[38;5;93m. . . \\ / | _/\\_____ \\/ ~ \\ .
|
\r[38;5;93m . . . \\ / | _/\\_____ \\/ ~ \\ .
|
||||||
\r[38;5;129m. . . / \\ | | \\/ \\ Y / .
|
\r[38;5;129m . . . / \\ | | \\/ \\ Y / .
|
||||||
\r[38;5;165m. . ./___/\\ \\ |____|_ /_______ /\\___|_ /. .
|
\r[38;5;165m . . ./___/\\ \\ |____|_ /_______ /\\___|_ /. .
|
||||||
\r[38;5;201m. . . . . .\\_/. . . . \\/ . . . .\\/ . . _ \\/ . .
|
\r[38;5;201m . . . . . .\\_/. . . . \\/ . . . .\\/ . . _ \\/ . .
|
||||||
\r[38;5;165m▬▬▬▬▬▬▬▬ [38;5;51mhttps://xrsh.isvery.ninja[38;5;165m ▬▬▬▬▬▬▬▬▬▬▬▬
|
\r[38;5;165m ▬▬▬▬▬▬▬▬ https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬
|
||||||
\r[38;5;165mlocal-first, polyglot, unixy WebXR IDE & runtime
|
\r[38;5;165m local-first, polyglot, unixy WebXR IDE & runtime
|
||||||
\r[38;5;57m
|
\r
|
||||||
\rcredits
|
\r credits
|
||||||
\r-------
|
\r -------
|
||||||
\r[38;5;51mhttps://www.w3.org/TR/webxr
|
\r @nlnet@nlnet.nl
|
||||||
\rhttps://xrfragment.org
|
\r @lvk@mastodon.online
|
||||||
\rhttps://threejs.org
|
\r @utopiah@mastodon.pirateparty.be
|
||||||
\rhttps://aframe.org
|
\r https://www.w3.org/TR/webxr
|
||||||
\rhttps://busybox.net
|
\r https://three.org
|
||||||
\rhttps://buildroot.org
|
\r https://aframe.org
|
||||||
\rfediverse: @lvk@mastodon.online @utopiah@mastodon.pirateparty.be @nlnet@nlnet.nl
|
`
|
||||||
\r`
|
|
||||||
|
|
||||||
const text_color = "\r[38;5;129m"
|
const text_color = "\r[38;5;129m"
|
||||||
const text_reset = "\033[0m"
|
const text_reset = "\033[0m"
|
||||||
const loadmsg = "\n\r"+loading[ Math.floor(Math.random()*1000) % loading.length ] + "..please wait \n\n\r"
|
const loadmsg = "\n\r "+loading[ Math.floor(Math.random()*1000) % loading.length ] + "..[please wait]"
|
||||||
const empowermsg = "\n\r"+text_reset+'"'+empower[ Math.floor(Math.random()*1000) % empower.length ] + '"\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}
|
this.emit('status',loadmsg)
|
||||||
}
|
this.emit('serial-output-string', motd + empowermsg + text_color + loadmsg + text_reset+"\n\r")
|
||||||
|
|
||||||
ISOTerminal.prototype.startVM = function(opts){
|
|
||||||
|
|
||||||
this.v86opts = opts
|
|
||||||
|
|
||||||
this.addEventListener('emulator-started', async (e) => {
|
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")
|
|
||||||
|
// OVERLAY FS *FIXME*
|
||||||
|
//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 line = ''
|
||||||
this.ready = false
|
this.ready = false
|
||||||
|
|
@ -275,31 +247,12 @@ ISOTerminal.prototype.startVM = function(opts){
|
||||||
const str = e.detail
|
const str = e.detail
|
||||||
|
|
||||||
// lets scan for a prompt so we can send a 'ready' event to the world
|
// 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 && str.match(/\n(\/ #|~%|\[.*\]>)/) ) this.postBoot()
|
||||||
|
|
||||||
if( this.ready || !this.opts.muteUntilPrompt ) this.emit('serial-output-string', e.detail )
|
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) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -327,27 +280,6 @@ ISOTerminal.prototype.bufferOutput = function(byte,cb,latency){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//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){
|
ISOTerminal.prototype.preventFrameDrop = function(cb){
|
||||||
// don't let workers cause framerate dropping
|
// don't let workers cause framerate dropping
|
||||||
const xr = this.instance.sceneEl.renderer.xr
|
const xr = this.instance.sceneEl.renderer.xr
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,7 @@ function PromiseWorker(file, onmessage){
|
||||||
this.resolvers = this.resolvers || {last:1,pending:{}}
|
this.resolvers = this.resolvers || {last:1,pending:{}}
|
||||||
msg.data.promiseId = this.resolvers.last++
|
msg.data.promiseId = this.resolvers.last++
|
||||||
// Send id and task to WebWorker
|
// Send id and task to WebWorker
|
||||||
let dataTransferable = PromiseWorker.prototype.getTransferable(msg.data)
|
worker.postMessage(msg, PromiseWorker.prototype.getTransferable(msg.data) )
|
||||||
worker.postMessage(msg, dataTransferable )
|
|
||||||
return new Promise( resolve => this.resolvers.pending[ msg.data.promiseId ] = resolve );
|
return new Promise( resolve => this.resolvers.pending[ msg.data.promiseId ] = resolve );
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -73,5 +72,6 @@ PromiseWorker.prototype.getTransferable = function(data){
|
||||||
for( var i in data ){
|
for( var i in data ){
|
||||||
if( isTransferable(data[i]) ) objs.push(data[i])
|
if( isTransferable(data[i]) ) objs.push(data[i])
|
||||||
}
|
}
|
||||||
|
if( objs.length ) debugger
|
||||||
return objs.length ? objs : undefined
|
return objs.length ? objs : undefined
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1349
com/isoterminal/VT100.js
Normal file
1349
com/isoterminal/VT100.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -21,6 +21,7 @@ emulator.fs9p.update_file = async function(file,data){
|
||||||
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;
|
||||||
|
me.postMessage({event:'exec',data:[`touch /mnt/${file}`]}) // update inode
|
||||||
return new Promise( (resolve,reject) => resolve(buf) )
|
return new Promise( (resolve,reject) => resolve(buf) )
|
||||||
}catch(e){
|
}catch(e){
|
||||||
console.error({file,data})
|
console.error({file,data})
|
||||||
|
|
@ -28,20 +29,6 @@ emulator.fs9p.update_file = async function(file,data){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 convert = ISOTerminal.prototype.convert
|
||||||
|
|
||||||
|
|
@ -63,47 +50,3 @@ emulator.fs9p.append_file = async function(file,data){
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,16 +1,8 @@
|
||||||
// this is restoring state to/from the v86 emulator
|
|
||||||
// however instead of passing the huge blob between webworker/browser
|
|
||||||
// we transfer it via localforage as a base64 string
|
|
||||||
|
|
||||||
if( typeof emulator != 'undefined' ){
|
if( typeof emulator != 'undefined' ){
|
||||||
// inside worker-thread
|
// inside worker-thread
|
||||||
importScripts("localforage.js") // we don't instance it again here (just use its functions)
|
importScripts("localforage.js") // we don't instance it again here (just use its functions)
|
||||||
|
|
||||||
this.restore_state = async function(data){
|
this['emulator.restore_state'] = async function(data){
|
||||||
// fastforward instance state
|
|
||||||
this.opts.muteUntilPrompt = false
|
|
||||||
this.ready = true
|
|
||||||
|
|
||||||
return new Promise( (resolve,reject) => {
|
return new Promise( (resolve,reject) => {
|
||||||
localforage.getItem("state", async (err,stateBase64) => {
|
localforage.getItem("state", async (err,stateBase64) => {
|
||||||
if( stateBase64 && !err ){
|
if( stateBase64 && !err ){
|
||||||
|
|
@ -22,22 +14,18 @@ if( typeof emulator != 'undefined' ){
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.save_state = async function(){
|
this['emulator.save_state'] = async function(){
|
||||||
return new Promise( async (resolve,reject ) => {
|
|
||||||
console.log("saving session")
|
console.log("saving session")
|
||||||
let state = await emulator.save_state()
|
let state = await emulator.save_state()
|
||||||
localforage.setDriver([
|
localforage.setDriver([
|
||||||
localforage.INDEXEDDB,
|
localforage.INDEXEDDB,
|
||||||
localforage.WEBSQL,
|
localforage.WEBSQL,
|
||||||
localforage.LOCALSTORAGE
|
localforage.LOCALSTORAGE
|
||||||
])
|
]).then( () => {
|
||||||
.then( () => {
|
|
||||||
localforage.setItem("state", ISOTerminal.prototype.convert.arrayBufferToBase64(state) )
|
localforage.setItem("state", ISOTerminal.prototype.convert.arrayBufferToBase64(state) )
|
||||||
console.log("state saved")
|
console.log("state saved")
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
.catch( reject )
|
|
||||||
})
|
})
|
||||||
|
console.dir(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -45,62 +33,41 @@ if( typeof emulator != 'undefined' ){
|
||||||
// inside browser-thread
|
// inside browser-thread
|
||||||
ISOTerminal.addEventListener('emulator-started', function(e){
|
ISOTerminal.addEventListener('emulator-started', function(e){
|
||||||
this.autorestore(e)
|
this.autorestore(e)
|
||||||
this.emit("autorestore-installed")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ISOTerminal.prototype.restore = async function(e){
|
ISOTerminal.prototype.autorestore = async function(e){
|
||||||
|
|
||||||
const onGetItem = (err,stateBase64) => {
|
localforage.setDriver([
|
||||||
const askConfirm = () => {
|
localforage.INDEXEDDB,
|
||||||
if( window.localStorage.getItem("restorestate") == "true" ) return true
|
localforage.WEBSQL,
|
||||||
try{
|
localforage.LOCALSTORAGE
|
||||||
const scene = document.querySelector('a-scene');
|
]).then( () => {
|
||||||
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() ){
|
localforage.getItem("state", async (err,stateBase64) => {
|
||||||
|
if( stateBase64 && !err && confirm('continue last session?') ){
|
||||||
this.noboot = true // see feat/boot.js
|
this.noboot = true // see feat/boot.js
|
||||||
try{
|
try{
|
||||||
this.worker.restore_state()
|
await this.worker['emulator.restore_state']()
|
||||||
.then( () => {
|
|
||||||
// simulate / fastforward boot events
|
// simulate / fastforward boot events
|
||||||
this.postBoot( () => {
|
this.postBoot( () => {
|
||||||
// force redraw terminal issue
|
this.send("l\n")
|
||||||
this.send("l")
|
this.send("hook wakeup\n")
|
||||||
setTimeout( () => this.send("l"), 200 )
|
|
||||||
//this.send("12")
|
|
||||||
this.emit("exec",["source /etc/profile.sh; hook wakeup\n"])
|
|
||||||
this.emit("restored")
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}catch(e){ console.error(e) }
|
}catch(e){ console.error(e) }
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.save = async () => {
|
||||||
|
await this.worker['emulator.save_state']()
|
||||||
}
|
}
|
||||||
|
|
||||||
const doRestore = () => {
|
|
||||||
|
|
||||||
localforage.getItem("state", (err,stateBase64) => onGetItem(err,stateBase64) )
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", function (e) {
|
window.addEventListener("beforeunload", function (e) {
|
||||||
var confirmationMessage = "Sure you want to leave?\nTIP: enter 'save' to continue this session later";
|
var confirmationMessage = "Sure you want to leave?\nTIP: enter 'save' to continue this session later";
|
||||||
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
|
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
|
||||||
return confirmationMessage; //Webkit, Safari, Chrome
|
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,30 +1,7 @@
|
||||||
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 = [
|
||||||
|
|
@ -33,18 +10,11 @@ ISOTerminal.prototype.boot = async function(e){
|
||||||
'export BROWSER=1',
|
'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()+'="'+decodeURIComponent( document.location[i]+'"') )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await this.emit("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 ){
|
||||||
|
|
@ -54,33 +24,3 @@ ISOTerminal.prototype.boot = async function(e){
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ISOTerminal.prototype.boot.fromImage = false
|
|
||||||
ISOTerminal.prototype.boot.menu = []
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -6,7 +6,7 @@ if( typeof emulator != 'undefined' ){
|
||||||
const convert = ISOTerminal.prototype.convert
|
const convert = ISOTerminal.prototype.convert
|
||||||
const buf = await this.emulator.read_file("dev/browser/js")
|
const buf = await this.emulator.read_file("dev/browser/js")
|
||||||
const script = convert.Uint8ArrayToString(buf)
|
const script = convert.Uint8ArrayToString(buf)
|
||||||
let PID=null
|
let PID="?"
|
||||||
try{
|
try{
|
||||||
if( script.match(/^PID/) ){
|
if( script.match(/^PID/) ){
|
||||||
PID = script.match(/^PID=([0-9]+);/)[1]
|
PID = script.match(/^PID=([0-9]+);/)[1]
|
||||||
|
|
@ -20,52 +20,22 @@ if( typeof emulator != 'undefined' ){
|
||||||
}else{
|
}else{
|
||||||
// inside browser-thread
|
// 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){
|
ISOTerminal.addEventListener('javascript-eval', async function(e){
|
||||||
const {script,PID} = e.detail
|
const {script,PID} = e.detail
|
||||||
let res;
|
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{
|
try{
|
||||||
let f = new Function(`${script}`);
|
res = (new Function(`${script}`))()
|
||||||
res = f();
|
if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2)
|
||||||
if( res && typeof res.then == 'function' ){ // if we got a promise
|
}catch(e){
|
||||||
res.then( (res) => output(res,PID) )
|
console.error(e)
|
||||||
res.catch( (e) => output(e,PID) )
|
console.info(script)
|
||||||
}else{ // normal sync function
|
res = "error: "+e.toString()
|
||||||
output(res,PID)
|
if( e.filename ){
|
||||||
|
res += "\n"+e.filename+":"+e.lineno+":"+e.colno
|
||||||
}
|
}
|
||||||
}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)
|
|
||||||
}
|
}
|
||||||
|
// update output to 9p with PID as filename (in /mnt/run)
|
||||||
|
this.emit('fs9p.update_file', [`run/${PID}`, this.convert.toUint8Array(res)] )
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,40 @@
|
||||||
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]
|
||||||
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]
|
||||||
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]
|
||||||
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(){
|
||||||
|
|
||||||
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 ){
|
this.emit('append_file', ["/dev/browser/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) {
|
||||||
|
|
@ -58,52 +49,4 @@ ISOTerminal.prototype.enableConsole = function(opts){
|
||||||
console.error(event);
|
console.error(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
if( opts.stdout ){
|
|
||||||
window.menu = () => this.bootMenu()
|
|
||||||
this.emit('serial-output-string', "\n\n\r☑ initialized javascript console\n");
|
|
||||||
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) )
|
|
||||||
}
|
|
||||||
120
com/isoterminal/feat/xterm.js
Normal file
120
com/isoterminal/feat/xterm.js
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
|
||||||
|
term.onRender( () => {
|
||||||
|
// xterm relies on requestAnimationFrame (which does not called in immersive mode)
|
||||||
|
let _window = term._core._coreBrowserService._window
|
||||||
|
if( !_window._XRSH_proxied ){ // patch the planet!
|
||||||
|
|
||||||
|
//_window.requestAnimationFrameAFRAME = function(cb){
|
||||||
|
// if( term.tid != null ) clearTimeout(term.tid)
|
||||||
|
// term.tid = setTimeout( function(){
|
||||||
|
// console.log("render")
|
||||||
|
// cb()
|
||||||
|
// term.tid = null
|
||||||
|
// },100)
|
||||||
|
//}
|
||||||
|
this.i = 0
|
||||||
|
const requestAnimationFrameAFRAME = AFRAME.utils.throttleLeadingAndTrailing(
|
||||||
|
function(cb){ cb() }
|
||||||
|
,150
|
||||||
|
)
|
||||||
|
|
||||||
|
// we proxy the _window object of xterm, and reroute
|
||||||
|
// requestAnimationFrame to requestAnimationFrameAFRAME
|
||||||
|
_window_new = new Proxy(_window,{
|
||||||
|
get(me,k){
|
||||||
|
if( k == '_XRSH_proxied' ) return true
|
||||||
|
if( k == 'requestAnimationFrame' ){
|
||||||
|
return requestAnimationFrameAFRAME.bind(me)
|
||||||
|
}
|
||||||
|
return typeof me[k] == 'function' ? me[k].bind(me) : me[k]
|
||||||
|
},
|
||||||
|
set(me,k,v){
|
||||||
|
me[k] = v
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
term._core._coreBrowserService._window = _window_new
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
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( () => {
|
||||||
|
if( isoterm?.emulator?.serial_adapter?.term ){
|
||||||
|
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) );
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
|
@ -1,7 +1,7 @@
|
||||||
importScripts("libv86.js");
|
importScripts("libv86.js");
|
||||||
importScripts("ISOTerminal.js") // we don't instance it again here (just use its functions)
|
importScripts("ISOTerminal.js") // we don't instance it again here (just use its functions)
|
||||||
|
|
||||||
this.runISO = async function(opts){
|
this.runISO = function(opts){
|
||||||
this.opts = opts
|
this.opts = opts
|
||||||
if( opts.debug ) console.dir(opts)
|
if( opts.debug ) console.dir(opts)
|
||||||
|
|
||||||
|
|
@ -12,52 +12,40 @@ this.runISO = async function(opts){
|
||||||
console.log("[worker.js] started emulator")
|
console.log("[worker.js] started emulator")
|
||||||
|
|
||||||
// event forwarding
|
// event forwarding
|
||||||
emulator.buf0 = {}
|
|
||||||
emulator.buf1 = {}
|
|
||||||
emulator.buf2 = {}
|
|
||||||
|
|
||||||
emulator.add_listener("serial0-output-byte", function(byte){
|
emulator.add_listener("serial0-output-byte", function(byte){
|
||||||
ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping
|
ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping
|
||||||
if( !str ) return
|
if( !str ) return
|
||||||
this.postMessage({event:"serial0-output-string",data:str});
|
this.postMessage({event:"serial0-output-string",data:str});
|
||||||
}, opts.bufferLatency, emulator.buf0 )
|
}, opts.bufferLatency )
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
emulator.add_listener("serial1-output-byte", function(byte){
|
emulator.add_listener("serial1-output-byte", function(byte){
|
||||||
ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping
|
ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping
|
||||||
if( !str ) return
|
if( !str ) return
|
||||||
this.postMessage({event:"serial1-output-string",data:str});
|
this.postMessage({event:"serial1-output-string",data:str});
|
||||||
}, opts.bufferLatency, emulator.buf1 )
|
}, opts.bufferLatency )
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
emulator.add_listener("serial2-output-byte", function(byte){
|
emulator.add_listener("serial2-output-byte", function(byte){
|
||||||
ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping
|
ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping
|
||||||
if( !str ) return
|
if( !str ) return
|
||||||
this.postMessage({event:"serial2-output-string",data:str});
|
this.postMessage({event:"serial2-output-string",data:str});
|
||||||
}, opts.bufferLatency, emulator.buf2 )
|
}, opts.bufferLatency )
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
emulator.add_listener("emulator-started", function(){
|
emulator.add_listener("emulator-started", function(){
|
||||||
importScripts("feat/9pfs_utils.js")
|
importScripts("feat/9pfs_utils.js")
|
||||||
this.postMessage({event:"emulator-started",data:false});
|
this.postMessage({event:"emulator-started",data:false});
|
||||||
if( opts.img ) this.restoreImage(opts)
|
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* forward events/functions so non-worker world can reach them
|
* forward events/functions so non-worker world can reach them
|
||||||
*/
|
*/
|
||||||
|
this['emulator.create_file'] = async function(){ return emulator.create_file.apply(emulator, arguments[0]) }
|
||||||
// stripping '/mnt' is needed (the 9p mounted fs does not know about this)
|
this['emulator.read_file'] = async function(){ return emulator.read_file.apply(emulator, arguments[0]) }
|
||||||
const stripMountDir = (arr) => {
|
this['emulator.append_file'] = async function(){ emulator.fs9p.append_file.apply(emulator.fs9p, arguments[0]) }
|
||||||
arr[0] = String(arr[0]).replace(/^\/mnt/,'')
|
this['emulator.update_file'] = async function(){ emulator.fs9p.update_file.apply(emulator.fs9p, arguments[0]) }
|
||||||
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
|
// filename will be read from 9pfs: "/mnt/"+filename
|
||||||
emulator.readFromPipe = function(filename,cb){
|
emulator.readFromPipe = function(filename,cb){
|
||||||
|
|
@ -71,9 +59,6 @@ this.runISO = async function(opts){
|
||||||
importScripts("feat/javascript.js")
|
importScripts("feat/javascript.js")
|
||||||
importScripts("feat/index.html.js")
|
importScripts("feat/index.html.js")
|
||||||
importScripts("feat/autorestore.js")
|
importScripts("feat/autorestore.js")
|
||||||
|
|
||||||
if( opts.overlayfs ) await this.addOverlayFS(opts)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
* forward events/functions so non-worker world can reach them
|
* forward events/functions so non-worker world can reach them
|
||||||
|
|
@ -99,36 +84,3 @@ this.onmessage = async function(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
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
@ -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 )
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
112
com/window.js
112
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,18 +43,21 @@ 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
|
||||||
|
|
||||||
|
|
@ -100,60 +70,20 @@ AFRAME.registerComponent('window', {
|
||||||
if( els.length < 2 ) return
|
if( els.length < 2 ) return
|
||||||
let current = els[ els.length-1 ]
|
let current = els[ els.length-1 ]
|
||||||
let last = els[ els.length-2 ]
|
let last = els[ els.length-2 ]
|
||||||
AFRAME.utils.positionObjectNextToNeighbor( current.object3D , last.object3D, 0.02 )
|
AFRAME.utils.positionObjectNextToNeighbor( current.object3D , last.object3D, els.length )
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
show: function(state){
|
show: function(state){
|
||||||
this.el.dom.closest('.winbox').style.display = state ? '' : 'none'
|
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 ){
|
AFRAME.utils.positionObjectNextToNeighbor = function positionObjectNextToNeighbor(object, lastNeighbor = null, neighbours, margin = 0.45, degree = 20) {
|
||||||
if( lastNeighbor == null || object == null) return
|
|
||||||
// *FIXME* this could be more sophisticated :)
|
// *FIXME* this could be more sophisticated :)
|
||||||
object.position.x = lastNeighbor.position.x + margin
|
object.position.x = lastNeighbor.position.x + ((neighbours-1) * margin)
|
||||||
object.position.y = lastNeighbor.position.y - margin
|
object.position.y = lastNeighbor.position.y
|
||||||
object.position.z = lastNeighbor.position.z + margin
|
object.position.z = lastNeighbor.position.z
|
||||||
|
//object.rotation.y += THREE.MathUtils.degToRad( (neighbours-1) * degree);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
268
com/xterm.js
Normal file
268
com/xterm.js
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* 2019 Mauve Ranger
|
||||||
|
* 2024 Leon van Kammen
|
||||||
|
*/
|
||||||
|
|
||||||
|
let terminalInstance = 0
|
||||||
|
|
||||||
|
const TERMINAL_THEME = {
|
||||||
|
theme_foreground: {
|
||||||
|
// 'default': '#ffffff'
|
||||||
|
},
|
||||||
|
theme_background: {
|
||||||
|
// 'default': '#000'
|
||||||
|
},
|
||||||
|
theme_cursor: {
|
||||||
|
// 'default': '#ffffff'
|
||||||
|
},
|
||||||
|
theme_selection: {
|
||||||
|
// 'default': 'rgba(255, 255, 255, 0.3)'
|
||||||
|
},
|
||||||
|
theme_black: {
|
||||||
|
// 'default': '#000000'
|
||||||
|
},
|
||||||
|
theme_red: {
|
||||||
|
// 'default': '#e06c75'
|
||||||
|
},
|
||||||
|
theme_brightRed: {
|
||||||
|
// 'default': '#e06c75'
|
||||||
|
},
|
||||||
|
theme_green: {
|
||||||
|
// 'default': '#A4EFA1'
|
||||||
|
},
|
||||||
|
theme_brightGreen: {
|
||||||
|
// 'default': '#A4EFA1'
|
||||||
|
},
|
||||||
|
theme_brightYellow: {
|
||||||
|
// 'default': '#EDDC96'
|
||||||
|
},
|
||||||
|
theme_yellow: {
|
||||||
|
// 'default': '#EDDC96'
|
||||||
|
},
|
||||||
|
theme_magenta: {
|
||||||
|
// 'default': '#e39ef7'
|
||||||
|
},
|
||||||
|
theme_brightMagenta: {
|
||||||
|
// 'default': '#e39ef7'
|
||||||
|
},
|
||||||
|
theme_cyan: {
|
||||||
|
// 'default': '#5fcbd8'
|
||||||
|
},
|
||||||
|
theme_brightBlue: {
|
||||||
|
// 'default': '#5fcbd8'
|
||||||
|
},
|
||||||
|
theme_brightCyan: {
|
||||||
|
// 'default': '#5fcbd8'
|
||||||
|
},
|
||||||
|
theme_blue: {
|
||||||
|
// 'default': '#5fcbd8'
|
||||||
|
},
|
||||||
|
theme_white: {
|
||||||
|
// 'default': '#d0d0d0'
|
||||||
|
},
|
||||||
|
theme_brightBlack: {
|
||||||
|
// 'default': '#808080'
|
||||||
|
},
|
||||||
|
theme_brightWhite: {
|
||||||
|
// 'default': '#ffffff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AFRAME.registerComponent('xterm', {
|
||||||
|
schema: Object.assign({
|
||||||
|
XRrenderer: { type: 'string', default: 'canvas', },
|
||||||
|
cols: { type: 'number', default: 110, },
|
||||||
|
rows: { type: 'number', default: Math.floor( (window.innerHeight * 0.7 ) * 0.054 ) },
|
||||||
|
canvasLatency:{ type:'number', default: 200 }
|
||||||
|
}, TERMINAL_THEME),
|
||||||
|
|
||||||
|
write: function(message) {
|
||||||
|
this.term.write(message)
|
||||||
|
},
|
||||||
|
init: function () {
|
||||||
|
const terminalElement = document.createElement('div')
|
||||||
|
terminalElement.setAttribute('style', `
|
||||||
|
width: 800px;
|
||||||
|
height: ${Math.floor( 800 * 0.527 )}px;
|
||||||
|
overflow: hidden;
|
||||||
|
`)
|
||||||
|
|
||||||
|
this.el.terminalElement = terminalElement
|
||||||
|
|
||||||
|
if( this.data.XRrenderer == 'canvas' ){
|
||||||
|
// setup slightly bigger black backdrop (this.el.getObject3D("mesh"))
|
||||||
|
// and terminal text (this.el.planeText.getObject("mesh"))
|
||||||
|
const w = 2;
|
||||||
|
const h = (this.data.rows*5/this.data.cols)
|
||||||
|
this.el.setAttribute("geometry",`primitive: box; width:${w}; height:${h}; depth: -0.12`)
|
||||||
|
this.el.setAttribute("material","shader:flat; color:black; opacity:0.5; transparent:true; ")
|
||||||
|
this.el.planeText = document.createElement('a-entity')
|
||||||
|
this.el.planeText.setAttribute("geometry",`primitive: plane; width:${w}; height:${h}`)
|
||||||
|
this.el.appendChild(this.el.planeText)
|
||||||
|
|
||||||
|
// we switch between dom/canvas rendering because canvas looks pixely in nonimmersive mode
|
||||||
|
this.el.sceneEl.addEventListener('enter-vr', this.enterImmersive.bind(this) )
|
||||||
|
this.el.sceneEl.addEventListener('enter-ar', this.enterImmersive.bind(this) )
|
||||||
|
this.el.sceneEl.addEventListener('exit-vr', this.exitImmersive.bind(this) )
|
||||||
|
this.el.sceneEl.addEventListener('exit-ar', this.exitImmersive.bind(this) )
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tick = AFRAME.utils.throttleLeadingAndTrailing( () => {
|
||||||
|
if( this.el.sceneEl.renderer.xr.isPresenting ){
|
||||||
|
// workaround
|
||||||
|
// xterm relies on window.requestAnimationFrame (which is not called WebXR immersive mode)
|
||||||
|
//this.term._core.viewport._innerRefresh()
|
||||||
|
this.term._core.renderer._renderDebouncer._innerRefresh()
|
||||||
|
}
|
||||||
|
}, this.data.canvasLatency)
|
||||||
|
|
||||||
|
// Build up a theme object
|
||||||
|
const theme = Object.keys(this.data).reduce((theme, key) => {
|
||||||
|
if (!key.startsWith('theme_')) return theme
|
||||||
|
const data = this.data[key]
|
||||||
|
if(!data) return theme
|
||||||
|
theme[key.slice('theme_'.length)] = data
|
||||||
|
return theme
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
this.fontSize = 14
|
||||||
|
|
||||||
|
const term = this.term = new Terminal({
|
||||||
|
logLevel:"off",
|
||||||
|
theme: theme,
|
||||||
|
allowTransparency: true,
|
||||||
|
cursorBlink: true,
|
||||||
|
disableStdin: false,
|
||||||
|
rows: this.data.rows,
|
||||||
|
cols: this.data.cols,
|
||||||
|
fontFamily: 'Cousine, monospace',
|
||||||
|
fontSize: this.fontSize,
|
||||||
|
lineHeight: 1.15,
|
||||||
|
useFlowControl: true,
|
||||||
|
rendererType: this.renderType // 'dom' // 'canvas'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.term.open(terminalElement)
|
||||||
|
this.term.focus()
|
||||||
|
this.setRenderType('dom')
|
||||||
|
|
||||||
|
terminalElement.querySelector('.xterm-viewport').style.background = 'transparent'
|
||||||
|
|
||||||
|
// now we can scale canvases to the parent element
|
||||||
|
const $screen = terminalElement.querySelector('.xterm-screen')
|
||||||
|
$screen.style.width = '100%'
|
||||||
|
|
||||||
|
term.on('refresh', AFRAME.utils.throttleLeadingAndTrailing( () => this.update(), 150 ) )
|
||||||
|
term.on('data', (data) => {
|
||||||
|
this.el.emit('xterm-input', data)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.el.addEventListener('serial-output-byte', (e) => {
|
||||||
|
const byte = e.detail
|
||||||
|
var chr = String.fromCharCode(byte);
|
||||||
|
this.term.write(chr)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.el.addEventListener('serial-output-string', (e) => {
|
||||||
|
this.term.write(e.detail)
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
update: function(){
|
||||||
|
if( this.renderType == 'canvas' ){
|
||||||
|
const material = this.el.planeText.getObject3D('mesh').material
|
||||||
|
if (!material.map ) return
|
||||||
|
if( this.cursorCanvas ) this.canvasContext.drawImage(this.cursorCanvas, 0,0)
|
||||||
|
else console.log("no cursorCanvas")
|
||||||
|
material.map.needsUpdate = true
|
||||||
|
//material.needsUpdate = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setRenderType: function(type){
|
||||||
|
|
||||||
|
|
||||||
|
if( type.match(/(dom|canvas)/) ){
|
||||||
|
|
||||||
|
if( type == 'dom'){
|
||||||
|
this.el.dom.appendChild(this.el.terminalElement)
|
||||||
|
this.term.setOption('fontSize', this.fontSize )
|
||||||
|
this.term.setOption('rendererType',type )
|
||||||
|
this.renderType = type
|
||||||
|
}
|
||||||
|
|
||||||
|
if( type == 'canvas'){
|
||||||
|
this.el.appendChild(this.el.terminalElement)
|
||||||
|
this.term.setOption('fontSize', this.fontSize * 3 )
|
||||||
|
this.term.setOption('rendererType',type )
|
||||||
|
this.renderType = type
|
||||||
|
this.update()
|
||||||
|
setTimeout( () => {
|
||||||
|
this.canvas = this.el.terminalElement.querySelector('.xterm-text-layer')
|
||||||
|
this.canvas.id = "xterm-canvas"
|
||||||
|
this.canvasContext = this.canvas.getContext('2d')
|
||||||
|
this.cursorCanvas = this.el.terminalElement.querySelector('.xterm-cursor-layer')
|
||||||
|
// Create a texture from the canvas
|
||||||
|
const canvasTexture = new THREE.Texture(this.canvas)
|
||||||
|
//canvasTexture.minFilter = THREE.NearestFilter //LinearFilter
|
||||||
|
//canvasTexture.magFilter = THREE.LinearMipMapLinearFilter //THREE.NearestFilter //LinearFilter
|
||||||
|
canvasTexture.needsUpdate = true; // Ensure the texture updates
|
||||||
|
let plane = this.el.planeText.getObject3D("mesh") //this.el.getObject3D('mesh')
|
||||||
|
if( plane.material ) plane.material.dispose()
|
||||||
|
plane.material = new THREE.MeshBasicMaterial({
|
||||||
|
map: canvasTexture, // Set the texture from the canvas
|
||||||
|
transparent: false, // Set transparency
|
||||||
|
//side: THREE.DoubleSide // Set to double-sided rendering
|
||||||
|
//blending: THREE.AdditiveBlending
|
||||||
|
});
|
||||||
|
this.el.object3D.scale.x = 0.2
|
||||||
|
this.el.object3D.scale.y = 0.2
|
||||||
|
this.el.object3D.scale.z = 0.2
|
||||||
|
},100)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.el.terminalElement.style.opacity = type == 'canvas' ? 0 : 1
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
enterImmersive: function(){
|
||||||
|
if( this.mode == 'immersive' ) return
|
||||||
|
this.el.object3D.visible = true
|
||||||
|
this.mode = "immersive"
|
||||||
|
this.setRenderType('canvas')
|
||||||
|
this.term.focus()
|
||||||
|
},
|
||||||
|
|
||||||
|
exitImmersive: function(){
|
||||||
|
if( this.mode == 'nonimmersive' ) return
|
||||||
|
this.el.object3D.visible = false
|
||||||
|
this.mode = "nonimmersive"
|
||||||
|
this.setRenderType('dom')
|
||||||
|
},
|
||||||
|
|
||||||
|
})
|
||||||
Loading…
Add table
Reference in a new issue