Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80d4710f3f | ||
|
|
3eac2fbff1 | ||
|
|
37502c7ac7 | ||
|
|
fe798703ea |
No files matched your search
@@ -50,6 +50,8 @@ Press the paintbrush button at the right end of the dashboard bar. Its panel set
|
||||
|
||||
Press the music note button on the dashboard bar, just left of the paintbrush. The assPod appears in the hand you pressed it with.
|
||||
|
||||
**assPod Model** on its main menu switches the body: **Video Gen 1** (the original) or **Nano Gen 2**, in anodized aluminum with white end caps. The nano has the same menus, laid out for its smaller screen with larger text (six rows, like the real one), and it plays no video. **Color** lists the current body's finishes: Black and White for the Video; Silver, Green, Blue, Pink, Black and (PRODUCT)RED for the nano. The model and colours you pick are kept across launches.
|
||||
|
||||
## Nothing persists
|
||||
|
||||
**The theme is never written to disk.** `lgs` injects the stylesheet into the *running* Steam client through its local devtools socket (`127.0.0.1:8080`; Steam on the Frame runs with `-cef-enable-debugging`). Any of these brings back the stock UI:
|
||||
@@ -60,15 +62,16 @@ Press the music note button on the dashboard bar, just left of the paintbrush. T
|
||||
|
||||
Nothing starts at boot. There are no systemd units, autostart entries or Steam file patches.
|
||||
|
||||
Only two things stay installed, so the toggle is there after a reboot, plus one file of your own:
|
||||
Only two things stay installed, so the toggle is there after a reboot, plus two files of your own:
|
||||
|
||||
| Path | What it is |
|
||||
|---|---|
|
||||
| `~/.local/share/applications/glass-shell.desktop` | The "LiquidAss" entry in + › Launch Program |
|
||||
| `~/.local/share/glass-shell/` | The toggle script and the theme files it reads |
|
||||
| `~/.config/glass-shell/tune.json` | Your glass settings from the paintbrush button on the bar (colour, intensity, refraction, frost, highlights). Written only when you change them; **Reset to default** in that panel deletes it. The one exception to "nothing persists", by request |
|
||||
| `~/.config/glass-shell/tune.json` | Your glass settings from the paintbrush button on the bar (colour, intensity, refraction, frost, highlights). Written only when you change them; **Reset to default** in that panel deletes it. An exception to "nothing persists", by request |
|
||||
| `~/.config/glass-shell/asspod.json` | The assPod model and colours you picked. Written only when you change them. The other exception, by request |
|
||||
|
||||
The [uninstall](#uninstall) command (or `python glass.py uninstall`) removes all three. (The install paths keep the project's original `glass-shell` name, so existing installs carry over.)
|
||||
The [uninstall](#uninstall) command (or `python glass.py uninstall`) removes all of them. (The install paths keep the project's original `glass-shell` name, so existing installs carry over.)
|
||||
|
||||
## From the PC
|
||||
|
||||
@@ -81,7 +84,7 @@ python glass.py uninstall
|
||||
|
||||
On the headset itself, `~/.local/share/glass-shell/device/lgs on|off|toggle|status|dial V` does the same.
|
||||
|
||||
Native glass works out of the box. Releases ship glassd built for the Frame (`native/glassd/prebuilt/glassd`), and the installer puts it in place. `python glass.py install` does the same, then rebuilds glassd from the checkout's sources over it (if that build fails, the shipped binary stays). The daemon also puts the shipped binary in place whenever glassd is missing, so the LiquidAss toggle never starts CSS-only. Native mode is on by default (`device/defaults.json`).
|
||||
Native glass works out of the box. Releases ship glassd built for the Frame (`native/glassd/prebuilt/glassd`), and the installer puts it in place. `python glass.py install` does the same, then rebuilds glassd from the checkout's sources over it (if that build fails, the shipped binary stays). The daemon also puts the shipped binary in place whenever glassd is missing, so the LiquidAss toggle never starts CSS-only. Native mode is on by default (`device/defaults.json`). The assPod's 3D renderer, podd, ships the same way (`native/podd/prebuilt/podd`).
|
||||
|
||||
## How it works
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
newmtl nano_back_black
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_back_black_color.png
|
||||
@@ -0,0 +1,63 @@
|
||||
# nano_back_black: the assPod nano body (tools/nano2-model/export.html)
|
||||
mtllib nano_back_black.mtl
|
||||
usemtl nano_back_black
|
||||
v -0.016750 -0.045000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.045000 -0.003250
|
||||
v 0.016750 -0.045000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.045000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.045000 -0.003250
|
||||
v 0.016750 0.045000 -0.003250
|
||||
vt 0.918750 -0.000000
|
||||
vt 0.918750 0.333333
|
||||
vt 0.081250 -0.000000
|
||||
vt 0.081250 -0.000000
|
||||
vt 0.918750 0.333333
|
||||
vt 0.081250 0.333333
|
||||
vt 0.918750 0.333333
|
||||
vt 0.918750 0.666667
|
||||
vt 0.081250 0.333333
|
||||
vt 0.081250 0.333333
|
||||
vt 0.918750 0.666667
|
||||
vt 0.081250 0.666667
|
||||
vt 0.918750 0.666667
|
||||
vt 0.918750 1.000000
|
||||
vt 0.081250 0.666667
|
||||
vt 0.081250 0.666667
|
||||
vt 0.918750 1.000000
|
||||
vt 0.081250 1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
f 1/1/1 2/2/2 3/3/3
|
||||
f 4/4/4 5/5/5 6/6/6
|
||||
f 7/7/7 8/8/8 9/9/9
|
||||
f 10/10/10 11/11/11 12/12/12
|
||||
f 13/13/13 14/14/14 15/15/15
|
||||
f 16/16/16 17/17/17 18/18/18
|
||||
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_back_blue
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_back_blue_color.png
|
||||
@@ -0,0 +1,63 @@
|
||||
# nano_back_blue: the assPod nano body (tools/nano2-model/export.html)
|
||||
mtllib nano_back_blue.mtl
|
||||
usemtl nano_back_blue
|
||||
v -0.016750 -0.045000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.045000 -0.003250
|
||||
v 0.016750 -0.045000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.045000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.045000 -0.003250
|
||||
v 0.016750 0.045000 -0.003250
|
||||
vt 0.918750 -0.000000
|
||||
vt 0.918750 0.333333
|
||||
vt 0.081250 -0.000000
|
||||
vt 0.081250 -0.000000
|
||||
vt 0.918750 0.333333
|
||||
vt 0.081250 0.333333
|
||||
vt 0.918750 0.333333
|
||||
vt 0.918750 0.666667
|
||||
vt 0.081250 0.333333
|
||||
vt 0.081250 0.333333
|
||||
vt 0.918750 0.666667
|
||||
vt 0.081250 0.666667
|
||||
vt 0.918750 0.666667
|
||||
vt 0.918750 1.000000
|
||||
vt 0.081250 0.666667
|
||||
vt 0.081250 0.666667
|
||||
vt 0.918750 1.000000
|
||||
vt 0.081250 1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
f 1/1/1 2/2/2 3/3/3
|
||||
f 4/4/4 5/5/5 6/6/6
|
||||
f 7/7/7 8/8/8 9/9/9
|
||||
f 10/10/10 11/11/11 12/12/12
|
||||
f 13/13/13 14/14/14 15/15/15
|
||||
f 16/16/16 17/17/17 18/18/18
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_back_green
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_back_green_color.png
|
||||
@@ -0,0 +1,63 @@
|
||||
# nano_back_green: the assPod nano body (tools/nano2-model/export.html)
|
||||
mtllib nano_back_green.mtl
|
||||
usemtl nano_back_green
|
||||
v -0.016750 -0.045000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.045000 -0.003250
|
||||
v 0.016750 -0.045000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.045000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.045000 -0.003250
|
||||
v 0.016750 0.045000 -0.003250
|
||||
vt 0.918750 -0.000000
|
||||
vt 0.918750 0.333333
|
||||
vt 0.081250 -0.000000
|
||||
vt 0.081250 -0.000000
|
||||
vt 0.918750 0.333333
|
||||
vt 0.081250 0.333333
|
||||
vt 0.918750 0.333333
|
||||
vt 0.918750 0.666667
|
||||
vt 0.081250 0.333333
|
||||
vt 0.081250 0.333333
|
||||
vt 0.918750 0.666667
|
||||
vt 0.081250 0.666667
|
||||
vt 0.918750 0.666667
|
||||
vt 0.918750 1.000000
|
||||
vt 0.081250 0.666667
|
||||
vt 0.081250 0.666667
|
||||
vt 0.918750 1.000000
|
||||
vt 0.081250 1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
f 1/1/1 2/2/2 3/3/3
|
||||
f 4/4/4 5/5/5 6/6/6
|
||||
f 7/7/7 8/8/8 9/9/9
|
||||
f 10/10/10 11/11/11 12/12/12
|
||||
f 13/13/13 14/14/14 15/15/15
|
||||
f 16/16/16 17/17/17 18/18/18
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_back_pink
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_back_pink_color.png
|
||||
@@ -0,0 +1,63 @@
|
||||
# nano_back_pink: the assPod nano body (tools/nano2-model/export.html)
|
||||
mtllib nano_back_pink.mtl
|
||||
usemtl nano_back_pink
|
||||
v -0.016750 -0.045000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.045000 -0.003250
|
||||
v 0.016750 -0.045000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.045000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.045000 -0.003250
|
||||
v 0.016750 0.045000 -0.003250
|
||||
vt 0.918750 -0.000000
|
||||
vt 0.918750 0.333333
|
||||
vt 0.081250 -0.000000
|
||||
vt 0.081250 -0.000000
|
||||
vt 0.918750 0.333333
|
||||
vt 0.081250 0.333333
|
||||
vt 0.918750 0.333333
|
||||
vt 0.918750 0.666667
|
||||
vt 0.081250 0.333333
|
||||
vt 0.081250 0.333333
|
||||
vt 0.918750 0.666667
|
||||
vt 0.081250 0.666667
|
||||
vt 0.918750 0.666667
|
||||
vt 0.918750 1.000000
|
||||
vt 0.081250 0.666667
|
||||
vt 0.081250 0.666667
|
||||
vt 0.918750 1.000000
|
||||
vt 0.081250 1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
f 1/1/1 2/2/2 3/3/3
|
||||
f 4/4/4 5/5/5 6/6/6
|
||||
f 7/7/7 8/8/8 9/9/9
|
||||
f 10/10/10 11/11/11 12/12/12
|
||||
f 13/13/13 14/14/14 15/15/15
|
||||
f 16/16/16 17/17/17 18/18/18
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_back_red
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_back_red_color.png
|
||||
@@ -0,0 +1,63 @@
|
||||
# nano_back_red: the assPod nano body (tools/nano2-model/export.html)
|
||||
mtllib nano_back_red.mtl
|
||||
usemtl nano_back_red
|
||||
v -0.016750 -0.045000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.045000 -0.003250
|
||||
v 0.016750 -0.045000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.045000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.045000 -0.003250
|
||||
v 0.016750 0.045000 -0.003250
|
||||
vt 0.918750 -0.000000
|
||||
vt 0.918750 0.333333
|
||||
vt 0.081250 -0.000000
|
||||
vt 0.081250 -0.000000
|
||||
vt 0.918750 0.333333
|
||||
vt 0.081250 0.333333
|
||||
vt 0.918750 0.333333
|
||||
vt 0.918750 0.666667
|
||||
vt 0.081250 0.333333
|
||||
vt 0.081250 0.333333
|
||||
vt 0.918750 0.666667
|
||||
vt 0.081250 0.666667
|
||||
vt 0.918750 0.666667
|
||||
vt 0.918750 1.000000
|
||||
vt 0.081250 0.666667
|
||||
vt 0.081250 0.666667
|
||||
vt 0.918750 1.000000
|
||||
vt 0.081250 1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
f 1/1/1 2/2/2 3/3/3
|
||||
f 4/4/4 5/5/5 6/6/6
|
||||
f 7/7/7 8/8/8 9/9/9
|
||||
f 10/10/10 11/11/11 12/12/12
|
||||
f 13/13/13 14/14/14 15/15/15
|
||||
f 16/16/16 17/17/17 18/18/18
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_back_silver
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_back_silver_color.png
|
||||
@@ -0,0 +1,63 @@
|
||||
# nano_back_silver: the assPod nano body (tools/nano2-model/export.html)
|
||||
mtllib nano_back_silver.mtl
|
||||
usemtl nano_back_silver
|
||||
v -0.016750 -0.045000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.045000 -0.003250
|
||||
v 0.016750 -0.045000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v -0.016750 -0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v 0.016750 -0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.045000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v 0.016750 0.015000 -0.003250
|
||||
v -0.016750 0.045000 -0.003250
|
||||
v 0.016750 0.045000 -0.003250
|
||||
vt 0.918750 -0.000000
|
||||
vt 0.918750 0.333333
|
||||
vt 0.081250 -0.000000
|
||||
vt 0.081250 -0.000000
|
||||
vt 0.918750 0.333333
|
||||
vt 0.081250 0.333333
|
||||
vt 0.918750 0.333333
|
||||
vt 0.918750 0.666667
|
||||
vt 0.081250 0.333333
|
||||
vt 0.081250 0.333333
|
||||
vt 0.918750 0.666667
|
||||
vt 0.081250 0.666667
|
||||
vt 0.918750 0.666667
|
||||
vt 0.918750 1.000000
|
||||
vt 0.081250 0.666667
|
||||
vt 0.081250 0.666667
|
||||
vt 0.918750 1.000000
|
||||
vt 0.081250 1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
vn 0.000000 0.000000 -1.000000
|
||||
f 1/1/1 2/2/2 3/3/3
|
||||
f 4/4/4 5/5/5 6/6/6
|
||||
f 7/7/7 8/8/8 9/9/9
|
||||
f 10/10/10 11/11/11 12/12/12
|
||||
f 13/13/13 14/14/14 15/15/15
|
||||
f 16/16/16 17/17/17 18/18/18
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_cap_bottom
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.1 0.1 0.1
|
||||
map_Kd nano_cap_bottom_color.png
|
||||
@@ -0,0 +1,503 @@
|
||||
# nano_cap_bottom: the assPod nano body (tools/nano2-model/export.html)
|
||||
mtllib nano_cap_bottom.mtl
|
||||
usemtl nano_cap_bottom
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.016750 -0.044750 -0.002850
|
||||
v 0.017122 -0.044750 -0.002826
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.017122 -0.044750 -0.002826
|
||||
v 0.017488 -0.044750 -0.002753
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.017488 -0.044750 -0.002753
|
||||
v 0.017841 -0.044750 -0.002633
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.017841 -0.044750 -0.002633
|
||||
v 0.018175 -0.044750 -0.002468
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.018175 -0.044750 -0.002468
|
||||
v 0.018485 -0.044750 -0.002261
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.018485 -0.044750 -0.002261
|
||||
v 0.018765 -0.044750 -0.002015
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.018765 -0.044750 -0.002015
|
||||
v 0.019011 -0.044750 -0.001735
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.019011 -0.044750 -0.001735
|
||||
v 0.019218 -0.044750 -0.001425
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.019218 -0.044750 -0.001425
|
||||
v 0.019383 -0.044750 -0.001091
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.019383 -0.044750 -0.001091
|
||||
v 0.019503 -0.044750 -0.000738
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.019503 -0.044750 -0.000738
|
||||
v 0.019576 -0.044750 -0.000372
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.019576 -0.044750 -0.000372
|
||||
v 0.019600 -0.044750 0.000000
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.019600 -0.044750 0.000000
|
||||
v 0.019576 -0.044750 0.000372
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.019576 -0.044750 0.000372
|
||||
v 0.019503 -0.044750 0.000738
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.019503 -0.044750 0.000738
|
||||
v 0.019383 -0.044750 0.001091
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.019383 -0.044750 0.001091
|
||||
v 0.019218 -0.044750 0.001425
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.019218 -0.044750 0.001425
|
||||
v 0.019011 -0.044750 0.001735
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.019011 -0.044750 0.001735
|
||||
v 0.018765 -0.044750 0.002015
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.018765 -0.044750 0.002015
|
||||
v 0.018485 -0.044750 0.002261
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.018485 -0.044750 0.002261
|
||||
v 0.018175 -0.044750 0.002468
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.018175 -0.044750 0.002468
|
||||
v 0.017841 -0.044750 0.002633
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.017841 -0.044750 0.002633
|
||||
v 0.017488 -0.044750 0.002753
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.017488 -0.044750 0.002753
|
||||
v 0.017122 -0.044750 0.002826
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.017122 -0.044750 0.002826
|
||||
v 0.016750 -0.044750 0.002850
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v 0.016750 -0.044750 0.002850
|
||||
v -0.016750 -0.044750 0.002850
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.016750 -0.044750 0.002850
|
||||
v -0.017122 -0.044750 0.002826
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.017122 -0.044750 0.002826
|
||||
v -0.017488 -0.044750 0.002753
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.017488 -0.044750 0.002753
|
||||
v -0.017841 -0.044750 0.002633
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.017841 -0.044750 0.002633
|
||||
v -0.018175 -0.044750 0.002468
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.018175 -0.044750 0.002468
|
||||
v -0.018485 -0.044750 0.002261
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.018485 -0.044750 0.002261
|
||||
v -0.018765 -0.044750 0.002015
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.018765 -0.044750 0.002015
|
||||
v -0.019011 -0.044750 0.001735
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.019011 -0.044750 0.001735
|
||||
v -0.019218 -0.044750 0.001425
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.019218 -0.044750 0.001425
|
||||
v -0.019383 -0.044750 0.001091
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.019383 -0.044750 0.001091
|
||||
v -0.019503 -0.044750 0.000738
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.019503 -0.044750 0.000738
|
||||
v -0.019576 -0.044750 0.000372
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.019576 -0.044750 0.000372
|
||||
v -0.019600 -0.044750 0.000000
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.019600 -0.044750 0.000000
|
||||
v -0.019576 -0.044750 -0.000372
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.019576 -0.044750 -0.000372
|
||||
v -0.019503 -0.044750 -0.000738
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.019503 -0.044750 -0.000738
|
||||
v -0.019383 -0.044750 -0.001091
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.019383 -0.044750 -0.001091
|
||||
v -0.019218 -0.044750 -0.001425
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.019218 -0.044750 -0.001425
|
||||
v -0.019011 -0.044750 -0.001735
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.019011 -0.044750 -0.001735
|
||||
v -0.018765 -0.044750 -0.002015
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.018765 -0.044750 -0.002015
|
||||
v -0.018485 -0.044750 -0.002261
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.018485 -0.044750 -0.002261
|
||||
v -0.018175 -0.044750 -0.002468
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.018175 -0.044750 -0.002468
|
||||
v -0.017841 -0.044750 -0.002633
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.017841 -0.044750 -0.002633
|
||||
v -0.017488 -0.044750 -0.002753
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.017488 -0.044750 -0.002753
|
||||
v -0.017122 -0.044750 -0.002826
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.017122 -0.044750 -0.002826
|
||||
v -0.016750 -0.044750 -0.002850
|
||||
v 0.000000 -0.044750 0.000000
|
||||
v -0.016750 -0.044750 -0.002850
|
||||
v 0.016750 -0.044750 -0.002850
|
||||
vt 0.500000 0.500000
|
||||
vt 0.918750 0.938462
|
||||
vt 0.928050 0.934710
|
||||
vt 0.500000 0.500000
|
||||
vt 0.928050 0.934710
|
||||
vt 0.937191 0.923521
|
||||
vt 0.500000 0.500000
|
||||
vt 0.937191 0.923521
|
||||
vt 0.946016 0.905086
|
||||
vt 0.500000 0.500000
|
||||
vt 0.946016 0.905086
|
||||
vt 0.954375 0.879719
|
||||
vt 0.500000 0.500000
|
||||
vt 0.954375 0.879719
|
||||
vt 0.962124 0.847855
|
||||
vt 0.500000 0.500000
|
||||
vt 0.962124 0.847855
|
||||
vt 0.969131 0.810039
|
||||
vt 0.500000 0.500000
|
||||
vt 0.969131 0.810039
|
||||
vt 0.975276 0.766918
|
||||
vt 0.500000 0.500000
|
||||
vt 0.975276 0.766918
|
||||
vt 0.980454 0.719231
|
||||
vt 0.500000 0.500000
|
||||
vt 0.980454 0.719231
|
||||
vt 0.984576 0.667792
|
||||
vt 0.500000 0.500000
|
||||
vt 0.984576 0.667792
|
||||
vt 0.987572 0.613482
|
||||
vt 0.500000 0.500000
|
||||
vt 0.987572 0.613482
|
||||
vt 0.989390 0.557231
|
||||
vt 0.500000 0.500000
|
||||
vt 0.989390 0.557231
|
||||
vt 0.990000 0.500000
|
||||
vt 0.500000 0.500000
|
||||
vt 0.990000 0.500000
|
||||
vt 0.989390 0.442769
|
||||
vt 0.500000 0.500000
|
||||
vt 0.989390 0.442769
|
||||
vt 0.987572 0.386518
|
||||
vt 0.500000 0.500000
|
||||
vt 0.987572 0.386518
|
||||
vt 0.984576 0.332208
|
||||
vt 0.500000 0.500000
|
||||
vt 0.984576 0.332208
|
||||
vt 0.980454 0.280769
|
||||
vt 0.500000 0.500000
|
||||
vt 0.980454 0.280769
|
||||
vt 0.975276 0.233082
|
||||
vt 0.500000 0.500000
|
||||
vt 0.975276 0.233082
|
||||
vt 0.969131 0.189961
|
||||
vt 0.500000 0.500000
|
||||
vt 0.969131 0.189961
|
||||
vt 0.962124 0.152145
|
||||
vt 0.500000 0.500000
|
||||
vt 0.962124 0.152145
|
||||
vt 0.954375 0.120281
|
||||
vt 0.500000 0.500000
|
||||
vt 0.954375 0.120281
|
||||
vt 0.946016 0.094914
|
||||
vt 0.500000 0.500000
|
||||
vt 0.946016 0.094914
|
||||
vt 0.937191 0.076479
|
||||
vt 0.500000 0.500000
|
||||
vt 0.937191 0.076479
|
||||
vt 0.928050 0.065290
|
||||
vt 0.500000 0.500000
|
||||
vt 0.928050 0.065290
|
||||
vt 0.918750 0.061538
|
||||
vt 0.500000 0.500000
|
||||
vt 0.918750 0.061538
|
||||
vt 0.081250 0.061538
|
||||
vt 0.500000 0.500000
|
||||
vt 0.081250 0.061538
|
||||
vt 0.071950 0.065290
|
||||
vt 0.500000 0.500000
|
||||
vt 0.071950 0.065290
|
||||
vt 0.062809 0.076479
|
||||
vt 0.500000 0.500000
|
||||
vt 0.062809 0.076479
|
||||
vt 0.053984 0.094914
|
||||
vt 0.500000 0.500000
|
||||
vt 0.053984 0.094914
|
||||
vt 0.045625 0.120281
|
||||
vt 0.500000 0.500000
|
||||
vt 0.045625 0.120281
|
||||
vt 0.037876 0.152145
|
||||
vt 0.500000 0.500000
|
||||
vt 0.037876 0.152145
|
||||
vt 0.030869 0.189961
|
||||
vt 0.500000 0.500000
|
||||
vt 0.030869 0.189961
|
||||
vt 0.024724 0.233082
|
||||
vt 0.500000 0.500000
|
||||
vt 0.024724 0.233082
|
||||
vt 0.019546 0.280769
|
||||
vt 0.500000 0.500000
|
||||
vt 0.019546 0.280769
|
||||
vt 0.015424 0.332208
|
||||
vt 0.500000 0.500000
|
||||
vt 0.015424 0.332208
|
||||
vt 0.012428 0.386518
|
||||
vt 0.500000 0.500000
|
||||
vt 0.012428 0.386518
|
||||
vt 0.010610 0.442769
|
||||
vt 0.500000 0.500000
|
||||
vt 0.010610 0.442769
|
||||
vt 0.010000 0.500000
|
||||
vt 0.500000 0.500000
|
||||
vt 0.010000 0.500000
|
||||
vt 0.010610 0.557231
|
||||
vt 0.500000 0.500000
|
||||
vt 0.010610 0.557231
|
||||
vt 0.012428 0.613482
|
||||
vt 0.500000 0.500000
|
||||
vt 0.012428 0.613482
|
||||
vt 0.015424 0.667792
|
||||
vt 0.500000 0.500000
|
||||
vt 0.015424 0.667792
|
||||
vt 0.019546 0.719231
|
||||
vt 0.500000 0.500000
|
||||
vt 0.019546 0.719231
|
||||
vt 0.024724 0.766918
|
||||
vt 0.500000 0.500000
|
||||
vt 0.024724 0.766918
|
||||
vt 0.030869 0.810039
|
||||
vt 0.500000 0.500000
|
||||
vt 0.030869 0.810039
|
||||
vt 0.037876 0.847855
|
||||
vt 0.500000 0.500000
|
||||
vt 0.037876 0.847855
|
||||
vt 0.045625 0.879719
|
||||
vt 0.500000 0.500000
|
||||
vt 0.045625 0.879719
|
||||
vt 0.053984 0.905086
|
||||
vt 0.500000 0.500000
|
||||
vt 0.053984 0.905086
|
||||
vt 0.062809 0.923521
|
||||
vt 0.500000 0.500000
|
||||
vt 0.062809 0.923521
|
||||
vt 0.071950 0.934710
|
||||
vt 0.500000 0.500000
|
||||
vt 0.071950 0.934710
|
||||
vt 0.081250 0.938462
|
||||
vt 0.500000 0.500000
|
||||
vt 0.081250 0.938462
|
||||
vt 0.918750 0.938462
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
vn 0.000000 -1.000000 0.000000
|
||||
f 1/1/1 2/2/2 3/3/3
|
||||
f 4/4/4 5/5/5 6/6/6
|
||||
f 7/7/7 8/8/8 9/9/9
|
||||
f 10/10/10 11/11/11 12/12/12
|
||||
f 13/13/13 14/14/14 15/15/15
|
||||
f 16/16/16 17/17/17 18/18/18
|
||||
f 19/19/19 20/20/20 21/21/21
|
||||
f 22/22/22 23/23/23 24/24/24
|
||||
f 25/25/25 26/26/26 27/27/27
|
||||
f 28/28/28 29/29/29 30/30/30
|
||||
f 31/31/31 32/32/32 33/33/33
|
||||
f 34/34/34 35/35/35 36/36/36
|
||||
f 37/37/37 38/38/38 39/39/39
|
||||
f 40/40/40 41/41/41 42/42/42
|
||||
f 43/43/43 44/44/44 45/45/45
|
||||
f 46/46/46 47/47/47 48/48/48
|
||||
f 49/49/49 50/50/50 51/51/51
|
||||
f 52/52/52 53/53/53 54/54/54
|
||||
f 55/55/55 56/56/56 57/57/57
|
||||
f 58/58/58 59/59/59 60/60/60
|
||||
f 61/61/61 62/62/62 63/63/63
|
||||
f 64/64/64 65/65/65 66/66/66
|
||||
f 67/67/67 68/68/68 69/69/69
|
||||
f 70/70/70 71/71/71 72/72/72
|
||||
f 73/73/73 74/74/74 75/75/75
|
||||
f 76/76/76 77/77/77 78/78/78
|
||||
f 79/79/79 80/80/80 81/81/81
|
||||
f 82/82/82 83/83/83 84/84/84
|
||||
f 85/85/85 86/86/86 87/87/87
|
||||
f 88/88/88 89/89/89 90/90/90
|
||||
f 91/91/91 92/92/92 93/93/93
|
||||
f 94/94/94 95/95/95 96/96/96
|
||||
f 97/97/97 98/98/98 99/99/99
|
||||
f 100/100/100 101/101/101 102/102/102
|
||||
f 103/103/103 104/104/104 105/105/105
|
||||
f 106/106/106 107/107/107 108/108/108
|
||||
f 109/109/109 110/110/110 111/111/111
|
||||
f 112/112/112 113/113/113 114/114/114
|
||||
f 115/115/115 116/116/116 117/117/117
|
||||
f 118/118/118 119/119/119 120/120/120
|
||||
f 121/121/121 122/122/122 123/123/123
|
||||
f 124/124/124 125/125/125 126/126/126
|
||||
f 127/127/127 128/128/128 129/129/129
|
||||
f 130/130/130 131/131/131 132/132/132
|
||||
f 133/133/133 134/134/134 135/135/135
|
||||
f 136/136/136 137/137/137 138/138/138
|
||||
f 139/139/139 140/140/140 141/141/141
|
||||
f 142/142/142 143/143/143 144/144/144
|
||||
f 145/145/145 146/146/146 147/147/147
|
||||
f 148/148/148 149/149/149 150/150/150
|
||||
|
After Width: | Height: | Size: 109 B |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_cap_top
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.1 0.1 0.1
|
||||
map_Kd nano_cap_top_color.png
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_center_black
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_center_black_color.png
|
||||
|
After Width: | Height: | Size: 110 B |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_center_blue
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_center_blue_color.png
|
||||
|
After Width: | Height: | Size: 110 B |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_center_green
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_center_green_color.png
|
||||
|
After Width: | Height: | Size: 110 B |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_center_pink
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_center_pink_color.png
|
||||
|
After Width: | Height: | Size: 110 B |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_center_red
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_center_red_color.png
|
||||
|
After Width: | Height: | Size: 111 B |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_center_silver
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_center_silver_color.png
|
||||
|
After Width: | Height: | Size: 111 B |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_ports
|
||||
Kd 1 1 1
|
||||
Ns 256
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.2 0.2 0.2
|
||||
map_Kd nano_ports_color.png
|
||||
|
After Width: | Height: | Size: 127 B |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_shell_black
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_shell_black_color.png
|
||||
|
After Width: | Height: | Size: 110 B |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_shell_blue
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_shell_blue_color.png
|
||||
|
After Width: | Height: | Size: 110 B |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_shell_green
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_shell_green_color.png
|
||||
|
After Width: | Height: | Size: 110 B |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_shell_pink
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_shell_pink_color.png
|
||||
|
After Width: | Height: | Size: 110 B |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_shell_red
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_shell_red_color.png
|
||||
|
After Width: | Height: | Size: 111 B |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_shell_silver
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.3 0.3 0.3
|
||||
map_Kd nano_shell_silver_color.png
|
||||
|
After Width: | Height: | Size: 111 B |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_well
|
||||
Kd 1 1 1
|
||||
Ns 256
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0 0 0
|
||||
map_Kd nano_well_color.png
|
||||
|
After Width: | Height: | Size: 107 B |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_wheel_black
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.08 0.08 0.08
|
||||
map_Kd nano_wheel_black_color.png
|
||||
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,8 @@
|
||||
newmtl nano_wheel_white
|
||||
Kd 1 1 1
|
||||
Ns 64
|
||||
d 1
|
||||
illum 2
|
||||
Ka 0 0 0
|
||||
Ks 0.08 0.08 0.08
|
||||
map_Kd nano_wheel_white_color.png
|
||||
|
After Width: | Height: | Size: 21 KiB |
@@ -4,6 +4,11 @@
|
||||
//
|
||||
// The static body is drawn once per finish into a layer; a frame blits it, then the screen and the
|
||||
// wheel state. draw() returns false when nothing changed, so the page repaints only on change.
|
||||
//
|
||||
// The nano (models.js) has only its screen here, in its window, filling most of the canvas: podd or
|
||||
// its render models draw the rest, and the page shows just the window's rect (windowRect('nano')).
|
||||
|
||||
import { MODELS } from './models.js';
|
||||
|
||||
export const BODY = { w: 61.8, h: 103.5, corner: 6.5 };
|
||||
const LCD = { cy: 24.2, w: 50.8, h: 38.1 };
|
||||
@@ -52,6 +57,41 @@ export function createFace(canvas, screen, opts = {}) {
|
||||
|
||||
const lcd = { x: X(-LCD.w / 2), y: Y(LCD.cy + LCD.h / 2), w: LCD.w * k, h: LCD.h * k };
|
||||
const wc = { x: X(0), y: Y(WHEEL.cy) };
|
||||
// the nano's window: 90% of the canvas's width, centred
|
||||
const NANO = MODELS.nano;
|
||||
const nk = W * 0.9 / NANO.win.w; // px per mm
|
||||
const nanoWin = { w: NANO.win.w * nk, h: NANO.win.h * nk };
|
||||
nanoWin.x = (W - nanoWin.w) / 2;
|
||||
nanoWin.y = (H - nanoWin.h) / 2;
|
||||
const nanoLcd = { w: NANO.lcd.w * nk, h: NANO.lcd.h * nk };
|
||||
nanoLcd.x = (W - nanoLcd.w) / 2;
|
||||
nanoLcd.y = (H - nanoLcd.h) / 2;
|
||||
|
||||
/** The model's screen window in this canvas (px): what the page shows over the window. */
|
||||
function windowRect(model) {
|
||||
if (model === 'nano') return { ...nanoWin };
|
||||
return { x: X(-WINDOW.w / 2), y: Y(LCD.cy + WINDOW.h / 2), w: WINDOW.w * k, h: WINDOW.h * k };
|
||||
}
|
||||
|
||||
// The nano: its LCD in a black window (rounded like the nano's), the backlight and the glass.
|
||||
function drawNano(s) {
|
||||
roundRect(g, nanoWin.x, nanoWin.y, nanoWin.w, nanoWin.h, 0.6 * nk);
|
||||
g.fillStyle = '#000';
|
||||
g.fill();
|
||||
g.imageSmoothingEnabled = true;
|
||||
g.imageSmoothingQuality = 'high';
|
||||
g.drawImage(screen, nanoLcd.x, nanoLcd.y, nanoLcd.w, nanoLcd.h);
|
||||
const veil = 1 - Math.max(0, Math.min(1, s.backlight));
|
||||
if (veil > 0.001) {
|
||||
g.fillStyle = `rgba(0,0,0,${veil})`;
|
||||
g.fillRect(nanoLcd.x, nanoLcd.y, nanoLcd.w, nanoLcd.h);
|
||||
}
|
||||
const refl = g.createLinearGradient(nanoLcd.x, nanoLcd.y, nanoLcd.x + nanoLcd.w, nanoLcd.y + nanoLcd.h);
|
||||
refl.addColorStop(0, 'rgba(255,255,255,0.07)');
|
||||
refl.addColorStop(0.5, 'rgba(255,255,255,0)');
|
||||
g.fillStyle = refl;
|
||||
g.fillRect(nanoLcd.x, nanoLcd.y, nanoLcd.w, nanoLcd.h);
|
||||
}
|
||||
|
||||
function bodyLayer(finish) {
|
||||
let c = layers.get(finish);
|
||||
@@ -204,12 +244,13 @@ export function createFace(canvas, screen, opts = {}) {
|
||||
if (marker !== target) {
|
||||
marker = target > marker ? Math.min(1, marker + dt / MARKER_IN) : Math.max(0, marker - dt / MARKER_OUT);
|
||||
}
|
||||
const key = [s.finish, Math.round(s.backlight * 100), s.pressed || '', marker > 0 ? Math.round(s.angle * 200) : -1,
|
||||
const key = [s.model || 'video', s.finish, Math.round(s.backlight * 100), s.pressed || '', marker > 0 ? Math.round(s.angle * 200) : -1,
|
||||
Math.round(marker * 20), s.reflVersion || 0, s.screenOnly ? 1 : 0, s.videoUnder ? 1 : 0].join('|');
|
||||
if (!s.screenDirty && key === last) return false;
|
||||
last = key;
|
||||
const f = FINISHES[s.finish] || FINISHES.black;
|
||||
g.clearRect(0, 0, W, H);
|
||||
if (s.model === 'nano') { drawNano(s); return true; }
|
||||
if (overlay) {
|
||||
roundRect(g, X(-WINDOW.w / 2), Y(LCD.cy + WINDOW.h / 2), WINDOW.w * k, WINDOW.h * k, WINDOW.r * k);
|
||||
g.fillStyle = '#000';
|
||||
@@ -252,7 +293,7 @@ export function createFace(canvas, screen, opts = {}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return { draw, get animating() { return marker > 0 && marker < 1; } };
|
||||
return { draw, windowRect, get animating() { return marker > 0 && marker < 1; } };
|
||||
}
|
||||
|
||||
/** The mirror-polished steel back with the peach engraved in it (static: drawn once). Polished
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// assPod: a 5th-generation-style music player in your hand in the SteamVR menu, playing the VLC
|
||||
// media library. This is its half inside SteamVR's systemui page (installed by the daemon as the
|
||||
// media library. The main menu's assPod Model swaps the body for a 2nd-generation nano (models.js):
|
||||
// the same OS, scaled onto its smaller screen, and no video. This is its half inside SteamVR's systemui page (installed by the daemon as the
|
||||
// page script device/vr/systemui.asspod.js, built from these sources by tools/build_asspod.py);
|
||||
// the daemon's plugin device/shell_ext/asspod.py runs VLC and reads the library.
|
||||
//
|
||||
@@ -12,8 +13,9 @@
|
||||
// Controls: both sticks turn the click wheel; a stick click presses the wheel button under it
|
||||
// (centred: the centre button). A is the centre button, B is the wheel's Menu, and the hamburger
|
||||
// (menu) button puts the assPod away; so does closing the SteamVR menu. The right controller's
|
||||
// buttons are read here from its model; the left one's (A and B in the same places) come from Steam
|
||||
// through the plugin (btn()). Music keeps playing: the player keeps running here at a few Hz, and
|
||||
// buttons are read here from its model, Steam's A / B / hamburger come through the plugin (btn())
|
||||
// otherwise. Both sticks act alike: only their models are read (Steam's d-pad directions, which the
|
||||
// left stick's tilts also send, are not relayed). Music keeps playing: the player keeps running here at a few Hz, and
|
||||
// the plugin keeps VLC up while anything plays and stops it when the assPod is away and nothing plays.
|
||||
//
|
||||
// The plugin talks to this page only through the returned api: poll() drains the commands for VLC
|
||||
@@ -33,6 +35,7 @@ import { createFace, createBack, BODY } from './face.js';
|
||||
import { drawPeachSilhouette } from './brand/peach.js';
|
||||
import { createReflector, pose, qmul, qrot, qconj } from './reflect.js';
|
||||
import { createSceneGraph } from './sg.js';
|
||||
import { MODELS, currentModel, currentFinish, bodyMetres } from './models.js';
|
||||
|
||||
const FACE_W = 476; // px; the body is 61.8 x 103.5 mm
|
||||
const FACE_H = Math.round(FACE_W * BODY.h / BODY.w);
|
||||
@@ -40,6 +43,10 @@ const FACE_WIDTH_M = 0.085; // shown about 1.4x the real body, fo
|
||||
const BODY_M = { // the 3D body in metres, at that scale
|
||||
w: FACE_WIDTH_M, h: FACE_WIDTH_M * BODY.h / BODY.w, d: FACE_WIDTH_M * 11 / BODY.w, r: FACE_WIDTH_M * BODY.corner / BODY.w,
|
||||
};
|
||||
const NANO_M = bodyMetres(MODELS.nano); // the nano, at its own scale
|
||||
// a model's geometry for the scene graph (model metres)
|
||||
const geoOf = (m) => ({ w: m.body.w / 1000, d: m.body.d / 1000, lcdCy: m.lcd.cy / 1000, lcdW: m.lcd.w / 1000,
|
||||
winW: m.win.w / 1000, winH: m.win.h / 1000, wheelCy: m.wheel.cy / 1000 });
|
||||
const BACK_W = 238; // px: the steel back's live reflection (half the face's resolution)
|
||||
const BACK_H = Math.round(BACK_W * BODY.h / BODY.w);
|
||||
const REFLECT_EVERY = 2; // frames between reflection updates (about 30 a second)
|
||||
@@ -151,9 +158,15 @@ export default function install(ctx) {
|
||||
library.onLoad(() => { art.clear(); os.ctx.popToRoot(); os.invalidate(); });
|
||||
// the EQ follows the setting (VLC's preset by name)
|
||||
settings.on('change', ({ key }) => { if (key === 'eq' || key === '*') send({ op: 'eq', name: settings.get('eq') }); });
|
||||
// the body's colour: podd follows the finish in the poll; the render-model body is rebuilt
|
||||
// The body: podd follows the model and its finish in the poll (one layout holds both); the scene
|
||||
// graph is rebuilt for the other body's screen panel and place in the hand, or for the render
|
||||
// models' colour. (Picking the nano stops a video: os/screens/main-menu.js.)
|
||||
settings.on('change', ({ key }) => {
|
||||
if ((key === 'finish' || key === '*') && st.open && !st.podd) st.holdPose = sg.show(showOpts(st.hand, st.world)) || st.holdPose;
|
||||
if (!st.open) return;
|
||||
if (key === 'model' || key === '*' || (!st.podd && (key === 'finish' || key === 'nanoFinish'))) {
|
||||
if (key === 'model' && st.world) unpin(false);
|
||||
st.holdPose = sg.show(showOpts(st.hand, st.world)) || st.holdPose;
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------- state
|
||||
@@ -246,11 +259,13 @@ export default function install(ctx) {
|
||||
videoMode();
|
||||
const screenDirty = os.render(t);
|
||||
// the painted reflections only without podd (podd shades the glass and the steel itself)
|
||||
if (!st.podd && st.frames % REFLECT_EVERY === 0 && reflector.update(st.hand, st.holdPose)) back.draw(reflector.back, reflector.version);
|
||||
const model = currentModel(settings).id;
|
||||
const painted = !st.podd && model === 'video'; // the nano's render models carry their own back
|
||||
if (painted && st.frames % REFLECT_EVERY === 0 && reflector.update(st.hand, st.holdPose)) back.draw(reflector.back, reflector.version);
|
||||
const ws = wheel.state;
|
||||
face.draw({
|
||||
reflection: !st.podd && reflector.version ? reflector.front : null, reflVersion: reflector.version, screenOnly: !!st.podd, videoUnder: !!st.videoMode,
|
||||
finish: settings.get('finish'), backlight: os.backlight, screenDirty,
|
||||
reflection: painted && reflector.version ? reflector.front : null, reflVersion: reflector.version, screenOnly: !!st.podd, videoUnder: !!st.videoMode,
|
||||
model, finish: currentFinish(settings), backlight: os.backlight, screenDirty,
|
||||
touching: ws.touching, angle: ws.angle, pressed: ws.pressedPart || (st.keys.select ? 'select' : st.keys.menu ? 'menu' : null), now: t,
|
||||
});
|
||||
} catch (err) { note(err); }
|
||||
@@ -268,9 +283,15 @@ export default function install(ctx) {
|
||||
if (player.state === 'playing') st.bgTimer = setTimeout(background, BACKGROUND_MS);
|
||||
}
|
||||
|
||||
// Reposition's places are kept per body: the Video's under the hand's name, the nano's as nano-<hand>
|
||||
const holdKey = (hand) => (currentModel(settings).id === 'video' ? hand : `${currentModel(settings).id}-${hand}`);
|
||||
|
||||
function showOpts(hand, world) {
|
||||
return { hand, body: BODY_M, rects, modelDir: st.modelDir, finish: settings.get('finish'), podd: st.podd,
|
||||
hold: st.holds[hand] || null, world: world || null, video: !!st.videoMode };
|
||||
const m = currentModel(settings);
|
||||
const wr = face.windowRect(m.id);
|
||||
rects.win = { x: rects.face.x + wr.x, y: rects.face.y + wr.y, w: wr.w, h: wr.h };
|
||||
return { hand, body: m.id === 'video' ? BODY_M : NANO_M, rects, modelDir: st.modelDir, model: m.id, geo: geoOf(m),
|
||||
finish: currentFinish(settings), podd: st.podd, hold: st.holds[holdKey(hand)] || null, world: world || null, video: !!st.videoMode };
|
||||
}
|
||||
|
||||
// A video on the screen with podd: podd shows its picture (the daemon's frames, full rate) in a panel
|
||||
@@ -302,7 +323,7 @@ export default function install(ctx) {
|
||||
const hp = pose('/user/hand/' + st.hand);
|
||||
if (hp) {
|
||||
const inv = qconj(hp.q);
|
||||
st.holds[st.hand] = {
|
||||
st.holds[holdKey(st.hand)] = {
|
||||
rot: qmul(inv, st.world.rot),
|
||||
centre: qrot(inv, st.world.centre.map((v, i) => v - hp.t[i])),
|
||||
};
|
||||
@@ -317,6 +338,7 @@ export default function install(ctx) {
|
||||
return {
|
||||
id: 'reposition',
|
||||
title: 'Reposition',
|
||||
adaptive: true,
|
||||
keepAwake: true,
|
||||
enter() { if (!pin()) note('reposition: no hand pose'); },
|
||||
exit() { if (st.world) unpin(false); },
|
||||
@@ -325,7 +347,7 @@ export default function install(ctx) {
|
||||
if (e.type !== 'press') return e.type === 'release' ? false : undefined;
|
||||
if (e.part === 'select') return done(() => unpin(true));
|
||||
if (e.part === 'menu') return done(() => unpin(false));
|
||||
if (e.part === 'play') return done(() => { st.holds[st.hand] = null; unpin(false); });
|
||||
if (e.part === 'play') return done(() => { st.holds[holdKey(st.hand)] = null; unpin(false); });
|
||||
return false;
|
||||
},
|
||||
draw(g, r) {
|
||||
@@ -379,6 +401,10 @@ export default function install(ctx) {
|
||||
for (const [p, t] of Object.entries(opts.resume)) if (Number.isFinite(t)) resume.set(p, t);
|
||||
}
|
||||
if (opts.holds && typeof opts.holds === 'object') st.holds = Object.assign({ left: null, right: null }, opts.holds);
|
||||
// the body and colours picked last time (the plugin keeps them: device/shell_ext/asspod.py PREFS)
|
||||
if (opts.prefs && typeof opts.prefs === 'object') {
|
||||
for (const key of ['model', 'finish', 'nanoFinish']) if (typeof opts.prefs[key] === 'string') settings.set(key, opts.prefs[key]);
|
||||
}
|
||||
st.world = null;
|
||||
st.videoMode = false;
|
||||
art.videoExternal = false;
|
||||
@@ -436,7 +462,8 @@ export default function install(ctx) {
|
||||
function status() {
|
||||
return {
|
||||
ok: true, open: st.open, hand: st.hand, closedBy: st.closedBy, loading: st.loading,
|
||||
hold: st.holdPose || null, bodyScale: BODY_M.w / 0.0618, finish: settings.get('finish'), podd: !!st.podd,
|
||||
hold: st.holdPose || null, bodyScale: BODY_M.w / 0.0618, nanoScale: MODELS.nano.scale, model: currentModel(settings).id,
|
||||
finish: currentFinish(settings), podd: !!st.podd,
|
||||
player: { state: player.state, current: player.current ? { id: player.current.id, title: player.current.title } : null,
|
||||
position: Math.round((player.position || 0) * 10) / 10, queue: player.queue.length, volume: player.volume },
|
||||
library: { tracks: library.tracks.length, albums: library.albums.length, videos: library.videos.length },
|
||||
@@ -452,6 +479,7 @@ export default function install(ctx) {
|
||||
os.ctx.push({
|
||||
id: 'notice',
|
||||
title: String(title || 'assPod'),
|
||||
adaptive: true,
|
||||
handle(e) {
|
||||
if (e.type === 'press' && (e.part === 'select' || e.part === 'menu')) { os.ctx.pop(); return true; }
|
||||
return e.type === 'scroll' ? false : undefined;
|
||||
@@ -493,7 +521,8 @@ export default function install(ctx) {
|
||||
playing: player.state === 'playing', state: player.state,
|
||||
current: player.current ? player.current.path : null,
|
||||
volume: player.volume, eq: settings.get('eq'),
|
||||
hold: st.holdPose || null, world: st.world, holds: st.holds, finish: settings.get('finish'),
|
||||
hold: st.holdPose || null, world: st.world, holds: st.holds, model: currentModel(settings).id, finish: currentFinish(settings),
|
||||
prefs: { model: currentModel(settings).id, finish: settings.get('finish'), nanoFinish: settings.get('nanoFinish') },
|
||||
position: Math.round((player.position || 0) * 1000) / 1000,
|
||||
video: player.current && player.current.kind === 'video' && os.ctx.top()?.id === 'videoplayer' && st.open,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// The assPod's bodies: the Video (5th generation, the original) and the nano (2nd generation). The
|
||||
// main menu's assPod Model picks one; Color picks among that body's own finishes. Both run the same
|
||||
// OS on a 4:3 screen; the nano's is 240 x 180 logical px against the Video's 320 x 240, so its text is
|
||||
// larger on its screen, as on the real one. The nano plays no video: its menus leave the Videos out.
|
||||
//
|
||||
// Geometry in millimetres, model frame (tools/asspod-model, tools/nano2-model GEOMETRY): origin at
|
||||
// the body's centre, y toward the top. `scale` is how much larger than life it is shown in the hand;
|
||||
// podd (native/podd/podd.cpp VIDEO / NANO) and its render models use the same numbers. `screen` is
|
||||
// the OS's logical screen on that body.
|
||||
|
||||
export const MODELS = Object.freeze({
|
||||
video: Object.freeze({
|
||||
id: 'video',
|
||||
label: 'Video Gen 1',
|
||||
about: 'assPod Video',
|
||||
body: { w: 61.8, h: 103.5, d: 11.0 },
|
||||
lcd: { cy: 24.2, w: 50.8, h: 38.1 },
|
||||
win: { w: 53.2, h: 40.5 },
|
||||
wheel: { cy: -24.0 },
|
||||
screen: { w: 320, h: 240 }, // logical px (os/theme.js setScreenSize)
|
||||
scale: 0.085 / 0.0618, // shown 85 mm wide
|
||||
finishKey: 'finish',
|
||||
finishes: [['black', 'Black'], ['white', 'White']],
|
||||
videos: true,
|
||||
}),
|
||||
nano: Object.freeze({
|
||||
id: 'nano',
|
||||
label: 'Nano Gen 2',
|
||||
about: 'assPod nano',
|
||||
body: { w: 40.0, h: 90.0, d: 6.5 },
|
||||
lcd: { cy: 28.88, w: 29.6, h: 22.2 },
|
||||
win: { w: 31.62, h: 24.04 },
|
||||
wheel: { cy: -13.3 },
|
||||
screen: { w: 240, h: 180 }, // the same text on less screen: six rows, as on the real nano
|
||||
scale: 1.3125, // shown 52.5 mm wide
|
||||
finishKey: 'nanoFinish',
|
||||
finishes: [['silver', 'Silver'], ['green', 'Green'], ['blue', 'Blue'], ['pink', 'Pink'], ['black', 'Black'], ['red', '(PRODUCT)RED']],
|
||||
videos: false,
|
||||
}),
|
||||
});
|
||||
|
||||
export const MODEL_ORDER = ['video', 'nano'];
|
||||
|
||||
/** The current body's definition. */
|
||||
export function currentModel(settings) {
|
||||
return MODELS[settings.get('model')] || MODELS.video;
|
||||
}
|
||||
|
||||
/** The current body's finish (one of its own; its first otherwise). */
|
||||
export function currentFinish(settings) {
|
||||
const m = currentModel(settings);
|
||||
const f = settings.get(m.finishKey);
|
||||
return m.finishes.some(([k]) => k === f) ? f : m.finishes[0][0];
|
||||
}
|
||||
|
||||
/** The body in metres as shown in the hand: { w, h, d } at the model's scale. */
|
||||
export function bodyMetres(m) {
|
||||
const k = m.scale / 1000;
|
||||
return { w: m.body.w * k, h: m.body.h * k, d: m.body.d * k };
|
||||
}
|
||||
@@ -66,7 +66,11 @@ export function createMenu(ctx, def) {
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
// the highlight stays on its item when the list changes around it (the main menu without Videos)
|
||||
const was = items[index];
|
||||
items = resolve(def.items) ?? [];
|
||||
const j = was ? items.indexOf(was) : -1;
|
||||
if (j >= 0) index = j;
|
||||
clampWindow();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// assPod OS core: screen stack, 220 ms slide transitions, status bar, backlight timer and dimming,
|
||||
// sleep/wake, the boot logo, the default button behavior, and render throttling for the screen
|
||||
// texture. Draws the 320x240 logical LCD onto the 640x480 canvas that main.js uploads as a texture.
|
||||
// texture. Draws the logical LCD (320x240 on the Video, 240x180 on the nano) onto the 640x480 canvas
|
||||
// that main.js uploads as a texture.
|
||||
|
||||
import { SCREEN_W } from '../config.js';
|
||||
import { currentModel } from '../models.js';
|
||||
import { settings as defaultSettings } from '../settings.js';
|
||||
import { drawPeachSilhouette } from '../brand/peach.js';
|
||||
import * as theme from './theme.js';
|
||||
@@ -11,7 +13,7 @@ import { createMenu } from './menu.js';
|
||||
import { createMainMenu } from './screens/main-menu.js';
|
||||
import { createNowPlaying } from './screens/now-playing.js';
|
||||
|
||||
const { M, C, ANIM } = theme;
|
||||
const { M, C, ANIM, setScreenSize } = theme;
|
||||
|
||||
/**
|
||||
* @typedef {import('../input/clickwheel.js').WheelEvent} WheelEvent
|
||||
@@ -44,8 +46,9 @@ const { M, C, ANIM } = theme;
|
||||
|
||||
// ---------------------------------------------------------------- constants
|
||||
|
||||
const CONTENT = Object.freeze({ x: 0, y: M.statusH, w: M.W, h: M.H - M.statusH });
|
||||
const FULL = Object.freeze({ x: 0, y: 0, w: M.W, h: M.H });
|
||||
// the content area under the status bar, and the whole screen (the logical size can change: models.js)
|
||||
const content = () => ({ x: 0, y: M.statusH, w: M.W, h: M.H - M.statusH });
|
||||
const full = () => ({ x: 0, y: 0, w: M.W, h: M.H });
|
||||
const FRAME_MS = 1000 / ANIM.maxFps;
|
||||
// rAF timestamps jitter by a fraction of a ms; without slack a 25 ms cap would skip a frame at 80 Hz.
|
||||
const FRAME_SLACK_MS = 1;
|
||||
@@ -74,7 +77,12 @@ function normKey(s) {
|
||||
*/
|
||||
export function createOS({ canvas, player, library, art, apps, settings = defaultSettings, boot: bootEnabled = false }) {
|
||||
const g = canvas.getContext('2d');
|
||||
const scale = canvas.width / SCREEN_W;
|
||||
// The body's logical screen (models.js): the canvas stays the same size and the scale follows.
|
||||
const applyModel = () => { const m = currentModel(settings); setScreenSize(m.screen.w, m.screen.h); };
|
||||
applyModel();
|
||||
if (typeof settings.on === 'function') {
|
||||
settings.on('change', ({ key }) => { if (key === 'model' || key === '*') { applyModel(); dirty = DIRTY_NOW; } });
|
||||
}
|
||||
|
||||
/** @type {Screen[]} */
|
||||
const stack = [];
|
||||
@@ -360,8 +368,18 @@ export function createOS({ canvas, player, library, art, apps, settings = defaul
|
||||
}
|
||||
|
||||
const reported = new WeakSet();
|
||||
// Menus and the screens marked `adaptive` lay themselves out on the current logical screen; the
|
||||
// rest (the games, the clock, Cover Flow...) are drawn for the Video's 320 x 240 and scaled to fit.
|
||||
function drawScreen(s, r, now) {
|
||||
try {
|
||||
if (!s.isMenu && !s.adaptive && M.W !== SCREEN_W) {
|
||||
const k = M.W / SCREEN_W;
|
||||
g.save();
|
||||
g.scale(k, k);
|
||||
s.draw(g, { x: r.x / k, y: r.y / k, w: r.w / k, h: r.h / k }, now);
|
||||
g.restore();
|
||||
return;
|
||||
}
|
||||
s.draw(g, r, now);
|
||||
} catch (err) {
|
||||
// One broken screen must not take the whole OS down every frame; report it once.
|
||||
@@ -373,12 +391,13 @@ export function createOS({ canvas, player, library, art, apps, settings = defaul
|
||||
if (dx <= -M.W || dx >= M.W) return;
|
||||
g.save();
|
||||
g.beginPath();
|
||||
g.rect(CONTENT.x, CONTENT.y, CONTENT.w, CONTENT.h);
|
||||
const r = content();
|
||||
g.rect(r.x, r.y, r.w, r.h);
|
||||
g.clip();
|
||||
g.translate(dx, 0);
|
||||
g.fillStyle = C.bg;
|
||||
g.fillRect(CONTENT.x, CONTENT.y, CONTENT.w, CONTENT.h);
|
||||
drawScreen(s, CONTENT, now);
|
||||
g.fillRect(r.x, r.y, r.w, r.h);
|
||||
drawScreen(s, r, now);
|
||||
g.restore();
|
||||
}
|
||||
|
||||
@@ -397,7 +416,7 @@ export function createOS({ canvas, player, library, art, apps, settings = defaul
|
||||
g.fillStyle = '#000000';
|
||||
g.fillRect(0, 0, M.W, M.H);
|
||||
}
|
||||
drawScreen(s, FULL, now);
|
||||
drawScreen(s, full(), now);
|
||||
g.restore();
|
||||
return;
|
||||
}
|
||||
@@ -409,6 +428,7 @@ export function createOS({ canvas, player, library, art, apps, settings = defaul
|
||||
}
|
||||
|
||||
function paint(now) {
|
||||
const scale = canvas.width / M.W;
|
||||
g.setTransform(scale, 0, 0, scale, 0, 0);
|
||||
g.globalAlpha = 1;
|
||||
g.globalCompositeOperation = 'source-over';
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { createRng, hashString } from '../../util/rng.js';
|
||||
import { createSettings } from './settings.js';
|
||||
import { createSearch } from './search.js';
|
||||
import { MODELS, MODEL_ORDER, currentModel, currentFinish } from '../../models.js';
|
||||
|
||||
// ---------------------------------------------------------------- sorting
|
||||
|
||||
@@ -32,7 +33,9 @@ function albumOrder(library) {
|
||||
* A list of songs (or videos, in playlists). Selecting row i plays the whole list from i,
|
||||
* the way the assPod queues whatever list you picked from, then opens Now Playing.
|
||||
*/
|
||||
export function songList(ctx, id, title, list) {
|
||||
export function songList(ctx, id, title, all) {
|
||||
// the nano plays no video: a playlist's videos are left out on it
|
||||
const list = currentModel(ctx.settings).videos ? all : all.filter((t) => t.kind !== 'video');
|
||||
const items = list.map((t, i) => ({
|
||||
label: t.title,
|
||||
speaker: () => ctx.player.current?.id === t.id && ctx.player.state !== 'stopped',
|
||||
@@ -170,30 +173,83 @@ function shuffleSongs(ctx) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- the body: model and colour
|
||||
|
||||
/** assPod Model: the Video or the nano, checkmarked; picking one switches the body and goes back.
|
||||
* The nano plays no video: a video playing stops when it is picked. */
|
||||
function modelMenu(ctx) {
|
||||
return ctx.menu({
|
||||
id: 'model',
|
||||
title: 'assPod Model',
|
||||
initialIndex: () => Math.max(0, MODEL_ORDER.indexOf(currentModel(ctx.settings).id)),
|
||||
items: MODEL_ORDER.map((id) => ({
|
||||
label: MODELS[id].label,
|
||||
check: () => currentModel(ctx.settings).id === id,
|
||||
action: (c) => {
|
||||
if (!MODELS[id].videos && c.player.current?.kind === 'video') c.player.stop();
|
||||
c.settings.set('model', id);
|
||||
c.pop();
|
||||
return true;
|
||||
},
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
/** Color: the current body's own finishes, checkmarked. */
|
||||
function colorMenu(ctx) {
|
||||
const m = currentModel(ctx.settings);
|
||||
return ctx.menu({
|
||||
id: 'color',
|
||||
title: 'Color',
|
||||
initialIndex: () => Math.max(0, m.finishes.findIndex(([k]) => k === currentFinish(ctx.settings))),
|
||||
items: m.finishes.map(([key, label]) => ({
|
||||
label,
|
||||
check: () => currentFinish(ctx.settings) === key,
|
||||
action: (c) => {
|
||||
c.settings.set(m.finishKey, key);
|
||||
c.pop();
|
||||
return true;
|
||||
},
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- main menu
|
||||
|
||||
export function createMainMenu(ctx) {
|
||||
const base = [
|
||||
const finishLabel = () => {
|
||||
const m = currentModel(ctx.settings);
|
||||
const f = currentFinish(ctx.settings);
|
||||
return (m.finishes.find(([k]) => k === f) || m.finishes[0])[1];
|
||||
};
|
||||
const all = [
|
||||
{ id: 'music', label: 'Music', open: createMusicMenu },
|
||||
{ id: 'videos', label: 'Videos', open: (c) => c.apps.videos(c) },
|
||||
{ id: 'extras', label: 'Extras', open: (c) => c.apps.extras(c) },
|
||||
{ id: 'settings', label: 'Settings', open: createSettings },
|
||||
{ id: 'reposition', label: 'Reposition', open: (c) => c.apps.reposition(c) },
|
||||
{
|
||||
id: 'color',
|
||||
label: 'Color',
|
||||
detail: () => (ctx.settings.get('finish') === 'white' ? 'White' : 'Black'),
|
||||
action: (c) => { c.settings.set('finish', c.settings.get('finish') === 'white' ? 'black' : 'white'); return true; },
|
||||
},
|
||||
{ id: 'model', label: 'assPod Model', detail: () => currentModel(ctx.settings).label, open: modelMenu },
|
||||
{ id: 'color', label: 'Color', detail: finishLabel, open: colorMenu },
|
||||
{ id: 'shuffle', label: 'Shuffle Songs', action: shuffleSongs },
|
||||
{ id: 'backlight', label: 'Backlight', action: (c) => { c.os.toggleBacklight(); return false; } },
|
||||
];
|
||||
const withNowPlaying = [
|
||||
...base,
|
||||
{ id: 'nowplaying', label: 'Now Playing', chevron: true, action: (c) => { c.nowPlaying(); return true; } },
|
||||
];
|
||||
const nowPlaying = { id: 'nowplaying', label: 'Now Playing', chevron: true, action: (c) => { c.nowPlaying(); return true; } };
|
||||
// the same item objects every time, so the menu keeps its place across a model switch
|
||||
const lists = new Map();
|
||||
return ctx.menu({
|
||||
id: 'main',
|
||||
title: 'assPod',
|
||||
// Like the 5G, Now Playing is listed only while something is queued (not after the queue ends).
|
||||
items: () => (ctx.player.current && ctx.player.state !== 'stopped' ? withNowPlaying : base),
|
||||
// Like the 5G, Now Playing is listed only while something is queued (not after the queue ends);
|
||||
// the nano has no Videos.
|
||||
items: () => {
|
||||
const videos = currentModel(ctx.settings).videos;
|
||||
const playing = !!(ctx.player.current && ctx.player.state !== 'stopped');
|
||||
const key = `${videos}|${playing}`;
|
||||
if (!lists.has(key)) {
|
||||
const base = videos ? all : all.filter((it) => it.id !== 'videos');
|
||||
lists.set(key, playing ? [...base, nowPlaying] : base);
|
||||
}
|
||||
return lists.get(key);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -2,23 +2,40 @@
|
||||
// artist / album on the right, and a progress bar with elapsed and remaining time.
|
||||
// Center click cycles progress -> scrubber -> rating; the wheel sets volume, seeks or rates.
|
||||
|
||||
import { SCREEN_SCALE } from '../../config.js';
|
||||
import { SCREEN_W, SCREEN_SCALE } from '../../config.js';
|
||||
import { M } from '../theme.js';
|
||||
|
||||
// ---------------------------------------------------------------- layout
|
||||
|
||||
const POS = { x: 10, y: 40 };
|
||||
const ART = { x: 10, y: 48, s: 112 };
|
||||
const TEXT = { x: 132, w: 180, title: 82, artist: 102, album: 122 };
|
||||
const BAR = { x: 10, y: 182, w: 300, h: 10 };
|
||||
const TIME_Y = 208;
|
||||
const STARS = { cx: 160, cy: 187 };
|
||||
const VOLUME = { x: 10, y: 180, w: 300 };
|
||||
// The Video's 320 x 240 layout, and the nano's on its 240 x 180 screen (the same text, less room:
|
||||
// smaller art and a shorter bar). relayout() picks the one for the current logical screen.
|
||||
const LAYOUTS = {
|
||||
320: {
|
||||
POS: { x: 10, y: 40 }, ART: { x: 10, y: 48, s: 112 }, TEXT: { x: 132, w: 180, title: 82, artist: 102, album: 122 },
|
||||
BAR: { x: 10, y: 182, w: 300, h: 10 }, TIME_Y: 208, STARS: { cx: 160, cy: 187 }, VOLUME: { x: 10, y: 180, w: 300 },
|
||||
GLYPH: { right: 310, w: 12, h: 9, gap: 5 },
|
||||
},
|
||||
240: {
|
||||
POS: { x: 8, y: 38 }, ART: { x: 8, y: 46, s: 80 }, TEXT: { x: 96, w: 136, title: 66, artist: 84, album: 102 },
|
||||
BAR: { x: 8, y: 136, w: 224, h: 9 }, TIME_Y: 160, STARS: { cx: 120, cy: 141 }, VOLUME: { x: 8, y: 134, w: 224 },
|
||||
GLYPH: { right: 232, w: 12, h: 9, gap: 5 },
|
||||
},
|
||||
};
|
||||
let POS, ART, TEXT, BAR, TIME_Y, STARS, VOLUME, GLYPH, ART_PX;
|
||||
let laidOut = 0;
|
||||
function relayout() {
|
||||
if (laidOut === M.W) return;
|
||||
laidOut = M.W;
|
||||
({ POS, ART, TEXT, BAR, TIME_Y, STARS, VOLUME, GLYPH } = LAYOUTS[M.W] || LAYOUTS[320]);
|
||||
// the cover is resampled to the art box's backing-store size (the canvas is 640 px wide)
|
||||
ART_PX = Math.round(ART.s * SCREEN_W * SCREEN_SCALE / M.W);
|
||||
}
|
||||
relayout();
|
||||
const VOLUME_STEPS = 16;
|
||||
const SCRUB_STEP_S = 2;
|
||||
const SCRUB_FAST_MS = 120; // steps closer than this count as "scrolling fast"
|
||||
|
||||
const MODES = ['progress', 'scrub', 'rating'];
|
||||
const GLYPH = { right: 310, w: 12, h: 9, gap: 5 }; // shuffle / repeat marks, right of "n of N"
|
||||
|
||||
/** Grey square with a beamed note, for items without cover art. */
|
||||
function drawPlaceholderArt(g, x, y, s) {
|
||||
@@ -94,14 +111,14 @@ function drawRepeatGlyph(g, x, y, color, one, oneFont) {
|
||||
|
||||
// The 256px cover is resampled once per album to the art box's backing-store size (224px), so
|
||||
// each 4 Hz (or 40 fps marquee) redraw is a 1:1 blit instead of a high-quality downscale.
|
||||
const ART_PX = ART.s * SCREEN_SCALE;
|
||||
let artKey = null;
|
||||
let artPx = 0;
|
||||
let artSrc = null;
|
||||
let artCanvas = null;
|
||||
|
||||
function scaledCover(src, key) {
|
||||
// The source canvas counts too: art.clear() redraws covers (late web fonts) as new canvases.
|
||||
if (key === artKey && src === artSrc && artCanvas) return artCanvas;
|
||||
if (key === artKey && src === artSrc && artCanvas && artPx === ART_PX) return artCanvas;
|
||||
if (typeof document === 'undefined') return src;
|
||||
artCanvas ??= document.createElement('canvas');
|
||||
artCanvas.width = artCanvas.height = ART_PX;
|
||||
@@ -111,6 +128,7 @@ function scaledCover(src, key) {
|
||||
g.drawImage(src, 0, 0, ART_PX, ART_PX);
|
||||
artKey = key;
|
||||
artSrc = src;
|
||||
artPx = ART_PX;
|
||||
return artCanvas;
|
||||
}
|
||||
|
||||
@@ -157,6 +175,7 @@ export function createNowPlaying(ctx) {
|
||||
|
||||
const screen = {
|
||||
id: 'nowplaying',
|
||||
adaptive: true,
|
||||
title: 'Now Playing',
|
||||
// Only a moving playhead needs the 4 Hz refresh; paused, the screen is static (no uploads).
|
||||
get refreshMs() { return player.state === 'playing' || player.seeking ? 250 : 0; },
|
||||
@@ -176,6 +195,7 @@ export function createNowPlaying(ctx) {
|
||||
},
|
||||
|
||||
handle(e) {
|
||||
relayout();
|
||||
const now = ctx.now();
|
||||
const cur = player.current;
|
||||
if (e.type === 'press' && e.part === 'select') {
|
||||
@@ -216,6 +236,7 @@ export function createNowPlaying(ctx) {
|
||||
},
|
||||
|
||||
draw(g, r, now) {
|
||||
relayout();
|
||||
g.fillStyle = C.bg;
|
||||
g.fillRect(r.x, r.y, r.w, r.h);
|
||||
const cur = player.current;
|
||||
@@ -310,6 +331,7 @@ export function createNowPlaying(ctx) {
|
||||
},
|
||||
|
||||
animating(now) {
|
||||
relayout();
|
||||
// One redraw when the volume overlay or a scrubber/rating mode times out, even when paused.
|
||||
if (volumeUntil && drawnAt < volumeUntil && now >= volumeUntil) return true;
|
||||
const modeEnd = modeAt + ANIM.npModeTimeoutMs;
|
||||
|
||||
@@ -11,7 +11,6 @@ import { songList, albumSongs } from './main-menu.js';
|
||||
|
||||
const KEYS = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ' ', 'DEL', 'DONE'];
|
||||
const STRIP_H = 44; // letter band at the bottom of the content area
|
||||
const RESULT_ROWS = 7; // 7 x 24 px rows above the band
|
||||
const KEY_W = 22;
|
||||
|
||||
function keyLabel(k) {
|
||||
@@ -26,6 +25,8 @@ export function createSearch(ctx) {
|
||||
const keyFont = font(15, 600);
|
||||
const smallKeyFont = font(10, 700);
|
||||
|
||||
// result rows above the letter band: 7 on the Video, 4 on the nano
|
||||
const resultRows = () => Math.floor((M.H - M.statusH - STRIP_H) / M.rowH);
|
||||
let query = '';
|
||||
let keyIndex = 0;
|
||||
let inResults = false;
|
||||
@@ -92,6 +93,7 @@ export function createSearch(ctx) {
|
||||
return {
|
||||
id: 'search',
|
||||
title: 'Search',
|
||||
adaptive: true, // lays itself out on the current logical screen (os.js)
|
||||
enter() { runSearch(); },
|
||||
handle(e) {
|
||||
if (e.type === 'scroll') {
|
||||
@@ -101,7 +103,7 @@ export function createSearch(ctx) {
|
||||
if (next >= results.length) return false;
|
||||
index = next;
|
||||
if (index < first) first = index;
|
||||
if (index >= first + RESULT_ROWS) first = index - RESULT_ROWS + 1;
|
||||
if (index >= first + resultRows()) first = index - resultRows() + 1;
|
||||
return true;
|
||||
}
|
||||
const next = keyIndex + e.delta;
|
||||
@@ -127,10 +129,11 @@ export function createSearch(ctx) {
|
||||
draw(g, r, now) {
|
||||
g.fillStyle = C.bg;
|
||||
g.fillRect(r.x, r.y, r.w, r.h);
|
||||
const listH = RESULT_ROWS * M.rowH;
|
||||
const scroll = results.length > RESULT_ROWS;
|
||||
const rows = resultRows();
|
||||
const listH = rows * M.rowH;
|
||||
const scroll = results.length > rows;
|
||||
const w = scroll ? M.W - M.scrollbarW : M.W;
|
||||
for (let i = first; i < Math.min(results.length, first + RESULT_ROWS); i++) {
|
||||
for (let i = first; i < Math.min(results.length, first + rows); i++) {
|
||||
const res = results[i];
|
||||
row.label = res.label;
|
||||
row.detail = res.kind;
|
||||
@@ -145,7 +148,7 @@ export function createSearch(ctx) {
|
||||
g.fillText('No results', M.W / 2, r.y + 60);
|
||||
g.restore();
|
||||
}
|
||||
if (scroll) ui.drawScrollbar(g, M.W - M.scrollbarW, r.y, listH, results.length, RESULT_ROWS, first);
|
||||
if (scroll) ui.drawScrollbar(g, M.W - M.scrollbarW, r.y, listH, results.length, rows, first);
|
||||
drawStrip(g, r.y + r.h - STRIP_H);
|
||||
},
|
||||
highlight() {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Settings, following the 5G: values cycle in place (Shuffle, Repeat, Clicker),
|
||||
// option lists show a checkmark and return to Settings when one is picked (Backlight Timer, EQ),
|
||||
// Brightness has its own slider screen, plus About, Date & Time, Legal and Reset Settings.
|
||||
// EQ is VLC's own equalizer presets. (The body's colour is on the main menu: Color.)
|
||||
// EQ is VLC's own equalizer presets. (The body and its colour are on the main menu: assPod Model, Color.)
|
||||
|
||||
import { currentModel } from '../../models.js';
|
||||
|
||||
// ---------------------------------------------------------------- option tables
|
||||
|
||||
@@ -132,22 +134,23 @@ function aboutScreen(ctx) {
|
||||
const videos = lib.videos?.length ?? 0;
|
||||
const albums = lib.albums?.length ?? 0;
|
||||
const secs = (list) => (list ?? []).reduce((s, t) => s + (t.duration || 0), 0);
|
||||
const hours = (secs(lib.tracks) + secs(lib.videos)) / 3600;
|
||||
const hours = (secs(lib.tracks) + (currentModel(ctx.settings).videos ? secs(lib.videos) : 0)) / 3600;
|
||||
const info = lib.info || {};
|
||||
const lines = [
|
||||
['Songs', songs.toLocaleString('en-US')],
|
||||
['Albums', albums.toLocaleString('en-US')],
|
||||
['Videos', videos.toLocaleString('en-US')],
|
||||
...(currentModel(ctx.settings).videos ? [['Videos', videos.toLocaleString('en-US')]] : []),
|
||||
['Playing Time', `${hours.toFixed(1)} h`],
|
||||
['Sources', String((info.sources || []).length)],
|
||||
['Player', 'VLC'],
|
||||
['Model', 'assPod Video'],
|
||||
['Model', currentModel(ctx.settings).about],
|
||||
];
|
||||
const total = lines.length + 1; // + the name line
|
||||
let first = 0;
|
||||
return {
|
||||
id: 'about',
|
||||
title: 'About',
|
||||
adaptive: true,
|
||||
handle(e) {
|
||||
if (e.type === 'scroll') {
|
||||
const next = Math.max(0, Math.min(total - M.rows, first + e.delta));
|
||||
|
||||
@@ -19,7 +19,7 @@ export const FONTS = {
|
||||
};
|
||||
|
||||
export const M = {
|
||||
W: SCREEN_W, H: SCREEN_H,
|
||||
W: SCREEN_W, H: SCREEN_H, // the logical screen: setScreenSize() switches it (the nano's is smaller)
|
||||
statusH: 22, // includes the 1px bottom border; content starts at y=22
|
||||
rowH: 24, rows: 9, // list area y=22..238 (2px white margin at the bottom)
|
||||
padX: 8, // label left inset
|
||||
@@ -28,6 +28,17 @@ export const M = {
|
||||
scrollbarW: 8, // at x=312..320 when a list overflows; rows then end at 312
|
||||
};
|
||||
|
||||
/**
|
||||
* The logical screen, in the theme's own px: the Video's 320 x 240, or the nano's 240 x 180
|
||||
* (models.js). Text and rows keep their size, so on the nano they fill more of the screen (six rows,
|
||||
* as on the real one); the list's row count follows.
|
||||
*/
|
||||
export function setScreenSize(w, h) {
|
||||
M.W = w;
|
||||
M.H = h;
|
||||
M.rows = Math.floor((h - M.statusH - 2) / M.rowH);
|
||||
}
|
||||
|
||||
export const C = {
|
||||
bg: '#ffffff', text: '#000000', textHi: '#ffffff', textDim: '#6b6b6b',
|
||||
status: [[0, '#fdfdfd'], [0.5, '#e4e4e4'], [1, '#c3c3c3']],
|
||||
|
||||
@@ -171,7 +171,7 @@ function drawBattery(g, x, y) {
|
||||
g.lineWidth = 1;
|
||||
roundRectPath(g, 0.5, 0.5, 23, 10, 1.5);
|
||||
g.stroke();
|
||||
// Terminal nub, 2x5 at x = 311.
|
||||
// Terminal nub, 2x5 at the body's right.
|
||||
g.fillStyle = C.batteryBorder;
|
||||
g.fillRect(24, 3, 2, 5);
|
||||
g.restore();
|
||||
@@ -189,7 +189,7 @@ export function drawStatusBar(g, s) {
|
||||
g.fillStyle = C.statusLine;
|
||||
g.fillRect(0, M.statusH - 1, M.W, 1);
|
||||
|
||||
const title = ellipsize(g, s.title ?? '', FONTS.title, 220);
|
||||
const title = ellipsize(g, s.title ?? '', FONTS.title, M.W - 100);
|
||||
g.font = FONTS.title;
|
||||
g.textAlign = 'center';
|
||||
g.textBaseline = 'alphabetic';
|
||||
@@ -200,7 +200,7 @@ export function drawStatusBar(g, s) {
|
||||
|
||||
if (s.play === 'playing' || s.play === 'paused') drawPlayIcon(g, 8, ICON_Y, s.play);
|
||||
if (s.locked) drawLock(g, 22, 5);
|
||||
drawBattery(g, 287, ICON_Y);
|
||||
drawBattery(g, M.W - 33, ICON_Y);
|
||||
g.restore();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// The assPod's settings, for this session only: kept in memory in SteamVR's page (never written to
|
||||
// disk, like the rest of LiquidAss), so a SteamVR restart starts from the defaults.
|
||||
// disk, like the rest of LiquidAss), so a SteamVR restart starts from the defaults. The exception, by
|
||||
// request: the body and its colours (model, finish, nanoFinish), which the plugin keeps across
|
||||
// launches (device/shell_ext/asspod.py PREFS) and hands back at each open.
|
||||
import { Emitter } from './util/emitter.js';
|
||||
|
||||
export const DEFAULTS = Object.freeze({
|
||||
@@ -11,7 +13,9 @@ export const DEFAULTS = Object.freeze({
|
||||
clicker: true,
|
||||
volume: 0.6, // 0..1
|
||||
timeFormat: '12', // '12' | '24'
|
||||
finish: 'black', // 'black' | 'white': the body
|
||||
model: 'video', // 'video' | 'nano': the body (models.js)
|
||||
finish: 'black', // the Video's colour: 'black' | 'white'
|
||||
nanoFinish: 'silver', // the nano's: 'silver' | 'green' | 'blue' | 'pink' | 'black' | 'red'
|
||||
ratings: {}, // trackId -> 0..5 (always replace the object, never mutate it)
|
||||
plays: {}, // trackId -> [playCount, lastPlayed]
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// The assPod's nodes in SteamVR's scene graph (systemui page): the assPod in one hand (the original
|
||||
// 3D model as SteamVR render models, device/asspod/model, with the live screen on a panel), and
|
||||
// The assPod's nodes in SteamVR's scene graph (systemui page): the assPod in one hand (its 3D model,
|
||||
// the Video or the nano, as SteamVR render models from device/asspod/model, or drawn by podd, with
|
||||
// the live screen on a panel), and
|
||||
// while it is out an invisible laser blocker where the menu was, with "(menu glyph) to close".
|
||||
//
|
||||
// vsg-transform parent-path=/user/hand/<side> the controller's pose
|
||||
@@ -26,12 +27,12 @@ const MAIN = 'system.standalone::valve.steam.gamepadui.main';
|
||||
// The sheet, in the window's frame units (the window is 2.67 x 1.5 above its bottom centre; the bar
|
||||
// hangs below it): from under the bar to just over the top, a little wider than the window.
|
||||
const SHEET = { w: 3.2, h: 2.25, y: 0.55, z: 0.06 }; // the blocker
|
||||
const MODEL_W = 0.0618; // m: the model's own width (tools/asspod-model, GEOMETRY.BODY)
|
||||
// The Video's geometry (model metres, tools/asspod-model GEOMETRY), the default when show() gets none.
|
||||
const VIDEO_GEO = { w: 0.0618, d: 0.0110, lcdCy: 0.0242, lcdW: 0.0508, winW: 0.0532, winH: 0.0405, wheelCy: -0.0240 };
|
||||
const VIDEO_SCALE = 0.085 / 0.0618; // as shown (main.js FACE_WIDTH_M): DEFAULT_HOLD is for this body
|
||||
const OVERLAY_DZ = 0.00025; // m (model units) in front of the plate: the screen panel (render-model body)
|
||||
const SCREEN_RECESS = 0.0004; // m (model units) behind the plate's surface: the screen under podd's glass
|
||||
const VIDEO_BELOW = 0.0001; // m (model units) behind the screen panel: podd's video picture
|
||||
const LCD_M = { cy: 0.0242, w: 0.0508 }; // the LCD on the model (face.js LCD, podd.cpp LCD_CY)
|
||||
const WIN_M = { w: 0.0532, h: 0.0405 }; // its window (face.js WINDOW, podd.cpp WIN)
|
||||
const CAPTION = { w: 1.0, y: 0.75, z: 0.075 };
|
||||
// The body in the hand (controller frame: x right, y up, -z forward): its rotation (w x y z) and
|
||||
// centre for the right hand, as placed with Reposition; the left hand mirrors it (defaultHold).
|
||||
@@ -196,24 +197,22 @@ export function createSceneGraph() {
|
||||
// stereoscopy: the left half of its rect to the left eye). podd renders each eye's view of the
|
||||
// model through each face; the faces write no depth, so the screen inside shows through podd's
|
||||
// glass. The faces match podd's (native/podd/podd.cpp makeLayout): centre, right, up, normal.
|
||||
function poddBody(parent, b, rects, pd, video) {
|
||||
const k = b.w / MODEL_W;
|
||||
// The screen panel is the window only, centred on it like the video panel: SteamVR orders
|
||||
// see-through panels by their centres, so two panels on one axis keep their order from any angle
|
||||
// (offset centres swapped it head on, and the screen hid the video).
|
||||
const ppm = rects.face.w / b.w;
|
||||
const ww = WIN_M.w * k * ppm, wh = WIN_M.h * k * ppm;
|
||||
const win = { x: rects.face.x + rects.face.w / 2 - ww / 2, y: rects.face.y + rects.face.h / 2 - LCD_M.cy * k * ppm - wh / 2, w: ww, h: wh };
|
||||
function poddBody(parent, b, rects, pd, video, geo) {
|
||||
const k = b.w / geo.w;
|
||||
// The screen panel is the window only (rects.win: where face.js draws it), centred on it like the
|
||||
// video panel: SteamVR orders see-through panels by their centres, so two panels on one axis keep
|
||||
// their order from any angle (offset centres swapped it head on, and the screen hid the video).
|
||||
const win = rects.win;
|
||||
if (video && Array.isArray(pd.video)) {
|
||||
// the video's picture (podd's video tile, the frame twice: one per eye) just under the screen,
|
||||
// which is clear over it but for the overlays; first, so the screen blends over it
|
||||
const [vx, vy, vw, vh] = pd.video;
|
||||
const pic = vxf([0, LCD_M.cy * k, b.d / 2 - (SCREEN_RECESS + VIDEO_BELOW) * k]);
|
||||
const pic = vxf([0, geo.lcdCy * k, b.d / 2 - (SCREEN_RECESS + VIDEO_BELOW) * k]);
|
||||
pic.appendChild(vnode('panel', {
|
||||
key: pd.key,
|
||||
uv_min: [r6(vx / pd.texW), r6(vy / pd.texH)],
|
||||
uv_max: [r6((vx + 2 * vw) / pd.texW), r6((vy + vh) / pd.texH)],
|
||||
'meters-per-pixel': r6(LCD_M.w * k / vw),
|
||||
'meters-per-pixel': r6(geo.lcdW * k / vw),
|
||||
origin: [0, 0],
|
||||
interactive: false,
|
||||
visibility: 0,
|
||||
@@ -224,8 +223,8 @@ export function createSceneGraph() {
|
||||
parent.appendChild(pic);
|
||||
}
|
||||
// recessed in the plate's opening, under podd's glass (which sits on the plate's surface)
|
||||
const front = vxf([0, LCD_M.cy * k, b.d / 2 - SCREEN_RECESS * k]);
|
||||
front.appendChild(pagePanel(win, b.w / rects.face.w, 'lgs-asspod-screen'));
|
||||
const front = vxf([0, geo.lcdCy * k, b.d / 2 - SCREEN_RECESS * k]);
|
||||
front.appendChild(pagePanel(win, geo.winW * k / win.w, 'lgs-asspod-screen'));
|
||||
parent.appendChild(front);
|
||||
const [hx, hy, hz] = pd.box;
|
||||
const faces = {
|
||||
@@ -262,8 +261,23 @@ export function createSceneGraph() {
|
||||
|
||||
// The body without podd: the model's parts as SteamVR render models (fixed lighting), the screen
|
||||
// panel just in front of the plate.
|
||||
function body(parent, b, rects, modelDir, finish) {
|
||||
const k = b.w / MODEL_W;
|
||||
function body(parent, b, rects, modelDir, finish, model, geo) {
|
||||
const k = b.w / geo.w;
|
||||
if (model === 'nano') {
|
||||
// the nano's parts: its colour's aluminum, the shared caps, ports and well; the screen panel
|
||||
// over its window
|
||||
const scaled = vxf([0, 0, 0]);
|
||||
scaled.setAttribute('scale', [k, k, k].map(r6).join(' '));
|
||||
const wheel = finish === 'black' ? 'black' : 'white';
|
||||
const parts = [`nano_shell_${finish}`, `nano_back_${finish}`, `nano_center_${finish}`, `nano_wheel_${wheel}`,
|
||||
'nano_cap_top', 'nano_cap_bottom', 'nano_ports', 'nano_well'];
|
||||
for (const name of parts) scaled.appendChild(vnode('rendermodel', { source: `${modelDir}/${name}/${name}.obj` }));
|
||||
parent.appendChild(scaled);
|
||||
const front = vxf([0, geo.lcdCy * k, b.d / 2 + OVERLAY_DZ * k]);
|
||||
front.appendChild(pagePanel(rects.win, geo.winW * k / rects.win.w, 'lgs-asspod-screen'));
|
||||
parent.appendChild(front);
|
||||
return;
|
||||
}
|
||||
const fin = finish === 'white' ? 'white' : 'black';
|
||||
const scaled = vxf([0, 0, 0]);
|
||||
scaled.setAttribute('scale', [k, k, k].map(r6).join(' '));
|
||||
@@ -281,15 +295,24 @@ export function createSceneGraph() {
|
||||
|
||||
// The default place in the hand: where you put it with Reposition (2026-10-09, right hand), and
|
||||
// its mirror image across the controller's centre line for the left hand.
|
||||
function defaultHold(side) {
|
||||
// Another body keeps its wheel where the Video's is (over the stick), front faces level.
|
||||
function defaultHold(side, b, geo) {
|
||||
const r = DEFAULT_HOLD;
|
||||
if (side === 'right') return { rot: r.rot.slice(), centre: r.centre.slice() };
|
||||
return { rot: [r.rot[0], r.rot[1], -r.rot[2], -r.rot[3]], centre: [-r.centre[0], r.centre[1], r.centre[2]] };
|
||||
const h = side === 'right'
|
||||
? { rot: r.rot.slice(), centre: r.centre.slice() }
|
||||
: { rot: [r.rot[0], r.rot[1], -r.rot[2], -r.rot[3]], centre: [-r.centre[0], r.centre[1], r.centre[2]] };
|
||||
const k = b.w / geo.w;
|
||||
const wheelV = [0, VIDEO_GEO.wheelCy * VIDEO_SCALE, VIDEO_GEO.d * VIDEO_SCALE / 2];
|
||||
const wheelM = [0, geo.wheelCy * k, b.d / 2];
|
||||
const d = qrot(h.rot, wheelV.map((v, i) => v - wheelM[i]));
|
||||
h.centre = h.centre.map((v, i) => v + d[i]);
|
||||
return h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the assPod in `hand` ('left' | 'right') with the sheet over the menu.
|
||||
* @param {{ hand: string, body: {w,h,d,r}, rects: {face, back, dim, text}, modelDir: string, finish: string,
|
||||
* @param {{ hand: string, body: {w,h,d,r}, rects: {face, back, win, dim, text}, modelDir: string, finish: string,
|
||||
* model?: 'video'|'nano', geo?: {w, d, lcdCy, lcdW, winW, winH, wheelCy} (model metres),
|
||||
* podd?: { key, texW, texH, box, tiles }, hold?: { rot, centre }, world?: { rot, centre } }} o
|
||||
* podd: its layout (podd-out.json) when podd draws the body; hold: a place in the hand other than
|
||||
* the default; world: pinned in the room (standing space) instead of on the hand
|
||||
@@ -300,14 +323,15 @@ export function createSceneGraph() {
|
||||
if (!attach()) return null;
|
||||
clearNodes();
|
||||
const side = o.hand === 'left' ? 'left' : 'right';
|
||||
const { rot, centre } = o.hold && o.hold.rot && o.hold.centre ? o.hold : defaultHold(side);
|
||||
const geo = o.geo || VIDEO_GEO;
|
||||
const { rot, centre } = o.hold && o.hold.rot && o.hold.centre ? o.hold : defaultHold(side, o.body, geo);
|
||||
// pinned in the room (Reposition: the controller moves, the assPod stays), else on the hand
|
||||
const hand = o.world
|
||||
? vxf(o.world.centre, o.world.rot)
|
||||
: vxf([0, 0, 0], null, { 'parent-path': '/user/hand/' + side });
|
||||
const hold = o.world ? vxf([0, 0, 0]) : vxf(centre, rot);
|
||||
if (o.podd && o.podd.tiles && Array.isArray(o.podd.box)) poddBody(hold, o.body, o.rects, o.podd, o.video);
|
||||
else body(hold, o.body, o.rects, o.modelDir, o.finish);
|
||||
if (o.podd && o.podd.tiles && Array.isArray(o.podd.box)) poddBody(hold, o.body, o.rects, o.podd, o.video, geo);
|
||||
else body(hold, o.body, o.rects, o.modelDir, o.finish, o.model || 'video', geo);
|
||||
hand.appendChild(hold);
|
||||
st.root.appendChild(hand);
|
||||
// the sheet and the caption, on Steam's window
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
device/vr/systemui.asspod.js), with the menu dimmed behind it.
|
||||
- While it is out (the daemon's bridge value 'asspod' {open, at, ttlMs}, renewed while it stays
|
||||
out): every gamepad input Steam gets is swallowed (FocusNavController's catch-all), so the
|
||||
sticks and buttons drive only the assPod. A (OK, or the left controller's button in that place,
|
||||
DIR_DOWN), B (CANCEL, or DIR_LEFT) and the hamburger (START) go to the daemon as asspod.btn: A is
|
||||
the centre button, B the wheel's Menu, the hamburger puts the assPod away. The Steam /
|
||||
sticks and buttons drive only the assPod. A (OK), B (CANCEL) and the hamburger (START) go to the
|
||||
daemon as asspod.btn: A is the centre button, B the wheel's Menu, the hamburger puts the assPod
|
||||
away. The d-pad directions are not relayed: Steam sends them for the left stick's tilts too, and
|
||||
both sticks reach the wheel from their controller models (device/asspod/src/input). The Steam /
|
||||
quick-access buttons pass. Steam's windows also take no laser clicks meanwhile
|
||||
(html.lgs-asspod-out, theme/34-asspod.css). A bridge value older
|
||||
than its ttl ends all of this on its own, so a dead daemon never leaves Steam's input taken.
|
||||
@@ -29,10 +30,10 @@ __LGS_RT.define({
|
||||
if (!R.patch.targets.dashboardBar) throw new Error('asspod: no dashboardBar target');
|
||||
|
||||
// Steam's gamepad buttons (EGamepadButton)
|
||||
const BTN = { OK: 1, CANCEL: 2, DIR_DOWN: 10, DIR_LEFT: 11, START: 14, STEAM_GUIDE: 27, STEAM_QUICK_MENU: 28 };
|
||||
// button -> [what it does, which controller]
|
||||
const MAP = { [BTN.OK]: ['ok', 'right'], [BTN.CANCEL]: ['menu', 'right'], [BTN.DIR_DOWN]: ['ok', 'left'],
|
||||
[BTN.DIR_LEFT]: ['menu', 'left'], [BTN.START]: ['close', 'right'] };
|
||||
const BTN = { OK: 1, CANCEL: 2, START: 14, STEAM_GUIDE: 27, STEAM_QUICK_MENU: 28 };
|
||||
// button -> [what it does, which controller]. No d-pad directions: tilting the left stick sends
|
||||
// them too, and pressed the centre button (down) or Menu (left).
|
||||
const MAP = { [BTN.OK]: ['ok', 'right'], [BTN.CANCEL]: ['menu', 'right'], [BTN.START]: ['close', 'right'] };
|
||||
const OUT_CLASS = 'lgs-asspod-out';
|
||||
const ICON = jsx('svg', { viewBox: '0 0 24 24', fill: 'currentColor', 'aria-hidden': 'true', children: [
|
||||
jsx('path', { d: 'M9 3.6 20 1.4v14.1a3.4 3.4 0 1 1-2-3.1V6.3L11 7.7v10.1A3.4 3.4 0 1 1 9 14.7z' }, 'n'),
|
||||
|
||||
@@ -54,8 +54,17 @@ ACTIONS = {
|
||||
PAGE = "systemui"
|
||||
MODEL_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "asspod", "model") # the 3D body
|
||||
PODD = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))), "native", "podd", "podd")
|
||||
PODD_PREBUILT = os.path.join(os.path.dirname(PODD), "prebuilt", "podd")
|
||||
PODD_SPEC = "/dev/shm/lgs/podd.json"
|
||||
PODD_OUT = "/dev/shm/lgs/podd-out.json"
|
||||
# the bodies podd draws and their colours (device/asspod/src/models.js, native/podd/podd.cpp)
|
||||
PODD_FINISHES = {"video": ("black", "white"), "nano": ("silver", "green", "blue", "pink", "black", "red")}
|
||||
NANO_SCALE = 1.3125 # the nano as shown (models.js MODELS.nano.scale)
|
||||
# The body you picked (assPod Model) and each body's colour, kept across launches beside the glass
|
||||
# tune (lgs.TUNE): {"model": "video"|"nano", "finish": <the Video's>, "nanoFinish": <the nano's>}.
|
||||
# Written only when you change them; uninstall removes it with the folder.
|
||||
PREFS = os.path.join(os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config"), "glass-shell", "asspod.json")
|
||||
PREFS_DEFAULT = {"model": "video", "finish": "black", "nanoFinish": "silver"}
|
||||
API = "window.__LGS_VRX && window.__LGS_VRX.asspod && window.__LGS_VRX.asspod.api"
|
||||
HOME = os.path.expanduser("~")
|
||||
FLATPAK_VLC = "org.videolan.VLC"
|
||||
@@ -596,14 +605,16 @@ async def _poll_loop(ctx):
|
||||
s["open"], s["playing"] = bool(r.get("open")), bool(r.get("playing"))
|
||||
if isinstance(r.get("holds"), dict):
|
||||
s["holds"] = r["holds"] # Reposition's places, kept for the next open
|
||||
_prefs_note(ctx, r.get("prefs")) # the body and colours you picked, kept across launches
|
||||
# podd follows the body: on the hand (hold) or pinned in the room while repositioning (world)
|
||||
if s["open"] and s.get("podd") is not None and s["podd"].returncode is None:
|
||||
h, w = r.get("hold") or {}, r.get("world")
|
||||
fin = r.get("finish") if r.get("finish") in ("black", "white") else "black"
|
||||
want = (json.dumps(h, sort_keys=True), json.dumps(w, sort_keys=True), fin)
|
||||
model = r.get("model") if r.get("model") in PODD_FINISHES else "video"
|
||||
fin = r.get("finish") if r.get("finish") in PODD_FINISHES[model] else PODD_FINISHES[model][0]
|
||||
want = (json.dumps(h, sort_keys=True), json.dumps(w, sort_keys=True), model, fin)
|
||||
if want != s.get("podd_pose") and h.get("rot") and h.get("centre"):
|
||||
s["podd_pose"] = want
|
||||
_podd_spec(s, rot=h["rot"], centre=h["centre"], world=w, finish=fin)
|
||||
_podd_spec(s, rot=h["rot"], centre=h["centre"], world=w, model=model, finish=fin)
|
||||
if s["open"]:
|
||||
s["hand"] = r.get("hand")
|
||||
if was_open and not s["open"]:
|
||||
@@ -1080,7 +1091,8 @@ def _model_dir():
|
||||
# ------------------------------------------------------------------ podd (native/podd: the body with real shaders)
|
||||
|
||||
def _podd_spec(s, **kw):
|
||||
spec = dict(s.get("podd_spec") or {"open": False, "hand": "right", "scale": 0.085 / 0.0618, "finish": "black"})
|
||||
spec = dict(s.get("podd_spec") or {"open": False, "hand": "right", "scale": 0.085 / 0.0618, "nanoScale": NANO_SCALE,
|
||||
"model": "video", "finish": "black"})
|
||||
spec.update(kw)
|
||||
s["podd_spec"] = spec
|
||||
os.makedirs(os.path.dirname(PODD_SPEC), exist_ok=True)
|
||||
@@ -1112,12 +1124,19 @@ def _podd_kill_strays():
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
async def _podd_start(ctx, model_dir, scale, finish):
|
||||
async def _podd_start(ctx, model_dir, scale, finish, nano_scale=NANO_SCALE, model="video"):
|
||||
"""podd up with its layout (podd-out.json), or None (then SteamVR's render models draw the body)."""
|
||||
s = _st(ctx)
|
||||
if not os.access(PODD, os.X_OK) and os.path.isfile(PODD_PREBUILT):
|
||||
# the release's shipped podd (built for the Frame), when none was built here
|
||||
try:
|
||||
shutil.copyfile(PODD_PREBUILT, PODD)
|
||||
os.chmod(PODD, 0o755)
|
||||
except OSError as e:
|
||||
_err(ctx, f"podd: {e!r}")
|
||||
if not os.access(PODD, os.X_OK):
|
||||
return None
|
||||
_podd_spec(s, open=False, scale=scale, finish=finish)
|
||||
_podd_spec(s, open=False, scale=scale, nanoScale=nano_scale, model=model, finish=finish)
|
||||
p = s.get("podd")
|
||||
# a podd older than its binary (a rebuild) is replaced
|
||||
if p is not None and p.returncode is None and os.path.getmtime(PODD) > s.get("podd_t0", 0):
|
||||
@@ -1169,6 +1188,49 @@ async def _podd_stop(ctx):
|
||||
pass
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ the body you picked, kept
|
||||
|
||||
def _prefs_clean(p):
|
||||
"""The model and colours from `p`, each checked; the defaults for anything missing or wrong."""
|
||||
p = p if isinstance(p, dict) else {}
|
||||
out = dict(PREFS_DEFAULT)
|
||||
if p.get("model") in PODD_FINISHES:
|
||||
out["model"] = p["model"]
|
||||
if p.get("finish") in PODD_FINISHES["video"]:
|
||||
out["finish"] = p["finish"]
|
||||
if p.get("nanoFinish") in PODD_FINISHES["nano"]:
|
||||
out["nanoFinish"] = p["nanoFinish"]
|
||||
return out
|
||||
|
||||
|
||||
def _prefs_load(s):
|
||||
if s.get("prefs") is None:
|
||||
try:
|
||||
with open(PREFS, encoding="utf-8") as f:
|
||||
s["prefs"] = _prefs_clean(json.load(f))
|
||||
except (OSError, ValueError):
|
||||
s["prefs"] = dict(PREFS_DEFAULT)
|
||||
return s["prefs"]
|
||||
|
||||
|
||||
def _prefs_note(ctx, p):
|
||||
"""The page's current model and colours (each poll): saved when they changed."""
|
||||
s = _st(ctx)
|
||||
if not isinstance(p, dict):
|
||||
return
|
||||
want = _prefs_clean(p)
|
||||
if want == _prefs_load(s):
|
||||
return
|
||||
s["prefs"] = want
|
||||
try:
|
||||
os.makedirs(os.path.dirname(PREFS), exist_ok=True)
|
||||
with open(PREFS + ".tmp", "w", encoding="utf-8") as f:
|
||||
json.dump(want, f)
|
||||
os.replace(PREFS + ".tmp", PREFS)
|
||||
except OSError as e:
|
||||
_err(ctx, f"prefs: {e!r}")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ actions
|
||||
|
||||
async def _open(ctx):
|
||||
@@ -1178,8 +1240,12 @@ async def _open(ctx):
|
||||
raise RuntimeError("the assPod page script is not installed in SteamVR (systemui)")
|
||||
if not st.get("open"):
|
||||
model_dir = _model_dir()
|
||||
podd = await _podd_start(ctx, model_dir, st.get("bodyScale") or 0.085 / 0.0618, st.get("finish") or "black")
|
||||
r = await _page(ctx, f"A.open({json.dumps({'modelDir': model_dir, 'podd': podd, 'holds': s.get('holds'), 'resume': _resume(s)})})")
|
||||
prefs = _prefs_load(s)
|
||||
model = prefs["model"]
|
||||
podd = await _podd_start(ctx, model_dir, st.get("bodyScale") or 0.085 / 0.0618,
|
||||
prefs["nanoFinish"] if model == "nano" else prefs["finish"],
|
||||
st.get("nanoScale") or NANO_SCALE, model)
|
||||
r = await _page(ctx, f"A.open({json.dumps({'modelDir': model_dir, 'podd': podd, 'holds': s.get('holds'), 'resume': _resume(s), 'prefs': prefs})})")
|
||||
if not r or not r.get("open"):
|
||||
await _podd_stop(ctx)
|
||||
raise RuntimeError(f"open failed: {(r or {}).get('error')}")
|
||||
|
||||
@@ -2319,6 +2319,11 @@ __e.drawEdge = drawEdge;
|
||||
//
|
||||
// The static body is drawn once per finish into a layer; a frame blits it, then the screen and the
|
||||
// wheel state. draw() returns false when nothing changed, so the page repaints only on change.
|
||||
//
|
||||
// The nano (models.js) has only its screen here, in its window, filling most of the canvas: podd or
|
||||
// its render models draw the rest, and the page shows just the window's rect (windowRect('nano')).
|
||||
|
||||
const { MODELS } = __req('models.js');
|
||||
|
||||
const BODY = { w: 61.8, h: 103.5, corner: 6.5 };
|
||||
const LCD = { cy: 24.2, w: 50.8, h: 38.1 };
|
||||
@@ -2367,6 +2372,41 @@ function createFace(canvas, screen, opts = {}) {
|
||||
|
||||
const lcd = { x: X(-LCD.w / 2), y: Y(LCD.cy + LCD.h / 2), w: LCD.w * k, h: LCD.h * k };
|
||||
const wc = { x: X(0), y: Y(WHEEL.cy) };
|
||||
// the nano's window: 90% of the canvas's width, centred
|
||||
const NANO = MODELS.nano;
|
||||
const nk = W * 0.9 / NANO.win.w; // px per mm
|
||||
const nanoWin = { w: NANO.win.w * nk, h: NANO.win.h * nk };
|
||||
nanoWin.x = (W - nanoWin.w) / 2;
|
||||
nanoWin.y = (H - nanoWin.h) / 2;
|
||||
const nanoLcd = { w: NANO.lcd.w * nk, h: NANO.lcd.h * nk };
|
||||
nanoLcd.x = (W - nanoLcd.w) / 2;
|
||||
nanoLcd.y = (H - nanoLcd.h) / 2;
|
||||
|
||||
/** The model's screen window in this canvas (px): what the page shows over the window. */
|
||||
function windowRect(model) {
|
||||
if (model === 'nano') return { ...nanoWin };
|
||||
return { x: X(-WINDOW.w / 2), y: Y(LCD.cy + WINDOW.h / 2), w: WINDOW.w * k, h: WINDOW.h * k };
|
||||
}
|
||||
|
||||
// The nano: its LCD in a black window (rounded like the nano's), the backlight and the glass.
|
||||
function drawNano(s) {
|
||||
roundRect(g, nanoWin.x, nanoWin.y, nanoWin.w, nanoWin.h, 0.6 * nk);
|
||||
g.fillStyle = '#000';
|
||||
g.fill();
|
||||
g.imageSmoothingEnabled = true;
|
||||
g.imageSmoothingQuality = 'high';
|
||||
g.drawImage(screen, nanoLcd.x, nanoLcd.y, nanoLcd.w, nanoLcd.h);
|
||||
const veil = 1 - Math.max(0, Math.min(1, s.backlight));
|
||||
if (veil > 0.001) {
|
||||
g.fillStyle = `rgba(0,0,0,${veil})`;
|
||||
g.fillRect(nanoLcd.x, nanoLcd.y, nanoLcd.w, nanoLcd.h);
|
||||
}
|
||||
const refl = g.createLinearGradient(nanoLcd.x, nanoLcd.y, nanoLcd.x + nanoLcd.w, nanoLcd.y + nanoLcd.h);
|
||||
refl.addColorStop(0, 'rgba(255,255,255,0.07)');
|
||||
refl.addColorStop(0.5, 'rgba(255,255,255,0)');
|
||||
g.fillStyle = refl;
|
||||
g.fillRect(nanoLcd.x, nanoLcd.y, nanoLcd.w, nanoLcd.h);
|
||||
}
|
||||
|
||||
function bodyLayer(finish) {
|
||||
let c = layers.get(finish);
|
||||
@@ -2519,12 +2559,13 @@ function createFace(canvas, screen, opts = {}) {
|
||||
if (marker !== target) {
|
||||
marker = target > marker ? Math.min(1, marker + dt / MARKER_IN) : Math.max(0, marker - dt / MARKER_OUT);
|
||||
}
|
||||
const key = [s.finish, Math.round(s.backlight * 100), s.pressed || '', marker > 0 ? Math.round(s.angle * 200) : -1,
|
||||
const key = [s.model || 'video', s.finish, Math.round(s.backlight * 100), s.pressed || '', marker > 0 ? Math.round(s.angle * 200) : -1,
|
||||
Math.round(marker * 20), s.reflVersion || 0, s.screenOnly ? 1 : 0, s.videoUnder ? 1 : 0].join('|');
|
||||
if (!s.screenDirty && key === last) return false;
|
||||
last = key;
|
||||
const f = FINISHES[s.finish] || FINISHES.black;
|
||||
g.clearRect(0, 0, W, H);
|
||||
if (s.model === 'nano') { drawNano(s); return true; }
|
||||
if (overlay) {
|
||||
roundRect(g, X(-WINDOW.w / 2), Y(LCD.cy + WINDOW.h / 2), WINDOW.w * k, WINDOW.h * k, WINDOW.r * k);
|
||||
g.fillStyle = '#000';
|
||||
@@ -2567,7 +2608,7 @@ function createFace(canvas, screen, opts = {}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return { draw, get animating() { return marker > 0 && marker < 1; } };
|
||||
return { draw, windowRect, get animating() { return marker > 0 && marker < 1; } };
|
||||
}
|
||||
|
||||
/** The mirror-polished steel back with the peach engraved in it (static: drawn once). Polished
|
||||
@@ -3048,7 +3089,8 @@ function createControllerReader() {
|
||||
__defs['main.js'] = function (__e) {
|
||||
__e.default = install;
|
||||
// assPod: a 5th-generation-style music player in your hand in the SteamVR menu, playing the VLC
|
||||
// media library. This is its half inside SteamVR's systemui page (installed by the daemon as the
|
||||
// media library. The main menu's assPod Model swaps the body for a 2nd-generation nano (models.js):
|
||||
// the same OS, scaled onto its smaller screen, and no video. This is its half inside SteamVR's systemui page (installed by the daemon as the
|
||||
// page script device/vr/systemui.asspod.js, built from these sources by tools/build_asspod.py);
|
||||
// the daemon's plugin device/shell_ext/asspod.py runs VLC and reads the library.
|
||||
//
|
||||
@@ -3061,8 +3103,9 @@ __e.default = install;
|
||||
// Controls: both sticks turn the click wheel; a stick click presses the wheel button under it
|
||||
// (centred: the centre button). A is the centre button, B is the wheel's Menu, and the hamburger
|
||||
// (menu) button puts the assPod away; so does closing the SteamVR menu. The right controller's
|
||||
// buttons are read here from its model; the left one's (A and B in the same places) come from Steam
|
||||
// through the plugin (btn()). Music keeps playing: the player keeps running here at a few Hz, and
|
||||
// buttons are read here from its model, Steam's A / B / hamburger come through the plugin (btn())
|
||||
// otherwise. Both sticks act alike: only their models are read (Steam's d-pad directions, which the
|
||||
// left stick's tilts also send, are not relayed). Music keeps playing: the player keeps running here at a few Hz, and
|
||||
// the plugin keeps VLC up while anything plays and stops it when the assPod is away and nothing plays.
|
||||
//
|
||||
// The plugin talks to this page only through the returned api: poll() drains the commands for VLC
|
||||
@@ -3082,6 +3125,7 @@ const { createFace, createBack, BODY } = __req('face.js');
|
||||
const { drawPeachSilhouette } = __req('brand/peach.js');
|
||||
const { createReflector, pose, qmul, qrot, qconj } = __req('reflect.js');
|
||||
const { createSceneGraph } = __req('sg.js');
|
||||
const { MODELS, currentModel, currentFinish, bodyMetres } = __req('models.js');
|
||||
|
||||
const FACE_W = 476; // px; the body is 61.8 x 103.5 mm
|
||||
const FACE_H = Math.round(FACE_W * BODY.h / BODY.w);
|
||||
@@ -3089,6 +3133,10 @@ const FACE_WIDTH_M = 0.085; // shown about 1.4x the real body, fo
|
||||
const BODY_M = { // the 3D body in metres, at that scale
|
||||
w: FACE_WIDTH_M, h: FACE_WIDTH_M * BODY.h / BODY.w, d: FACE_WIDTH_M * 11 / BODY.w, r: FACE_WIDTH_M * BODY.corner / BODY.w,
|
||||
};
|
||||
const NANO_M = bodyMetres(MODELS.nano); // the nano, at its own scale
|
||||
// a model's geometry for the scene graph (model metres)
|
||||
const geoOf = (m) => ({ w: m.body.w / 1000, d: m.body.d / 1000, lcdCy: m.lcd.cy / 1000, lcdW: m.lcd.w / 1000,
|
||||
winW: m.win.w / 1000, winH: m.win.h / 1000, wheelCy: m.wheel.cy / 1000 });
|
||||
const BACK_W = 238; // px: the steel back's live reflection (half the face's resolution)
|
||||
const BACK_H = Math.round(BACK_W * BODY.h / BODY.w);
|
||||
const REFLECT_EVERY = 2; // frames between reflection updates (about 30 a second)
|
||||
@@ -3200,9 +3248,15 @@ function install(ctx) {
|
||||
library.onLoad(() => { art.clear(); os.ctx.popToRoot(); os.invalidate(); });
|
||||
// the EQ follows the setting (VLC's preset by name)
|
||||
settings.on('change', ({ key }) => { if (key === 'eq' || key === '*') send({ op: 'eq', name: settings.get('eq') }); });
|
||||
// the body's colour: podd follows the finish in the poll; the render-model body is rebuilt
|
||||
// The body: podd follows the model and its finish in the poll (one layout holds both); the scene
|
||||
// graph is rebuilt for the other body's screen panel and place in the hand, or for the render
|
||||
// models' colour. (Picking the nano stops a video: os/screens/main-menu.js.)
|
||||
settings.on('change', ({ key }) => {
|
||||
if ((key === 'finish' || key === '*') && st.open && !st.podd) st.holdPose = sg.show(showOpts(st.hand, st.world)) || st.holdPose;
|
||||
if (!st.open) return;
|
||||
if (key === 'model' || key === '*' || (!st.podd && (key === 'finish' || key === 'nanoFinish'))) {
|
||||
if (key === 'model' && st.world) unpin(false);
|
||||
st.holdPose = sg.show(showOpts(st.hand, st.world)) || st.holdPose;
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------- state
|
||||
@@ -3295,11 +3349,13 @@ function install(ctx) {
|
||||
videoMode();
|
||||
const screenDirty = os.render(t);
|
||||
// the painted reflections only without podd (podd shades the glass and the steel itself)
|
||||
if (!st.podd && st.frames % REFLECT_EVERY === 0 && reflector.update(st.hand, st.holdPose)) back.draw(reflector.back, reflector.version);
|
||||
const model = currentModel(settings).id;
|
||||
const painted = !st.podd && model === 'video'; // the nano's render models carry their own back
|
||||
if (painted && st.frames % REFLECT_EVERY === 0 && reflector.update(st.hand, st.holdPose)) back.draw(reflector.back, reflector.version);
|
||||
const ws = wheel.state;
|
||||
face.draw({
|
||||
reflection: !st.podd && reflector.version ? reflector.front : null, reflVersion: reflector.version, screenOnly: !!st.podd, videoUnder: !!st.videoMode,
|
||||
finish: settings.get('finish'), backlight: os.backlight, screenDirty,
|
||||
reflection: painted && reflector.version ? reflector.front : null, reflVersion: reflector.version, screenOnly: !!st.podd, videoUnder: !!st.videoMode,
|
||||
model, finish: currentFinish(settings), backlight: os.backlight, screenDirty,
|
||||
touching: ws.touching, angle: ws.angle, pressed: ws.pressedPart || (st.keys.select ? 'select' : st.keys.menu ? 'menu' : null), now: t,
|
||||
});
|
||||
} catch (err) { note(err); }
|
||||
@@ -3317,9 +3373,15 @@ function install(ctx) {
|
||||
if (player.state === 'playing') st.bgTimer = setTimeout(background, BACKGROUND_MS);
|
||||
}
|
||||
|
||||
// Reposition's places are kept per body: the Video's under the hand's name, the nano's as nano-<hand>
|
||||
const holdKey = (hand) => (currentModel(settings).id === 'video' ? hand : `${currentModel(settings).id}-${hand}`);
|
||||
|
||||
function showOpts(hand, world) {
|
||||
return { hand, body: BODY_M, rects, modelDir: st.modelDir, finish: settings.get('finish'), podd: st.podd,
|
||||
hold: st.holds[hand] || null, world: world || null, video: !!st.videoMode };
|
||||
const m = currentModel(settings);
|
||||
const wr = face.windowRect(m.id);
|
||||
rects.win = { x: rects.face.x + wr.x, y: rects.face.y + wr.y, w: wr.w, h: wr.h };
|
||||
return { hand, body: m.id === 'video' ? BODY_M : NANO_M, rects, modelDir: st.modelDir, model: m.id, geo: geoOf(m),
|
||||
finish: currentFinish(settings), podd: st.podd, hold: st.holds[holdKey(hand)] || null, world: world || null, video: !!st.videoMode };
|
||||
}
|
||||
|
||||
// A video on the screen with podd: podd shows its picture (the daemon's frames, full rate) in a panel
|
||||
@@ -3351,7 +3413,7 @@ function install(ctx) {
|
||||
const hp = pose('/user/hand/' + st.hand);
|
||||
if (hp) {
|
||||
const inv = qconj(hp.q);
|
||||
st.holds[st.hand] = {
|
||||
st.holds[holdKey(st.hand)] = {
|
||||
rot: qmul(inv, st.world.rot),
|
||||
centre: qrot(inv, st.world.centre.map((v, i) => v - hp.t[i])),
|
||||
};
|
||||
@@ -3366,6 +3428,7 @@ function install(ctx) {
|
||||
return {
|
||||
id: 'reposition',
|
||||
title: 'Reposition',
|
||||
adaptive: true,
|
||||
keepAwake: true,
|
||||
enter() { if (!pin()) note('reposition: no hand pose'); },
|
||||
exit() { if (st.world) unpin(false); },
|
||||
@@ -3374,7 +3437,7 @@ function install(ctx) {
|
||||
if (e.type !== 'press') return e.type === 'release' ? false : undefined;
|
||||
if (e.part === 'select') return done(() => unpin(true));
|
||||
if (e.part === 'menu') return done(() => unpin(false));
|
||||
if (e.part === 'play') return done(() => { st.holds[st.hand] = null; unpin(false); });
|
||||
if (e.part === 'play') return done(() => { st.holds[holdKey(st.hand)] = null; unpin(false); });
|
||||
return false;
|
||||
},
|
||||
draw(g, r) {
|
||||
@@ -3428,6 +3491,10 @@ function install(ctx) {
|
||||
for (const [p, t] of Object.entries(opts.resume)) if (Number.isFinite(t)) resume.set(p, t);
|
||||
}
|
||||
if (opts.holds && typeof opts.holds === 'object') st.holds = Object.assign({ left: null, right: null }, opts.holds);
|
||||
// the body and colours picked last time (the plugin keeps them: device/shell_ext/asspod.py PREFS)
|
||||
if (opts.prefs && typeof opts.prefs === 'object') {
|
||||
for (const key of ['model', 'finish', 'nanoFinish']) if (typeof opts.prefs[key] === 'string') settings.set(key, opts.prefs[key]);
|
||||
}
|
||||
st.world = null;
|
||||
st.videoMode = false;
|
||||
art.videoExternal = false;
|
||||
@@ -3485,7 +3552,8 @@ function install(ctx) {
|
||||
function status() {
|
||||
return {
|
||||
ok: true, open: st.open, hand: st.hand, closedBy: st.closedBy, loading: st.loading,
|
||||
hold: st.holdPose || null, bodyScale: BODY_M.w / 0.0618, finish: settings.get('finish'), podd: !!st.podd,
|
||||
hold: st.holdPose || null, bodyScale: BODY_M.w / 0.0618, nanoScale: MODELS.nano.scale, model: currentModel(settings).id,
|
||||
finish: currentFinish(settings), podd: !!st.podd,
|
||||
player: { state: player.state, current: player.current ? { id: player.current.id, title: player.current.title } : null,
|
||||
position: Math.round((player.position || 0) * 10) / 10, queue: player.queue.length, volume: player.volume },
|
||||
library: { tracks: library.tracks.length, albums: library.albums.length, videos: library.videos.length },
|
||||
@@ -3501,6 +3569,7 @@ function install(ctx) {
|
||||
os.ctx.push({
|
||||
id: 'notice',
|
||||
title: String(title || 'assPod'),
|
||||
adaptive: true,
|
||||
handle(e) {
|
||||
if (e.type === 'press' && (e.part === 'select' || e.part === 'menu')) { os.ctx.pop(); return true; }
|
||||
return e.type === 'scroll' ? false : undefined;
|
||||
@@ -3542,7 +3611,8 @@ function install(ctx) {
|
||||
playing: player.state === 'playing', state: player.state,
|
||||
current: player.current ? player.current.path : null,
|
||||
volume: player.volume, eq: settings.get('eq'),
|
||||
hold: st.holdPose || null, world: st.world, holds: st.holds, finish: settings.get('finish'),
|
||||
hold: st.holdPose || null, world: st.world, holds: st.holds, model: currentModel(settings).id, finish: currentFinish(settings),
|
||||
prefs: { model: currentModel(settings).id, finish: settings.get('finish'), nanoFinish: settings.get('nanoFinish') },
|
||||
position: Math.round((player.position || 0) * 1000) / 1000,
|
||||
video: player.current && player.current.kind === 'video' && os.ctx.top()?.id === 'videoplayer' && st.open,
|
||||
};
|
||||
@@ -4398,6 +4468,75 @@ class Player extends Emitter {
|
||||
this.#settings.set('plays', { ...plays, [item.id]: [item.playCount ?? 0, item.lastPlayed] });
|
||||
}
|
||||
}
|
||||
};
|
||||
// ---------------------------------------------------------------- models.js
|
||||
__defs['models.js'] = function (__e) {
|
||||
__e.currentModel = currentModel;
|
||||
__e.currentFinish = currentFinish;
|
||||
__e.bodyMetres = bodyMetres;
|
||||
// The assPod's bodies: the Video (5th generation, the original) and the nano (2nd generation). The
|
||||
// main menu's assPod Model picks one; Color picks among that body's own finishes. Both run the same
|
||||
// OS on a 4:3 screen; the nano's is 240 x 180 logical px against the Video's 320 x 240, so its text is
|
||||
// larger on its screen, as on the real one. The nano plays no video: its menus leave the Videos out.
|
||||
//
|
||||
// Geometry in millimetres, model frame (tools/asspod-model, tools/nano2-model GEOMETRY): origin at
|
||||
// the body's centre, y toward the top. `scale` is how much larger than life it is shown in the hand;
|
||||
// podd (native/podd/podd.cpp VIDEO / NANO) and its render models use the same numbers. `screen` is
|
||||
// the OS's logical screen on that body.
|
||||
|
||||
const MODELS = Object.freeze({
|
||||
video: Object.freeze({
|
||||
id: 'video',
|
||||
label: 'Video Gen 1',
|
||||
about: 'assPod Video',
|
||||
body: { w: 61.8, h: 103.5, d: 11.0 },
|
||||
lcd: { cy: 24.2, w: 50.8, h: 38.1 },
|
||||
win: { w: 53.2, h: 40.5 },
|
||||
wheel: { cy: -24.0 },
|
||||
screen: { w: 320, h: 240 }, // logical px (os/theme.js setScreenSize)
|
||||
scale: 0.085 / 0.0618, // shown 85 mm wide
|
||||
finishKey: 'finish',
|
||||
finishes: [['black', 'Black'], ['white', 'White']],
|
||||
videos: true,
|
||||
}),
|
||||
nano: Object.freeze({
|
||||
id: 'nano',
|
||||
label: 'Nano Gen 2',
|
||||
about: 'assPod nano',
|
||||
body: { w: 40.0, h: 90.0, d: 6.5 },
|
||||
lcd: { cy: 28.88, w: 29.6, h: 22.2 },
|
||||
win: { w: 31.62, h: 24.04 },
|
||||
wheel: { cy: -13.3 },
|
||||
screen: { w: 240, h: 180 }, // the same text on less screen: six rows, as on the real nano
|
||||
scale: 1.3125, // shown 52.5 mm wide
|
||||
finishKey: 'nanoFinish',
|
||||
finishes: [['silver', 'Silver'], ['green', 'Green'], ['blue', 'Blue'], ['pink', 'Pink'], ['black', 'Black'], ['red', '(PRODUCT)RED']],
|
||||
videos: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const MODEL_ORDER = ['video', 'nano'];
|
||||
|
||||
/** The current body's definition. */
|
||||
function currentModel(settings) {
|
||||
return MODELS[settings.get('model')] || MODELS.video;
|
||||
}
|
||||
|
||||
/** The current body's finish (one of its own; its first otherwise). */
|
||||
function currentFinish(settings) {
|
||||
const m = currentModel(settings);
|
||||
const f = settings.get(m.finishKey);
|
||||
return m.finishes.some(([k]) => k === f) ? f : m.finishes[0][0];
|
||||
}
|
||||
|
||||
/** The body in metres as shown in the hand: { w, h, d } at the model's scale. */
|
||||
function bodyMetres(m) {
|
||||
const k = m.scale / 1000;
|
||||
return { w: m.body.w * k, h: m.body.h * k, d: m.body.d * k };
|
||||
}
|
||||
|
||||
__e.MODELS = MODELS;
|
||||
__e.MODEL_ORDER = MODEL_ORDER;
|
||||
};
|
||||
// ---------------------------------------------------------------- os/menu.js
|
||||
__defs['os/menu.js'] = function (__e) {
|
||||
@@ -4470,7 +4609,11 @@ function createMenu(ctx, def) {
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
// the highlight stays on its item when the list changes around it (the main menu without Videos)
|
||||
const was = items[index];
|
||||
items = resolve(def.items) ?? [];
|
||||
const j = was ? items.indexOf(was) : -1;
|
||||
if (j >= 0) index = j;
|
||||
clampWindow();
|
||||
}
|
||||
|
||||
@@ -4584,9 +4727,11 @@ function createMenu(ctx, def) {
|
||||
__e.createOS = createOS;
|
||||
// assPod OS core: screen stack, 220 ms slide transitions, status bar, backlight timer and dimming,
|
||||
// sleep/wake, the boot logo, the default button behavior, and render throttling for the screen
|
||||
// texture. Draws the 320x240 logical LCD onto the 640x480 canvas that main.js uploads as a texture.
|
||||
// texture. Draws the logical LCD (320x240 on the Video, 240x180 on the nano) onto the 640x480 canvas
|
||||
// that main.js uploads as a texture.
|
||||
|
||||
const { SCREEN_W } = __req('config.js');
|
||||
const { currentModel } = __req('models.js');
|
||||
const { settings: defaultSettings } = __req('settings.js');
|
||||
const { drawPeachSilhouette } = __req('brand/peach.js');
|
||||
const theme = __req('os/theme.js');
|
||||
@@ -4595,7 +4740,7 @@ const { createMenu } = __req('os/menu.js');
|
||||
const { createMainMenu } = __req('os/screens/main-menu.js');
|
||||
const { createNowPlaying } = __req('os/screens/now-playing.js');
|
||||
|
||||
const { M, C, ANIM } = theme;
|
||||
const { M, C, ANIM, setScreenSize } = theme;
|
||||
|
||||
/**
|
||||
* @typedef {import('../input/clickwheel.js').WheelEvent} WheelEvent
|
||||
@@ -4628,8 +4773,9 @@ const { M, C, ANIM } = theme;
|
||||
|
||||
// ---------------------------------------------------------------- constants
|
||||
|
||||
const CONTENT = Object.freeze({ x: 0, y: M.statusH, w: M.W, h: M.H - M.statusH });
|
||||
const FULL = Object.freeze({ x: 0, y: 0, w: M.W, h: M.H });
|
||||
// the content area under the status bar, and the whole screen (the logical size can change: models.js)
|
||||
const content = () => ({ x: 0, y: M.statusH, w: M.W, h: M.H - M.statusH });
|
||||
const full = () => ({ x: 0, y: 0, w: M.W, h: M.H });
|
||||
const FRAME_MS = 1000 / ANIM.maxFps;
|
||||
// rAF timestamps jitter by a fraction of a ms; without slack a 25 ms cap would skip a frame at 80 Hz.
|
||||
const FRAME_SLACK_MS = 1;
|
||||
@@ -4658,7 +4804,12 @@ function normKey(s) {
|
||||
*/
|
||||
function createOS({ canvas, player, library, art, apps, settings = defaultSettings, boot: bootEnabled = false }) {
|
||||
const g = canvas.getContext('2d');
|
||||
const scale = canvas.width / SCREEN_W;
|
||||
// The body's logical screen (models.js): the canvas stays the same size and the scale follows.
|
||||
const applyModel = () => { const m = currentModel(settings); setScreenSize(m.screen.w, m.screen.h); };
|
||||
applyModel();
|
||||
if (typeof settings.on === 'function') {
|
||||
settings.on('change', ({ key }) => { if (key === 'model' || key === '*') { applyModel(); dirty = DIRTY_NOW; } });
|
||||
}
|
||||
|
||||
/** @type {Screen[]} */
|
||||
const stack = [];
|
||||
@@ -4944,8 +5095,18 @@ function createOS({ canvas, player, library, art, apps, settings = defaultSettin
|
||||
}
|
||||
|
||||
const reported = new WeakSet();
|
||||
// Menus and the screens marked `adaptive` lay themselves out on the current logical screen; the
|
||||
// rest (the games, the clock, Cover Flow...) are drawn for the Video's 320 x 240 and scaled to fit.
|
||||
function drawScreen(s, r, now) {
|
||||
try {
|
||||
if (!s.isMenu && !s.adaptive && M.W !== SCREEN_W) {
|
||||
const k = M.W / SCREEN_W;
|
||||
g.save();
|
||||
g.scale(k, k);
|
||||
s.draw(g, { x: r.x / k, y: r.y / k, w: r.w / k, h: r.h / k }, now);
|
||||
g.restore();
|
||||
return;
|
||||
}
|
||||
s.draw(g, r, now);
|
||||
} catch (err) {
|
||||
// One broken screen must not take the whole OS down every frame; report it once.
|
||||
@@ -4957,12 +5118,13 @@ function createOS({ canvas, player, library, art, apps, settings = defaultSettin
|
||||
if (dx <= -M.W || dx >= M.W) return;
|
||||
g.save();
|
||||
g.beginPath();
|
||||
g.rect(CONTENT.x, CONTENT.y, CONTENT.w, CONTENT.h);
|
||||
const r = content();
|
||||
g.rect(r.x, r.y, r.w, r.h);
|
||||
g.clip();
|
||||
g.translate(dx, 0);
|
||||
g.fillStyle = C.bg;
|
||||
g.fillRect(CONTENT.x, CONTENT.y, CONTENT.w, CONTENT.h);
|
||||
drawScreen(s, CONTENT, now);
|
||||
g.fillRect(r.x, r.y, r.w, r.h);
|
||||
drawScreen(s, r, now);
|
||||
g.restore();
|
||||
}
|
||||
|
||||
@@ -4981,7 +5143,7 @@ function createOS({ canvas, player, library, art, apps, settings = defaultSettin
|
||||
g.fillStyle = '#000000';
|
||||
g.fillRect(0, 0, M.W, M.H);
|
||||
}
|
||||
drawScreen(s, FULL, now);
|
||||
drawScreen(s, full(), now);
|
||||
g.restore();
|
||||
return;
|
||||
}
|
||||
@@ -4993,6 +5155,7 @@ function createOS({ canvas, player, library, art, apps, settings = defaultSettin
|
||||
}
|
||||
|
||||
function paint(now) {
|
||||
const scale = canvas.width / M.W;
|
||||
g.setTransform(scale, 0, 0, scale, 0, 0);
|
||||
g.globalAlpha = 1;
|
||||
g.globalCompositeOperation = 'source-over';
|
||||
@@ -5226,6 +5389,7 @@ __e.createMainMenu = createMainMenu;
|
||||
const { createRng, hashString } = __req('util/rng.js');
|
||||
const { createSettings } = __req('os/screens/settings.js');
|
||||
const { createSearch } = __req('os/screens/search.js');
|
||||
const { MODELS, MODEL_ORDER, currentModel, currentFinish } = __req('models.js');
|
||||
|
||||
// ---------------------------------------------------------------- sorting
|
||||
|
||||
@@ -5254,7 +5418,9 @@ function albumOrder(library) {
|
||||
* A list of songs (or videos, in playlists). Selecting row i plays the whole list from i,
|
||||
* the way the assPod queues whatever list you picked from, then opens Now Playing.
|
||||
*/
|
||||
function songList(ctx, id, title, list) {
|
||||
function songList(ctx, id, title, all) {
|
||||
// the nano plays no video: a playlist's videos are left out on it
|
||||
const list = currentModel(ctx.settings).videos ? all : all.filter((t) => t.kind !== 'video');
|
||||
const items = list.map((t, i) => ({
|
||||
label: t.title,
|
||||
speaker: () => ctx.player.current?.id === t.id && ctx.player.state !== 'stopped',
|
||||
@@ -5392,31 +5558,84 @@ function shuffleSongs(ctx) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- the body: model and colour
|
||||
|
||||
/** assPod Model: the Video or the nano, checkmarked; picking one switches the body and goes back.
|
||||
* The nano plays no video: a video playing stops when it is picked. */
|
||||
function modelMenu(ctx) {
|
||||
return ctx.menu({
|
||||
id: 'model',
|
||||
title: 'assPod Model',
|
||||
initialIndex: () => Math.max(0, MODEL_ORDER.indexOf(currentModel(ctx.settings).id)),
|
||||
items: MODEL_ORDER.map((id) => ({
|
||||
label: MODELS[id].label,
|
||||
check: () => currentModel(ctx.settings).id === id,
|
||||
action: (c) => {
|
||||
if (!MODELS[id].videos && c.player.current?.kind === 'video') c.player.stop();
|
||||
c.settings.set('model', id);
|
||||
c.pop();
|
||||
return true;
|
||||
},
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
/** Color: the current body's own finishes, checkmarked. */
|
||||
function colorMenu(ctx) {
|
||||
const m = currentModel(ctx.settings);
|
||||
return ctx.menu({
|
||||
id: 'color',
|
||||
title: 'Color',
|
||||
initialIndex: () => Math.max(0, m.finishes.findIndex(([k]) => k === currentFinish(ctx.settings))),
|
||||
items: m.finishes.map(([key, label]) => ({
|
||||
label,
|
||||
check: () => currentFinish(ctx.settings) === key,
|
||||
action: (c) => {
|
||||
c.settings.set(m.finishKey, key);
|
||||
c.pop();
|
||||
return true;
|
||||
},
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- main menu
|
||||
|
||||
function createMainMenu(ctx) {
|
||||
const base = [
|
||||
const finishLabel = () => {
|
||||
const m = currentModel(ctx.settings);
|
||||
const f = currentFinish(ctx.settings);
|
||||
return (m.finishes.find(([k]) => k === f) || m.finishes[0])[1];
|
||||
};
|
||||
const all = [
|
||||
{ id: 'music', label: 'Music', open: createMusicMenu },
|
||||
{ id: 'videos', label: 'Videos', open: (c) => c.apps.videos(c) },
|
||||
{ id: 'extras', label: 'Extras', open: (c) => c.apps.extras(c) },
|
||||
{ id: 'settings', label: 'Settings', open: createSettings },
|
||||
{ id: 'reposition', label: 'Reposition', open: (c) => c.apps.reposition(c) },
|
||||
{
|
||||
id: 'color',
|
||||
label: 'Color',
|
||||
detail: () => (ctx.settings.get('finish') === 'white' ? 'White' : 'Black'),
|
||||
action: (c) => { c.settings.set('finish', c.settings.get('finish') === 'white' ? 'black' : 'white'); return true; },
|
||||
},
|
||||
{ id: 'model', label: 'assPod Model', detail: () => currentModel(ctx.settings).label, open: modelMenu },
|
||||
{ id: 'color', label: 'Color', detail: finishLabel, open: colorMenu },
|
||||
{ id: 'shuffle', label: 'Shuffle Songs', action: shuffleSongs },
|
||||
{ id: 'backlight', label: 'Backlight', action: (c) => { c.os.toggleBacklight(); return false; } },
|
||||
];
|
||||
const withNowPlaying = [
|
||||
...base,
|
||||
{ id: 'nowplaying', label: 'Now Playing', chevron: true, action: (c) => { c.nowPlaying(); return true; } },
|
||||
];
|
||||
const nowPlaying = { id: 'nowplaying', label: 'Now Playing', chevron: true, action: (c) => { c.nowPlaying(); return true; } };
|
||||
// the same item objects every time, so the menu keeps its place across a model switch
|
||||
const lists = new Map();
|
||||
return ctx.menu({
|
||||
id: 'main',
|
||||
title: 'assPod',
|
||||
// Like the 5G, Now Playing is listed only while something is queued (not after the queue ends).
|
||||
items: () => (ctx.player.current && ctx.player.state !== 'stopped' ? withNowPlaying : base),
|
||||
// Like the 5G, Now Playing is listed only while something is queued (not after the queue ends);
|
||||
// the nano has no Videos.
|
||||
items: () => {
|
||||
const videos = currentModel(ctx.settings).videos;
|
||||
const playing = !!(ctx.player.current && ctx.player.state !== 'stopped');
|
||||
const key = `${videos}|${playing}`;
|
||||
if (!lists.has(key)) {
|
||||
const base = videos ? all : all.filter((it) => it.id !== 'videos');
|
||||
lists.set(key, playing ? [...base, nowPlaying] : base);
|
||||
}
|
||||
return lists.get(key);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -5427,23 +5646,40 @@ __e.createNowPlaying = createNowPlaying;
|
||||
// artist / album on the right, and a progress bar with elapsed and remaining time.
|
||||
// Center click cycles progress -> scrubber -> rating; the wheel sets volume, seeks or rates.
|
||||
|
||||
const { SCREEN_SCALE } = __req('config.js');
|
||||
const { SCREEN_W, SCREEN_SCALE } = __req('config.js');
|
||||
const { M } = __req('os/theme.js');
|
||||
|
||||
// ---------------------------------------------------------------- layout
|
||||
|
||||
const POS = { x: 10, y: 40 };
|
||||
const ART = { x: 10, y: 48, s: 112 };
|
||||
const TEXT = { x: 132, w: 180, title: 82, artist: 102, album: 122 };
|
||||
const BAR = { x: 10, y: 182, w: 300, h: 10 };
|
||||
const TIME_Y = 208;
|
||||
const STARS = { cx: 160, cy: 187 };
|
||||
const VOLUME = { x: 10, y: 180, w: 300 };
|
||||
// The Video's 320 x 240 layout, and the nano's on its 240 x 180 screen (the same text, less room:
|
||||
// smaller art and a shorter bar). relayout() picks the one for the current logical screen.
|
||||
const LAYOUTS = {
|
||||
320: {
|
||||
POS: { x: 10, y: 40 }, ART: { x: 10, y: 48, s: 112 }, TEXT: { x: 132, w: 180, title: 82, artist: 102, album: 122 },
|
||||
BAR: { x: 10, y: 182, w: 300, h: 10 }, TIME_Y: 208, STARS: { cx: 160, cy: 187 }, VOLUME: { x: 10, y: 180, w: 300 },
|
||||
GLYPH: { right: 310, w: 12, h: 9, gap: 5 },
|
||||
},
|
||||
240: {
|
||||
POS: { x: 8, y: 38 }, ART: { x: 8, y: 46, s: 80 }, TEXT: { x: 96, w: 136, title: 66, artist: 84, album: 102 },
|
||||
BAR: { x: 8, y: 136, w: 224, h: 9 }, TIME_Y: 160, STARS: { cx: 120, cy: 141 }, VOLUME: { x: 8, y: 134, w: 224 },
|
||||
GLYPH: { right: 232, w: 12, h: 9, gap: 5 },
|
||||
},
|
||||
};
|
||||
let POS, ART, TEXT, BAR, TIME_Y, STARS, VOLUME, GLYPH, ART_PX;
|
||||
let laidOut = 0;
|
||||
function relayout() {
|
||||
if (laidOut === M.W) return;
|
||||
laidOut = M.W;
|
||||
({ POS, ART, TEXT, BAR, TIME_Y, STARS, VOLUME, GLYPH } = LAYOUTS[M.W] || LAYOUTS[320]);
|
||||
// the cover is resampled to the art box's backing-store size (the canvas is 640 px wide)
|
||||
ART_PX = Math.round(ART.s * SCREEN_W * SCREEN_SCALE / M.W);
|
||||
}
|
||||
relayout();
|
||||
const VOLUME_STEPS = 16;
|
||||
const SCRUB_STEP_S = 2;
|
||||
const SCRUB_FAST_MS = 120; // steps closer than this count as "scrolling fast"
|
||||
|
||||
const MODES = ['progress', 'scrub', 'rating'];
|
||||
const GLYPH = { right: 310, w: 12, h: 9, gap: 5 }; // shuffle / repeat marks, right of "n of N"
|
||||
|
||||
/** Grey square with a beamed note, for items without cover art. */
|
||||
function drawPlaceholderArt(g, x, y, s) {
|
||||
@@ -5519,14 +5755,14 @@ function drawRepeatGlyph(g, x, y, color, one, oneFont) {
|
||||
|
||||
// The 256px cover is resampled once per album to the art box's backing-store size (224px), so
|
||||
// each 4 Hz (or 40 fps marquee) redraw is a 1:1 blit instead of a high-quality downscale.
|
||||
const ART_PX = ART.s * SCREEN_SCALE;
|
||||
let artKey = null;
|
||||
let artPx = 0;
|
||||
let artSrc = null;
|
||||
let artCanvas = null;
|
||||
|
||||
function scaledCover(src, key) {
|
||||
// The source canvas counts too: art.clear() redraws covers (late web fonts) as new canvases.
|
||||
if (key === artKey && src === artSrc && artCanvas) return artCanvas;
|
||||
if (key === artKey && src === artSrc && artCanvas && artPx === ART_PX) return artCanvas;
|
||||
if (typeof document === 'undefined') return src;
|
||||
artCanvas ??= document.createElement('canvas');
|
||||
artCanvas.width = artCanvas.height = ART_PX;
|
||||
@@ -5536,6 +5772,7 @@ function scaledCover(src, key) {
|
||||
g.drawImage(src, 0, 0, ART_PX, ART_PX);
|
||||
artKey = key;
|
||||
artSrc = src;
|
||||
artPx = ART_PX;
|
||||
return artCanvas;
|
||||
}
|
||||
|
||||
@@ -5582,6 +5819,7 @@ function createNowPlaying(ctx) {
|
||||
|
||||
const screen = {
|
||||
id: 'nowplaying',
|
||||
adaptive: true,
|
||||
title: 'Now Playing',
|
||||
// Only a moving playhead needs the 4 Hz refresh; paused, the screen is static (no uploads).
|
||||
get refreshMs() { return player.state === 'playing' || player.seeking ? 250 : 0; },
|
||||
@@ -5601,6 +5839,7 @@ function createNowPlaying(ctx) {
|
||||
},
|
||||
|
||||
handle(e) {
|
||||
relayout();
|
||||
const now = ctx.now();
|
||||
const cur = player.current;
|
||||
if (e.type === 'press' && e.part === 'select') {
|
||||
@@ -5641,6 +5880,7 @@ function createNowPlaying(ctx) {
|
||||
},
|
||||
|
||||
draw(g, r, now) {
|
||||
relayout();
|
||||
g.fillStyle = C.bg;
|
||||
g.fillRect(r.x, r.y, r.w, r.h);
|
||||
const cur = player.current;
|
||||
@@ -5735,6 +5975,7 @@ function createNowPlaying(ctx) {
|
||||
},
|
||||
|
||||
animating(now) {
|
||||
relayout();
|
||||
// One redraw when the volume overlay or a scrubber/rating mode times out, even when paused.
|
||||
if (volumeUntil && drawnAt < volumeUntil && now >= volumeUntil) return true;
|
||||
const modeEnd = modeAt + ANIM.npModeTimeoutMs;
|
||||
@@ -5768,7 +6009,6 @@ const { songList, albumSongs } = __req('os/screens/main-menu.js');
|
||||
|
||||
const KEYS = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ' ', 'DEL', 'DONE'];
|
||||
const STRIP_H = 44; // letter band at the bottom of the content area
|
||||
const RESULT_ROWS = 7; // 7 x 24 px rows above the band
|
||||
const KEY_W = 22;
|
||||
|
||||
function keyLabel(k) {
|
||||
@@ -5783,6 +6023,8 @@ function createSearch(ctx) {
|
||||
const keyFont = font(15, 600);
|
||||
const smallKeyFont = font(10, 700);
|
||||
|
||||
// result rows above the letter band: 7 on the Video, 4 on the nano
|
||||
const resultRows = () => Math.floor((M.H - M.statusH - STRIP_H) / M.rowH);
|
||||
let query = '';
|
||||
let keyIndex = 0;
|
||||
let inResults = false;
|
||||
@@ -5849,6 +6091,7 @@ function createSearch(ctx) {
|
||||
return {
|
||||
id: 'search',
|
||||
title: 'Search',
|
||||
adaptive: true, // lays itself out on the current logical screen (os.js)
|
||||
enter() { runSearch(); },
|
||||
handle(e) {
|
||||
if (e.type === 'scroll') {
|
||||
@@ -5858,7 +6101,7 @@ function createSearch(ctx) {
|
||||
if (next >= results.length) return false;
|
||||
index = next;
|
||||
if (index < first) first = index;
|
||||
if (index >= first + RESULT_ROWS) first = index - RESULT_ROWS + 1;
|
||||
if (index >= first + resultRows()) first = index - resultRows() + 1;
|
||||
return true;
|
||||
}
|
||||
const next = keyIndex + e.delta;
|
||||
@@ -5884,10 +6127,11 @@ function createSearch(ctx) {
|
||||
draw(g, r, now) {
|
||||
g.fillStyle = C.bg;
|
||||
g.fillRect(r.x, r.y, r.w, r.h);
|
||||
const listH = RESULT_ROWS * M.rowH;
|
||||
const scroll = results.length > RESULT_ROWS;
|
||||
const rows = resultRows();
|
||||
const listH = rows * M.rowH;
|
||||
const scroll = results.length > rows;
|
||||
const w = scroll ? M.W - M.scrollbarW : M.W;
|
||||
for (let i = first; i < Math.min(results.length, first + RESULT_ROWS); i++) {
|
||||
for (let i = first; i < Math.min(results.length, first + rows); i++) {
|
||||
const res = results[i];
|
||||
row.label = res.label;
|
||||
row.detail = res.kind;
|
||||
@@ -5902,7 +6146,7 @@ function createSearch(ctx) {
|
||||
g.fillText('No results', M.W / 2, r.y + 60);
|
||||
g.restore();
|
||||
}
|
||||
if (scroll) ui.drawScrollbar(g, M.W - M.scrollbarW, r.y, listH, results.length, RESULT_ROWS, first);
|
||||
if (scroll) ui.drawScrollbar(g, M.W - M.scrollbarW, r.y, listH, results.length, rows, first);
|
||||
drawStrip(g, r.y + r.h - STRIP_H);
|
||||
},
|
||||
highlight() {
|
||||
@@ -5919,7 +6163,9 @@ __e.createSettings = createSettings;
|
||||
// Settings, following the 5G: values cycle in place (Shuffle, Repeat, Clicker),
|
||||
// option lists show a checkmark and return to Settings when one is picked (Backlight Timer, EQ),
|
||||
// Brightness has its own slider screen, plus About, Date & Time, Legal and Reset Settings.
|
||||
// EQ is VLC's own equalizer presets. (The body's colour is on the main menu: Color.)
|
||||
// EQ is VLC's own equalizer presets. (The body and its colour are on the main menu: assPod Model, Color.)
|
||||
|
||||
const { currentModel } = __req('models.js');
|
||||
|
||||
// ---------------------------------------------------------------- option tables
|
||||
|
||||
@@ -6050,22 +6296,23 @@ function aboutScreen(ctx) {
|
||||
const videos = lib.videos?.length ?? 0;
|
||||
const albums = lib.albums?.length ?? 0;
|
||||
const secs = (list) => (list ?? []).reduce((s, t) => s + (t.duration || 0), 0);
|
||||
const hours = (secs(lib.tracks) + secs(lib.videos)) / 3600;
|
||||
const hours = (secs(lib.tracks) + (currentModel(ctx.settings).videos ? secs(lib.videos) : 0)) / 3600;
|
||||
const info = lib.info || {};
|
||||
const lines = [
|
||||
['Songs', songs.toLocaleString('en-US')],
|
||||
['Albums', albums.toLocaleString('en-US')],
|
||||
['Videos', videos.toLocaleString('en-US')],
|
||||
...(currentModel(ctx.settings).videos ? [['Videos', videos.toLocaleString('en-US')]] : []),
|
||||
['Playing Time', `${hours.toFixed(1)} h`],
|
||||
['Sources', String((info.sources || []).length)],
|
||||
['Player', 'VLC'],
|
||||
['Model', 'assPod Video'],
|
||||
['Model', currentModel(ctx.settings).about],
|
||||
];
|
||||
const total = lines.length + 1; // + the name line
|
||||
let first = 0;
|
||||
return {
|
||||
id: 'about',
|
||||
title: 'About',
|
||||
adaptive: true,
|
||||
handle(e) {
|
||||
if (e.type === 'scroll') {
|
||||
const next = Math.max(0, Math.min(total - M.rows, first + e.delta));
|
||||
@@ -6252,6 +6499,7 @@ __e.EQ_NAMES = EQ_NAMES;
|
||||
};
|
||||
// ---------------------------------------------------------------- os/theme.js
|
||||
__defs['os/theme.js'] = function (__e) {
|
||||
__e.setScreenSize = setScreenSize;
|
||||
// 5G visual spec, logical px on the 320x240 screen
|
||||
const { SCREEN_W, SCREEN_H } = __req('config.js');
|
||||
|
||||
@@ -6273,7 +6521,7 @@ const FONTS = {
|
||||
};
|
||||
|
||||
const M = {
|
||||
W: SCREEN_W, H: SCREEN_H,
|
||||
W: SCREEN_W, H: SCREEN_H, // the logical screen: setScreenSize() switches it (the nano's is smaller)
|
||||
statusH: 22, // includes the 1px bottom border; content starts at y=22
|
||||
rowH: 24, rows: 9, // list area y=22..238 (2px white margin at the bottom)
|
||||
padX: 8, // label left inset
|
||||
@@ -6282,6 +6530,17 @@ const M = {
|
||||
scrollbarW: 8, // at x=312..320 when a list overflows; rows then end at 312
|
||||
};
|
||||
|
||||
/**
|
||||
* The logical screen, in the theme's own px: the Video's 320 x 240, or the nano's 240 x 180
|
||||
* (models.js). Text and rows keep their size, so on the nano they fill more of the screen (six rows,
|
||||
* as on the real one); the list's row count follows.
|
||||
*/
|
||||
function setScreenSize(w, h) {
|
||||
M.W = w;
|
||||
M.H = h;
|
||||
M.rows = Math.floor((h - M.statusH - 2) / M.rowH);
|
||||
}
|
||||
|
||||
const C = {
|
||||
bg: '#ffffff', text: '#000000', textHi: '#ffffff', textDim: '#6b6b6b',
|
||||
status: [[0, '#fdfdfd'], [0.5, '#e4e4e4'], [1, '#c3c3c3']],
|
||||
@@ -6508,7 +6767,7 @@ function drawBattery(g, x, y) {
|
||||
g.lineWidth = 1;
|
||||
roundRectPath(g, 0.5, 0.5, 23, 10, 1.5);
|
||||
g.stroke();
|
||||
// Terminal nub, 2x5 at x = 311.
|
||||
// Terminal nub, 2x5 at the body's right.
|
||||
g.fillStyle = C.batteryBorder;
|
||||
g.fillRect(24, 3, 2, 5);
|
||||
g.restore();
|
||||
@@ -6526,7 +6785,7 @@ function drawStatusBar(g, s) {
|
||||
g.fillStyle = C.statusLine;
|
||||
g.fillRect(0, M.statusH - 1, M.W, 1);
|
||||
|
||||
const title = ellipsize(g, s.title ?? '', FONTS.title, 220);
|
||||
const title = ellipsize(g, s.title ?? '', FONTS.title, M.W - 100);
|
||||
g.font = FONTS.title;
|
||||
g.textAlign = 'center';
|
||||
g.textBaseline = 'alphabetic';
|
||||
@@ -6537,7 +6796,7 @@ function drawStatusBar(g, s) {
|
||||
|
||||
if (s.play === 'playing' || s.play === 'paused') drawPlayIcon(g, 8, ICON_Y, s.play);
|
||||
if (s.locked) drawLock(g, 22, 5);
|
||||
drawBattery(g, 287, ICON_Y);
|
||||
drawBattery(g, M.W - 33, ICON_Y);
|
||||
g.restore();
|
||||
}
|
||||
|
||||
@@ -6970,7 +7229,9 @@ __e.qconj = qconj;
|
||||
// ---------------------------------------------------------------- settings.js
|
||||
__defs['settings.js'] = function (__e) {
|
||||
// The assPod's settings, for this session only: kept in memory in SteamVR's page (never written to
|
||||
// disk, like the rest of LiquidAss), so a SteamVR restart starts from the defaults.
|
||||
// disk, like the rest of LiquidAss), so a SteamVR restart starts from the defaults. The exception, by
|
||||
// request: the body and its colours (model, finish, nanoFinish), which the plugin keeps across
|
||||
// launches (device/shell_ext/asspod.py PREFS) and hands back at each open.
|
||||
const { Emitter } = __req('util/emitter.js');
|
||||
|
||||
const DEFAULTS = Object.freeze({
|
||||
@@ -6982,7 +7243,9 @@ const DEFAULTS = Object.freeze({
|
||||
clicker: true,
|
||||
volume: 0.6, // 0..1
|
||||
timeFormat: '12', // '12' | '24'
|
||||
finish: 'black', // 'black' | 'white': the body
|
||||
model: 'video', // 'video' | 'nano': the body (models.js)
|
||||
finish: 'black', // the Video's colour: 'black' | 'white'
|
||||
nanoFinish: 'silver', // the nano's: 'silver' | 'green' | 'blue' | 'pink' | 'black' | 'red'
|
||||
ratings: {}, // trackId -> 0..5 (always replace the object, never mutate it)
|
||||
plays: {}, // trackId -> [playCount, lastPlayed]
|
||||
});
|
||||
@@ -7011,8 +7274,9 @@ __e.settings = settings;
|
||||
// ---------------------------------------------------------------- sg.js
|
||||
__defs['sg.js'] = function (__e) {
|
||||
__e.createSceneGraph = createSceneGraph;
|
||||
// The assPod's nodes in SteamVR's scene graph (systemui page): the assPod in one hand (the original
|
||||
// 3D model as SteamVR render models, device/asspod/model, with the live screen on a panel), and
|
||||
// The assPod's nodes in SteamVR's scene graph (systemui page): the assPod in one hand (its 3D model,
|
||||
// the Video or the nano, as SteamVR render models from device/asspod/model, or drawn by podd, with
|
||||
// the live screen on a panel), and
|
||||
// while it is out an invisible laser blocker where the menu was, with "(menu glyph) to close".
|
||||
//
|
||||
// vsg-transform parent-path=/user/hand/<side> the controller's pose
|
||||
@@ -7039,12 +7303,12 @@ const MAIN = 'system.standalone::valve.steam.gamepadui.main';
|
||||
// The sheet, in the window's frame units (the window is 2.67 x 1.5 above its bottom centre; the bar
|
||||
// hangs below it): from under the bar to just over the top, a little wider than the window.
|
||||
const SHEET = { w: 3.2, h: 2.25, y: 0.55, z: 0.06 }; // the blocker
|
||||
const MODEL_W = 0.0618; // m: the model's own width (tools/asspod-model, GEOMETRY.BODY)
|
||||
// The Video's geometry (model metres, tools/asspod-model GEOMETRY), the default when show() gets none.
|
||||
const VIDEO_GEO = { w: 0.0618, d: 0.0110, lcdCy: 0.0242, lcdW: 0.0508, winW: 0.0532, winH: 0.0405, wheelCy: -0.0240 };
|
||||
const VIDEO_SCALE = 0.085 / 0.0618; // as shown (main.js FACE_WIDTH_M): DEFAULT_HOLD is for this body
|
||||
const OVERLAY_DZ = 0.00025; // m (model units) in front of the plate: the screen panel (render-model body)
|
||||
const SCREEN_RECESS = 0.0004; // m (model units) behind the plate's surface: the screen under podd's glass
|
||||
const VIDEO_BELOW = 0.0001; // m (model units) behind the screen panel: podd's video picture
|
||||
const LCD_M = { cy: 0.0242, w: 0.0508 }; // the LCD on the model (face.js LCD, podd.cpp LCD_CY)
|
||||
const WIN_M = { w: 0.0532, h: 0.0405 }; // its window (face.js WINDOW, podd.cpp WIN)
|
||||
const CAPTION = { w: 1.0, y: 0.75, z: 0.075 };
|
||||
// The body in the hand (controller frame: x right, y up, -z forward): its rotation (w x y z) and
|
||||
// centre for the right hand, as placed with Reposition; the left hand mirrors it (defaultHold).
|
||||
@@ -7209,24 +7473,22 @@ function createSceneGraph() {
|
||||
// stereoscopy: the left half of its rect to the left eye). podd renders each eye's view of the
|
||||
// model through each face; the faces write no depth, so the screen inside shows through podd's
|
||||
// glass. The faces match podd's (native/podd/podd.cpp makeLayout): centre, right, up, normal.
|
||||
function poddBody(parent, b, rects, pd, video) {
|
||||
const k = b.w / MODEL_W;
|
||||
// The screen panel is the window only, centred on it like the video panel: SteamVR orders
|
||||
// see-through panels by their centres, so two panels on one axis keep their order from any angle
|
||||
// (offset centres swapped it head on, and the screen hid the video).
|
||||
const ppm = rects.face.w / b.w;
|
||||
const ww = WIN_M.w * k * ppm, wh = WIN_M.h * k * ppm;
|
||||
const win = { x: rects.face.x + rects.face.w / 2 - ww / 2, y: rects.face.y + rects.face.h / 2 - LCD_M.cy * k * ppm - wh / 2, w: ww, h: wh };
|
||||
function poddBody(parent, b, rects, pd, video, geo) {
|
||||
const k = b.w / geo.w;
|
||||
// The screen panel is the window only (rects.win: where face.js draws it), centred on it like the
|
||||
// video panel: SteamVR orders see-through panels by their centres, so two panels on one axis keep
|
||||
// their order from any angle (offset centres swapped it head on, and the screen hid the video).
|
||||
const win = rects.win;
|
||||
if (video && Array.isArray(pd.video)) {
|
||||
// the video's picture (podd's video tile, the frame twice: one per eye) just under the screen,
|
||||
// which is clear over it but for the overlays; first, so the screen blends over it
|
||||
const [vx, vy, vw, vh] = pd.video;
|
||||
const pic = vxf([0, LCD_M.cy * k, b.d / 2 - (SCREEN_RECESS + VIDEO_BELOW) * k]);
|
||||
const pic = vxf([0, geo.lcdCy * k, b.d / 2 - (SCREEN_RECESS + VIDEO_BELOW) * k]);
|
||||
pic.appendChild(vnode('panel', {
|
||||
key: pd.key,
|
||||
uv_min: [r6(vx / pd.texW), r6(vy / pd.texH)],
|
||||
uv_max: [r6((vx + 2 * vw) / pd.texW), r6((vy + vh) / pd.texH)],
|
||||
'meters-per-pixel': r6(LCD_M.w * k / vw),
|
||||
'meters-per-pixel': r6(geo.lcdW * k / vw),
|
||||
origin: [0, 0],
|
||||
interactive: false,
|
||||
visibility: 0,
|
||||
@@ -7237,8 +7499,8 @@ function createSceneGraph() {
|
||||
parent.appendChild(pic);
|
||||
}
|
||||
// recessed in the plate's opening, under podd's glass (which sits on the plate's surface)
|
||||
const front = vxf([0, LCD_M.cy * k, b.d / 2 - SCREEN_RECESS * k]);
|
||||
front.appendChild(pagePanel(win, b.w / rects.face.w, 'lgs-asspod-screen'));
|
||||
const front = vxf([0, geo.lcdCy * k, b.d / 2 - SCREEN_RECESS * k]);
|
||||
front.appendChild(pagePanel(win, geo.winW * k / win.w, 'lgs-asspod-screen'));
|
||||
parent.appendChild(front);
|
||||
const [hx, hy, hz] = pd.box;
|
||||
const faces = {
|
||||
@@ -7275,8 +7537,23 @@ function createSceneGraph() {
|
||||
|
||||
// The body without podd: the model's parts as SteamVR render models (fixed lighting), the screen
|
||||
// panel just in front of the plate.
|
||||
function body(parent, b, rects, modelDir, finish) {
|
||||
const k = b.w / MODEL_W;
|
||||
function body(parent, b, rects, modelDir, finish, model, geo) {
|
||||
const k = b.w / geo.w;
|
||||
if (model === 'nano') {
|
||||
// the nano's parts: its colour's aluminum, the shared caps, ports and well; the screen panel
|
||||
// over its window
|
||||
const scaled = vxf([0, 0, 0]);
|
||||
scaled.setAttribute('scale', [k, k, k].map(r6).join(' '));
|
||||
const wheel = finish === 'black' ? 'black' : 'white';
|
||||
const parts = [`nano_shell_${finish}`, `nano_back_${finish}`, `nano_center_${finish}`, `nano_wheel_${wheel}`,
|
||||
'nano_cap_top', 'nano_cap_bottom', 'nano_ports', 'nano_well'];
|
||||
for (const name of parts) scaled.appendChild(vnode('rendermodel', { source: `${modelDir}/${name}/${name}.obj` }));
|
||||
parent.appendChild(scaled);
|
||||
const front = vxf([0, geo.lcdCy * k, b.d / 2 + OVERLAY_DZ * k]);
|
||||
front.appendChild(pagePanel(rects.win, geo.winW * k / rects.win.w, 'lgs-asspod-screen'));
|
||||
parent.appendChild(front);
|
||||
return;
|
||||
}
|
||||
const fin = finish === 'white' ? 'white' : 'black';
|
||||
const scaled = vxf([0, 0, 0]);
|
||||
scaled.setAttribute('scale', [k, k, k].map(r6).join(' '));
|
||||
@@ -7294,15 +7571,24 @@ function createSceneGraph() {
|
||||
|
||||
// The default place in the hand: where you put it with Reposition (2026-10-09, right hand), and
|
||||
// its mirror image across the controller's centre line for the left hand.
|
||||
function defaultHold(side) {
|
||||
// Another body keeps its wheel where the Video's is (over the stick), front faces level.
|
||||
function defaultHold(side, b, geo) {
|
||||
const r = DEFAULT_HOLD;
|
||||
if (side === 'right') return { rot: r.rot.slice(), centre: r.centre.slice() };
|
||||
return { rot: [r.rot[0], r.rot[1], -r.rot[2], -r.rot[3]], centre: [-r.centre[0], r.centre[1], r.centre[2]] };
|
||||
const h = side === 'right'
|
||||
? { rot: r.rot.slice(), centre: r.centre.slice() }
|
||||
: { rot: [r.rot[0], r.rot[1], -r.rot[2], -r.rot[3]], centre: [-r.centre[0], r.centre[1], r.centre[2]] };
|
||||
const k = b.w / geo.w;
|
||||
const wheelV = [0, VIDEO_GEO.wheelCy * VIDEO_SCALE, VIDEO_GEO.d * VIDEO_SCALE / 2];
|
||||
const wheelM = [0, geo.wheelCy * k, b.d / 2];
|
||||
const d = qrot(h.rot, wheelV.map((v, i) => v - wheelM[i]));
|
||||
h.centre = h.centre.map((v, i) => v + d[i]);
|
||||
return h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the assPod in `hand` ('left' | 'right') with the sheet over the menu.
|
||||
* @param {{ hand: string, body: {w,h,d,r}, rects: {face, back, dim, text}, modelDir: string, finish: string,
|
||||
* @param {{ hand: string, body: {w,h,d,r}, rects: {face, back, win, dim, text}, modelDir: string, finish: string,
|
||||
* model?: 'video'|'nano', geo?: {w, d, lcdCy, lcdW, winW, winH, wheelCy} (model metres),
|
||||
* podd?: { key, texW, texH, box, tiles }, hold?: { rot, centre }, world?: { rot, centre } }} o
|
||||
* podd: its layout (podd-out.json) when podd draws the body; hold: a place in the hand other than
|
||||
* the default; world: pinned in the room (standing space) instead of on the hand
|
||||
@@ -7313,14 +7599,15 @@ function createSceneGraph() {
|
||||
if (!attach()) return null;
|
||||
clearNodes();
|
||||
const side = o.hand === 'left' ? 'left' : 'right';
|
||||
const { rot, centre } = o.hold && o.hold.rot && o.hold.centre ? o.hold : defaultHold(side);
|
||||
const geo = o.geo || VIDEO_GEO;
|
||||
const { rot, centre } = o.hold && o.hold.rot && o.hold.centre ? o.hold : defaultHold(side, o.body, geo);
|
||||
// pinned in the room (Reposition: the controller moves, the assPod stays), else on the hand
|
||||
const hand = o.world
|
||||
? vxf(o.world.centre, o.world.rot)
|
||||
: vxf([0, 0, 0], null, { 'parent-path': '/user/hand/' + side });
|
||||
const hold = o.world ? vxf([0, 0, 0]) : vxf(centre, rot);
|
||||
if (o.podd && o.podd.tiles && Array.isArray(o.podd.box)) poddBody(hold, o.body, o.rects, o.podd, o.video);
|
||||
else body(hold, o.body, o.rects, o.modelDir, o.finish);
|
||||
if (o.podd && o.podd.tiles && Array.isArray(o.podd.box)) poddBody(hold, o.body, o.rects, o.podd, o.video, geo);
|
||||
else body(hold, o.body, o.rects, o.modelDir, o.finish, o.model || 'video', geo);
|
||||
hand.appendChild(hold);
|
||||
st.root.appendChild(hand);
|
||||
// the sheet and the caption, on Steam's window
|
||||
|
||||
@@ -91,6 +91,11 @@ install_app() {
|
||||
cp "$tmp/new/native/glassd/prebuilt/glassd" "$tmp/new/native/glassd/glassd"
|
||||
chmod 755 "$tmp/new/native/glassd/glassd"
|
||||
fi
|
||||
# The assPod's 3D body: the release's shipped podd (built for the Frame from the same source)
|
||||
if [ -f "$tmp/new/native/podd/prebuilt/podd" ]; then
|
||||
cp "$tmp/new/native/podd/prebuilt/podd" "$tmp/new/native/podd/podd"
|
||||
chmod 755 "$tmp/new/native/podd/podd"
|
||||
fi
|
||||
rm -rf "$DEST.old"
|
||||
[ -d "$DEST" ] && mv "$DEST" "$DEST.old"
|
||||
mv "$tmp/new" "$DEST"
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// podd: the assPod's body with real shaders, for SteamVR's scene graph.
|
||||
//
|
||||
// SteamVR draws render models with fixed lighting only, so this renders the original model
|
||||
// (device/asspod/model, exported from tools/asspod-model) itself on the GPU: clearcoat plastic on
|
||||
// the front, mirror-polished steel on the back and edges, the engraved peach as satin, glass over the
|
||||
// screen, all lit by a studio environment and one key light.
|
||||
// SteamVR draws render models with fixed lighting only, so this renders the models (device/asspod/
|
||||
// model) itself on the GPU, lit by a studio environment and one key light. Two bodies, picked by the
|
||||
// spec's "model":
|
||||
// video the Video (tools/asspod-model): clearcoat plastic on the front, mirror-polished steel on
|
||||
// the back and edges, the engraved peach as satin, glass over the screen
|
||||
// nano the nano, 2nd generation (tools/nano2-model): anodized aluminum (a tinted, bead-blasted
|
||||
// metal under a thin oxide) with the laser-etched back paler and smoother, white plastic
|
||||
// end caps, the click wheel and an aluminum center button
|
||||
//
|
||||
// How it reaches the headset: the body sits in a box of six panels in SteamVR's scene graph
|
||||
// (device/asspod/src/sg.js), rigid with the hand. For each face and each eye, podd renders the model
|
||||
@@ -13,11 +17,12 @@
|
||||
// at the compositor's rate. The screen is not drawn here: the window shows the live screen panel
|
||||
// inside the box (glass reflections over it, transparent otherwise).
|
||||
//
|
||||
// Input: --spec /dev/shm/lgs/podd.json {open, hand, rot [w,x,y,z], centre [x,y,z], scale, finish}
|
||||
// (the body in the controller's frame, written by device/shell_ext/asspod.py)
|
||||
// Input: --spec /dev/shm/lgs/podd.json {open, hand, rot [w,x,y,z], centre [x,y,z], scale, nanoScale, model, finish}
|
||||
// (the body in the controller's frame, written by device/shell_ext/asspod.py; scale is the
|
||||
// Video's, nanoScale the nano's; finish is the current model's colour)
|
||||
// Output: --out /dev/shm/lgs/podd-out.json {pid, ok, key, texW, texH, box [hx,hy,hz], tiles {face: [x,y,w,h]}}
|
||||
// tiles are the left eye's rect in texture pixels (v = 0 at the top), with `pad` px of
|
||||
// overscan around each eye; the right eye's starts tw + 2 pad to its right. A face's panel
|
||||
// tiles are the left eye's rect in texture pixels (v = 0 at the top), with `pad` px around
|
||||
// each eye (PAD px of overscan, then BAND px clear); the right eye's starts tw + 2 pad to its right. A face's panel
|
||||
// shows [tx - pad, tx + 2 tw + 3 pad] x [ty - pad, ty + th + pad]. The overlay's mouse scale
|
||||
// is half the texture's width (one eye).
|
||||
//
|
||||
@@ -30,6 +35,7 @@
|
||||
#include <time.h>
|
||||
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
@@ -42,16 +48,38 @@
|
||||
#include "json.h"
|
||||
#include "vmath.h"
|
||||
|
||||
// ------------------------------------------------------------------ model constants (tools/asspod-model)
|
||||
static const float BODY_W = 0.0618f, BODY_H = 0.1035f, BODY_D = 0.0110f;
|
||||
static const float FRONT_Z = BODY_D / 2;
|
||||
static const float LCD_CY = 0.0242f, LCD_W = 0.0508f, LCD_H = 0.0381f;
|
||||
static const float WIN_W = 0.0532f, WIN_H = 0.0405f;
|
||||
static const float WHEEL_CY = -0.0240f, WHEEL_R = 0.0197f, WHEEL_RC = 0.0073f, WHEEL_RM = 0.0135f;
|
||||
// ------------------------------------------------------------------ the models (model metres)
|
||||
struct Geo {
|
||||
float w, h, d; // body
|
||||
float lcdCy, lcdW, lcdH; // the LCD (transparent: the screen panel shows through)
|
||||
float winW, winH, winR; // its window (winR: rounded corners drawn here; 0 = the plate's own hole)
|
||||
float wheelCy, wheelRm; // the wheel's centre and the touch marker's radius
|
||||
float markerSigma; // the marker's glow
|
||||
float rockDeg, sink; // press motion
|
||||
};
|
||||
// tools/asspod-model GEOMETRY
|
||||
static const Geo VIDEO = {0.0618f, 0.1035f, 0.0110f, 0.0242f, 0.0508f, 0.0381f, 0.0532f, 0.0405f, 0.f,
|
||||
-0.0240f, 0.0135f, 0.0028f, 2.5f, 0.0005f};
|
||||
// tools/nano2-model GEOMETRY
|
||||
static const Geo NANO = {0.040f, 0.090f, 0.0065f, 0.02888f, 0.0296f, 0.0222f, 0.03162f, 0.02404f, 0.0006f,
|
||||
-0.0133f, 0.0108f, 0.0021f, 2.2f, 0.0004f};
|
||||
// The nano's anodized colours (tools/nano2-model FINISHES metal, sRGB) and roughness.
|
||||
struct Finish { const char *name; float r, g, b, rough; };
|
||||
static const Finish NANO_FINISHES[] = {
|
||||
{"silver", 0xd3 / 255.f, 0xd6 / 255.f, 0xda / 255.f, 0.42f}, {"green", 0x7f / 255.f, 0xd1 / 255.f, 0x2a / 255.f, 0.40f},
|
||||
{"blue", 0x1e / 255.f, 0xa9 / 255.f, 0xe3 / 255.f, 0.40f}, {"pink", 0xec / 255.f, 0x2c / 255.f, 0x95 / 255.f, 0.40f},
|
||||
{"black", 0x16 / 255.f, 0x17 / 255.f, 0x1a / 255.f, 0.46f}, {"red", 0xe0 / 255.f, 0x12 / 255.f, 0x1b / 255.f, 0.40f},
|
||||
};
|
||||
static const float MARGIN = 0.004f; // m around the body: the box of panels
|
||||
static const float PX_PER_M = 4200.f; // panel pixels per metre (about 1 px per 0.24 mm)
|
||||
static const int PAD = 3; // px rendered past each face's edge: neighbouring faces overlap,
|
||||
// so no panel edge (where texels blend with the gap) shows
|
||||
static const int BAND = 4; // px left clear around each eye's image (outside its PAD): SteamVR
|
||||
// splits a panel's rect between the eyes at its middle, and
|
||||
// filtering at that seam took a sliver of the other eye's image,
|
||||
// whose body colour at its edge showed as a faint line along the
|
||||
// box's edge, about a cm off the body
|
||||
static const int SPAN = PAD + BAND; // each eye's image to its block's edge
|
||||
|
||||
static const float kPi = 3.14159265f;
|
||||
static volatile sig_atomic_t g_stop = 0;
|
||||
@@ -188,10 +216,15 @@ in vec2 vUv;
|
||||
in vec3 vModel;
|
||||
uniform vec3 uEye; // body space
|
||||
uniform mat3 uToWorld; // body -> world rotation (the environment is fixed in the room)
|
||||
uniform int uMat; // 0 plate, 1 steel, 2 steel back (engraving), 3 wheel, 4 well, 5 ports, 6 window glass
|
||||
uniform int uMat; // 0 plate, 1 steel, 2 steel back (engraving), 3 wheel, 4 well, 5 ports, 6 window glass,
|
||||
// 7 anodized aluminum, 8 anodized back (engraving), 9 white plastic (texture), 10 palette (texture)
|
||||
uniform vec3 uBase; // base colour (linear)
|
||||
uniform sampler2D uTex; // wheel labels / engraving mask
|
||||
uniform float uRough; // anodized: the blasted metal's roughness
|
||||
uniform sampler2D uTex; // wheel labels / engraving mask / cap print / palette
|
||||
uniform vec4 uMarker; // x angle (rad, 0 = top, clockwise), y opacity
|
||||
uniform vec3 uWheel; // the wheel's centre y, the marker's radius, its glow (model metres)
|
||||
uniform vec3 uLcd; // the LCD's centre y, half width, half height
|
||||
uniform vec3 uWin; // the window's half width, half height, corner radius (0: square)
|
||||
out vec4 frag;
|
||||
|
||||
const float PI = 3.14159265;
|
||||
@@ -220,6 +253,15 @@ vec3 irradiance(vec3 n) {
|
||||
return c + vec3(1.0, 0.98, 0.95) * max(dot(n, KEY), 0.0) * 0.32 + vec3(0.14) * max(dot(n, FILL), 0.0);
|
||||
}
|
||||
float fresnel(float f0, float cosT) { return f0 + (1.0 - f0) * pow(1.0 - cosT, 5.0); }
|
||||
// value noise for the blasted aluminum's grain
|
||||
float hash3(vec3 p) { return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453); }
|
||||
float vnoise(vec3 p) {
|
||||
vec3 i = floor(p), f = fract(p);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
float a = mix(mix(hash3(i), hash3(i + vec3(1, 0, 0)), f.x), mix(hash3(i + vec3(0, 1, 0)), hash3(i + vec3(1, 1, 0)), f.x), f.y);
|
||||
float b = mix(mix(hash3(i + vec3(0, 0, 1)), hash3(i + vec3(1, 0, 1)), f.x), mix(hash3(i + vec3(0, 1, 1)), hash3(i + vec3(1, 1, 1)), f.x), f.y);
|
||||
return mix(a, b, f.z);
|
||||
}
|
||||
vec3 fresnel3(vec3 f0, float cosT) { return f0 + (1.0 - f0) * pow(1.0 - cosT, 5.0); }
|
||||
|
||||
void main() {
|
||||
@@ -244,10 +286,10 @@ void main() {
|
||||
col = base * irradiance(nw) + fresnel(0.04, cosT) * envColor(r, 0.45);
|
||||
// the touch marker: a soft glow where the thumb is
|
||||
if (uMarker.y > 0.001) {
|
||||
vec2 p = vModel.xy - vec2(0.0, -0.0240);
|
||||
vec2 m = vec2(sin(uMarker.x), cos(uMarker.x)) * 0.0135;
|
||||
vec2 p = vModel.xy - vec2(0.0, uWheel.x);
|
||||
vec2 m = vec2(sin(uMarker.x), cos(uMarker.x)) * uWheel.y;
|
||||
float d = length(p - m);
|
||||
col += vec3(0.55, 0.72, 1.0) * uMarker.y * exp(-d * d / (2.0 * 0.0028 * 0.0028)) * 0.9;
|
||||
col += vec3(0.55, 0.72, 1.0) * uMarker.y * exp(-d * d / (2.0 * uWheel.z * uWheel.z)) * 0.9;
|
||||
}
|
||||
} else if (uMat == 4) {
|
||||
col = uBase * irradiance(nw);
|
||||
@@ -255,8 +297,12 @@ void main() {
|
||||
col = uBase * irradiance(nw) + fresnel(0.3, cosT) * envColor(r, 0.35) * 0.5;
|
||||
} else if (uMat == 6) {
|
||||
// the window: black glossy border; the LCD itself transparent, with the glass's reflection
|
||||
vec2 p = vModel.xy - vec2(0.0, 0.0242);
|
||||
bool lcd = abs(p.x) < 0.0254 && abs(p.y) < 0.01905;
|
||||
vec2 p = vModel.xy - vec2(0.0, uLcd.x);
|
||||
if (uWin.z > 0.0) { // rounded corners (the nano's window lies on the aluminum)
|
||||
vec2 q = abs(p) - (uWin.xy - uWin.z);
|
||||
if (length(max(q, 0.0)) > uWin.z) discard;
|
||||
}
|
||||
bool lcd = abs(p.x) < uLcd.y && abs(p.y) < uLcd.z;
|
||||
vec3 refl = fresnel(0.04, cosT) * envColor(r, 0.02);
|
||||
if (lcd) {
|
||||
float a = clamp(max(refl.r, max(refl.g, refl.b)), 0.0, 1.0);
|
||||
@@ -265,6 +311,30 @@ void main() {
|
||||
return;
|
||||
}
|
||||
col = vec3(0.004) + refl;
|
||||
} else if (uMat == 7 || uMat == 8) {
|
||||
// anodized aluminum: a dyed metal, bead-blasted (fine grain), under a thin clear oxide. The
|
||||
// laser engraving on the back takes the dye and the blasting with it: paler and smoother.
|
||||
float etch = uMat == 8 ? texture(uTex, vec2(vUv.x, 1.0 - vUv.y)).r : 0.0;
|
||||
vec3 gp = vModel * 5200.0; // grain cells about 0.2 mm
|
||||
float grain = vnoise(gp) * 0.65 + vnoise(gp * 2.3 + 7.1) * 0.35 - 0.5;
|
||||
vec3 jitter = vec3(vnoise(gp + 3.1), vnoise(gp + 5.7), vnoise(gp + 9.2)) - 0.5;
|
||||
vec3 ng = normalize(n + jitter * 0.07 * (1.0 - etch));
|
||||
vec3 rg = normalize(uToWorld * reflect(-v, ng));
|
||||
float cosG = clamp(dot(ng, v), 0.0, 1.0);
|
||||
float rough = mix(uRough + grain * 0.08, 0.16, etch);
|
||||
vec3 tint = mix(uBase, mix(uBase, vec3(0.85), 0.55), etch);
|
||||
vec3 spec = fresnel3(tint, cosG) * envColor(rg, rough);
|
||||
vec3 diff = tint * irradiance(nw) * 0.85; // the blasted surface scatters: its colour shows from any side
|
||||
vec3 coat = fresnel(0.04, cosT) * envColor(r, 0.1) * 0.5;
|
||||
col = (spec + diff) * (1.0 + grain * 0.1) + coat;
|
||||
} else if (uMat == 9) {
|
||||
// the nano's end caps: white satin plastic (the print from the texture)
|
||||
vec3 base = pow(texture(uTex, vec2(vUv.x, 1.0 - vUv.y)).rgb, vec3(2.2));
|
||||
col = base * irradiance(nw) * (1.0 - fresnel(0.04, cosT)) + fresnel(0.04, cosT) * envColor(r, 0.3) * 0.6;
|
||||
} else if (uMat == 10) {
|
||||
// the nano's ports: each part's colour from the palette texture
|
||||
vec3 base = pow(texture(uTex, vec2(vUv.x, 1.0 - vUv.y)).rgb, vec3(2.2));
|
||||
col = base * irradiance(nw) + fresnel(0.3, cosT) * envColor(r, 0.35) * 0.5 * max(base.r, 0.1);
|
||||
} else {
|
||||
// the front plate and centre button: plastic under a clear coat
|
||||
vec3 diffuse = uBase * irradiance(nw);
|
||||
@@ -283,8 +353,8 @@ struct Face {
|
||||
const char *name;
|
||||
v3 c, R, U, N; // body space: centre, right, up, outward normal
|
||||
float w, h; // metres
|
||||
int tx, ty, tw, th; // the left eye's tile (texture px, top-left origin) inside PAD px of overscan;
|
||||
// the right eye's at tx + tw + 2 PAD
|
||||
int tx, ty, tw, th; // the left eye's tile (texture px, top-left origin) inside PAD px of overscan
|
||||
// and BAND px clear; the right eye's at tx + tw + 2 SPAN
|
||||
};
|
||||
|
||||
struct Layout {
|
||||
@@ -299,11 +369,12 @@ struct Layout {
|
||||
static const int VIDEO_W = 320, VIDEO_H = 240;
|
||||
static const char *VIDEO_FILE = "/dev/shm/lgs/podd-video";
|
||||
|
||||
static Layout makeLayout(float scale) {
|
||||
// The box holds either body at its scale, so switching models keeps the layout (and the page's panels).
|
||||
static Layout makeLayout(float scale, float nanoScale) {
|
||||
Layout L;
|
||||
L.hx = BODY_W * scale / 2 + MARGIN;
|
||||
L.hy = BODY_H * scale / 2 + MARGIN;
|
||||
L.hz = BODY_D * scale / 2 + MARGIN;
|
||||
L.hx = std::max(VIDEO.w * scale, NANO.w * nanoScale) / 2 + MARGIN;
|
||||
L.hy = std::max(VIDEO.h * scale, NANO.h * nanoScale) / 2 + MARGIN;
|
||||
L.hz = std::max(VIDEO.d * scale, NANO.d * nanoScale) / 2 + MARGIN;
|
||||
const float hx = L.hx, hy = L.hy, hz = L.hz;
|
||||
L.f = {{
|
||||
{"front", {0, 0, hz}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}, 2 * hx, 2 * hy},
|
||||
@@ -314,17 +385,20 @@ static Layout makeLayout(float scale) {
|
||||
{"bottom", {0, -hy, 0}, {1, 0, 0}, {0, 0, 1}, {0, -1, 0}, 2 * hx, 2 * hz},
|
||||
}};
|
||||
// two rows: front, back, right, left (all as tall as the body); then top and bottom. A face's block
|
||||
// is both eyes with PAD px around each: [PAD | left eye | 2 PAD | right eye | PAD]
|
||||
const int gap = 4;
|
||||
// is both eyes, each in PAD px of overscan and BAND px clear: [SPAN | left eye | 2 SPAN | right eye | SPAN]. The blocks (and
|
||||
// the opaque video tile) stand GAP px apart: a panel seen at a slant samples well past its own edge
|
||||
// (coarse texture levels), and 4 px let it pick up a neighbour's body or the black video tile, a
|
||||
// thin flickering line along the box's edges, a few mm off the body.
|
||||
const int gap = 64;
|
||||
int x = 0, row0 = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Face &F = L.f[size_t(i)];
|
||||
F.tw = int(std::lround(F.w * PX_PER_M));
|
||||
F.th = int(std::lround(F.h * PX_PER_M));
|
||||
F.tx = x + PAD;
|
||||
F.ty = PAD;
|
||||
x += 2 * F.tw + 4 * PAD + gap;
|
||||
row0 = std::max(row0, F.th + 2 * PAD);
|
||||
F.tx = x + SPAN;
|
||||
F.ty = SPAN;
|
||||
x += 2 * F.tw + 4 * SPAN + gap;
|
||||
row0 = std::max(row0, F.th + 2 * SPAN);
|
||||
}
|
||||
int w0 = x;
|
||||
x = 0;
|
||||
@@ -333,10 +407,10 @@ static Layout makeLayout(float scale) {
|
||||
Face &F = L.f[size_t(i)];
|
||||
F.tw = int(std::lround(F.w * PX_PER_M));
|
||||
F.th = int(std::lround(F.h * PX_PER_M));
|
||||
F.tx = x + PAD;
|
||||
F.ty = row0 + gap + PAD;
|
||||
x += 2 * F.tw + 4 * PAD + gap;
|
||||
row1 = std::max(row1, F.th + 2 * PAD);
|
||||
F.tx = x + SPAN;
|
||||
F.ty = row0 + gap + SPAN;
|
||||
x += 2 * F.tw + 4 * SPAN + gap;
|
||||
row1 = std::max(row1, F.th + 2 * SPAN);
|
||||
}
|
||||
// the video tile under the second row
|
||||
L.vx = x;
|
||||
@@ -426,6 +500,8 @@ struct Spec {
|
||||
quat rot;
|
||||
v3 centre;
|
||||
float scale = 1.375f;
|
||||
float nanoScale = 1.3125f;
|
||||
std::string model = "video";
|
||||
std::string finish = "black";
|
||||
bool world = false; // pinned in the room (Reposition): wrot / wcentre in standing space
|
||||
quat wrot;
|
||||
@@ -441,6 +517,8 @@ static bool readSpec(const std::string &path, Spec &s) {
|
||||
s.open = j.boolean("open", false);
|
||||
s.hand = j.str("hand", "right");
|
||||
s.scale = float(j.num("scale", 1.375));
|
||||
s.nanoScale = float(j.num("nanoScale", 1.3125));
|
||||
s.model = j.str("model", "video");
|
||||
s.finish = j.str("finish", "black");
|
||||
const JVal *r = j.get("rot"), *c = j.get("centre");
|
||||
if (r && r->type == JVal::Arr && r->a.size() == 4) s.rot = qnorm({float(r->a[0].n), float(r->a[1].n), float(r->a[2].n), float(r->a[3].n)});
|
||||
@@ -495,53 +573,74 @@ int main(int argc, char **argv) {
|
||||
Program prog;
|
||||
if (!linkProgram(prog, VS, FS, "pod")) return 1;
|
||||
|
||||
// the parts (both finishes' plates, wheels and buttons are loaded; the spec picks)
|
||||
struct Part { std::string name; int mat; v3 base; Mesh mesh; GLuint tex = 0; bool wheel = false, center = false; };
|
||||
// The parts of both models. A part with a variant is drawn only in that finish (the Video's
|
||||
// plates; the nano's wheels: black on the black one, white on the others); a tinted part takes the
|
||||
// nano's finish colour (one mesh for every colour). tex: the RGBA podd samples (a path in the
|
||||
// model dir), if any.
|
||||
struct Part {
|
||||
std::string model, name, variant, tex;
|
||||
int mat;
|
||||
v3 base;
|
||||
bool tint = false;
|
||||
Mesh mesh;
|
||||
GLuint texId = 0;
|
||||
bool wheel = false, center = false;
|
||||
};
|
||||
const v3 W1 = {1, 1, 1};
|
||||
std::vector<Part> parts = {
|
||||
{"asspod_steel_back", 2, {0.9f, 0.9f, 0.9f}},
|
||||
{"asspod_steel_edge", 1, {0.9f, 0.9f, 0.9f}},
|
||||
{"asspod_ports", 5, {0.03f, 0.03f, 0.035f}},
|
||||
{"asspod_plate_black", 0, {0.004f, 0.004f, 0.0045f}},
|
||||
{"asspod_plate_white", 0, {0.86f, 0.86f, 0.84f}},
|
||||
{"asspod_well_black", 4, {0.002f, 0.002f, 0.002f}},
|
||||
{"asspod_well_white", 4, {0.25f, 0.25f, 0.24f}},
|
||||
{"asspod_wheel_black", 3, {1, 1, 1}},
|
||||
{"asspod_wheel_white", 3, {1, 1, 1}},
|
||||
{"asspod_center_black", 0, {0.004f, 0.004f, 0.0045f}},
|
||||
{"asspod_center_white", 0, {0.86f, 0.86f, 0.84f}},
|
||||
{"video", "asspod_steel_back", "", "asspod_steel_back/asspod_steel_back_mask.rgba", 2, {0.9f, 0.9f, 0.9f}},
|
||||
{"video", "asspod_steel_edge", "", "", 1, {0.9f, 0.9f, 0.9f}},
|
||||
{"video", "asspod_ports", "", "", 5, {0.03f, 0.03f, 0.035f}},
|
||||
{"video", "asspod_plate_black", "black", "", 0, {0.004f, 0.004f, 0.0045f}},
|
||||
{"video", "asspod_plate_white", "white", "", 0, {0.86f, 0.86f, 0.84f}},
|
||||
{"video", "asspod_well_black", "black", "", 4, {0.002f, 0.002f, 0.002f}},
|
||||
{"video", "asspod_well_white", "white", "", 4, {0.25f, 0.25f, 0.24f}},
|
||||
{"video", "asspod_wheel_black", "black", "asspod_wheel_black/asspod_wheel_black_color.rgba", 3, W1},
|
||||
{"video", "asspod_wheel_white", "white", "asspod_wheel_white/asspod_wheel_white_color.rgba", 3, W1},
|
||||
{"video", "asspod_center_black", "black", "", 0, {0.004f, 0.004f, 0.0045f}},
|
||||
{"video", "asspod_center_white", "white", "", 0, {0.86f, 0.86f, 0.84f}},
|
||||
{"nano", "nano_shell_silver", "", "", 7, W1, true},
|
||||
{"nano", "nano_back_silver", "", "nano_back_mask/nano_back_mask.rgba", 8, W1, true},
|
||||
{"nano", "nano_center_silver", "", "", 7, W1, true},
|
||||
{"nano", "nano_wheel_white", "white", "nano_wheel_white/nano_wheel_white_color.rgba", 3, W1},
|
||||
{"nano", "nano_wheel_black", "black", "nano_wheel_black/nano_wheel_black_color.rgba", 3, W1},
|
||||
{"nano", "nano_cap_top", "", "nano_cap_top/nano_cap_top_color.rgba", 9, W1},
|
||||
{"nano", "nano_cap_bottom", "", "nano_cap_bottom/nano_cap_bottom_color.rgba", 9, W1},
|
||||
{"nano", "nano_ports", "", "nano_ports/nano_ports_color.rgba", 10, W1},
|
||||
{"nano", "nano_well", "", "", 4, {0.002f, 0.002f, 0.002f}},
|
||||
};
|
||||
for (auto &p : parts) {
|
||||
std::string dir = modelDir + "/" + p.name + "/";
|
||||
if (!loadObj(dir + p.name + ".obj", p.mesh)) { std::printf("cannot load %s\n", p.name.c_str()); return 1; }
|
||||
if (p.mat == 3) p.tex = loadRgba(dir + p.name + "_color.rgba");
|
||||
if (p.mat == 2) p.tex = loadRgba(dir + p.name + "_mask.rgba");
|
||||
if (!p.tex.empty()) p.texId = loadRgba(modelDir + "/" + p.tex);
|
||||
p.wheel = p.name.find("wheel") != std::string::npos;
|
||||
p.center = p.name.find("center") != std::string::npos;
|
||||
}
|
||||
// the window: a quad over the plate's opening (border + transparent LCD), in model metres
|
||||
Mesh window;
|
||||
{
|
||||
const float z = FRONT_Z + 0.00008f, hw = WIN_W / 2, hh = WIN_H / 2, cy = LCD_CY;
|
||||
// the windows: a quad over each model's screen (border + transparent LCD), in model metres
|
||||
auto windowQuad = [](const Geo &G) {
|
||||
Mesh m;
|
||||
const float z = G.d / 2 + 0.00008f, hw = G.winW / 2, hh = G.winH / 2, cy = G.lcdCy;
|
||||
const float q[] = {-hw, cy - hh, z, 0, 0, 1, 0, 0, hw, cy - hh, z, 0, 0, 1, 1, 0, hw, cy + hh, z, 0, 0, 1, 1, 1,
|
||||
-hw, cy - hh, z, 0, 0, 1, 0, 0, hw, cy + hh, z, 0, 0, 1, 1, 1, -hw, cy + hh, z, 0, 0, 1, 0, 1};
|
||||
glGenVertexArrays(1, &window.vao);
|
||||
glGenBuffers(1, &window.vbo);
|
||||
glBindVertexArray(window.vao);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, window.vbo);
|
||||
glGenVertexArrays(1, &m.vao);
|
||||
glGenBuffers(1, &m.vbo);
|
||||
glBindVertexArray(m.vao);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m.vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof q, q, GL_STATIC_DRAW);
|
||||
for (int i = 0; i < 3; i++) glEnableVertexAttribArray(GLuint(i));
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 32, reinterpret_cast<void *>(0));
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 32, reinterpret_cast<void *>(12));
|
||||
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 32, reinterpret_cast<void *>(24));
|
||||
glBindVertexArray(0);
|
||||
window.count = 6;
|
||||
}
|
||||
(void)LCD_W; (void)LCD_H; (void)WHEEL_R; (void)WHEEL_RC; (void)WHEEL_RM;
|
||||
m.count = 6;
|
||||
return m;
|
||||
};
|
||||
const Mesh windowVideo = windowQuad(VIDEO), windowNano = windowQuad(NANO);
|
||||
|
||||
Spec spec;
|
||||
struct stat lastSt{};
|
||||
Layout L = makeLayout(spec.scale);
|
||||
float layoutScale = -1;
|
||||
Layout L = makeLayout(spec.scale, spec.nanoScale);
|
||||
float layoutScale = -1, layoutNanoScale = -1;
|
||||
|
||||
vr::VROverlayHandle_t ov = vr::k_ulOverlayHandleInvalid;
|
||||
if (vr::VROverlay()->CreateOverlay(key.c_str(), "assPod", &ov) != vr::VROverlayError_None) {
|
||||
@@ -608,7 +707,7 @@ int main(int argc, char **argv) {
|
||||
|
||||
auto writeOut = [&](bool ok, long frames, float fpsNow, float ms) {
|
||||
std::string o = "{\"pid\": " + std::to_string(getpid()) + ", \"ok\": " + (ok ? "true" : "false") + ", \"key\": \"" + key +
|
||||
"\", \"texW\": " + std::to_string(L.texW) + ", \"texH\": " + std::to_string(L.texH) + ", \"pad\": " + std::to_string(PAD) + ", \"box\": [" +
|
||||
"\", \"texW\": " + std::to_string(L.texW) + ", \"texH\": " + std::to_string(L.texH) + ", \"pad\": " + std::to_string(SPAN) + ", \"box\": [" +
|
||||
std::to_string(L.hx) + ", " + std::to_string(L.hy) + ", " + std::to_string(L.hz) + "], \"tiles\": {";
|
||||
for (size_t i = 0; i < L.f.size(); i++) {
|
||||
const Face &F = L.f[i];
|
||||
@@ -649,6 +748,8 @@ int main(int argc, char **argv) {
|
||||
handIn[0].path = pathL;
|
||||
handIn[1].path = pathR;
|
||||
float marker = 0, markerAngle = 0, sink = 0, rock = 0, rockAngle = 0;
|
||||
std::string debugMode;
|
||||
struct stat debugSt{};
|
||||
|
||||
long frames = 0;
|
||||
uint64_t lastOut = 0, fpsT0 = nowNs();
|
||||
@@ -670,9 +771,10 @@ int main(int argc, char **argv) {
|
||||
lastSt = st;
|
||||
readSpec(specPath, spec);
|
||||
}
|
||||
if (std::fabs(spec.scale - layoutScale) > 1e-5f) {
|
||||
L = makeLayout(spec.scale);
|
||||
if (std::fabs(spec.scale - layoutScale) > 1e-5f || std::fabs(spec.nanoScale - layoutNanoScale) > 1e-5f) {
|
||||
L = makeLayout(spec.scale, spec.nanoScale);
|
||||
layoutScale = spec.scale;
|
||||
layoutNanoScale = spec.nanoScale;
|
||||
if (!allocate()) { std::printf("cannot allocate %dx%d\n", L.texW, L.texH); return 1; }
|
||||
writeOut(true, frames, 0, 0);
|
||||
std::printf("layout %dx%d (scale %.3f)\n", L.texW, L.texH, double(spec.scale));
|
||||
@@ -734,6 +836,21 @@ int main(int argc, char **argv) {
|
||||
rock += (((clicked && !centred) ? 1.f : 0.f) - rock) * std::min(1.f, dt / 0.05f);
|
||||
if (clicked && !centred) rockAngle = std::round(markerAngle / (kPi / 2)) * (kPi / 2);
|
||||
|
||||
// debugging: /tmp/lgs/podd-debug "blank" draws nothing; "dot" only a white dot at the front face's
|
||||
// centre (each eye); "faces:front,left" only those faces
|
||||
{
|
||||
struct stat ds{};
|
||||
if (stat("/tmp/lgs/podd-debug", &ds) == 0) {
|
||||
if (ds.st_mtim.tv_sec != debugSt.st_mtim.tv_sec || ds.st_mtim.tv_nsec != debugSt.st_mtim.tv_nsec) {
|
||||
debugSt = ds;
|
||||
readFile("/tmp/lgs/podd-debug", debugMode);
|
||||
while (!debugMode.empty() && std::isspace(static_cast<unsigned char>(debugMode.back()))) debugMode.pop_back();
|
||||
}
|
||||
} else {
|
||||
debugMode.clear();
|
||||
debugSt = {};
|
||||
}
|
||||
}
|
||||
// render: every face, both eyes
|
||||
DmaTarget &B = bufs[size_t(cur)];
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
|
||||
@@ -745,28 +862,41 @@ int main(int argc, char **argv) {
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glDisable(GL_BLEND);
|
||||
const bool nano = spec.model == "nano";
|
||||
const Geo &G = nano ? NANO : VIDEO;
|
||||
Finish fin = NANO_FINISHES[0];
|
||||
for (const Finish &f : NANO_FINISHES)
|
||||
if (spec.finish == f.name) fin = f;
|
||||
auto lin = [](float c) { return std::pow(c, 2.2f); };
|
||||
prog.use();
|
||||
prog.set("uScale", spec.scale);
|
||||
prog.set("uScale", nano ? spec.nanoScale : spec.scale);
|
||||
prog.set("uRough", fin.rough);
|
||||
prog.set("uWheel", G.wheelCy, G.wheelRm, G.markerSigma);
|
||||
prog.set("uLcd", G.lcdCy, G.lcdW / 2, G.lcdH / 2);
|
||||
prog.set("uWin", G.winW / 2, G.winH / 2, G.winR);
|
||||
prog.set("uTex", 0);
|
||||
float toWorld[9];
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int j = 0; j < 3; j++) toWorld[j * 3 + i] = body.r[i][j]; // column-major
|
||||
glUniformMatrix3fv(prog.loc("uToWorld"), 1, GL_FALSE, toWorld);
|
||||
prog.set("uMarker", markerAngle, marker, 0.f, 0.f);
|
||||
const bool white = spec.finish == "white";
|
||||
// the variant drawn: the Video's finish; the nano's wheel is black only on the black one
|
||||
const std::string variant = nano ? (spec.finish == "black" ? "black" : "white") : (spec.finish == "white" ? "white" : "black");
|
||||
for (int eye = 0; eye < 2; eye++) {
|
||||
const Pose e2h = fromVR(vr::VRSystem()->GetEyeToHeadTransform(eye ? vr::Eye_Right : vr::Eye_Left));
|
||||
const v3 eyeW = (hmd * e2h).t;
|
||||
const v3 eyeB = toBody.apply(eyeW);
|
||||
prog.set("uEye", eyeB.x, eyeB.y, eyeB.z);
|
||||
for (const Face &F : L.f) {
|
||||
if (debugMode == "blank" || debugMode == "dot") continue;
|
||||
if (debugMode.rfind("faces:", 0) == 0 && debugMode.find(F.name) == std::string::npos) continue;
|
||||
float P[16], V[16];
|
||||
// the face grown by PAD px on every side (the panel shows it all)
|
||||
Face G = F;
|
||||
G.w = F.w * float(F.tw + 2 * PAD) / float(F.tw);
|
||||
G.h = F.h * float(F.th + 2 * PAD) / float(F.th);
|
||||
if (!faceMatrices(G, eyeB, P, V)) continue;
|
||||
const int x = F.tx - PAD + eye * (F.tw + 2 * PAD);
|
||||
Face grown = F;
|
||||
grown.w = F.w * float(F.tw + 2 * PAD) / float(F.tw);
|
||||
grown.h = F.h * float(F.th + 2 * PAD) / float(F.th);
|
||||
if (!faceMatrices(grown, eyeB, P, V)) continue;
|
||||
const int x = F.tx - PAD + eye * (F.tw + 2 * SPAN);
|
||||
const int y = F.ty - PAD, vw = F.tw + 2 * PAD, vh = F.th + 2 * PAD;
|
||||
// GL rows count from the buffer's first row (the texture's top)
|
||||
glViewport(x, y, vw, vh);
|
||||
@@ -775,15 +905,15 @@ int main(int argc, char **argv) {
|
||||
prog.mat4("uProj", P);
|
||||
prog.mat4("uView", V);
|
||||
for (auto &p : parts) {
|
||||
const bool isWhite = p.name.find("_white") != std::string::npos, isBlack = p.name.find("_black") != std::string::npos;
|
||||
if ((isWhite && !white) || (isBlack && white)) continue;
|
||||
if (p.model != (nano ? "nano" : "video")) continue;
|
||||
if (!p.variant.empty() && p.variant != variant) continue;
|
||||
Pose m; // the part's own motion, about the wheel's centre (model metres)
|
||||
if (p.center) m.t = {0, 0, -0.0005f * sink};
|
||||
if (p.center) m.t = {0, 0, -G.sink * sink};
|
||||
if (p.wheel && rock > 0.001f) {
|
||||
const float ang = -2.5f * kPi / 180 * rock;
|
||||
const float ang = -G.rockDeg * kPi / 180 * rock;
|
||||
const v3 axis = normalize(v3{std::cos(rockAngle), -std::sin(rockAngle), 0});
|
||||
Pose R = poseFrom(qnorm({std::cos(ang / 2), axis.x * std::sin(ang / 2), axis.y * std::sin(ang / 2), 0}), {});
|
||||
const v3 pivot = {0, WHEEL_CY, FRONT_Z};
|
||||
const v3 pivot = {0, G.wheelCy, G.d / 2};
|
||||
R.t = pivot - R.rotate(pivot);
|
||||
m = R;
|
||||
}
|
||||
@@ -791,9 +921,10 @@ int main(int argc, char **argv) {
|
||||
m.toGL(M);
|
||||
prog.mat4("uModel", M);
|
||||
prog.set("uMat", p.mat);
|
||||
prog.set("uBase", p.base.x, p.base.y, p.base.z);
|
||||
if (p.tint) prog.set("uBase", lin(fin.r), lin(fin.g), lin(fin.b));
|
||||
else prog.set("uBase", p.base.x, p.base.y, p.base.z);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, p.tex);
|
||||
glBindTexture(GL_TEXTURE_2D, p.texId);
|
||||
glBindVertexArray(p.mesh.vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, p.mesh.count);
|
||||
}
|
||||
@@ -803,10 +934,21 @@ int main(int argc, char **argv) {
|
||||
id.toGL(M);
|
||||
prog.mat4("uModel", M);
|
||||
prog.set("uMat", 6);
|
||||
glBindVertexArray(window.vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, window.count);
|
||||
const Mesh &win = nano ? windowNano : windowVideo;
|
||||
glBindVertexArray(win.vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, win.count);
|
||||
}
|
||||
}
|
||||
if (debugMode == "dot") {
|
||||
const Face &F = L.f[0];
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glClearColor(1, 1, 1, 1);
|
||||
for (int eye = 0; eye < 2; eye++) {
|
||||
glScissor(F.tx + eye * (F.tw + 2 * SPAN) + F.tw / 2 - 4, F.ty + F.th / 2 - 4, 8, 8);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
glClearColor(0, 0, 0, 0);
|
||||
}
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glBindVertexArray(0);
|
||||
// resolve the samples into the buffer SteamVR reads
|
||||
@@ -840,6 +982,28 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glFinish(); // SteamVR samples the dmabuf from another process
|
||||
// debugging: the texture as SteamVR gets it, once, when /tmp/lgs/podd-dump.req appears
|
||||
// ("RGBA", u32 width, u32 height, rows as stored: row 0 is the texture's top), with the spec
|
||||
// and the eyes in body space beside it
|
||||
if (access("/tmp/lgs/podd-dump.req", F_OK) == 0) {
|
||||
std::remove("/tmp/lgs/podd-dump.req");
|
||||
std::vector<uint8_t> px(size_t(L.texW) * L.texH * 4);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, B.fbo);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, L.texW, L.texH, GL_RGBA, GL_UNSIGNED_BYTE, px.data());
|
||||
std::string d = "RGBA";
|
||||
uint32_t wh[2] = {uint32_t(L.texW), uint32_t(L.texH)};
|
||||
d.append(reinterpret_cast<const char *>(wh), 8);
|
||||
d.append(reinterpret_cast<const char *>(px.data()), px.size());
|
||||
writeAtomic("/tmp/lgs/podd-dump.rgba", d);
|
||||
std::string eyes;
|
||||
for (int eye = 0; eye < 2; eye++) {
|
||||
const Pose e2h = fromVR(vr::VRSystem()->GetEyeToHeadTransform(eye ? vr::Eye_Right : vr::Eye_Left));
|
||||
const v3 eb = toBody.apply((hmd * e2h).t);
|
||||
eyes += std::string(eye ? ", " : "") + "[" + std::to_string(eb.x) + ", " + std::to_string(eb.y) + ", " + std::to_string(eb.z) + "]";
|
||||
}
|
||||
writeAtomic("/tmp/lgs/podd-dump.json", "{\"eyes\": [" + eyes + "], \"model\": \"" + spec.model + "\"}\n");
|
||||
}
|
||||
vr::SharedTextureHandle_t h = vr::SharedTextureHandle_t(B.vrHandle);
|
||||
vr::Texture_t tex = {&h, vr::TextureType_SharedTextureHandle, vr::ColorSpace_Gamma};
|
||||
auto e = vr::VROverlay()->SetOverlayTexture(ov, &tex);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export the assPod's 3D body into device/asspod/model/ (SteamVR render models).
|
||||
"""Export the assPods' 3D bodies into device/asspod/model/ (SteamVR render models).
|
||||
|
||||
The body is the original procedural model (tools/asspod-model/asspod-model.js, three.js). Its
|
||||
exporter runs in a browser: this serves the repo on http://127.0.0.1:8766/, opens
|
||||
tools/asspod-model/export.html, and writes the files the page sends back (only under
|
||||
device/asspod/model/). Ctrl+C when it says "done".
|
||||
The bodies are procedural three.js models: the Video (tools/asspod-model/asspod-model.js) and the
|
||||
nano (tools/nano2-model/nano2-model.js). Their exporters run in a browser: this serves the repo on
|
||||
http://127.0.0.1:8766/, opens the exporter page, and writes the files the page sends back (only
|
||||
under device/asspod/model/). Ctrl+C when it says "done".
|
||||
|
||||
python tools/build_asspod_model.py [--no-open]
|
||||
python tools/build_asspod_model.py [--no-open] [--page tools/nano2-model/export.html]
|
||||
"""
|
||||
import base64
|
||||
import http.server
|
||||
@@ -52,7 +52,10 @@ class Handler(http.server.SimpleHTTPRequestHandler):
|
||||
|
||||
def main():
|
||||
srv = http.server.ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
|
||||
url = f"http://127.0.0.1:{PORT}/tools/asspod-model/export.html"
|
||||
page = "tools/asspod-model/export.html"
|
||||
if "--page" in sys.argv[1:]:
|
||||
page = sys.argv[sys.argv.index("--page") + 1]
|
||||
url = f"http://127.0.0.1:{PORT}/{page}"
|
||||
print(f"serving {ROOT}; open {url}")
|
||||
if "--no-open" not in sys.argv[1:]:
|
||||
webbrowser.open(url)
|
||||
|
||||
@@ -15,5 +15,5 @@ trap 'rm -rf "$stage"' EXIT
|
||||
python tools/build_asspod.py --check # the assPod page script must match its sources
|
||||
git archive --format=tar HEAD device theme lab native | tar -xf - -C "$stage"
|
||||
printf '%s\n' "$ver" > "$stage/VERSION"
|
||||
tar -czf dist/LiquidAss-Frame.tar.gz -C "$stage" VERSION device theme lab native
|
||||
tar -czf dist/LiquidAss-Frame.tar.gz --owner=0 --group=0 --numeric-owner -C "$stage" VERSION device theme lab native # no local user names in the archive
|
||||
echo "dist/LiquidAss-Frame.tar.gz ($ver, $(git rev-parse --short HEAD))"
|
||||