diff --git a/manyfold/README.md b/manyfold/README.md index b1eaea8..4847875 100644 --- a/manyfold/README.md +++ b/manyfold/README.md @@ -53,6 +53,7 @@ $ docker run -t xrforge docker.io/coderofsalvation/xrforge:latest -v ./config:/c | `APPNAME` | `XRForge` | manyfold instance name | | `HOMEPAGE` | `/models` | show '/models' URL as homepage (use `/` for manyfold default) | | `THEME` | `default` | bootstrap theme | +| 'JANUSXR' | `` | run local JanusXR stack (janus-server, janus-gateway, janusweb) | | `AFRAME_VERSION` | `1.7.0` | AFRAME version | | `GODOT_VERSION` | `4.4.1-stable`| godot editor version | | `GODOT_TEMPLATE_ZIP` | `` | godot template zip URL or file (default is empty godot project) | @@ -188,3 +189,32 @@ For a quick dev-environment run: $ mkdir /dev $ manyfold/cli/manyfold.sh run -e DEV=1 ``` + +# JanusXR + +When running xrforge with the `JANUSXR=1` env-flag, the opensource [JanusXR](https://janusxr.org) stack will be installed and started: + +* [janus-server](https://github.com/janusvr/janus-server) for chat + syncing avatar positions +* [janus-gateway](https://github.com/meetecho/janus-gateway) for video/audio +* [janusweb](https://github.com/meetecho/janus-gateway) the viewer using the above services + +> NOTE: consider this a fingers-crossed 'rolling release' installation, as this is not officially part of XRForge (just a helper for intranets). + +Note that janus-server exposes a http websocket at port 5566, so you need to configure your reverse proxy as following: + +* https://presence.foo.bar.com => 5566 + +> This assumes environment-var `FEDERATE_DRIVE_HOST` is set to `https://foo.bar.com` (`presence` subdomain is automatically prefixed by the installer) + +#### persist JanusXR stack + +When running the container run the following cmds to speed up the boot-time: + +``` +$ docker cp xrforge:/mnt/janusweb . +$ docker cp xrforge:/root/janus-server . +``` + +then add the following flags to your docker cmd: `-v ./janusweb:/mnt/janusweb -v ./janus-server:/root/janus-server` + +> That way JanusXR does not have to be installed every time during boot diff --git a/manyfold/cli/manyfold.sh b/manyfold/cli/manyfold.sh index 011897e..981c29e 100755 --- a/manyfold/cli/manyfold.sh +++ b/manyfold/cli/manyfold.sh @@ -28,7 +28,8 @@ run(){ #-e NO_DEFAULTDB=true \ #-e PUBLIC_HOSTNAME=localhost \ #-e PUBLIC_PORT=80 \ - echo ${oci} run "$@" -p 8790:3214 -p 8791:3215 --name xrforge \ + #-e JANUSXR=1 \ + echo ${oci} run "$@" -p 8790:3214 -p 8791:3215 -p 5566:5566 -p 5577:5577 --name xrforge \ -e SECRET_KEY_BASE=lkjwljlkwejrlkjek34k234l \ -e DATABASE_ADAPTER=sqlite3 \ -e FEDERATE_DRIVE_HOST=http://localhost:8791 \ @@ -49,6 +50,7 @@ overlayfs(){ echocolor "[$APPNAME]" "applying filesystem overlay" cd /manyfold rsync -rvzi * /. + #apply_patches } # cron-like function using sleep (./manifold.sh infinite 3600 zip -r /backup.zip /) @@ -99,6 +101,20 @@ start_hook_daemon(){ #find /mnt | grep datapackage | xargs -n1 $0 hook inotify_MODIFY } +apply_patches(){ + echocolor "[$APPNAME]" "applying patches" + for patch_file in /manyfold/patches/*.patch; do + if patch -p1 -N --forward < "$patch_file"; then + echo "✅ Successfully applied **$(basename "$patch_file")**" + else + echo "🛑 Failed to apply **$(basename "$patch_file")**" + echo "Aborting script. Please inspect the failed patch and resolve conflicts." + # Use 'exit' to stop the script on the first failure + exit 1 + fi + done +} + db(){ default(){ @@ -133,7 +149,7 @@ set_upload_path(){ } mount_dir(){ - find /mnt -type d -mindepth 1 -maxdepth 1 | while read dir; do + find /mnt -type d -mindepth 1 -maxdepth 1 | grep -v janusweb | while read dir; do echocolor "[$APPNAME]" "mounting $dir as library" add_lib_to_db "$dir" done @@ -235,21 +251,49 @@ force_public(){ infinite 60 rails_query 'Model.find_each { |it| it.grant_permission_to("view", nil) }' & } -get_xrfragment_assets(){ +import_assets(){ test -n "$NO_ASSETS" && return 0 # nothing to do here - test -d /mnt/asset || { - echocolor "fetching XR Fragments asset & templates" - mkdir -p /mnt/asset/xrfragments /mnt/templates/xrfragments - cd /tmp - timeout 50 wget "https://codeberg.org/coderofsalvation/xrfragment/archive/main.zip" - unzip main.zip - cp -r xrfragment/assets/library /mnt/asset/xrfragments/\#1 - find xrfragment/assets/template -maxdepth 1 -mindepth 1 -type d | awk '{ system("cp -r "$0" /mnt/templates/xrfragments/#"(NR+1)) }' - } add_lib_to_db /mnt/asset add_lib_to_db /mnt/templates } +janusxr(){ + start_server(){ + while sleep 2s; do + flock -n "$@" + echocolor "[$1]" "'$2 $3 $4' exited (why?)...restarting" + done + } + + cd /root/corsanywhere + npm install + PORT=5577 start_server ~/.run-corsanywhere node server.js & + + cd /root + chmod +x janus_server-linux + PORT=5566 start_server ~/.run-janus-server ./janus_server-linux & + + ## we should do this in nix/docker.nix but the image gets into GB's :/ + #which git || apk add git + #which janus || apk add janus-gateway + #which node || apk add nodejs + #which bash || apk add bash + #cd /root + + ## install server + #test -d janus-server || git clone --depth 1 https://github.com/janusvr/janus-server + #cd janus-server + #test -d node_modules || { apk add npm && npm install; } + #test -f config.js || ln -f /root/.config/janus-server/config.js . + #start_server(){ + # while sleep 2s; do + # flock -n ~/.janus-server node server.js + # echocolor "[janus-server]" "'node server.js' exited (why?)...restarting" + # done + #} + #test -f ~/.janus-server || start_server & +} + init_database(){ test -f ${db}.xrforgeinit && exit 0 # already inited sleep 3 @@ -261,7 +305,7 @@ init_database(){ set_global model_path_template "replace('--- \"{creator}/{modelId} \"\\n','\\n',char(10))" set_upload_path #set_global about "$ABOUT" - get_xrfragment_assets + import_assets mount_dir BOOT_SCAN=1 scan_libraries & touch ${db}.xrforgeinit @@ -276,6 +320,8 @@ boot(){ set_homepage start_hook_daemon mount_rclone + janusxr + cp /root/templates/ARhome/* /mnt/janusweb/. force_public & # enable development mode (disables template caching etc) diff --git a/manyfold/mnt/janusweb/media/assets/webui/preview.html b/manyfold/mnt/janusweb/media/assets/webui/preview.html new file mode 100644 index 0000000..7573c47 --- /dev/null +++ b/manyfold/mnt/janusweb/media/assets/webui/preview.html @@ -0,0 +1 @@ + diff --git a/manyfold/mnt/janusweb/media/assets/webui/preview.json b/manyfold/mnt/janusweb/media/assets/webui/preview.json new file mode 100644 index 0000000..84dbc57 --- /dev/null +++ b/manyfold/mnt/janusweb/media/assets/webui/preview.json @@ -0,0 +1,20 @@ +{ + "apps": { + "inventory": "./apps/inventory/inventory.json", + "editor": "./apps/editor/editor.json", + "locomotion": "./apps/locomotion/locomotion.json", + "virtualgamepad": "./apps/virtualgamepad/virtualgamepad.json", + "buttons": "./apps/buttons/buttons.json", + "xrmenu": "./apps/xrmenu/xrmenu.json" + }, + "includes": [ + ], + "templates": { + "janusweb.ui": "./preview.html" + }, + "css": [ + "./themes/preview.css" + ], + "scripts": [ + ] +} diff --git a/manyfold/mnt/janusweb/media/assets/webui/themes/preview.css b/manyfold/mnt/janusweb/media/assets/webui/themes/preview.css new file mode 100644 index 0000000..44c6328 --- /dev/null +++ b/manyfold/mnt/janusweb/media/assets/webui/themes/preview.css @@ -0,0 +1,1377 @@ +@font-face { + font-family: "Montserrat"; + src: url('../fonts/Montserrat-Regular.ttf'); +} + + +html { + margin: 0; + padding: 0; + height: 100%; +} +body { + background: #333; + color: #eee; + font-family: "Montserrat", sans-serif; + height: 100%; + margin: 0; + padding: 0; +} +div[data-elation-component="janusweb.client"] h2 { + border: 1px solid black; + background: #4cb96f; + padding: 0 .2em; + box-shadow: 0px 0px 5px rgba(0,0,0,.8); + color: black; +} + +.dark *.error { + color: red; +} + +*[hidden] { + display: none !important; +} + +/************** + * Containers * + **************/ + +/* */ +ui-list, +ui-checklist { + display: block; + list-style: none; + position: relative; + overflow: auto; + border-radius: .5em; + border: 1px solid #111; + background: rgba(0,0,0,.1); + margin: 0; + padding: 0; +} +ui-list>ui-item, +ui-checklist>ui-checklistitem { + display: block; + transform: translate3d(0, 0, 0); + position: relative; + user-select: none; + border: 1px solid transparent; + padding: .2em .5em; + border-bottom: 1px solid rgba(0,0,0,.2); +} +ui-list>ui-item:first-of-type +ui-checklist>ui-checklistitem:first-of-type { +} +ui-list>ui-item:last-of-type, +ui-checklist>ui-checklistitem:last-of-type { + border-bottom: 0; +} +ui-list>ui-item[hover], +ui-checklist>ui-checklistitem[hover] { + background-color: rgba(128,128,255,.05); +} +ui-list[selectable]>ui-item[hover] { + background-color: rgba(128,128,255,.2); + cursor: pointer; +} +ui-list>ui-item.state_selected, +ui-checklist>ui-checklistitem.state_selected { + background: rgba(128,128,255,.5); +} +ui-list[selectable] { + cursor: text; + user-select: text; +} +ui-list>[selectable] { + cursor: text; + user-select: text; +} + +/* */ +ui-grid { + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(10em, 1fr)); +} +ui-grid[scrollable] { + overflow-x: auto; + overflow-y: auto; +} +ui-grid[scrollable-x] { + overflow-x: auto; + overflow-y: hidden; +} +ui-grid[scrollable-y] { + overflow-x: hidden; + overflow-y: auto; +} +ui-grid>ui-item { + display: inline-block; + border-radius: .5em; + border: 1px solid #111; + background: rgba(0,0,0,.1); + margin: .2em; + padding: .2em; +} +ui-grid>ui-item[hover] { + background-color: rgba(128,128,255,.2); +} +ui-grid.twocolumn>ui-item { + width: 50%; +} +ui-grid.threecolumn>ui-item { + width: calc(33% - .8em - 2px); +} +ui-grid.fourcolumn>ui-item { + width: 25%; +} +ui-grid.fivecolumn>ui-item { + width: 20%; +} +ui-grid.sixcolumn>ui-item { + width: 16%; +} +ui-grid.sevencolumn>ui-item { + width: 14%; +} +ui-grid.eightcolumn>ui-item { + width: 12.5%; +} +ui-grid.ninecolumn>ui-item { + width: 11%; +} +ui-grid.tencolumn>ui-item { + width: 10%; +} +ui-grid.fivecolumn>ui-item { + width: 20%; +} + +/* */ +ui-tabs { + display: flex; + flex-direction: column; + position: relative; + flex: 1 1; +} +/* */ +ui-tab { +} +ui-tabs>ui-tab { + display: none; + position: relative; + border: 1px solid black; + border-top: 0; + padding: .5em; + background: #3a3f44; +} +ui-tabs>ui-tabbar { + position: relative; + display: block; + color: white; + border-bottom: 1px solid #1c1e22; + font-size: .8em; + z-index: -1px; + order: -1; + white-space: nowrap; +} +ui-tabs>ui-tabbar>ui-button, +ui-tabs>ui-tabbar>ui-tabcountbutton { + min-width: 5em; + text-align: center; + padding: 0em .4em; + border: 1px solid #1c1e22; + border-bottom: 1px solid #1c1e22; + border-radius: .5em .5em 0 0; + margin-bottom: 0; + white-space: nowrap; + display: inline-block; + max-width:10em; + text-overflow: ellipsis; + direction: rtl; + overflow: hidden; +} +ui-tabs>ui-tabbar>ui-button[selected], +ui-tabs>ui-tabbar>ui-button[selected][hover], +ui-tabs>ui-tabbar>ui-tabcountbutton[selected], +ui-tabs>ui-tabbar>ui-tabcountbutton[selected][hover] { + background-color: #4cb96f; + background-repeat: no-repeat; + + border: 1px solid #1c1e22; + border-bottom-color: transparent; +} +ui-tabs>ui-tabbar>ui-button[hover], +ui-tabs>ui-tabbar>ui-tabcountbutton[hover] { + background-image: linear-gradient(rgba(128,128,128,1), rgba(128,128,128,1) 60%, rgba(128,128,128,1)); + background-repeat: no-repeat; + border-color: #1c1e22 #1c1e22 transparent #1c1e22; +} +ui-tabs>ui-tabbar>ui-button[disabled], +ui-tabs>ui-tabbar>ui-tabcountbutton[disabled] { + color: #777; +} +ui-tabs>ui-tab[selected] { + display: flex; + flex: 1 1; + flex-direction: column; +} +ui-tabcountbutton>ui-indicator::before { + content: '('; +} +ui-tabcountbutton>ui-indicator::after { + content: ')'; +} +/* */ +ui-panel, +ui-collapsiblepanel { + position: absolute; + z-index: 5; +} +ui-panel[top], +ui-collapsiblepanel[top] { + top: 0; +} +ui-panel[bottom], +ui-collapsiblepanel[bottom] { + bottom: 0; +} +ui-panel[left], +ui-collapsiblepanel[left] { + left: 0; +} +ui-panel[right], +ui-collapsiblepanel[right] { + right: 0; +} +ui-panel[top]:not([left]):not([right]) { + left: 50%; + transform: translateX(-50%); +} + +ui-flexpanel { + display: flex; + width: calc(100% - 1em); + height: calc(100% - 1em); + padding: .5em; +} +ui-flexpanel[vertical] { + flex-direction: column; +} +ui-flexpanel>* { + flex: 1 1; +} +ui-flexpanel>*[noflex] { + flex: 0 1; +} + +/* */ +ui-window { + position: absolute; + top: 0; + left: 0; + vertical-align: top; + z-index: 5; + border: 1px solid black; + background-color: rgba(0,0,0,.5); + border-radius: .5em; + border: 1px solid #666; + box-shadow: 1px 1px 10px rgba(0,0,0,1); + color: white; + display: flex; + flex-direction: column; +} +ui-window>ui-window-titlebar { + display: flex; + position: relative; + cursor: default; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + margin: 0; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + vertical-align: middle; + border-bottom: 1px solid #666; + order: 0; +} +ui-window.state_movable>ui-window-titlebar:hover { + cursor: -webkit-grab; + cursor: -moz-grab; +} +ui-window.state_movable>ui-window-titlebar.state_dragging { + cursor: -webkit-grabbing; + cursor: -moz-grabbing; +} +ui-window>ui-window-titlebar>span { + flex: 1; + order: 1; + padding: 0 0 0 .5em; + font-size: 1em; + line-height: 1.8em; +} +ui-window>ui-window-titlebar>ui-buttonbar { + flex: 0; + order: 2; +} +ui-window>ui-window-titlebar>ui-buttonbar>ui-button { + background: none; + border: none; +} +ui-window>.ui_window_resizer { + position: absolute; + bottom: -2px; + right: -2px; + width: 16px; + height: 16px; + cursor: se-resize; + border-bottom: 2px solid #ccc; + border-right: 2px solid #ccc; + border-radius: 0 0 5px 0; + z-index: 10; +} +ui-window>.ui_window_content { + display: block; + overflow: auto; + min-width: 100%; + padding: .5em; + box-sizing: border-box; + -moz-box-sizing: border-box; + position: relative; +} +ui-window>ui-window-content { + order: 1; + position: relative; +} +ui-window[scrollable]>ui-window-content { + overflow: auto; + display: block; +} +ui-window[maximized] { + z-index: 1000; + border-width: 0; + border-radius: 0; +} +ui-window[maximized]>ui-window-content { + max-width: none; + max-height: none; + padding: 0; +} +ui-window>.ui_window_resizer, +ui-window[left]>.ui_window_resizer { + right: -2px; + left: auto; + border-left: 0; + border-right: 2px solid #ccc; +} +ui-window[right]>.ui_window_resizer { + right: auto; + left: -2px; + border-right: 0; + border-left: 2px solid #ccc; +} +ui-window[top]>.ui_window_resizer { + bottom: -2px; + top: auto; + border-top: 0; + border-bottom: 2px solid #ccc; +} +ui-window[bottom]>.ui_window_resizer { + top: -2px; + bottom: auto; + border-bottom: 0; + border-top: 2px solid #ccc; +} +ui-window[top][right]>.ui_window_resizer { + border-radius: 0 0 0 5px; + cursor: sw-resize; +} +ui-window[top][left]>.ui_window_resizer { + border-radius: 0 0 5px 0; + cursor: se-resize; +} +ui-window[bottom][right]>.ui_window_resizer { + border-radius: 5px 0 0 0; + cursor: se-resize; +} +ui-window[bottom][left]>.ui_window_resizer { + border-radius: 0 5px 0 0; + cursor: sw-resize; +} + +ui-tooltip { + position: absolute; + top: 0; + left: 0; + vertical-align: top; + z-index: 5; + border: 1px solid black; + background-color: rgba(0,0,0,.5); + border-radius: .5em; + border: 1px solid #666; + box-shadow: 1px 1px 10px rgba(0,0,0,1); + color: white; + pointer-events: none; + white-space: nowrap; + padding: .2em .4em; +} + +/***************** + * Form Elements * + *****************/ + +/* */ +ui-input { + display: flex; +} +ui-input>ui-label { + min-width: 5em; +} +ui-input>input { + flex: 1 1; + background: #999; + border: 1px solid black; + border-radius: 4px; +} +ui-input>input:hover, +ui-input>input[hover] { + background: #aaa; +} +ui-input>input:focus { + background: #bbb; +} +ui-input>input:active { +} +ui-input>input:disabled { +} + +/* */ +ui-textarea { +} +ui-textarea>textarea { + resize: vertical; +} +ui-textarea>textarea[hover] { +} +ui-textarea>textarea:focus { +} +ui-textarea>textarea:active { +} +ui-textarea>textarea:disabled { +} + +/* */ +ui-dropdown { +} +ui-dropdown>ui-list { +} + +/* */ +ui-slider { +} +ui-slider[hover] { +} +ui-slider:active { +} +ui-slider-track { +} +ui-slider-track[hover] { +} +ui-slider-track:active { +} +ui-slider-handle { +} +ui-slider-handle[hover] { +} +ui-slider-handle:active { +} +ui-slider:disabled { +} +ui-slider:disabled ui-slider-track { +} +ui-slider:disabled ui-slider-handle { +} + +/* */ +ui-toggle { + display: flex; +} +ui-toggle>div { + border: 1px solid black; + border-radius: .5em; + padding: .5em; + position: relative; + user-select: none; + -ms-user-select: none; + cursor: pointer; + z-index: 2; + display: inline-block; + width: 2em; + vertical-align: middle; + margin: .2em .8em .2em .4em; + background: #333; +} +ui-toggle[hover]>div { + background: #444; +} +ui-toggle[align="left"]>div { + order: -1; +} +ui-toggle input { + display: none; +} +ui-toggle span { + cursor: pointer; +} +ui-toggle>div::before { + display: block; + position: absolute; + top: 2px; + left: 4px; + bottom: 2px; + width: 40%; + background: #700; + content: ' '; + z-index: -1; + border-radius: 10px; + box-shadow: 2px 0 5px rgba(0,0,0,.8); + content: ''; +/* + font-size: 0px; + font-weight: bold; + font-family: Arial, sans-serif; + text-align: center; + line-height: 1.8em; + color: #aaa; +*/ +} +ui-toggle>div::after { + display: none; + position: absolute; + top: 2px; + right: 4px; + bottom: 2px; + width: 40%; + content: ' '; + z-index: -1; + border-radius: .5em 0 0 .5em; + box-shadow: 0px 0px 5px rgba(0,0,0,.8); +} +ui-toggle[checked]>div::before { + right: 4px; + left: auto; + background: green; + box-shadow: -2px 0 5px rgba(0,0,0,.8); + border-radius: 10px; + content: ''; + color: #fff; +} +ui-toggle[checked]>div::after { + right: auto; + left: 4px; + border-radius: 0 .5em .5em 0; + display: none; +} +ui-toggle[disabled]>div { + color: #999; + cursor: not-allowed; +} +ui-toggle[disabled] span { + cursor: not-allowed; +} +ui-toggle[disabled]>div::before { + cursor: not-allowed; + background: #666; +} + +/* */ +ui-checkbox { + display: flex; + white-space: nowrap; + min-width: 6em; + cursor: pointer; + user-select: none; + -ms-user-select: none; + -moz-user-select: none; +} +ui-checkbox>input { + -webkit-appearance: none; + background-color: #fafafa; + border: 1px solid #cacece; + box-shadow: 0 1px 2px rgba(0,0,0,0.05), inset 0px -15px 10px -12px rgba(0,0,0,0.05); + padding: 9px; + border-radius: 3px; + display: inline-block; + position: relative; + vertical-align: middle; + cursor: pointer; + margin-right: .5em; + height: 1em; +} +ui-checkbox[align="left"]>input { + order: -1; +} +ui-checkbox>input:active, ui-checkbox>input:checked:active { + box-shadow: 0 1px 2px rgba(0,0,0,0.05), inset 0px 1px 3px rgba(0,0,0,0.1); +} +ui-checkbox[hover]>input, +ui-checkbox[hover]>input:checked { + box-shadow: 0 0 6px rgba(0,255,0,1); + background: #ddffee; +} + +ui-checkbox>input:checked { + background-color: #e9ecee; + border: 1px solid #adb8c0; + box-shadow: 0 1px 2px rgba(0,0,0,0.05), inset 0px -15px 10px -12px rgba(0,0,0,0.05), inset 15px 10px -12px rgba(255,255,255,0.1); + color: #99a1a7; +} +ui-checkbox>input:checked:after { + /* content: '\1f4a9'; */ + content: '\2714'; + font-size: 1em; + position: absolute; + top: 0px; + left: 0px; + right: 0; + bottom: 0; + color: #000; + text-align: center; + vertical-align: middle; +} +ui-checkbox[disabled] { + cursor: not-allowed; + color: #666; +} +ui-checkbox[disabled]>input { + background: #aaa; + color: #666; + pointer-events: none; +} +ui-checkbox[disabled][hover]>input, +ui-checkbox[disabled][hover]>input:checked { + box-shadow: none; + background: #aaa; +} +ui-slider { + display: inline-block; + min-width: 8em; +} +ui-slider>ui-slider-track { + width: 100%; + height: .5em; + margin: .5em 0; + background: #4cb96f; + border: 1px solid black; + border-radius: .2em; + display: inline-block; + cursor: pointer; + position: relative; +} +ui-slider>ui-slider-track[hover] { + background: #46db76; +} +ui-slider-handle { + display: block; + position: absolute; + width: .5em; + height: 1.2em; + border-radius: .5em; + border: 1px solid black; + background: #999; + cursor: pointer; + box-shadow: 0 0 5px rgba(0,0,0,.8); +} +ui-slider-handle>ui-label { + pointer-events: none; + position: absolute; + left: 20px; + z-index: 100; + background: rgba(0,0,0,.5); + border-radius: 20px; + padding: 0 .4em; +} +ui-slider-track[hover] ui-slider-handle>ui-label { + display: block !important; +} + + +/*********** + * Buttons * + ***********/ + +/* Common styling */ + +ui-button, +ui-togglebutton, +ui-dropdownbutton, +ui-tabcountbutton, +ui-popupbutton, +ui-notificationbutton { + display: inline-block; + border: none; + border-radius: .5em; + padding: .1em .5em; + color: white; + cursor: pointer; + text-align: center; + -ms-user-select: none; + -moz-user-select: none; + user-select: none; + + background-image: linear-gradient(#484e55, #3a3f44 60%, #313539); + background-color: transparent; + position: relative; +} +ui-button[hover], +ui-togglebutton[hover], +ui-dropdownbutton[hover], +ui-tabcountbutton[hover], +ui-popupbutton[hover], +ui-notificationbutton[hover] { + background-image: linear-gradient(#323232, #404142 40%, #393b3d); +} +ui-button:active, +ui-togglebutton:active, +ui-dropdownbutton:active, +ui-tabcountbutton:active, +ui-popupbutton:active, +ui-notificationbutton:active { + background-image: linear-gradient(#020202, #101112 40%, #191b1d); +} +ui-button[disabled], +ui-togglebutton:disabled, +ui-dropdownbutton:disabled, +ui-tabcountbutton:disabled, +ui-popupbutton:disabled, +ui-notificationbutton:disabled { + color: #999; + background-image: linear-gradient(#383e45, #2a2f34 60%, #212549); + cursor: not-allowed; +} + +/* */ +ui-button { +} +ui-button[hover] { +} +ui-button:active { +} +ui-button:disabled { +} + +/* */ +ui-togglebutton { +} +ui-togglebutton[hover] { +} +ui-togglebutton[active] { + background-image: linear-gradient(#028202, #109112 40%, #199b1d); +} +ui-togglebutton:disabled { +} + +/* */ +ui-dropdownbutton { + display: inline-block; + position: relative; + padding-right: 1em; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +ui-dropdownbutton::after { + display: inline-block; + content: '\25be'; + position: absolute; + right: 0; + top: 0; + padding-top: .2em; +} +ui-dropdownbutton[hover] { +} +ui-dropdownbutton:active { +} +ui-dropdownbutton:disabled { +} +ui-dropdownbutton>ui-button { + display: none; + border: none; + border-radius: 0; + width: 100%; +} +ui-dropdownbutton:active>ui-button { + display: block; +} + +/* */ +ui-notificationbutton { + position: relative; + padding: 0; + transform: scale(1, 1); + transition: transform 125ms ease-out; +} +ui-notificationbutton ui-indicator { + display: inline-block; + min-width: 1em; + top: -.8em; + right: -.8em; + font-size: .8em; + background: rgba(255,0,0,.8); + border: 1px solid rgba(0,0,0,.5); + border-radius: .5em; + text-align: center; + vertical-align: middle; + box-shadow: 0 0 5px rgba(0,0,0,.8); + padding: 0 .2em; +} +ui-notificationbutton[count="0"] { + transform: scale(0, 0); +} +ui-notificationbutton[hover] { +} +ui-notificationbutton:active { +} +ui-dropdownbutton:disabled { +} + +/* */ +ui-buttonbar, +ui-radiobuttonbar { + display: flex; +} +ui-buttonbar>ui-button, +ui-buttonbar>ui-togglebutton, +ui-radiobuttonbar>ui-togglebutton { + border-radius: 0; + border-right: 1px solid #333; + border-left: 0; + display: inline-block; +} +ui-buttonbar>ui-button:first-of-type, +ui-buttonbar>ui-togglebutton:first-of-type, +ui-radiobuttonbar>ui-togglebutton:first-of-type { + border-radius: .5em 0 0 .5em; + border-left: 1px solid transparent; +} +ui-buttonbar>ui-button:last-of-type, +ui-buttonbar>ui-togglebutton:last-of-type, +ui-radiobuttonbar>ui-togglebutton:last-of-type { + border-radius: 0 .5em .5em 0; + border-right: 1px solid transparent; +} + +/* */ +ui-buttonlist { + display: block; +} +ui-buttonlist>ui-button { + border-radius: 0; + border-right: 1px solid #333; + border-left: 0; + display: block; + text-align: center; +} +ui-buttonlist>ui-button:first-of-type { + border-radius: .5em .5em 0 0; + border-left: 1px solid transparent; +} +ui-buttonlist>ui-button:last-of-type { + border-radius: 0 0 .5em .5em; + border-right: 1px solid transparent; +} + +ui-label { + cursor: pointer; + user-select: none; + white-space: nowrap; + text-shadow: 0 0 1px #000; +} +ui-select { + white-space: nowrap; + display: flex; +} +ui-select>select { + background: #444; + color: white; + border: 1px solid #222; + box-shadow: 0 0 2px #222; +} +ui-select:hover>select { + background: #555; +} +ui-select>ui-label { + min-width: 5em; +} + + +ui-formgroup { + position: relative; + display: flex; + flex-direction: column; + margin: .5em 0; + width: 100%; +} +ui-formgroup>ui-label.groupheader { + display: block; + order: -1; + font-weight: bold; + border-bottom: 1px solid #666; + width: 100%; +} +ui-formgroup>ui-input, +ui-formgroup>ui-select, +ui-formgroup>ui-toggle, +ui-formgroup>ui-slider, +ui-formgroup>ui-textarea { + display: flex; + padding: .2em; +} +ui-formgroup>ui-input>input, +ui-formgroup>ui-textarea>textarea, +ui-formgroup>ui-select>select, +ui-formgroup>ui-slider>ui-slider-track { + flex: 1; +} +ui-formgroup ui-label, +ui-formgroup ui-text { + display: inline-block; + width: 10em; +} + +/* Column layout */ +ui-columnlayout { + display: flex; + flex-direction: row; + height: 100%; +} +ui-columnlayout>* { + width: calc(33% - 1px - 1em); + border-left: 1px solid #666; + margin-left: .5em; + padding-left: .5em; + flex: 1 1; + display: flex; + flex-direction: column; +} +ui-columnlayout>*:first-child { + border-left: 0; + margin-left: 0; +} + +/* Collapsible panel */ +ui-collapsiblepanel { + display: flex; + flex-direction: column; + max-width: calc(100% - 2em); + max-height: calc(100% - 2em); + min-height: .8em; + position: relative; + border: 1px solid black; + background: #333; + border-radius: 0; + padding: .2em; + transition: width 150ms ease-out,height 150ms ease-out,min-height 150ms ease-out; + box-shadow: 0 0 8px black; +} +ui-collapsiblepanel.default { + width: 24em; +} +ui-collapsiblepanel>.container { + display: flex; + width: 100%; + overflow: hidden; +} +ui-collapsiblepanel>.container>.container-inner { + transition: transform 150ms ease-out; + transform: translate(0, 0); + display: flex; + flex-direction: column; + flex: 1 1; +} +ui-collapsiblepanel>.container>.container-inner>* { + flex: 1 1; +} +ui-collapsiblepanel[top], +ui-collapsiblepanel[bottom] { + min-height: 12em +} +ui-collapsiblepanel[top] { + border-radius: 0 0 .5em 0; +} +ui-collapsiblepanel[bottom] { + border-radius: 0 .5em 0 0; +} +ui-collapsiblepanel[left] { + border-radius: 0 0 .5em 0; +} +ui-collapsiblepanel[right] { + border-radius: 0 0 0 .5em; +} +ui-collapsiblepanel[top][collapsed] { + height: 0; + min-height: 0; + padding-top: 0; +} +ui-collapsiblepanel[top][collapsed]>.container { + transform: translate(0, -100%); +} +ui-collapsiblepanel[bottom][collapsed] { + height: 0; + min-height: 0; + padding-bottom: 0; +} +ui-collapsiblepanel[left][collapsed] { + width: 0; + padding-left: 0; +} +ui-collapsiblepanel[left][collapsed]>.container { + transform: translate(-100%,0); +} +ui-collapsiblepanel[right][collapsed] { + width: 0; + padding-right: 0; +} +ui-collapsiblepanel>ui-togglebutton { + position: absolute; + transform-origin: bottom left; + background: #333; + border: 1px solid black; + line-height: 1em; +} +ui-collapsiblepanel[top]>ui-togglebutton { + bottom: -1.4em; + left: -1px; + border-radius: 0 0 .5em .5em; + border-top: none; +} +ui-collapsiblepanel[bottom]>ui-togglebutton { + top: -1.4em; + left: -1px; + border-radius: .5em .5em 0 0; + border-bottom: none; +} +ui-collapsiblepanel[left]>ui-togglebutton { + right: -1.4em; + top: -1px; + border-radius: 0 .5em .5em 0; + border-left: none; +} +ui-collapsiblepanel[right]>ui-togglebutton { + left: -1.4em; + top: -1px; + border-radius: .5em 0 0 .5em; + border-right: none; +} +janus-ui-main { + overflow: hidden; +} +ui-content[align="right"] { + align-content: end; +} + + +ui-treeview { +/* + background: black; + color: white; +*/ + display: block; +} +ui-treeview ui-list { + /*background: rgba(0,0,0,.8);*/ + list-style: none; + border-left: 0.15em solid #999; + margin: 0 0 0 .3em; + padding-left: .8em; + -webkit-user-select: none; + -webkit-tap-highlight-color: rgba(0,0,0,0); +} +ui-treeview>ui-list { + border-left: 0; + margin-left: 0; + padding-left: 0; +} +ui-treeview ui-treeviewitem { + position: relative; + white-space: nowrap; + /*max-height: 1.2em;*/ + overflow: hidden; + line-height: 1.2em; + display: flex; + flex-direction: column; + order: 2; + padding-left: 1em; +} +ui-treeview ui-treeviewitem[collapsed]>ui-treeviewitem { + display: none; +} +ui-treeview ui-treeviewitem>ui-text { + order: 1; + user-select: none; +} +ui-treeview ui-treeviewitem:hover>ui-text { + color: #9c9; + cursor: pointer; +} +ui-treeview ui-treeviewitem>ui-text:hover { + color: #7f7; +} +ui-treeview ui-treeviewitem.state_selected>ui-text { + color: #0f0; +} +ui-treeview ul li.state_selected, +ui-treeview ul li.state_hover li.state_selected { + max-height: none; +} +ui-treeview ui-treeviewitem ui-text:before { + content: ' '; + display: inline-block; + float: left; + clear: left; + width: .8em; + padding-right: .5em; +} +ui-treeview ui-treeviewitem.haschildren>ui-text:before { + content: '▼'; +} +ui-treeview ui-treeviewitem.haschildren[collapsed]>ui-text:before { + content: '▶'; +} +ui-treeview ui-treeviewitem:last-child>ul { + border-left: 0; +} +/* +ui-treeview ul li.state_disabled:before, +ui-treeview ul li.state_disabled, +ui-treeview ul li.state_hover li.state_disabled, +ui-treeview ul li.state_selected li.state_disabled { + color: #666; +} +ui-treeview ul li.state_hover, +ui-treeview ul li.state_selected li.state_hover { + color: yellow; + background: rgba(1,1,0,.5); +} +ui-treeview ul li.state_hover:before, +ui-treeview ul li.state_selected li.state_hover:before { + color: white; +} +ui-treeview ul li.state_selected, +ui-treeview ul li.state_hover li.state_selected { + color: red; +} +ui-treeview ul li.state_hover li, +ui-treeview ul li.state_selected li { + color: white; + background: transparent; +} +*/ + + + +/* Rotating CSS loading spinner by Don Sammut - http://codepen.io/domsammut/pen/eJbly */ +@keyframes rotate-loading { + 0% {transform: rotate(0deg);-ms-transform: rotate(0deg); -webkit-transform: rotate(0deg); -o-transform: rotate(0deg); -moz-transform: rotate(0deg);} + 100% {transform: rotate(360deg);-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); -o-transform: rotate(360deg); -moz-transform: rotate(360deg);} +} + +@keyframes rotate-loading { + 0% {transform: rotate(0deg);-ms-transform: rotate(0deg); -webkit-transform: rotate(0deg); -o-transform: rotate(0deg); -moz-transform: rotate(0deg);} + 100% {transform: rotate(360deg);-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); -o-transform: rotate(360deg); -moz-transform: rotate(360deg);} +} + +@keyframes loading-text-opacity { + 0% {opacity: .5} + 20% {opacity: .5} + 50% {opacity: 1} + 100%{opacity: .5} +} + +ui-spinner .loading-container, +ui-spinner .loading { + height: 100px; + position: relative; + width: 100px; + border-radius: 100%; +} + + +ui-spinner .loading-container { + overflow: hidden; + margin: 0px auto; +} + +ui-spinner .loading { + border: 2px solid transparent; + border-color: transparent #fff transparent #FFF; + -moz-animation: rotate-loading 1.5s linear 0s infinite normal; + -moz-transform-origin: 50% 50%; + -o-animation: rotate-loading 1.5s linear 0s infinite normal; + -o-transform-origin: 50% 50%; + -webkit-animation: rotate-loading 1.5s linear 0s infinite normal; + -webkit-transform-origin: 50% 50%; + animation: rotate-loading 1.5s linear 0s infinite normal; + transform-origin: 50% 50%; +} + +ui-spinner .loading-container:hover .loading { + border-color: transparent #E45635 transparent #E45635; +} +ui-spinner .loading-container:hover .loading, +ui-spinner .loading-container .loading { + -webkit-transition: all 0.5s ease-in-out; + -moz-transition: all 0.5s ease-in-out; + -ms-transition: all 0.5s ease-in-out; + -o-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out; +} + +ui-spinner .loading-text { + -moz-animation: loading-text-opacity 2s linear 0s infinite normal; + -o-animation: loading-text-opacity 2s linear 0s infinite normal; + -webkit-animation: loading-text-opacity 2s linear 0s infinite normal; + animation: loading-text-opacity 2s linear 0s infinite normal; + color: #ffffff; + font-family: "Helvetica Neue", "Helvetica", "arial"; + font-size: 10px; + font-weight: bold; + margin-top: 45px; + opacity: 0; + position: absolute; + text-align: center; + text-transform: uppercase; + top: 0; + width: 100px; +} + + +ui-spinner .loading-container.dark .loading { + border-color: transparent #000 transparent #000; +} +ui-spinner .loading-container.dark:hover .loading { + border-color: transparent #E45635 transparent #E45635; +} +ui-spinner .loading-container.dark .loading-text { + color: #000; +} +/* end css loading spinner */ + +ui-spinner[full] { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: rgba(0,0,0,.1); +} +ui-image-picker { + display: block; +} +ui-image-picker ui-label { + vertical-align: top; + min-width: 5em; + display: inline-block; +} +ui-image-picker canvas { + width: 30px; + height: 30px; + border: 1px solid black; + border-radius: 2px; + padding: 2px; + display: inline-block; + vertical-align: top; +} + +/* */ +ui-wizard { + display: flex; + flex-direction: column; + flex: 1; +} +ui-wizard[type="paginate"]>ui-wizard-step { + display: none; + opacity: 0; + transition: opacity 500ms linear; + flex: 1; +} +ui-wizard[step=""]>ui-wizard-backbutton, +ui-wizard[step="1"]>ui-wizard-backbutton { + display: none; +} +/* Up to 10 steps - wizards with more steps should probably be split up some */ +ui-wizard[type="paginate"][step=""]>ui-wizard-step:nth-of-type(1), +ui-wizard[type="paginate"][step="1"]>ui-wizard-step:nth-of-type(1), +ui-wizard[type="paginate"][step="2"]>ui-wizard-step:nth-of-type(2), +ui-wizard[type="paginate"][step="3"]>ui-wizard-step:nth-of-type(3), +ui-wizard[type="paginate"][step="4"]>ui-wizard-step:nth-of-type(4), +ui-wizard[type="paginate"][step="5"]>ui-wizard-step:nth-of-type(5), +ui-wizard[type="paginate"][step="6"]>ui-wizard-step:nth-of-type(6), +ui-wizard[type="paginate"][step="7"]>ui-wizard-step:nth-of-type(7), +ui-wizard[type="paginate"][step="8"]>ui-wizard-step:nth-of-type(8), +ui-wizard[type="paginate"][step="9"]>ui-wizard-step:nth-of-type(9), +ui-wizard[type="paginate"][step="10"]>ui-wizard-step:nth-of-type(10) { + display: flex; + opacity: 1; + flex: 1; + flex-direction: column; +} +ui-wizard-step>section { + padding: 0 .5em 0 .5em; +} +ui-wizard[type="paginate"] ui-wizard-pagination { + display: grid; + grid-template-columns: 50% 50%; + align-items: center; + justify-items: center; +} +ui-wizard[type="paginate"] ui-wizard-pagination ui-button.back { + grid-column-start: 1; +} +ui-wizard[type="paginate"] ui-wizard-pagination ui-button.next { + grid-column-start: 2; +} +ui-wizard-navigation { + display: flex; + flex-direction: row; +} +ui-wizard-navigation ui-buttonbar { + width: 100%; +} +ui-wizard-navigation ui-buttonbar ui-button { + margin: 0; + position: relative; + clip-path: polygon(0 0, 120% 0%, 120% 100%, 0 100%, 1lh 50%); + padding-left: 1lh; +} +ui-wizard-navigation ui-buttonbar ui-button:first-child { + clip-path: none; + border-radius: 0; +} +ui-wizard-navigation ui-buttonbar ui-button::after { + width:0; + height: 0; + content: ''; + display: block; + position: absolute; + right: calc(-1lh + 1px); + border-top: 1lh solid transparent; + border-bottom: 1lh solid transparent; + border-left: 1lh solid var(--logo-green); + top: 0; +} +ui-wizard-navigation ui-buttonbar ui-button:last-child::after { + display: none; +} +ui-wizard-navigation ui-buttonbar ui-button[disabled]::after { + border-left: 1lh solid #666; +} +ui-wizard-navigation ui-buttonbar ui-button.state_active { + background: white; +} +ui-wizard-navigation ui-buttonbar ui-button.state_active::after { + border-left: 1lh solid white; +} diff --git a/manyfold/root/hook.d/boot/httpserver.sh b/manyfold/root/hook.d/boot/httpserver.sh index a87db6b..4fef2a9 100755 --- a/manyfold/root/hook.d/boot/httpserver.sh +++ b/manyfold/root/hook.d/boot/httpserver.sh @@ -17,5 +17,5 @@ test -n "$FEDERATE_DRIVE_CERT" && test -m "$FEDERATE_DRIVE_KEY" && { set -x rclone serve http \ - --exclude .xrforge --poll-interval $FEDERATE_DRIVE_CACHE \ + --links --exclude .xrforge --poll-interval $FEDERATE_DRIVE_CACHE \ --addr 0.0.0.0:$FEDERATE_DRIVE_PORT ${AUTH} ${SSL} $FEDERATE_DRIVE_PATH &> /var/log/rclone.log & diff --git a/manyfold/root/hook.d/boot/runtests.sh b/manyfold/root/hook.d/boot/runtests.sh new file mode 100755 index 0000000..d2c6ce2 --- /dev/null +++ b/manyfold/root/hook.d/boot/runtests.sh @@ -0,0 +1,3 @@ +#!/bin/sh +test -n "$RUNTESTS" || exit 0 +exec /test/runtests.sh diff --git a/manyfold/root/hook.d/experience_updated/1000-extract-textures.rb b/manyfold/root/hook.d/experience_updated/1000-extract-textures.rb index 68b325e..cb1362d 100755 --- a/manyfold/root/hook.d/experience_updated/1000-extract-textures.rb +++ b/manyfold/root/hook.d/experience_updated/1000-extract-textures.rb @@ -52,15 +52,20 @@ begin # Iterate through the images array gltf['images'].each_with_index do |img, i| - new_filename = "#{dir}/#{filenameWithoutExt}_img#{i}.png" - old_filename = "#{dir}/.xrforge/#{filenameWithoutExt}_img#{i}.png" + imgExts = ["jpg","png"] + new_filename = "#{dir}/#{filenameWithoutExt}_img#{i}" + old_filename = "#{dir}/.xrforge/#{filenameWithoutExt}_img#{i}" - # move file to modeldirectory (but dont overwrite if user overwrite it) - if File.exist?(old_filename) && !File.exist?(new_filename) - puts "✅ Renaming #{old_filename} -> #{new_filename}" - File.rename(old_filename, new_filename) - else - puts "✅ Not overwriting (useruploaded) #{new_filename}" + imgExts.each do |imgExt| + # move file to modeldirectory (but dont overwrite if user overwrite it) + if File.exist?("#{old_filename}.#{imgExt}") && !File.exist?("#{new_filename}.#{imgExt}") + puts "✅ Renaming #{old_filename}.#{imgExt} -> #{new_filename}.#{imgExt}" + File.rename( "#{old_filename}.#{imgExt}", "#{new_filename}.#{imgExt}" ) + else + if File.exist?("#{new_filename}.#{imgExt}") + puts "✅ Not overwriting (useruploaded) #{new_filename}.#{imgExt}" + end + end end end end diff --git a/manyfold/root/hook.d/experience_updated/1050-compile-textures.rb b/manyfold/root/hook.d/experience_updated/1050-compile-textures.rb index 51acb11..484da0d 100755 --- a/manyfold/root/hook.d/experience_updated/1050-compile-textures.rb +++ b/manyfold/root/hook.d/experience_updated/1050-compile-textures.rb @@ -48,15 +48,17 @@ begin # Iterate through the images array gltf['images'].each_with_index do |img, i| - new_filename = "" - new_filename = "#{dir}/#{filenameWithoutExt}_img#{i}.png" # move file to modeldirectory (but dont overwrite if user overwrite it) - XRForge.log("🤔 checking #{new_filename}",logfile) - if File.exist?(new_filename) - XRForge.log("✅ importing #{new_filename} -> #{resource['path']}",logfile) - img['uri'] = new_filename # NOTE: editing uri will cause assimp to drop image['name'] when exporting :/ - update = true + imgExts = ["jpg","png"] + imgExts.each do |imgExt| + new_filename = "#{dir}/#{filenameWithoutExt}_img#{i}.#{imgExt}" + if File.exist?(new_filename) + XRForge.log("🤔 detected #{File.basename(new_filename)}",logfile) + XRForge.log("✅ importing #{File.basename(new_filename)} -> #{resource['path']}",logfile) + img['uri'] = new_filename # NOTE: editing uri will cause assimp to drop image['name'] when exporting :/ + update = true + end end end if update diff --git a/manyfold/root/hook.d/experience_updated/300-package_janusxr.rb b/manyfold/root/hook.d/experience_updated/300-package_janusxr.rb index 5a9eeb0..aa15c08 100755 --- a/manyfold/root/hook.d/experience_updated/300-package_janusxr.rb +++ b/manyfold/root/hook.d/experience_updated/300-package_janusxr.rb @@ -37,7 +37,7 @@ begin # Check if a model file was found after the loop if ! model_file XRForge.log("❌ No suitable 3D file found for JanusXR-compatible experience", logfile) - exit 0 + exit end # Get the value of the environment variable FEDERATE_DRIVE_HOST @@ -72,8 +72,11 @@ begin end end - private = data['keywords'].include?('singleuser') ? "private='true'" : "" + if ! data['keywords'] + data['keywords'] = [] + end + private = data['keywords'].include?('singleuser') ? "private='true'" : "" # tags to JML rooms *REFACTOR PLEASE* use_local_asset = "" use_local_asset = data['keywords'].include?('room1') ? "use_local_asset=\"room1\"" : use_local_asset @@ -86,14 +89,25 @@ begin use_local_asset = data['keywords'].include?('room2_pedestal') ? "use_local_asset=\"room2_pedestal\"" : use_local_asset use_local_asset = data['keywords'].include?('room2_narrow') ? "use_local_asset=\"room3_narrow\"" : use_local_asset - objects = objects + " " + objects = objects + " " + + #janusweb_src = "https://web.janusvr.com/janusweb.js" + janusweb_src = ! ENV['DEV'].empty? ? "/janusweb/janusweb.js" : "/janusweb/janusweb.min.js" + + server = "" + if ENV['JANUSXR'] && ! ENV['JANUSXR'].empty? + serverUrl = federate_drive_host.gsub("://","://presence.") + .gsub(/:[0-9].*/,"") + .gsub(/\/$/,"") + server = "server=#{serverUrl}:5566/" + end jml = <<~JML #{assets} - + #{objects} @@ -120,22 +134,34 @@ begin - janusxr room + #{data['title']} - JanusXR - - + #{jml} - + + + - + HTML diff --git a/manyfold/test/10-rclone.sh b/manyfold/test/10-rclone.sh index 33753a3..142a281 100755 --- a/manyfold/test/10-rclone.sh +++ b/manyfold/test/10-rclone.sh @@ -1,5 +1,5 @@ #!/bin/sh -which rclone &>/dev/null || { echo "[!] rclone not installed"; exit 0; } +which rclone &>/dev/null || { echo "❌ rclone not installed"; exit 1; } -test -d /mnt/models || { echo "[!] /mnt/models does not exist"; exit 0; } +test -d /mnt/experiences || { echo "❌ /mnt/models does not exist"; exit 1; } diff --git a/manyfold/test/1000-extract-textures.sh b/manyfold/test/1000-extract-textures.sh new file mode 100755 index 0000000..60c6b75 --- /dev/null +++ b/manyfold/test/1000-extract-textures.sh @@ -0,0 +1,24 @@ +#!/bin/sh +experience=/mnt/templates/xrfragments/\#4 +dir=/tmp/hook-extract-textures +xrfdir=$dir/.xrforge + +which assimp || { + echo "❌ assimp was not installed"; + exit 1; +} + +mkdir -p $xrfdir || true +rm $experience/*_img*.* || true +/root/hook.d/experience_updated/*-extract-textures.rb $experience/datapackage.json 2>&1 \ + | grep "Wrote texture 0" || { + echo "❌ $experience/murial3D_img0.png was not written"; + exit 1; +} +test -f $experience/murial3D_img0.png || { + echo "❌ $experience/murial3D_img0.png was not written"; + exit 1; +} + +rm -rf $dir +exit 0 diff --git a/manyfold/test/1050-compile-textures.sh b/manyfold/test/1050-compile-textures.sh new file mode 100755 index 0000000..c6a4117 --- /dev/null +++ b/manyfold/test/1050-compile-textures.sh @@ -0,0 +1,18 @@ +#!/bin/sh +experience=/mnt/templates/xrfragments/\#4 +dir=/tmp/hook-compile-textures +xrfdir=$dir/.xrforge + +which assimp || { + echo "❌ assimp was not installed"; + exit 1; +} + +mkdir -p $xrfdir || true +/root/hook.d/experience_updated/*-compile-textures.rb $experience/datapackage.json 2>&1 | grep "wrote output" || { + echo "❌ $experience/murial3D.glb was not updated"; + exit 1; +} + +rm -rf $dir +exit 0 diff --git a/manyfold/test/11-hook-reset-log.sh b/manyfold/test/11-hook-reset-log.sh new file mode 100755 index 0000000..1e6eb88 --- /dev/null +++ b/manyfold/test/11-hook-reset-log.sh @@ -0,0 +1,12 @@ +#!/bin/sh +dir=/tmp/11-hook +xrfdir=$dir/.xrforge +mkdir -p $xrfdir +echo 1 > $xrfdir/log.txt +/root/hook.d/experience_updated/*-reset-log.sh $dir/datapackage.json +test "$(cat $xrfdir/log.txt)" = "1" && { + echo "❌ log.txt was not reset"; + exit 1; +} +rm -rf $dir +exit 0 diff --git a/manyfold/test/300-package_godot_zip.sh b/manyfold/test/300-package_godot_zip.sh new file mode 100755 index 0000000..62a7ac3 --- /dev/null +++ b/manyfold/test/300-package_godot_zip.sh @@ -0,0 +1,12 @@ +#!/bin/sh +dir=/tmp/hook-package_godot_zip +xrfdir=$dir/.xrforge +mkdir -p $xrfdir +touch $xrfdir/foo.glb +/root/hook.d/experience_updated/*-package_godot_zip.sh $dir/datapackage.json +test -f $xrfdir/godot.zip || { + echo "❌ godot.zip was not created"; + exit 1; +} +rm -rf $dir +exit 0 diff --git a/manyfold/test/300-package_janusxr.sh b/manyfold/test/300-package_janusxr.sh new file mode 100755 index 0000000..3929955 --- /dev/null +++ b/manyfold/test/300-package_janusxr.sh @@ -0,0 +1,36 @@ +#!/bin/sh +id=janusxr +dir=/tmp/hook-package_$id +xrfdir=$dir/.xrforge +mkdir -p $xrfdir + +testjml(){ + /root/hook.d/experience_updated/*-package_$id.rb $dir/datapackage.json + test -f $xrfdir/scene.jml || { + echo "❌ scene.jml was not created"; + exit 1; + } + test -f $xrfdir/janusxr.html || { + echo "❌ janusxr.html was not created"; + exit 1; + } +} + +echo "### test xrfragment heuristic" +touch $dir/foo.png +touch $dir/foo.glb +echo '{ + "image":"foo.png", + "resources": [{"path":"foo.glb"}] +}' > $dir/datapackage.json +testjml + +echo "### test first-suitable-3d-file heuristic" +rm $dir/foo.png +echo '{ + "resources": [{"path":"foo.glb"}] +}' > $dir/datapackage.json +testjml + +rm -rf $dir +exit 0 diff --git a/manyfold/test/janus-server b/manyfold/test/janus-server new file mode 100755 index 0000000..f6cc70e --- /dev/null +++ b/manyfold/test/janus-server @@ -0,0 +1,15 @@ +#!/bin/sh +test $(ps aux | grep run-janus-server | wc -l) = 2 || { + echo "❌ janus-server is not running" + exit 1; +} +echo "✅ janus-server is running" + +test $(ps aux | grep run-corsanywhere | wc -l) = 2 || { + echo "❌ corsanywhere is not running" + exit 1; +} +echo "✅ corsanywhere is running" + + + diff --git a/manyfold/test/runtests.sh b/manyfold/test/runtests.sh index e6de882..af24d9b 100755 --- a/manyfold/test/runtests.sh +++ b/manyfold/test/runtests.sh @@ -1,13 +1,16 @@ #!/bin/sh -test -z "$RUNTESTS" && exit 0 # nothing to do - echo "" -echo "[!] RUNTESTS=1 was set " -echo "[.] running tests in /test/*" +echo "running tests in /test/*" echo "" -find -L /test/* -type f -executable -maxdepth 1 | while read testscript; do - echo "[.] test: "$testscript - $testscript "$@" 2>&1 | awk '{ print " | "$0 }' +error(){ echocolor "❌" "$*"; exit 1; } +ok(){ echocolor "✅" "$*"; return 0; } +echocolor(){ printf "\033[96m%s\033[0m \033[95m%s\033[0m %s\n" "$1" "$2" "$3"; } + +find -L /test/* -type f -executable -maxdepth 1 | grep -v runtests | sort -V | while read testscript; do + echo "🛠 $testscript" + { + $testscript "$@" 2>&1 && ok "$testscript" || error "$testscript" + } | awk '{ print " | "$0 }' done diff --git a/manyfold/usr/src/app/app/components/preview_frame.rb b/manyfold/usr/src/app/app/components/preview_frame.rb index c11e0af..de4bf36 100644 --- a/manyfold/usr/src/app/app/components/preview_frame.rb +++ b/manyfold/usr/src/app/app/components/preview_frame.rb @@ -14,7 +14,9 @@ class Components::PreviewFrame < Components::Base end def view_template - a href: "/view?#{model_model_file_path(@file.model, @file, format: @file.extension)}", target:"_blank" do + + a href: ENV['FEDERATE_DRIVE_HOST']+"/"+@file.model.library.name+"/"+@file.model.path.gsub("#","%23")+"/.xrforge/janusxr.html?networking=false", target: "_blank" do + #a href: "/view?#{model_model_file_path(@file.model, @file, format: @file.extension)}", target:"_blank", alt: "launch single-user experience" do if @file local elsif @object.remote? diff --git a/manyfold/usr/src/app/app/views/layouts/card_list_page.html.erb b/manyfold/usr/src/app/app/views/layouts/card_list_page.html.erb new file mode 100644 index 0000000..290d860 --- /dev/null +++ b/manyfold/usr/src/app/app/views/layouts/card_list_page.html.erb @@ -0,0 +1,21 @@ +<%= yield :page_header %> + +
+
+ + <% if ENV['DEV'] %> + + + + <% end %> + + <%= yield :items %> +
+ +
+ +<% parent_layout "application" %> diff --git a/manyfold/usr/src/app/app/views/models/show.html.erb b/manyfold/usr/src/app/app/views/models/show.html.erb index af3064c..45d3b10 100644 --- a/manyfold/usr/src/app/app/views/models/show.html.erb +++ b/manyfold/usr/src/app/app/views/models/show.html.erb @@ -23,6 +23,7 @@
+ <% if @locked_files > 0 %>
<%= t(".preview", count: @locked_files) %>
<% end %> diff --git a/manyfold/usr/src/app/config/initializers/xrforge.rb b/manyfold/usr/src/app/config/initializers/xrforge.rb index bd21b36..bc616df 100644 --- a/manyfold/usr/src/app/config/initializers/xrforge.rb +++ b/manyfold/usr/src/app/config/initializers/xrforge.rb @@ -31,7 +31,7 @@ Rails.application.config.to_prepare do # Use `write` to set the new time. Caching a string representation is often safer/easier. Rails.cache.write(cache_key, now.to_s, expires_in: ttl + 3.second) - puts "[app/config/initializers/xrforge.rb] running hook\n" + puts "[app/config/initializers/xrforge.rb] running hook #{file}\n" Bundler.with_unbundled_env do #`TS_SLOTS=5 ts /manyfold/cli/manyfold.sh hook experience_updated #{file} &` `/manyfold/cli/manyfold.sh hook experience_updated #{file}` diff --git a/manyfold/usr/src/app/public/assets/lobby.png b/manyfold/usr/src/app/public/assets/lobby.png new file mode 100644 index 0000000..fbee904 Binary files /dev/null and b/manyfold/usr/src/app/public/assets/lobby.png differ diff --git a/nix/docker.nix b/nix/docker.nix index 487ec9c..c1f842d 100644 --- a/nix/docker.nix +++ b/nix/docker.nix @@ -38,6 +38,68 @@ let finalImageTag = "latest"; }; + #### JANUSXR Stack + + janusweb = builtins.fetchTarball { + url = "https://github.com/coderofsalvation/janusweb/releases/download/1.5.56-xrf/janusweb-1.5.56.tar.gz"; + # Get the SHA256 hash by running: nix-prefetch-url --unpack + sha256 = "0zkmfv07zxdf1nkhgr4g959fj86p2yp9f7n1ll1zyhlm186sfh31"; + }; + + # Fetch the source from the GitHub tag + corsanywhere = pkgs.fetchzip { + url = "https://github.com/Rob--W/cors-anywhere/archive/0.4.4.tar.gz"; + # You need to calculate the correct SHA256 hash for version 0.4.4 + # You can get this by running 'nix-prefetch-url --unpack https://github.com/Rob--W/cors-anywhere/archive/0.4.4.tar.gz' + sha256 = "0zb3xzlpbc400rvibm66qb27y0lla8sml1ns41ksxcc0l0kp0bwz"; + }; + + janusServer = pkgs.fetchurl { + url = "https://github.com/janusvr/janus-server/releases/download/v0.2.1/janus_server-linux"; + # You can calculate the hash using: nix-prefetch-url https://... + sha256 = "1rrmabvqn4lm7c2k1q2crqwyk7lvpmfihc3hx3qx2hzsjyp5v3ja"; + name = "janus_server-linux"; + }; + + ## 2. Create a minimal derivation to make the file executable + #janusServerBin = pkgs.stdenv.mkDerivation { + # pname = "janus-server"; + # version = "1.0"; + # src = janusServer; + # dontUnpack = true; + # dontBuild = true; + # installPhase = '' + # mkdir -p $out/bin + # cp $src $out/bin/janus_server-linux + # chmod +x $out/bin/janus_server-linux + # ''; + #}; + + janus = pkgs.runCommand "janusweb-content" {} '' + mkdir -p $out/mnt/janusweb $out/root/corsanywhere $out/root + cp -rT ${janusweb} $out/mnt/janusweb + cp -rT ${corsanywhere} $out/root/corsanywhere/. + cp -rT ${janusServer} $out/root/janus_server-linux + ''; + + #### XR FRAGMENTS + + ## $ nix-shell -p nix-prefetch-git --command 'nix-prefetch-git https://codeberg.org/coderofsalvation/xrfragment.git ' + xrfragmentsRepo = pkgs.fetchFromGitea { + "domain" = "codeberg.org"; + "owner" = "coderofsalvation"; + "repo" = "xrfragment"; + "rev" = "823736a74dbdabd46924ebbc3dc58a076c3c900b"; + "hash" = "sha256-P/RVwoDu0EYfwc0n2h1f4j0JZshtE4AtQsTU9iEkAcA="; + }; + + xrfragments = pkgs.runCommand "xrfragments-content" {} '' + mkdir -p $out/mnt/asset/xrfragments $out/mnt/templates/xrfragments + cp -r ${xrfragmentsRepo}/assets/library $out/mnt/asset/xrfragments/\#1 + find ${xrfragmentsRepo}//assets/template -maxdepth 1 -mindepth 1 -type d | awk '{ system("cp -r "$0" '$out'/mnt/templates/xrfragments/#"(NR+1)) }' + ''; + + ## generate the reproducable blob below via: ## $ nix-shell -p nix-prefetch-github --command 'nix-prefetch-github assimp assimp --rev e778c84cd62bc8b38d8e491ad3d2c27cb8ed37d5' #assimpSrc = pkgs.fetchFromGitHub { @@ -66,19 +128,23 @@ rec # add nix pkgs + local files copyToRoot = pkgs.buildEnv { name = "image-root"; - pathsToLink = ["/manyfold" "/bin" ]; + pathsToLink = ["/manyfold" "/bin" "/mnt" "/root"]; paths = [ #pkgs.pkgsStatic.rsync pkgs.rsync pkgs.sqlite pkgs.rclone pkgs.fuse3 + pkgs.janus-gateway # webrtc voip for JanusXR + pkgs.nodejs_20 # for corsanywhere #pkgs.acl # getfacl e.g. #pkgs.inotify-tools # inotifywait e.g. pkgs.zip # inotifywait e.g. pkgs.assimp ##pkgs.ts # job management #myAssimp # updated build of assimp + janus + xrfragments ../. ]; };