From d632ed3d75355b62b2a96f14f53ef737c3d8af61 Mon Sep 17 00:00:00 2001 From: "Hochsteger, Matthias" Date: Fri, 30 Jun 2023 10:11:01 +0200 Subject: [PATCH] Unified webgui.Draw() --- python/webgui.py | 414 +++++++++++++++++++++++------------------------ 1 file changed, 203 insertions(+), 211 deletions(-) diff --git a/python/webgui.py b/python/webgui.py index a093b635..9567fa84 100644 --- a/python/webgui.py +++ b/python/webgui.py @@ -3,248 +3,240 @@ import numpy as np from time import time import os +import webgui_jupyter_widgets from webgui_jupyter_widgets import BaseWebGuiScene, encodeData, WebGuiDocuWidget import webgui_jupyter_widgets.widget as wg +from packaging.version import parse + +if parse(webgui_jupyter_widgets.__version__) >= parse("0.2.18"): + _default_width = None + _default_height = None +else: + _default_width = "100%" + _default_height = "50vh" + + +_registered_draw_types = {} + + +def register_draw_type(*types): + def inner(func): + for typ in types: + _registered_draw_types[typ] = func + + return inner + + class WebGLScene(BaseWebGuiScene): - def __init__(self, mesh, clipping, on_init): - from IPython.display import display, Javascript - import threading - self.mesh = mesh - self.clipping = clipping - self.on_init = on_init + def __init__(self, obj, args=[], kwargs={}): + self.obj = obj + self.args = args + self.kwargs = kwargs + self.encoding = "b64" def GetData(self, set_minmax=True): - import json - # d = BuildRenderData(self.mesh, self.cf, self.order, draw_surf=self.draw_surf, draw_vol=self.draw_vol, deformation=self.deformation, region=self.region) - d = self.mesh._webgui_data() - bp = d['Bezier_trig_points'] - for i in range(len(bp)): - bp[i] = encodeData(np.array(bp[i], dtype=np.float32)) + self.kwargs["encoding"] = self.encoding + typ = type(self.obj) + d = None + if type(self.obj) in _registered_draw_types: + d = _registered_draw_types[typ](self.obj, self.args, self.kwargs) + else: + import inspect - ep = d['edges'] - for i in range(len(ep)): - ep[i] = encodeData(np.array(ep[i], dtype=np.float32)) + for t in inspect.getmro(typ): + if t in _registered_draw_types: + print("check type", t) + d = _registered_draw_types[t](self.obj, self.args, self.kwargs) + break + if d is None and hasattr(self.obj, "_webgui_data"): + d = self.obj._webgui_data() + bp = d["Bezier_trig_points"] + for i in range(len(bp)): + bp[i] = encodeData(np.array(bp[i], dtype=np.float32)) - if self.clipping is not None: - d['clipping'] = True - if isinstance(self.clipping, dict): + ep = d["edges"] + for i in range(len(ep)): + ep[i] = encodeData(np.array(ep[i], dtype=np.float32)) + + if d is None: + raise RuntimeError(f"Cannot draw object of type {typ}") + + args = self.args + kwargs = self.kwargs + if "clipping" in kwargs: + clipping = kwargs["clipping"] + d["clipping"] = True + if isinstance(clipping, dict): allowed_args = ("x", "y", "z", "dist", "function", "pnt", "vec") - if "vec" in self.clipping: - vec = self.clipping["vec"] - self.clipping["x"] = vec[0] - self.clipping["y"] = vec[1] - self.clipping["z"] = vec[2] - if "pnt" in self.clipping: - d['mesh_center'] = list(self.clipping["pnt"]) - for name, val in self.clipping.items(): + if "vec" in clipping: + vec = clipping["vec"] + clipping["x"] = vec[0] + clipping["y"] = vec[1] + clipping["z"] = vec[2] + if "pnt" in clipping: + d["mesh_center"] = list(clipping["pnt"]) + for name, val in clipping.items(): if not (name in allowed_args): - raise Exception('Only {} allowed as arguments for clipping!'.format(", ".join(allowed_args))) - d['clipping_' + name] = val + raise Exception( + "Only {} allowed as arguments for clipping!".format( + ", ".join(allowed_args) + ) + ) + d["clipping_" + name] = val - if self.on_init: - d['on_init'] = self.on_init + if "on_init" in kwargs: + d["on_init"] = kwargs["on_init"] + + if set_minmax: + if "min" in kwargs: + d["funcmin"] = kwargs["min"] + if "max" in kwargs: + d["funcmax"] = kwargs["max"] + d["autoscale"] = kwargs["autoscale"] + + if "vectors" in kwargs: + d["vectors"] = True + if isinstance(kwargs["vectors"], dict): + for name, val in kwargs["vectors"].items(): + if not (name in ("grid_size", "offset")): + raise Exception( + 'Only "grid_size" and "offset" allowed as arguments for vectors!' + ) + d["vectors_" + name] = val + + if "eval_function" in kwargs: + d["user_eval_function"] = kwargs["eval_function"] + + # see shaders/utils.h for value explanation (function_mode) + if "eval_" in kwargs: + eval_ = kwargs["eval"] + if isinstance(eval_, int): + d["eval"] = eval_ + elif eval_ == "norm": + d["eval"] = 3 + elif eval_ == "real": + d["eval"] = 5 + elif eval_ == "imag": + d["eval"] = 6 return d -bezier_trig_trafos = { } # cache trafos for different orders +bezier_trig_trafos = {} # cache trafos for different orders -def BuildRenderData(mesh, func, order=2, draw_surf=True, draw_vol=True, deformation=None, region=True): - d = {} - d['ngsolve_version'] = "Netgen" - d['mesh_dim'] = 3 # mesh.dim TODO +# def Draw(shape, clipping=None, js_code=None, filename=""): +# # todo: also handle occ geometry, list of shapes, etc. - d['order2d'] = 1 - d['order3d'] = 0 +# scene = WebGLScene(shape, clipping=clipping, on_init=js_code) - d['draw_vol'] = False - d['draw_surf'] = True - d['funcdim'] = 1 - - func2 = None - if func and func.is_complex: - d['is_complex'] = True - func1 = func[0].real - func2 = ngs.CoefficientFunction( (func[0].imag, 0.0) ) - elif func and func.dim>1: - func1 = func[0] - func2 = ngs.CoefficientFunction( tuple(func[i] if i we are just drawing a mesh, eval mesh element index instead - mats = mesh.GetMaterials() - bnds = mesh.GetBoundaries() - nmats = len(mesh.GetMaterials()) - nbnds = len(mesh.GetBoundaries()) - n = max(nmats, nbnds) - func1 = ngs.CoefficientFunction(list(range(n))) - n_regions = [0, 0, nmats, nbnds] - d['mesh_regions_2d'] = n_regions[mesh.dim] - d['mesh_regions_3d'] = nmats if mesh.dim==3 else 0 - d['funcdim'] = 0 - func1 = ngs.CoefficientFunction( (ngs.x, ngs.y, ngs.z, func1 ) ) - func0 = ngs.CoefficientFunction( (ngs.x, ngs.y, ngs.z, 0.0 ) ) - if deformation is not None: - func1 += ngs.CoefficientFunction((deformation, 0.0)) - func0 += ngs.CoefficientFunction((deformation, 0.0)) - - d['show_wireframe'] = False - d['show_mesh'] = True - if order2d>0: - og = order2d - d['show_wireframe'] = True - d['show_mesh'] = True - timer2.Start() - - timer3Bvals.Start() - - # transform point-values to Bernsteinbasis - def Binomial(n,i): return math.factorial(n) / math.factorial(i) / math.factorial(n-i) - def Bernstein(x, i, n): return Binomial(n,i) * x**i*(1-x)**(n-i) - Bvals = ngs.Matrix(og+1,og+1) - for i in range(og+1): - for j in range(og+1): - Bvals[i,j] = Bernstein(i/og, j, og) - iBvals = Bvals.I - timer3Bvals.Stop() - # print (Bvals) - # print (iBvals) - - - Bezier_points = [] - - ipts = [(i/og,0) for i in range(og+1)] + [(0, i/og) for i in range(og+1)] + [(i/og,1.0-i/og) for i in range(og+1)] - ir_trig = ngs.IntegrationRule(ipts, [0,]*len(ipts)) - ipts = [(i/og,0) for i in range(og+1)] + [(0, i/og) for i in range(og+1)] + [(i/og,1.0) for i in range(og+1)] + [(1.0, i/og) for i in range(og+1)] - ir_quad = ngs.IntegrationRule(ipts, [0,]*len(ipts)) - - vb = [ngs.VOL, ngs.BND][mesh.dim-2] - if region and region.VB() == vb: - vb = region - cf = func1 if draw_surf else func0 - timer2map.Start() - pts = mesh.MapToAllElements({ngs.ET.TRIG: ir_trig, ngs.ET.QUAD: ir_quad}, vb) - timer2map.Stop() - pmat = cf(pts) - - timermult.Start() - pmat = pmat.reshape(-1, og+1, 4) - if False: - BezierPnts = np.tensordot(iBvals.NumPy(), pmat, axes=(1,1)) - else: - BezierPnts = np.zeros( (og+1, pmat.shape[0], 4) ) - for i in range(4): - ngsmat = ngs.Matrix(pmat[:,:,i].transpose()) - BezierPnts[:,:,i] = iBvals * ngsmat - timermult.Stop() - - timer2list.Start() - for i in range(og+1): - Bezier_points.append(encodeData(BezierPnts[i], dtype=np.float32)) - timer2list.Stop() - - d['Bezier_points'] = Bezier_points - - ipts = [(i/og,0) for i in range(og+1)] - ir_seg = ngs.IntegrationRule(ipts, [0,]*len(ipts)) - vb = [ngs.VOL, ngs.BND, ngs.BBND][mesh.dim-1] - if region and region.VB() == vb: - vb = region - pts = mesh.MapToAllElements(ir_seg, vb) - pmat = func0(pts) - pmat = pmat.reshape(-1, og+1, 4) - edge_data = np.tensordot(iBvals.NumPy(), pmat, axes=(1,1)) - edges = [] - for i in range(og+1): - edges.append(encodeData(edge_data[i], dtype=np.float32)) - d['edges'] = edges - - ndtrig = int((og+1)*(og+2)/2) - - if og in bezier_trig_trafos.keys(): - iBvals_trig = bezier_trig_trafos[og] - else: - def BernsteinTrig(x, y, i, j, n): - return math.factorial(n)/math.factorial(i)/math.factorial(j)/math.factorial(n-i-j) \ - * x**i*y**j*(1-x-y)**(n-i-j) - Bvals = ngs.Matrix(ndtrig, ndtrig) - ii = 0 - for ix in range(og+1): - for iy in range(og+1-ix): - jj = 0 - for jx in range(og+1): - for jy in range(og+1-jx): - Bvals[ii,jj] = BernsteinTrig(ix/og, iy/og, jx, jy, og) - jj += 1 - ii += 1 - iBvals_trig = Bvals.I - bezier_trig_trafos[og] = iBvals_trig - - - # Bezier_points = [ [] for i in range(ndtrig) ] - Bezier_points = [] - - ipts = [(i/og,j/og) for j in range(og+1) for i in range(og+1-j)] - ir_trig = ngs.IntegrationRule(ipts, [0,]*len(ipts)) - ipts = ([(i/og,j/og) for j in range(og+1) for i in range(og+1-j)] + - [(1-i/og,1-j/og) for j in range(og+1) for i in range(og+1-j)]) - ir_quad = ngs.IntegrationRule(ipts, [0,]*len(ipts)) - - vb = [ngs.VOL, ngs.BND][mesh.dim-2] - if region and region.VB() == vb: - vb = region - pts = mesh.MapToAllElements({ngs.ET.TRIG: ir_trig, ngs.ET.QUAD: ir_quad}, vb) - - pmat = ngs.CoefficientFunction( func1 if draw_surf else func0 ) (pts) - - - funcmin = np.min(pmat[:,3]) - funcmax = np.max(pmat[:,3]) - pmin = np.min(pmat[:,0:3], axis=0) - pmax = np.max(pmat[:,0:3], axis=0) - - mesh_center = 0.5*(pmin+pmax) - mesh_radius = np.linalg.norm(pmax-pmin)/2 - - pmat = pmat.reshape(-1, len(ir_trig), 4) - - BezierPnts = np.tensordot(iBvals_trig.NumPy(), pmat, axes=(1,1)) - - for i in range(ndtrig): - Bezier_points.append(encodeData(BezierPnts[i], dtype=np.float32)) +# if wg._IN_IPYTHON: +# if wg._IN_GOOGLE_COLAB: +# from IPython.display import display, HTML +# html = scene.GenerateHTML() +# display(HTML(html)) +# else: +# # render scene using widgets.DOMWidget +# scene.Draw() +# return scene +# else: +# if filename: +# scene.GenerateHTML(filename=filename) +# return scene - d['Bezier_trig_points'] = Bezier_points - d['mesh_center'] = list(mesh_center) - d['mesh_radius'] = mesh_radius +def _get_draw_default_args(): + return dict( + name="function", + order=2, + draw_vol=True, + draw_surf=True, + autoscale=True, + deformation=False, + interpolate_multidim=False, + animate=False, + objects=[], + nodal_p1=False, + settings={}, + width=_default_width, + height=_default_height, + ) +def Draw(obj, *args, **kwargs): + kwargs_with_defaults = _get_draw_default_args() + kwargs_with_defaults.update(kwargs) - if func: - d['funcmin'] = funcmin - d['funcmax'] = funcmax - return d - -def Draw(shape, clipping=None, js_code=None, filename=""): - # todo: also handle occ geometry, list of shapes, etc. - - scene = WebGLScene(shape, clipping=clipping, on_init=js_code) - + scene = WebGLScene(obj, args, kwargs_with_defaults) if wg._IN_IPYTHON: if wg._IN_GOOGLE_COLAB: from IPython.display import display, HTML + html = scene.GenerateHTML() display(HTML(html)) else: + import webgui_jupyter_widgets as wjw + from packaging.version import parse + # render scene using widgets.DOMWidget - scene.Draw() + if parse(wjw.__version__) < parse("0.2.15"): + scene.Draw() + else: + scene.Draw( + kwargs_with_defaults["width"], kwargs_with_defaults["height"] + ) return scene else: - if filename: - scene.GenerateHTML(filename=filename) + if "filename" in kwargs_with_defaults: + scene.GenerateHTML(filename=kwargs_with_defaults["filename"]) return scene + +def _DrawDocu(obj, *args, **kwargs): + kwargs_with_defaults = _get_draw_default_args() + kwargs_with_defaults.update(kwargs) + scene = WebGLScene(obj, args, kwargs) + import json + + docu_path = os.environ["NETGEN_DOCUMENTATION_OUT_DIR"] + src_path = os.environ["NETGEN_DOCUMENTATION_SRC_DIR"] + cwd_path = os.path.abspath(".") + rel_path = os.path.relpath(".", src_path) + path = os.path.join(docu_path, rel_path) + + if not os.path.exists(path): + os.makedirs(path) + counter_file = os.path.join(docu_path, ".counter") + if os.path.exists(counter_file): + file_counter = int(open(counter_file, "r").read()) + 1 + else: + file_counter = 0 + + open(counter_file, "w").write(str(file_counter)) + + data_file = "render_data_{}.json".format(file_counter) + data_file_abs = os.path.join(path, data_file) + preview_file = "preview_{}.png".format(file_counter) + preview_file_abs = os.path.join(path, preview_file) + + widget = WebGuiDocuWidget() + widget.value = {"render_data": data_file, "preview": preview_file} + scene.widget = widget + data = scene.GetData() + json.dump(data, open(data_file_abs, "w")) + scene.MakeScreenshot(preview_file_abs, 1200, 600) + scene.Redraw = lambda: None + from IPython.display import display, HTML + + display(widget) + return scene + + +if "NETGEN_DOCUMENTATION_SRC_DIR" in os.environ: + # we are buiding the documentation, some things are handled differently: + # 1) Draw() is generating a .png (using headless chromium via selenium) and a render_data.json + # to show a preview image and load the render_data only when requested by user + # 2) return a NGSDocuWebGuiWidget instead of NGSWebGuiWidget implementing the preview/load on demand of webgui + + _Draw = Draw + Draw = _DrawDocu