mirror of
https://github.com/its-ven/FrameDepth.git
synced 2026-10-17 13:00:46 +02:00
268 lines
11 KiB
Python
268 lines
11 KiB
Python
"""
|
|
Floating settings panel for depth3d_vr. Drawn with Pillow into a texture and operated
|
|
with VR controller laser pointers (trigger = click / drag).
|
|
"""
|
|
import time
|
|
|
|
import numpy as np
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
PW, PH = 1280, 800 # panel texture size in pixels
|
|
PANEL_W_M = 1.6 # physical size in the VR world (metres)
|
|
PANEL_H_M = PANEL_W_M * PH / PW
|
|
|
|
COL_BG = (16, 18, 26, 240)
|
|
COL_TEXT = (235, 238, 245, 255)
|
|
COL_DIM = (150, 156, 172, 255)
|
|
COL_ACCENT = (64, 156, 255, 255)
|
|
COL_WIDGET = (48, 52, 68, 255)
|
|
COL_HOVER = (72, 78, 100, 255)
|
|
COL_WARN = (255, 190, 80, 255)
|
|
|
|
TABS = ["3D", "Screen", "Environment"]
|
|
|
|
_fonts = {}
|
|
|
|
|
|
def font(size, bold=False):
|
|
key = (size, bold)
|
|
if key in _fonts:
|
|
return _fonts[key]
|
|
names = (["segoeuib.ttf", "arialbd.ttf", "DejaVuSans-Bold.ttf"] if bold
|
|
else ["segoeui.ttf", "arial.ttf", "DejaVuSans.ttf"])
|
|
f = None
|
|
for n in names:
|
|
try:
|
|
f = ImageFont.truetype(n, size)
|
|
break
|
|
except OSError:
|
|
continue
|
|
if f is None:
|
|
f = ImageFont.load_default(size)
|
|
_fonts[key] = f
|
|
return f
|
|
|
|
|
|
def ray_panel_hit(origin, direction, inv_model):
|
|
"""Intersect a world-space ray with the panel. Returns (t, px, py) or None."""
|
|
o = inv_model @ np.append(origin, 1.0)
|
|
d = inv_model[:3, :3] @ direction
|
|
if abs(d[2]) < 1e-6:
|
|
return None
|
|
t = -o[2] / d[2]
|
|
if t <= 0:
|
|
return None
|
|
x, y = o[0] + t * d[0], o[1] + t * d[1]
|
|
if abs(x) > PANEL_W_M / 2 or abs(y) > PANEL_H_M / 2:
|
|
return None
|
|
return t, (x / PANEL_W_M + 0.5) * PW, (0.5 - y / PANEL_H_M) * PH
|
|
|
|
|
|
class Panel:
|
|
def __init__(self, st, on_action):
|
|
self.st = st
|
|
self.on_action = on_action
|
|
self.tab = 0
|
|
self.info = dict(env_name="", env_layout="", env_video=False, env_status="", ipd=0.063)
|
|
self.widgets = []
|
|
self.hover = None
|
|
self.active = None
|
|
self._down = False
|
|
self.dirty = True
|
|
self._toast = ("", 0.0)
|
|
self._layout()
|
|
|
|
# ------------------------------------------------------------------ state
|
|
def set_info(self, **kw):
|
|
if any(self.info.get(k) != v for k, v in kw.items()):
|
|
self.info.update(kw)
|
|
self.dirty = True
|
|
self._layout()
|
|
|
|
def toast(self, text, seconds=2.5):
|
|
self._toast = (text, time.time() + seconds)
|
|
self.dirty = True
|
|
|
|
def invalidate(self):
|
|
self.dirty = True
|
|
self._layout()
|
|
|
|
# ------------------------------------------------------------------ layout
|
|
def _layout(self):
|
|
W = []
|
|
for i, name in enumerate(TABS):
|
|
W.append(dict(t="tab", idx=i, label=name, rect=(60 + i * 230, 92, 60 + i * 230 + 210, 148)))
|
|
|
|
y = 190
|
|
row = 84
|
|
|
|
def slider(key, label, lo, hi, fmt):
|
|
nonlocal y
|
|
W.append(dict(t="slider", key=key, label=label, lo=lo, hi=hi, fmt=fmt,
|
|
rect=(500, y - 28, 1010, y + 28), track=(520, 990), y=y))
|
|
y += row
|
|
|
|
def toggle(key, label):
|
|
nonlocal y
|
|
W.append(dict(t="toggle", key=key, label=label, rect=(500, y - 28, 640, y + 28), y=y))
|
|
y += row
|
|
|
|
def button(action, label, x0=60, x1=480, big=False):
|
|
W.append(dict(t="button", action=action, label=label, rect=(x0, y - 30, x1, y + 30), y=y))
|
|
return y
|
|
|
|
if self.tab == 0:
|
|
slider("depth_cm", "3D depth", 0.0, 30.0, "{:.1f} cm")
|
|
slider("conv", "Convergence", 0.0, 1.0, "{:.2f}")
|
|
toggle("stereo3d", "3D enabled")
|
|
toggle("swap", "Swap eyes")
|
|
toggle("debug", "Show depth map")
|
|
elif self.tab == 1:
|
|
slider("fov", "Screen size", 20.0, 180.0, "{:.0f}\u00b0")
|
|
slider("distance", "Distance", 1.0, 10.0, "{:.1f} m")
|
|
slider("height", "Height", -1.5, 1.5, "{:+.2f} m")
|
|
slider("sharpen", "Text sharpening", 0.0, 1.0, "{:.2f}")
|
|
button("recenter", "Recenter screen", 500, 900)
|
|
y += row
|
|
else:
|
|
W.append(dict(t="button", action="env_prev", label="\u25c0", rect=(60, y - 30, 160, y + 30), y=y))
|
|
W.append(dict(t="button", action="env_next", label="\u25b6", rect=(1120, y - 30, 1220, y + 30), y=y))
|
|
y += row
|
|
W.append(dict(t="button", action="env_layout", label="Layout: " + (self.info["env_layout"] or "-"),
|
|
rect=(60, y - 30, 600, y + 30), y=y))
|
|
y += row
|
|
slider("env_brightness", "Brightness", 0.0, 1.5, "{:.2f}")
|
|
slider("env_yaw", "Rotation", -180.0, 180.0, "{:+.0f}\u00b0")
|
|
if self.info["env_video"]:
|
|
toggle("env_play", "Play video")
|
|
|
|
fy = 742
|
|
for i, (label, action) in enumerate((("Save", "save"), ("Reset", "reset"), ("Close", "close"))):
|
|
W.append(dict(t="button", action=action, label=label,
|
|
rect=(60 + i * 240, fy - 32, 60 + i * 240 + 210, fy + 32), y=fy))
|
|
self.widgets = W
|
|
self.dirty = True
|
|
|
|
# ------------------------------------------------------------------ interaction
|
|
def _widget_at(self, px, py):
|
|
for w in self.widgets:
|
|
x0, y0, x1, y1 = w["rect"]
|
|
if x0 <= px <= x1 and y0 <= py <= y1:
|
|
return w
|
|
return None
|
|
|
|
def _set_slider(self, w, px):
|
|
t0, t1 = w["track"]
|
|
t = min(1.0, max(0.0, (px - t0) / (t1 - t0)))
|
|
self.st[w["key"]] = w["lo"] + t * (w["hi"] - w["lo"])
|
|
self.dirty = True
|
|
|
|
def pointer(self, px, py, down):
|
|
"""Feed the pointer position in panel pixels (None = not pointing at the panel)."""
|
|
press = down and not self._down
|
|
self._down = down
|
|
if px is None:
|
|
if self.hover is not None:
|
|
self.hover = None
|
|
self.dirty = True
|
|
if not down:
|
|
self.active = None
|
|
return
|
|
w = self._widget_at(px, py)
|
|
if w is not self.hover:
|
|
self.hover = w
|
|
self.dirty = True
|
|
if not down:
|
|
self.active = None
|
|
return
|
|
if self.active is not None:
|
|
self._set_slider(self.active, px)
|
|
return
|
|
if not press or w is None:
|
|
return
|
|
if w["t"] == "slider":
|
|
self.active = w
|
|
self._set_slider(w, px)
|
|
elif w["t"] == "toggle":
|
|
self.st[w["key"]] = not self.st[w["key"]]
|
|
self.dirty = True
|
|
self.on_action("toggled:" + w["key"])
|
|
elif w["t"] == "button":
|
|
self.on_action(w["action"])
|
|
elif w["t"] == "tab":
|
|
self.tab = w["idx"]
|
|
self.hover = None
|
|
self._layout()
|
|
|
|
# ------------------------------------------------------------------ drawing
|
|
def render_if_dirty(self):
|
|
if self._toast[0] and time.time() > self._toast[1]:
|
|
self._toast = ("", 0.0)
|
|
self.dirty = True
|
|
if not self.dirty:
|
|
return None
|
|
self.dirty = False
|
|
return self.render()
|
|
|
|
def render(self):
|
|
img = Image.new("RGBA", (PW, PH), (0, 0, 0, 0))
|
|
d = ImageDraw.Draw(img)
|
|
d.rounded_rectangle((0, 0, PW - 1, PH - 1), radius=40, fill=COL_BG)
|
|
d.text((60, 46), "Depth 3D", font=font(44, True), fill=COL_TEXT, anchor="lm")
|
|
d.text((PW - 60, 46), "trigger: click / drag", font=font(24), fill=COL_DIM, anchor="rm")
|
|
|
|
f_label, f_val = font(32), font(32, True)
|
|
for w in self.widgets:
|
|
hov = w is self.hover or w is self.active
|
|
x0, y0, x1, y1 = w["rect"]
|
|
if w["t"] == "tab":
|
|
sel = w["idx"] == self.tab
|
|
d.rounded_rectangle(w["rect"], radius=18,
|
|
fill=COL_ACCENT if sel else (COL_HOVER if hov else COL_WIDGET))
|
|
d.text(((x0 + x1) / 2, (y0 + y1) / 2), w["label"], font=font(30, True),
|
|
fill=COL_TEXT, anchor="mm")
|
|
elif w["t"] == "slider":
|
|
d.text((60, w["y"]), w["label"], font=f_label, fill=COL_TEXT, anchor="lm")
|
|
t0, t1 = w["track"]
|
|
val = float(self.st[w["key"]])
|
|
frac = min(1.0, max(0.0, (val - w["lo"]) / (w["hi"] - w["lo"])))
|
|
kx = t0 + frac * (t1 - t0)
|
|
d.rounded_rectangle((t0, w["y"] - 6, t1, w["y"] + 6), radius=6, fill=COL_WIDGET)
|
|
d.rounded_rectangle((t0, w["y"] - 6, kx, w["y"] + 6), radius=6, fill=COL_ACCENT)
|
|
d.ellipse((kx - 20, w["y"] - 20, kx + 20, w["y"] + 20),
|
|
fill=(255, 255, 255, 255) if hov else COL_TEXT)
|
|
d.text((1040, w["y"]), w["fmt"].format(val), font=f_val, fill=COL_TEXT, anchor="lm")
|
|
elif w["t"] == "toggle":
|
|
d.text((60, w["y"]), w["label"], font=f_label, fill=COL_TEXT, anchor="lm")
|
|
on = bool(self.st[w["key"]])
|
|
d.rounded_rectangle(w["rect"], radius=28,
|
|
fill=COL_ACCENT if on else (COL_HOVER if hov else COL_WIDGET))
|
|
kx = x1 - 28 if on else x0 + 28
|
|
d.ellipse((kx - 22, w["y"] - 22, kx + 22, w["y"] + 22), fill=COL_TEXT)
|
|
elif w["t"] == "button":
|
|
d.rounded_rectangle(w["rect"], radius=20, fill=COL_HOVER if hov else COL_WIDGET)
|
|
d.text(((x0 + x1) / 2, (y0 + y1) / 2), w["label"], font=font(30, True),
|
|
fill=COL_TEXT, anchor="mm")
|
|
|
|
if self.tab == 0:
|
|
far_cm = self.st["depth_cm"] * self.st["conv"]
|
|
if far_cm / 100.0 > self.info["ipd"]:
|
|
d.text((60, 640), "Far parts exceed your eye spacing - lower depth or convergence.",
|
|
font=font(26), fill=COL_WARN, anchor="lm")
|
|
else:
|
|
d.text((60, 640), "Tip: keep depth x convergence under about 6 for comfort.",
|
|
font=font(26), fill=COL_DIM, anchor="lm")
|
|
elif self.tab == 2:
|
|
name = self.info["env_name"] or "None (black void)"
|
|
d.text((640, 190), name, font=font(32, True), fill=COL_TEXT, anchor="mm")
|
|
status = self.info["env_status"]
|
|
if status:
|
|
d.text((60, 640), status, font=font(26), fill=COL_WARN, anchor="lm")
|
|
else:
|
|
d.text((60, 640), "Put 360/180 images or videos in the 'environments' folder.",
|
|
font=font(26), fill=COL_DIM, anchor="lm")
|
|
|
|
if self._toast[0]:
|
|
d.text((PW - 60, 742), self._toast[0], font=font(30, True), fill=COL_ACCENT, anchor="rm")
|
|
return img.tobytes()
|