From 9153be2272a54514275943e5cff2df0c91d0ecb6 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Tue, 9 Apr 2024 13:35:43 +0200 Subject: [PATCH] Import code and add info to readme Signed-off-by: Christopher Arndt --- README.md | 43 ++++++- knob.py | 247 ++++++++++++++++++++++++++++++++++++++++ nanogui_customwidget.py | 78 +++++++++++++ nanogui_helloworld.py | 54 +++++++++ nanogui_knobs.png | Bin 0 -> 22056 bytes nanogui_knobs.py | 82 +++++++++++++ requirements.txt | 1 + 7 files changed, 504 insertions(+), 1 deletion(-) create mode 100644 knob.py create mode 100644 nanogui_customwidget.py create mode 100644 nanogui_helloworld.py create mode 100644 nanogui_knobs.png create mode 100644 nanogui_knobs.py create mode 100644 requirements.txt diff --git a/README.md b/README.md index 8cbe6c7..6ec9fed 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,43 @@ -# nanogui-experiments +# NanoGUI Experiments +![NanoGUI Knobs example app](nanogui_knobs.png) + +## Quickstart + +```con +python3 -m venv venv +source venv/bin/activate +(venv) pip install -r requirenments.txt +(venv) python3 nanogui_helloworld.py +``` + + +## Knobs Example + +```con +(venv) python3 nanogui_knob.py +``` + +### Key and Mouse Bindings + +| | | | +| ----------------------------------------- | -------------------- |------------------------ | +| Mouse click-and-drag knob | coarse in-/decrement | +/- one 100th / pixel | +| Shift + mouse click-and-drag knob | fine in-/decrement | +/- 0.1 / pixel | +| Mouse wheel scroll over knob | coarse in-/decrement | one 100th / click * 2.0 | +| Shift + mouse wheel scroll over knob | fine in-/decrement | +/- 0.1 / click | +| Mouse wheel scroll over value entry | fine in-/decrement | +/- 0.1 | +| Ctrl + left click on knob | set to default value | | +| Up/down key while mouse over knob | coarse in-/decrement | +/- one 100th | +| Shift + up/down key while mouse over knob | fine in-/decrement | +/- 0.1 | +| Page up/down key while mouse over knob | fine in-/decrement | +/- one 10th | +| Mouse click on value entry up/down arrow | fine in-/decrement | +/- 0.1 | +| Return/Enter while editing value entry | accept & set value | | +| Escape key | quit | | + + +### Notes + +* No support for scaling or re-layouting on window resize yet. +* Only the knob value gauge color can currently be specified on instantation, + the knob gradient and outline colors are currently hard-coded. diff --git a/knob.py b/knob.py new file mode 100644 index 0000000..3561854 --- /dev/null +++ b/knob.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 + +import math + +from nanogui import Color, Cursor, FloatBox, Vector2f, Vector2i, Widget, glfw +from nanogui.nanovg import BUTT, RGB, RGBA, RGBAf, LerpRGBA, NVGwinding, RGBAf + + +class Knob(Widget): + def __init__( + self, + parent, + label, + default=0.0, + min_val=0.0, + max_val=127.0, + unit="%", + increment=0.1, + size=80, + color=RGBA(192, 192, 192, 255), + ): + super().__init__(parent) + self.label = label + + # sizes + self._size = size + # value gauge width as ratio of knob radius + self.gauge_width = 0.125 + # value indicator line length as ratio of knob radius + self.indicator_size = 0.35 + + # value and range + self.value = default + self.min_val = min_val + self.max_val = max_val + self.default = default + self.unit = unit + self.increment = increment + + # behaviour + self.fine_mode = False + self.scroll_speed = 2.0 + self.scroll_increment = (self.max_val - self.min_val) / 100.0 * self.scroll_speed + self.drag_increment = (self.max_val - self.min_val) / 100.0 + self.callback = None + self.set_cursor(Cursor.Hand) + + # colors + self.color = color + self.knob_color_1 = RGB(86, 92, 95) + self.knob_color_2 = RGB(39, 42, 43) + self.outline_color_1 = RGBA(190, 190, 190, 0.0) + self.outline_color_2 = RGB(23, 23, 23) + + @property + def color(self): + return self._color + + @color.setter + def color(self, col): + if isinstance(col, Color): + col = RGBAf(col.r, col.g, col.b, col.w) + + self._color = col + self.gauge_color = LerpRGBA(RGB(40, 40, 40), col, 0.3) + + def preferred_size(self, ctx): + return Vector2i(self._size, self._size) + + def _adjust_value(self, val, inc): + if self.fine_mode: + inc = self.increment + + new_val = self.value + val * inc + new_val = max(self.min_val, min(self.max_val, new_val)) + + if new_val != self.value: + self.value = new_val + + if self.callback: + self.callback(new_val) + + return True + + return False + + def mouse_enter_event(self, pos: Vector2i, enter: bool): + if enter: + self.request_focus() + + self.set_focused(enter) + return True + + def mouse_button_event(self, pos: Vector2i, button: int, down: bool, modifiers: int): + if button == glfw.MOUSE_BUTTON_1 and modifiers == glfw.MOD_CONTROL and down: + if self.value != self.default: + self.value = self.default + + if self.callback: + self.callback(self.value) + + return True + + def mouse_drag_event(self, p: Vector2i, rel: Vector2i, button: int, modifiers: int): + self._adjust_value(-rel[1], self.drag_increment) + return True + + def scroll_event(self, p: Vector2i, rel: Vector2f): + self._adjust_value(rel[1], self.scroll_increment) + return True + + def keyboard_event(self, key, scancode, action, modifiers): + if key in (glfw.KEY_LEFT_SHIFT, glfw.KEY_RIGHT_SHIFT): + if action == glfw.PRESS: + self.fine_mode = True + elif action == glfw.RELEASE: + self.fine_mode = False + return True + elif action in (glfw.PRESS, glfw.REPEAT): + if key == glfw.KEY_UP: + self._adjust_value(1, self.drag_increment) + return True + elif key == glfw.KEY_DOWN: + self._adjust_value(-1, self.drag_increment) + return True + elif key == glfw.KEY_PAGE_UP: + self._adjust_value(10, self.drag_increment) + return True + elif key == glfw.KEY_PAGE_DOWN: + self._adjust_value(-10, self.drag_increment) + return True + + return False + + def set_callback(self, callback): + if callable(callback): + self.callback = callback + else: + raise TypeError("Not a callable.") + + def set_value(self, value, trigger_callback=False): + new_val = max(self.min_val, min(self.max_val, value)) + if new_val != self.value: + self.value = new_val + + if trigger_callback and self.callback: + self.callback(value) + + def create_value_entry(self, parent, editable=True, font_size=20, widget=FloatBox, **kwargs): + entry = widget(parent, **kwargs) + entry.set_size((int(self._size * 1.1), int(font_size * 1.2))) + entry.set_font_size(font_size) + entry.set_editable(editable) + entry.set_spinnable(True) + entry.set_value(self.value) + # entry.number_format("%02.2f") # method missing in Python bindings + entry.set_min_value(self.min_val) + entry.set_max_value(self.max_val) + entry.set_value_increment(self.increment) + + if self.unit: + entry.set_units(self.unit) + + self.set_callback(entry.set_value) + entry.set_callback(self.set_value) + + return entry + + def draw(self, ctx): + pos = self.position() + size = self.size() + height = float(size[1]) + radius = height / 2.0 + + gauge_width = radius * self.gauge_width + margin = gauge_width / 2.0 + percent_filled = (self.value - self.min_val) / (self.max_val - self.min_val) + knob_diameter = (radius - gauge_width) * 2.0 - margin + indicator_length = radius * self.indicator_size + + ctx.Save() + ctx.Translate(pos[0], pos[1]) + + # Gauge (background) + ctx.BeginPath() + + ctx.StrokeWidth(gauge_width) + ctx.StrokeColor(self.gauge_color) + ctx.LineCap(BUTT.ROUND) + ctx.Arc(radius, radius, radius - margin, 0.75 * math.pi, 0.25 * math.pi, NVGwinding.CW) + ctx.Stroke() + + # Gauge (fill) + ctx.BeginPath() + + ctx.StrokeWidth(gauge_width) + ctx.StrokeColor(self.color) + ctx.LineCap(BUTT.ROUND) + ctx.Arc( + radius, + radius, + radius - margin, + 0.75 * math.pi, + (0.75 + 1.5 * percent_filled) * math.pi, + NVGwinding.CW, + ) + ctx.Stroke() + + # Knob + ctx.BeginPath() + + ctx.StrokeWidth(2.0) + outline_paint = ctx.LinearGradient( + 0, + 0, + 0, + height - 10, + self.outline_color_1, + self.outline_color_2, + ) + ctx.StrokePaint(outline_paint) + + knob_paint = ctx.LinearGradient( + radius, gauge_width, radius, knob_diameter, self.knob_color_1, self.knob_color_2 + ) + ctx.FillPaint(knob_paint) + + ctx.Circle(radius, radius, knob_diameter / 2.0) + ctx.Fill() + ctx.Stroke() + + # Indicator + ctx.BeginPath() + + ctx.Translate(radius, radius) + ctx.Rotate((2.0 + ((percent_filled - 0.5) * 1.5)) * math.pi) + + ctx.StrokeColor(self.color) + ctx.StrokeWidth(gauge_width) + ctx.LineCap(BUTT.ROUND) + indicator_start = radius - margin - indicator_length + ctx.MoveTo(0, -indicator_start) + ctx.LineTo(0, -(indicator_start + indicator_length)) + ctx.Stroke() + + ctx.Restore() + ctx.ClosePath() diff --git a/nanogui_customwidget.py b/nanogui_customwidget.py new file mode 100644 index 0000000..8342ef9 --- /dev/null +++ b/nanogui_customwidget.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +import nanogui +from nanogui import glfw +from nanogui import BoxLayout, Color, Label, Orientation, Screen, Widget, Window, Vector2i +from nanogui.nanovg import RGBAf, RGB + + +class CustomWidget(Widget): + def __init__(self, parent, size=(100, 100), color=Color(192, 0, 0, 255)): + super().__init__(parent) + self._size = size + self._color = RGBAf(color.r, color.g, color.b, color.w) + + def preferred_size(self, ctx): + return Vector2i(self._size) + + def draw(self, ctx): + pos = self.position() + size = self.size() + + ctx.BeginPath() + ctx.Rect(pos[0], pos[1], size[0], size[1]) + ctx.FillColor(RGB(128, 128, 0)) + ctx.Fill() + ctx.BeginPath() + ctx.StrokeWidth(5.0) + ctx.StrokeColor(self._color) + ctx.MoveTo(pos[0], pos[1]) + ctx.LineTo(pos[0] + size[0], pos[1] + size[1] / 3.0) + ctx.Stroke() + ctx.ClosePath() + + +class CustomWidgetApp(Screen): + def __init__(self, size=(400, 300)): + super().__init__(size, "NanoGUI CustomWidget") + self.win = Window(self, "Demo Window") + + self.win.set_layout(BoxLayout(Orientation.Vertical, margin=20, spacing=20)) + self.resize_event(size) + + Label(self.win, "NanoGUI CustomWidget Demo", "sans-bold") + + cw = CustomWidget(self.win, (200, 100)) + + self.draw_all() + self.set_visible(True) + self.perform_layout() + + def resize_event(self, size): + self.win.set_fixed_size(size) + self.win.set_size(size) + self.win.center() + self.win.perform_layout(self.nvg_context()) + super().resize_event(size) + return True + + def keyboard_event(self, key, scancode, action, modifiers): + if super().keyboard_event(key, scancode, action, modifiers): + return True + + if key == glfw.KEY_ESCAPE and action == glfw.PRESS: + self.set_visible(False) + return True + + return False + + +if __name__ == "__main__": + import gc + + nanogui.init() + app = CustomWidgetApp() + nanogui.mainloop(refresh=1 / 60.0 * 1000) + del app + gc.collect() + nanogui.shutdown() diff --git a/nanogui_helloworld.py b/nanogui_helloworld.py new file mode 100644 index 0000000..ced3920 --- /dev/null +++ b/nanogui_helloworld.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +import nanogui +from nanogui import glfw +from nanogui import BoxLayout, Button, Label, Orientation, Screen, Window + + +class HelloWorldApp(Screen): + def __init__(self, size=(400, 300)): + super().__init__(size, "NanoGUI Hello World") + self.win = Window(self, "Demo Window") + + self.win.set_layout(BoxLayout(Orientation.Vertical, margin=20, spacing=20)) + self.resize_event(size) + + Label(self.win, "NanoGUI Demo", "sans-bold") + b = Button(self.win, "Say hello!") + + def cb(): + print("Well, 'ello there!") + + b.set_callback(cb) + + self.set_visible(True) + self.perform_layout() + + def resize_event(self, size): + self.win.set_fixed_size(size) + self.win.set_size(size) + self.win.center() + self.win.perform_layout(self.nvg_context()) + super().resize_event(size) + return True + + def keyboard_event(self, key, scancode, action, modifiers): + if super().keyboard_event(key, scancode, action, modifiers): + return True + + if key == glfw.KEY_ESCAPE and action == glfw.PRESS: + self.set_visible(False) + return True + + return False + + +if __name__ == "__main__": + import gc + + nanogui.init() + app = HelloWorldApp() + nanogui.mainloop(refresh=1 / 60.0 * 1000) + del app + gc.collect() + nanogui.shutdown() diff --git a/nanogui_knobs.png b/nanogui_knobs.png new file mode 100644 index 0000000000000000000000000000000000000000..29766f636834af2929992b586c711ded360d0a30 GIT binary patch literal 22056 zcmdpeWmFtN*Cq)BClClQI0>4d6WoIbcN=tYcXtVH!QI{6HOSx=7$jJ5clRCMZ}&TA z_niH?e|8Q{_f%I`-Kx5Et?x6T^0ML>sD!9+aBvtBKSUJa;NC!B2LKrnRsvB~b%vb~ z9E2p4kzpTCWWx|RIC3}%5kY0w^urFfWGywfofj^bvIduu2Af37p~T2YjK%;AfPVnK z-xvTJ`KKRq+%K9>=szXiQvOash#l>YMoB>jQyU7P1vZ*Lw=%hj#|MEfR!U#((S zw@B_LH=#`R+~sBCgg}nzkX#!jSm3GZ#KhJ1HUgCf`3OS8e~0OLVWDey!$hjFp9^dC zV))sczu5o@Sm*q}NObFI{^CsvAOxM7OqBJ{FHHF@)C4UaFKW+ce=OZsfoiTa90+mq zaQ6W;<8Qo9y3VkveMypgb87N(WOFyOQ(lfLM?!1buIt`=K9a~(z z%Cb#I9*FMf9^iizR~c|$MS6+v4bI$XJn_VV>t((TzLR8QK0r7&3%0z{K|EU=L}t_jPylnP(P9$6W=R zObuSw*|kf|56Zhk6aE&{LfVy9&)wZ!I6ZUoeb%av+fq#%r@u*f!U~hG;jt4nu2UmU zaGK~pp8h`KnP*`i|H<(ZveO=E*IO#fUcR4xgM@bU8}sGqePzpYB=2Qi*6B=K^ql7y z7tijlaP?-Y>pT>^X*Yy=>BiL0H+ieur)h>_0E-dR(z- z*{(_5rFA{hX6`Ty-@Mt~J=~-qd*D1^x7-Zuow(@Ne8z;W16((BPHW;o$A^z3LpztbY&B{@(uN$u5FFw+(Gi5pEv;t=?6*yu6Pwt>3m-^2{XjetB>Ge2R3}_407i za*bi5zfs80|8(LycVattw7Nz?=1%63IX~vH^1vVemG`9Mh~%Wu=Zf(8xauWcZt3v@ zH&5H`MAwZs9R0}ldXhGTq{+K&?bB$ImLssu+LI;gZf2F4jZLHRM7nDNeeSq5#>Qu( z?6E(~`&aJ-**fOSt{hM%4ugoJ?jVTcLhj}CpN5IZs z4mMLScbDGBoZb;>?3VN%V@9|qTqGBYk`^*p^h>+a)A9T%cms`=)=$@6=e1HyI|A)n zR-Q*vSyk?17qBgJCp__nncF=!PyBtI^RjysF(Yrz)1lu(Sj8BR^#fj2*D;2p)6Ku_ zi^t)Fs_m+mbvaJw`bqu!b%v~s6aJ?zjfE~$xB)v(=gS{w>Neg7Jg!icDtNus6+X{v zGX93$RktUI%;Vn%n}3;POa0xU8!8VImk;Qe7;jH6vQ{RZy*%FBZqIf0ZK<`#I=JY3 zXH0wdPDv>)B0BOLKYQ!$s!Xwf<%c_%0+%0-J1^Kfudb?o z4dvf;2++$}o*5HJ8G?*~2pVBI=x9?W!S-@} z#dNmeak8CyVDoQV&NIyCK3MK-b0eCe__61Y{Yl4-E$m=Dbd5A6$c(CM%+m~S zw&M}RvYpd*nLSdcsnr;lbpH7A{7{s&jRa5QI*=%IGnRx&^4T2ZS)C7c*Y^uI8s&-K zN8&$zTkHN^sN@Ku*5!)E;j^yPew=y`w%BONmsT=_()m!~^A0s9VSK#O&1(h5zq`za zy^t|t?`Lv2FFIB1@BsnN0*K)wB*HxQ`4&%o7MWZ!3ypHFFDL{Tl`=2fu z4GB%we-V5O&^tA;8{W8{ZVviK0p$!3R&PdxKo`FoEhS`ZD1w3!TQ06oY&T@O6b?Oz z__%~j)bLGg1d^uWD72pL4?_DlF7Dfn4>`4m~h zY&@^RJ#P(s#ynPU1ifwNZUBUYl-Qxw=Ft7xiPoEAp1acl{@*dZ!LIr@Ox0Gwx^2$* zLEzel8WtkytTE%eoeA$Cf%{Y4>YdPKV)x_j@MnHFwa$GuVpn&v0C<|_?R%oZe5J$1 zw%}LkX}rw+p5Ftmf{lp7%2?m?Fk+YyIdWaM^ENPu^kgG3w0p6`85SD(wB{|J4VAazYWS+yfNxmy;MKOpbb#*HWcuX_J|yFVsI?QwX- zP)7-7=7KeHd_K|wA=1=J4o+|XV+*@H#^TzBNB~q`?P1D0 zvDee^FTDS~`5%x-^3fhk%y7vJYqyzx9%H^}Dr6z6{C-+wwn-4akEDVm?wEOvjFg28 zE8&(u8Y6jzY@N%=O6>-U|0LkG8m?)@s||BiVN>z9C|-XelkagIzw5h2hiM_3K~JcN ze^dJw12LEVPd<-pz0~bo&rWjW3vo~cY^bxT%;*Y|cE>F{>u#hpvgx$PYLZ|Lyv`E? zz?Q^v0mD$szEZ^#%fOXa7Bf0Z?=N!Xu&OGf?hCH*QIm-VU96w>uLMlT2gwDHf43ML zile#fqv7`h1B?V<{XRc?7Zw)6dN-LR*6LWfN8y(H^WDxx>s&_^tPfmWkDUycZ{NQ4>_f}$ zo>pJHqKLU(ukDC~VM9MZvhhjl4aS78_IhxxY}rm-ugP@Z|I+zPB`apKujRgDS?`~UtI@V8SK^MNizrTIl5twl4ydbe$+>VuE{C(p^ z>UF75KWn+%io1HV&}c;jvUn}=T-_P7@n-4h=BZzyv z(tUAqcXw~zO4MwAIBsh>?Ll)pvd|4bdFZ!ybap;(ned9@Icbk`I+(S)f~w))tU6B1 z7!9}&N$K>s?PSnk_WCoU{n7vU)9kBe1dBlB0iLD`?B5vjzsVfo!^NiPr-vJdr>nU- zSOfZH^l?M4c!Ur&Ve^qEU z#r~kYor?DE6UO92=pNC1wr6bq_cmU!-+M3s3A5H@g7kGQj9})jhDk=CVpxO4+$`@b ztffIF?=v*LYkp}#^~xV)dE}$N=jQ=g&zD6&nL7G>)FM)9llupM@@_lg`q|sg2^=}z z-&;w#<6JxG!n(R-n7pTOf>_l?BY3bWU2Kf38|OxYo4D9LI%4qn4h7YjO;f`K1qFo- zkPs5;4@8k%-rOWLSesm9qNCfwHc_oP^eel~ik1`o>doHXUQ94pF!`Ms5VeOjVTh4s zlyYelV6f#~EUTY_lBVOlV5H@;?9|`&=K6SfM22IzZ>7WIZq$Td$+HR!fbEOcLx-0_1k$&5!58F=qD3z4Q<+ha&5fO2^ z87G(MwLQAy{mRtPDsMJ5EjyS!uH?DHv%2PoL`s`Tpl4|KQ?2VB>geKfaoUUN0Na)N z07TG1>VW%xNs)2JvN%>EkGbdzH-_!An+cPX1_F5K3yJB8n+|ZOkTrt)f(N}E~y6+sj*V~(T!0P)zrz+7ZYl@WVfSH6P59NoFH(G77Il4ULy$nuhn|$QgT(` z0aIF?3YhhH9NT^?Nli0|*^U#u@Q4M$JnF-r6-&k3d zEvXU-FGe5CNy;t({^aSo07*WZf!PAaaB@~$vHig5!ZP^ys^_u)FQKRZ?{AkEFn$+i7G}P> zJlP}j?WVm`2wW9fR!E)}z=K@cz(vaa@P&xuWTPZD=Z~Z%B>tve%pOp@Ob%mIiedg( zIcq397?i49%yk$O5pa1Q*Ehw1Mw~<}mFI0i1n0&M{zApEe%! zq&uwVx<^fLA~z83e3Dh50EX&g5B#zFh=+sOhHT@r(Yu${`E z_1NZqji@2+f&AX|D%p36e#YHs^KlritQ!}45E|StjoAQSgf|%ot#y6KJ4H!C_!xhb zMClP>}1qGc?i z1|K7A|DyNq&HmE||4T^Tq%=>NEz27S_ETktG>0FL3XfVH+SR=`|Mr%ksAj;*o*sL? z6)nzHIGX6!ZzY`D^FAJ%%7f|!=$5U7zMV~F(Z*TCfOZ_?YQ`y{`;sc82u-)DyQnT_ zpdq594oGbia|Wjl+PgI zcK4XRZzB6!Ak`3?pI@qM?Nq@KXCdMSdJhr-5*w+evsVnYg5`36=sNle33ejoys63$ zMESp8skZ^@=@NhHC3Fo@=A8aM4y@3oQEdCK?3j(&aTtC^PC;c=%Av+JB^Lgm zg2k4-$hzBvp$ei#JB&5#M!S0)B3{kF3P}G%?pO41wx@IIfh(vH8<~U3a3Q~lE5U`< z_8yKNQUjG=yb*1T0&>4;r9&HfUf;qK4;R`H>3RM*Ys2uWx7Ps^H-tH+V2S!#XEvJPED=R(IB_AM@d zwnDYO7vmQl10>I213cg%sr$z`@jv~^ou-?hB-xR z#vON3BD=K^$3r$t;Df?IHG%uoYR7`9wE-)uyC@-ckr`DhUWDk>7orNb#3L*@%1|n7 z5d%B)Y1&3!`O?#uJu+Hel!?b|ry438#7`}jL6+Py)EPZDOFa*aBrCYP>B>~DQI zrD!&hvhP>_>Qeu)y20Zah|HiBvlH4ZwL<_`Q*=rWP2;>c5QAL_qF!bRXQvP0G`61= z(5JgurO_|?16g}vN*=z4yl-r0(D+Y2H8pZC9CT061@Dk+csH$hjdByzb}~gNsaz#A z7DGz9zVYknQMwgr^ zP!9~CK7CzgY~?}PrbjHnHK$#u73QJG7P&GvCi+!%W(Nh_*_MB~3<28&Jr+Gy|zO2I7Q%i0ns|Q{N!$ zt^0I5XMIGi6>o!gFQVz&A_Jr>LL7XTCsQUqOM=v!Mk=YOO&U%Ka9sW=(`eV%mAZ26N44U7yY3P=u~GL0x1Pj7rJ~B2aLIUbfkR z%5Ib%f!kZ|61Mo^A|l}y!05^kQ)lWT)Q#+<6@W=kQG+c5=ZfGj1xlW_-D6EbR#KbY zX(fhu#lTUfMM$W~6!?erpZ-CSPup{uiHvKxT4j?0fBVu=vs7_nX?lT6Hc%L0GR5&+07PSW0_(>Ta7oW5~9a&Nf7I4<^t$OB$uHGenfwuZT{oO*11#a%j*Xw{yFI7duZYW)C(o7Ur7W-d;f-Vpl&gC#N699dt%aojmQscxd<-Lp94QV|Xva^2d%NCVW@j7fRot*x%Eei!)K%UDo{+ ztTd`0E~KfR;Vk9^M@F3bGD;_;l4>r1=sSRfgdOgs&IeyfKJk)@=fGxKqBbw?2kY$o zIRo5CEpV)dGuM+nJhNWm1BPrM*H`lGJjIy8vDS{LnrNtmN6`p1eQamIi$ zn!>S~Zzz)B54~ziqdxBv3yKDmJ>9)x4I@j8NhMnzkJr9vf#k}h*+(ii+;j+=G3 zg9{bg>8;NKB%&=3egr}&i<)vQ#26sey(S8hP3fxEjFCBV{$U>!HH<}nASDutHpqAQ zQ!|Toe8ZR`l>4X=eCkR(UAwohDaf1B7s~Fl9%iPITo`J^<60@jX_l?e28s$oHn*u=1u>Gabv;USb^r*#xUGW-!d0Md!}WR9v>S78|FxpZLfOyxT(Texj&up2L_ zA%tW`44e>Wn!8n4la1I*EE!t`a#70!I8ac#)XNu|>)3oQ20qW}WAk`6ZZ#a#c35OEt8; zd}A8tZuBimRa?a?>Ju;?Rnt6T;}1a|&c}OPIus1fS-+%q;NaT|*z8sDFiFZC(C zIpKB0qF()@VGAGRyFB&bEnoLtuTaG1X8#7AUj`yHZW|<%>bv;yTut7Vf z)DjxL&VEy*B^I!aF&!vU^ng2}fFR&w0|Y@smcZa@=80m4z3Qu?FqaYX)=tmWv)y~ z7VK^d?vaC#oPV*+(3ug{7I*E)PL2@Uz}zlvKVgYts9iL5nYB!Y=~6xdLF#hLIZ7;qJ> zc1bi`=62Q4r5^>6MjAtDxI_Ba8GLF+vBLXN9X%OMofKNag4hz4ZMah0X0{~E$^}lr1>C?ilE7J_@>BG730&#KWWbB%3 zt!pndS!`<5kJ|lZ+uZS@3>J_=2)!;v$6>)_X7v;gwkbWf-l+!^m5ZQSbHyC-6=Et- z3^6#Sn^N|>aVV`q!-Ij681OJo5mFt z6IhOX#*SPU$-B)XRZY@h`w8=(X}#6{_+8<| z;t}r-fdh>>@$lW_Q|K2Vy^5R^%iOKyc0#2ugqz(0Q>q{pMNrk-6NI3KT62~6a#cOb z`VmDTZCGW?Aas9US8)^Ya1MK$x-rb0{5%CvY9XFg|9DRaiUc;V*Yx8rv6J4o{FOR8N z(2dRr;K5Mg_;z`|a&$S5WZ!WK=x}~kTjnfdWk%ae=5WY$Ng#!8x>V(0JhE|QF@txC ztEVtC$cJ#qex&;&K8+>$ASVZy^&Rb}$8H4!=1S?9A@%sx0xM$r;IRho4 z>pILILYIWut|k%CId5WtQFX&@sKRQi#U9H^z}+RaIeRAd0{6{JWV?n+%PtAxKtJQX z)Xx_50T%|)PuErHqm81=cR%Py=*nAq3k5DMJChq+88ax*g`-?k9Wu_j|5$w#n_w`? zQ!w~9t<-2z(q0kYGqf95i*O?SK#C2ra!-*!9fN!7Rm$XVH6-CbScAZX z^?$SyO#H!wBDuix1^VCd(G7nP%Jg|nmIAotaGQqRxC*}th!yk6Zj$ymf3DjHnjKMC zkQyXB5Muw{uJt0gFohnZWhnBU|E2_5sHg;$N)Triq&iCe-@Bc*%R8nbius@do*yYlDXf`hL-#tR!8S!TPHGF@7NL=6 z2}9-vp0q~|qRTDArF^Sql60NvO)bkAcgKMzYNgPhjeAwd9nNeyz*O^EhI$Zf0Vw5~ z5-b{`5%FP?Ey(nzQee^dX_%XBv7p7wvWj1TTqLb5D$o(tM!U}eA7kM7$I7PcFaIwO zK;fKDS~Mxec5g&7X4c=y-1CI}wLqo$(j|ZliOu1CHPxjCB9Ddi$LEucyLV>L^2e(3 z-Ui&K!&L`{&h)yJnXuvTQsuHZ?;k!!oGNCC()8q$P)Ko4P2(gir^8%@6A2em%#);8 z>ZYJ;5HSO)SNo7JEgffc^^q;WY7rL8{`EFIjL4S&Z(Jk8xMz>G3}13AI4)u{tA};W zs6w1S`LmAQUtB!uDXJ8d9iy6k=@}gB?mV+5hWbC<0>)OKKi(r|p`c&m&3&nO>M*Y7 z-g{>{@U^XpJB%s~g@#;#En7QB;kUY`sT1|6X@Ng5zF>;VJa8U?GS!_r{hrXE;<;b? zQ-rx(F#cb5Q42iSdje*LcF*^825NHdSU3B|5hxB`(8ez()w{q7alHZ{9q2Id_j4o% zB-|XZ!Q|krX?E42wdIoj@DOj#Mhd!A9_?DS`fiT0?OR14W>j18Un?_9$-gy~>^r_? z5z(s3ZvOW(x-j~5Un9lHk^ApP3czbXf44rclv#|#LBZfEMV%8|Uh%_DS2m#c?0Zpnv; zyZkZG*8D=IgHzZ`GeY$WidBe?t^{6`eUJ{V|8BYX$tLyZm%x@-Xup4OFFkiJaT~|mN~YB}zCvd=^U1DO zyCg^NC`Hv0&S{;t`uZ8y0&81%Y%M+7RAq7)L+ES2eIjj6p1q$AoDlw0X*O<-W-lOV zA8717GXWU;RmvYQ`8_z$N|Ip%&G^2sNLX7vdx4fsSew%&yxrHXM@!u3YchVJU9V$} z^oVw}eDFs_LCJ{@Auzk+n++%DhU+}RAK3-Rqo%l~k#ct!(lRg0gH(2Gsv7zdImB;3 z_?7rmsP*qrck=s4*ia*(^=N>(DS(9*`tM88+>b%u619Ub(SyEySK!3=E7iJewLQkW z<4*5c*ZCdw|COLzz?up}H7Exbc1`iA)inU%r%I*i!uNALU=PpB8z}oJgmhTQ7q&<{ zG@!1M@R8^vo2LhJ0}PHRWA{$ecNJ!Y>AFh$r*=n`+r+R#1+@96_TyD`A_?rxGuqr0 z54Npo%eAEWhcdQwz zb)P^}-$2k@Z-_dDNbwj}!N;8UD4t!+Su5WtPrZb+Iq}^Se_iM+@wkbElExq8I9_p0 zV#s1mvL%wT#N$=a&J9=A1m(^B^QbQU@EN^<0i;l=Qfv{No|X_qNMRfTPByJT@rUcR zd<4`O!`@O&68188mxI-vfDn)iI_yPwu6QVEgU~ow*aHBjFZNd72VzWx*;kkc$`E;# zRX91!&FA87fyQR>7t2|f0136oJ-^6LYs`5?>5=@qX=VsVn2lu=L6$-pRe!gODJ8#5 z<%vXtAoII;8@7>Wh|7uCFcM2=2~0i1Yn&SkOjRnOmJ4lTk|dEekd1T=WTX#xnfV~+ zj$C{RECej`_!481la~4X-hDTeJmaiamJ*BnIyDH4fD`t<>QziK3sH@?P$}!*Wf!DX zF)#>GH3F$r^1mHv?N^UsGyVw(5>jf`1Jx)w1CjmVA|cGm{pB2RrHl*^N89wCb*7?Y z>c$TPK-5{b3TltD!$&qeWbgdE|Q`SO-C*(Y{!*ULJ(<|EHB$YqO zfs7RG8V#t*Ok7T1G&U5^_dTWgq(uZ|@VY9~OVy|aMJdH{lFY(CpmN-X@v|BQ zjE^fHPq(k8vSaw1Me2WntpSO-as{#!iL_mi7PI8&A>ATg(|yWt)RY^uGI55hG-{aT++ry`k@so|2C*2Ue~b@V_D?ewt7_uQk2X1K+q_KyUR05x9H^$1KKDw36hsM zib#*fBS@p53Jq0LPvI=Y;}7ZhbR7FhG!%zKY1kExKeCj(4nU=vRykWrb{9>pQ|=M% z_%v3Jgj}!*gGk9fXb^0uINs})$_WyUMyRSUt30H(G$1>%izPTs#+>71$G#~v!wz}_ zqmTys%Q4GDYImn8z+ylhYO;`&LGB}WCDc|Cls1vq+ruS4L^vI7RF+kV7w*XU9dmJ{ z(P>fB7;3;JUn(x5QdPE7Hi{jO`hHVrye#?TF!tw|nh2frbpAxSw{JRflCc|xk_`5) z>zR!erQH!n?uN;lD3g(XW>(%{+^LOCII~*`%%rc{Hz*yVqJ=(|*n|TOgtvNymU^A_ zH*2|;Jm1r->vcH-9&$fKusKxiS{?!VS67v~&e`EOvV?;_In$$_Sze)iyBg7xLK-M- zbXw4?u?p5=)SNNkQkbD2?4?v_5YRJ=I4jZW#=u=nCq^5SwG%$iStr>x`*3Crr#IO6{k-BgvPI&u2-CqC*<_I?|J*jhR?6!@}NY84feFW}4{ zT!$GqvGv1IIT&X#g@8}r*=Y-Ui?ErPt!(S_cOwsX@!l#f$ywyijJ>VAVLyCc<$|+? zP*KX!f?2{Kd&qIMJ^Myu4NascI0OL`96a%+!rO(XKSDKVSy(R;p-l=Y_l{v_eoTw3|-(r7Q)&;wGq+Dhe%K#feM zW3vzzf4^nQZ3FmSJXNI{ZcFd~Fps)4pqlM)1auJLPZGC?#0kwdU`qrJ&-##`hBOro z13ZpMIHQEwlPI!A3^YbIj5SMx3(9(npRofKOl~!X%f3O>sE>@QRMa9%{!u5=MsNoZ zjK@gqlR%t(#Qw}j;*!!@Thp-67bur~Z_)wa5`rTLh>XlzSJ@FDdra?Mp612S_fjuweD z4E04%E$CR}KMCT^JZq-adG5YERzdM*P6Z``tU^_ZMqCH6>E0Q0Mr`p1ooHz_@`dQg zQl9r4l_aops2(*Xi!ii+iUuNRxNQ)34~ySZG2pG`i(c05e*%CZdWR1vi2SROE324$ z*U|zve9@OPl%_HR`^cqj?kkFHgh*9(a&dI&=Rg&Oy5MyFA=7&|&(#5EV)VKWYSrW% z<><^PHW6i~!n!gslvVA03agyHb`{y}&X<6J{5e%T%H}>&@JldkieT-@9 zjh9+NjxC&5PZiYljsDYKiG%7gm7^5zulh~y>iHAcFMaXC(vi#*w%w)O8F z(6UgRZ+W>M%#^32z5!@m%#@R>NUOsN~>PntVA%O+T{D{mORuHB3lEh+rK=7>rUiGieE z83%UCYEX-FbHFk}ML&Ul(E3*Bx$XYfMMFwuUSOsU9?0roFTV?t{VeSFdG)mlE8&E< z#vOYere@?uQ5Faf)Xy`~qefS%gQLKtC!a@D5^CBGd@l;*C}Q>)9-#kwaM3KF>z8(M z%8Ln859OaH5r2XQXEqVI(IqIc=ZVZO8;pr(T+6SKW?~#|Bdi^GKGb<42EF&77+6NK zUDjGWfJJ|WRHO-y;~+{<@JqqwnPoRy2D>(H1;k1r+=s;FOsnarfm^^@X)iqQQ@Uv^ zOxZf0vbr5e)+^{#2_4YuA0N3|cOEsM9+B*Z(MI%hMiFY|VvlD95m9{S1mn1w-62Q} z|KaRNJyf#eU8OmFYD8hnr=~ge$_m+-tXcR;D9k!zTU&_}JRzzRJ$<63*f=JSXIB)R zbBJSAF%TBTkgObBbzyEgv@M}&4J_vjX%OH zU};Y9iWI}%vV@GT%*}#Cxt;&bCGSz^M(MFCd^BcH>;qgi2kcMis~8%AEY8&he~vm( zcbkXpazXw5rmzkvhlW*>Kxh_81K{^&$&j?Bc|$=__!yfQF!CpJ zvxq}p=6gC%(z*7KN%}6xTML#C77@&KRSXT1j#fPd?m0+l3YKi`2nCv)Qu-0{$R%Rt zhQ-`FhVtTVgXjndotUWfWo&Al<3=o-EP*&n%X6VE{P1juRAp;XR2B^5zuwDLQ=iir z9k9N=~j{0HC;t(tJ-?XVu{Yg$K3t9 zLR}eyASxEIjG9TYkd7Ajw@15oX8s*sn>1d$bN%(Bi3j=4%e*NiU*Z)Vot^$!j?WQ~ zR*B)r?|k{U9Z=eUnc!=W<0(f%`?T5oS2r&>-rp0} zZ^wp-5D&;}O?_BfRr*xo*~w79e|)$}g%WHUDYmxuo#D>+c^bU0Q2+ky2yMiL7D>RK zAoNtc`A%8W6gssK={!!6f1ksU8wDQo@QNug8H)Ho&KWOKF$k{sidphi3THW{-zy(~ z?&|d1?K@cGDl1qIIA~O7sPqLcc8Icu;f;tzA9OmVr@5aRyyO$q3=(etoNxX$4R`7S zU`y8Lw46AX$I*xzw=Y7I_0f&(&+dB0hiD^s#t_1CmtmXy<;34x86T|EPGNDbBxlHO z`IIs*0Um^*@vZFX$I6J$}j7c=*@$wlNL z{5CQs#JP8pGr*k`qOf9j43KcoTLhfBG}V)OomHr_B_{lF-JC%InH3g@f8(EX=I;Bd zT9c*Wweg_jEoI>ZR!h$$EKHs|jaWs!C`|9UCZjpgdRO`=sGdeg?*0b&(wR6r9C6ZD z1=7Sq*UH@|FAdzsW%UE*;yz!X96HbdbGMRO7dCL7ElCoMVkUOZyf9a9P-||frR-+G zKe*Wfcec^iS}PgX&p6AaRa^|r0txp`chNa6#97wJ@+Ugsq!NNfa*dhb*P=B+(X?g7 zpOCqna=#ciYanZ2$nAJm9y>DAjlY;L{u@yOOIb8POI5$->xD_xAC zR(1<#W>x(|;$2qIFkBZZ+3@j`f>U*gNpnghfq~YaQA|FdqN45| z9yRo6s~10HWY;~$9B}9u;*2ydudlHq#n4z5XCxa5B!w#7dZ}7$li^crRtYEH&_|GtsyEW(cvf2 zWcjT%Tg7Z@;IA|Fa3Xz%+UtYUF&W3rXWg-mH1tto^+v`tTo=La;?OJNuHpAGtfl z9imzhURaSrdxs&PET5Yum8%J&i;yX0$%L z^j0nA+<}xePFZaM&Hve+oQw{=lhuus`r2smoEr|`c!YZ<$5cM>brdXFO;mI#ismKn z8D_p;n(~Xp&5E;H%D@Z}4x5!xSpNRZjLhEF&)q4{5bBC()A?3dEa~-5Dw{=!$mheQ z7A3}1DzX25(2DS{Ca*&cv4D4a9F3g)Yx=yyjw~$Wy{)+SBTRU;+^&afG??Sc@!wHC zTp#t`Jh^pnkVe;8%rPP&A~wUYbNCg~V}tq!H89lWY?ZFux0L+6ynsdPHmj8#5@UfI z$uEtT3qAe#y3$w(hYNMVuqXUF9 zyVYx2f4NpmEIiWtYL^oo7yt#oLmGyrPM}kJ%goCw{WSq}X+TeXHCau1Q@^ELg(uky zih#jG=ym8}ne)^Mk~KD~{PZLGFsMa%;#7mxBKyi&lnd;QRW#o<0AI)Tla0^A;?C1^ zS!HY3)xq5K;$j=^mYM2)j<}5t6Ab(D+V>bdry~&@9PYsD?*gQ8Fg5!aH9EYMs&#}} zRJH_Bh)3Dpo`Q8QQs-=YF*xX(J;!A8_6N0I6Xb6pwH5fi1AOM!$UkmOHT6ap2rk0?N3-=zy7v^IvpX0M?nJHt* z$zgyP3`E&&4#upnvfz&u%B0~{#JMjvTJgg2_m@0QI^bT{?xuZ*@AB%(?tF8IYqbrA zoOs1ji2Cej40W0}($w_NZoJ}ZUXND*%GEv;&Hco3XN4L46+F<3DKMHB&5zm*Lk_i^ zL5M=d@?Ze$cS&^`SH6&$m=*vDiIJmYz2zdT^RN$aaMP?@C+!!myE(!09UdHT1_lO? zvo=q7MC_KYC=&JhbC^WyVWx~5f(x_@m@y4qIC@{p*e67()I)04R;^ThR^Ve4>5DZ759e>Ul z)eGb0AJlQQ1<9$Y`g(fVUl1GEg`)4i&9$t;rW6Kw5)OWk+eE_Sj83b355qr@qZKF{-f_6@mdUH(Ip<)IT$$YzL|_ z2G_8?E(8}8wh(SevFUHLwY6cO0%~gN;YK-W>Gavd%I0-ctvQA^I9>Xur>FVGF+X@D zPTPNQjaGH$&{ENdM`v)Qg8)B&)NE>0RMa~A>WxH!VmS;QEI9Ci4{scXdWkxD;C(#p z?YByi4uWCB;#2-ak?wh5(M-LK3U+fRj#J0Gn5$zdx2lk!?~{Z862+$DPtP~0eUl0{6>#JH50>2A+<3%^*nD_mFw`Lo)3nz4aGX}B^STe{s@h|bNL~L$ z2-y}HI6B6gWV!{t9%pBEvlW^HXBz{HpAB8O_nqY8+}Sv+*n+3DMmk3uCz#PIJMYa& zJq~L`ecqA~6B}+oA}v=tGiqvp5C{bH?5`oc?Q!e_1CiKmC8(XhG`Y9{6*An22f9oU zjf2)y^biqBQ1QZ1k#o!bM=@6#4`tuB$!)nwbR#=i{-UHv1Gf)E{U;Z%X`lAydU2$@AKvPcA1&$y8g@gKhEPge~!~Ja-Ho% zke>ZW!G;z(Lr%)QUJCtPZdGej4sE#aDDg$m8#T0}x=|$IUS4ADVO{$6`q2GXyz#t& zEEx`=>?>#c?C6d9FZO-@A}RCpl;1A2KT~EvMJ7WX>cNMpVD=_u(&u+ zI2bkcl+n}I&#$Y?Ld*M@fFo&7t`LL4@1j^aIR!3VqI^9R-!^YqsTdc47Q}l5Batjj zT2{%k(gC$Lp(*KTF)@GC)z!H~V3)Ay_m-jx9OvG=d2_eBMP5qks+k#++Q|+B zoPC~ZS6|=cr6m^=6BAuy<4YnUG-#~0v9Yo4y0s|Ha~;WmjibcGbFew@EJgVR1&61m zdjD+%%E29VcyPT7&2%dV?;aW}w?33Vi^~a~$Z*LhrcrfkYs<3S{HC>acXxNo zMle253F3~{yCxjT)tZoe1R=4$Dy+1vW+L) zDJ70`)YR1A%~G|fuyb~FJoC6i{pwXx@$@?_dwY{pQxny9n6tC91A~IzJ9V9dz+3t8 z!#rJBT)cB5cw}-?-`bkp%gZZj{R ze(CF5APlHkL?_V;9w~Cr9bDq)r|0Ibn2B8tu9wO*I7>_GAj`H~Ek%d%z0LC0Yto<9 zBG2+et6R+!>8M&(QxjsFK=7-zS>qm5|Kz_&&A`CmIZ+u4nQ?G^N6_~fvIOb6ZA^qu-!b_t|=@RxuB_-}N zUQd=(F8XKo-ClF>#f*gE8Wt9AQ`69ph90hBPL_7}_X%IEeZ)e;zj@2UGOd;9LVOw- zC1&AA0}`KINIctCZD=iy|C>?2=rWG--QQi6uSA0Dr*FHuI?1w~G;KI|Z0aEyA!^!` z=4dH2ZrFreXC&7`F6}5b+DSm-po!X2V%z@2?C0ml&(9wd(P$x(rV@3sYq4AGme-CF z9J+!TA>Tk3rp>GSjJ){gQyp`*ZK|@|<0t}llxr-3NyN>|i+25PQd~Y&YDkjU-cyEv z>Wr{&k4x71Xn3*+cgMT=*);FW#)!1V&J>AZ{4R5!uN4{dt|^UH;N9j=TH(uVM|4XD zis{#$BM|oEdkBG~gOR24iL|-5|B50$HUu797E)*wx#HGXsKmA!Sa)|eD_Vj->43(S z>td!TMYrT(jB;u?Q==GzIu#G^73$aY>Qs3={~mL4t56KRx|^;8BYY=YAW6*=L(gWy z0@I?R6@CJr#l$gc!)NLm|MzV#F%P*=@vW_`y7C(F875X%dNA4o^!ozc+&E*)Xj`W| z)!k83h*m$a4aZkI00n^n2g@h#*Erpylg;RX$?7}zx^t8L6ga@V3a~lw>sM%uHTa$v z7m+t^XgWCXL`Fs)Z}&>rG@_W2x)_FnnV6UyswRFx7UZ2wbo)7|L05vOBzgzeaTNg@ zlb3D8aW8RIKn&{Y>kEzBF1jSLR8>|AiHp0y+cU6B^-G)IYEy~U;1yZFP4NBd8W=1@ zpB`~DOW5%X3qR7~3X5U3k9by4P@pUCGtq;^E`E8&$MRI$C|jum{H^0vu5^$WTkkg+ z3*pyhPk+nXgAe)sM?Q0B7nhW@wDwpU=1;!!XE-@I)6&v>zmH47J9wv6uAXJ;+}`&0 zo^8So4$=z<2t*MWwRjK+F31c<7!70^E5A~>#|S>dHILR#dyCuF zX+}n>`ZS-&oq~^8@7gu|&-u0xu~H#w?bm)^gUHv{Gb^)tLJvPIW^|-AhVKu5{n`nW zxO(jxjcfnQd0=Y1^783pr50p;<7!pARuK^@6c1W_ZWiM`PuZD#w z0&6y1Ry#c=td%^Hp9y_1IeGJInNNYP5{dTf zXKKgubl@^SuZ48;@mU78FO#EW#u!T^3udL}NK@0cxw$#s9qqui^>uvDa}gqrwYO^m zObxB?!UDeoRWUp~?B?mY@U1ogyrX0-M@rYt$HvAOnVAVS%_oMchL+~$ov;KwKs#!| z&D?&-YZ9o$N%4SG3;FXPf@FLncqVe+R$E7hG!&fAijjx17~@=X`+bbs$)nzM3BoSU zo-Fyri%xJkTh(}TCgcxJPWF_TV%|+{$Rt+RJ}CvC`0(^JHu^V#o8w32nCOE|0a_Fbv+cwU=;CXDo?0ImDmApqaZLhsDW85%&?jEHi z%UM}DIsesRfy$~Xt6Kj;rPId!8GQX_lar(4{bsP8AA?koF(M-x+_ki{8VLQ$4F^kT z;vriZYe(Yp3HixrbT9CHH+Od=hLT-vgxnfoseR9%u2A*8s35fy#Fdzs*ptM>;gJyy zAhCc7YsbI^8|)+`!xCEYTYU<&uWD;E@m)zHV`Ck_dtC=|1Y~4VA#_Mn*>QDsN^5It zEG#T@XrHF}{%qy7V_~p&bEt$Sk6?@gfLwO0#1-RaXX#$$6bTCp&qH^DB6uDo(590; z>oW6F6sLftJ!Q`tuvVzRRr=wzNel!T$jQlVnj#d>U7`Od?ABP3$kljzpUA-^HE&BRO?Ze?5%L}_06L^Az)RZGY8U0ehKlh(HFD$;9a zqo#Z~t!uoU*T0s7jsZGVKdAQYFBvm>RW3SKR@Q~3rIh64S2-v&+pU9x1DMaNNKPS< z+yNa84YK->O+f@A2l(IXk=0LutHX|w---i|_Ph|sKQ#ajS~jEhmCtc1X*xR#T)uqS zz`&r-opUzoNQ_tzsAxL;o0gW=rXlnh5UaZtf2NIje%N4TXLkY!hgemtmikJ2_G~ex zik5<`qoX73`ST%ID=b?6L1HfKOY<()`Co@7vT5T{5YnEdx2NYiW9)KH_>Z??&;{Vi zAgQt~uw}T_=9BNe!$TCH!!eg?#^aA4<)CrGczuWjY~PyV;i7k29PQ_Ypz@N}X(ASH zeV6+=n-_sc;1s7l&kKp%jfoLPI72iB!`Fy4$Me5 z&OX}E)|QjF^>7|#FJA^ZaDV1yAmpOozxSJ~pvXZiXEa3~6_=Oq){86JzAKAq9vVVs zWo3&CmD9 zU{SE8k{~HfkjR!t43~)c=I$(a+_X2QriKXfr5K$cCC>2uABDBGR*hf2;cz-$USbmy z6DoV-PNSovlCrXa72NVgtGg&UuSw025P9)*8rWU90A!D1Cn>)YsVXT+FDa7oZN144 zPXWtWW?hp=+`R^F=>$+T0WfN#9F^`&8Q0NOAotInoriF9^Y8%v%ka1(zdyGwH6)?s z)-5Q@*wp%KHQw=uz8yrHJz;sERDR2ioZW&~0p1xNuB@o2_<2E2aQxR0^~Z>JPwp=w zX7}3bikmt!07P%zWP?+^xVJGCbqYy9SV-uifB-oOs0Aa86E^Ge8kT(o^E5G0T24;e z&W;NI@*Fr>95kBH}gmao$(qhsUgIZe-lRGJZ&h+cr3fYGoG% z`11Z{IkEi(6iyuE&<8ss#$$Y&ot@hH`ufGi#h=||Q?j!S&CJGL9-_ppNQvQ_pU=yv zWp#lpSPmH7hmRkL^vB&@_|c%fM|EpA1Rz5@8zJC34j%xT%RHKa19UmW8P~brK zp=xpsKmfYM+#)PtKU7x_LC(rn4tWPNpR9h|)1%+n)ukA=U4ZJ{en5xN1;7RV4lMM0 zurjfZ2jXX9qITSt@jO)9cp=4XOx2hokykxD{sv%yG7Kd^3UG-rm#OyK{XPq--msOaoXxK0dx<=*M7o z)Vmr8ny3eABja^JB9I0$urAf_&OnB*@?X?H2N?_S9Q551Ks&Pexj>p9v1A85KHR*$ zDFnNQU}C^1ET-&XTa1m9CKC%iMv4qG37qIRrAsm@D&IpKq7OfmP}=BdYrEJc*;RD8 z$x_&8_t~15{MFXhCS%t{JBn+m)CYo=6n)IM*?fusiD~;Yxa;rK?wBroX3zs?S+*@y zBdNR_95jf$-FSz(>z&W33}0=@gw+6 z1EJ{B{k@u+ht~=IBX<`ouihuf$M?X(E{r_Ax1)3_5RjEJhHt2M4RXZ8acFA z|G@H}Jen8%+l3snaO<`yvqM5L@ePF`x#hs~{pkMxwEsU;3;(^_JRD>*4W4;Eg!!JA znq(`!pj|Z~+5xhyy%JQpBpSPR`rP2oTZ#J{=Rx%3C)hasYplQNgtGOok-cF13awvB N^t6mL->Tob|33l6Ycc=; literal 0 HcmV?d00001 diff --git a/nanogui_knobs.py b/nanogui_knobs.py new file mode 100644 index 0000000..8b34c50 --- /dev/null +++ b/nanogui_knobs.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +from random import randint, shuffle, uniform + +import nanogui +from nanogui import BoxLayout, Color, Label, Orientation, Screen, Widget, Window, glfw +from nanogui.nanovg import RGB + +from knob import Knob + + +class KnobsApp(Screen): + def __init__(self, size=(450, 250)): + super().__init__(size, "NanoGUI Knobs") + self.set_background(Color(96, 96, 96, 255)) + self.win = Window(self, "Envelope") + self.win.set_layout(BoxLayout(Orientation.Horizontal, margin=20, spacing=20)) + self.resize_event(size) + + knobs = ( + ("Attack", 0.0, 0.0, 5.0, "s", 0.01), + ("Decay", 0.0, 0.0, 5.0, "s", 0.01), + ("Sustain", 0.0, 0.0, 100.0, "%", 0.01), + ("Release", 0.0, 0.0, 5.0, "s", 0.01), + ) + + self.knobs = [] + self.entries = [] + self.labels = [] + + for i, knobspec in enumerate(knobs): + box = Widget(self.win) + box.set_layout(BoxLayout(Orientation.Vertical, spacing=10)) + + rgb = [128 + randint(0, 127), randint(0, 127), 64] + shuffle(rgb) + knob = Knob(box, *knobspec) + knob.color = RGB(*rgb) + knob.value = round(uniform(knob.min_val, knob.max_val), 2) + self.knobs.append(knob) + self.entries.append(knob.create_value_entry(box)) + self.labels.append(Label(box, knob.label, font="sans", font_size=20)) + + self.draw_all() + self.set_visible(True) + self.perform_layout() + + def resize_event(self, size): + self.win.set_fixed_size(size) + self.win.set_size(size) + self.win.center() + self.win.perform_layout(self.nvg_context()) + super().resize_event(size) + return True + + def keyboard_event(self, key, scancode, action, modifiers): + if super().keyboard_event(key, scancode, action, modifiers): + return True + + if key == glfw.KEY_ESCAPE and action == glfw.PRESS: + self.set_visible(False) + return True + + return True + + def __del__(self): + # callbacks referencing other widgets cause memory leakage at cleanup + for entry in self.entries: + entry.set_callback(lambda x: True) + for entry in self.knobs: + entry.set_callback(lambda x: True) + + +if __name__ == "__main__": + import gc + + nanogui.init() + app = KnobsApp() + nanogui.mainloop(refresh=1 / 60.0 * 1000) + del app + gc.collect() + nanogui.shutdown() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1c3c393 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +nanogui==0.2.0