diff --git a/CHANGELOG.md b/CHANGELOG.md index 637d4c1..9d049e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added envelope creation from parametric vaults, middle meshes, and bound meshes, with dedicated Rhino scene layers and drawing controls. +* Added TNO analysis for minimum and maximum thrust, minimum thickness, best fit, maximum applied load, and support displacement objectives. +* Added objective-specific optimisation constraints and variables, with SLSQP and IPOPT solver selection. +* Added assessment load workflows for envelope self-weight, external point loads, fill weight, and clearing all loads. +* Added 3D thrust-network drawing, editing, and explicit form/thrust vertex and edge selection on the `FormDiagram` scene object. +* Added envelope bounds and crack visualisation, force and reaction labels, load vectors, support displacement vectors, and configurable thrust faces, edges, vertices, and pipes. +* Added outward, inward, downward, and manual support displacement presets. +* Added direct template-pattern creation through `compas_tna`, including pattern-specific discretisation options. +* Added toolbar commands and icons for envelope creation, load assignment, TNO analysis, block export, thrust inspection, and session export. + ### Changed +* Changed the 3D thrust network to use the existing `FormDiagram` as the single source of equilibrium geometry and force data, while retaining compatibility with existing RhinoVAULT save files. +* Changed TNA vertical equilibrium, thrust editing, diagram export, and DEM block export to operate directly on the `FormDiagram`. +* Changed envelope-based analysis loads to remain fixed during equilibrium updates and added an explicit RV/TNO sign-convention bridge. +* Changed successful TNO analyses to update an existing `ForceDiagram` from the optimised form forces. +* Changed maximum-load analysis to collect initial loads on selected thrust-network vertices and display the optimised load vectors. +* Changed support-displacement analysis to collect and display displacement vectors on selected 3D supports. +* Changed thrust-load drawing to display the sum of `pz` and the optimised `pzext` contribution. +* Changed pointed-vault input validation so the rise is at least half of the larger span, and changed dome envelopes to use a fixed dense discretisation. +* Changed the minimum `compas_tno` requirement to `0.4.0`. +* Changed the Rhino 8 toolbar order and icons to expose the TNA, TNO, modification, and session workflows. + ### Removed +* Removed the separate `ThrustDiagram` data structure and `RhinoThrustObject` scene object. +* Removed the obsolete `RV_form_solve` command. +* Removed the local pattern-template implementations superseded by the `compas_tna` factories. + ## [0.9.5] 2025-07-04 diff --git a/commands/RV_dem_blocks.py b/commands/RV_dem_blocks.py index 08c3337..32906dd 100644 --- a/commands/RV_dem_blocks.py +++ b/commands/RV_dem_blocks.py @@ -15,14 +15,14 @@ def RunCommand(): session = RVSession() - thrust = session.find_thrustdiagram() - if not thrust: - print("There is no ThrustDiagram in the scene.") + form = session.find_formdiagram() + if not form: + print("There is no FormDiagram in the scene.") return option = rs.GetString(message="DEM Blocks From", strings=["Dual", "MeshPattern"]) - mesh: Mesh = thrust.diagram.copy() + mesh: Mesh = form.diagram.copy() for face in list(mesh.faces_where(_is_loaded=False)): mesh.delete_face(face) diff --git a/commands/RV_envelope.py b/commands/RV_envelope.py new file mode 100644 index 0000000..9d8e4f5 --- /dev/null +++ b/commands/RV_envelope.py @@ -0,0 +1,262 @@ +#! python3 +# venv: brg-csd +# r: compas_rv>=0.9.5 + +import rhinoscriptsyntax as rs # type: ignore + +import compas_rhino +import compas_rhino.conversions +import compas_rhino.objects +from compas.datastructures import Mesh +from compas_rv.session import RVSession +from compas_tna.envelope import BarrelVaultEnvelope +from compas_tna.envelope import CrossVaultEnvelope +from compas_tna.envelope import DomeEnvelope +from compas_tna.envelope import MeshEnvelope +from compas_tna.envelope import PavillionVaultEnvelope +from compas_tna.envelope import PointedVaultEnvelope + + +ENVELOPE_LAYER = "RhinoVAULT::Envelope" + + +def get_location(): + option = rs.GetString("Envelope location", "Origin", ["Origin", "Coordinates", "Point"]) + if not option: + return + if option == "Origin": + return 0.0, 0.0 + if option == "Coordinates": + x = rs.GetReal("X", 0.0) + if x is None: + return + y = rs.GetReal("Y", 0.0) + if y is None: + return + return x, y + point = rs.GetPoint("Point") + if not point: + return + return point[0], point[1] + + +def get_size(default_x=10.0, default_y=10.0): + x_size = rs.GetReal("X size", default_x, minimum=0.0) + if x_size is None: + return + y_size = rs.GetReal("Y size", default_y, minimum=0.0) + if y_size is None: + return + return x_size, y_size + + +def get_thickness(default=0.5): + return rs.GetReal("Thickness", default, minimum=0.0) + + +def get_crossvault(): + point = get_location() + if point is None: + return + size = get_size() + if size is None: + return + thickness = get_thickness() + if thickness is None: + return + return CrossVaultEnvelope(x_span=(point[0], point[0] + size[0]), y_span=(point[1], point[1] + size[1]), thickness=thickness) + + +def get_barrelvault(): + point = get_location() + if point is None: + return + span = rs.GetReal("Span", 10.0, minimum=0.0) + if span is None: + return + depth = rs.GetReal("Depth", 10.0, minimum=0.0) + if depth is None: + return + rise = rs.GetReal("Rise", 3.0, minimum=0.0) + if rise is None: + return + thickness = get_thickness() + if thickness is None: + return + return BarrelVaultEnvelope(rise=rise, span=span, x0=point[0], y_span=(point[1], point[1] + depth), thickness=thickness) + + +def get_pointedvault(): + point = get_location() + if point is None: + return + size = get_size() + if size is None: + return + minimum_rise = 0.5 * max(size) + message = "Rise must be greater than or equal to {0:.3f}".format(minimum_rise) + rise = rs.GetReal(message, minimum_rise, minimum=minimum_rise) + if rise is None: + return + thickness = get_thickness() + if thickness is None: + return + return PointedVaultEnvelope(x_span=(point[0], point[0] + size[0]), y_span=(point[1], point[1] + size[1]), thickness=thickness, hc=rise) + + +def get_pavilionvault(): + point = get_location() + if point is None: + return + size = get_size() + if size is None: + return + thickness = get_thickness() + if thickness is None: + return + angle = rs.GetReal("Springing angle", 45.0, minimum=0.0, maximum=90.0) + if angle is None: + return + return PavillionVaultEnvelope(x_span=(point[0], point[0] + size[0]), y_span=(point[1], point[1] + size[1]), thickness=thickness, spr_angle=angle) + + +def get_dome(): + center = get_location() + if center is None: + return + radius = rs.GetReal("Radius", 5.0, minimum=0.0) + if radius is None: + return + thickness = get_thickness() + if thickness is None: + return + r_oculus = rs.GetReal("Oculus radius", 0.5, minimum=0.0) + if r_oculus is None or r_oculus >= radius: + return + return DomeEnvelope(center=center, radius=radius, thickness=thickness, n_hoops=40, n_parallels=40, r_oculus=r_oculus) + + +def get_from_middle(): + guid = compas_rhino.objects.select_mesh("Select middle mesh") + if not guid: + return + obj = compas_rhino.objects.find_object(guid) + mesh = compas_rhino.conversions.mesh_to_compas(obj.Geometry, cls=Mesh) + thickness = get_thickness() + if thickness is None: + return + rs.HideObject(guid) + return MeshEnvelope.from_middle_mesh(mesh, thickness) + + +def get_from_bounds(): + guids = [] + + guid = compas_rhino.objects.select_mesh("Select intrados") + rs.UnselectAllObjects() + if not guid: + return + guids.append(guid) + obj = compas_rhino.objects.find_object(guid) + intrados = compas_rhino.conversions.mesh_to_compas(obj.Geometry, cls=Mesh) + + guid = compas_rhino.objects.select_mesh("Select extrados") + rs.UnselectAllObjects() + if not guid: + return + guids.append(guid) + obj = compas_rhino.objects.find_object(guid) + extrados = compas_rhino.conversions.mesh_to_compas(obj.Geometry, cls=Mesh) + + guid = compas_rhino.objects.select_mesh("Select middle (optional)") + rs.UnselectAllObjects() + if guid: + guids.append(guid) + obj = compas_rhino.objects.find_object(guid) + middle = compas_rhino.conversions.mesh_to_compas(obj.Geometry, cls=Mesh) + else: + middle = None + + guid = compas_rhino.objects.select_mesh("Select fill mesh (optional)") + rs.UnselectAllObjects() + if guid: + guids.append(guid) + obj = compas_rhino.objects.find_object(guid) + fill = compas_rhino.conversions.mesh_to_compas(obj.Geometry, cls=Mesh) + else: + fill = None + + rs.HideObjects(guids) + + envelope = MeshEnvelope.from_meshes(intrados, extrados, middle) + if fill: + envelope.fill = fill + return envelope + + +LIBRARY = { + "BarrelVault": get_barrelvault, + "CrossVault": get_crossvault, + "PointedVault": get_pointedvault, + "PavilionVault": get_pavilionvault, + "Dome": get_dome, +} + + +def RunCommand(): + session = RVSession() + + option = rs.GetString("Envelope from", "FromLibrary", ["FromLibrary", "FromMiddle", "FromBounds"]) + if not option: + return + + if option == "FromLibrary": + pattern = rs.GetString("Envelope pattern", "BarrelVault", list(LIBRARY.keys())) + if not pattern: + return + envelope = LIBRARY[pattern]() + elif option == "FromMiddle": + envelope = get_from_middle() + elif option == "FromBounds": + envelope = get_from_bounds() + else: + return + + if not envelope: + return session.warn("Error creating Envelope. Try again.") + + rho = rs.GetInteger("Density masonry (rho)", int(envelope.rho), minimum=0, maximum=200) + if rho is None: + return + envelope.rho = rho + + if envelope.fill: + rho_fill = rs.GetInteger("Density masonry fill (rho_fill)", int(envelope.rho_fill), minimum=0, maximum=200) + if rho_fill is None: + return + envelope.rho_fill = rho_fill + + session.clear_envelope(redraw=False) + session["envelope"] = envelope + + settings = session.settings.envelope + if envelope.intrados: + session.scene.add(envelope.intrados, disjoint=True, show=settings.show_intrados, name="Intrados", layer=ENVELOPE_LAYER) + if envelope.middle: + session.scene.add(envelope.middle, disjoint=True, show=settings.show_middle, name="Middle", layer=ENVELOPE_LAYER) + if envelope.extrados: + session.scene.add(envelope.extrados, disjoint=True, show=settings.show_extrados, name="Extrados", layer=ENVELOPE_LAYER) + if envelope.fill: + session.scene.add(envelope.fill, disjoint=True, show=settings.show_fill, name="Fill", layer=ENVELOPE_LAYER) + + session.scene.redraw() + rs.Redraw() + + print("Envelope successfully created.") + + if session.settings.autosave: + session.record(name="TNO Envelope") + + +if __name__ == "__main__": + RunCommand() diff --git a/commands/RV_form.py b/commands/RV_form.py index 643a268..e78752a 100644 --- a/commands/RV_form.py +++ b/commands/RV_form.py @@ -5,7 +5,6 @@ import rhinoscriptsyntax as rs # type: ignore from compas_rv.datastructures import FormDiagram -from compas_rv.datastructures import ThrustDiagram from compas_rv.session import RVSession @@ -40,11 +39,8 @@ def RunCommand(): formdiagram.vertices_attribute(name="z", value=0) formdiagram.flip_cycles_if_normal_down() - thrustdiagram: ThrustDiagram = formdiagram.copy(cls=ThrustDiagram) - thrustdiagram.name = "ThrustDiagram" - - # set an initial value for zmax - session.settings.tna.vertical_zmax = thrustdiagram.compute_zmax() + # set an initial value for zmax using the form diagram + session.settings.tna.vertical_zmax = formdiagram.compute_zmax() # ============================================================================= # Update scene @@ -55,7 +51,6 @@ def RunCommand(): pattern.show = False session.scene.add(formdiagram, name=formdiagram.name, layer="RhinoVAULT::FormDiagram") # type: ignore - session.scene.add(thrustdiagram, name=thrustdiagram.name, show=False, layer="RhinoVAULT::ThrustDiagram") # type: ignore session.scene.redraw() rs.Redraw() diff --git a/commands/RV_form_modify.py b/commands/RV_form_modify.py index 3e30dae..4f16cd5 100644 --- a/commands/RV_form_modify.py +++ b/commands/RV_form_modify.py @@ -17,7 +17,6 @@ def RunCommand(): return force = session.find_forcediagram(warn=False) - thrust = session.find_thrustdiagram(warn=False) RECREATE_FORCE = False @@ -42,14 +41,9 @@ def RunCommand(): if not action: return - if thrust: - thrust.show_vertices = False # type: ignore - thrust.redraw_vertices() - if action == "Add": - form.show_vertices = list(form.diagram.vertices_where(is_support=False, is_vertex_internal=True)) - form.redraw_vertices() - selected = form.select_vertices() + vertices = list(form.diagram.vertices_where(is_support=False, is_vertex_internal=True)) + selected = form.select_form_vertices(vertices=vertices) if selected: form.diagram.vertices_attribute(name="is_support", value=True, keys=selected) @@ -60,9 +54,7 @@ def RunCommand(): if not selectable: return session.warn("There are no internal supports.") - form.show_vertices = selectable - form.redraw_vertices() - selected = form.select_vertices() + selected = form.select_form_vertices(vertices=selectable) if selected: form.diagram.vertices_attribute(name="is_support", value=False, keys=selected) @@ -72,14 +64,9 @@ def RunCommand(): # they can't be removed or added # movement on the formdiagram is only permitted in XY - elif option == "BoundarySupports": - if thrust: - thrust.show_vertices = False # type: ignore - thrust.redraw_vertices() - - form.show_vertices = list(form.diagram.vertices_where(is_support=True, is_vertex_internal=False)) - form.redraw_vertices() - selected = form.select_vertices() + elif option == "Supports": + vertices = list(form.diagram.vertices_where(is_support=True, is_vertex_internal=False)) + selected = form.select_form_vertices(vertices=vertices) if selected: directions = ["X", "Y", "XY"] @@ -93,9 +80,10 @@ def RunCommand(): # positive values are in the negative z-direction elif option == "Loads": - form.show_vertices = list(form.diagram.vertices_where(is_support=False)) - form.redraw_vertices() - selected = form.select_vertices() + vertices = list(form.diagram.vertices_where(is_support=False)) + selected = form.select_form_vertices(vertices=vertices) + + print("Selected vertices: {}".format(selected)) if selected: form.update_vertex_attributes(selected, names=["pz", "t"]) @@ -112,16 +100,12 @@ def RunCommand(): return if action == "DeleteFaces": - form.show_faces = list(form.diagram.faces_where(_is_loaded=True)) - form.redraw_faces() - selected = form.select_faces_manual() + faces = list(form.diagram.faces_where(_is_loaded=True)) + selected = form.select_form_faces(faces=faces) if selected: for face in selected: if form.diagram.has_face(face): form.diagram.delete_face(face) - if thrust: - if thrust.diagram.has_face(face): - thrust.diagram.delete_face(face) if force: RECREATE_FORCE = True @@ -133,9 +117,8 @@ def RunCommand(): # min/max dual length elif option == "EdgeConstraints": - form.show_edges = list(form.diagram.edges_where(_is_edge=True)) - form.redraw_edges() - selected = form.select_edges() + edges = list(form.diagram.edges_where(_is_edge=True)) + selected = form.select_form_edges(edges=edges) if selected: form.update_edge_attributes(selected, names=["lmin", "lmax", "hmin", "hmax"]) @@ -152,9 +135,6 @@ def RunCommand(): if RECREATE_FORCE: form.diagram.update_boundaries() - if thrust: - thrust.diagram.update_boundaries() - forcediagram: ForceDiagram = ForceDiagram.from_formdiagram(form.diagram) forcediagram.update_position() diff --git a/commands/RV_form_solve.py b/commands/RV_form_solve.py deleted file mode 100644 index f77ac18..0000000 --- a/commands/RV_form_solve.py +++ /dev/null @@ -1,48 +0,0 @@ -#! python3 -# venv: brg-csd -# r: compas_rv>=0.9.5 - -import rhinoscriptsyntax as rs # type: ignore - -from compas_rv.session import RVSession - - -def RunCommand(): - session = RVSession() - - form = session.find_formdiagram() - if not form: - return - - # ============================================================================= - # Pattern relax - # ============================================================================= - - rs.UnselectAllObjects() - - form.diagram.solve_fd() - - # ============================================================================= - # Update scene - # ============================================================================= - - rs.UnselectAllObjects() - - form.show_vertices = True - form.show_free = False - form.show_fixed = True - form.show_supports = True - form.show_edges = True - - form.redraw() - - if session.settings.autosave: - session.record(name="Relax the FormDiagram") - - -# ============================================================================= -# Run as main -# ============================================================================= - -if __name__ == "__main__": - RunCommand() diff --git a/commands/RV_loads.py b/commands/RV_loads.py new file mode 100644 index 0000000..6818592 --- /dev/null +++ b/commands/RV_loads.py @@ -0,0 +1,109 @@ +#! python3 +# venv: brg-csd +# r: compas_rv>=0.9.5 + +import rhinoscriptsyntax as rs # type: ignore + +from compas_rv.session import RVSession + + +def invert_vertical_loads(formdiagram): + for vertex in formdiagram.vertices(): + pz = formdiagram.vertex_attribute(vertex, "pz") + formdiagram.vertex_attribute(vertex, "pz", -pz if pz is not None else 0.0) + + +def clear_optimised_loads(formdiagram): + for vertex in formdiagram.vertices(): + formdiagram.unset_vertex_attribute(vertex, "pzext") + + +def select_loaded_vertices(formobject): + candidates = list(formobject.diagram.vertices_where(is_support=False)) + return formobject.select_thrust_vertices( + vertices=candidates, + message="Select vertices for external loads", + use_edges=False, + ) + + +def RunCommand(): + session = RVSession() + + formobject = session.find_formdiagram() + if not formobject: + return + + option = rs.GetString("Load source", "FromEnvelope", ["FromEnvelope", "External", "FromFill", "ClearAll"]) + if not option: + return + + formdiagram = formobject.diagram + envelope = None + + if option in ("FromEnvelope", "FromFill"): + envelope = session.find_envelope() + if not envelope: + return + + if option == "FromEnvelope": + normalize = rs.GetString("Normalize loads to envelope self-weight", "Yes", ["Yes", "No"]) + if not normalize: + return + + envelope.apply_selfweight_to_formdiagram(formdiagram, normalize=normalize == "Yes") + invert_vertical_loads(formdiagram) + clear_optimised_loads(formdiagram) + print("Self-weight from the envelope applied to the FormDiagram.") + + elif option == "External": + vertices = select_loaded_vertices(formobject) + if not vertices: + return session.warn("Select at least one non-support vertex.") + + load = rs.GetReal("External vertical load (positive downward)", 1.0, minimum=0.0) + if load is None: + return + + for vertex in vertices: + pz = formdiagram.vertex_attribute(vertex, "pz") or 0.0 + formdiagram.vertex_attribute(vertex, "pz", pz + load) + print("Load at vertex {0} updated from {1:.2f} to {2:.2f}".format(vertex, pz, pz + load)) + clear_optimised_loads(formdiagram) + + elif option == "FromFill": + if not envelope.fill: + return session.warn("There is no Fill mesh. Re-create the envelope with a fill mesh.") + + invert_vertical_loads(formdiagram) + try: + envelope.apply_fill_weight_to_formdiagram(formdiagram) + finally: + invert_vertical_loads(formdiagram) + clear_optimised_loads(formdiagram) + print("Fill weight applied to the FormDiagram.") + + elif option == "ClearAll": + formdiagram.vertices_attribute(name="pz", value=0.0) + clear_optimised_loads(formdiagram) + formdiagram.attributes["loads_from_envelope"] = False + print("All vertical loads cleared from the FormDiagram.") + + else: + raise NotImplementedError + + if option != "ClearAll": + formdiagram.attributes["loads_from_envelope"] = True + formobject.show_thrust = True + session.settings.drawing.show_loads = True + + rs.UnselectAllObjects() + formobject.redraw() + rs.Redraw() + + if session.settings.autosave: + session.record(name="Update Loads") + + +if __name__ == "__main__": + RunCommand() diff --git a/commands/RV_pattern.py b/commands/RV_pattern.py index ca17462..8be3777 100644 --- a/commands/RV_pattern.py +++ b/commands/RV_pattern.py @@ -10,12 +10,150 @@ from compas_rv.commands import make_pattern_from_rhinosurface from compas_rv.commands import make_pattern_from_skeleton from compas_rv.commands import make_pattern_from_triangulation -from compas_rv.patterns.circular import create_circular_radial_pattern -from compas_rv.patterns.circular import create_circular_radial_spaced_pattern -from compas_rv.patterns.circular import create_circular_spiral_pattern -from compas_rv.patterns.rectangular import create_cross_pattern -from compas_rv.patterns.rectangular import create_fan_pattern +from compas_rv.datastructures import Pattern from compas_rv.session import RVSession +from compas_tna.diagrams.diagram_circular import create_circular_radial_mesh +from compas_tna.diagrams.diagram_circular import create_circular_radial_spaced_mesh +from compas_tna.diagrams.diagram_circular import create_circular_spiral_mesh +from compas_tna.diagrams.diagram_rectangular import create_cross_mesh +from compas_tna.diagrams.diagram_rectangular import create_fan_mesh +from compas_tna.diagrams.diagram_rectangular import create_ortho_mesh +from compas_tna.diagrams.diagram_rectangular import create_parametric_fan_mesh + + +def get_location(): + option = rs.GetString("Pattern location", "Origin", ["Origin", "Coordinates", "Point"]) + if not option: + return + if option == "Origin": + return 0.0, 0.0 + if option == "Coordinates": + x = rs.GetReal("X", 0.0) + if x is None: + return + y = rs.GetReal("Y", 0.0) + if y is None: + return + return x, y + point = rs.GetPoint("Point") + if not point: + return + return point[0], point[1] + + +def get_size(): + x_size = rs.GetReal("X size", 10.0, minimum=0.0) + if x_size is None: + return + y_size = rs.GetReal("Y size", 10.0, minimum=0.0) + if y_size is None: + return + return x_size, y_size + + +def get_circular_pattern(factory): + center = get_location() + if center is None: + return + radius = rs.GetReal("Radius", 5.0, minimum=0.0) + if radius is None: + return + n_hoops = rs.GetInteger("Hoops", 12, minimum=4) + if n_hoops is None: + return + n_parallels = rs.GetInteger("Radials", 24, minimum=12) + if n_parallels is None: + return + r_oculus = rs.GetReal("Oculus radius", 0.0, minimum=0.0) + if r_oculus is None: + return + if r_oculus >= radius: + rs.MessageBox("The oculus radius should be smaller than the pattern radius.", title="Warning") + return + return factory(center=center, radius=radius, n_hoops=n_hoops, n_parallels=n_parallels, r_oculus=r_oculus).copy(cls=Pattern) + + +def get_cross_pattern(): + point = get_location() + if point is None: + return + size = get_size() + if size is None: + return + resolution = rs.GetInteger("Resolution", 10, minimum=1) + if resolution is None: + return + return create_cross_mesh( + x_span=(point[0], point[0] + size[0]), + y_span=(point[1], point[1] + size[1]), + n=resolution, + ).copy(cls=Pattern) + + +def get_fan_pattern(): + point = get_location() + if point is None: + return + size = get_size() + if size is None: + return + n_fans = rs.GetInteger("Fans", 10, minimum=2) + if n_fans is None: + return + n_hoops = rs.GetInteger("Hoops", 10, minimum=2) + if n_hoops is None: + return + if n_fans % 2 or n_hoops % 2: + rs.MessageBox("The fan and hoop discretisation should be even.", title="Warning") + return + return create_fan_mesh( + x_span=(point[0], point[0] + size[0]), + y_span=(point[1], point[1] + size[1]), + n_fans=n_fans, + n_hoops=n_hoops, + ).copy(cls=Pattern) + + +def get_ortho_pattern(): + point = get_location() + if point is None: + return + size = get_size() + if size is None: + return + nx = rs.GetInteger("X faces", 10, minimum=2) + if nx is None: + return + ny = rs.GetInteger("Y faces", nx, minimum=2) + if ny is None: + return + return create_ortho_mesh( + x_span=(point[0], point[0] + size[0]), + y_span=(point[1], point[1] + size[1]), + nx=nx, + ny=ny, + ).copy(cls=Pattern) + + +def get_parametric_pattern(): + point = get_location() + if point is None: + return + size = get_size() + if size is None: + return + resolution = rs.GetInteger("Resolution", 10, minimum=2) + if resolution is None: + return + inclination = rs.GetReal("Lambda inclination", 0.5, minimum=0.0, maximum=1.0) + if inclination is None: + return + return create_parametric_fan_mesh( + x_span=(point[0], point[0] + size[0]), + y_span=(point[1], point[1] + size[1]), + n=resolution, + lambd=inclination, + ).copy(cls=Pattern) def RunCommand(): @@ -33,7 +171,7 @@ def RunCommand(): if not session.confirm("This will remove all current RhinoVAULT data and objects. Do you wish to proceed?"): return - session.scene.clear() + session.clear() # ============================================================================= # Make a Force "Pattern" @@ -83,30 +221,41 @@ def RunCommand(): "Spiral", "Cross", "Fan", + "Ortho", + "Parametric", ], ) if option2 == "Radial": - pattern = create_circular_radial_pattern() + pattern = get_circular_pattern(create_circular_radial_mesh) elif option2 == "RadialSpaced": - pattern = create_circular_radial_spaced_pattern() + pattern = get_circular_pattern(create_circular_radial_spaced_mesh) elif option2 == "Spiral": - pattern = create_circular_spiral_pattern() + pattern = get_circular_pattern(create_circular_spiral_mesh) elif option2 == "Cross": - pattern = create_cross_pattern() + pattern = get_cross_pattern() elif option2 == "Fan": - pattern = create_fan_pattern() + pattern = get_fan_pattern() + + elif option2 == "Ortho": + pattern = get_ortho_pattern() + + elif option2 == "Parametric": + pattern = get_parametric_pattern() else: - raise NotImplementedError + return else: return + if not pattern: + return + # ============================================================================= # Update scene # ============================================================================= diff --git a/commands/RV_scene_clear.py b/commands/RV_scene_clear.py index e2b2298..6919fe9 100644 --- a/commands/RV_scene_clear.py +++ b/commands/RV_scene_clear.py @@ -9,7 +9,7 @@ def RunCommand(): session = RVSession() if session.confirm("Note that this will remove all RhinoVAULT data and objects. Do you wish to proceed?"): - session.scene.clear() + session.clear() if session.settings.autosave: session.record(name="Clear") diff --git a/commands/RV_session_export.py b/commands/RV_session_export.py index bfabbcd..5fe4e43 100644 --- a/commands/RV_session_export.py +++ b/commands/RV_session_export.py @@ -48,6 +48,7 @@ def RunCommand(): return mesh: Mesh = form.diagram.copy() + mesh.vertices_attribute(name="z", value=0) for face in list(mesh.faces_where(_is_loaded=False)): mesh.delete_face(face) @@ -74,11 +75,11 @@ def RunCommand(): compas.json_dump(mesh, filepath) elif option == "ThrustDiagram": - thrust = session.find_thrustdiagram() - if not thrust: + form = session.find_formdiagram() + if not form: return - mesh: Mesh = thrust.diagram.copy() + mesh: Mesh = form.diagram.copy() for face in list(mesh.faces_where(_is_loaded=False)): mesh.delete_face(face) diff --git a/commands/RV_session_open.py b/commands/RV_session_open.py index 4440658..b71ed83 100644 --- a/commands/RV_session_open.py +++ b/commands/RV_session_open.py @@ -16,13 +16,12 @@ def RunCommand(): if not filepath: return - session.scene.clear() + session.clear() session.load(filepath) pattern = session.find_pattern(warn=False) form = session.find_formdiagram(warn=False) force = session.find_forcediagram(warn=False) - thrust = session.find_thrustdiagram(warn=False) if pattern: pattern.layer = "RhinoVAULT::Pattern" @@ -33,9 +32,6 @@ def RunCommand(): if force: force.layer = "RhinoVAULT::ForceDiagram" - if thrust: - thrust.layer = "RhinoVAULT::ThrustDiagram" - if form and force: form.diagram.dual = force.diagram force.diagram.primal = form.diagram diff --git a/commands/RV_session_redo.py b/commands/RV_session_redo.py index 4321f61..33edb31 100644 --- a/commands/RV_session_redo.py +++ b/commands/RV_session_redo.py @@ -16,7 +16,6 @@ def RunCommand(): pattern = session.find_pattern(warn=False) form = session.find_formdiagram(warn=False) force = session.find_forcediagram(warn=False) - thrust = session.find_thrustdiagram(warn=False) if pattern: pattern.layer = "RhinoVAULT::Pattern" @@ -27,9 +26,6 @@ def RunCommand(): if force: force.layer = "RhinoVAULT::ForceDiagram" - if thrust: - thrust.layer = "RhinoVAULT::ThrustDiagram" - if form and force: form.diagram.dual = force.diagram force.diagram.primal = form.diagram diff --git a/commands/RV_session_undo.py b/commands/RV_session_undo.py index 9ec6fcf..00c1ecf 100644 --- a/commands/RV_session_undo.py +++ b/commands/RV_session_undo.py @@ -16,7 +16,6 @@ def RunCommand(): pattern = session.find_pattern(warn=False) form = session.find_formdiagram(warn=False) force = session.find_forcediagram(warn=False) - thrust = session.find_thrustdiagram(warn=False) if pattern: pattern.layer = "RhinoVAULT::Pattern" @@ -27,9 +26,6 @@ def RunCommand(): if force: force.layer = "RhinoVAULT::ForceDiagram" - if thrust: - thrust.layer = "RhinoVAULT::ThrustDiagram" - if form and force: form.diagram.dual = force.diagram force.diagram.primal = form.diagram diff --git a/commands/RV_settings.py b/commands/RV_settings.py index 91edc62..1417daa 100644 --- a/commands/RV_settings.py +++ b/commands/RV_settings.py @@ -33,7 +33,7 @@ def update_settings(model, title): def RunCommand(): session = RVSession() - options = ["RhinoVault", "ThrustNetworkAnalysis", "Drawing"] + options = ["RhinoVault", "ThrustNetworkAnalysis", "ThrustNetworkOptimisation", "Envelope", "Drawing"] while True: option = rs.GetString(message="Choose a settings section, or escape/cancel to exit.", strings=options) @@ -46,6 +46,12 @@ def RunCommand(): elif option == "ThrustNetworkAnalysis": update_settings(session.settings.tna, title=option) + elif option == "ThrustNetworkOptimisation": + update_settings(session.settings.tno, title=option) + + elif option == "Envelope": + update_settings(session.settings.envelope, title=option) + elif option == "Drawing": update_settings(session.settings.drawing, title=option) diff --git a/commands/RV_thrust_info.py b/commands/RV_thrust_info.py index 26cca86..ba12f5b 100644 --- a/commands/RV_thrust_info.py +++ b/commands/RV_thrust_info.py @@ -9,20 +9,20 @@ def RunCommand(): session = RVSession() - thrust = session.find_thrustdiagram() - if not thrust: - print("There is no ThrustDiagram in the scene.") + form = session.find_formdiagram() + if not form: + print("There is no FormDiagram in the scene.") return - form = MeshInfoForm( - thrust.diagram, + form_info = MeshInfoForm( + form.diagram, vertex_attr_names=["x", "y", "z", "px", "py", "pz", "is_support", "_rx", "_ry", "_rz"], edge_attr_names=["q"], face_attr_names=["_is_loaded"], - title="Thrust Diagram Info", + title="Form Diagram Info (3D Thrust Surface)", ) - form.show() + form_info.show() # ============================================================================= diff --git a/commands/RV_thrust_modify.py b/commands/RV_thrust_modify.py index e47543a..6855f2f 100644 --- a/commands/RV_thrust_modify.py +++ b/commands/RV_thrust_modify.py @@ -22,11 +22,6 @@ def RunCommand(): print("There is no ForceDiagram in the scene.") return - thrust = session.find_thrustdiagram() - if not thrust: - print("There is no ThrustDiagram in the scene.") - return - # ============================================================================= # Modify pattern vertices # ============================================================================= @@ -42,60 +37,43 @@ def RunCommand(): return if option == "VertexAttributes": - thrust.show_vertices = list(thrust.diagram.vertices()) - thrust.redraw_vertices() - selected = thrust.select_vertices() + vertices = list(form.diagram.vertices()) + selected = form.select_thrust_vertices(vertices=vertices) if selected: - thrust.update_vertex_attributes(selected) + form.update_vertex_attributes(selected) elif option == "EdgeAttributes": - thrust.show_edges = list(thrust.diagram.edges_where(_is_edge=True)) - thrust.redraw_edges() - selected = thrust.select_edges() + edges = list(form.diagram.edges_where(_is_edge=True)) + selected = form.select_thrust_edges(edges=edges) if selected: - thrust.update_edge_attributes(selected) + form.update_edge_attributes(selected) elif option == "MoveSupports": - form.show_vertices = False - form.redraw_vertices() - thrust.show_vertices = list(thrust.diagram.vertices_where(is_support=True)) - thrust.redraw_vertices() - selected = thrust.select_vertices() + vertices = list(form.diagram.vertices_where(is_support=True)) + selected = form.select_thrust_vertices(vertices=vertices) if selected: - thrust.move_vertices_direction(selected, direction="Z") + form.move_vertices_direction(selected, direction="Z") elif option == "ScaleForceDensities": - thrust.show_edges = list(thrust.diagram.edges_where(_is_edge=True)) - thrust.redraw_edges() - selected = thrust.select_edges() + edges = list(form.diagram.edges_where(_is_edge=True)) + selected = form.select_thrust_edges(edges=edges) if selected: selected = list(set(selected)) factor = rs.GetReal("Scale factor", number=1.0, minimum=0) if not factor: return for edge in selected: - q = factor * thrust.diagram.edge_attribute(edge, "q") + q = factor * form.diagram.edge_attribute(edge, "q") form.diagram.edge_attribute(edge, "q", q) form.diagram.solve_fd() update_force_from_form(force.diagram, form.diagram) - _, scale = vertical_from_zmax(form.diagram, zmax, kmax=kmax) + density = 0.0 if form.diagram.attributes.get("loads_from_envelope") else 1.0 + _, scale = vertical_from_zmax(form.diagram, zmax, kmax=kmax, density=density) force.diagram.attributes["scale"] = scale force.diagram.update_position() - for vertex in form.diagram.vertices(): - form_attr = form.diagram.vertex_attributes(vertex) - thrust_attr = thrust.diagram.vertex_attributes(vertex) - thrust_attr.update(form_attr) # type: ignore - - for edge in form.diagram.edges(): - form_attr = form.diagram.edge_attributes(edge) - thrust_attr = thrust.diagram.edge_attributes(edge) - thrust_attr.update(form_attr) # type: ignore - - form.diagram.vertices_attribute(name="z", value=0) - else: raise NotImplementedError @@ -120,11 +98,17 @@ def RunCommand(): force.show_supports = True force.show_edges = True - thrust.show_vertices = True # type: ignore - thrust.show_free = False - thrust.show_fixed = True - thrust.show_supports = True - thrust.show_edges = False + form.show_thrust = True + session.settings.drawing.show_thrust_vertices = True + session.settings.drawing.show_thrust_free = False + session.settings.drawing.show_thrust_fixed = True + session.settings.drawing.show_thrust_supports = True + session.settings.drawing.show_thrust_edges = False + session.settings.drawing.show_thrust_faces = True + session.settings.drawing.show_reactions = True + session.settings.drawing.show_pipes = False + session.settings.drawing.show_force_labels = False + session.settings.drawing.show_reaction_labels = False session.scene.redraw() diff --git a/commands/RV_tna_vertical.py b/commands/RV_tna_vertical.py index 9e12cee..c5987b4 100644 --- a/commands/RV_tna_vertical.py +++ b/commands/RV_tna_vertical.py @@ -22,13 +22,8 @@ def RunCommand(): print("There is no ForceDiagram in the scene.") return - thrust = session.find_thrustdiagram() - if not thrust: - print("There is no ThrustDiagram in the scene.") - return - # ============================================================================= - # Compute horizontal + # Compute vertical # ============================================================================= kmax = session.settings.tna.vertical_kmax @@ -40,34 +35,14 @@ def RunCommand(): session.settings.tna.vertical_zmax = zmax - # copy the vertical coordinates of the thrust diagram onto the form diagram - for vertex in thrust.diagram.vertices_where(is_support=True): - z = thrust.diagram.vertex_attribute(vertex, "z") - form.diagram.vertex_attribute(vertex, "z", z) - - _, scale = vertical_from_zmax(form.diagram, zmax, kmax=kmax) - - if not _: # this makes no sense - print("Vertical equilibrium failed!") - return + # Compute vertical equilibrium directly on the form diagram + density = 0.0 if form.diagram.attributes.get("loads_from_envelope") else 1.0 + _, scale = vertical_from_zmax(form.diagram, zmax, kmax=kmax, density=density) force.diagram.attributes["scale"] = scale - for vertex in form.diagram.vertices(): - form_attr = form.diagram.vertex_attributes(vertex) - thrust_attr = thrust.diagram.vertex_attributes(vertex) - thrust_attr.update(form_attr) # type: ignore - - for edge in form.diagram.edges(): - form_attr = form.diagram.edge_attributes(edge) - thrust_attr = thrust.diagram.edge_attributes(edge) - thrust_attr.update(form_attr) # type: ignore - - # flatten the formdiagram again - form.diagram.vertices_attribute(name="z", value=0) - - # show the thrust diagram - thrust.show = True + # Enable 3D mode for the form diagram to show the thrust surface + form.show_thrust = True # ============================================================================= # Update scene @@ -75,10 +50,11 @@ def RunCommand(): rs.UnselectAllObjects() - thrust.redraw() + # Redraw the form diagram to show the updated 3D geometry + form.redraw() print("Vertical equilibrium found!") - print("ThrustDiagram object successfully created with target height of {}.".format(zmax)) + print("FormDiagram object updated with target height of {}.".format(zmax)) if session.settings.autosave: session.record(name="TNA Vertical") diff --git a/commands/RV_tno_analysis.py b/commands/RV_tno_analysis.py new file mode 100644 index 0000000..f030bd5 --- /dev/null +++ b/commands/RV_tno_analysis.py @@ -0,0 +1,323 @@ +#! python3 +# venv: brg-csd +# r: compas_rv>=0.9.5 + +import numpy as np +import rhinoscriptsyntax as rs # type: ignore +from compas_tno.analysis import Analysis + +from compas_rui.forms import NamedValuesForm +from compas_rv.conventions import invert_formdiagram_signs +from compas_rv.session import RVSession +from compas_rv.solvers import update_force_from_form +from compas_tna.envelope import MeshEnvelope + +OBJECTIVES = [ + "MinimumThrust", + "MaximumThrust", + "MinimumThickness", + "Bestfit", + "MaximumLoad", + "SupportDisplacement", +] + + +def get_optimisation_options(objective): + constraint_options = [ + ("Funicular", True), + ("Envelope", objective != "Bestfit"), + ("ReactionDirections", False), + ] + constraint_options = rs.CheckListBox(constraint_options, "Validate the constraints of the optimisation.", "TNO Constraints") + if constraint_options is None: + return + + variable_options = [] + if objective == "MinimumThickness": + variable_options.append(("Thickness", True)) + variable_options.extend( + [ + ("ForceDensities", True), + ("SupportHeights", True), + ] + ) + if objective == "MaximumLoad": + variable_options.append(("LoadMultiplier", True)) + variable_options = rs.CheckListBox(variable_options, "Validate the variables of the optimisation.", "TNO Variables") + if variable_options is None: + return + + constraint_names = { + "Funicular": "funicular", + "Envelope": "envelope", + "ReactionDirections": "reac_bounds", + } + variable_names = { + "Thickness": "t", + "ForceDensities": "q", + "SupportHeights": "zb", + "LoadMultiplier": "lambdv", + } + + constraints = [constraint_names[name] for name, checked in constraint_options if checked] + variables = [variable_names[name] for name, checked in variable_options if checked] + + return constraints, variables + + +def get_load_direction(formobject): + n = formobject.diagram.number_of_vertices() + load_direction = np.zeros((n, 1)) + index_vertex = formobject.diagram.index_vertex() + candidates = list(formobject.diagram.vertices_where(is_support=False)) + assigned = False + + while True: + vertices = formobject.select_thrust_vertices( + vertices=candidates, + message="Select vertices for maximum applied load", + use_edges=False, + ) + if not vertices: + break + + force = rs.GetReal("Initial vertical load p0 (negative downward)", -10.0) + if force is None: + return + + for vertex in vertices: + load_direction[index_vertex[vertex]] = force + assigned = True + + rs.UnselectAllObjects() + more = rs.GetString("Apply initial loads on additional vertices", "No", ["No", "Yes"]) + if more != "Yes": + break + + if not assigned: + formobject.session.warn("Select at least one vertex and assign an initial load.") + return + + return load_direction + + +def get_displacement_vector(defaults): + form = NamedValuesForm(["Ux", "Uy", "Uz"], defaults, title="Support Displacement", width=350, height=180) + if not form.show(): + return + + try: + return [float(form.attributes[name]) for name in ("Ux", "Uy", "Uz")] + except (TypeError, ValueError): + rs.MessageBox("Ux, Uy and Uz must be numbers.", title="Invalid Support Displacement") + + +DISPLACEMENT_DIRECTIONS = ["Outward", "Inward", "Downward", "Manual"] + + +def get_displacement_values(formobject, vertices, manual_defaults, magnitude_default): + """Prompt for the displacement to apply to a batch of selected supports. + + Returns + ------- + tuple[dict[int, list[float]], list[float], float] or None + A mapping from vertex to ``[ux, uy, uz]``, together with the updated + manual-entry and magnitude defaults to reuse for the next prompt, or + None if the user cancelled. + """ + direction = rs.GetString("Support displacement direction", "Manual", DISPLACEMENT_DIRECTIONS) + if not direction: + return None + + if direction == "Manual": + values = get_displacement_vector(manual_defaults) + if values is None: + return None + return {vertex: values for vertex in vertices}, values, magnitude_default + + magnitude = rs.GetReal("{0} displacement magnitude".format(direction), magnitude_default, minimum=0.0) + if magnitude is None: + return None + + if direction == "Downward": + vectors = {vertex: [0.0, 0.0, -magnitude] for vertex in vertices} + else: + outward = formobject.diagram.find_outward_displacement(vertices) + sign = -1.0 if direction == "Inward" else 1.0 + vectors = {vertex: [sign * magnitude * ux, sign * magnitude * uy, 0.0] for vertex, (ux, uy, _) in outward.items()} + + return vectors, manual_defaults, magnitude + + +def get_support_displacement(formobject): + supports = list(formobject.diagram.supports()) + displacement = np.zeros((len(supports), 3)) + assignments = {} + manual_defaults = [-1.0, -1.0, 0.0] + magnitude_default = 1.0 + + while True: + vertices = formobject.select_thrust_vertices( + vertices=supports, + message="Select supports for displacement", + use_edges=False, + ) + if not vertices: + break + + result = get_displacement_values(formobject, vertices, manual_defaults, magnitude_default) + if result is None: + return + vectors, manual_defaults, magnitude_default = result + + for vertex in vertices: + values = vectors[vertex] + displacement[supports.index(vertex)] = np.array(values) + assignments[vertex] = values + print("Applied displacement {0} to support {1}".format(values, vertex)) + + rs.UnselectAllObjects() + more = rs.GetString("Define additional displacement vectors", "No", ["No", "Yes"]) + if more != "Yes": + break + + if not assignments: + formobject.session.warn("Select at least one support and assign a displacement vector.") + return + + for support in supports: + formobject.diagram.vertex_attributes(support, ["ux", "uy", "uz"], assignments.get(support, [0.0, 0.0, 0.0])) + + return displacement + + +def create_analysis(objective, formobject, envelope): + settings = formobject.session.settings.tno + formdiagram = formobject.diagram + kwargs = { + "printout": settings.printout, + "max_iter": settings.max_iter, + "starting_point": settings.starting_point, + "solver": settings.solver, + } + + if objective == "MinimumThrust": + analysis = Analysis.create_minthrust_analysis(formdiagram, envelope, **kwargs) + elif objective == "MaximumThrust": + analysis = Analysis.create_maxthrust_analysis(formdiagram, envelope, **kwargs) + elif objective == "MinimumThickness": + if isinstance(envelope, MeshEnvelope): + formobject.session.warn("Minimum thickness analysis is only available for parametric envelopes.") + return + analysis = Analysis.create_minthk_analysis(formdiagram, envelope, **kwargs) + elif objective == "Bestfit": + analysis = Analysis.create_bestfit_analysis(formdiagram, envelope, **kwargs) + elif objective == "MaximumLoad": + load_direction = get_load_direction(formobject) + if load_direction is None: + return + max_lambd = rs.GetReal("Maximum load multiplier", 9999.0, minimum=0.0) + if max_lambd is None: + return + analysis = Analysis.create_max_load_analysis(formdiagram, envelope, load_direction=load_direction, max_lambd=max_lambd, **kwargs) + elif objective == "SupportDisplacement": + support_displacement = get_support_displacement(formobject) + if support_displacement is None: + return + analysis = Analysis.create_compl_energy_analysis(formdiagram, envelope, support_displacement=support_displacement, **kwargs) + else: + raise NotImplementedError + + return analysis + + +def report_result(objective, analysis): + result = analysis.result + fopt = result.fopt + + if objective in ("MinimumThrust", "MaximumThrust"): + print("Optimal horizontal thrust calculated: {0:.3f}".format(fopt)) + elif objective == "MinimumThickness": + print("Minimum thickness calculated: {0:.3f}".format(fopt)) + if result.exitflag == 0: + analysis.envelope.thickness = fopt + analysis.envelope.update_envelope() + elif objective == "MaximumLoad": + print("Maximum load multiplier calculated: {0:.3f}".format(fopt)) + elif objective == "SupportDisplacement": + print("Complementary energy to assigned displacements: {0:.3f}".format(fopt)) + elif objective == "Bestfit": + print("Optimal squared vertical distance to middle surface: {0:.3f}".format(fopt)) + + +def RunCommand(): + session = RVSession() + + formobject = session.find_formdiagram() + if not formobject: + return + + envelope = session.find_envelope() + if not envelope: + return + + objective = rs.GetString("TNO objective", "MinimumThrust", OBJECTIVES) + if not objective: + return + + solver = rs.GetString("Solver", session.settings.tno.solver, ["SLSQP", "IPOPT"]) + if not solver: + return + session.settings.tno.solver = solver + + options = get_optimisation_options(objective) + if not options: + return + constraints, variables = options + + analysis = create_analysis(objective, formobject, envelope) + if not analysis: + return + + analysis.optimiser.set_constraints(constraints) + analysis.optimiser.set_variables(variables) + + invert_formdiagram_signs(formobject.diagram) + try: + # Apply the envelope bounds and use the loads prepared on the FormDiagram. + envelope.apply_bounds_to_formdiagram(formobject.diagram) + + if abs(sum(formobject.diagram.vertices_attribute("pz"))) < 0.001: + return session.warn("There are no loads applied to the FormDiagram. Use RV_loads before running TNO analysis.") + + analysis.set_up_optimiser() + analysis.run() + session["analysis"] = analysis + finally: + invert_formdiagram_signs(formobject.diagram) + + forceobject = None + if analysis.result.success: + if objective == "MaximumLoad": + session.settings.drawing.show_loads = True + forceobject = session.find_forcediagram(warn=False) + if forceobject: + update_force_from_form(forceobject.diagram, formobject.diagram) + forceobject.diagram.update_position() + forceobject.diagram.update_angle_deviations() + + report_result(objective, analysis) + + formobject.show_thrust = True + + rs.UnselectAllObjects() + formobject.redraw() + if forceobject: + forceobject.redraw() + + if session.settings.autosave: + session.record(name="TNO Analysis") + + +if __name__ == "__main__": + RunCommand() diff --git a/compas-RV.rhproj b/compas-RV.rhproj index a92018d..6aa84c6 100644 --- a/compas-RV.rhproj +++ b/compas-RV.rhproj @@ -8,7 +8,7 @@ "id": "a6dc4669-0e8e-40ea-8d71-b9b0f4764ec1", "identity": { "name": "COMPAS-RhinoVAULT", - "version": "0.6.44", + "version": "0.6.47-beta", "publisher": { "email": "tom.v.mele@gmail.com", "name": "Tom Van Mele", @@ -76,7 +76,7 @@ "id": "*.*.python", "version": "3.*.*" }, - "title": "RV", + "title": "RV_init", "uri": "commands/RV.py", "image": { "light": { @@ -340,6 +340,90 @@ } } }, + { + "id": "54be0499-c175-4f78-a748-4c1322f2edc9", + "language": { + "id": "*.*.python", + "version": "3.*.*" + }, + "title": "RV_envelope", + "uri": "commands/RV_envelope.py", + "image": { + "light": { + "type": "svg", + "data": "PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzMiAzMiI+CiAgPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI5LjguMTAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiAyLjEuMSBCdWlsZCAyKSAgLS0+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC5zdDAgewogICAgICAgIHN0cm9rZS1taXRlcmxpbWl0OiAzLjY7CiAgICAgICAgc3Ryb2tlLXdpZHRoOiAuMXB4OwogICAgICB9CgogICAgICAuc3QwLCAuc3QxIHsKICAgICAgICBzdHJva2U6ICNhNmE2YWU7CiAgICAgICAgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOwogICAgICB9CgogICAgICAuc3QwLCAuc3QxLCAuc3QyLCAuc3QzIHsKICAgICAgICBmaWxsOiBub25lOwogICAgICB9CgogICAgICAuc3QxIHsKICAgICAgICBzdHJva2UtbWl0ZXJsaW1pdDogMy4xMzsKICAgICAgICBzdHJva2Utd2lkdGg6IC4wOXB4OwogICAgICB9CgogICAgICAuc3Q0IHsKICAgICAgICBmaWxsOiAjNmY2Zjc4OwogICAgICB9CgogICAgICAuc3QyIHsKICAgICAgICBzdHJva2U6ICMxMTExMTQ7CiAgICAgIH0KCiAgICAgIC5zdDIsIC5zdDMgewogICAgICAgIHN0cm9rZS1saW5lam9pbjogcm91bmQ7CiAgICAgICAgc3Ryb2tlLXdpZHRoOiAuNHB4OwogICAgICB9CgogICAgICAuc3Q1IHsKICAgICAgICBvcGFjaXR5OiAuMTE7CiAgICAgIH0KCiAgICAgIC5zdDMgewogICAgICAgIHN0cm9rZTogIzRhNGE1MjsKICAgICAgfQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPGcgY2xhc3M9InN0NSI+CiAgICA8cG9seWdvbiBjbGFzcz0ic3Q0IiBwb2ludHM9IjI4LjUyIDE0LjcxIDI3LjM5IDExLjUgMjYuMTEgOC40NiAyNC42NiA2LjM3IDIzLjA4IDYuMTkgMjIuNjcgNi45MSAyMi40MiA3LjEgMjIuMjEgNi44MiAxOS4xNCA0LjE0IDE1LjkgMy4wMiAxMi42NyA0LjE0IDkuNTkgNi44MiA5LjM5IDcuMSA5LjEyIDYuOSA4LjcyIDYuMTkgNy4xNCA2LjM3IDUuNjkgOC40NiA0LjQxIDExLjUgMy4yOSAxNC43MSA2LjE0IDEyLjI0IDguODUgMTAuMjMgMTAuNDggOS41IDExLjkyIDE0LjE1IDEzLjQ2IDIwLjg1IDE0Ljk1IDI4LjI1IDE2Ljg1IDI4LjI1IDE4LjM0IDIwLjg1IDE5Ljg4IDE0LjE1IDIxLjMzIDkuNSAyMi45NSAxMC4yMyAyNS42NiAxMi4yNCAyOC41MiAxNC43MSIvPgogIDwvZz4KICA8ZyBjbGFzcz0ic3Q1Ij4KICAgIDxwb2x5Z29uIGNsYXNzPSJzdDQiIHBvaW50cz0iMjguMTEgMTQuOTggMjcuMDIgMTEuODcgMjUuNzggOC45NCAyNC4zOCA2LjkgMjIuODUgNi43NCAyMi40NSA3LjQ0IDIyLjIxIDcuNjIgMjIuMDEgNy4zNCAxOS4wMyA0Ljc1IDE1LjkgMy42NyAxMi43OCA0Ljc1IDkuOCA3LjM0IDkuNiA3LjYxIDkuMzUgNy40MiA4Ljk2IDYuNzQgNy40MyA2LjkgNi4wMyA4Ljk0IDQuNzkgMTEuODcgMy43IDE0Ljk4IDYuNDYgMTIuNTkgOS4wOSAxMC42NCAxMC42NiA5Ljk0IDEyLjA1IDE0LjQ0IDEzLjU0IDIwLjkyIDE0Ljk5IDI4LjA4IDE2LjgyIDI4LjA4IDE4LjI3IDIwLjkyIDE5Ljc2IDE0LjQ0IDIxLjE1IDkuOTQgMjIuNzIgMTAuNjQgMjUuMzQgMTIuNTkgMjguMTEgMTQuOTgiLz4KICA8L2c+CiAgPGcgY2xhc3M9InN0NSI+CiAgICA8cG9seWdvbiBjbGFzcz0ic3Q0IiBwb2ludHM9IjI3LjcgMTUuMjQgMjYuNjQgMTIuMjQgMjUuNDUgOS40MSAyNC4wOSA3LjQ0IDIyLjYyIDcuMjggMjIuMjMgNy45NiAyMiA4LjEzIDIxLjggNy44NyAxOC45MyA1LjM2IDE1LjkxIDQuMzIgMTIuODggNS4zNiAxMC4wMSA3Ljg3IDkuODIgOC4xMyA5LjU3IDcuOTQgOS4yIDcuMjggNy43MiA3LjQ0IDYuMzcgOS40MSA1LjE3IDEyLjI0IDQuMTIgMTUuMjQgNi43OSAxMi45MyA5LjMyIDExLjA2IDEwLjg0IDEwLjM3IDEyLjE5IDE0LjcyIDEzLjYzIDIwLjk5IDE1LjAyIDI3LjkgMTYuNzkgMjcuOSAxOC4xOSAyMC45OSAxOS42MyAxNC43MiAyMC45OCAxMC4zNyAyMi40OSAxMS4wNiAyNS4wMyAxMi45MyAyNy43IDE1LjI0Ii8+CiAgPC9nPgogIDxnIGNsYXNzPSJzdDUiPgogICAgPHBvbHlnb24gY2xhc3M9InN0NCIgcG9pbnRzPSIyNy4yOSAxNS41MSAyNi4yNyAxMi42MiAyNS4xMiA5Ljg4IDIzLjgxIDcuOTggMjIuMzkgNy44MyAyMi4wMSA4LjQ4IDIxLjc5IDguNjUgMjEuNiA4LjM5IDE4LjgzIDUuOTggMTUuOTEgNC45NyAxMi45OSA1Ljk4IDEwLjIyIDguMzkgMTAuMDQgOC42NCA5LjggOC40NiA5LjQzIDcuODMgOC4wMSA3Ljk4IDYuNyA5Ljg4IDUuNTUgMTIuNjIgNC41MyAxNS41MSA3LjExIDEzLjI4IDkuNTYgMTEuNDcgMTEuMDIgMTAuODEgMTIuMzIgMTUgMTMuNzEgMjEuMDUgMTUuMDYgMjcuNzIgMTYuNzYgMjcuNzIgMTguMTEgMjEuMDUgMTkuNSAxNSAyMC44IDEwLjgxIDIyLjI3IDExLjQ3IDI0LjcxIDEzLjI4IDI3LjI5IDE1LjUxIi8+CiAgPC9nPgogIDxnIGNsYXNzPSJzdDUiPgogICAgPHBvbHlnb24gY2xhc3M9InN0NCIgcG9pbnRzPSIyNi44OCAxNS43OCAyNS45IDEyLjk5IDI0Ljc5IDEwLjM1IDIzLjUzIDguNTIgMjIuMTYgOC4zNyAyMS44IDkgMjEuNTggOS4xNyAyMS40IDguOTEgMTguNzMgNi41OSAxNS45MSA1LjYyIDEzLjEgNi41OSAxMC40MyA4LjkxIDEwLjI1IDkuMTYgMTAuMDIgOC45OSA5LjY3IDguMzcgOC4zIDguNTIgNy4wNCAxMC4zNSA1LjkzIDEyLjk5IDQuOTUgMTUuNzggNy40MyAxMy42MyA5Ljc5IDExLjg4IDExLjIgMTEuMjUgMTIuNDUgMTUuMjkgMTMuNzkgMjEuMTIgMTUuMDkgMjcuNTUgMTYuNzQgMjcuNTUgMTguMDQgMjEuMTIgMTkuMzggMTUuMjkgMjAuNjMgMTEuMjUgMjIuMDQgMTEuODggMjQuNCAxMy42MyAyNi44OCAxNS43OCIvPgogIDwvZz4KICA8Zz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIzLjk0IiB5MT0iMTQuMjIiIHgyPSI0LjY0IiB5Mj0iMTMuNTciLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI0LjA1IiB5MT0iMTQuMTUiIHgyPSI0LjMxIiB5Mj0iMTMuODciLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIzLjk0IiB5MT0iMTQuMjIiIHgyPSI0LjA1IiB5Mj0iMTQuMTUiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI5LjY3IiB5MT0iNi45OCIgeDI9IjkuNjkiIHkyPSI2LjciLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI0LjE2IiB5MT0iMTQuMDgiIHgyPSI0LjMyIiB5Mj0iMTMuODciLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI0LjA1IiB5MT0iMTQuMTUiIHgyPSI0LjE2IiB5Mj0iMTQuMDgiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI5LjU3IiB5MT0iNi44NiIgeDI9IjkuNjkiIHkyPSI2LjciLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI0LjI3IiB5MT0iMTQuMDEiIHgyPSI0LjQ0IiB5Mj0iMTMuNzYiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI0LjE2IiB5MT0iMTQuMDgiIHgyPSI0LjI3IiB5Mj0iMTQuMDEiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyNy4zNiIgeTE9IjEzLjU3IiB4Mj0iMjcuNjEiIHkyPSIxMy44MiIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjI3Ljk1IiB5MT0iMTQuMTUiIHgyPSIyOC4wNiIgeTI9IjE0LjIyIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjcuNjkiIHkxPSIxMy44NyIgeDI9IjI3Ljk1IiB5Mj0iMTQuMTUiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyNy44NCIgeTE9IjE0LjA4IiB4Mj0iMjcuOTUiIHkyPSIxNC4xNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjI3LjY4IiB5MT0iMTMuODciIHgyPSIyNy44NCIgeTI9IjE0LjA4Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjcuNzMiIHkxPSIxNC4wMSIgeDI9IjI3Ljg0IiB5Mj0iMTQuMDgiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyNy41NiIgeTE9IjEzLjc2IiB4Mj0iMjcuNzMiIHkyPSIxNC4wMSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjIyLjMxIiB5MT0iNi43IiB4Mj0iMjIuNDMiIHkyPSI2Ljg2Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjIuMzEiIHkxPSI2LjciIHgyPSIyMi4zMyIgeTI9IjYuOTgiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI0LjUxIiB5MT0iMTEuMzgiIHgyPSI1LjAyIiB5Mj0iMTEuNzMiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIzLjM5IiB5MT0iMTQuNTkiIHgyPSIzLjQ5IiB5Mj0iMTQuNTIiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI1Ljc5IiB5MT0iOC4zNSIgeDI9IjYuNjciIHkyPSI5LjE0Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iNi42NyIgeTE9IjkuMTQiIHgyPSI3LjUxIiB5Mj0iOS43NyIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjUuMDIiIHkxPSIxMS43MyIgeDI9IjUuNDgiIHkyPSIxMi4wMSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjMuNDkiIHkxPSIxNC41MiIgeDI9IjMuNjEiIHkyPSIxNC40NSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjcuMjQiIHkxPSI2LjI1IiB4Mj0iOC40MSIgeTI9IjcuNDMiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI5LjYzIiB5MT0iOC4zNSIgeDI9IjEwLjE1IiB5Mj0iOC4zOSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjguNDEiIHkxPSI3LjQzIiB4Mj0iOS42MyIgeTI9IjguMzUiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI5LjYzIiB5MT0iOC4zNSIgeDI9IjEwLjI0IiB5Mj0iOC41NiIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjguMzEiIHkxPSIxMC4xMyIgeDI9IjEwLjQ4IiB5Mj0iOS4wNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjcuNTEiIHkxPSI5Ljc3IiB4Mj0iOC4zMSIgeTI9IjEwLjEzIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iOC4zMSIgeTE9IjEwLjEzIiB4Mj0iOC45NSIgeTI9IjEwLjExIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iNS40OCIgeTE9IjEyLjAxIiB4Mj0iNS45IiB5Mj0iMTIuMTYiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIzLjcyIiB5MT0iMTQuMzgiIHgyPSI1LjkiIHkyPSIxMi4xNiIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjMuNjEiIHkxPSIxNC40NSIgeDI9IjMuNzIiIHkyPSIxNC4zOCIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjUuOSIgeTE9IjEyLjE2IiB4Mj0iOC4zMSIgeTI9IjEwLjEzIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iNS45IiB5MT0iMTIuMTYiIHgyPSI2LjI0IiB5Mj0iMTIuMTIiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIzLjcyIiB5MT0iMTQuMzgiIHgyPSIzLjgzIiB5Mj0iMTQuMjkiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIxNS4wNSIgeTE9IjI4LjEzIiB4Mj0iMTUuMjkiIHkyPSIyOC4xMyIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjE1LjI5IiB5MT0iMjguMTMiIHgyPSIxNS41MiIgeTI9IjI4LjEzIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMTUuNTIiIHkxPSIyOC4xMyIgeDI9IjE1Ljc3IiB5Mj0iMjguMTMiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIxNS43NyIgeTE9IjI4LjEzIiB4Mj0iMTYiIHkyPSIyOC4xMyIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjE2IiB5MT0iMjguMTMiIHgyPSIxNi4yMyIgeTI9IjI4LjEzIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMTYuMjMiIHkxPSIyOC4xMyIgeDI9IjE2LjQ4IiB5Mj0iMjguMTMiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIxNi40OCIgeTE9IjI4LjEzIiB4Mj0iMTYuNzEiIHkyPSIyOC4xMyIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjE2LjcxIiB5MT0iMjguMTMiIHgyPSIxNi45NSIgeTI9IjI4LjEzIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjEuODUiIHkxPSI4LjM5IiB4Mj0iMjIuMzciIHkyPSI4LjM1Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjEuNzYiIHkxPSI4LjU2IiB4Mj0iMjIuMzciIHkyPSI4LjM1Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjMuNjkiIHkxPSIxMC4xMyIgeDI9IjI0LjQ5IiB5Mj0iOS43NyIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjIxLjUyIiB5MT0iOS4wNSIgeDI9IjIzLjY5IiB5Mj0iMTAuMTMiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyMy4wNSIgeTE9IjEwLjExIiB4Mj0iMjMuNjkiIHkyPSIxMC4xMyIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjIyLjM3IiB5MT0iOC4zNSIgeDI9IjIzLjU5IiB5Mj0iNy40MyIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjIzLjU5IiB5MT0iNy40MyIgeDI9IjI0Ljc2IiB5Mj0iNi4yNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjI1LjMzIiB5MT0iOS4xNCIgeDI9IjI2LjIxIiB5Mj0iOC4zNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjI0LjQ5IiB5MT0iOS43NyIgeDI9IjI1LjMzIiB5Mj0iOS4xNCIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjI2LjUyIiB5MT0iMTIuMDEiIHgyPSIyNi45OCIgeTI9IjExLjczIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjYuOTgiIHkxPSIxMS43MyIgeDI9IjI3LjQ5IiB5Mj0iMTEuMzgiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyMy42OSIgeTE9IjEwLjEzIiB4Mj0iMjYuMSIgeTI9IjEyLjE2Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjUuNzYiIHkxPSIxMi4xMiIgeDI9IjI2LjEiIHkyPSIxMi4xNiIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjI2LjEiIHkxPSIxMi4xNiIgeDI9IjI2LjUyIiB5Mj0iMTIuMDEiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyNi4xIiB5MT0iMTIuMTYiIHgyPSIyOC4yOCIgeTI9IjE0LjM4Ii8+CiAgICA8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTIuMDksOC42NmwuMjItLjMyYy4xMi0uMTguMjQtLjU4LjI5LS44OXMuMDgtLjg3LjExLTEuMjUuMDUtMS4wMi4wNS0xLjQzbC4wMi0uNzUiLz4KICAgIDxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xOS4yMyw0LjAybC4wMi43NWMwLC40MS4wNCwxLjA1LjA1LDEuNDNzLjA2Ljk0LjExLDEuMjUuMTYuNzEuMjkuODlsLjIyLjMyIi8+CiAgICA8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTAuODUsOGwuOS0uNTZjLjUtLjMxLDEuNjUtLjc0LDIuNTctLjk2czIuNDItLjIzLDMuMzQsMCwyLjA3LjY2LDIuNTcuOTZsLjkuNTYiLz4KICAgIDxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0zLjM5LDE0LjU5bDEuNS0xLjI5Yy43NS0uNjUsMS45Ny0xLjYzLDIuNzItMi4xOHMxLjcyLTEuMTcsMi4xNy0xLjM3bC44MS0uMzciLz4KICAgIDxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0yMS40Miw5LjM4bC44MS4zN2MuNDUuMjEsMS40Mi44MiwyLjE3LDEuMzdzMS45NywxLjUzLDIuNzIsMi4xOGwxLjUsMS4yOSIvPgogICAgPHBhdGggY2xhc3M9InN0MCIgZD0iTTkuNjksNi43bDEuNzEtMS41Yy43NS0uNjUsMi4wOC0xLjQzLDIuOTctMS43NHMyLjM0LS4zMSwzLjIzLDAsMi4yMywxLjA5LDIuOTcsMS43NGwxLjcxLDEuNSIvPgogICAgPHBhdGggY2xhc3M9InN0MCIgZD0iTTMuMzksMTQuNTlsLjU2LTEuNmMuMzEtLjg4Ljg2LTIuMjksMS4yMS0zLjEzLjM1LS44NC45Ni0xLjk5LDEuMzctMi41N3MxLjA4LTEuMDksMS41MS0xLjE0LDEuMTUuNTQsMS41OSwxLjMyLDEuMDUsMi4xNywxLjMzLDMuMTNsLjUzLDEuNzFjLjI5Ljk1LjcxLDIuNTEuOTQsMy40OGwuNzMsMy4xOGMuMjMuOTYuNTcsMi41NS43NiwzLjUybDEuMTQsNS42MyIvPgogICAgPHBhdGggY2xhc3M9InN0MCIgZD0iTTE2Ljk1LDI4LjEzbDEuMTQtNS42M2MuMi0uOTcuNTQtMi41Ni43Ni0zLjUybC43My0zLjE4Yy4yMy0uOTYuNjQtMi41My45NC0zLjQ4bC41My0xLjcxYy4yOS0uOTUuODktMi4zNSwxLjMzLTMuMTNzMS4xNi0xLjM2LDEuNTktMS4zMiwxLjExLjU2LDEuNTEsMS4xNCwxLjAxLDEuNzMsMS4zNywyLjU3Ljg5LDIuMjQsMS4yMSwzLjEzbC41NiwxLjYiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIxNS4wNSIgeTE9IjI4LjEzIiB4Mj0iMTYuOTUiIHkyPSIyOC4xMyIvPgogIDwvZz4KICA8Zz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSI1LjUyIiB5MT0iMTUuMzUiIHgyPSI2LjEyIiB5Mj0iMTQuNzkiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSI1LjYyIiB5MT0iMTUuMjkiIHgyPSI1Ljg0IiB5Mj0iMTUuMDUiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSI1LjUyIiB5MT0iMTUuMzUiIHgyPSI1LjYyIiB5Mj0iMTUuMjkiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIxMC41IiB5MT0iOS4wNiIgeDI9IjEwLjUyIiB5Mj0iOC44MSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjUuNzEiIHkxPSIxNS4yMyIgeDI9IjUuODUiIHkyPSIxNS4wNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjUuNjIiIHkxPSIxNS4yOSIgeDI9IjUuNzEiIHkyPSIxNS4yMyIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjEwLjQxIiB5MT0iOC45NSIgeDI9IjEwLjUyIiB5Mj0iOC44MSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjUuOCIgeTE9IjE1LjE2IiB4Mj0iNS45NSIgeTI9IjE0Ljk1Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iNS43MSIgeTE9IjE1LjIzIiB4Mj0iNS44IiB5Mj0iMTUuMTYiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyNS44OCIgeTE9IjE0Ljc5IiB4Mj0iMjYuMDkiIHkyPSIxNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjI2LjM4IiB5MT0iMTUuMjkiIHgyPSIyNi40OCIgeTI9IjE1LjM1Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMjYuMTYiIHkxPSIxNS4wNSIgeDI9IjI2LjM4IiB5Mj0iMTUuMjkiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyNi4yOSIgeTE9IjE1LjIzIiB4Mj0iMjYuMzgiIHkyPSIxNS4yOSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjI2LjE1IiB5MT0iMTUuMDUiIHgyPSIyNi4yOSIgeTI9IjE1LjIzIi8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMjYuMiIgeTE9IjE1LjE2IiB4Mj0iMjYuMjkiIHkyPSIxNS4yMyIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjI2LjA1IiB5MT0iMTQuOTUiIHgyPSIyNi4yIiB5Mj0iMTUuMTYiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyMS40OCIgeTE9IjguODEiIHgyPSIyMS41OSIgeTI9IjguOTUiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyMS40OCIgeTE9IjguODEiIHgyPSIyMS41IiB5Mj0iOS4wNiIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjYuMDEiIHkxPSIxMi44OSIgeDI9IjYuNDUiIHkyPSIxMy4xOCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjUuMDQiIHkxPSIxNS42NyIgeDI9IjUuMTMiIHkyPSIxNS42MSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjcuMTMiIHkxPSIxMC4yNSIgeDI9IjcuODkiIHkyPSIxMC45NCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjcuODkiIHkxPSIxMC45NCIgeDI9IjguNjIiIHkyPSIxMS40OCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjYuNDUiIHkxPSIxMy4xOCIgeDI9IjYuODYiIHkyPSIxMy40MyIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjUuMTMiIHkxPSIxNS42MSIgeDI9IjUuMjMiIHkyPSIxNS41NSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjguMzkiIHkxPSI4LjQyIiB4Mj0iOS40MSIgeTI9IjkuNDUiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIxMC40NiIgeTE9IjEwLjI1IiB4Mj0iMTAuOTIiIHkyPSIxMC4yOSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjkuNDEiIHkxPSI5LjQ1IiB4Mj0iMTAuNDYiIHkyPSIxMC4yNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjEwLjQ2IiB5MT0iMTAuMjUiIHgyPSIxMSIgeTI9IjEwLjQzIi8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iOS4zMiIgeTE9IjExLjgiIHgyPSIxMS4yIiB5Mj0iMTAuODYiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSI4LjYyIiB5MT0iMTEuNDgiIHgyPSI5LjMyIiB5Mj0iMTEuOCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjkuMzIiIHkxPSIxMS44IiB4Mj0iOS44OCIgeTI9IjExLjc4Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iNi44NiIgeTE9IjEzLjQzIiB4Mj0iNy4yMiIgeTI9IjEzLjU2Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iNS4zMyIgeTE9IjE1LjQ5IiB4Mj0iNy4yMiIgeTI9IjEzLjU2Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iNS4yMyIgeTE9IjE1LjU1IiB4Mj0iNS4zMyIgeTI9IjE1LjQ5Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iNy4yMiIgeTE9IjEzLjU2IiB4Mj0iOS4zMiIgeTI9IjExLjgiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSI3LjIyIiB5MT0iMTMuNTYiIHgyPSI3LjUyIiB5Mj0iMTMuNTMiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSI1LjMzIiB5MT0iMTUuNDkiIHgyPSI1LjQyIiB5Mj0iMTUuNDEiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIxNS4xOCIgeTE9IjI3LjQ0IiB4Mj0iMTUuMzgiIHkyPSIyNy40NCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjE1LjM4IiB5MT0iMjcuNDQiIHgyPSIxNS41OCIgeTI9IjI3LjQ0Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMTUuNTgiIHkxPSIyNy40NCIgeDI9IjE1LjgiIHkyPSIyNy40NCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjE1LjgiIHkxPSIyNy40NCIgeDI9IjE2IiB5Mj0iMjcuNDQiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIxNiIgeTE9IjI3LjQ0IiB4Mj0iMTYuMiIgeTI9IjI3LjQ0Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMTYuMiIgeTE9IjI3LjQ0IiB4Mj0iMTYuNDIiIHkyPSIyNy40NCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjE2LjQyIiB5MT0iMjcuNDQiIHgyPSIxNi42MiIgeTI9IjI3LjQ0Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMTYuNjIiIHkxPSIyNy40NCIgeDI9IjE2LjgyIiB5Mj0iMjcuNDQiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyMS4wOCIgeTE9IjEwLjI5IiB4Mj0iMjEuNTQiIHkyPSIxMC4yNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjIxIiB5MT0iMTAuNDMiIHgyPSIyMS41NCIgeTI9IjEwLjI1Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMjIuNjgiIHkxPSIxMS44IiB4Mj0iMjMuMzgiIHkyPSIxMS40OCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjIwLjgiIHkxPSIxMC44NiIgeDI9IjIyLjY4IiB5Mj0iMTEuOCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjIyLjEyIiB5MT0iMTEuNzgiIHgyPSIyMi42OCIgeTI9IjExLjgiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyMS41NCIgeTE9IjEwLjI1IiB4Mj0iMjIuNTkiIHkyPSI5LjQ1Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMjIuNTkiIHkxPSI5LjQ1IiB4Mj0iMjMuNjEiIHkyPSI4LjQyIi8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMjQuMTEiIHkxPSIxMC45NCIgeDI9IjI0Ljg3IiB5Mj0iMTAuMjUiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyMy4zOCIgeTE9IjExLjQ4IiB4Mj0iMjQuMTEiIHkyPSIxMC45NCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjI1LjE0IiB5MT0iMTMuNDMiIHgyPSIyNS41NSIgeTI9IjEzLjE4Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMjUuNTUiIHkxPSIxMy4xOCIgeDI9IjI1Ljk5IiB5Mj0iMTIuODkiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyMi42OCIgeTE9IjExLjgiIHgyPSIyNC43OCIgeTI9IjEzLjU2Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMjQuNDgiIHkxPSIxMy41MyIgeDI9IjI0Ljc4IiB5Mj0iMTMuNTYiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyNC43OCIgeTE9IjEzLjU2IiB4Mj0iMjUuMTQiIHkyPSIxMy40MyIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjI0Ljc4IiB5MT0iMTMuNTYiIHgyPSIyNi42NyIgeTI9IjE1LjQ5Ii8+CiAgICA8cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTIuNiwxMC41MmwuMTktLjI4Yy4xLS4xNi4yMS0uNS4yNS0uNzhzLjA3LS43Ni4wOS0xLjA5LjA0LS44OC4wNS0xLjI1bC4wMi0uNjUiLz4KICAgIDxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0xOC44MSw2LjQ5bC4wMi42NWMwLC4zNi4wMy45Mi4wNSwxLjI1cy4wNS44MS4wOSwxLjA5LjE0LjYyLjI1Ljc4bC4xOS4yOCIvPgogICAgPHBhdGggY2xhc3M9InN0MSIgZD0iTTExLjUzLDkuOTRsLjc4LS40OWMuNDMtLjI3LDEuNDMtLjY0LDIuMjMtLjg0czIuMTEtLjIsMi45MSwwLDEuOC41NywyLjIzLjg0bC43OC40OSIvPgogICAgPHBhdGggY2xhc3M9InN0MSIgZD0iTTUuMDQsMTUuNjdsMS4zLTEuMTJjLjY1LS41NiwxLjcyLTEuNDIsMi4zNy0xLjlzMS41LTEuMDIsMS44OS0xLjE5bC43LS4zMiIvPgogICAgPHBhdGggY2xhc3M9InN0MSIgZD0iTTIwLjcxLDExLjE1bC43LjMyYy4zOS4xOCwxLjI0LjcxLDEuODksMS4xOXMxLjcyLDEuMzMsMi4zNywxLjlsMS4zLDEuMTIiLz4KICAgIDxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0xMC41Miw4LjgxbDEuNDktMS4zYy42NS0uNTYsMS44MS0xLjI1LDIuNTgtMS41MXMyLjA0LS4yNywyLjgxLDAsMS45My45NSwyLjU4LDEuNTFsMS40OSwxLjMiLz4KICAgIDxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik01LjA0LDE1LjY3bC40OS0xLjM5Yy4yNy0uNzcuNzQtMS45OSwxLjA1LTIuNzIuMzEtLjczLjg0LTEuNzMsMS4xOS0yLjIzcy45NC0uOTUsMS4zMi0uOTksMSwuNDcsMS4zOSwxLjE0LjkxLDEuODksMS4xNiwyLjcybC40NiwxLjQ5Yy4yNS44Mi42MiwyLjE5LjgxLDMuMDJsLjYzLDIuNzZjLjIuODQuNDksMi4yMi42NiwzLjA2bC45OSw0Ljg5Ii8+CiAgICA8cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTYuODIsMjcuNDRsLjk5LTQuODljLjE3LS44NS40Ny0yLjIyLjY2LTMuMDZsLjYzLTIuNzZjLjItLjg0LjU2LTIuMi44MS0zLjAybC40Ni0xLjQ5Yy4yNS0uODIuNzgtMi4wNCwxLjE2LTIuNzJzMS4wMS0xLjE4LDEuMzktMS4xNC45Ni40OSwxLjMyLjk5Ljg4LDEuNSwxLjE5LDIuMjMuNzgsMS45NSwxLjA1LDIuNzJsLjQ5LDEuMzkiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIxNS4xOCIgeTE9IjI3LjQ0IiB4Mj0iMTYuODIiIHkyPSIyNy40NCIvPgogIDwvZz4KICA8cG9seWdvbiBjbGFzcz0ic3QzIiBwb2ludHM9IjI2Ljg4IDE1Ljc4IDI1LjkgMTIuOTkgMjQuNzkgMTAuMzUgMjMuNTMgOC41MiAyMi4xNiA4LjM3IDIxLjggOSAyMS41OCA5LjE3IDIxLjQgOC45MSAxOC43MyA2LjU5IDE1LjkxIDUuNjIgMTMuMSA2LjU5IDEwLjQzIDguOTEgMTAuMjUgOS4xNiAxMC4wMiA4Ljk5IDkuNjcgOC4zNyA4LjMgOC41MiA3LjA0IDEwLjM1IDUuOTMgMTIuOTkgNC45NSAxNS43OCA3LjQzIDEzLjYzIDkuNzkgMTEuODggMTEuMiAxMS4yNSAxMi40NSAxNS4yOSAxMy43OSAyMS4xMiAxNS4wOSAyNy41NSAxNi43NCAyNy41NSAxOC4wNCAyMS4xMiAxOS4zOCAxNS4yOSAyMC42MyAxMS4yNSAyMi4wNCAxMS44OCAyNC40IDEzLjYzIDI2Ljg4IDE1Ljc4Ii8+CiAgPHBvbHlnb24gY2xhc3M9InN0MiIgcG9pbnRzPSIyOC41MiAxNC43MSAyNy4zOSAxMS41IDI2LjExIDguNDYgMjQuNjYgNi4zNyAyMy4wOCA2LjE5IDIyLjY3IDYuOTEgMjIuNDIgNy4xIDIyLjIxIDYuODIgMTkuMTQgNC4xNCAxNS45IDMuMDIgMTIuNjcgNC4xNCA5LjU5IDYuODIgOS4zOSA3LjEgOS4xMiA2LjkgOC43MiA2LjE5IDcuMTQgNi4zNyA1LjY5IDguNDYgNC40MSAxMS41IDMuMjkgMTQuNzEgNi4xNCAxMi4yNCA4Ljg1IDEwLjIzIDEwLjQ4IDkuNSAxMS45MiAxNC4xNSAxMy40NiAyMC44NSAxNC45NSAyOC4yNSAxNi44NSAyOC4yNSAxOC4zNCAyMC44NSAxOS44OCAxNC4xNSAyMS4zMyA5LjUgMjIuOTUgMTAuMjMgMjUuNjYgMTIuMjQgMjguNTIgMTQuNzEiLz4KPC9zdmc+Cg==" + }, + "rendered": { + "light": { + "bytes": "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAGKADAAQAAAABAAAAGAAAAADiNXWtAAADxElEQVRIDd1US2gbVxSdn6SRR9ZoPLZlIkseSXZtWR9DzcQk4EDooi2ldN91MV5nqa2LMU4XWRhhXEp3pVjdBgItVFBooSRVCW6rSkGNpbjNuDWy5v\u002BfvqkzQspHUeim9C3m3nfn3HPeu\u002B\u002B\u002BB0H/pVEoFIiNjQ3fq6wJHgV8/fq7WZ7nmGAQpyVJxhDE/2hmhnlw\u002B/ZnzZfloy8DsOxVlmFiuWg0psXjKSuZTJ0TBAHbtpwjSVo/OWn9OYzjGYHDw0P09PTsrcnJqVw\u002BX7hEUZN0JpOZcxyYdRw7YxhGliSJ\u002BNLScrfVagdZ9vI4gvgur6yw9M7O1u/lctnpF3ymRCx75Q2GWYguLCSSgiB0A4FxAkGcrM\u002BHHQNy3k2GYThiWTaDovj3HPcIn51NmNXqPU6SzmuVSuVuv8DADtLpXLxQyK7G4/M\u002BVVWvEUQ4D8MQRhDBtm07hm3boDQQAsTsQGCsa1laNhymsrIsUrFYrC6K3bl0Ot1sNBqqJ4J4jmtDIf9yMBjiYdh8WwOj3W5rpmnCum6ZqqoATVUxDE3Vdd0lMGTZMI\u002BPH1iOY8CWBb0Hw9hfjx\u002BfvdbP2RNYW1sLM8zcIuiUK/V6DY9EJqqRCHWv1Xo4pSiSCMpjeomuL0mC3Gw2ZpLJ9NcoGvgV\u002BEQms5yn6amrjuP0eHuOIKhp0B0TPN8trK6\u002B/gXPdxzbxg857uxE03TaI/dsp9OdVVXt205H\u002BkrXjW4qlb7TbD68Fg4H8PX1dcbD9QQWF1PvE0QIdM2lu5omh6anI7dKpQ9/8fv93wjC\u002BYyX4FlRFCdwHPquVLr5I8cpH1sWFqDpaBWCsPlYLPGmh8NcB2wJ3d3dvSMI1jw41MTWVnHPAwiCfCSKyjsU5UUurCDw/lBo6sidlcslEZiPbtwo7iwtZcvd7mnLLRPoNnugTff39xc2NzcbFxQX32g0SuRyq5/k89kfwD0w3KhtI\u002BP371fnKpUvPwAkA32/vb1NF4vFM4\u002BjVyI38DS5G\u002BM4TrIs/Q/QOb09KMr5JChF7WlyF99P7s4HBNzA8wdyJMty76B5XiV9PvPn52MHoyMJwLBRU1UZ91I1TcJIcrruzYfZkQR4nm\u002BCww4gCOriA5KkODQdOhlG7P0bSSCVSp2C50CxbSuoKBoJ3iHu4OBA9kiG2ZEEwAtpWZbRVlWdAq8FhSD2SOVxhUcSeLLCmiBIlChqQRSFB1r5X\u002B/AJRgbw\u002Bsoiq34/ViGJMnfhpH2//vnJvcHXuQnEomfIAj9PBwmtb29T9vgDrwI\u002Bj\u002BL/w13FKdOMSDM/wAAAABJRU5ErkJggg==", + "width": 24, + "height": 24 + }, + "dark": { + "bytes": "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAGKADAAQAAAABAAAAGAAAAADiNXWtAAADxElEQVRIDd1US2gbVxSdn6SRR9ZoPLZlIkseSXZtWR9DzcQk4EDooi2ldN91MV5nqa2LMU4XWRhhXEp3pVjdBgItVFBooSRVCW6rSkGNpbjNuDWy5v\u002BfvqkzQspHUeim9C3m3nfn3HPeu\u002B\u002B\u002BB0H/pVEoFIiNjQ3fq6wJHgV8/fq7WZ7nmGAQpyVJxhDE/2hmhnlw\u002B/ZnzZfloy8DsOxVlmFiuWg0psXjKSuZTJ0TBAHbtpwjSVo/OWn9OYzjGYHDw0P09PTsrcnJqVw\u002BX7hEUZN0JpOZcxyYdRw7YxhGliSJ\u002BNLScrfVagdZ9vI4gvgur6yw9M7O1u/lctnpF3ymRCx75Q2GWYguLCSSgiB0A4FxAkGcrM\u002BHHQNy3k2GYThiWTaDovj3HPcIn51NmNXqPU6SzmuVSuVuv8DADtLpXLxQyK7G4/M\u002BVVWvEUQ4D8MQRhDBtm07hm3boDQQAsTsQGCsa1laNhymsrIsUrFYrC6K3bl0Ot1sNBqqJ4J4jmtDIf9yMBjiYdh8WwOj3W5rpmnCum6ZqqoATVUxDE3Vdd0lMGTZMI\u002BPH1iOY8CWBb0Hw9hfjx\u002BfvdbP2RNYW1sLM8zcIuiUK/V6DY9EJqqRCHWv1Xo4pSiSCMpjeomuL0mC3Gw2ZpLJ9NcoGvgV\u002BEQms5yn6amrjuP0eHuOIKhp0B0TPN8trK6\u002B/gXPdxzbxg857uxE03TaI/dsp9OdVVXt205H\u002BkrXjW4qlb7TbD68Fg4H8PX1dcbD9QQWF1PvE0QIdM2lu5omh6anI7dKpQ9/8fv93wjC\u002BYyX4FlRFCdwHPquVLr5I8cpH1sWFqDpaBWCsPlYLPGmh8NcB2wJ3d3dvSMI1jw41MTWVnHPAwiCfCSKyjsU5UUurCDw/lBo6sidlcslEZiPbtwo7iwtZcvd7mnLLRPoNnugTff39xc2NzcbFxQX32g0SuRyq5/k89kfwD0w3KhtI\u002BP371fnKpUvPwAkA32/vb1NF4vFM4\u002BjVyI38DS5G\u002BM4TrIs/Q/QOb09KMr5JChF7WlyF99P7s4HBNzA8wdyJMty76B5XiV9PvPn52MHoyMJwLBRU1UZ91I1TcJIcrruzYfZkQR4nm\u002BCww4gCOriA5KkODQdOhlG7P0bSSCVSp2C50CxbSuoKBoJ3iHu4OBA9kiG2ZEEwAtpWZbRVlWdAq8FhSD2SOVxhUcSeLLCmiBIlChqQRSFB1r5X\u002B/AJRgbw\u002Bsoiq34/ViGJMnfhpH2//vnJvcHXuQnEomfIAj9PBwmtb29T9vgDrwI\u002Bj\u002BL/w13FKdOMSDM/wAAAABJRU5ErkJggg==", + "width": 24, + "height": 24 + } + } + } + }, + { + "id": "4d193e06-a5bf-4411-8933-cb2470015c83", + "language": { + "id": "*.*.python", + "version": "3.*.*" + }, + "title": "RV_loads", + "uri": "commands/RV_loads.py", + "image": { + "light": { + "type": "svg", + "data": "PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzMiAzMiI+CiAgPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI5LjguMTAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiAyLjEuMSBCdWlsZCAyKSAgLS0+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC5zdDAgewogICAgICAgIHN0cm9rZS1taXRlcmxpbWl0OiAzLjY7CiAgICAgICAgc3Ryb2tlLXdpZHRoOiAuMXB4OwogICAgICB9CgogICAgICAuc3QwLCAuc3QxIHsKICAgICAgICBzdHJva2U6ICNhNmE2YWU7CiAgICAgIH0KCiAgICAgIC5zdDAsIC5zdDEsIC5zdDIgewogICAgICAgIHN0cm9rZS1saW5lY2FwOiByb3VuZDsKICAgICAgfQoKICAgICAgLnN0MCwgLnN0MSwgLnN0MiwgLnN0MywgLnN0NCB7CiAgICAgICAgZmlsbDogbm9uZTsKICAgICAgfQoKICAgICAgLnN0MSB7CiAgICAgICAgc3Ryb2tlLW1pdGVybGltaXQ6IDMuMTM7CiAgICAgICAgc3Ryb2tlLXdpZHRoOiAuMDlweDsKICAgICAgfQoKICAgICAgLnN0NSB7CiAgICAgICAgZmlsbDogIzE3OGEzYTsKICAgICAgfQoKICAgICAgLnN0NiB7CiAgICAgICAgZmlsbDogIzZmNmY3ODsKICAgICAgfQoKICAgICAgLnN0MiB7CiAgICAgICAgc3Ryb2tlOiAjMTc4YTNhOwogICAgICAgIHN0cm9rZS1taXRlcmxpbWl0OiAzLjM3OwogICAgICAgIHN0cm9rZS13aWR0aDogMS4xNHB4OwogICAgICB9CgogICAgICAuc3QzIHsKICAgICAgICBzdHJva2U6ICMxMTExMTQ7CiAgICAgICAgc3Ryb2tlLXdpZHRoOiAuNTZweDsKICAgICAgfQoKICAgICAgLnN0MywgLnN0NCB7CiAgICAgICAgc3Ryb2tlLWxpbmVqb2luOiByb3VuZDsKICAgICAgfQoKICAgICAgLnN0NyB7CiAgICAgICAgb3BhY2l0eTogLjExOwogICAgICB9CgogICAgICAuc3Q0IHsKICAgICAgICBzdHJva2U6ICM0YTRhNTI7CiAgICAgICAgc3Ryb2tlLXdpZHRoOiAuNDNweDsKICAgICAgfQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPGcgY2xhc3M9InN0NyI+CiAgICA8cG9seWdvbiBjbGFzcz0ic3Q2IiBwb2ludHM9IjI4LjUyIDE0LjcxIDI3LjM5IDExLjUgMjYuMTEgOC40NiAyNC42NiA2LjM3IDIzLjA4IDYuMTkgMjIuNjcgNi45MSAyMi40MiA3LjEgMjIuMjEgNi44MiAxOS4xNCA0LjE0IDE1LjkgMy4wMiAxMi42NyA0LjE0IDkuNTkgNi44MiA5LjM5IDcuMSA5LjEyIDYuOSA4LjcyIDYuMTkgNy4xNCA2LjM3IDUuNjkgOC40NiA0LjQxIDExLjUgMy4yOSAxNC43MSA2LjE0IDEyLjI0IDguODUgMTAuMjMgMTAuNDggOS41IDExLjkyIDE0LjE1IDEzLjQ2IDIwLjg1IDE0Ljk1IDI4LjI1IDE2Ljg1IDI4LjI1IDE4LjM0IDIwLjg1IDE5Ljg4IDE0LjE1IDIxLjMzIDkuNSAyMi45NSAxMC4yMyAyNS42NiAxMi4yNCAyOC41MiAxNC43MSIvPgogIDwvZz4KICA8ZyBjbGFzcz0ic3Q3Ij4KICAgIDxwb2x5Z29uIGNsYXNzPSJzdDYiIHBvaW50cz0iMjguMTEgMTQuOTggMjcuMDIgMTEuODcgMjUuNzggOC45NCAyNC4zOCA2LjkgMjIuODUgNi43NCAyMi40NSA3LjQ0IDIyLjIxIDcuNjIgMjIuMDEgNy4zNCAxOS4wMyA0Ljc1IDE1LjkgMy42NyAxMi43OCA0Ljc1IDkuOCA3LjM0IDkuNiA3LjYxIDkuMzUgNy40MiA4Ljk2IDYuNzQgNy40MyA2LjkgNi4wMyA4Ljk0IDQuNzkgMTEuODcgMy43IDE0Ljk4IDYuNDYgMTIuNTkgOS4wOSAxMC42NCAxMC42NiA5Ljk0IDEyLjA1IDE0LjQ0IDEzLjU0IDIwLjkyIDE0Ljk5IDI4LjA4IDE2LjgyIDI4LjA4IDE4LjI3IDIwLjkyIDE5Ljc2IDE0LjQ0IDIxLjE1IDkuOTQgMjIuNzIgMTAuNjQgMjUuMzQgMTIuNTkgMjguMTEgMTQuOTgiLz4KICA8L2c+CiAgPGcgY2xhc3M9InN0NyI+CiAgICA8cG9seWdvbiBjbGFzcz0ic3Q2IiBwb2ludHM9IjI3LjcgMTUuMjQgMjYuNjQgMTIuMjQgMjUuNDUgOS40MSAyNC4wOSA3LjQ0IDIyLjYyIDcuMjggMjIuMjMgNy45NiAyMiA4LjEzIDIxLjggNy44NyAxOC45MyA1LjM2IDE1LjkxIDQuMzIgMTIuODggNS4zNiAxMC4wMSA3Ljg3IDkuODIgOC4xMyA5LjU3IDcuOTQgOS4yIDcuMjggNy43MiA3LjQ0IDYuMzcgOS40MSA1LjE3IDEyLjI0IDQuMTIgMTUuMjQgNi43OSAxMi45MyA5LjMyIDExLjA2IDEwLjg0IDEwLjM3IDEyLjE5IDE0LjcyIDEzLjYzIDIwLjk5IDE1LjAyIDI3LjkgMTYuNzkgMjcuOSAxOC4xOSAyMC45OSAxOS42MyAxNC43MiAyMC45OCAxMC4zNyAyMi40OSAxMS4wNiAyNS4wMyAxMi45MyAyNy43IDE1LjI0Ii8+CiAgPC9nPgogIDxnIGNsYXNzPSJzdDciPgogICAgPHBvbHlnb24gY2xhc3M9InN0NiIgcG9pbnRzPSIyNy4yOSAxNS41MSAyNi4yNyAxMi42MiAyNS4xMiA5Ljg4IDIzLjgxIDcuOTggMjIuMzkgNy44MyAyMi4wMSA4LjQ4IDIxLjc5IDguNjUgMjEuNiA4LjM5IDE4LjgzIDUuOTggMTUuOTEgNC45NyAxMi45OSA1Ljk4IDEwLjIyIDguMzkgMTAuMDQgOC42NCA5LjggOC40NiA5LjQzIDcuODMgOC4wMSA3Ljk4IDYuNyA5Ljg4IDUuNTUgMTIuNjIgNC41MyAxNS41MSA3LjExIDEzLjI4IDkuNTYgMTEuNDcgMTEuMDIgMTAuODEgMTIuMzIgMTUgMTMuNzEgMjEuMDUgMTUuMDYgMjcuNzIgMTYuNzYgMjcuNzIgMTguMTEgMjEuMDUgMTkuNSAxNSAyMC44IDEwLjgxIDIyLjI3IDExLjQ3IDI0LjcxIDEzLjI4IDI3LjI5IDE1LjUxIi8+CiAgPC9nPgogIDxnIGNsYXNzPSJzdDciPgogICAgPHBvbHlnb24gY2xhc3M9InN0NiIgcG9pbnRzPSIyNi44OCAxNS43OCAyNS45IDEyLjk5IDI0Ljc5IDEwLjM1IDIzLjUzIDguNTIgMjIuMTYgOC4zNyAyMS44IDkgMjEuNTggOS4xNyAyMS40IDguOTEgMTguNzMgNi41OSAxNS45MSA1LjYyIDEzLjEgNi41OSAxMC40MyA4LjkxIDEwLjI1IDkuMTYgMTAuMDIgOC45OSA5LjY3IDguMzcgOC4zIDguNTIgNy4wNCAxMC4zNSA1LjkzIDEyLjk5IDQuOTUgMTUuNzggNy40MyAxMy42MyA5Ljc5IDExLjg4IDExLjIgMTEuMjUgMTIuNDUgMTUuMjkgMTMuNzkgMjEuMTIgMTUuMDkgMjcuNTUgMTYuNzQgMjcuNTUgMTguMDQgMjEuMTIgMTkuMzggMTUuMjkgMjAuNjMgMTEuMjUgMjIuMDQgMTEuODggMjQuNCAxMy42MyAyNi44OCAxNS43OCIvPgogIDwvZz4KICA8Zz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIzLjk0IiB5MT0iMTQuMjIiIHgyPSI0LjY0IiB5Mj0iMTMuNTciLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI0LjA1IiB5MT0iMTQuMTUiIHgyPSI0LjMxIiB5Mj0iMTMuODciLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIzLjk0IiB5MT0iMTQuMjIiIHgyPSI0LjA1IiB5Mj0iMTQuMTUiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI5LjY3IiB5MT0iNi45OCIgeDI9IjkuNjkiIHkyPSI2LjciLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI0LjE2IiB5MT0iMTQuMDgiIHgyPSI0LjMyIiB5Mj0iMTMuODciLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI0LjA1IiB5MT0iMTQuMTUiIHgyPSI0LjE2IiB5Mj0iMTQuMDgiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI5LjU3IiB5MT0iNi44NiIgeDI9IjkuNjkiIHkyPSI2LjciLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI0LjI3IiB5MT0iMTQuMDEiIHgyPSI0LjQ0IiB5Mj0iMTMuNzYiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI0LjE2IiB5MT0iMTQuMDgiIHgyPSI0LjI3IiB5Mj0iMTQuMDEiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyNy4zNiIgeTE9IjEzLjU3IiB4Mj0iMjcuNjEiIHkyPSIxMy44MiIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjI3Ljk1IiB5MT0iMTQuMTUiIHgyPSIyOC4wNiIgeTI9IjE0LjIyIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjcuNjkiIHkxPSIxMy44NyIgeDI9IjI3Ljk1IiB5Mj0iMTQuMTUiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyNy44NCIgeTE9IjE0LjA4IiB4Mj0iMjcuOTUiIHkyPSIxNC4xNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjI3LjY4IiB5MT0iMTMuODciIHgyPSIyNy44NCIgeTI9IjE0LjA4Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjcuNzMiIHkxPSIxNC4wMSIgeDI9IjI3Ljg0IiB5Mj0iMTQuMDgiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyNy41NiIgeTE9IjEzLjc2IiB4Mj0iMjcuNzMiIHkyPSIxNC4wMSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjIyLjMxIiB5MT0iNi43IiB4Mj0iMjIuNDMiIHkyPSI2Ljg2Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjIuMzEiIHkxPSI2LjciIHgyPSIyMi4zMyIgeTI9IjYuOTgiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI0LjUxIiB5MT0iMTEuMzgiIHgyPSI1LjAyIiB5Mj0iMTEuNzMiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIzLjM5IiB5MT0iMTQuNTkiIHgyPSIzLjQ5IiB5Mj0iMTQuNTIiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI1Ljc5IiB5MT0iOC4zNSIgeDI9IjYuNjciIHkyPSI5LjE0Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iNi42NyIgeTE9IjkuMTQiIHgyPSI3LjUxIiB5Mj0iOS43NyIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjUuMDIiIHkxPSIxMS43MyIgeDI9IjUuNDgiIHkyPSIxMi4wMSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjMuNDkiIHkxPSIxNC41MiIgeDI9IjMuNjEiIHkyPSIxNC40NSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjcuMjQiIHkxPSI2LjI1IiB4Mj0iOC40MSIgeTI9IjcuNDMiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI5LjYzIiB5MT0iOC4zNSIgeDI9IjEwLjE1IiB5Mj0iOC4zOSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjguNDEiIHkxPSI3LjQzIiB4Mj0iOS42MyIgeTI9IjguMzUiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSI5LjYzIiB5MT0iOC4zNSIgeDI9IjEwLjI0IiB5Mj0iOC41NiIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjguMzEiIHkxPSIxMC4xMyIgeDI9IjEwLjQ4IiB5Mj0iOS4wNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjcuNTEiIHkxPSI5Ljc3IiB4Mj0iOC4zMSIgeTI9IjEwLjEzIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iOC4zMSIgeTE9IjEwLjEzIiB4Mj0iOC45NSIgeTI9IjEwLjExIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iNS40OCIgeTE9IjEyLjAxIiB4Mj0iNS45IiB5Mj0iMTIuMTYiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIzLjcyIiB5MT0iMTQuMzgiIHgyPSI1LjkiIHkyPSIxMi4xNiIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjMuNjEiIHkxPSIxNC40NSIgeDI9IjMuNzIiIHkyPSIxNC4zOCIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjUuOSIgeTE9IjEyLjE2IiB4Mj0iOC4zMSIgeTI9IjEwLjEzIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iNS45IiB5MT0iMTIuMTYiIHgyPSI2LjI0IiB5Mj0iMTIuMTIiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIzLjcyIiB5MT0iMTQuMzgiIHgyPSIzLjgzIiB5Mj0iMTQuMjkiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIxNS4wNSIgeTE9IjI4LjEzIiB4Mj0iMTUuMjkiIHkyPSIyOC4xMyIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjE1LjI5IiB5MT0iMjguMTMiIHgyPSIxNS41MiIgeTI9IjI4LjEzIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMTUuNTIiIHkxPSIyOC4xMyIgeDI9IjE1Ljc3IiB5Mj0iMjguMTMiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIxNS43NyIgeTE9IjI4LjEzIiB4Mj0iMTYiIHkyPSIyOC4xMyIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjE2IiB5MT0iMjguMTMiIHgyPSIxNi4yMyIgeTI9IjI4LjEzIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMTYuMjMiIHkxPSIyOC4xMyIgeDI9IjE2LjQ4IiB5Mj0iMjguMTMiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIxNi40OCIgeTE9IjI4LjEzIiB4Mj0iMTYuNzEiIHkyPSIyOC4xMyIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjE2LjcxIiB5MT0iMjguMTMiIHgyPSIxNi45NSIgeTI9IjI4LjEzIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjEuODUiIHkxPSI4LjM5IiB4Mj0iMjIuMzciIHkyPSI4LjM1Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjEuNzYiIHkxPSI4LjU2IiB4Mj0iMjIuMzciIHkyPSI4LjM1Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjMuNjkiIHkxPSIxMC4xMyIgeDI9IjI0LjQ5IiB5Mj0iOS43NyIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjIxLjUyIiB5MT0iOS4wNSIgeDI9IjIzLjY5IiB5Mj0iMTAuMTMiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyMy4wNSIgeTE9IjEwLjExIiB4Mj0iMjMuNjkiIHkyPSIxMC4xMyIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjIyLjM3IiB5MT0iOC4zNSIgeDI9IjIzLjU5IiB5Mj0iNy40MyIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjIzLjU5IiB5MT0iNy40MyIgeDI9IjI0Ljc2IiB5Mj0iNi4yNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjI1LjMzIiB5MT0iOS4xNCIgeDI9IjI2LjIxIiB5Mj0iOC4zNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjI0LjQ5IiB5MT0iOS43NyIgeDI9IjI1LjMzIiB5Mj0iOS4xNCIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjI2LjUyIiB5MT0iMTIuMDEiIHgyPSIyNi45OCIgeTI9IjExLjczIi8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjYuOTgiIHkxPSIxMS43MyIgeDI9IjI3LjQ5IiB5Mj0iMTEuMzgiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyMy42OSIgeTE9IjEwLjEzIiB4Mj0iMjYuMSIgeTI9IjEyLjE2Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjUuNzYiIHkxPSIxMi4xMiIgeDI9IjI2LjEiIHkyPSIxMi4xNiIvPgogICAgPGxpbmUgY2xhc3M9InN0MCIgeDE9IjI2LjEiIHkxPSIxMi4xNiIgeDI9IjI2LjUyIiB5Mj0iMTIuMDEiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyNi4xIiB5MT0iMTIuMTYiIHgyPSIyOC4yOCIgeTI9IjE0LjM4Ii8+CiAgICA8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTIuMDksOC42NmwuMjItLjMyYy4xMi0uMTguMjQtLjU4LjI5LS44OXMuMDgtLjg3LjExLTEuMjUuMDUtMS4wMi4wNS0xLjQzbC4wMi0uNzUiLz4KICAgIDxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xOS4yMyw0LjAybC4wMi43NWMwLC40MS4wNCwxLjA1LjA1LDEuNDNzLjA2Ljk0LjExLDEuMjUuMTYuNzEuMjkuODlsLjIyLjMyIi8+CiAgICA8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTAuODUsOGwuOS0uNTZjLjUtLjMxLDEuNjUtLjc0LDIuNTctLjk2czIuNDItLjIzLDMuMzQsMCwyLjA3LjY2LDIuNTcuOTZsLjkuNTYiLz4KICAgIDxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0zLjM5LDE0LjU5bDEuNS0xLjI5Yy43NS0uNjUsMS45Ny0xLjYzLDIuNzItMi4xOHMxLjcyLTEuMTcsMi4xNy0xLjM3bC44MS0uMzciLz4KICAgIDxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0yMS40Miw5LjM4bC44MS4zN2MuNDUuMjEsMS40Mi44MiwyLjE3LDEuMzdzMS45NywxLjUzLDIuNzIsMi4xOGwxLjUsMS4yOSIvPgogICAgPHBhdGggY2xhc3M9InN0MCIgZD0iTTkuNjksNi43bDEuNzEtMS41Yy43NS0uNjUsMi4wOC0xLjQzLDIuOTctMS43NHMyLjM0LS4zMSwzLjIzLDAsMi4yMywxLjA5LDIuOTcsMS43NGwxLjcxLDEuNSIvPgogICAgPHBhdGggY2xhc3M9InN0MCIgZD0iTTMuMzksMTQuNTlsLjU2LTEuNmMuMzEtLjg4Ljg2LTIuMjksMS4yMS0zLjEzLjM1LS44NC45Ni0xLjk5LDEuMzctMi41N3MxLjA4LTEuMDksMS41MS0xLjE0LDEuMTUuNTQsMS41OSwxLjMyLDEuMDUsMi4xNywxLjMzLDMuMTNsLjUzLDEuNzFjLjI5Ljk1LjcxLDIuNTEuOTQsMy40OGwuNzMsMy4xOGMuMjMuOTYuNTcsMi41NS43NiwzLjUybDEuMTQsNS42MyIvPgogICAgPHBhdGggY2xhc3M9InN0MCIgZD0iTTE2Ljk1LDI4LjEzbDEuMTQtNS42M2MuMi0uOTcuNTQtMi41Ni43Ni0zLjUybC43My0zLjE4Yy4yMy0uOTYuNjQtMi41My45NC0zLjQ4bC41My0xLjcxYy4yOS0uOTUuODktMi4zNSwxLjMzLTMuMTNzMS4xNi0xLjM2LDEuNTktMS4zMiwxLjExLjU2LDEuNTEsMS4xNCwxLjAxLDEuNzMsMS4zNywyLjU3Ljg5LDIuMjQsMS4yMSwzLjEzbC41NiwxLjYiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDAiIHgxPSIxNS4wNSIgeTE9IjI4LjEzIiB4Mj0iMTYuOTUiIHkyPSIyOC4xMyIvPgogIDwvZz4KICA8Zz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSI1LjUyIiB5MT0iMTUuMzUiIHgyPSI2LjEyIiB5Mj0iMTQuNzkiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSI1LjYyIiB5MT0iMTUuMjkiIHgyPSI1Ljg0IiB5Mj0iMTUuMDUiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSI1LjUyIiB5MT0iMTUuMzUiIHgyPSI1LjYyIiB5Mj0iMTUuMjkiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIxMC41IiB5MT0iOS4wNiIgeDI9IjEwLjUyIiB5Mj0iOC44MSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjUuNzEiIHkxPSIxNS4yMyIgeDI9IjUuODUiIHkyPSIxNS4wNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjUuNjIiIHkxPSIxNS4yOSIgeDI9IjUuNzEiIHkyPSIxNS4yMyIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjEwLjQxIiB5MT0iOC45NSIgeDI9IjEwLjUyIiB5Mj0iOC44MSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjUuOCIgeTE9IjE1LjE2IiB4Mj0iNS45NSIgeTI9IjE0Ljk1Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iNS43MSIgeTE9IjE1LjIzIiB4Mj0iNS44IiB5Mj0iMTUuMTYiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyNS44OCIgeTE9IjE0Ljc5IiB4Mj0iMjYuMDkiIHkyPSIxNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjI2LjM4IiB5MT0iMTUuMjkiIHgyPSIyNi40OCIgeTI9IjE1LjM1Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMjYuMTYiIHkxPSIxNS4wNSIgeDI9IjI2LjM4IiB5Mj0iMTUuMjkiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyNi4yOSIgeTE9IjE1LjIzIiB4Mj0iMjYuMzgiIHkyPSIxNS4yOSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjI2LjE1IiB5MT0iMTUuMDUiIHgyPSIyNi4yOSIgeTI9IjE1LjIzIi8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMjYuMiIgeTE9IjE1LjE2IiB4Mj0iMjYuMjkiIHkyPSIxNS4yMyIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjI2LjA1IiB5MT0iMTQuOTUiIHgyPSIyNi4yIiB5Mj0iMTUuMTYiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyMS40OCIgeTE9IjguODEiIHgyPSIyMS41OSIgeTI9IjguOTUiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyMS40OCIgeTE9IjguODEiIHgyPSIyMS41IiB5Mj0iOS4wNiIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjYuMDEiIHkxPSIxMi44OSIgeDI9IjYuNDUiIHkyPSIxMy4xOCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjUuMDQiIHkxPSIxNS42NyIgeDI9IjUuMTMiIHkyPSIxNS42MSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjcuMTMiIHkxPSIxMC4yNSIgeDI9IjcuODkiIHkyPSIxMC45NCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjcuODkiIHkxPSIxMC45NCIgeDI9IjguNjIiIHkyPSIxMS40OCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjYuNDUiIHkxPSIxMy4xOCIgeDI9IjYuODYiIHkyPSIxMy40MyIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjUuMTMiIHkxPSIxNS42MSIgeDI9IjUuMjMiIHkyPSIxNS41NSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjguMzkiIHkxPSI4LjQyIiB4Mj0iOS40MSIgeTI9IjkuNDUiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIxMC40NiIgeTE9IjEwLjI1IiB4Mj0iMTAuOTIiIHkyPSIxMC4yOSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjkuNDEiIHkxPSI5LjQ1IiB4Mj0iMTAuNDYiIHkyPSIxMC4yNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjEwLjQ2IiB5MT0iMTAuMjUiIHgyPSIxMSIgeTI9IjEwLjQzIi8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iOS4zMiIgeTE9IjExLjgiIHgyPSIxMS4yIiB5Mj0iMTAuODYiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSI4LjYyIiB5MT0iMTEuNDgiIHgyPSI5LjMyIiB5Mj0iMTEuOCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjkuMzIiIHkxPSIxMS44IiB4Mj0iOS44OCIgeTI9IjExLjc4Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iNi44NiIgeTE9IjEzLjQzIiB4Mj0iNy4yMiIgeTI9IjEzLjU2Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iNS4zMyIgeTE9IjE1LjQ5IiB4Mj0iNy4yMiIgeTI9IjEzLjU2Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iNS4yMyIgeTE9IjE1LjU1IiB4Mj0iNS4zMyIgeTI9IjE1LjQ5Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iNy4yMiIgeTE9IjEzLjU2IiB4Mj0iOS4zMiIgeTI9IjExLjgiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSI3LjIyIiB5MT0iMTMuNTYiIHgyPSI3LjUyIiB5Mj0iMTMuNTMiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSI1LjMzIiB5MT0iMTUuNDkiIHgyPSI1LjQyIiB5Mj0iMTUuNDEiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIxNS4xOCIgeTE9IjI3LjQ0IiB4Mj0iMTUuMzgiIHkyPSIyNy40NCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjE1LjM4IiB5MT0iMjcuNDQiIHgyPSIxNS41OCIgeTI9IjI3LjQ0Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMTUuNTgiIHkxPSIyNy40NCIgeDI9IjE1LjgiIHkyPSIyNy40NCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjE1LjgiIHkxPSIyNy40NCIgeDI9IjE2IiB5Mj0iMjcuNDQiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIxNiIgeTE9IjI3LjQ0IiB4Mj0iMTYuMiIgeTI9IjI3LjQ0Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMTYuMiIgeTE9IjI3LjQ0IiB4Mj0iMTYuNDIiIHkyPSIyNy40NCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjE2LjQyIiB5MT0iMjcuNDQiIHgyPSIxNi42MiIgeTI9IjI3LjQ0Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMTYuNjIiIHkxPSIyNy40NCIgeDI9IjE2LjgyIiB5Mj0iMjcuNDQiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyMS4wOCIgeTE9IjEwLjI5IiB4Mj0iMjEuNTQiIHkyPSIxMC4yNSIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjIxIiB5MT0iMTAuNDMiIHgyPSIyMS41NCIgeTI9IjEwLjI1Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMjIuNjgiIHkxPSIxMS44IiB4Mj0iMjMuMzgiIHkyPSIxMS40OCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjIwLjgiIHkxPSIxMC44NiIgeDI9IjIyLjY4IiB5Mj0iMTEuOCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjIyLjEyIiB5MT0iMTEuNzgiIHgyPSIyMi42OCIgeTI9IjExLjgiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyMS41NCIgeTE9IjEwLjI1IiB4Mj0iMjIuNTkiIHkyPSI5LjQ1Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMjIuNTkiIHkxPSI5LjQ1IiB4Mj0iMjMuNjEiIHkyPSI4LjQyIi8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMjQuMTEiIHkxPSIxMC45NCIgeDI9IjI0Ljg3IiB5Mj0iMTAuMjUiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyMy4zOCIgeTE9IjExLjQ4IiB4Mj0iMjQuMTEiIHkyPSIxMC45NCIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjI1LjE0IiB5MT0iMTMuNDMiIHgyPSIyNS41NSIgeTI9IjEzLjE4Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMjUuNTUiIHkxPSIxMy4xOCIgeDI9IjI1Ljk5IiB5Mj0iMTIuODkiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyMi42OCIgeTE9IjExLjgiIHgyPSIyNC43OCIgeTI9IjEzLjU2Ii8+CiAgICA8bGluZSBjbGFzcz0ic3QxIiB4MT0iMjQuNDgiIHkxPSIxMy41MyIgeDI9IjI0Ljc4IiB5Mj0iMTMuNTYiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyNC43OCIgeTE9IjEzLjU2IiB4Mj0iMjUuMTQiIHkyPSIxMy40MyIvPgogICAgPGxpbmUgY2xhc3M9InN0MSIgeDE9IjI0Ljc4IiB5MT0iMTMuNTYiIHgyPSIyNi42NyIgeTI9IjE1LjQ5Ii8+CiAgICA8cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTIuNiwxMC41MmwuMTktLjI4Yy4xLS4xNi4yMS0uNS4yNS0uNzhzLjA3LS43Ni4wOS0xLjA5LjA0LS44OC4wNS0xLjI1bC4wMi0uNjUiLz4KICAgIDxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0xOC44MSw2LjQ5bC4wMi42NWMwLC4zNi4wMy45Mi4wNSwxLjI1cy4wNS44MS4wOSwxLjA5LjE0LjYyLjI1Ljc4bC4xOS4yOCIvPgogICAgPHBhdGggY2xhc3M9InN0MSIgZD0iTTExLjUzLDkuOTRsLjc4LS40OWMuNDMtLjI3LDEuNDMtLjY0LDIuMjMtLjg0czIuMTEtLjIsMi45MSwwLDEuOC41NywyLjIzLjg0bC43OC40OSIvPgogICAgPHBhdGggY2xhc3M9InN0MSIgZD0iTTUuMDQsMTUuNjdsMS4zLTEuMTJjLjY1LS41NiwxLjcyLTEuNDIsMi4zNy0xLjlzMS41LTEuMDIsMS44OS0xLjE5bC43LS4zMiIvPgogICAgPHBhdGggY2xhc3M9InN0MSIgZD0iTTIwLjcxLDExLjE1bC43LjMyYy4zOS4xOCwxLjI0LjcxLDEuODksMS4xOXMxLjcyLDEuMzMsMi4zNywxLjlsMS4zLDEuMTIiLz4KICAgIDxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0xMC41Miw4LjgxbDEuNDktMS4zYy42NS0uNTYsMS44MS0xLjI1LDIuNTgtMS41MXMyLjA0LS4yNywyLjgxLDAsMS45My45NSwyLjU4LDEuNTFsMS40OSwxLjMiLz4KICAgIDxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik01LjA0LDE1LjY3bC40OS0xLjM5Yy4yNy0uNzcuNzQtMS45OSwxLjA1LTIuNzIuMzEtLjczLjg0LTEuNzMsMS4xOS0yLjIzcy45NC0uOTUsMS4zMi0uOTksMSwuNDcsMS4zOSwxLjE0LjkxLDEuODksMS4xNiwyLjcybC40NiwxLjQ5Yy4yNS44Mi42MiwyLjE5LjgxLDMuMDJsLjYzLDIuNzZjLjIuODQuNDksMi4yMi42NiwzLjA2bC45OSw0Ljg5Ii8+CiAgICA8cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTYuODIsMjcuNDRsLjk5LTQuODljLjE3LS44NS40Ny0yLjIyLjY2LTMuMDZsLjYzLTIuNzZjLjItLjg0LjU2LTIuMi44MS0zLjAybC40Ni0xLjQ5Yy4yNS0uODIuNzgtMi4wNCwxLjE2LTIuNzJzMS4wMS0xLjE4LDEuMzktMS4xNC45Ni40OSwxLjMyLjk5Ljg4LDEuNSwxLjE5LDIuMjMuNzgsMS45NSwxLjA1LDIuNzJsLjQ5LDEuMzkiLz4KICAgIDxsaW5lIGNsYXNzPSJzdDEiIHgxPSIxNS4xOCIgeTE9IjI3LjQ0IiB4Mj0iMTYuODIiIHkyPSIyNy40NCIvPgogIDwvZz4KICA8cG9seWdvbiBjbGFzcz0ic3Q0IiBwb2ludHM9IjI2Ljg4IDE1Ljc4IDI1LjkgMTIuOTkgMjQuNzkgMTAuMzUgMjMuNTMgOC41MiAyMi4xNiA4LjM3IDIxLjggOSAyMS41OCA5LjE3IDIxLjQgOC45MSAxOC43MyA2LjU5IDE1LjkxIDUuNjIgMTMuMSA2LjU5IDEwLjQzIDguOTEgMTAuMjUgOS4xNiAxMC4wMiA4Ljk5IDkuNjcgOC4zNyA4LjMgOC41MiA3LjA0IDEwLjM1IDUuOTMgMTIuOTkgNC45NSAxNS43OCA3LjQzIDEzLjYzIDkuNzkgMTEuODggMTEuMiAxMS4yNSAxMi40NSAxNS4yOSAxMy43OSAyMS4xMiAxNS4wOSAyNy41NSAxNi43NCAyNy41NSAxOC4wNCAyMS4xMiAxOS4zOCAxNS4yOSAyMC42MyAxMS4yNSAyMi4wNCAxMS44OCAyNC40IDEzLjYzIDI2Ljg4IDE1Ljc4Ii8+CiAgPHBvbHlnb24gY2xhc3M9InN0MyIgcG9pbnRzPSIyOC41MiAxNC43MSAyNy4zOSAxMS41IDI2LjExIDguNDYgMjQuNjYgNi4zNyAyMy4wOCA2LjE5IDIyLjY3IDYuOTEgMjIuNDIgNy4xIDIyLjIxIDYuODIgMTkuMTQgNC4xNCAxNS45IDMuMDIgMTIuNjcgNC4xNCA5LjU5IDYuODIgOS4zOSA3LjEgOS4xMiA2LjkgOC43MiA2LjE5IDcuMTQgNi4zNyA1LjY5IDguNDYgNC40MSAxMS41IDMuMjkgMTQuNzEgNi4xNCAxMi4yNCA4Ljg1IDEwLjIzIDEwLjQ4IDkuNSAxMS45MiAxNC4xNSAxMy40NiAyMC44NSAxNC45NSAyOC4yNSAxNi44NSAyOC4yNSAxOC4zNCAyMC44NSAxOS44OCAxNC4xNSAyMS4zMyA5LjUgMjIuOTUgMTAuMjMgMjUuNjYgMTIuMjQgMjguNTIgMTQuNzEiLz4KICA8bGluZSBjbGFzcz0ic3QyIiB4MT0iNS40OCIgeTE9IjIuOTgiIHgyPSI1LjQ4IiB5Mj0iOC4zNCIvPgogIDxwb2x5Z29uIGNsYXNzPSJzdDUiIHBvaW50cz0iNS40OCAxMC45NyAzLjQ2IDcuOTcgNy41MSA3Ljk3IDUuNDggMTAuOTciLz4KICA8bGluZSBjbGFzcz0ic3QyIiB4MT0iMjYuMiIgeTE9IjIuOTgiIHgyPSIyNi4yIiB5Mj0iOC4zNCIvPgogIDxwb2x5Z29uIGNsYXNzPSJzdDUiIHBvaW50cz0iMjYuMiAxMC45NyAyNC4xOCA3Ljk3IDI4LjIyIDcuOTcgMjYuMiAxMC45NyIvPgogIDxsaW5lIGNsYXNzPSJzdDIiIHgxPSI5LjkzIiB5MT0iLjk3IiB4Mj0iOS45MyIgeTI9IjYuMzMiLz4KICA8cG9seWdvbiBjbGFzcz0ic3Q1IiBwb2ludHM9IjkuOTMgOC45NyA3LjkxIDUuOTYgMTEuOTYgNS45NiA5LjkzIDguOTciLz4KICA8bGluZSBjbGFzcz0ic3QyIiB4MT0iMjEuMzEiIHkxPSIuOTciIHgyPSIyMS4zMSIgeTI9IjYuMzMiLz4KICA8cG9seWdvbiBjbGFzcz0ic3Q1IiBwb2ludHM9IjIxLjMxIDguOTcgMTkuMjkgNS45NiAyMy4zMyA1Ljk2IDIxLjMxIDguOTciLz4KICA8bGluZSBjbGFzcz0ic3QyIiB4MT0iMTUuNjIiIHkxPSIuNiIgeDI9IjE1LjYyIiB5Mj0iNS45NiIvPgogIDxwb2x5Z29uIGNsYXNzPSJzdDUiIHBvaW50cz0iMTUuNjIgOC41OSAxMy42IDUuNTggMTcuNjQgNS41OCAxNS42MiA4LjU5Ii8+Cjwvc3ZnPgo=" + } + } + }, + { + "id": "999051c0-e7a0-4af8-937c-233ff85436b1", + "language": { + "id": "*.*.python", + "version": "3.*.*" + }, + "title": "RV_tno_analysis", + "uri": "commands/RV_tno_analysis.py", + "image": { + "light": { + "type": "svg", + "data": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0zIC0zIDM4IDM4IiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0icnZiYXIiIHgxPSIxOC42OCIgeTE9IjAiIHgyPSIyOS44OSIgeTI9IjAiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMyZTRhYjUiPjwvc3RvcD48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNiZGM5ZmEiPjwvc3RvcD48L2xpbmVhckdyYWRpZW50PjwvZGVmcz4KPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTAuODAwIC0xLjE4MCkgc2NhbGUoMS4wNSkiPjxnIG9wYWNpdHk9Ii40Ij48cG9seWdvbiBmaWxsPSIjRDhEOERFIiBwb2ludHM9IjI5Ljg5IDE1LjEgMjguNjQgMTEuNTQgMjcuMjIgOC4xNyAyNS42MSA1Ljg0IDIzLjg2IDUuNjUgMjMuNCA2LjQ1IDIzLjEyIDYuNjYgMjIuODkgNi4zNCAxOS40OCAzLjM3IDE1Ljg5IDIuMTMgMTIuMyAzLjM3IDguODkgNi4zNCA4LjY2IDYuNjUgOC4zNyA2LjQzIDcuOTIgNS42NSA2LjE3IDUuODQgNC41NiA4LjE3IDMuMTQgMTEuNTQgMS44OSAxNS4xIDUuMDYgMTIuMzYgOC4wNyAxMC4xMyA5Ljg3IDkuMzIgMTEuNDcgMTQuNDggMTMuMTggMjEuOTIgMTQuODQgMzAuMTMgMTYuOTQgMzAuMTMgMTguNiAyMS45MiAyMC4zMSAxNC40OCAyMS45MSA5LjMyIDIzLjcxIDEwLjEzIDI2LjcyIDEyLjM2IDI5Ljg5IDE1LjEiPjwvcG9seWdvbj48L2c+PC9nPgo8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLjgwMCAxLjE4MCkgc2NhbGUoMC45NSkiPjxnIG9wYWNpdHk9Ii40Ij48cG9seWdvbiBmaWxsPSIjRDhEOERFIiBwb2ludHM9IjI5Ljg5IDE1LjEgMjguNjQgMTEuNTQgMjcuMjIgOC4xNyAyNS42MSA1Ljg0IDIzLjg2IDUuNjUgMjMuNCA2LjQ1IDIzLjEyIDYuNjYgMjIuODkgNi4zNCAxOS40OCAzLjM3IDE1Ljg5IDIuMTMgMTIuMyAzLjM3IDguODkgNi4zNCA4LjY2IDYuNjUgOC4zNyA2LjQzIDcuOTIgNS42NSA2LjE3IDUuODQgNC41NiA4LjE3IDMuMTQgMTEuNTQgMS44OSAxNS4xIDUuMDYgMTIuMzYgOC4wNyAxMC4xMyA5Ljg3IDkuMzIgMTEuNDcgMTQuNDggMTMuMTggMjEuOTIgMTQuODQgMzAuMTMgMTYuOTQgMzAuMTMgMTguNiAyMS45MiAyMC4zMSAxNC40OCAyMS45MSA5LjMyIDIzLjcxIDEwLjEzIDI2LjcyIDEyLjM2IDI5Ljg5IDE1LjEiPjwvcG9seWdvbj48L2c+PC9nPgo8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMS4xMjAgLTEuNjkwKSBzY2FsZSgxLjA3KSI+PGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjQzJDMkM5IiBzdHJva2Utd2lkdGg9IjAuMTEiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PGxpbmUgeDE9IjIuNjIiIHkxPSIxNC41NiIgeDI9IjMuMzkiIHkyPSIxMy44NCI+PC9saW5lPjxsaW5lIHgxPSIyLjc0IiB5MT0iMTQuNDgiIHgyPSIzLjAzIiB5Mj0iMTQuMTciPjwvbGluZT48bGluZSB4MT0iMi42MiIgeTE9IjE0LjU2IiB4Mj0iMi43NCIgeTI9IjE0LjQ4Ij48L2xpbmU+PGxpbmUgeDE9IjguOTgiIHkxPSI2LjUyIiB4Mj0iOSIgeTI9IjYuMjEiPjwvbGluZT48bGluZSB4MT0iMi44NiIgeTE9IjE0LjQiIHgyPSIzLjA0IiB5Mj0iMTQuMTciPjwvbGluZT48bGluZSB4MT0iMi43NCIgeTE9IjE0LjQ4IiB4Mj0iMi44NiIgeTI9IjE0LjQiPjwvbGluZT48bGluZSB4MT0iOC44NiIgeTE9IjYuMzkiIHgyPSI5IiB5Mj0iNi4yMSI+PC9saW5lPjxsaW5lIHgxPSIyLjk4IiB5MT0iMTQuMzIiIHgyPSIzLjE3IiB5Mj0iMTQuMDUiPjwvbGluZT48bGluZSB4MT0iMi44NiIgeTE9IjE0LjQiIHgyPSIyLjk4IiB5Mj0iMTQuMzIiPjwvbGluZT48bGluZSB4MT0iMjguNjEiIHkxPSIxMy44NCIgeDI9IjI4Ljg5IiB5Mj0iMTQuMTEiPjwvbGluZT48bGluZSB4MT0iMjkuMjYiIHkxPSIxNC40OCIgeDI9IjI5LjM4IiB5Mj0iMTQuNTYiPjwvbGluZT48bGluZSB4MT0iMjguOTciIHkxPSIxNC4xNyIgeDI9IjI5LjI2IiB5Mj0iMTQuNDgiPjwvbGluZT48bGluZSB4MT0iMjkuMTQiIHkxPSIxNC40IiB4Mj0iMjkuMjYiIHkyPSIxNC40OCI+PC9saW5lPjxsaW5lIHgxPSIyOC45NiIgeTE9IjE0LjE3IiB4Mj0iMjkuMTQiIHkyPSIxNC40Ij48L2xpbmU+PGxpbmUgeDE9IjI5LjAyIiB5MT0iMTQuMzIiIHgyPSIyOS4xNCIgeTI9IjE0LjQiPjwvbGluZT48bGluZSB4MT0iMjguODMiIHkxPSIxNC4wNSIgeDI9IjI5LjAyIiB5Mj0iMTQuMzIiPjwvbGluZT48bGluZSB4MT0iMjMiIHkxPSI2LjIxIiB4Mj0iMjMuMTQiIHkyPSI2LjM5Ij48L2xpbmU+PGxpbmUgeDE9IjIzIiB5MT0iNi4yMSIgeDI9IjIzLjAyIiB5Mj0iNi41MiI+PC9saW5lPjxsaW5lIHgxPSIzLjI1IiB5MT0iMTEuNDEiIHgyPSIzLjgxIiB5Mj0iMTEuNzkiPjwvbGluZT48bGluZSB4MT0iMiIgeTE9IjE0Ljk3IiB4Mj0iMi4xMiIgeTI9IjE0Ljg5Ij48L2xpbmU+PGxpbmUgeDE9IjQuNjciIHkxPSI4LjA0IiB4Mj0iNS42NSIgeTI9IjguOTIiPjwvbGluZT48bGluZSB4MT0iNS42NSIgeTE9IjguOTIiIHgyPSI2LjU4IiB5Mj0iOS42MiI+PC9saW5lPjxsaW5lIHgxPSIzLjgxIiB5MT0iMTEuNzkiIHgyPSI0LjMzIiB5Mj0iMTIuMSI+PC9saW5lPjxsaW5lIHgxPSIyLjEyIiB5MT0iMTQuODkiIHgyPSIyLjI1IiB5Mj0iMTQuODEiPjwvbGluZT48bGluZSB4MT0iNi4yOCIgeTE9IjUuNzEiIHgyPSI3LjU4IiB5Mj0iNy4wMiI+PC9saW5lPjxsaW5lIHgxPSI4LjkzIiB5MT0iOC4wNCIgeDI9IjkuNTEiIHkyPSI4LjA5Ij48L2xpbmU+PGxpbmUgeDE9IjcuNTgiIHkxPSI3LjAyIiB4Mj0iOC45MyIgeTI9IjguMDQiPjwvbGluZT48bGluZSB4MT0iOC45MyIgeTE9IjguMDQiIHgyPSI5LjYxIiB5Mj0iOC4yOCI+PC9saW5lPjxsaW5lIHgxPSI3LjQ3IiB5MT0iMTAuMDIiIHgyPSI5Ljg3IiB5Mj0iOC44MiI+PC9saW5lPjxsaW5lIHgxPSI2LjU4IiB5MT0iOS42MiIgeDI9IjcuNDciIHkyPSIxMC4wMiI+PC9saW5lPjxsaW5lIHgxPSI3LjQ3IiB5MT0iMTAuMDIiIHgyPSI4LjE4IiB5Mj0iMTAiPjwvbGluZT48bGluZSB4MT0iNC4zMyIgeTE9IjEyLjEiIHgyPSI0Ljc5IiB5Mj0iMTIuMjciPjwvbGluZT48bGluZSB4MT0iMi4zNyIgeTE9IjE0LjczIiB4Mj0iNC43OSIgeTI9IjEyLjI3Ij48L2xpbmU+PGxpbmUgeDE9IjIuMjUiIHkxPSIxNC44MSIgeDI9IjIuMzciIHkyPSIxNC43MyI+PC9saW5lPjxsaW5lIHgxPSI0Ljc5IiB5MT0iMTIuMjciIHgyPSI3LjQ3IiB5Mj0iMTAuMDIiPjwvbGluZT48bGluZSB4MT0iNC43OSIgeTE9IjEyLjI3IiB4Mj0iNS4xNyIgeTI9IjEyLjIzIj48L2xpbmU+PGxpbmUgeDE9IjIuMzciIHkxPSIxNC43MyIgeDI9IjIuNDkiIHkyPSIxNC42NCI+PC9saW5lPjxsaW5lIHgxPSIxNC45NSIgeTE9IjMwIiB4Mj0iMTUuMjEiIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIxNS4yMSIgeTE9IjMwIiB4Mj0iMTUuNDciIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIxNS40NyIgeTE9IjMwIiB4Mj0iMTUuNzQiIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIxNS43NCIgeTE9IjMwIiB4Mj0iMTYiIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIxNiIgeTE9IjMwIiB4Mj0iMTYuMjYiIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIxNi4yNiIgeTE9IjMwIiB4Mj0iMTYuNTMiIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIxNi41MyIgeTE9IjMwIiB4Mj0iMTYuNzkiIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIxNi43OSIgeTE9IjMwIiB4Mj0iMTcuMDUiIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIyMi40OSIgeTE9IjguMDkiIHgyPSIyMy4wNyIgeTI9IjguMDQiPjwvbGluZT48bGluZSB4MT0iMjIuMzkiIHkxPSI4LjI4IiB4Mj0iMjMuMDciIHkyPSI4LjA0Ij48L2xpbmU+PGxpbmUgeDE9IjI0LjUzIiB5MT0iMTAuMDIiIHgyPSIyNS40MiIgeTI9IjkuNjIiPjwvbGluZT48bGluZSB4MT0iMjIuMTMiIHkxPSI4LjgyIiB4Mj0iMjQuNTMiIHkyPSIxMC4wMiI+PC9saW5lPjxsaW5lIHgxPSIyMy44MiIgeTE9IjEwIiB4Mj0iMjQuNTMiIHkyPSIxMC4wMiI+PC9saW5lPjxsaW5lIHgxPSIyMy4wNyIgeTE9IjguMDQiIHgyPSIyNC40MiIgeTI9IjcuMDIiPjwvbGluZT48bGluZSB4MT0iMjQuNDIiIHkxPSI3LjAyIiB4Mj0iMjUuNzIiIHkyPSI1LjcxIj48L2xpbmU+PGxpbmUgeDE9IjI2LjM1IiB5MT0iOC45MiIgeDI9IjI3LjMzIiB5Mj0iOC4wNCI+PC9saW5lPjxsaW5lIHgxPSIyNS40MiIgeTE9IjkuNjIiIHgyPSIyNi4zNSIgeTI9IjguOTIiPjwvbGluZT48bGluZSB4MT0iMjcuNjciIHkxPSIxMi4xIiB4Mj0iMjguMTkiIHkyPSIxMS43OSI+PC9saW5lPjxsaW5lIHgxPSIyOC4xOSIgeTE9IjExLjc5IiB4Mj0iMjguNzUiIHkyPSIxMS40MSI+PC9saW5lPjxsaW5lIHgxPSIyNC41MyIgeTE9IjEwLjAyIiB4Mj0iMjcuMjEiIHkyPSIxMi4yNyI+PC9saW5lPjxsaW5lIHgxPSIyNi44MyIgeTE9IjEyLjIzIiB4Mj0iMjcuMjEiIHkyPSIxMi4yNyI+PC9saW5lPjxsaW5lIHgxPSIyNy4yMSIgeTE9IjEyLjI3IiB4Mj0iMjcuNjciIHkyPSIxMi4xIj48L2xpbmU+PGxpbmUgeDE9IjI3LjIxIiB5MT0iMTIuMjciIHgyPSIyOS42MyIgeTI9IjE0LjczIj48L2xpbmU+PHBhdGggZD0iTTExLjY2LDguMzlsLjI0LS4zNmMuMTMtLjIuMjctLjY0LjMyLS45OXMuMDktLjk3LjEyLTEuMzkuMDUtMS4xMy4wNi0xLjU5bC4wMi0uODMiPjwvcGF0aD48cGF0aCBkPSJNMTkuNTksMy4yNGwuMDIuODNjLjAxLjQ2LjA0LDEuMTcuMDYsMS41OXMuMDcsMS4wNC4xMiwxLjM5LjE4Ljc5LjMyLjk5bC4yNC4zNiI+PC9wYXRoPjxwYXRoIGQ9Ik0xMC4yOSw3LjY1bDEtLjYyYy41NS0uMzQsMS44My0uODIsMi44NS0xLjA3czIuNjktLjI1LDMuNzEsMCwyLjMuNzMsMi44NSwxLjA3bDEsLjYyIj48L3BhdGg+PHBhdGggZD0iTTIsMTQuOTdsMS42Ni0xLjQzYy44My0uNzIsMi4xOS0xLjgxLDMuMDItMi40MnMxLjkxLTEuMywyLjQxLTEuNTJsLjktLjQxIj48L3BhdGg+PHBhdGggZD0iTTIyLjAxLDkuMTlsLjkuNDFjLjUuMjMsMS41OC45MSwyLjQxLDEuNTJzMi4xOSwxLjcsMy4wMiwyLjQybDEuNjYsMS40MyI+PC9wYXRoPjxwYXRoIGQ9Ik05LDYuMjFsMS45LTEuNjZjLjgzLS43MiwyLjMxLTEuNTksMy4zLTEuOTNzMi42LS4zNCwzLjU5LDAsMi40NywxLjIxLDMuMywxLjkzbDEuOSwxLjY2Ij48L3BhdGg+PHBhdGggZD0iTTIsMTQuOTdsLjYyLTEuNzhjLjM0LS45OC45NS0yLjU0LDEuMzQtMy40N3MxLjA3LTIuMjEsMS41Mi0yLjg1LDEuMi0xLjIxLDEuNjgtMS4yNiwxLjI4LjYsMS43NywxLjQ2LDEuMTYsMi40MSwxLjQ4LDMuNDdsLjU5LDEuOWMuMzIsMS4wNS43OSwyLjc5LDEuMDQsMy44NmwuODEsMy41M2MuMjUsMS4wNy42MywyLjgzLjg0LDMuOTFsMS4yNiw2LjI1Ij48L3BhdGg+PHBhdGggZD0iTTE3LjA1LDMwbDEuMjYtNi4yNWMuMjItMS4wOC42LTIuODQuODQtMy45MWwuODEtMy41M2MuMjUtMS4wNy43MS0yLjgxLDEuMDQtMy44NmwuNTktMS45Yy4zMi0xLjA1Ljk5LTIuNjEsMS40OC0zLjQ3czEuMjktMS41MSwxLjc3LTEuNDYsMS4yMy42MiwxLjY4LDEuMjYsMS4xMiwxLjkyLDEuNTIsMi44NS45OSwyLjQ5LDEuMzQsMy40N2wuNjIsMS43OCI+PC9wYXRoPjxsaW5lIHgxPSIxNC45NSIgeTE9IjMwIiB4Mj0iMTcuMDUiIHkyPSIzMCI+PC9saW5lPjwvZz48L2c+CjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEuMTIwIDEuNjkwKSBzY2FsZSgwLjkzKSI+PGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjQzJDMkM5IiBzdHJva2Utd2lkdGg9IjAuMTEiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PGxpbmUgeDE9IjIuNjIiIHkxPSIxNC41NiIgeDI9IjMuMzkiIHkyPSIxMy44NCI+PC9saW5lPjxsaW5lIHgxPSIyLjc0IiB5MT0iMTQuNDgiIHgyPSIzLjAzIiB5Mj0iMTQuMTciPjwvbGluZT48bGluZSB4MT0iMi42MiIgeTE9IjE0LjU2IiB4Mj0iMi43NCIgeTI9IjE0LjQ4Ij48L2xpbmU+PGxpbmUgeDE9IjguOTgiIHkxPSI2LjUyIiB4Mj0iOSIgeTI9IjYuMjEiPjwvbGluZT48bGluZSB4MT0iMi44NiIgeTE9IjE0LjQiIHgyPSIzLjA0IiB5Mj0iMTQuMTciPjwvbGluZT48bGluZSB4MT0iMi43NCIgeTE9IjE0LjQ4IiB4Mj0iMi44NiIgeTI9IjE0LjQiPjwvbGluZT48bGluZSB4MT0iOC44NiIgeTE9IjYuMzkiIHgyPSI5IiB5Mj0iNi4yMSI+PC9saW5lPjxsaW5lIHgxPSIyLjk4IiB5MT0iMTQuMzIiIHgyPSIzLjE3IiB5Mj0iMTQuMDUiPjwvbGluZT48bGluZSB4MT0iMi44NiIgeTE9IjE0LjQiIHgyPSIyLjk4IiB5Mj0iMTQuMzIiPjwvbGluZT48bGluZSB4MT0iMjguNjEiIHkxPSIxMy44NCIgeDI9IjI4Ljg5IiB5Mj0iMTQuMTEiPjwvbGluZT48bGluZSB4MT0iMjkuMjYiIHkxPSIxNC40OCIgeDI9IjI5LjM4IiB5Mj0iMTQuNTYiPjwvbGluZT48bGluZSB4MT0iMjguOTciIHkxPSIxNC4xNyIgeDI9IjI5LjI2IiB5Mj0iMTQuNDgiPjwvbGluZT48bGluZSB4MT0iMjkuMTQiIHkxPSIxNC40IiB4Mj0iMjkuMjYiIHkyPSIxNC40OCI+PC9saW5lPjxsaW5lIHgxPSIyOC45NiIgeTE9IjE0LjE3IiB4Mj0iMjkuMTQiIHkyPSIxNC40Ij48L2xpbmU+PGxpbmUgeDE9IjI5LjAyIiB5MT0iMTQuMzIiIHgyPSIyOS4xNCIgeTI9IjE0LjQiPjwvbGluZT48bGluZSB4MT0iMjguODMiIHkxPSIxNC4wNSIgeDI9IjI5LjAyIiB5Mj0iMTQuMzIiPjwvbGluZT48bGluZSB4MT0iMjMiIHkxPSI2LjIxIiB4Mj0iMjMuMTQiIHkyPSI2LjM5Ij48L2xpbmU+PGxpbmUgeDE9IjIzIiB5MT0iNi4yMSIgeDI9IjIzLjAyIiB5Mj0iNi41MiI+PC9saW5lPjxsaW5lIHgxPSIzLjI1IiB5MT0iMTEuNDEiIHgyPSIzLjgxIiB5Mj0iMTEuNzkiPjwvbGluZT48bGluZSB4MT0iMiIgeTE9IjE0Ljk3IiB4Mj0iMi4xMiIgeTI9IjE0Ljg5Ij48L2xpbmU+PGxpbmUgeDE9IjQuNjciIHkxPSI4LjA0IiB4Mj0iNS42NSIgeTI9IjguOTIiPjwvbGluZT48bGluZSB4MT0iNS42NSIgeTE9IjguOTIiIHgyPSI2LjU4IiB5Mj0iOS42MiI+PC9saW5lPjxsaW5lIHgxPSIzLjgxIiB5MT0iMTEuNzkiIHgyPSI0LjMzIiB5Mj0iMTIuMSI+PC9saW5lPjxsaW5lIHgxPSIyLjEyIiB5MT0iMTQuODkiIHgyPSIyLjI1IiB5Mj0iMTQuODEiPjwvbGluZT48bGluZSB4MT0iNi4yOCIgeTE9IjUuNzEiIHgyPSI3LjU4IiB5Mj0iNy4wMiI+PC9saW5lPjxsaW5lIHgxPSI4LjkzIiB5MT0iOC4wNCIgeDI9IjkuNTEiIHkyPSI4LjA5Ij48L2xpbmU+PGxpbmUgeDE9IjcuNTgiIHkxPSI3LjAyIiB4Mj0iOC45MyIgeTI9IjguMDQiPjwvbGluZT48bGluZSB4MT0iOC45MyIgeTE9IjguMDQiIHgyPSI5LjYxIiB5Mj0iOC4yOCI+PC9saW5lPjxsaW5lIHgxPSI3LjQ3IiB5MT0iMTAuMDIiIHgyPSI5Ljg3IiB5Mj0iOC44MiI+PC9saW5lPjxsaW5lIHgxPSI2LjU4IiB5MT0iOS42MiIgeDI9IjcuNDciIHkyPSIxMC4wMiI+PC9saW5lPjxsaW5lIHgxPSI3LjQ3IiB5MT0iMTAuMDIiIHgyPSI4LjE4IiB5Mj0iMTAiPjwvbGluZT48bGluZSB4MT0iNC4zMyIgeTE9IjEyLjEiIHgyPSI0Ljc5IiB5Mj0iMTIuMjciPjwvbGluZT48bGluZSB4MT0iMi4zNyIgeTE9IjE0LjczIiB4Mj0iNC43OSIgeTI9IjEyLjI3Ij48L2xpbmU+PGxpbmUgeDE9IjIuMjUiIHkxPSIxNC44MSIgeDI9IjIuMzciIHkyPSIxNC43MyI+PC9saW5lPjxsaW5lIHgxPSI0Ljc5IiB5MT0iMTIuMjciIHgyPSI3LjQ3IiB5Mj0iMTAuMDIiPjwvbGluZT48bGluZSB4MT0iNC43OSIgeTE9IjEyLjI3IiB4Mj0iNS4xNyIgeTI9IjEyLjIzIj48L2xpbmU+PGxpbmUgeDE9IjIuMzciIHkxPSIxNC43MyIgeDI9IjIuNDkiIHkyPSIxNC42NCI+PC9saW5lPjxsaW5lIHgxPSIxNC45NSIgeTE9IjMwIiB4Mj0iMTUuMjEiIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIxNS4yMSIgeTE9IjMwIiB4Mj0iMTUuNDciIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIxNS40NyIgeTE9IjMwIiB4Mj0iMTUuNzQiIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIxNS43NCIgeTE9IjMwIiB4Mj0iMTYiIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIxNiIgeTE9IjMwIiB4Mj0iMTYuMjYiIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIxNi4yNiIgeTE9IjMwIiB4Mj0iMTYuNTMiIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIxNi41MyIgeTE9IjMwIiB4Mj0iMTYuNzkiIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIxNi43OSIgeTE9IjMwIiB4Mj0iMTcuMDUiIHkyPSIzMCI+PC9saW5lPjxsaW5lIHgxPSIyMi40OSIgeTE9IjguMDkiIHgyPSIyMy4wNyIgeTI9IjguMDQiPjwvbGluZT48bGluZSB4MT0iMjIuMzkiIHkxPSI4LjI4IiB4Mj0iMjMuMDciIHkyPSI4LjA0Ij48L2xpbmU+PGxpbmUgeDE9IjI0LjUzIiB5MT0iMTAuMDIiIHgyPSIyNS40MiIgeTI9IjkuNjIiPjwvbGluZT48bGluZSB4MT0iMjIuMTMiIHkxPSI4LjgyIiB4Mj0iMjQuNTMiIHkyPSIxMC4wMiI+PC9saW5lPjxsaW5lIHgxPSIyMy44MiIgeTE9IjEwIiB4Mj0iMjQuNTMiIHkyPSIxMC4wMiI+PC9saW5lPjxsaW5lIHgxPSIyMy4wNyIgeTE9IjguMDQiIHgyPSIyNC40MiIgeTI9IjcuMDIiPjwvbGluZT48bGluZSB4MT0iMjQuNDIiIHkxPSI3LjAyIiB4Mj0iMjUuNzIiIHkyPSI1LjcxIj48L2xpbmU+PGxpbmUgeDE9IjI2LjM1IiB5MT0iOC45MiIgeDI9IjI3LjMzIiB5Mj0iOC4wNCI+PC9saW5lPjxsaW5lIHgxPSIyNS40MiIgeTE9IjkuNjIiIHgyPSIyNi4zNSIgeTI9IjguOTIiPjwvbGluZT48bGluZSB4MT0iMjcuNjciIHkxPSIxMi4xIiB4Mj0iMjguMTkiIHkyPSIxMS43OSI+PC9saW5lPjxsaW5lIHgxPSIyOC4xOSIgeTE9IjExLjc5IiB4Mj0iMjguNzUiIHkyPSIxMS40MSI+PC9saW5lPjxsaW5lIHgxPSIyNC41MyIgeTE9IjEwLjAyIiB4Mj0iMjcuMjEiIHkyPSIxMi4yNyI+PC9saW5lPjxsaW5lIHgxPSIyNi44MyIgeTE9IjEyLjIzIiB4Mj0iMjcuMjEiIHkyPSIxMi4yNyI+PC9saW5lPjxsaW5lIHgxPSIyNy4yMSIgeTE9IjEyLjI3IiB4Mj0iMjcuNjciIHkyPSIxMi4xIj48L2xpbmU+PGxpbmUgeDE9IjI3LjIxIiB5MT0iMTIuMjciIHgyPSIyOS42MyIgeTI9IjE0LjczIj48L2xpbmU+PHBhdGggZD0iTTExLjY2LDguMzlsLjI0LS4zNmMuMTMtLjIuMjctLjY0LjMyLS45OXMuMDktLjk3LjEyLTEuMzkuMDUtMS4xMy4wNi0xLjU5bC4wMi0uODMiPjwvcGF0aD48cGF0aCBkPSJNMTkuNTksMy4yNGwuMDIuODNjLjAxLjQ2LjA0LDEuMTcuMDYsMS41OXMuMDcsMS4wNC4xMiwxLjM5LjE4Ljc5LjMyLjk5bC4yNC4zNiI+PC9wYXRoPjxwYXRoIGQ9Ik0xMC4yOSw3LjY1bDEtLjYyYy41NS0uMzQsMS44My0uODIsMi44NS0xLjA3czIuNjktLjI1LDMuNzEsMCwyLjMuNzMsMi44NSwxLjA3bDEsLjYyIj48L3BhdGg+PHBhdGggZD0iTTIsMTQuOTdsMS42Ni0xLjQzYy44My0uNzIsMi4xOS0xLjgxLDMuMDItMi40MnMxLjkxLTEuMywyLjQxLTEuNTJsLjktLjQxIj48L3BhdGg+PHBhdGggZD0iTTIyLjAxLDkuMTlsLjkuNDFjLjUuMjMsMS41OC45MSwyLjQxLDEuNTJzMi4xOSwxLjcsMy4wMiwyLjQybDEuNjYsMS40MyI+PC9wYXRoPjxwYXRoIGQ9Ik05LDYuMjFsMS45LTEuNjZjLjgzLS43MiwyLjMxLTEuNTksMy4zLTEuOTNzMi42LS4zNCwzLjU5LDAsMi40NywxLjIxLDMuMywxLjkzbDEuOSwxLjY2Ij48L3BhdGg+PHBhdGggZD0iTTIsMTQuOTdsLjYyLTEuNzhjLjM0LS45OC45NS0yLjU0LDEuMzQtMy40N3MxLjA3LTIuMjEsMS41Mi0yLjg1LDEuMi0xLjIxLDEuNjgtMS4yNiwxLjI4LjYsMS43NywxLjQ2LDEuMTYsMi40MSwxLjQ4LDMuNDdsLjU5LDEuOWMuMzIsMS4wNS43OSwyLjc5LDEuMDQsMy44NmwuODEsMy41M2MuMjUsMS4wNy42MywyLjgzLjg0LDMuOTFsMS4yNiw2LjI1Ij48L3BhdGg+PHBhdGggZD0iTTE3LjA1LDMwbDEuMjYtNi4yNWMuMjItMS4wOC42LTIuODQuODQtMy45MWwuODEtMy41M2MuMjUtMS4wNy43MS0yLjgxLDEuMDQtMy44NmwuNTktMS45Yy4zMi0xLjA1Ljk5LTIuNjEsMS40OC0zLjQ3czEuMjktMS41MSwxLjc3LTEuNDYsMS4yMy42MiwxLjY4LDEuMjYsMS4xMiwxLjkyLDEuNTIsMi44NS45OSwyLjQ5LDEuMzQsMy40N2wuNjIsMS43OCI+PC9wYXRoPjxsaW5lIHgxPSIxNC45NSIgeTE9IjMwIiB4Mj0iMTcuMDUiIHkyPSIzMCI+PC9saW5lPjwvZz48L2c+CjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xLjEyMCAtMS42OTApIHNjYWxlKDEuMDcpIj48cG9seWdvbiBmaWxsPSJub25lIiBzdHJva2U9IiNBOUE5QjEiIHN0cm9rZS13aWR0aD0iMC40NSIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgcG9pbnRzPSIyOS44OSAxNS4xIDI4LjY0IDExLjU0IDI3LjIyIDguMTcgMjUuNjEgNS44NCAyMy44NiA1LjY1IDIzLjQgNi40NSAyMy4xMiA2LjY2IDIyLjg5IDYuMzQgMTkuNDggMy4zNyAxNS44OSAyLjEzIDEyLjMgMy4zNyA4Ljg5IDYuMzQgOC42NiA2LjY1IDguMzcgNi40MyA3LjkyIDUuNjUgNi4xNyA1Ljg0IDQuNTYgOC4xNyAzLjE0IDExLjU0IDEuODkgMTUuMSA1LjA2IDEyLjM2IDguMDcgMTAuMTMgOS44NyA5LjMyIDExLjQ3IDE0LjQ4IDEzLjE4IDIxLjkyIDE0Ljg0IDMwLjEzIDE2Ljk0IDMwLjEzIDE4LjYgMjEuOTIgMjAuMzEgMTQuNDggMjEuOTEgOS4zMiAyMy43MSAxMC4xMyAyNi43MiAxMi4zNiAyOS44OSAxNS4xIj48L3BvbHlnb24+PC9nPgo8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxLjEyMCAxLjY5MCkgc2NhbGUoMC45MykiPjxwb2x5Z29uIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzhCOEI5MyIgc3Ryb2tlLXdpZHRoPSIwLjUiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHBvaW50cz0iMjkuODkgMTUuMSAyOC42NCAxMS41NCAyNy4yMiA4LjE3IDI1LjYxIDUuODQgMjMuODYgNS42NSAyMy40IDYuNDUgMjMuMTIgNi42NiAyMi44OSA2LjM0IDE5LjQ4IDMuMzcgMTUuODkgMi4xMyAxMi4zIDMuMzcgOC44OSA2LjM0IDguNjYgNi42NSA4LjM3IDYuNDMgNy45MiA1LjY1IDYuMTcgNS44NCA0LjU2IDguMTcgMy4xNCAxMS41NCAxLjg5IDE1LjEgNS4wNiAxMi4zNiA4LjA3IDEwLjEzIDkuODcgOS4zMiAxMS40NyAxNC40OCAxMy4xOCAyMS45MiAxNC44NCAzMC4xMyAxNi45NCAzMC4xMyAxOC42IDIxLjkyIDIwLjMxIDE0LjQ4IDIxLjkxIDkuMzIgMjMuNzEgMTAuMTMgMjYuNzIgMTIuMzYgMjkuODkgMTUuMSI+PC9wb2x5Z29uPjwvZz4KPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjQTMyRkM0IiBzdHJva2Utd2lkdGg9IjAuMyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj48bGluZSB4MT0iMi42MiIgeTE9IjE0LjU2IiB4Mj0iMy4zOSIgeTI9IjEzLjg0Ij48L2xpbmU+PGxpbmUgeDE9IjIuNzQiIHkxPSIxNC40OCIgeDI9IjMuMDMiIHkyPSIxNC4xNyI+PC9saW5lPjxsaW5lIHgxPSIyLjYyIiB5MT0iMTQuNTYiIHgyPSIyLjc0IiB5Mj0iMTQuNDgiPjwvbGluZT48bGluZSB4MT0iOC45OCIgeTE9IjYuNTIiIHgyPSI5IiB5Mj0iNi4yMSI+PC9saW5lPjxsaW5lIHgxPSIyLjg2IiB5MT0iMTQuNCIgeDI9IjMuMDQiIHkyPSIxNC4xNyI+PC9saW5lPjxsaW5lIHgxPSIyLjc0IiB5MT0iMTQuNDgiIHgyPSIyLjg2IiB5Mj0iMTQuNCI+PC9saW5lPjxsaW5lIHgxPSI4Ljg2IiB5MT0iNi4zOSIgeDI9IjkiIHkyPSI2LjIxIj48L2xpbmU+PGxpbmUgeDE9IjIuOTgiIHkxPSIxNC4zMiIgeDI9IjMuMTciIHkyPSIxNC4wNSI+PC9saW5lPjxsaW5lIHgxPSIyLjg2IiB5MT0iMTQuNCIgeDI9IjIuOTgiIHkyPSIxNC4zMiI+PC9saW5lPjxsaW5lIHgxPSIyOC42MSIgeTE9IjEzLjg0IiB4Mj0iMjguODkiIHkyPSIxNC4xMSI+PC9saW5lPjxsaW5lIHgxPSIyOS4yNiIgeTE9IjE0LjQ4IiB4Mj0iMjkuMzgiIHkyPSIxNC41NiI+PC9saW5lPjxsaW5lIHgxPSIyOC45NyIgeTE9IjE0LjE3IiB4Mj0iMjkuMjYiIHkyPSIxNC40OCI+PC9saW5lPjxsaW5lIHgxPSIyOS4xNCIgeTE9IjE0LjQiIHgyPSIyOS4yNiIgeTI9IjE0LjQ4Ij48L2xpbmU+PGxpbmUgeDE9IjI4Ljk2IiB5MT0iMTQuMTciIHgyPSIyOS4xNCIgeTI9IjE0LjQiPjwvbGluZT48bGluZSB4MT0iMjkuMDIiIHkxPSIxNC4zMiIgeDI9IjI5LjE0IiB5Mj0iMTQuNCI+PC9saW5lPjxsaW5lIHgxPSIyOC44MyIgeTE9IjE0LjA1IiB4Mj0iMjkuMDIiIHkyPSIxNC4zMiI+PC9saW5lPjxsaW5lIHgxPSIyMyIgeTE9IjYuMjEiIHgyPSIyMy4xNCIgeTI9IjYuMzkiPjwvbGluZT48bGluZSB4MT0iMjMiIHkxPSI2LjIxIiB4Mj0iMjMuMDIiIHkyPSI2LjUyIj48L2xpbmU+PGxpbmUgeDE9IjMuMjUiIHkxPSIxMS40MSIgeDI9IjMuODEiIHkyPSIxMS43OSI+PC9saW5lPjxsaW5lIHgxPSIyIiB5MT0iMTQuOTciIHgyPSIyLjEyIiB5Mj0iMTQuODkiPjwvbGluZT48bGluZSB4MT0iNC42NyIgeTE9IjguMDQiIHgyPSI1LjY1IiB5Mj0iOC45MiI+PC9saW5lPjxsaW5lIHgxPSI1LjY1IiB5MT0iOC45MiIgeDI9IjYuNTgiIHkyPSI5LjYyIj48L2xpbmU+PGxpbmUgeDE9IjMuODEiIHkxPSIxMS43OSIgeDI9IjQuMzMiIHkyPSIxMi4xIj48L2xpbmU+PGxpbmUgeDE9IjIuMTIiIHkxPSIxNC44OSIgeDI9IjIuMjUiIHkyPSIxNC44MSI+PC9saW5lPjxsaW5lIHgxPSI2LjI4IiB5MT0iNS43MSIgeDI9IjcuNTgiIHkyPSI3LjAyIj48L2xpbmU+PGxpbmUgeDE9IjguOTMiIHkxPSI4LjA0IiB4Mj0iOS41MSIgeTI9IjguMDkiPjwvbGluZT48bGluZSB4MT0iNy41OCIgeTE9IjcuMDIiIHgyPSI4LjkzIiB5Mj0iOC4wNCI+PC9saW5lPjxsaW5lIHgxPSI4LjkzIiB5MT0iOC4wNCIgeDI9IjkuNjEiIHkyPSI4LjI4Ij48L2xpbmU+PGxpbmUgeDE9IjcuNDciIHkxPSIxMC4wMiIgeDI9IjkuODciIHkyPSI4LjgyIj48L2xpbmU+PGxpbmUgeDE9IjYuNTgiIHkxPSI5LjYyIiB4Mj0iNy40NyIgeTI9IjEwLjAyIj48L2xpbmU+PGxpbmUgeDE9IjcuNDciIHkxPSIxMC4wMiIgeDI9IjguMTgiIHkyPSIxMCI+PC9saW5lPjxsaW5lIHgxPSI0LjMzIiB5MT0iMTIuMSIgeDI9IjQuNzkiIHkyPSIxMi4yNyI+PC9saW5lPjxsaW5lIHgxPSIyLjM3IiB5MT0iMTQuNzMiIHgyPSI0Ljc5IiB5Mj0iMTIuMjciPjwvbGluZT48bGluZSB4MT0iMi4yNSIgeTE9IjE0LjgxIiB4Mj0iMi4zNyIgeTI9IjE0LjczIj48L2xpbmU+PGxpbmUgeDE9IjQuNzkiIHkxPSIxMi4yNyIgeDI9IjcuNDciIHkyPSIxMC4wMiI+PC9saW5lPjxsaW5lIHgxPSI0Ljc5IiB5MT0iMTIuMjciIHgyPSI1LjE3IiB5Mj0iMTIuMjMiPjwvbGluZT48bGluZSB4MT0iMi4zNyIgeTE9IjE0LjczIiB4Mj0iMi40OSIgeTI9IjE0LjY0Ij48L2xpbmU+PGxpbmUgeDE9IjE0Ljk1IiB5MT0iMzAiIHgyPSIxNS4yMSIgeTI9IjMwIj48L2xpbmU+PGxpbmUgeDE9IjE1LjIxIiB5MT0iMzAiIHgyPSIxNS40NyIgeTI9IjMwIj48L2xpbmU+PGxpbmUgeDE9IjE1LjQ3IiB5MT0iMzAiIHgyPSIxNS43NCIgeTI9IjMwIj48L2xpbmU+PGxpbmUgeDE9IjE1Ljc0IiB5MT0iMzAiIHgyPSIxNiIgeTI9IjMwIj48L2xpbmU+PGxpbmUgeDE9IjE2IiB5MT0iMzAiIHgyPSIxNi4yNiIgeTI9IjMwIj48L2xpbmU+PGxpbmUgeDE9IjE2LjI2IiB5MT0iMzAiIHgyPSIxNi41MyIgeTI9IjMwIj48L2xpbmU+PGxpbmUgeDE9IjE2LjUzIiB5MT0iMzAiIHgyPSIxNi43OSIgeTI9IjMwIj48L2xpbmU+PGxpbmUgeDE9IjE2Ljc5IiB5MT0iMzAiIHgyPSIxNy4wNSIgeTI9IjMwIj48L2xpbmU+PGxpbmUgeDE9IjIyLjQ5IiB5MT0iOC4wOSIgeDI9IjIzLjA3IiB5Mj0iOC4wNCI+PC9saW5lPjxsaW5lIHgxPSIyMi4zOSIgeTE9IjguMjgiIHgyPSIyMy4wNyIgeTI9IjguMDQiPjwvbGluZT48bGluZSB4MT0iMjQuNTMiIHkxPSIxMC4wMiIgeDI9IjI1LjQyIiB5Mj0iOS42MiI+PC9saW5lPjxsaW5lIHgxPSIyMi4xMyIgeTE9IjguODIiIHgyPSIyNC41MyIgeTI9IjEwLjAyIj48L2xpbmU+PGxpbmUgeDE9IjIzLjgyIiB5MT0iMTAiIHgyPSIyNC41MyIgeTI9IjEwLjAyIj48L2xpbmU+PGxpbmUgeDE9IjIzLjA3IiB5MT0iOC4wNCIgeDI9IjI0LjQyIiB5Mj0iNy4wMiI+PC9saW5lPjxsaW5lIHgxPSIyNC40MiIgeTE9IjcuMDIiIHgyPSIyNS43MiIgeTI9IjUuNzEiPjwvbGluZT48bGluZSB4MT0iMjYuMzUiIHkxPSI4LjkyIiB4Mj0iMjcuMzMiIHkyPSI4LjA0Ij48L2xpbmU+PGxpbmUgeDE9IjI1LjQyIiB5MT0iOS42MiIgeDI9IjI2LjM1IiB5Mj0iOC45MiI+PC9saW5lPjxsaW5lIHgxPSIyNy42NyIgeTE9IjEyLjEiIHgyPSIyOC4xOSIgeTI9IjExLjc5Ij48L2xpbmU+PGxpbmUgeDE9IjI4LjE5IiB5MT0iMTEuNzkiIHgyPSIyOC43NSIgeTI9IjExLjQxIj48L2xpbmU+PGxpbmUgeDE9IjI0LjUzIiB5MT0iMTAuMDIiIHgyPSIyNy4yMSIgeTI9IjEyLjI3Ij48L2xpbmU+PGxpbmUgeDE9IjI2LjgzIiB5MT0iMTIuMjMiIHgyPSIyNy4yMSIgeTI9IjEyLjI3Ij48L2xpbmU+PGxpbmUgeDE9IjI3LjIxIiB5MT0iMTIuMjciIHgyPSIyNy42NyIgeTI9IjEyLjEiPjwvbGluZT48bGluZSB4MT0iMjcuMjEiIHkxPSIxMi4yNyIgeDI9IjI5LjYzIiB5Mj0iMTQuNzMiPjwvbGluZT48cGF0aCBkPSJNMTEuNjYsOC4zOWwuMjQtLjM2Yy4xMy0uMi4yNy0uNjQuMzItLjk5cy4wOS0uOTcuMTItMS4zOS4wNS0xLjEzLjA2LTEuNTlsLjAyLS44MyI+PC9wYXRoPjxwYXRoIGQ9Ik0xOS41OSwzLjI0bC4wMi44M2MuMDEuNDYuMDQsMS4xNy4wNiwxLjU5cy4wNywxLjA0LjEyLDEuMzkuMTguNzkuMzIuOTlsLjI0LjM2Ij48L3BhdGg+PHBhdGggZD0iTTEwLjI5LDcuNjVsMS0uNjJjLjU1LS4zNCwxLjgzLS44MiwyLjg1LTEuMDdzMi42OS0uMjUsMy43MSwwLDIuMy43MywyLjg1LDEuMDdsMSwuNjIiPjwvcGF0aD48cGF0aCBkPSJNMiwxNC45N2wxLjY2LTEuNDNjLjgzLS43MiwyLjE5LTEuODEsMy4wMi0yLjQyczEuOTEtMS4zLDIuNDEtMS41MmwuOS0uNDEiPjwvcGF0aD48cGF0aCBkPSJNMjIuMDEsOS4xOWwuOS40MWMuNS4yMywxLjU4LjkxLDIuNDEsMS41MnMyLjE5LDEuNywzLjAyLDIuNDJsMS42NiwxLjQzIj48L3BhdGg+PHBhdGggZD0iTTksNi4yMWwxLjktMS42NmMuODMtLjcyLDIuMzEtMS41OSwzLjMtMS45M3MyLjYtLjM0LDMuNTksMCwyLjQ3LDEuMjEsMy4zLDEuOTNsMS45LDEuNjYiPjwvcGF0aD48cGF0aCBkPSJNMiwxNC45N2wuNjItMS43OGMuMzQtLjk4Ljk1LTIuNTQsMS4zNC0zLjQ3czEuMDctMi4yMSwxLjUyLTIuODUsMS4yLTEuMjEsMS42OC0xLjI2LDEuMjguNiwxLjc3LDEuNDYsMS4xNiwyLjQxLDEuNDgsMy40N2wuNTksMS45Yy4zMiwxLjA1Ljc5LDIuNzksMS4wNCwzLjg2bC44MSwzLjUzYy4yNSwxLjA3LjYzLDIuODMuODQsMy45MWwxLjI2LDYuMjUiPjwvcGF0aD48cGF0aCBkPSJNMTcuMDUsMzBsMS4yNi02LjI1Yy4yMi0xLjA4LjYtMi44NC44NC0zLjkxbC44MS0zLjUzYy4yNS0xLjA3LjcxLTIuODEsMS4wNC0zLjg2bC41OS0xLjljLjMyLTEuMDUuOTktMi42MSwxLjQ4LTMuNDdzMS4yOS0xLjUxLDEuNzctMS40NiwxLjIzLjYyLDEuNjgsMS4yNiwxLjEyLDEuOTIsMS41MiwyLjg1Ljk5LDIuNDksMS4zNCwzLjQ3bC42MiwxLjc4Ij48L3BhdGg+PGxpbmUgeDE9IjE0Ljk1IiB5MT0iMzAiIHgyPSIxNy4wNSIgeTI9IjMwIj48L2xpbmU+PC9nPjxnIGZpbGw9IiNBMzJGQzQiPjxwYXRoIGQ9Ik0yLjEyLDE0Ljg5YzEuNTUtMi43MSwyLjcxLTUuOTksNS4zNi03LjgzLjM4LS4yNy45NS0uMjgsMS40Mi0uMjYuMzMsMCwuMzMuNTIsMCwuNS0uMzgtLjAxLS44NS0uMDktMS4yMS4xNS0yLjY0LDEuNzEtMy45Miw0Ljg2LTUuNTcsNy40NWgwWiI+PC9wYXRoPjxwYXRoIGQ9Ik0yLjI1LDE0LjgxYzEuODUtMi41NCwzLjgzLTUuMyw2LjU0LTYuOTcuMTEtLjA4LjI3LS4wNi4zNS4wNS4wOC4xMS4wNi4yNy0uMDUuMzUtLjY4LjQ1LTEuMzcuODMtMS45OSwxLjMzLTEuOCwxLjQyLTMuMzksMy40OC00Ljg1LDUuMjRoMFoiPjwvcGF0aD48cGF0aCBkPSJNMTEuMzMsOC4wOGMuOTMsMS42MSwxLjYzLDMuNDIsMi4wMiw1LjI2LDAsMCwuOTIsNS41NC45Miw1LjU0LjQ3LDMuNy44NSw3LjQxLDEuMjEsMTEuMTItLjY0LTUuMzQtMS40NC0xMS4yNi0yLjU2LTE2LjU1LS40NC0xLjgxLTEuMTMtMy40OC0yLjExLTUuMDgtLjItLjM0LjM1LS42NS41My0uMjloMFoiPjwvcGF0aD48cGF0aCBkPSJNMTYuNTMsMzAuMTRjLjM2LTMuNzEuNzQtNy40MiwxLjIxLTExLjEyLDAsMCwuOTItNS41NC45Mi01LjU0LjM5LTEuODQsMS4wOS0zLjY1LDIuMDItNS4yNi4xNy0uMzUuNzItLjA4LjU0LjI3LS45OCwxLjYxLTEuNjgsMy4yOC0yLjEyLDUuMSwwLDAtMS4wNiw1LjQ3LTEuMDYsNS40Ny0uNTcsMy42OC0xLjA1LDcuMzgtMS41LDExLjA4aDBaIj48L3BhdGg+PHBhdGggZD0iTTIzLjA5LDYuNzljMS4wOC0uMTEsMS44My40MSwyLjUzLDEuMTUsMS44MywyLjA0LDIuOTgsNC41Niw0LjI2LDYuOTUtMS42NC0yLjU4LTIuOTItNS43NC01LjU3LTcuNDUtLjM2LS4yMy0uODMtLjE2LTEuMjEtLjE1LS4zMy4wMi0uMzQtLjUxLDAtLjVoMFoiPjwvcGF0aD48cGF0aCBkPSJNMjMuMjEsNy44NGMyLjcxLDEuNjcsNC42OSw0LjQzLDYuNTQsNi45Ny0xLjk3LTIuNDItNC4wMy01LjAzLTYuODItNi41Ni0uMjktLjE3LDAtLjYyLjI4LS40MWgwWiI+PC9wYXRoPjxwYXRoIGQ9Ik04LjMyLDUuNTdjLjc4LjYzLDIuMTgsMS44NywyLjk3LDIuNDQuNzkuMzEsMS42Mi40OCwyLjUzLjQxLjcyLS4wNCwxLjQ1LS4xOCwyLjE4LS4yNi0uNzIuMTctMS40MS4zOS0yLjE1LjUxLS43NC4xLTEuNDkuMTktMi4yOS0uMDUtLjQtLjA5LS43NS0uMTctMS4wNy0uNDMtLjYtLjQtMi4xNC0xLjQxLTIuNzYtMS44Mi0uNTctLjM5LjA2LTEuMjQuNi0uOGgwWiI+PC9wYXRoPjxwYXRoIGQ9Ik0xNiw4LjE2YzEuNTguMjUsMy4yOS41LDQuNzctLjE2Ljc3LS42MywyLjIzLTEuODUsMy4wMS0yLjQ5LjUtLjQyLDEuMTQuMzUuNjQuNzctLjkuNTktMi40MywxLjU5LTMuMzEsMi4xNy0xLjczLjYzLTMuNDMuMTYtNS4xLS4yOWgwWiI+PC9wYXRoPjxwYXRoIGQ9Ik0xOC44OSwyMS45OWMtLjg3LjQ5LTEuODMsMS4wMi0yLjg1LDEuMDYsMS4wMS0uMjIsMS43OC0uODUsMi41Ny0xLjQ4LjI3LS4yMy41OS4yNC4yOC40MWgwWiI+PC9wYXRoPjxwYXRoIGQ9Ik0xNi4wNCwxNi44M2MxLjMtLjE3LDIuNC0uODgsMy4zMi0xLjc4LjMyLS4yOC42Mi0uNTguOTMtLjg5LjEtLjEuMjYtLjEuMzUsMCwuMTEuMS4xLjI4LS4wMi4zNy0xLjMyLDEuMS0yLjc5LDIuMjUtNC41OSwyLjNoMFoiPjwvcGF0aD48cGF0aCBkPSJNMjIuNCw4LjhjLS44OC44MS0xLjc3LDEuNjktMi44NCwyLjI3LTEuMDguNTktMi4zMi43LTMuNTIuNzQsMS4xOS0uMTIsMi40LS4zMSwzLjQtLjk2Ljk5LS42MywxLjc5LTEuNTQsMi42MS0yLjQuMjItLjI2LjYxLjEyLjM2LjM1aDBaIj48L3BhdGg+PHBhdGggZD0iTTE2LjM3LDIuMzlsLS4zOCwyNy42MS0uMzgtMjcuNjFjMC0uNDkuNzUtLjUuNzUsMGgwWiI+PC9wYXRoPjxwYXRoIGQ9Ik0xMy40MywyMS41OGMuNzkuNjMsMS41NiwxLjI2LDIuNTcsMS40OC0xLjAyLS4wNC0xLjk4LS41Ny0yLjg1LTEuMDYtLjEyLS4wNy0uMTctLjIyLS4xLS4zNC4wNy0uMTQuMjYtLjE3LjM4LS4wN2gwWiI+PC9wYXRoPjxwYXRoIGQ9Ik0xNiwxNi44M2MtMS43OS0uMDUtMy4yNi0xLjItNC41OS0yLjMtLjExLS4wOS0uMTItLjI0LS4wNC0uMzUuMDktLjEyLjI3LS4xMy4zNy0uMDIsMS4yLDEuMjEsMi41LDIuNDUsNC4yNSwyLjY3aDBaIj48L3BhdGg+PHBhdGggZD0iTTkuOTksOC40NWMuODIuODYsMS42MSwxLjc3LDIuNjEsMi40LDEsLjY0LDIuMjEuODMsMy40Ljk2LTEuMi0uMDQtMi40NS0uMTUtMy41Mi0uNzQtMS4wNy0uNTgtMS45Ny0xLjQ2LTIuODUtMi4yNy0uMjQtLjIzLjE0LS42LjM2LS4zNWgwWiI+PC9wYXRoPjwvZz4KPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMi43MCAyLjY0KSBzY2FsZSgwLjk4OSkiPjxyZWN0IHg9IjE4LjMiIHk9IjE4LjM2IiB3aWR0aD0iMTIiIGhlaWdodD0iMTIuMTMiIGZpbGw9IiNmZmZmZmYiIHN0cm9rZT0iIzExMTExNCIgc3Ryb2tlLXdpZHRoPSIwLjciPjwvcmVjdD48cmVjdCB4PSIxOC42OCIgeT0iMTguNjgiIHdpZHRoPSIxMS4yMSIgaGVpZ2h0PSIxLjEiIGZpbGw9InVybCgjcnZiYXIpIj48L3JlY3Q+PGxpbmUgeDE9IjE4LjQzIiB5MT0iMjAuMzUiIHgyPSIzMC4zIiB5Mj0iMjAuMzUiIHN0cm9rZT0iIzExMTExNCIgc3Ryb2tlLXdpZHRoPSIwLjQ1Ij48L2xpbmU+PGxpbmUgeDE9IjE5LjMxIiB5MT0iMjguNTIiIHgyPSIyOS4yMiIgeTI9IjI4LjUyIiBzdHJva2U9IiMxMTExMTQiIHN0cm9rZS13aWR0aD0iMC40NSI+PC9saW5lPjxwb2x5bGluZSBwb2ludHM9IjE5LjMxIDI3LjAzIDIwLjgyIDI3LjAzIDIzLjM4IDIxLjcgMjQuODYgMjQuODcgMjYuNjQgMjQuODcgMjguNTkgMjcuMzkiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzExMTExNCIgc3Ryb2tlLXdpZHRoPSIwLjciIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjwvcG9seWxpbmU+PC9nPgo8L3N2Zz4K" + }, + "rendered": { + "light": { + "bytes": "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAGKADAAQAAAABAAAAGAAAAADiNXWtAAAEvklEQVRIDbVV3W8UVRS/c\u002Bdzd2Z3urW01CDQYqW0koYCNTGgRCoBwpekadVoa8T0ScMDD33gQR\u002BM\u002BmCixvgPEJIKfmxFAgJtWigLCbRhXWrDR21py1KgZXc7\u002BzE7c\u002B/c68zKbNZQ7fLgTW7O7577O\u002Bd358w9MwD8z4N9mvxHjhyRGxoa6MDAACk2jimGePx4z1qeFyp5VngeWRam1BoTBN\u002BdXbs2jy8Wv6hAMHhmvZyU93iAImk3zHKO4SxPDfMwSdJJ3R8PtrTsvvFfIk8IUErZkyfPbaWI8zA8TsnpQCO\u002Bz2wEVVnFnGAqACBAquai3KSUtMqyEVyq96Y1Y4XohdGdO7cNMQxjFQpyhQsHB4OnN/kzpc2ZMGkCaUi5LXDCks1VPhyIpXRsQpsjmox3Xoov80J/RvuJ7hBVD2DrSOjEiXNO8iEnjzscfn50d595TpkPbJM8ggeb1sr0FG6Mh9AWn6SmvMukpPgM1LkAk/Ut92oqV5qMX8g0G1G6LmuYKxTRy\u002BOo8Mbg4GAgn9AG/xBQFGYNZ/HS/QvaHiHAW55qPsFkIReP6AEjZkBgAsBghgKM8XwYlUHEQaEaamJAwrPh1F6VV8SZmUTNggJHj57y8zH5VWzgF1kBWigK4qQKn0e12hzNUCiqrMFIADvTlqEMC5C\u002BKpGQa5mz\u002BB5MwaWU2uVf7jPVffZ7zB88DzguWyVy3IrYJaOBNz03M5uiPbPK9KcmhyY8jewcShLePZmDaRmirIgvPvDNfYVem/2ZnZKmY\u002BeNzdCA5ceO9VS53LyAX1Hfm/vVbBIrmdvp9Y9O8l7r887ON29RyxqwJF3Qp7HiBjnYKjcNw2D629t3jYxPXf86uW6uHy4htzIh8JLP53/d5eYEnKsJR0pGUZLGzI3pvrb27d\u002B1trbmrhthyVVtLOtBcSI5QaUvex84WJ/UBX\u002BpPOz4urq6km917Pws1RC7iBJmxAyBSbdMOQHn7mrDfx6VO6132tp2fOIEuYOUJK7jElOz3ysHWUhwAgsIYpao6P7uD1654/Ic29Gx/7D3ff5DdPvRWTtn7nPyRKMVBrj4\u002B29Od5cz5SuXNpeMOb67g7M1c6l46O1DOw65nH\u002BzTzTaQkRE0LBhmvXunvEIyxiSYVUt22CXYsFDIkRndD12tygBxmtFTN3IlccR0dPI\u002BiX8I9zX8e1VTgwAVRGB3ycCnz3/xgK4cvGH4S\u002B/ONxUlIBHFkdNzUSpm1i1CGGpkk2NjoenPn53O6ipWeI\u002B2GNr2NYAvz2c1BxHUQKWkJoBghKX6mEKIySSeRCLRqf06upq0NjYCIaGhkAodBkcPPjRYxEA/H5f1lnk\u002ByC/swBwrqyJ6XjsWurZ\u002BO/ZSsjQkUJaOBwBdXVrQF9fP8hkMoVbxQnkIij9g5YZBPmzBoOtnAAhBFy7Fgb19XWguXkrEEUBdHcfdz5VeZGiSuSwVUUY1XrxaowIEJpKJ2wXPzNzD8zPJ0FLy35gtzXYsGE9qK1dDS5duvz0Ag/56SvllS\u002BcEomA5BrD\u002BYutlWUFVFRUgEgkkk/oALv\u002B\u002BXXRT3DgwIGkHdXhRkpSyWwwGOxV1YDu\u002Bgrt9PTkmN0jCzdJIXER7LX3F2w024/saf4FDc8wtXMedT4AAAAASUVORK5CYII=", + "width": 24, + "height": 24 + }, + "dark": { + "bytes": "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAGKADAAQAAAABAAAAGAAAAADiNXWtAAAEvklEQVRIDbVV3W8UVRS/c\u002Bdzd2Z3urW01CDQYqW0koYCNTGgRCoBwpekadVoa8T0ScMDD33gQR\u002BM\u002BmCixvgPEJIKfmxFAgJtWigLCbRhXWrDR21py1KgZXc7\u002BzE7c\u002B/c68zKbNZQ7fLgTW7O7577O\u002Bd358w9MwD8z4N9mvxHjhyRGxoa6MDAACk2jimGePx4z1qeFyp5VngeWRam1BoTBN\u002BdXbs2jy8Wv6hAMHhmvZyU93iAImk3zHKO4SxPDfMwSdJJ3R8PtrTsvvFfIk8IUErZkyfPbaWI8zA8TsnpQCO\u002Bz2wEVVnFnGAqACBAquai3KSUtMqyEVyq96Y1Y4XohdGdO7cNMQxjFQpyhQsHB4OnN/kzpc2ZMGkCaUi5LXDCks1VPhyIpXRsQpsjmox3Xoov80J/RvuJ7hBVD2DrSOjEiXNO8iEnjzscfn50d595TpkPbJM8ggeb1sr0FG6Mh9AWn6SmvMukpPgM1LkAk/Ut92oqV5qMX8g0G1G6LmuYKxTRy\u002BOo8Mbg4GAgn9AG/xBQFGYNZ/HS/QvaHiHAW55qPsFkIReP6AEjZkBgAsBghgKM8XwYlUHEQaEaamJAwrPh1F6VV8SZmUTNggJHj57y8zH5VWzgF1kBWigK4qQKn0e12hzNUCiqrMFIADvTlqEMC5C\u002BKpGQa5mz\u002BB5MwaWU2uVf7jPVffZ7zB88DzguWyVy3IrYJaOBNz03M5uiPbPK9KcmhyY8jewcShLePZmDaRmirIgvPvDNfYVem/2ZnZKmY\u002BeNzdCA5ceO9VS53LyAX1Hfm/vVbBIrmdvp9Y9O8l7r887ON29RyxqwJF3Qp7HiBjnYKjcNw2D629t3jYxPXf86uW6uHy4htzIh8JLP53/d5eYEnKsJR0pGUZLGzI3pvrb27d\u002B1trbmrhthyVVtLOtBcSI5QaUvex84WJ/UBX\u002BpPOz4urq6km917Pws1RC7iBJmxAyBSbdMOQHn7mrDfx6VO6132tp2fOIEuYOUJK7jElOz3ysHWUhwAgsIYpao6P7uD1654/Ic29Gx/7D3ff5DdPvRWTtn7nPyRKMVBrj4\u002B29Od5cz5SuXNpeMOb67g7M1c6l46O1DOw65nH\u002BzTzTaQkRE0LBhmvXunvEIyxiSYVUt22CXYsFDIkRndD12tygBxmtFTN3IlccR0dPI\u002BiX8I9zX8e1VTgwAVRGB3ycCnz3/xgK4cvGH4S\u002B/ONxUlIBHFkdNzUSpm1i1CGGpkk2NjoenPn53O6ipWeI\u002B2GNr2NYAvz2c1BxHUQKWkJoBghKX6mEKIySSeRCLRqf06upq0NjYCIaGhkAodBkcPPjRYxEA/H5f1lnk\u002ByC/swBwrqyJ6XjsWurZ\u002BO/ZSsjQkUJaOBwBdXVrQF9fP8hkMoVbxQnkIij9g5YZBPmzBoOtnAAhBFy7Fgb19XWguXkrEEUBdHcfdz5VeZGiSuSwVUUY1XrxaowIEJpKJ2wXPzNzD8zPJ0FLy35gtzXYsGE9qK1dDS5duvz0Ag/56SvllS\u002BcEomA5BrD\u002BYutlWUFVFRUgEgkkk/oALv\u002B\u002BXXRT3DgwIGkHdXhRkpSyWwwGOxV1YDu\u002Bgrt9PTkmN0jCzdJIXER7LX3F2w024/saf4FDc8wtXMedT4AAAAASUVORK5CYII=", + "width": 24, + "height": 24 + } + } + } + }, + { + "id": "b4188b27-d157-4b8c-bd8e-7f8543ccaa93", + "language": { + "id": "*.*.python", + "version": "3.*.*" + }, + "title": "RV_dem_blocks", + "uri": "commands/RV_dem_blocks.py", + "image": { + "light": { + "type": "svg", + "data": "PHN2ZyBpZD0iTGF5ZXJfMiIgZGF0YS1uYW1lPSJMYXllciAyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMzIgMzIiPgogIDxkZWZzPgogICAgPHN0eWxlPgogICAgICAuY2xzLTEgewogICAgICAgIGZpbGw6IG5vbmU7CiAgICAgIH0KCiAgICAgIC5jbHMtMiB7CiAgICAgICAgY2xpcC1wYXRoOiB1cmwoI2NsaXBwYXRoKTsKICAgICAgfQoKICAgICAgLmNscy0zIHsKICAgICAgICBmaWxsOiAjMDA5MmQyOwogICAgICB9CiAgICA8L3N0eWxlPgogICAgPGNsaXBQYXRoIGlkPSJjbGlwcGF0aCI+CiAgICAgIDxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMzguMzQiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIvPgogICAgPC9jbGlwUGF0aD4KICA8L2RlZnM+CiAgPGcgY2xhc3M9ImNscy0yIj4KICAgIDxpbWFnZSB3aWR0aD0iNjEzNSIgaGVpZ2h0PSIzMzc4IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyMC4yNyAtMS4yNCkgc2NhbGUoLjAxKSIgeGxpbms6aHJlZj0iLi4vLi4vLi4vLi4vLi4vRG9jdW1lbnRzL0dpdEh1Yi9vYnNpZGlhbi9FVEhfUGhELzYwMF9EZXYvNjEwX0xpYnMvY29tcGFzX21hc29ucnkvaWNvbnMvd2lwL2FyY2gxL1ZpZXcucG5nIi8+CiAgPC9nPgogIDxnPgogICAgPHBhdGggY2xhc3M9ImNscy0zIiBkPSJNMTAuNDEsMTEuNTRoLTEuNzR2LTUuMDFoLTEuNTd2LTEuNDJoNC44N3YxLjQyaC0xLjU2djUuMDFaIi8+CiAgICA8cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0xOC4zNCwxMS41NGgtMi4yOGwtMi4zNS00LjUzaC0uMDRjLjA2LjcxLjA4LDEuMjYuMDgsMS42M3YyLjloLTEuNTR2LTYuNDJoMi4yN2wyLjM0LDQuNDZoLjAzYy0uMDQtLjY1LS4wNi0xLjE3LS4wNi0xLjU2di0yLjloMS41NXY2LjQyWiIvPgogICAgPHBhdGggY2xhc3M9ImNscy0zIiBkPSJNMjIuOTksMTEuNTRsLS4zMi0xLjJoLTIuMDlsLS4zMywxLjJoLTEuOTFsMi4xLTYuNDVoMi4zMmwyLjEyLDYuNDVoLTEuOVpNMjIuMzIsOC45MmwtLjI4LTEuMDVjLS4wNi0uMjMtLjE0LS41NC0uMjQtLjkxLS4wOS0uMzctLjE1LS42NC0uMTgtLjgtLjAzLjE1LS4wOC40LS4xNi43NC0uMDguMzQtLjI1LDEuMDItLjUyLDIuMDNoMS4zN1oiLz4KICA8L2c+CiAgPGc+CiAgICA8cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0xMi45MiwyNC4wNGMwLDEuMDctLjMsMS45LS44OSwyLjQ4LS41OS41OC0xLjQyLjg3LTIuNDkuODdoLTIuMDh2LTYuNDJoMi4yMmMxLjAzLDAsMS44My4yNiwyLjM5Ljc5LjU2LjUzLjg0LDEuMjkuODQsMi4yOVpNMTEuMTIsMjQuMWMwLS41OS0uMTItMS4wMy0uMzUtMS4zMS0uMjMtLjI4LS41OS0uNDMtMS4wNi0uNDNoLS41MXYzLjZoLjM5Yy41MywwLC45MS0uMTUsMS4xNi0uNDZzLjM3LS43Ny4zNy0xLjRaIi8+CiAgICA8cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0xNi45OSwyNy4zOWgtMy44MXYtNi40MmgzLjgxdjEuMzloLTIuMDh2MS4wMWgxLjkydjEuMzloLTEuOTJ2MS4yMWgyLjA4djEuNDJaIi8+CiAgICA8cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0yMC4wMSwyNy4zOWwtMS4zMS00LjYzaC0uMDRjLjA2Ljc5LjA5LDEuNC4wOSwxLjg0djIuNzloLTEuNTR2LTYuNDJoMi4zMWwxLjM0LDQuNTZoLjA0bDEuMzEtNC41NmgyLjMydjYuNDJoLTEuNTl2LTIuODJjMC0uMTUsMC0uMzEsMC0uNDksMC0uMTguMDItLjYyLjA2LTEuMzFoLS4wNGwtMS4zLDQuNjJoLTEuNjVaIi8+CiAgPC9nPgogIDxnPgogICAgPHBvbHlnb24gcG9pbnRzPSIxOC4yOSAxNS4zNiAxNiAxOS4zMiAxMy43MSAxNS4zNiAxOC4yOSAxNS4zNiIvPgogICAgPHJlY3QgeD0iMTUuMzUiIHk9IjEzLjE5IiB3aWR0aD0iMS4zMSIgaGVpZ2h0PSIyLjg0Ii8+CiAgPC9nPgo8L3N2Zz4K" + } + } + }, { "id": "3bf1a0c6-b98e-4123-b989-bc1e206b7b87", "language": { @@ -421,6 +505,21 @@ } } }, + { + "id": "04e977e4-9c76-46a1-8378-a7d5eb00a25a", + "language": { + "id": "*.*.python", + "version": "3.*.*" + }, + "title": "RV_thrust_info", + "uri": "commands/RV_thrust_info.py", + "image": { + "light": { + "type": "svg", + "data": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0zIC0zIDM4IDM4IiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0wLjgwMCAtMS4xODApIHNjYWxlKDEuMDUpIiBvcGFjaXR5PSIuMjIiPjxwb2x5Z29uIGZpbGw9IiNEOEQ4REUiIHBvaW50cz0iMjkuODkgMTUuMSAyOC42NCAxMS41NCAyNy4yMiA4LjE3IDI1LjYxIDUuODQgMjMuODYgNS42NSAyMy40IDYuNDUgMjMuMTIgNi42NiAyMi44OSA2LjM0IDE5LjQ4IDMuMzcgMTUuODkgMi4xMyAxMi4zIDMuMzcgOC44OSA2LjM0IDguNjYgNi42NSA4LjM3IDYuNDMgNy45MiA1LjY1IDYuMTcgNS44NCA0LjU2IDguMTcgMy4xNCAxMS41NCAxLjg5IDE1LjEgNS4wNiAxMi4zNiA4LjA3IDEwLjEzIDkuODcgOS4zMiAxMS40NyAxNC40OCAxMy4xOCAyMS45MiAxNC44NCAzMC4xMyAxNi45NCAzMC4xMyAxOC42IDIxLjkyIDIwLjMxIDE0LjQ4IDIxLjkxIDkuMzIgMjMuNzEgMTAuMTMgMjYuNzIgMTIuMzYgMjkuODkgMTUuMSI+PC9wb2x5Z29uPjwvZz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLjgwMCAxLjE4MCkgc2NhbGUoMC45NSkiIG9wYWNpdHk9Ii4yMiI+PHBvbHlnb24gZmlsbD0iI0Q4RDhERSIgcG9pbnRzPSIyOS44OSAxNS4xIDI4LjY0IDExLjU0IDI3LjIyIDguMTcgMjUuNjEgNS44NCAyMy44NiA1LjY1IDIzLjQgNi40NSAyMy4xMiA2LjY2IDIyLjg5IDYuMzQgMTkuNDggMy4zNyAxNS44OSAyLjEzIDEyLjMgMy4zNyA4Ljg5IDYuMzQgOC42NiA2LjY1IDguMzcgNi40MyA3LjkyIDUuNjUgNi4xNyA1Ljg0IDQuNTYgOC4xNyAzLjE0IDExLjU0IDEuODkgMTUuMSA1LjA2IDEyLjM2IDguMDcgMTAuMTMgOS44NyA5LjMyIDExLjQ3IDE0LjQ4IDEzLjE4IDIxLjkyIDE0Ljg0IDMwLjEzIDE2Ljk0IDMwLjEzIDE4LjYgMjEuOTIgMjAuMzEgMTQuNDggMjEuOTEgOS4zMiAyMy43MSAxMC4xMyAyNi43MiAxMi4zNiAyOS44OSAxNS4xIj48L3BvbHlnb24+PC9nPjxnIG9wYWNpdHk9Ii41Ij48ZyBmaWxsPSJub25lIiBzdHJva2U9IiNBMzJGQzQiIHN0cm9rZS13aWR0aD0iMC4zIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjxsaW5lIHgxPSIyLjYyIiB5MT0iMTQuNTYiIHgyPSIzLjM5IiB5Mj0iMTMuODQiPjwvbGluZT48bGluZSB4MT0iMi43NCIgeTE9IjE0LjQ4IiB4Mj0iMy4wMyIgeTI9IjE0LjE3Ij48L2xpbmU+PGxpbmUgeDE9IjIuNjIiIHkxPSIxNC41NiIgeDI9IjIuNzQiIHkyPSIxNC40OCI+PC9saW5lPjxsaW5lIHgxPSI4Ljk4IiB5MT0iNi41MiIgeDI9IjkiIHkyPSI2LjIxIj48L2xpbmU+PGxpbmUgeDE9IjIuODYiIHkxPSIxNC40IiB4Mj0iMy4wNCIgeTI9IjE0LjE3Ij48L2xpbmU+PGxpbmUgeDE9IjIuNzQiIHkxPSIxNC40OCIgeDI9IjIuODYiIHkyPSIxNC40Ij48L2xpbmU+PGxpbmUgeDE9IjguODYiIHkxPSI2LjM5IiB4Mj0iOSIgeTI9IjYuMjEiPjwvbGluZT48bGluZSB4MT0iMi45OCIgeTE9IjE0LjMyIiB4Mj0iMy4xNyIgeTI9IjE0LjA1Ij48L2xpbmU+PGxpbmUgeDE9IjIuODYiIHkxPSIxNC40IiB4Mj0iMi45OCIgeTI9IjE0LjMyIj48L2xpbmU+PGxpbmUgeDE9IjI4LjYxIiB5MT0iMTMuODQiIHgyPSIyOC44OSIgeTI9IjE0LjExIj48L2xpbmU+PGxpbmUgeDE9IjI5LjI2IiB5MT0iMTQuNDgiIHgyPSIyOS4zOCIgeTI9IjE0LjU2Ij48L2xpbmU+PGxpbmUgeDE9IjI4Ljk3IiB5MT0iMTQuMTciIHgyPSIyOS4yNiIgeTI9IjE0LjQ4Ij48L2xpbmU+PGxpbmUgeDE9IjI5LjE0IiB5MT0iMTQuNCIgeDI9IjI5LjI2IiB5Mj0iMTQuNDgiPjwvbGluZT48bGluZSB4MT0iMjguOTYiIHkxPSIxNC4xNyIgeDI9IjI5LjE0IiB5Mj0iMTQuNCI+PC9saW5lPjxsaW5lIHgxPSIyOS4wMiIgeTE9IjE0LjMyIiB4Mj0iMjkuMTQiIHkyPSIxNC40Ij48L2xpbmU+PGxpbmUgeDE9IjI4LjgzIiB5MT0iMTQuMDUiIHgyPSIyOS4wMiIgeTI9IjE0LjMyIj48L2xpbmU+PGxpbmUgeDE9IjIzIiB5MT0iNi4yMSIgeDI9IjIzLjE0IiB5Mj0iNi4zOSI+PC9saW5lPjxsaW5lIHgxPSIyMyIgeTE9IjYuMjEiIHgyPSIyMy4wMiIgeTI9IjYuNTIiPjwvbGluZT48bGluZSB4MT0iMy4yNSIgeTE9IjExLjQxIiB4Mj0iMy44MSIgeTI9IjExLjc5Ij48L2xpbmU+PGxpbmUgeDE9IjIiIHkxPSIxNC45NyIgeDI9IjIuMTIiIHkyPSIxNC44OSI+PC9saW5lPjxsaW5lIHgxPSI0LjY3IiB5MT0iOC4wNCIgeDI9IjUuNjUiIHkyPSI4LjkyIj48L2xpbmU+PGxpbmUgeDE9IjUuNjUiIHkxPSI4LjkyIiB4Mj0iNi41OCIgeTI9IjkuNjIiPjwvbGluZT48bGluZSB4MT0iMy44MSIgeTE9IjExLjc5IiB4Mj0iNC4zMyIgeTI9IjEyLjEiPjwvbGluZT48bGluZSB4MT0iMi4xMiIgeTE9IjE0Ljg5IiB4Mj0iMi4yNSIgeTI9IjE0LjgxIj48L2xpbmU+PGxpbmUgeDE9IjYuMjgiIHkxPSI1LjcxIiB4Mj0iNy41OCIgeTI9IjcuMDIiPjwvbGluZT48bGluZSB4MT0iOC45MyIgeTE9IjguMDQiIHgyPSI5LjUxIiB5Mj0iOC4wOSI+PC9saW5lPjxsaW5lIHgxPSI3LjU4IiB5MT0iNy4wMiIgeDI9IjguOTMiIHkyPSI4LjA0Ij48L2xpbmU+PGxpbmUgeDE9IjguOTMiIHkxPSI4LjA0IiB4Mj0iOS42MSIgeTI9IjguMjgiPjwvbGluZT48bGluZSB4MT0iNy40NyIgeTE9IjEwLjAyIiB4Mj0iOS44NyIgeTI9IjguODIiPjwvbGluZT48bGluZSB4MT0iNi41OCIgeTE9IjkuNjIiIHgyPSI3LjQ3IiB5Mj0iMTAuMDIiPjwvbGluZT48bGluZSB4MT0iNy40NyIgeTE9IjEwLjAyIiB4Mj0iOC4xOCIgeTI9IjEwIj48L2xpbmU+PGxpbmUgeDE9IjQuMzMiIHkxPSIxMi4xIiB4Mj0iNC43OSIgeTI9IjEyLjI3Ij48L2xpbmU+PGxpbmUgeDE9IjIuMzciIHkxPSIxNC43MyIgeDI9IjQuNzkiIHkyPSIxMi4yNyI+PC9saW5lPjxsaW5lIHgxPSIyLjI1IiB5MT0iMTQuODEiIHgyPSIyLjM3IiB5Mj0iMTQuNzMiPjwvbGluZT48bGluZSB4MT0iNC43OSIgeTE9IjEyLjI3IiB4Mj0iNy40NyIgeTI9IjEwLjAyIj48L2xpbmU+PGxpbmUgeDE9IjQuNzkiIHkxPSIxMi4yNyIgeDI9IjUuMTciIHkyPSIxMi4yMyI+PC9saW5lPjxsaW5lIHgxPSIyLjM3IiB5MT0iMTQuNzMiIHgyPSIyLjQ5IiB5Mj0iMTQuNjQiPjwvbGluZT48bGluZSB4MT0iMTQuOTUiIHkxPSIzMCIgeDI9IjE1LjIxIiB5Mj0iMzAiPjwvbGluZT48bGluZSB4MT0iMTUuMjEiIHkxPSIzMCIgeDI9IjE1LjQ3IiB5Mj0iMzAiPjwvbGluZT48bGluZSB4MT0iMTUuNDciIHkxPSIzMCIgeDI9IjE1Ljc0IiB5Mj0iMzAiPjwvbGluZT48bGluZSB4MT0iMTUuNzQiIHkxPSIzMCIgeDI9IjE2IiB5Mj0iMzAiPjwvbGluZT48bGluZSB4MT0iMTYiIHkxPSIzMCIgeDI9IjE2LjI2IiB5Mj0iMzAiPjwvbGluZT48bGluZSB4MT0iMTYuMjYiIHkxPSIzMCIgeDI9IjE2LjUzIiB5Mj0iMzAiPjwvbGluZT48bGluZSB4MT0iMTYuNTMiIHkxPSIzMCIgeDI9IjE2Ljc5IiB5Mj0iMzAiPjwvbGluZT48bGluZSB4MT0iMTYuNzkiIHkxPSIzMCIgeDI9IjE3LjA1IiB5Mj0iMzAiPjwvbGluZT48bGluZSB4MT0iMjIuNDkiIHkxPSI4LjA5IiB4Mj0iMjMuMDciIHkyPSI4LjA0Ij48L2xpbmU+PGxpbmUgeDE9IjIyLjM5IiB5MT0iOC4yOCIgeDI9IjIzLjA3IiB5Mj0iOC4wNCI+PC9saW5lPjxsaW5lIHgxPSIyNC41MyIgeTE9IjEwLjAyIiB4Mj0iMjUuNDIiIHkyPSI5LjYyIj48L2xpbmU+PGxpbmUgeDE9IjIyLjEzIiB5MT0iOC44MiIgeDI9IjI0LjUzIiB5Mj0iMTAuMDIiPjwvbGluZT48bGluZSB4MT0iMjMuODIiIHkxPSIxMCIgeDI9IjI0LjUzIiB5Mj0iMTAuMDIiPjwvbGluZT48bGluZSB4MT0iMjMuMDciIHkxPSI4LjA0IiB4Mj0iMjQuNDIiIHkyPSI3LjAyIj48L2xpbmU+PGxpbmUgeDE9IjI0LjQyIiB5MT0iNy4wMiIgeDI9IjI1LjcyIiB5Mj0iNS43MSI+PC9saW5lPjxsaW5lIHgxPSIyNi4zNSIgeTE9IjguOTIiIHgyPSIyNy4zMyIgeTI9IjguMDQiPjwvbGluZT48bGluZSB4MT0iMjUuNDIiIHkxPSI5LjYyIiB4Mj0iMjYuMzUiIHkyPSI4LjkyIj48L2xpbmU+PGxpbmUgeDE9IjI3LjY3IiB5MT0iMTIuMSIgeDI9IjI4LjE5IiB5Mj0iMTEuNzkiPjwvbGluZT48bGluZSB4MT0iMjguMTkiIHkxPSIxMS43OSIgeDI9IjI4Ljc1IiB5Mj0iMTEuNDEiPjwvbGluZT48bGluZSB4MT0iMjQuNTMiIHkxPSIxMC4wMiIgeDI9IjI3LjIxIiB5Mj0iMTIuMjciPjwvbGluZT48bGluZSB4MT0iMjYuODMiIHkxPSIxMi4yMyIgeDI9IjI3LjIxIiB5Mj0iMTIuMjciPjwvbGluZT48bGluZSB4MT0iMjcuMjEiIHkxPSIxMi4yNyIgeDI9IjI3LjY3IiB5Mj0iMTIuMSI+PC9saW5lPjxsaW5lIHgxPSIyNy4yMSIgeTE9IjEyLjI3IiB4Mj0iMjkuNjMiIHkyPSIxNC43MyI+PC9saW5lPjxwYXRoIGQ9Ik0xMS42Niw4LjM5bC4yNC0uMzZjLjEzLS4yLjI3LS42NC4zMi0uOTlzLjA5LS45Ny4xMi0xLjM5LjA1LTEuMTMuMDYtMS41OWwuMDItLjgzIj48L3BhdGg+PHBhdGggZD0iTTE5LjU5LDMuMjRsLjAyLjgzYy4wMS40Ni4wNCwxLjE3LjA2LDEuNTlzLjA3LDEuMDQuMTIsMS4zOS4xOC43OS4zMi45OWwuMjQuMzYiPjwvcGF0aD48cGF0aCBkPSJNMTAuMjksNy42NWwxLS42MmMuNTUtLjM0LDEuODMtLjgyLDIuODUtMS4wN3MyLjY5LS4yNSwzLjcxLDAsMi4zLjczLDIuODUsMS4wN2wxLC42MiI+PC9wYXRoPjxwYXRoIGQ9Ik0yLDE0Ljk3bDEuNjYtMS40M2MuODMtLjcyLDIuMTktMS44MSwzLjAyLTIuNDJzMS45MS0xLjMsMi40MS0xLjUybC45LS40MSI+PC9wYXRoPjxwYXRoIGQ9Ik0yMi4wMSw5LjE5bC45LjQxYy41LjIzLDEuNTguOTEsMi40MSwxLjUyczIuMTksMS43LDMuMDIsMi40MmwxLjY2LDEuNDMiPjwvcGF0aD48cGF0aCBkPSJNOSw2LjIxbDEuOS0xLjY2Yy44My0uNzIsMi4zMS0xLjU5LDMuMy0xLjkzczIuNi0uMzQsMy41OSwwLDIuNDcsMS4yMSwzLjMsMS45M2wxLjksMS42NiI+PC9wYXRoPjxwYXRoIGQ9Ik0yLDE0Ljk3bC42Mi0xLjc4Yy4zNC0uOTguOTUtMi41NCwxLjM0LTMuNDdzMS4wNy0yLjIxLDEuNTItMi44NSwxLjItMS4yMSwxLjY4LTEuMjYsMS4yOC42LDEuNzcsMS40NiwxLjE2LDIuNDEsMS40OCwzLjQ3bC41OSwxLjljLjMyLDEuMDUuNzksMi43OSwxLjA0LDMuODZsLjgxLDMuNTNjLjI1LDEuMDcuNjMsMi44My44NCwzLjkxbDEuMjYsNi4yNSI+PC9wYXRoPjxwYXRoIGQ9Ik0xNy4wNSwzMGwxLjI2LTYuMjVjLjIyLTEuMDguNi0yLjg0Ljg0LTMuOTFsLjgxLTMuNTNjLjI1LTEuMDcuNzEtMi44MSwxLjA0LTMuODZsLjU5LTEuOWMuMzItMS4wNS45OS0yLjYxLDEuNDgtMy40N3MxLjI5LTEuNTEsMS43Ny0xLjQ2LDEuMjMuNjIsMS42OCwxLjI2LDEuMTIsMS45MiwxLjUyLDIuODUuOTksMi40OSwxLjM0LDMuNDdsLjYyLDEuNzgiPjwvcGF0aD48bGluZSB4MT0iMTQuOTUiIHkxPSIzMCIgeDI9IjE3LjA1IiB5Mj0iMzAiPjwvbGluZT48L2c+PGcgZmlsbD0iI0EzMkZDNCI+PHBhdGggZD0iTTIuMTIsMTQuODljMS41NS0yLjcxLDIuNzEtNS45OSw1LjM2LTcuODMuMzgtLjI3Ljk1LS4yOCwxLjQyLS4yNi4zMywwLC4zMy41MiwwLC41LS4zOC0uMDEtLjg1LS4wOS0xLjIxLjE1LTIuNjQsMS43MS0zLjkyLDQuODYtNS41Nyw3LjQ1aDBaIj48L3BhdGg+PHBhdGggZD0iTTIuMjUsMTQuODFjMS44NS0yLjU0LDMuODMtNS4zLDYuNTQtNi45Ny4xMS0uMDguMjctLjA2LjM1LjA1LjA4LjExLjA2LjI3LS4wNS4zNS0uNjguNDUtMS4zNy44My0xLjk5LDEuMzMtMS44LDEuNDItMy4zOSwzLjQ4LTQuODUsNS4yNGgwWiI+PC9wYXRoPjxwYXRoIGQ9Ik0xMS4zMyw4LjA4Yy45MywxLjYxLDEuNjMsMy40MiwyLjAyLDUuMjYsMCwwLC45Miw1LjU0LjkyLDUuNTQuNDcsMy43Ljg1LDcuNDEsMS4yMSwxMS4xMi0uNjQtNS4zNC0xLjQ0LTExLjI2LTIuNTYtMTYuNTUtLjQ0LTEuODEtMS4xMy0zLjQ4LTIuMTEtNS4wOC0uMi0uMzQuMzUtLjY1LjUzLS4yOWgwWiI+PC9wYXRoPjxwYXRoIGQ9Ik0xNi41MywzMC4xNGMuMzYtMy43MS43NC03LjQyLDEuMjEtMTEuMTIsMCwwLC45Mi01LjU0LjkyLTUuNTQuMzktMS44NCwxLjA5LTMuNjUsMi4wMi01LjI2LjE3LS4zNS43Mi0uMDguNTQuMjctLjk4LDEuNjEtMS42OCwzLjI4LTIuMTIsNS4xLDAsMC0xLjA2LDUuNDctMS4wNiw1LjQ3LS41NywzLjY4LTEuMDUsNy4zOC0xLjUsMTEuMDhoMFoiPjwvcGF0aD48cGF0aCBkPSJNMjMuMDksNi43OWMxLjA4LS4xMSwxLjgzLjQxLDIuNTMsMS4xNSwxLjgzLDIuMDQsMi45OCw0LjU2LDQuMjYsNi45NS0xLjY0LTIuNTgtMi45Mi01Ljc0LTUuNTctNy40NS0uMzYtLjIzLS44My0uMTYtMS4yMS0uMTUtLjMzLjAyLS4zNC0uNTEsMC0uNWgwWiI+PC9wYXRoPjxwYXRoIGQ9Ik0yMy4yMSw3Ljg0YzIuNzEsMS42Nyw0LjY5LDQuNDMsNi41NCw2Ljk3LTEuOTctMi40Mi00LjAzLTUuMDMtNi44Mi02LjU2LS4yOS0uMTcsMC0uNjIuMjgtLjQxaDBaIj48L3BhdGg+PHBhdGggZD0iTTguMzIsNS41N2MuNzguNjMsMi4xOCwxLjg3LDIuOTcsMi40NC43OS4zMSwxLjYyLjQ4LDIuNTMuNDEuNzItLjA0LDEuNDUtLjE4LDIuMTgtLjI2LS43Mi4xNy0xLjQxLjM5LTIuMTUuNTEtLjc0LjEtMS40OS4xOS0yLjI5LS4wNS0uNC0uMDktLjc1LS4xNy0xLjA3LS40My0uNi0uNC0yLjE0LTEuNDEtMi43Ni0xLjgyLS41Ny0uMzkuMDYtMS4yNC42LS44aDBaIj48L3BhdGg+PHBhdGggZD0iTTE2LDguMTZjMS41OC4yNSwzLjI5LjUsNC43Ny0uMTYuNzctLjYzLDIuMjMtMS44NSwzLjAxLTIuNDkuNS0uNDIsMS4xNC4zNS42NC43Ny0uOS41OS0yLjQzLDEuNTktMy4zMSwyLjE3LTEuNzMuNjMtMy40My4xNi01LjEtLjI5aDBaIj48L3BhdGg+PHBhdGggZD0iTTE4Ljg5LDIxLjk5Yy0uODcuNDktMS44MywxLjAyLTIuODUsMS4wNiwxLjAxLS4yMiwxLjc4LS44NSwyLjU3LTEuNDguMjctLjIzLjU5LjI0LjI4LjQxaDBaIj48L3BhdGg+PHBhdGggZD0iTTE2LjA0LDE2LjgzYzEuMy0uMTcsMi40LS44OCwzLjMyLTEuNzguMzItLjI4LjYyLS41OC45My0uODkuMS0uMS4yNi0uMS4zNSwwLC4xMS4xLjEuMjgtLjAyLjM3LTEuMzIsMS4xLTIuNzksMi4yNS00LjU5LDIuM2gwWiI+PC9wYXRoPjxwYXRoIGQ9Ik0yMi40LDguOGMtLjg4LjgxLTEuNzcsMS42OS0yLjg0LDIuMjctMS4wOC41OS0yLjMyLjctMy41Mi43NCwxLjE5LS4xMiwyLjQtLjMxLDMuNC0uOTYuOTktLjYzLDEuNzktMS41NCwyLjYxLTIuNC4yMi0uMjYuNjEuMTIuMzYuMzVoMFoiPjwvcGF0aD48cGF0aCBkPSJNMTYuMzcsMi4zOWwtLjM4LDI3LjYxLS4zOC0yNy42MWMwLS40OS43NS0uNS43NSwwaDBaIj48L3BhdGg+PHBhdGggZD0iTTEzLjQzLDIxLjU4Yy43OS42MywxLjU2LDEuMjYsMi41NywxLjQ4LTEuMDItLjA0LTEuOTgtLjU3LTIuODUtMS4wNi0uMTItLjA3LS4xNy0uMjItLjEtLjM0LjA3LS4xNC4yNi0uMTcuMzgtLjA3aDBaIj48L3BhdGg+PHBhdGggZD0iTTE2LDE2LjgzYy0xLjc5LS4wNS0zLjI2LTEuMi00LjU5LTIuMy0uMTEtLjA5LS4xMi0uMjQtLjA0LS4zNS4wOS0uMTIuMjctLjEzLjM3LS4wMiwxLjIsMS4yMSwyLjUsMi40NSw0LjI1LDIuNjdoMFoiPjwvcGF0aD48cGF0aCBkPSJNOS45OSw4LjQ1Yy44Mi44NiwxLjYxLDEuNzcsMi42MSwyLjQsMSwuNjQsMi4yMS44MywzLjQuOTYtMS4yLS4wNC0yLjQ1LS4xNS0zLjUyLS43NC0xLjA3LS41OC0xLjk3LTEuNDYtMi44NS0yLjI3LS4yNC0uMjMuMTQtLjYuMzYtLjM1aDBaIj48L3BhdGg+PC9nPjwvZz48Zz48bGluZSB4MT0iMTguNiIgeTE9IjE4LjYiIHgyPSIzMC41IiB5Mj0iMzAuNSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjUuNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj48L2xpbmU+PGNpcmNsZSBjeD0iMTMiIGN5PSIxMyIgcj0iOS4yIiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iNSI+PC9jaXJjbGU+PGNpcmNsZSBjeD0iMTMiIGN5PSIxMyIgcj0iOS4yIiBmaWxsPSIjZmZmZmZmIiBmaWxsLW9wYWNpdHk9IjAuMjgiIHN0cm9rZT0iIzExMTExNCIgc3Ryb2tlLXdpZHRoPSIyLjEiPjwvY2lyY2xlPjxsaW5lIHgxPSIxOS4yIiB5MT0iMTkuMiIgeDI9IjMwIiB5Mj0iMzAiIHN0cm9rZT0iIzExMTExNCIgc3Ryb2tlLXdpZHRoPSIzLjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PC9saW5lPjwvZz48L3N2Zz4=" + } + } + }, { "id": "2046fcd3-8377-4c58-b672-8507fdb9b064", "language": { @@ -529,6 +628,21 @@ } } }, + { + "id": "82655369-f673-4500-9cae-0395ad799563", + "language": { + "id": "*.*.python", + "version": "3.*.*" + }, + "title": "RV_session_export", + "uri": "commands/RV_session_export.py", + "image": { + "light": { + "type": "svg", + "data": "PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMzIgMzIiPgogIDwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyOS44LjEwLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogMi4xLjEgQnVpbGQgMikgIC0tPgogIDxkZWZzPgogICAgPHN0eWxlPgogICAgICAuc3QwIHsKICAgICAgICBmaWxsOiB1cmwoI2xpbmVhci1ncmFkaWVudCk7CiAgICAgICAgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOwogICAgICB9CgogICAgICAuc3QwLCAuc3QxIHsKICAgICAgICBzdHJva2U6ICMwMDA7CiAgICAgIH0KCiAgICAgIC5zdDAsIC5zdDIgewogICAgICAgIGZpbGwtcnVsZTogZXZlbm9kZDsKICAgICAgfQoKICAgICAgLnN0MyB7CiAgICAgICAgZmlsbDogIzIyYjE0YzsKICAgICAgICBzdHJva2U6ICMxNzhhM2E7CiAgICAgICAgc3Ryb2tlLXdpZHRoOiAuODRweDsKICAgICAgfQoKICAgICAgLnN0MywgLnN0MSB7CiAgICAgICAgc3Ryb2tlLWxpbmVqb2luOiByb3VuZDsKICAgICAgfQoKICAgICAgLnN0MSB7CiAgICAgICAgZmlsbDogbm9uZTsKICAgICAgfQoKICAgICAgLnN0MiB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQxKTsKICAgICAgICBpc29sYXRpb246IGlzb2xhdGU7CiAgICAgICAgb3BhY2l0eTogLjc1OwogICAgICB9CiAgICA8L3N0eWxlPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHgxPSIxMi4wMiIgeTE9IjguMDQiIHgyPSIxMS45NCIgeTI9IjE5LjY5IiBncmFkaWVudFRyYW5zZm9ybT0idHJhbnNsYXRlKDAgMzQpIHNjYWxlKDEgLTEpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2U4OGEwZCIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNlODhkMTAiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIuMDkiIHN0b3AtY29sb3I9IiNlZWE5MjYiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIuMTgiIHN0b3AtY29sb3I9IiNmM2MwMzciLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIuMjkiIHN0b3AtY29sb3I9IiNmN2QyNDUiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIuNDIiIHN0b3AtY29sb3I9IiNmYWRlNGYiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIuNTkiIHN0b3AtY29sb3I9IiNmYmU1NTUiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmNlODU3Ii8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQxIiB4MT0iMTQuODQiIHkxPSI2LjgzIiB4Mj0iMTQuODQiIHkyPSIxNS41IiBncmFkaWVudFRyYW5zZm9ybT0idHJhbnNsYXRlKDAgMzQpIHNjYWxlKDEgLTEpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmYzkyNCIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNmYWY1YzQiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgPC9kZWZzPgogIDxnIGlkPSJSVi1sb2dvIj4KICAgIDxwb2x5Z29uIGNsYXNzPSJzdDAiIHBvaW50cz0iMS41IDMwLjUgMS41IDE1IDQgMTIuNSAxMSAxMi41IDEzIDE0LjUgMjEuNSAxNC41IDIyLjUgMTUuNSAyMi41IDMwLjUgMS41IDMwLjUiLz4KICAgIDxnPgogICAgICA8cG9seWdvbiBjbGFzcz0ic3QyIiBwb2ludHM9IjEuNSAzMC41IDYuODMgMTguNSAyOC4xNyAxOC41IDIyLjgzIDMwLjUgMS41IDMwLjUiLz4KICAgICAgPHBvbHlnb24gY2xhc3M9InN0MSIgcG9pbnRzPSIxLjUgMzAuNSA2LjgzIDE4LjUgMjguMTcgMTguNSAyMi44MyAzMC41IDEuNSAzMC41Ii8+CiAgICA8L2c+CiAgICA8Zz4KICAgICAgPHBvbHlnb24gY2xhc3M9InN0MyIgcG9pbnRzPSIyMC43NSAxLjAyIDI2LjkyIDEuMDIgMjYuOTIgMTAuMDkgMjAuNzUgMTAuMDkgMjAuNzUgMS4wMiIvPgogICAgICA8cG9seWdvbiBjbGFzcz0ic3QzIiBwb2ludHM9IjE2LjggMTAuMDkgMzAuODcgMTAuMDkgMjMuODQgMTguNDYgMTYuOCAxMC4wOSIvPgogICAgPC9nPgogIDwvZz4KPC9zdmc+Cg==" + } + } + }, { "id": "65337e9c-96b6-4cc3-a257-7a6ae95ddcd9", "language": { @@ -609,42 +723,6 @@ } } } - }, - { - "id": "82655369-f673-4500-9cae-0395ad799563", - "language": { - "id": "*.*.python", - "version": "3.*.*" - }, - "title": "RV_session_export", - "uri": "commands/RV_session_export.py" - }, - { - "id": "04e977e4-9c76-46a1-8378-a7d5eb00a25a", - "language": { - "id": "*.*.python", - "version": "3.*.*" - }, - "title": "RV_thrust_info", - "uri": "commands/RV_thrust_info.py" - }, - { - "id": "b4188b27-d157-4b8c-bd8e-7f8543ccaa93", - "language": { - "id": "*.*.python", - "version": "3.*.*" - }, - "title": "RV_dem_blocks", - "uri": "commands/RV_dem_blocks.py" - }, - { - "id": "f2ffb9b6-2616-48a2-818b-9d512ac0a3f3", - "language": { - "id": "*.*.python", - "version": "3.*.*" - }, - "title": "RV_form_solve", - "uri": "commands/RV_form_solve.py" } ], "resources": [ @@ -661,4 +739,4 @@ "uri": "resources/splash/compas-RV_dark.jpg" } ] -} \ No newline at end of file +} diff --git a/requirements.txt b/requirements.txt index b12943f..f5e5780 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,6 @@ compas_rui >=0.5.1 compas_session >=0.5.5 compas_skeleton compas_tna >=0.5 +compas_tno >=0.4.0 compas_triangle >=1.2.1 tessagon diff --git a/resources/icons/RV_blockexport.svg b/resources/icons/RV_blockexport.svg new file mode 100644 index 0000000..0971211 --- /dev/null +++ b/resources/icons/RV_blockexport.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/RV_envelope.svg b/resources/icons/RV_envelope.svg new file mode 100644 index 0000000..e917679 --- /dev/null +++ b/resources/icons/RV_envelope.svg @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/RV_export.svg b/resources/icons/RV_export.svg new file mode 100644 index 0000000..b0263b9 --- /dev/null +++ b/resources/icons/RV_export.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/RV_loads.svg b/resources/icons/RV_loads.svg new file mode 100644 index 0000000..982f133 --- /dev/null +++ b/resources/icons/RV_loads.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/RV_thrust_inspect.svg b/resources/icons/RV_thrust_inspect.svg new file mode 100644 index 0000000..6400f24 --- /dev/null +++ b/resources/icons/RV_thrust_inspect.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/RV_tno.svg b/resources/icons/RV_tno.svg new file mode 100644 index 0000000..84265cd --- /dev/null +++ b/resources/icons/RV_tno.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/compas_rv/conventions.py b/src/compas_rv/conventions.py new file mode 100644 index 0000000..8dd14b1 --- /dev/null +++ b/src/compas_rv/conventions.py @@ -0,0 +1,19 @@ +VERTEX_ATTRIBUTES = ["px", "py", "pz", "pzext", "_rx", "_ry", "_rz"] +EDGE_ATTRIBUTES = ["q", "_f"] + + +def invert_formdiagram_signs(formdiagram): + """Invert the equilibrium sign convention of a form diagram. + + Notes + ----- + Applying this function twice restores the original values. + + """ + for vertex in formdiagram.vertices(): + values = formdiagram.vertex_attributes(vertex, VERTEX_ATTRIBUTES) + formdiagram.vertex_attributes(vertex, VERTEX_ATTRIBUTES, [-value if value is not None else None for value in values]) + + for edge in formdiagram.edges_where(_is_edge=True): + values = formdiagram.edge_attributes(edge, EDGE_ATTRIBUTES) + formdiagram.edge_attributes(edge, EDGE_ATTRIBUTES, [-value if value is not None else None for value in values]) diff --git a/src/compas_rv/datastructures/__init__.py b/src/compas_rv/datastructures/__init__.py index 927c125..34e4894 100644 --- a/src/compas_rv/datastructures/__init__.py +++ b/src/compas_rv/datastructures/__init__.py @@ -1,13 +1,10 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division +from __future__ import absolute_import, division, print_function -from .subdmesh import SubdMesh -from .pattern import Pattern from .diagram import Diagram -from .formdiagram import FormDiagram from .forcediagram import ForceDiagram -from .thrustdiagram import ThrustDiagram +from .formdiagram import FormDiagram +from .pattern import Pattern +from .subdmesh import SubdMesh __all__ = [ "SubdMesh", @@ -15,5 +12,4 @@ "Diagram", "FormDiagram", "ForceDiagram", - "ThrustDiagram", ] diff --git a/src/compas_rv/datastructures/formdiagram.py b/src/compas_rv/datastructures/formdiagram.py index e9ceae9..e311b1f 100644 --- a/src/compas_rv/datastructures/formdiagram.py +++ b/src/compas_rv/datastructures/formdiagram.py @@ -1,4 +1,10 @@ +from compas.geometry import Box +from compas.geometry import bounding_box +from compas.geometry import centroid_points +from compas.geometry import cross_vectors +from compas.geometry import length_vector from compas.geometry import scale_vector +from compas.geometry import subtract_vectors from compas.geometry import sum_vectors from compas_fd.solvers import fd_numpy from compas_tna.diagrams import FormDiagram as BaseFormDiagram @@ -12,6 +18,17 @@ class FormDiagram(Diagram, BaseFormDiagram): Data structure for form diagrams. """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.default_vertex_attributes.update( + { + "pzext": None, + "ux": 0.0, + "uy": 0.0, + "uz": 0.0, + } + ) + @classmethod def from_pattern(cls, pattern: Pattern) -> "FormDiagram": """Construct a form diagram from a pattern. @@ -97,3 +114,125 @@ def flip_cycles_if_normal_down(self): normal = scale_vector(sum_vectors(normals), scale) if normal[2] < 0: self.flip_cycles() + + def vertex_tributary_area(self, vertex: int) -> float: + """ + Compute the tributary area of a vertex taking into account only the loaded faces. + + Parameters + ---------- + vertex : int + The vertex identifier. + + Returns + ------- + float + + """ + area = 0 + p0 = self.vertex_coordinates(vertex) + for nbr in self.halfedge[vertex]: + p1 = self.vertex_coordinates(nbr) + v1 = subtract_vectors(p1, p0) + fkey = self.halfedge[vertex][nbr] + if fkey is not None: + if self.face_attribute(fkey, "_is_loaded"): + p2 = self.face_centroid(fkey) + v2 = subtract_vectors(p2, p0) + area += length_vector(cross_vectors(v1, v2)) + fkey = self.halfedge[nbr][vertex] + if fkey is not None: + if self.face_attribute(fkey, "_is_loaded"): + p3 = self.face_centroid(fkey) + v3 = subtract_vectors(p3, p0) + area += length_vector(cross_vectors(v1, v3)) + return 0.25 * area + + def vertex_lumped_stress(self, vertex: int) -> float: + """ + Compute an approximation of the compressive stress at a vertex. + + Parameters + ---------- + vertex : int + The vertex identifier. + + Returns + ------- + float + + """ + stress = 0 + neighbors = self.vertex_neighbors(vertex) + count = 0 + for nbr in neighbors: + edge_area = 0 + edge_thickness = sum(self.vertices_attribute("t", keys=[vertex, nbr])) / 2 + edge_force = self.edge_attribute((vertex, nbr), "_f") + + if abs(edge_force) <= 0: + continue + + mp = self.edge_midpoint((vertex, nbr)) + + f0 = self.halfedge_face((vertex, nbr)) + if f0 is not None: + if self.face_attribute(f0, "_is_loaded"): + f0_c = self.face_center(f0) + area = length_vector(subtract_vectors(f0_c, mp)) * edge_thickness + if area > 0: + edge_area += area + f1 = self.halfedge_face((nbr, vertex)) + if f1 is not None: + if self.face_attribute(f1, "_is_loaded"): + f1_c = self.face_center(f1) + area = length_vector(subtract_vectors(f1_c, mp)) * edge_thickness + if area > 0: + edge_area += area + + if edge_area > 0: + stress += edge_force / edge_area + count += 1 + + return stress / count + + def find_outward_displacement(self, vertices: list[int]) -> dict[int, list[float]]: + """Compute unit horizontal displacement vectors pointing away from the centroid of the supports. + + Parameters + ---------- + vertices : list[int] + The vertices for which to compute an outward displacement direction. + + Returns + ------- + dict[int, list[float]] + Mapping from vertex identifier to a unit ``[ux, uy, 0.0]`` vector pointing + away from the centroid of the diagram's supports, in the XY plane. + + """ + supports = list(self.supports()) + centroid = centroid_points(self.vertices_attributes("xyz", keys=supports)) + + directions = {} + for vertex in vertices: + x, y, _ = self.vertex_attributes(vertex, "xyz") # type: ignore + vector = [x - centroid[0], y - centroid[1], 0.0] + length = length_vector(vector) + if length > 1e-9: + vector = scale_vector(vector, 1.0 / length) + directions[vertex] = vector + return directions + + def compute_zmax(self) -> float: + """Compute a suitable value for zmax based on the length of the diagonal of the bounding box of the projection of the diagram in XY. + + Returns + ------- + float + Maximum Z coordinate. + + """ + bbox = Box.from_bounding_box(bounding_box(self.vertices_attributes("xyz"))) + diagonal = bbox.points[2] - bbox.points[0] + return 0.25 * diagonal.length diff --git a/src/compas_rv/datastructures/thrustdiagram.py b/src/compas_rv/datastructures/thrustdiagram.py deleted file mode 100644 index fa16ec7..0000000 --- a/src/compas_rv/datastructures/thrustdiagram.py +++ /dev/null @@ -1,105 +0,0 @@ -from compas.geometry import Box -from compas.geometry import bounding_box -from compas.geometry import cross_vectors -from compas.geometry import length_vector -from compas.geometry import subtract_vectors - -from .formdiagram import FormDiagram - - -class ThrustDiagram(FormDiagram): - """Data structure for thrust diagrams.""" - - def vertex_tributary_area(self, vertex: int) -> float: - """ - Compute the tributary area of a vertex taking into account only the loaded faces. - - Parameters - ---------- - vertex : int - The vertex identifier. - - Returns - ------- - float - - """ - area = 0 - p0 = self.vertex_coordinates(vertex) - for nbr in self.halfedge[vertex]: - p1 = self.vertex_coordinates(nbr) - v1 = subtract_vectors(p1, p0) - fkey = self.halfedge[vertex][nbr] - if fkey is not None: - if self.face_attribute(fkey, "_is_loaded"): - p2 = self.face_centroid(fkey) - v2 = subtract_vectors(p2, p0) - area += length_vector(cross_vectors(v1, v2)) - fkey = self.halfedge[nbr][vertex] - if fkey is not None: - if self.face_attribute(fkey, "_is_loaded"): - p3 = self.face_centroid(fkey) - v3 = subtract_vectors(p3, p0) - area += length_vector(cross_vectors(v1, v3)) - return 0.25 * area - - def vertex_lumped_stress(self, vertex: int) -> float: - """ - Compute an approximation of the compressive stress at a vertex. - - Parameters - ---------- - vertex : int - The vertex identifier. - - Returns - ------- - float - - """ - stress = 0 - neighbors = self.vertex_neighbors(vertex) - count = 0 - for nbr in neighbors: - edge_area = 0 - edge_thickness = sum(self.vertices_attribute("t", keys=[vertex, nbr])) / 2 - edge_force = self.edge_attribute((vertex, nbr), "_f") - - if abs(edge_force) <= 0: - continue - - mp = self.edge_midpoint((vertex, nbr)) - - f0 = self.halfedge_face((vertex, nbr)) - if f0 is not None: - if self.face_attribute(f0, "_is_loaded"): - f0_c = self.face_center(f0) - area = length_vector(subtract_vectors(f0_c, mp)) * edge_thickness - if area > 0: - edge_area += area - f1 = self.halfedge_face((nbr, vertex)) - if f1 is not None: - if self.face_attribute(f1, "_is_loaded"): - f1_c = self.face_center(f1) - area = length_vector(subtract_vectors(f1_c, mp)) * edge_thickness - if area > 0: - edge_area += area - - if edge_area > 0: - stress += edge_force / edge_area - count += 1 - - return stress / count - - def compute_zmax(self) -> float: - """Compute a suitable value for zmax based on the length of the diagonal of the bounding box of the projection of the diagram in XY. - - Returns - ------- - float - Maximum Z coordinate. - - """ - bbox = Box.from_bounding_box(bounding_box(self.vertices_attributes("xyz"))) - diagonal = bbox.points[2] - bbox.points[0] - return 0.25 * diagonal.length diff --git a/src/compas_rv/patterns/__init__.py b/src/compas_rv/patterns/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/compas_rv/patterns/circular.py b/src/compas_rv/patterns/circular.py deleted file mode 100644 index 3211bac..0000000 --- a/src/compas_rv/patterns/circular.py +++ /dev/null @@ -1,294 +0,0 @@ -import math - -from compas.geometry import intersection_line_line_xy -from compas_rv.datastructures import Pattern - - -def create_circular_radial_pattern( - center=[5.0, 5.0], - radius=5.0, - discretisation=[8, 20], - r_oculus=0.0, - diagonal=False, - partial_diagonal=False, -) -> Pattern: - """Construct a circular radial FormDiagram with hoops equally spaced in plan. - - Parameters - ---------- - center : list, optional - Planar coordinates of the form-diagram [xc, yc], by default [5.0, 5.0] - radius : float, optional - Radius of the form diagram, by default 5.0 - discretisation : list, optional - Number of hoops, and of parallels of the dome form diagram], by default [8, 20] - r_oculus : float, optional - Value of the radius of the oculus, if no oculus is present should be set to zero, by default 0.0 - diagonal : bool, optional - Activate diagonal in the quads, by default False - partial_diagonal : bool, optional - Activate partial diagonal in the quads, by default False - - Returns - ------- - :class:`~compas_tno.diagrams.FormDiagram` - The FormDiagram created. - - """ - - xc = center[0] - yc = center[1] - n_radial = discretisation[0] - n_spikes = discretisation[1] - theta = 2 * math.pi / n_spikes - r_div = (radius - r_oculus) / n_radial - lines = [] - - for nr in range(n_radial + 1): - for nc in range(n_spikes): - if (r_oculus + nr * r_div) > 0.0: - # Meridian Elements - xa = xc + (r_oculus + nr * r_div) * math.cos(theta * nc) - xb = xc + (r_oculus + nr * r_div) * math.cos(theta * (nc + 1)) - ya = yc + (r_oculus + nr * r_div) * math.sin(theta * nc) - yb = yc + (r_oculus + nr * r_div) * math.sin(theta * (nc + 1)) - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - - if nr <= n_radial - 1: - # Radial Elements - xa = xc + (r_oculus + nr * r_div) * math.cos(theta * nc) - xb = xc + (r_oculus + (nr + 1) * r_div) * math.cos(theta * nc) - ya = yc + (r_oculus + nr * r_div) * math.sin(theta * nc) - yb = yc + (r_oculus + (nr + 1) * r_div) * math.sin(theta * nc) - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - - if diagonal: - for nr in range(n_radial): - for nc in range(n_spikes): - if (r_oculus + nr * r_div) > 0.0: - # Meridian Element i - xa = xc + (r_oculus + nr * r_div) * math.cos(theta * nc) - xb = xc + (r_oculus + nr * r_div) * math.cos(theta * (nc + 1)) - ya = yc + (r_oculus + nr * r_div) * math.sin(theta * nc) - yb = yc + (r_oculus + nr * r_div) * math.sin(theta * (nc + 1)) - - # Meridian Element i + 1 - xa_ = xc + (r_oculus + (nr + 1) * r_div) * math.cos(theta * nc) - xb_ = xc + (r_oculus + (nr + 1) * r_div) * math.cos(theta * (nc + 1)) - ya_ = yc + (r_oculus + (nr + 1) * r_div) * math.sin(theta * nc) - yb_ = yc + (r_oculus + (nr + 1) * r_div) * math.sin(theta * (nc + 1)) - - if partial_diagonal == "right": - if nc + 1 > n_spikes / 2: - lines.append([[xa, ya, 0.0], [xb_, yb_, 0.0]]) - else: - lines.append([[xa_, ya_, 0.0], [xb, yb, 0.0]]) - elif partial_diagonal == "left": - if nc + 1 > n_spikes / 2: - lines.append([[xa_, ya_, 0.0], [xb, yb, 0.0]]) - else: - lines.append([[xa, ya, 0.0], [xb_, yb_, 0.0]]) - elif partial_diagonal == "rotation": - lines.append([[xa, ya, 0.0], [xb_, yb_, 0.0]]) - elif partial_diagonal == "straight": - midx, midy, _ = intersection_line_line_xy([[xa, ya], [xb_, yb_]], [[xa_, ya_], [xb, yb]]) # type: ignore - lines.append([[xa, ya, 0.0], [midx, midy, 0.0]]) - lines.append([[midx, midy, 0.0], [xb_, yb_, 0.0]]) - lines.append([[xa_, ya_, 0.0], [midx, midy, 0.0]]) - lines.append([[midx, midy, 0.0], [xb, yb, 0.0]]) - else: - midx = (xa + xa_ + xb + xb_) / 4 - midy = (ya + ya_ + yb + yb_) / 4 - lines.append([[xa, ya, 0.0], [midx, midy, 0.0]]) - lines.append([[midx, midy, 0.0], [xb_, yb_, 0.0]]) - lines.append([[xa_, ya_, 0.0], [midx, midy, 0.0]]) - lines.append([[midx, midy, 0.0], [xb, yb, 0.0]]) - - pattern: Pattern = Pattern.from_lines(lines, delete_boundary_face=True) # type: ignore - return pattern - - -def create_circular_radial_spaced_pattern( - center=[5.0, 5.0], - radius=5.0, - discretisation=[8, 20], - r_oculus=0.0, - diagonal=False, - partial_diagonal=False, -) -> Pattern: - """Construct a circular radial FormDiagram with hoops not equally spaced in plan, but equally spaced with regards to the projection on a hemisphere. - - Parameters - ---------- - center : list, optional - Planar coordinates of the form-diagram [xc, yc], by default [5.0, 5.0] - radius : float, optional - Radius of the form diagram, by default 5.0 - discretisation : list, optional - Number of hoops, and of parallels of the dome form diagram], by default [8, 20] - r_oculus : float, optional - Value of the radius of the oculus, if no oculus is present should be set to zero, by default 0.0 - diagonal : bool, optional - Activate diagonal in the quads, by default False - partial_diagonal : bool, optional - Activate partial diagonal in the quads, by default False - - Returns - ------- - form : :class:`~compas_tno.diagrams.FormDiagram` - The FormDiagram created. - - """ - xc = center[0] - yc = center[1] - n_radial = discretisation[0] - n_spikes = discretisation[1] - theta = 2 * math.pi / n_spikes - r_div = (radius - r_oculus) / n_radial - lines = [] - - for nr in range(n_radial + 1): - for nc in range(n_spikes): - if (r_oculus + nr) > 0: - # Meridian Elements - xa = xc + (r_oculus + radius * math.cos((n_radial - nr) / n_radial * math.pi / 2)) * math.cos(theta * nc) - xb = xc + (r_oculus + radius * math.cos((n_radial - nr) / n_radial * math.pi / 2)) * math.cos(theta * (nc + 1)) - ya = yc + (r_oculus + radius * math.cos((n_radial - nr) / n_radial * math.pi / 2)) * math.sin(theta * nc) - yb = yc + (r_oculus + radius * math.cos((n_radial - nr) / n_radial * math.pi / 2)) * math.sin(theta * (nc + 1)) - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - - if nr <= n_radial - 1: - # Radial Elements - xa = xc + (r_oculus + radius * math.cos((n_radial - nr) / n_radial * math.pi / 2)) * math.cos(theta * nc) - xb = xc + (r_oculus + radius * math.cos((n_radial - (nr + 1)) / n_radial * math.pi / 2)) * math.cos(theta * nc) - ya = yc + (r_oculus + radius * math.cos((n_radial - nr) / n_radial * math.pi / 2)) * math.sin(theta * nc) - yb = yc + (r_oculus + radius * math.cos((n_radial - (nr + 1)) / n_radial * math.pi / 2)) * math.sin(theta * nc) - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - - if diagonal: - for nr in range(n_radial): - for nc in range(n_spikes): - if (r_oculus + nr * r_div) > 0.0: - # Meridian Element i - xa = xc + (r_oculus + radius * math.cos((n_radial - nr) / n_radial * math.pi / 2)) * math.cos(theta * nc) - xb = xc + (r_oculus + radius * math.cos((n_radial - nr) / n_radial * math.pi / 2)) * math.cos(theta * (nc + 1)) - ya = yc + (r_oculus + radius * math.cos((n_radial - nr) / n_radial * math.pi / 2)) * math.sin(theta * nc) - yb = yc + (r_oculus + radius * math.cos((n_radial - nr) / n_radial * math.pi / 2)) * math.sin(theta * (nc + 1)) - - # radius * math.cos((n_radial - (nr + 1)) / n_radial * math.pi / 2) - - # Meridian Element i + 1 - xa_ = xc + (r_oculus + radius * math.cos((n_radial - (nr + 1)) / n_radial * math.pi / 2)) * math.cos(theta * nc) - xb_ = xc + (r_oculus + radius * math.cos((n_radial - (nr + 1)) / n_radial * math.pi / 2)) * math.cos(theta * (nc + 1)) - ya_ = yc + (r_oculus + radius * math.cos((n_radial - (nr + 1)) / n_radial * math.pi / 2)) * math.sin(theta * nc) - yb_ = yc + (r_oculus + radius * math.cos((n_radial - (nr + 1)) / n_radial * math.pi / 2)) * math.sin(theta * (nc + 1)) - if partial_diagonal == "right": - if nc + 1 > n_spikes / 2: - lines.append([[xa, ya, 0.0], [xb_, yb_, 0.0]]) - else: - lines.append([[xa_, ya_, 0.0], [xb, yb, 0.0]]) - elif partial_diagonal == "left": - if nc + 1 > n_spikes / 2: - lines.append([[xa_, ya_, 0.0], [xb, yb, 0.0]]) - else: - lines.append([[xa, ya, 0.0], [xb_, yb_, 0.0]]) - elif partial_diagonal == "straight": - midx, midy, _ = intersection_line_line_xy([[xa, ya], [xb_, yb_]], [[xa_, ya_], [xb, yb]]) # type: ignore - lines.append([[xa, ya, 0.0], [midx, midy, 0.0]]) - lines.append([[midx, midy, 0.0], [xb_, yb_, 0.0]]) - lines.append([[xa_, ya_, 0.0], [midx, midy, 0.0]]) - lines.append([[midx, midy, 0.0], [xb, yb, 0.0]]) - else: - midx = (xa + xa_ + xb + xb_) / 4 - midy = (ya + ya_ + yb + yb_) / 4 - lines.append([[xa, ya, 0.0], [midx, midy, 0.0]]) - lines.append([[midx, midy, 0.0], [xb_, yb_, 0.0]]) - lines.append([[xa_, ya_, 0.0], [midx, midy, 0.0]]) - lines.append([[midx, midy, 0.0], [xb, yb, 0.0]]) - - pattern: Pattern = Pattern.from_lines(lines, delete_boundary_face=True) # type: ignore - return pattern - - -def create_circular_spiral_pattern( - center=[5.0, 5.0], - radius=5.0, - discretisation=[8, 20], - r_oculus=0.0, -) -> Pattern: - """Construct a circular radial FormDiagram with hoops not equally spaced in plan, but equally spaced with regards to the projection on a hemisphere. - - Parameters - ---------- - center : list, optional - Planar coordinates of the form-diagram [xc, yc], by default [5.0, 5.0] - radius : float, optional - Radius of the form diagram, by default 5.0 - discretisation : list, optional - Number of hoops, and of parallels of the dome form diagram], by default [8, 20] - r_oculus : float, optional - Value of the radius of the oculus, if no oculus is present should be set to zero, by default 0.0 - - Returns - ------- - :class:`~compas_tno.diagrams.FormDiagram` - The FormDiagram created. - - """ - xc = center[0] - yc = center[1] - n_radial = discretisation[0] - n_spikes = discretisation[1] - theta = 2 * math.pi / n_spikes - r_div = (radius - r_oculus) / n_radial - lines = [] - - for nr in range(n_radial + 1): - for nc in range(n_spikes): - if nr > 0.0: # This avoid the center... - if nr % 2 == 0: - # Diagonal to Up - xa = xc + (r_oculus + nr * r_div) * math.cos(theta * nc) - xb = xc + (r_oculus + (nr - 1) * r_div) * math.cos(theta * (nc + 1 / 2)) - ya = yc + (r_oculus + nr * r_div) * math.sin(theta * nc) - yb = yc + (r_oculus + (nr - 1) * r_div) * math.sin(theta * (nc + 1 / 2)) - - # Diagonal to Down - xa_ = xc + (r_oculus + nr * r_div) * math.cos(theta * nc) - xb_ = xc + (r_oculus + (nr - 1) * r_div) * math.cos(theta * (nc - 1 / 2)) - ya_ = yc + (r_oculus + nr * r_div) * math.sin(theta * nc) - yb_ = yc + (r_oculus + (nr - 1) * r_div) * math.sin(theta * (nc - 1 / 2)) - - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - lines.append([[xa_, ya_, 0.0], [xb_, yb_, 0.0]]) - else: - # Diagonal to Up - xa = xc + (r_oculus + nr * r_div) * math.cos(theta * (nc + 1 / 2)) - xb = xc + (r_oculus + (nr - 1) * r_div) * math.cos(theta * (nc + 1)) - ya = yc + (r_oculus + nr * r_div) * math.sin(theta * (nc + 1 / 2)) - yb = yc + (r_oculus + (nr - 1) * r_div) * math.sin(theta * (nc + 1)) - - # Diagonal to Down - xa_ = xc + (r_oculus + nr * r_div) * math.cos(theta * (nc + 1 / 2)) - xb_ = xc + (r_oculus + (nr - 1) * r_div) * math.cos(theta * (nc)) - ya_ = yc + (r_oculus + nr * r_div) * math.sin(theta * (nc + 1 / 2)) - yb_ = yc + (r_oculus + (nr - 1) * r_div) * math.sin(theta * (nc)) - - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - lines.append([[xa_, ya_, 0.0], [xb_, yb_, 0.0]]) - if nr == n_radial: - xa = xc + (r_oculus + nr * r_div) * math.cos(theta * nc) - xb = xc + (r_oculus + nr * r_div) * math.cos(theta * (nc + 1)) - ya = yc + (r_oculus + nr * r_div) * math.sin(theta * nc) - yb = yc + (r_oculus + nr * r_div) * math.sin(theta * (nc + 1)) - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - if nr == 0 and r_oculus > 0.0: - # If oculus, this will be the compression ring - xa = xc + (r_oculus) * math.cos(theta * (nc)) - xb = xc + (r_oculus) * math.cos(theta * (nc + 1)) - ya = yc + (r_oculus) * math.sin(theta * (nc)) - yb = yc + (r_oculus) * math.sin(theta * (nc + 1)) - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - - pattern: Pattern = Pattern.from_lines(lines, delete_boundary_face=True) # type: ignore - return pattern diff --git a/src/compas_rv/patterns/rectangular.py b/src/compas_rv/patterns/rectangular.py deleted file mode 100644 index b5b5778..0000000 --- a/src/compas_rv/patterns/rectangular.py +++ /dev/null @@ -1,603 +0,0 @@ -import math - -from compas.geometry import distance_point_point_xy -from compas.geometry import mirror_points_line -from compas.geometry import rotate_points_xy -from compas_rv.datastructures import Pattern - - -def mirror_4x(line, line_hor, line_ver, lines): - """Helper to mirror an object 4 times.""" - lines.append(line) - a_mirror, b_mirror = mirror_points_line(line, line_hor) - lines.append([a_mirror, b_mirror]) - a_mirror, b_mirror = mirror_points_line([a_mirror, b_mirror], line_ver) - lines.append([a_mirror, b_mirror]) - a_mirror, b_mirror = mirror_points_line(line, line_ver) - lines.append([a_mirror, b_mirror]) - return lines - - -def mirror_8x(line, origin, line_hor, line_ver, lines): - """Helper to mirror an object 8 times.""" - lines = mirror_4x(line, line_hor, line_ver, lines) - rot = rotate_points_xy(line, math.pi / 2, origin=origin) - lines = mirror_4x(rot, line_hor, line_ver, lines) - return lines - - -def append_mirrored_lines(line, list_, line_hor, line_ver): - """Helper to mirror an object 8 times and add to the list""" - mirror_a = mirror_points_line(line, line_hor) - mirror_b = mirror_points_line(mirror_a, line_ver) - mirror_c = mirror_points_line(line, line_ver) - list_.append(mirror_a) - list_.append(mirror_b) - list_.append(mirror_c) - - -def create_cross_pattern( - xy_span=[[0.0, 10.0], [0.0, 10.0]], - discretisation=10, -) -> Pattern: - """Construct a FormDiagram based on cross discretiastion with orthogonal arrangement and diagonal. - - Parameters - ---------- - xy_span : list, optional - List with initial- and end-points of the vault, by default [[0.0, 10.0], [0.0, 10.0]] - discretisation : int, optional - Set the density of the grid in x and y directions, by default 10 - fix : str, optional - Option to select the constrained nodes: 'corners', 'all' are accepted., by default 'corners' - - Returns - ------- - :class:`~compas_tno.diagrams.FormDiagram` - The FormDiagram created. - - Notes - ---------------------- - Position of the quadrants is as in the schema below: - - Q3 - Q2 Q1 - Q4 - """ - if isinstance(discretisation, list): - discretisation = discretisation[0] - - y1 = float(xy_span[1][1]) - y0 = float(xy_span[1][0]) - x1 = float(xy_span[0][1]) - x0 = float(xy_span[0][0]) - x_span = x1 - x0 - y_span = y1 - y0 - dx = x_span / discretisation - dy = y_span / discretisation - - lines = [] - - for i in range(discretisation + 1): - for j in range(discretisation + 1): - if i < discretisation and j < discretisation: - # Vertical Members: - xa = x0 + dx * i - ya = y0 + dy * j - xb = x0 + dx * (i + 1) - yb = y0 + dy * j - # Horizontal Members: - xc = x0 + dx * i - yc = y0 + dy * j - xd = x0 + dx * i - yd = y0 + dy * (j + 1) - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - lines.append([[xc, yc, 0.0], [xd, yd, 0.0]]) - if i == j: - # Diagonal Members in + Direction: - xc = x0 + dx * i - yc = y0 + dy * j - xd = x0 + dx * (i + 1) - yd = y0 + dy * (j + 1) - lines.append([[xc, yc, 0.0], [xd, yd, 0.0]]) - if i + j == discretisation: - # Diagonal Members in - Direction: - xc = x0 + dx * i - yc = y0 + dy * j - xd = x0 + dx * (i - 1) - yd = y0 + dy * (j + 1) - lines.append([[xc, yc, 0.0], [xd, yd, 0.0]]) - if i == (discretisation - 1): - xc = x0 + dx * i - yc = y0 + dy * j - xd = x0 + dx * (i + 1) - yd = y0 + dy * (j - 1) - lines.append([[xc, yc, 0.0], [xd, yd, 0.0]]) - else: - if i == discretisation and j < discretisation: - # Vertical Members on last column: - xa = x0 + dx * j - ya = y0 + dy * i - xb = x0 + dx * (j + 1) - yb = y0 + dy * i - # Horizontal Members: - xc = x0 + dx * i - yc = y0 + dy * j - xd = x0 + dx * i - yd = y0 + dy * (j + 1) - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - lines.append([[xc, yc, 0.0], [xd, yd, 0.0]]) - - pattern: Pattern = Pattern.from_lines(lines, delete_boundary_face=True) # type: ignore - return pattern - - -def create_cross_diagonal_pattern( - xy_span=[[0.0, 10.0], [0.0, 10.0]], - partial_bracing_modules=None, - discretisation=10, -) -> Pattern: - """Construct a FormDiagram based on a mixture of cross and fan discretiastion - - Parameters - ---------- - xy_span : list, optional - List with initial- and end-points of the vault, by default [[0.0, 10.0], [0.0, 10.0]] - discretisation : int, optional - Set the density of the grid in x and y directions, by default 10 - partial_bracing_modules : str, optional - If partial bracing modules are included, by default None - fix : str, optional - Option to select the constrained nodes: 'corners', 'all' are accepted, by default 'corners' - - Returns - ------- - :class:`~compas_tno.diagrams.FormDiagram` - The FormDiagram created. - - """ - if isinstance(discretisation, list): - discretisation = discretisation[0] - - y1 = float(xy_span[1][1]) - y0 = float(xy_span[1][0]) - x1 = float(xy_span[0][1]) - x0 = float(xy_span[0][0]) - x_span = x1 - x0 - y_span = y1 - y0 - dx = x_span / discretisation - dy = y_span / discretisation - - xc0 = x0 + x_span / 2 - yc0 = y0 + y_span / 2 - - nx = ny = int(discretisation / 2) - if partial_bracing_modules is None: - nstop = 0 - else: - nstop = nx - partial_bracing_modules # Test to stop - - line_hor = [[x0, yc0, 0.0], [xc0, yc0, 0.0]] - line_ver = [[xc0, y0, 0.0], [xc0, yc0, 0.0]] - origin = [xc0, yc0, 0.0] - - lines = [] - - for i in range(nx): - for j in range(ny + 1): - if j <= i: - if i >= nstop and j >= nstop: - # Diagonal Members: - xa = x0 + dx * i - ya = y0 + dy * 1 * (i - j) / (nx - j) + dy * j - xb = x0 + dx * (i + 1) - yb = y0 + dy * 1 * (i - j + 1) / (nx - j) + dy * j - lin = [[xa, ya, 0.0], [xb, yb, 0.0]] - lines = mirror_8x(lin, origin, line_hor, line_ver, lines) - - if i == j and i < nx - 1: - # Main diagonal: - xa = x0 + dx * i - ya = y0 + dy * j - xb = x0 + dx * (i + 1) - yb = y0 + dy * (j + 1) - lin = [[xa, ya, 0.0], [xb, yb, 0.0]] - lines = mirror_4x(lin, line_hor, line_ver, lines) - - # Horizontal Members: - xa = x0 + dx * i - ya = y0 + dy * j - xb = x0 + dx * (i + 1) - yb = y0 + dy * j - lin = [[xa, ya, 0.0], [xb, yb, 0.0]] - lines = mirror_8x(lin, origin, line_hor, line_ver, lines) - - i += 1 - # Vertical Members: - xa = x0 + dx * i - ya = y0 + dy * j - xb = x0 + dx * i - yb = y0 + dy * (j + 1) - - if i >= nstop and j >= nstop: - x_ = xa - y_ = y0 + dy * 1 * (i - j) / (nx - j) + dy * j - lin = [[xa, ya, 0.0], [x_, y_, 0.0]] - lines = mirror_8x(lin, origin, line_hor, line_ver, lines) - lin = [[x_, y_, 0.0], [xb, yb, 0.0]] - lines = mirror_8x(lin, origin, line_hor, line_ver, lines) - else: - lin = [[xa, ya, 0.0], [xb, yb, 0.0]] - lines = mirror_8x(lin, origin, line_hor, line_ver, lines) - - i -= 1 - - pattern: Pattern = Pattern.from_lines(lines, delete_boundary_face=True) # type: ignore - return pattern - - -def create_cross_with_diagonal_pattern( - xy_span=[[0.0, 10.0], [0.0, 10.0]], - discretisation=10, -) -> Pattern: - """Construct a FormDiagram based on cross discretiastion with diagonals. - - Parameters - ---------- - xy_span : list, optional - List with initial- and end-points of the vault, by default [[0.0, 10.0], [0.0, 10.0]] - discretisation : int, optional - Set the density of the grid in x and y directions, by default 10 - fix : str, optional - Option to select the constrained nodes: 'corners', 'all' are accepted, by default 'corners' - - Returns - ------- - :class:`~compas_tno.diagrams.FormDiagram` - The FormDiagram created. - """ - - if isinstance(discretisation, list): - discretisation = discretisation[0] - - y1 = float(xy_span[1][1]) - y0 = float(xy_span[1][0]) - x1 = float(xy_span[0][1]) - x0 = float(xy_span[0][0]) - x_span = x1 - x0 - y_span = y1 - y0 - dx = x_span / discretisation - dy = y_span / discretisation - - lines = [] - - for i in range(discretisation + 1): - for j in range(discretisation + 1): - if i < discretisation and j < discretisation: - # Hor Members: - xa = x0 + dx * i - ya = y0 + dy * j - xb = x0 + dx * (i + 1) - yb = y0 + dy * j - # Ver Members: - xc = x0 + dx * i - yc = y0 + dy * j - xd = x0 + dx * i - yd = y0 + dy * (j + 1) - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - lines.append([[xc, yc, 0.0], [xd, yd, 0.0]]) - # lines.append([[xc, yc, 0.0], [xd, yd, 0.0]]) - if (i < discretisation / 2 and j < discretisation / 2) or (i >= discretisation / 2 and j >= discretisation / 2): - # Diagonal Members in + Direction: - xc = x0 + dx * i - yc = y0 + dy * j - xd = x0 + dx * (i + 1) - yd = y0 + dy * (j + 1) - lines.append([[xc, yc, 0.0], [xd, yd, 0.0]]) - else: - # Diagonal Members in - Direction: - xc = x0 + dx * i - yc = y0 + dy * (j + 1) - xd = x0 + dx * (i + 1) - yd = y0 + dy * j - lines.append([[xc, yc, 0.0], [xd, yd, 0.0]]) - # if i == (discretisation - 1): - # xc = x0 + dx*i - # yc = y0 + dy*j - # xd = x0 + dx*(i + 1) - # yd = y0 + dy*(j - 1) - # lines.append([[xc, yc, 0.0], [xd, yd, 0.0]]) - else: - if i == discretisation and j < discretisation: - # Vertical Members on last column: - xa = x0 + dx * j - ya = y0 + dy * i - xb = x0 + dx * (j + 1) - yb = y0 + dy * i - # Horizontal Members: - xc = x0 + dx * i - yc = y0 + dy * j - xd = x0 + dx * i - yd = y0 + dy * (j + 1) - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - lines.append([[xc, yc, 0.0], [xd, yd, 0.0]]) - - pattern: Pattern = Pattern.from_lines(lines, delete_boundary_face=True) # type: ignore - return pattern - - -def create_fan_pattern( - xy_span=[[0.0, 10.0], [0.0, 10.0]], - discretisation=[10, 10], -) -> Pattern: - """Helper to construct a FormDiagram based on fan discretiastion with straight lines to the corners. - - Parameters - ---------- - xy_span : list, optional - List with initial- and end-points of the vault, by default [[0.0, 10.0], [0.0, 10.0]] - discretisation : int, optional - Set the density of the grid in x and y directions, by default 10 - fix : str, optional - Option to select the constrained nodes: 'corners', 'all' are accepted, by default 'corners' - - Returns - ------- - :class:`~compas_tno.diagrams.FormDiagram` - The FormDiagram created. - """ - - if isinstance(discretisation, int): - discretisation = [discretisation, discretisation] - if discretisation[0] % 2 != 0 or discretisation[1] % 2 != 0: - msg = "Warning!: discretisation of this form diagram has to be even." - raise ValueError(msg) - - y1 = float(xy_span[1][1]) - y0 = float(xy_span[1][0]) - x1 = float(xy_span[0][1]) - x0 = float(xy_span[0][0]) - - x_span = x1 - x0 - y_span = y1 - y0 - xc0 = x0 + x_span / 2 - yc0 = y0 + y_span / 2 - division_x = discretisation[0] - division_y = discretisation[1] - dx = float(x_span / division_x) - dy = float(y_span / division_y) - nx = int(division_x / 2) - ny = int(division_y / 2) - line_hor = [[x0, yc0, 0.0], [xc0, yc0, 0.0]] - line_ver = [[xc0, y0, 0.0], [xc0, yc0, 0.0]] - - lines = [] - - for i in range(nx): - for j in range(ny + 1): - # Diagonal Members: - xa = x0 + dx * i - ya = y0 + dy * j * i / nx - xb = x0 + dx * (i + 1) - yb = y0 + dy * j * (i + 1) / nx - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - - a_mirror, b_mirror = mirror_points_line([[xa, ya, 0.0], [xb, yb, 0.0]], line_hor) - lines.append([a_mirror, b_mirror]) - a_mirror, b_mirror = mirror_points_line([a_mirror, b_mirror], line_ver) - lines.append([a_mirror, b_mirror]) - a_mirror, b_mirror = mirror_points_line([[xa, ya, 0.0], [xb, yb, 0.0]], line_ver) - lines.append([a_mirror, b_mirror]) - - xa_ = x0 + dx * j * i / nx - ya_ = y0 + dy * i - xb_ = x0 + dx * j * (i + 1) / nx - yb_ = y0 + dy * (i + 1) - lines.append([[xa_, ya_, 0.0], [xb_, yb_, 0.0]]) - - a_mirror, b_mirror = mirror_points_line([[xa_, ya_, 0.0], [xb_, yb_, 0.0]], line_hor) - lines.append([a_mirror, b_mirror]) - a_mirror, b_mirror = mirror_points_line([a_mirror, b_mirror], line_ver) - lines.append([a_mirror, b_mirror]) - a_mirror, b_mirror = mirror_points_line([[xa_, ya_, 0.0], [xb_, yb_, 0.0]], line_ver) - lines.append([a_mirror, b_mirror]) - - if j < ny: - # Vertical or Horizontal Members: - xc = x0 + dx * (i + 1) - yc = y0 + dy * j * (i + 1) / nx - xd = x0 + dx * (i + 1) - yd = y0 + dy * (j + 1) * (i + 1) / nx - lines.append([[xc, yc, 0.0], [xd, yd, 0.0]]) - - c_mirror, d_mirror = mirror_points_line([[xc, yc, 0.0], [xd, yd, 0.0]], line_hor) - lines.append([c_mirror, d_mirror]) - c_mirror, d_mirror = mirror_points_line([c_mirror, d_mirror], line_ver) - lines.append([c_mirror, d_mirror]) - c_mirror, d_mirror = mirror_points_line([[xc, yc, 0.0], [xd, yd, 0.0]], line_ver) - lines.append([c_mirror, d_mirror]) - - xc_ = x0 + dx * j * (i + 1) / nx - yc_ = y0 + dy * (i + 1) - xd_ = x0 + dx * (j + 1) * (i + 1) / nx - yd_ = y0 + dy * (i + 1) - lines.append([[xc_, yc_, 0.0], [xd_, yd_, 0.0]]) - - c_mirror, d_mirror = mirror_points_line([[xc_, yc_, 0.0], [xd_, yd_, 0.0]], line_hor) - lines.append([c_mirror, d_mirror]) - c_mirror, d_mirror = mirror_points_line([c_mirror, d_mirror], line_ver) - lines.append([c_mirror, d_mirror]) - c_mirror, d_mirror = mirror_points_line([[xc_, yc_, 0.0], [xd_, yd_, 0.0]], line_ver) - lines.append([c_mirror, d_mirror]) - - pattern: Pattern = Pattern.from_lines(lines, delete_boundary_face=True) # type: ignore - return pattern - - -def create_ortho_pattern( - xy_span=[[0.0, 10.0], [0.0, 10.0]], - discretisation=[10, 10], -) -> Pattern: - """Helper to construct a FormDiagram based on a simple orthogonal discretisation. - - Parameters - ---------- - xy_span : list, optional - List with initial- and end-points of the vault, by default [[0.0, 10.0], [0.0, 10.0]] - discretisation : int, optional - Set the density of the grid in x and y directions, by default 10 - fix : str, optional - Option to select the constrained nodes: 'corners', 'all' are accepted, by default 'corners' - - Returns - ------- - :class:`~compas_tno.diagrams.FormDiagram` - The FormDiagram created. - """ - - if isinstance(discretisation, int): - discretisation = [discretisation, discretisation] - # if discretisation[0] % 2 != 0 or discretisation[1] % 2 != 0: - # msg = "Warning!: discretisation of this form diagram has to be even." - # raise ValueError(msg) - - y1 = float(xy_span[1][1]) - y0 = float(xy_span[1][0]) - x1 = float(xy_span[0][1]) - x0 = float(xy_span[0][0]) - x_span = x1 - x0 - y_span = y1 - y0 - division_x = discretisation[0] - division_y = discretisation[1] - dx = float(x_span / division_x) - dy = float(y_span / division_y) - - vertices = [] - faces = [] - - for j in range(division_y + 1): - for i in range(division_x + 1): - xi = x0 + dx * i - yi = y0 + dy * j - vertices.append([xi, yi, 0.0]) - if i < division_x and j < division_y: - p1 = j * (division_x + 1) + i - p2 = j * (division_x + 1) + i + 1 - p3 = (j + 1) * (division_x + 1) + i + 1 - p4 = (j + 1) * (division_x + 1) + i - face = [p1, p2, p3, p4, p1] - faces.append(face) - print(face) - - pattern: Pattern = Pattern.from_vertices_and_faces(vertices, faces) # type: ignore - return pattern - - -def create_parametric_pattern( - xy_span=[[0.0, 10.0], [0.0, 10.0]], - discretisation=10, - lambd=0.5, -) -> Pattern: - """Create a parametric form diagram based on the inclination lambda of the arches - - Parameters - ---------- - xy_span : [[float, float], [float, float]], optional - List with initial- and end-points of the vault, by default, by default [[0.0, 10.0], [0.0, 10.0]] - discretisation : int, optional - Set the density of the grid in x and y directions, by default 10 - lambd : float, optional - Inclination of the arches in the diagram (0.0 will result in cross and 1.0 in fan diagrams), by default 0.5 - fix : str, optional - Option to select the constrained nodes: 'corners', 'all' are accepted, by default 'corners' - - Returns - ------- - :class:`~compas_tno.diagrams.FormDiagram` - The FormDiagram created. - - Notes - --------- - Diagram implemented after `N. A. Nodargi et al., 2022 `_. - """ - if 0.0 > lambd or lambd > 1.0: - raise ValueError("Lambda should be between 0.0 and 1.0") - - x_span = xy_span[0][1] - xy_span[0][0] - y_span = xy_span[1][1] - xy_span[1][0] - - if abs(x_span - y_span) > 1e-6: - y_span = x_span = 10.0 - x0, x1 = y0, y1 = 0.0, 10.0 - else: - x0, x1 = xy_span[0][0], xy_span[0][1] - y0, y1 = xy_span[1][0], xy_span[1][1] - - xc = (x1 + x0) / 2 - yc = (y1 + y0) / 2 - - xc0 = x0 + x_span / 2 - yc0 = y0 + y_span / 2 - division_x = discretisation - division_y = discretisation - dx = float(x_span / division_x) - dy = float(y_span / division_y) - nx = int(division_x / 2) - line_hor = [[x0, yc0, 0.0], [xc0, yc0, 0.0]] - line_ver = [[xc0, y0, 0.0], [xc0, yc0, 0.0]] - - lines = [] - - for i in range(nx + 1): - j = i - - xa = xc - ya = xc - dy * j - xb = (ya - y0) * (1 - lambd) - yb = (ya - y0) * (1 - lambd) - - if distance_point_point_xy([xa, ya], [xb, yb]): - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - - append_mirrored_lines([[xa, ya, 0.0], [xb, yb, 0.0]], lines, line_hor, line_ver) - - if i == 0: - xa = x0 - ya = y0 - - if distance_point_point_xy([xa, ya], [xb, yb]): - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - - append_mirrored_lines([[xa, ya, 0.0], [xb, yb, 0.0]], lines, line_hor, line_ver) - - xa = xc - dx * j - xb = xc - dx * j - ya = yc - dy * j - yb = y0 - - if distance_point_point_xy([xa, ya], [xb, yb]): - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - - append_mirrored_lines([[xa, ya, 0.0], [xb, yb, 0.0]], lines, line_hor, line_ver) - - xa = yc - dy * j - ya = yc - xb = (xa - x0) * (1 - lambd) - yb = xb * (y1 - y0) / (x1 - x0) + y0 - - if distance_point_point_xy([xa, ya], [xb, yb]): - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - - append_mirrored_lines([[xa, ya, 0.0], [xb, yb, 0.0]], lines, line_hor, line_ver) - - xa = xc - dx * j - xb = x0 - ya = yc - dy * j - yb = yc - dy * j - - if distance_point_point_xy([xa, ya], [xb, yb]): - lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) - - append_mirrored_lines([[xa, ya, 0.0], [xb, yb, 0.0]], lines, line_hor, line_ver) - - # clean_lines = split_intersection_lines(lines) diff --git a/src/compas_rv/scene/__init__.py b/src/compas_rv/scene/__init__.py index e690dbe..71ed7e6 100644 --- a/src/compas_rv/scene/__init__.py +++ b/src/compas_rv/scene/__init__.py @@ -1,20 +1,15 @@ from compas.plugins import plugin from compas.scene.context import register -from compas_rv.datastructures import ForceDiagram -from compas_rv.datastructures import FormDiagram -from compas_rv.datastructures import Pattern -from compas_rv.datastructures import ThrustDiagram +from compas_rv.datastructures import ForceDiagram, FormDiagram, Pattern from .forceobject import RhinoForceObject from .formobject import RhinoFormObject from .patternobject import RhinoPatternObject -from .thrustobject import RhinoThrustObject @plugin(category="factories", pluggable_name="register_scene_objects", requires=["Rhino"]) def register_scene_objects_rhino(): register(Pattern, RhinoPatternObject, context="Rhino") register(FormDiagram, RhinoFormObject, context="Rhino") - register(ThrustDiagram, RhinoThrustObject, context="Rhino") register(ForceDiagram, RhinoForceObject, context="Rhino") diff --git a/src/compas_rv/scene/diagramobject.py b/src/compas_rv/scene/diagramobject.py index 2460e20..e4eec4f 100644 --- a/src/compas_rv/scene/diagramobject.py +++ b/src/compas_rv/scene/diagramobject.py @@ -122,7 +122,12 @@ def draw(self): return self.guids - def draw_vertices(self): + def draw_vertices(self, anchorcolor=None, fixedcolor=None, freecolor=None): + # enable definiting the colors to serve form and thrust diagrams + anchorcolor = anchorcolor or self.anchorcolor + fixedcolor = fixedcolor or self.fixedcolor + freecolor = freecolor or self.freecolor + if self.show_vertices is True: vertices = [] if self.show_free: @@ -135,11 +140,11 @@ def draw_vertices(self): for vertex in self.diagram.vertices(): if self.diagram.vertex_attribute(vertex, "is_support"): - self.vertexcolor[vertex] = self.anchorcolor + self.vertexcolor[vertex] = anchorcolor elif self.diagram.vertex_attribute(vertex, "is_fixed"): - self.vertexcolor[vertex] = self.fixedcolor + self.vertexcolor[vertex] = fixedcolor else: - self.vertexcolor[vertex] = self.freecolor + self.vertexcolor[vertex] = freecolor return super().draw_vertices() diff --git a/src/compas_rv/scene/forceobject.py b/src/compas_rv/scene/forceobject.py index df5baaa..1fba751 100644 --- a/src/compas_rv/scene/forceobject.py +++ b/src/compas_rv/scene/forceobject.py @@ -49,3 +49,4 @@ def forces(self): # ============================================================================= # Redraw # ============================================================================= + diff --git a/src/compas_rv/scene/formobject.py b/src/compas_rv/scene/formobject.py index 3206647..8b10b3b 100644 --- a/src/compas_rv/scene/formobject.py +++ b/src/compas_rv/scene/formobject.py @@ -1,5 +1,18 @@ +from contextlib import contextmanager + +import rhinoscriptsyntax as rs # type: ignore +import scriptcontext as sc # type: ignore + +import compas_rhino.conversions +import compas_rhino.objects from compas.colors import Color +from compas.geometry import Cylinder +from compas.geometry import Line +from compas.geometry import Sphere +from compas.geometry import Vector +from compas.scene.descriptors.color import ColorAttribute from compas.scene.descriptors.colordict import ColorDictAttribute +from compas_rui.scene import RUIMeshObject from compas_rv.datastructures import FormDiagram from compas_rv.session import RVSession @@ -10,16 +23,59 @@ class RhinoFormObject(RhinoDiagramObject): session = RVSession() diagram: FormDiagram # type: ignore + # Planar form diagram colors (existing) vertexcolor = ColorDictAttribute(default=Color.green()) edgecolor = ColorDictAttribute(default=Color.green().darkened(50)) facecolor = ColorDictAttribute(default=Color.green().lightened(25)) + # Thrust diagram colors (for 3D representation) + thrust_vertexcolor = ColorDictAttribute(default=Color.purple()) + thrust_edgecolor = ColorDictAttribute(default=Color.purple().darkened(50)) + thrust_facecolor = ColorDictAttribute(default=Color.purple().lightened(25)) + thrust_freecolor = ColorAttribute(default=Color.purple()) + thrust_anchorcolor = ColorAttribute(default=Color.red()) + thrust_fixedcolor = ColorAttribute(default=Color.cyan()) + + residualcolor = ColorAttribute(default=Color.cyan()) + reactioncolor = ColorAttribute(default=Color.green()) + loadcolor = ColorAttribute(default=Color.green().darkened(50)) + displacementcolor = ColorAttribute(default=Color.orange()) + selfweightcolor = ColorAttribute(default=Color.white()) + compressioncolor = ColorAttribute(default=Color.blue()) + tensioncolor = ColorAttribute(default=Color.red()) + ecrackcolor = ColorAttribute(default=Color.green()) + icrackcolor = ColorAttribute(default=Color.blue()) + boundscolor = ColorAttribute(default=Color.magenta()) + + form_layer = "RhinoVAULT::FormDiagram" + thrust_layer = "RhinoVAULT::ThrustDiagram" + envelope_layer = "RhinoVAULT::Envelope" + def __init__( self, vertexgroup="RhinoVAULT::FormDiagram::Vertices", edgegroup="RhinoVAULT::FormDiagram::Edges", facegroup="RhinoVAULT::FormDiagram::Faces", - layer="RhinoVAULT::FormDiagram", + layer=form_layer, + thrust_vertexgroup="RhinoVAULT::ThrustDiagram::Vertices", + thrust_edgegroup="RhinoVAULT::ThrustDiagram::Edges", + thrust_facegroup="RhinoVAULT::ThrustDiagram::Faces", + loadgroup="RhinoVAULT::ThrustDiagram::Loads", + displacementgroup="RhinoVAULT::ThrustDiagram::SupportDisplacements", + selfweightgroup="RhinoVAULT::ThrustDiagram::Selfweight", + forcegroup="RhinoVAULT::ThrustDiagram::Forces", + labelgroup="RhinoVAULT::ThrustDiagram::Labels", + reactiongroup="RhinoVAULT::ThrustDiagram::Reactions", + residualgroup="RhinoVAULT::ThrustDiagram::Residuals", + crackgroup="RhinoVAULT::Envelope::Cracks", + boundsgroup="RhinoVAULT::Envelope::Bounds", + show_thrust=False, + show_thrust_vertices=True, + show_thrust_edges=True, + show_thrust_faces=True, + show_thrust_supports=True, + show_thrust_fixed=True, + show_thrust_free=False, **kwargs, ): super().__init__( @@ -30,24 +86,669 @@ def __init__( **kwargs, ) + # Store thrust diagram layer groups + self.thrust_vertexgroup = thrust_vertexgroup + self.thrust_edgegroup = thrust_edgegroup + self.thrust_facegroup = thrust_facegroup + self.loadgroup = loadgroup + self.displacementgroup = displacementgroup + self.selfweightgroup = selfweightgroup + self.forcegroup = forcegroup + self.labelgroup = labelgroup + self.reactiongroup = reactiongroup + self.residualgroup = residualgroup + self.crackgroup = crackgroup + self.boundsgroup = boundsgroup + + self.show_supports = True + self.show_fixed = True + self.show_free = False + + self.show_thrust = show_thrust + self.show_thrust_vertices = show_thrust_vertices + self.show_thrust_edges = show_thrust_edges + self.show_thrust_faces = show_thrust_faces + self.show_thrust_supports = show_thrust_supports + self.show_thrust_fixed = show_thrust_fixed + self.show_thrust_free = show_thrust_free + + self._selection_thrust_vertices = None + self._selection_thrust_edges = None + self._selection_active = False + # ============================================================================= # Properties # ============================================================================= + @property + def settings(self): + settings = super().settings + settings["show_thrust"] = self.show_thrust + settings["show_thrust_vertices"] = self.show_thrust_vertices + settings["show_thrust_edges"] = self.show_thrust_edges + settings["show_thrust_faces"] = self.show_thrust_faces + settings["show_thrust_supports"] = self.show_thrust_supports + settings["show_thrust_fixed"] = self.show_thrust_fixed + settings["show_thrust_free"] = self.show_thrust_free + return settings + + # ============================================================================= + # Envelope helpers + # ============================================================================= + + def envelope(self): + return self.session.find_envelope(warn=False) + + def vertex_bound(self, vertex): + ub = self.diagram.vertex_attribute(vertex, "ub") + lb = self.diagram.vertex_attribute(vertex, "lb") + if ub is None or lb is None: + return + point = self.diagram.vertex_point(vertex) + a = point.copy() + a.z = ub + b = point.copy() + b.z = lb + return Line(a, b) + + def vertex_is_on_upper_bound(self, vertex, tol=1e-6): + ub = self.diagram.vertex_attribute(vertex, "ub") + if ub is None: + return False + point = self.diagram.vertex_point(vertex) + return abs(point.z - ub) < tol + + def vertex_is_on_lower_bound(self, vertex, tol=1e-6): + lb = self.diagram.vertex_attribute(vertex, "lb") + if lb is None: + return False + point = self.diagram.vertex_point(vertex) + return abs(point.z - lb) < tol + + def vertex_bound_name(self, vertex): + return f"{self.diagram.name}.vertex.{vertex}.bound" + + def vertex_crack_name(self, vertex): + return f"{self.diagram.name}.vertex.{vertex}.crack" + def edges(self, **kwargs): return self.diagram.edges_where(_is_edge=True) def faces(self, **kwargs): return self.diagram.faces_where(_is_loaded=True) + # ============================================================================= + # Select + # ============================================================================= + + @contextmanager + def _selection_context(self, representation, vertices=False, edges=False, faces=False): + state = { + "show_vertices": self.show_vertices, + "show_edges": self.show_edges, + "show_faces": self.show_faces, + "show_thrust": self.show_thrust, + "selection_thrust_vertices": self._selection_thrust_vertices, + "selection_thrust_edges": self._selection_thrust_edges, + "selection_active": self._selection_active, + } + + try: + self._selection_active = True + if representation == "form": + self.show_vertices = vertices + self.show_edges = edges + self.show_faces = faces + self.show_thrust = False + elif representation == "thrust": + self.show_vertices = False + self.show_edges = False + self.show_faces = False + self.show_thrust = True + self._selection_thrust_vertices = vertices + self._selection_thrust_edges = edges + else: + raise ValueError("Unknown diagram representation: {}".format(representation)) + + self.redraw() + yield + finally: + self.show_vertices = state["show_vertices"] + self.show_edges = state["show_edges"] + self.show_faces = state["show_faces"] + self.show_thrust = state["show_thrust"] + self._selection_thrust_vertices = state["selection_thrust_vertices"] + self._selection_thrust_edges = state["selection_thrust_edges"] + self._selection_active = state["selection_active"] + rs.UnselectAllObjects() + self.redraw() + + def select_form_vertices(self, vertices=None, message="Select Form Vertices", use_edges=True): + vertices = list(self.diagram.vertices()) if vertices is None else list(vertices) + edges = list(self.edges()) if use_edges else False + allowed = set(vertices) + + with self._selection_context("form", vertices=vertices, edges=edges): + selected = super().select_vertices(message=message, use_edges=use_edges) + + if selected is None: + return + return list(dict.fromkeys(vertex for vertex in selected if vertex in allowed)) + + def select_thrust_vertices(self, vertices=None, message="Select Thrust Vertices", use_edges=True): + vertices = list(self.diagram.vertices()) if vertices is None else list(vertices) + edges = list(self.edges()) if use_edges else False + allowed = set(vertices) + + with self._selection_context("thrust", vertices=vertices, edges=edges): + selected = super().select_vertices(message=message, use_edges=use_edges) + + if selected is None: + return + return list(dict.fromkeys(vertex for vertex in selected if vertex in allowed)) + + def select_form_edges(self, edges=None, message="Select Form Edges"): + edges = list(self.edges()) if edges is None else list(edges) + allowed = set(edges) + + with self._selection_context("form", edges=edges): + selected = super().select_edges(message=message) + + if selected is None: + return + return list(dict.fromkeys(edge for edge in selected if edge in allowed)) + + def select_thrust_edges(self, edges=None, message="Select Thrust Edges"): + edges = list(self.edges()) if edges is None else list(edges) + allowed = set(edges) + + with self._selection_context("thrust", edges=edges): + selected = super().select_edges(message=message) + + if selected is None: + return + return list(dict.fromkeys(edge for edge in selected if edge in allowed)) + + def select_form_faces(self, faces=None): + faces = list(self.faces()) if faces is None else list(faces) + allowed = set(faces) + + with self._selection_context("form", faces=faces): + selected = super().select_faces_manual() + + if selected is None: + return + return list(dict.fromkeys(face for face in selected if face in allowed)) + + def select_edges_loop(self): + guids = compas_rhino.objects.select_lines(message="Select Loop Edges") + edges = [] + for guid in guids or []: + edge = self._guid_edge.get(guid) + if edge is not None: + edges += list(self.diagram.edge_loop(edge)) + return edges + + def select_edges_strip(self): + guids = compas_rhino.objects.select_lines(message="Select Strip Edges") + edges = [] + for guid in guids or []: + edge = self._guid_edge.get(guid) + if edge is not None: + edges += list(self.diagram.edge_strip(edge)) + return edges + # ============================================================================= # Clear # ============================================================================= + # ============================================================================= + # Draw Planar + # ============================================================================= + + def draw_formdiagram(self): + """Draw the form diagram in planar mode (z=0 for all vertices).""" + self.layer = self.form_layer + # Store original z coordinates + original_z = {} + for vertex in self.diagram.vertices(): + original_z[vertex] = self.diagram.vertex_attribute(vertex, "z") + self.diagram.vertex_attribute(vertex, "z", 0) + + # Use existing planar colors + for vertex in self.diagram.vertices(): + if self.diagram.vertex_attribute(vertex, "is_support"): + self.vertexcolor[vertex] = self.anchorcolor + elif self.diagram.vertex_attribute(vertex, "is_fixed"): + self.vertexcolor[vertex] = self.fixedcolor + else: + self.vertexcolor[vertex] = self.freecolor + + # Draw using parent class method + guids = super().draw() + + # Restore original z coordinates + for vertex, z in original_z.items(): + self.diagram.vertex_attribute(vertex, "z", z) + + return guids + + # ============================================================================= + # Draw Thrust Diagram + # ============================================================================= + + def compute_pipe_colors(self, tol=1e-3) -> None: + edges = list(self.edges()) + forces = [self.diagram.edge_attribute(edge, "_f") for edge in edges] + if not forces or any(force is None for force in forces): + return + magnitudes = [abs(f) for f in forces] + fmin = min(magnitudes) + fmax = max(magnitudes) + + if fmax - fmin < tol: + # the size of the range is already checked here + # no need to do this again in the loop + return + + colors = [] + for force, magnitude in zip(forces, magnitudes): + # this will need to be updated when we include tension edges + colors.append(Color.from_i((magnitude - fmin) / (fmax - fmin))) + + return dict(zip(edges, colors)) + + def draw_thrustdiagram(self): + self.layer = self.thrust_layer + if self.session.settings.drawing.show_thrust_faces: + self.draw_thrust_faces() + self.draw_thrust_vertices() + self.draw_thrust_edges() + + if self._selection_active: + return self.guids + + if self.session.settings.drawing.show_reactions: + self.draw_thrust_reactions() + if self.session.settings.drawing.show_loads: + self.draw_thrust_loads() + if self.session.settings.drawing.show_support_displacements: + self.draw_support_displacements() + if self.session.settings.drawing.show_selfweight: + self.draw_thrust_selfweight() + if self.session.settings.drawing.show_pipes: + self.draw_thrust_pipes() + if self.session.settings.drawing.show_force_labels: + self.draw_thrust_force_labels() + if self.envelope() and self.session.settings.envelope.show_bounds: + self.draw_bounds() + if self.envelope() and self.session.settings.envelope.show_cracks: + self.draw_cracks() + + return self.guids + + def draw_thrust_vertices(self): + settings = self.session.settings.drawing + if self._selection_thrust_vertices is not None: + vertices = self._selection_thrust_vertices + else: + vertices = [] + if settings.show_thrust_vertices: + if settings.show_thrust_free: + vertices += list(self.diagram.vertices_where(is_support=False, is_fixed=False)) + if settings.show_thrust_fixed: + vertices += list(self.diagram.vertices_where(is_fixed=True)) + if settings.show_thrust_supports: + vertices += list(self.diagram.vertices_where(is_support=True)) + + show_vertices = self.show_vertices + self.show_vertices = vertices + try: + guids = super().draw_vertices( + anchorcolor=self.thrust_anchorcolor, + fixedcolor=self.thrust_fixedcolor, + freecolor=self.thrust_freecolor, + ) + finally: + self.show_vertices = show_vertices + + if guids: + if self.thrust_vertexgroup: + self.add_to_group(self.thrust_vertexgroup, guids) + elif self.group: + self.add_to_group(self.group, guids) + + self._guids += guids + return guids + + def draw_thrust_edges(self): + if self._selection_thrust_edges is not None: + edges = self._selection_thrust_edges + else: + edges = [] + if self.session.settings.drawing.show_thrust_edges: + edges = list(self.diagram.edges_where(_is_edge=True)) + + if edges: + for edge in edges: + self.edgecolor[edge] = self.thrust_edgecolor + + show_edges = self.show_edges + self.show_edges = edges + try: + guids = RUIMeshObject.draw_edges(self) + finally: + self.show_edges = show_edges + + if guids: + if self.thrust_edgegroup: + self.add_to_group(self.thrust_edgegroup, guids) + elif self.group: + self.add_to_group(self.group, guids) + + self._guids += guids + return guids + + def draw_thrust_faces(self): + faces = list(self.diagram.faces_where(_is_loaded=True)) + for face in faces: + self.facecolor[face] = self.thrust_facecolor + + show_faces = self.show_faces + self.show_faces = faces + try: + guids = RUIMeshObject.draw_faces(self) + finally: + self.show_faces = show_faces + + if guids: + if self.thrust_facegroup: + self.add_to_group(self.thrust_facegroup, guids) + elif self.group: + self.add_to_group(self.group, guids) + + self._guids += guids + return guids + + def draw_thrust_reactions(self): + guids = [] + + scale = self.session.settings.drawing.scale_reactions + tol = self.session.settings.drawing.tol_vectors + + for vertex in self.diagram.vertices_where(is_support=True): + residual = Vector(*self.diagram.vertex_attributes(vertex, ["_rx", "_ry", "_rz"])) + vector = residual * scale + + if vector.length > tol: + name = "{}.vertex.{}.reaction".format(self.diagram.name, vertex) + attr = self.compile_attributes(name=name, color=Color.green(), arrow="start") + point = self.diagram.vertex_point(vertex) + line = Line.from_point_and_vector(point, vector) + guid = sc.doc.Objects.AddLine(compas_rhino.conversions.line_to_rhino(line), attr) + guids.append(guid) + + if self.session.settings.drawing.show_reaction_labels and residual.length >= self.session.settings.drawing.tol_labels: + text = "{0:.1f}".format(residual.length) + attr = self.compile_attributes(name=name + ".label", color=self.reactioncolor) + guid = sc.doc.Objects.AddTextDot(text, compas_rhino.conversions.point_to_rhino(line.midpoint), attr) + guids.append(guid) + + if guids: + if self.reactiongroup: + self.add_to_group(self.reactiongroup, guids) + elif self.group: + self.add_to_group(self.group, guids) + + self._guids += guids + return guids + + def draw_thrust_loads(self): + guids = [] + + scale = self.session.settings.drawing.scale_loads + color = self.loadcolor + tol = self.session.settings.drawing.tol_vectors + + for vertex in self.diagram.vertices_where(is_support=False): + load = self.diagram.vertex_attributes(vertex, ["px", "py", "pz"]) + pzext = self.diagram.vertex_attribute(vertex, "pzext") + if pzext is not None: + load[2] += pzext + + if load is not None: + vector = Vector(*load) * scale + if vector.length > tol: + name = "{}.vertex.{}.load".format(self.diagram.name, vertex) + attr = self.compile_attributes(name=name, color=color, arrow="start") + point = self.diagram.vertex_point(vertex) + line = Line.from_point_and_vector(point, vector) + guid = sc.doc.Objects.AddLine(compas_rhino.conversions.line_to_rhino(line), attr) + guids.append(guid) + + if guids: + if self.loadgroup: + self.add_to_group(self.loadgroup, guids) + elif self.group: + self.add_to_group(self.group, guids) + + self._guids += guids + return guids + + def draw_support_displacements(self): + guids = [] + + scale = self.session.settings.drawing.scale_support_displacements + tol = self.session.settings.drawing.tol_vectors + + for vertex in self.diagram.vertices_where(is_support=True): + displacement = self.diagram.vertex_attributes(vertex, ["ux", "uy", "uz"]) + if displacement is None: + continue + + vector = Vector(*displacement) * scale + if vector.length <= tol: + continue + + name = "{}.vertex.{}.supportdisplacement".format(self.diagram.name, vertex) + attr = self.compile_attributes(name=name, color=self.displacementcolor, arrow="end") + point = self.diagram.vertex_point(vertex) + line = Line.from_point_and_vector(point, vector) + guid = sc.doc.Objects.AddLine(compas_rhino.conversions.line_to_rhino(line), attr) + guids.append(guid) + + if guids: + if self.displacementgroup: + self.add_to_group(self.displacementgroup, guids) + elif self.group: + self.add_to_group(self.group, guids) + + self._guids += guids + return guids + + def draw_thrust_selfweight(self): + guids = [] + + scale = self.session.settings.drawing.scale_selfweight + color = self.selfweightcolor + tol = self.session.settings.drawing.tol_vectors + + for vertex in self.diagram.vertices_where(is_support=False): + thickness = self.diagram.vertex_attribute(vertex, "t") + + if thickness: + area = self.diagram.vertex_area(vertex) + weight = area * thickness + point = self.diagram.vertex_point(vertex) + vector = Vector(0, 0, -weight * scale) + if vector.length > tol: + line = Line.from_point_and_vector(point, vector) + name = "{}.vertex.{}.selfweight".format(self.diagram.name, vertex) + attr = self.compile_attributes(name=name, color=color, arrow="end") + guid = sc.doc.Objects.AddLine(compas_rhino.conversions.line_to_rhino(line), attr) + guids.append(guid) + + if guids: + if self.selfweightgroup: + self.add_to_group(self.selfweightgroup, guids) + elif self.group: + self.add_to_group(self.group, guids) + + self._guids += guids + return guids + + def draw_thrust_pipes(self): + guids = [] + + scale = self.session.settings.drawing.scale_pipes + tol = self.session.settings.drawing.tol_pipes + + pipe_colors = self.compute_pipe_colors() + + for edge in self.edges(): + force = self.diagram.edge_attribute(edge, "_f") + + if force: + line = self.diagram.edge_line(edge) + radius = abs(force) * scale + + color = self.compressioncolor + if self.session.settings.drawing.show_forces and pipe_colors: + color = pipe_colors[edge] + + if radius > tol: + pipe = Cylinder.from_line_and_radius(line, radius) + name = "{}.edge.{}.force".format(self.diagram.name, edge) + attr = self.compile_attributes(name=name, color=color) + guid = sc.doc.Objects.AddBrep(compas_rhino.conversions.cylinder_to_rhino_brep(pipe), attr) + guids.append(guid) + + if guids: + if self.forcegroup: + self.add_to_group(self.forcegroup, guids) + elif self.group: + self.add_to_group(self.group, guids) + + self._guids += guids + return guids + + def draw_thrust_force_labels(self): + guids = [] + + for edge in self.diagram.edges_where(_is_edge=True): + q = self.diagram.edge_attribute(edge, "q") + if q is None: + continue + + force = q * self.diagram.edge_length(edge) + if abs(force) < self.session.settings.drawing.tol_labels: + continue + + name = "{}.edge.{}.force.label".format(self.diagram.name, edge) + attr = self.compile_attributes(name=name, color=self.compressioncolor) + text = "{0:.1f}".format(force) + point = self.diagram.edge_midpoint(edge) + guid = sc.doc.Objects.AddTextDot(text, compas_rhino.conversions.point_to_rhino(point), attr) + guids.append(guid) + + if guids: + if self.labelgroup: + self.add_to_group(self.labelgroup, guids) + elif self.group: + self.add_to_group(self.group, guids) + + self._guids += guids + return guids + + def draw_bounds(self): + guids = [] + layer = self.layer + self.layer = self.envelope_layer + + try: + for vertex in self.diagram.vertices(): + bound = self.vertex_bound(vertex) + if bound: + name = self.vertex_bound_name(vertex) + attr = self.compile_attributes(name=name, color=self.boundscolor) + guid = sc.doc.Objects.AddLine(compas_rhino.conversions.line_to_rhino(bound), attr) + guids.append(guid) + guid = sc.doc.Objects.AddPoint(compas_rhino.conversions.point_to_rhino(bound.start), attr) + guids.append(guid) + guid = sc.doc.Objects.AddPoint(compas_rhino.conversions.point_to_rhino(bound.end), attr) + guids.append(guid) + finally: + self.layer = layer + + if guids: + if self.boundsgroup: + self.add_to_group(self.boundsgroup, guids) + elif self.group: + self.add_to_group(self.group, guids) + + self._guids += guids + return guids + + def draw_cracks(self): + guids = [] + layer = self.layer + self.layer = self.envelope_layer + + try: + for vertex in self.diagram.vertices(): + if self.vertex_is_on_lower_bound(vertex): + name = self.vertex_crack_name(vertex) + attr = self.compile_attributes(name=name, color=self.icrackcolor) + elif self.vertex_is_on_upper_bound(vertex): + name = self.vertex_crack_name(vertex) + attr = self.compile_attributes(name=name, color=self.ecrackcolor) + else: + continue + + point = self.diagram.vertex_point(vertex) + radius = self.session.settings.envelope.crack_radius + sphere = Sphere(radius, point=point) + guid = sc.doc.Objects.AddSphere(compas_rhino.conversions.sphere_to_rhino(sphere), attr) + guids.append(guid) + finally: + self.layer = layer + + if guids: + if self.crackgroup: + self.add_to_group(self.crackgroup, guids) + elif self.group: + self.add_to_group(self.group, guids) + + self._guids += guids + return guids + # ============================================================================= # Draw # ============================================================================= + def draw(self): + """Draw method shows 2D and 3D if enabled, otherwise shows 2D only.""" + self.draw_formdiagram() + if self.show_thrust: + self.draw_thrustdiagram() + return self.guids + # ============================================================================= # Redraw # ============================================================================= + + def redraw(self): + rs.EnableRedraw(False) + self.clear() + self.draw() + rs.EnableRedraw(True) + rs.Redraw() + + def redraw_vertices(self): + self.redraw() + + def redraw_edges(self): + self.redraw() + + def redraw_faces(self): + self.redraw() diff --git a/src/compas_rv/scene/thrustobject.py b/src/compas_rv/scene/thrustobject.py deleted file mode 100644 index dffe17b..0000000 --- a/src/compas_rv/scene/thrustobject.py +++ /dev/null @@ -1,356 +0,0 @@ -import rhinoscriptsyntax as rs # type: ignore -import scriptcontext as sc # type: ignore - -import compas_rhino.conversions -from compas.colors import Color -from compas.geometry import Cylinder -from compas.geometry import Line -from compas.geometry import Vector -from compas.scene.descriptors.color import ColorAttribute -from compas.scene.descriptors.colordict import ColorDictAttribute -from compas_rui.scene import RUIMeshObject -from compas_rv.datastructures import ThrustDiagram -from compas_rv.session import RVSession - - -class RhinoThrustObject(RUIMeshObject): - session = RVSession() - mesh: ThrustDiagram - - vertexcolor = ColorDictAttribute(default=Color.purple()) - edgecolor = ColorDictAttribute(default=Color.purple().darkened(50)) - facecolor = ColorDictAttribute(default=Color.purple().lightened(25)) - freecolor = ColorAttribute(default=Color.purple()) - anchorcolor = ColorAttribute(default=Color.red()) - fixedcolor = ColorAttribute(default=Color.cyan()) - residualcolor = ColorAttribute(default=Color.cyan()) - reactioncolor = ColorAttribute(default=Color.green()) - loadcolor = ColorAttribute(default=Color.green().darkened(50)) - selfweightcolor = ColorAttribute(default=Color.white()) - compressioncolor = ColorAttribute(default=Color.blue()) - tensioncolor = ColorAttribute(default=Color.red()) - - def __init__( - self, - disjoint=True, - show_supports=True, - show_fixed=True, - show_free=False, - vertexgroup="RhinoVAULT::ThrustDiagram::Vertices", - edgegroup="RhinoVAULT::ThrustDiagram::Edges", - facegroup="RhinoVAULT::ThrustDiagram::Faces", - loadgroup="RhinoVAULT::ThrustDiagram::Loads", - selfweightgroup="RhinoVAULT::ThrustDiagram::Selfweight", - forcegroup="RhinoVAULT::ThrustDiagram::Forces", - reactiongroup="RhinoVAULT::ThrustDiagram::Reactions", - residualgroup="RhinoVAULT::ThrustDiagram::Residuals", - **kwargs, - ): - super().__init__( - disjoint=disjoint, - vertexgroup=vertexgroup, - edgegroup=edgegroup, - facegroup=facegroup, - **kwargs, - ) - - self.show_faces = True - self.show_edges = False - self.show_supports = show_supports - self.show_fixed = show_fixed - self.show_free = show_free - self.loadgroup = loadgroup - self.selfweightgroup = selfweightgroup - self.forcegroup = forcegroup - self.reactiongroup = reactiongroup - self.residualgroup = residualgroup - - @property - def settings(self): - settings = super().settings - settings["show_supports"] = self.show_supports - settings["show_fixed"] = self.show_fixed - settings["show_free"] = self.show_free - return settings - - @property - def diagram(self) -> ThrustDiagram: - return self.mesh - - @diagram.setter - def diagram(self, diagram: ThrustDiagram) -> None: - self.mesh = diagram - - def compute_pipe_colors(self, tol=1e-3) -> None: - edges = list(self.diagram.edges()) - forces = [self.diagram.edge_attribute(edge, "_f") for edge in edges] - magnitudes = [abs(f) for f in forces] - fmin = min(magnitudes) - fmax = max(magnitudes) - - if fmax - fmin < tol: - # the size of the range is already checked here - # no need to do this again in the loop - return - - colors = [] - for force, magnitude in zip(forces, magnitudes): - # this will need to be updated when we include tension edges - colors.append(Color.from_i((magnitude - fmin) / (fmax - fmin))) - - return dict(zip(edges, colors)) - - # ============================================================================= - # Clear - # ============================================================================= - - # ============================================================================= - # Draw - # ============================================================================= - - def draw(self): - faces = [] - if self.show_faces: - faces += list(self.diagram.faces_where(_is_loaded=True)) - if faces: - self.show_faces = faces - - for vertex in self.diagram.vertices(): - if self.diagram.vertex_attribute(vertex, "is_support"): - self.vertexcolor[vertex] = self.anchorcolor - elif self.diagram.vertex_attribute(vertex, "is_fixed"): - self.vertexcolor[vertex] = self.fixedcolor - else: - self.vertexcolor[vertex] = self.freecolor - - super().draw() - - if self.session.settings.drawing.show_reactions: - self.draw_reactions() - if self.session.settings.drawing.show_loads: - self.draw_loads() - if self.session.settings.drawing.show_selfweight: - self.draw_selfweight() - if self.session.settings.drawing.show_pipes: - self.draw_pipes() - - return self.guids - - def draw_vertices(self): - if self.show_vertices is True: - vertices = [] - if self.show_free: - vertices += list(self.diagram.vertices_where(is_support=False, is_fixed=False)) - if self.show_fixed: - vertices += list(self.diagram.vertices_where(is_fixed=True)) - if self.show_supports: - vertices += list(self.diagram.vertices_where(is_support=True)) - self.show_vertices = vertices - - for vertex in self.diagram.vertices(): - if self.diagram.vertex_attribute(vertex, "is_support"): - self.vertexcolor[vertex] = self.anchorcolor - elif self.diagram.vertex_attribute(vertex, "is_fixed"): - self.vertexcolor[vertex] = self.fixedcolor - else: - self.vertexcolor[vertex] = self.freecolor - - return super().draw_vertices() - - def draw_edges(self): - if self.show_edges is True: - edges = list(self.diagram.edges_where(_is_edge=True)) - if edges: - self.show_edges = edges - - return super().draw_edges() - - def draw_faces(self): - faces = [] - if self.show_faces: - faces += list(self.diagram.faces_where(_is_loaded=True)) - if faces: - self.show_faces = faces - - return super().draw_faces() - - def draw_loads(self): - guids = [] - - scale = self.session.settings.drawing.scale_loads - color = self.loadcolor - tol = self.session.settings.drawing.tol_vectors - - for vertex in self.diagram.vertices_where(is_support=False): - load = self.diagram.vertex_attributes(vertex, ["px", "py", "pz"]) - - if load is not None: - vector = Vector(*load) * scale - if vector.length > tol: - name = "{}.vertex.{}.load".format(self.diagram.name, vertex) - attr = self.compile_attributes(name=name, color=color, arrow="start") - point = self.diagram.vertex_point(vertex) - line = Line.from_point_and_vector(point, vector) - guid = sc.doc.Objects.AddLine(compas_rhino.conversions.line_to_rhino(line), attr) - guids.append(guid) - - if guids: - if self.loadgroup: - self.add_to_group(self.loadgroup, guids) - elif self.group: - self.add_to_group(self.group, guids) - - self._guids += guids - return guids - - def draw_selfweight(self): - guids = [] - - scale = self.session.settings.drawing.scale_selfweight - color = self.selfweightcolor - tol = self.session.settings.drawing.tol_vectors - - for vertex in self.diagram.vertices_where(is_support=False): - thickness = self.diagram.vertex_attribute(vertex, "t") - - if thickness: - area = self.diagram.vertex_area(vertex) - weight = area * thickness - point = self.diagram.vertex_point(vertex) - vector = Vector(0, 0, -weight * scale) - if vector.length > tol: - line = Line.from_point_and_vector(point, vector) - name = "{}.vertex.{}.selfweight".format(self.diagram.name, vertex) - attr = self.compile_attributes(name=name, color=color, arrow="end") - guid = sc.doc.Objects.AddLine(compas_rhino.conversions.line_to_rhino(line), attr) - guids.append(guid) - - if guids: - if self.selfweightgroup: - self.add_to_group(self.selfweightgroup, guids) - elif self.group: - self.add_to_group(self.group, guids) - - self._guids += guids - return guids - - def draw_pipes(self): - guids = [] - - scale = self.session.settings.drawing.scale_pipes - tol = self.session.settings.drawing.tol_pipes - - pipe_colors = self.compute_pipe_colors() - - for edge in self.diagram.edges(): - force = self.diagram.edge_attribute(edge, "_f") - - if force != 0: - line = self.diagram.edge_line(edge) - radius = abs(force) * scale - - color = self.compressioncolor - if self.session.settings.drawing.show_forces: - color = pipe_colors[edge] - - if radius > tol: - pipe = Cylinder.from_line_and_radius(line, radius) - name = "{}.edge.{}.force".format(self.diagram.name, edge) - attr = self.compile_attributes(name=name, color=color) - guid = sc.doc.Objects.AddBrep(compas_rhino.conversions.cylinder_to_rhino_brep(pipe), attr) - guids.append(guid) - - if guids: - if self.forcegroup: - self.add_to_group(self.forcegroup, guids) - elif self.group: - self.add_to_group(self.group, guids) - - self._guids += guids - return guids - - def draw_reactions(self): - guids = [] - - scale = self.session.settings.drawing.scale_reactions - tol = self.session.settings.drawing.tol_vectors - - for vertex in self.diagram.vertices_where(is_support=True): - residual = Vector(*self.diagram.vertex_attributes(vertex, ["_rx", "_ry", "_rz"])) - vector = residual * scale - - if vector.length > tol: - name = "{}.vertex.{}.reaction".format(self.diagram.name, vertex) - attr = self.compile_attributes(name=name, color=self.reactioncolor, arrow="start") - point = self.diagram.vertex_point(vertex) - line = Line.from_point_and_vector(point, vector) - guid = sc.doc.Objects.AddLine(compas_rhino.conversions.line_to_rhino(line), attr) - guids.append(guid) - - if guids: - if self.reactiongroup: - self.add_to_group(self.reactiongroup, guids) - elif self.group: - self.add_to_group(self.group, guids) - - self._guids += guids - return guids - - def draw_residuals(self): - guids = [] - - scale = self.session.settings.drawing.scale_residuals - tol = self.session.settings.drawing.tol_vectors - - for vertex in self.diagram.vertices_where(is_support=False): - residual = Vector(*self.diagram.vertex_attributes(vertex, ["_rx", "_ry", "_rz"])) - - vector = residual * scale - if vector.length > tol: - name = "{}.vertex.{}.residual".format(self.diagram.name, vertex) - attr = self.compile_attributes(name=name, color=self.residualcolor, arrow="end") - point = self.diagram.vertex_point(vertex) - line = Line.from_point_and_vector(point, vector) - guid = sc.doc.Objects.AddLine(compas_rhino.conversions.line_to_rhino(line), attr) - guids.append(guid) - - if guids: - if self.residualgroup: - self.add_to_group(self.residualgroup, guids) - elif self.group: - self.add_to_group(self.group, guids) - - self._guids += guids - return guids - - # ============================================================================= - # Redraw - # ============================================================================= - - def redraw_vertices(self): - rs.EnableRedraw(False) - self.clear_vertices() - self.draw_vertices() - rs.EnableRedraw(True) - rs.Redraw() - - def redraw_edges(self): - rs.EnableRedraw(False) - self.clear_edges() - self.draw_edges() - rs.EnableRedraw(True) - rs.Redraw() - - def redraw_faces(self): - rs.EnableRedraw(False) - self.clear_faces() - self.draw_faces() - rs.EnableRedraw(True) - rs.Redraw() - - def redraw(self): - rs.EnableRedraw(False) - self.clear() - self.draw() - rs.EnableRedraw(True) - rs.Redraw() diff --git a/src/compas_rv/session.py b/src/compas_rv/session.py index 91a964d..dfc6880 100644 --- a/src/compas_rv/session.py +++ b/src/compas_rv/session.py @@ -14,6 +14,14 @@ def find_all_by_itemtype(scene: Scene, itemtype) -> list[RhinoSceneObject]: return sceneobjects +def find_all_by_items(scene: Scene, items) -> list[RhinoSceneObject]: + sceneobjects = [] + for obj in scene.objects: + if any(obj.item is item for item in items): + sceneobjects.append(obj) + return sceneobjects + + class RVSession(Session): settings: RVSettings # type: ignore @@ -32,6 +40,7 @@ def clear(self, clear_scene=True, clear_context=True): if hasattr(sceneobject, "clear_conduits"): sceneobject.clear_conduits() # type: ignore self.scene.clear(clear_scene=clear_scene, clear_context=clear_context) + self.data.clear() def clear_conduits(self): for sceneobject in self.scene.objects: @@ -68,15 +77,12 @@ def find_forcediagram(self, warn=True): if warn: rs.MessageBox("There is no ForceDiagram.", title="Warning") - def find_thrustdiagram(self, warn=True): - from compas_rv.datastructures import ThrustDiagram - from compas_rv.scene import RhinoThrustObject - - thrust: RhinoThrustObject = self.scene.find_by_itemtype(ThrustDiagram) # type: ignore - if thrust: - return thrust + def find_envelope(self, warn=True): + envelope = self.get("envelope") + if envelope: + return envelope if warn: - rs.MessageBox("There is no ThrustDiagram.", title="Warning") + rs.MessageBox("There is no Envelope.", title="Warning") def clear_all_patterns(self, redraw=True): from compas_rv.datastructures import Pattern @@ -91,7 +97,6 @@ def clear_all_patterns(self, redraw=True): def clear_all_diagrams(self, redraw=True): self.clear_all_formdiagrams(redraw=False) self.clear_all_forcediagrams(redraw=False) - self.clear_all_thrustdiagrams(redraw=False) if redraw: self.scene.redraw() rs.Redraw() @@ -116,12 +121,23 @@ def clear_all_forcediagrams(self, redraw=True): self.scene.redraw() rs.Redraw() - def clear_all_thrustdiagrams(self, redraw=True): - from compas_rv.datastructures import ThrustDiagram + def clear_envelope(self, redraw=True): + formobject = self.find_formdiagram(warn=False) + if formobject: + formobject.diagram.attributes["loads_from_envelope"] = False + + envelope = self.get("envelope") + if not envelope: + return - for obj in find_all_by_itemtype(self.scene, ThrustDiagram): + items = [mesh for mesh in [envelope.intrados, envelope.middle, envelope.extrados, getattr(envelope, "fill", None)] if mesh] + for obj in find_all_by_items(self.scene, items): obj.clear() self.scene.remove(obj) + + if "envelope" in self: + del self.data["envelope"] + if redraw: self.scene.redraw() rs.Redraw() diff --git a/src/compas_rv/settings.py b/src/compas_rv/settings.py index 5782bea..c2c5bfe 100644 --- a/src/compas_rv/settings.py +++ b/src/compas_rv/settings.py @@ -14,14 +14,41 @@ class TNASettings(BaseModel): vertical_zmax: float = 4.0 +class TNOSettings(BaseModel): + solver: str = "SLSQP" + max_iter: int = 500 + starting_point: str = "loadpath" + printout: bool = True + + +class EnvelopeSettings(BaseModel): + show_intrados: bool = True + show_middle: bool = False + show_extrados: bool = True + show_fill: bool = True + show_bounds: bool = False + show_cracks: bool = True + crack_radius: float = 0.05 + + class DrawingSettings(BaseModel): show_angles: bool = True show_forces: bool = False + show_thrust_vertices: bool = True + show_thrust_edges: bool = True + show_thrust_faces: bool = True + show_thrust_supports: bool = True + show_thrust_fixed: bool = True + show_thrust_free: bool = False + show_reactions: bool = True show_residuals: bool = False show_pipes: bool = False + show_force_labels: bool = False + show_reaction_labels: bool = False show_loads: bool = False + show_support_displacements: bool = True show_selfweight: bool = False show_thickness: bool = False @@ -30,10 +57,12 @@ class DrawingSettings(BaseModel): scale_residuals: float = 1.0 scale_pipes: float = 0.01 scale_loads: float = 1.0 + scale_support_displacements: float = 1.0 scale_selfweight: float = 1.0 tol_vectors: float = 1e-3 tol_pipes: float = 1e-2 + tol_labels: float = 0.1 class RVSettings(Settings): @@ -41,4 +70,6 @@ class RVSettings(Settings): autosave: bool = True tna: TNASettings = TNASettings() + tno: TNOSettings = TNOSettings() + envelope: EnvelopeSettings = EnvelopeSettings() drawing: DrawingSettings = DrawingSettings() diff --git a/src/compas_rv/solvers/scalehorizonal.py b/src/compas_rv/solvers/scalehorizonal.py index ce0b054..970155d 100644 --- a/src/compas_rv/solvers/scalehorizonal.py +++ b/src/compas_rv/solvers/scalehorizonal.py @@ -5,7 +5,7 @@ import compas_rhino.conversions from compas_fd.solvers.fd_numerical_data import FDNumericalData from compas_rv.conduits import EdgesConduit -from compas_rv.datastructures import ThrustDiagram +from compas_rv.datastructures import FormDiagram from compas_tna.equilibrium.diagrams import update_z from compas_tna.loads import LoadUpdater @@ -13,9 +13,9 @@ class InteractiveScaleHorizontal: def __init__( self, - thrust: ThrustDiagram, + form: FormDiagram, ): - self.thrust = thrust + self.form = form self.scale = 1.0 self._numdata = None @@ -42,12 +42,12 @@ def conduit_edges(self) -> EdgesConduit: @property def numdata(self) -> FDNumericalData: if self._numdata is None: - vertex_index = self.thrust.vertex_index() - vertices: list[list[float]] = self.thrust.vertices_attributes("xyz") # type: ignore - loads: list[list[float]] = [self.thrust.vertex_attributes(vertex, ["px", "py", "pz"]) or [0, 0, 0] for vertex in self.thrust.vertices()] # type: ignore - fixed = [vertex_index[vertex] for vertex in self.thrust.vertices_where(is_support=True)] - edges = list(self.thrust.edges_where(_is_edge=True)) - forcedensities: list[float] = list(self.thrust.edges_attribute(name="q", keys=edges)) # type: ignore + vertex_index = self.form.vertex_index() + vertices: list[list[float]] = self.form.vertices_attributes("xyz") # type: ignore + loads: list[list[float]] = [self.form.vertex_attributes(vertex, ["px", "py", "pz"]) or [0, 0, 0] for vertex in self.form.vertices()] # type: ignore + fixed = [vertex_index[vertex] for vertex in self.form.vertices_where(is_support=True)] + edges = list(self.form.edges_where(_is_edge=True)) + forcedensities: list[float] = list(self.form.edges_attribute(name="q", keys=edges)) # type: ignore edges = [(vertex_index[u], vertex_index[v]) for u, v in edges] self._numdata = FDNumericalData.from_params(vertices, fixed, edges, forcedensities, loads) return self._numdata @@ -56,11 +56,12 @@ def numdata(self) -> FDNumericalData: @property def loadupdater(self) -> LoadUpdater: if self._loadupdater is None: + density = 0.0 if self.form.attributes.get("loads_from_envelope") else 1.0 self._loadupdater = LoadUpdater( - self.thrust, + self.form, array(self.numdata.p, copy=True), - array(self.thrust.vertices_attribute("t"), dtype=float64).reshape((-1, 1)), # type: ignore - 1.0, + array(self.form.vertices_attribute("t"), dtype=float64).reshape((-1, 1)), # type: ignore + density, ) return self._loadupdater diff --git a/tests/test_conventions.py b/tests/test_conventions.py new file mode 100644 index 0000000..c5319e0 --- /dev/null +++ b/tests/test_conventions.py @@ -0,0 +1,44 @@ +from compas_rv.conventions import invert_formdiagram_signs + + +class FormDiagram: + def __init__(self): + self.vertex = { + 0: {"px": 1.0, "py": 2.0, "pz": 3.0, "pzext": 7.0, "_rx": 4.0, "_ry": 5.0, "_rz": 6.0}, + 1: {"px": 0.0, "py": 0.0, "pz": 7.0, "pzext": None, "_rx": 0.0, "_ry": 0.0, "_rz": 0.0}, + } + self.edge = {(0, 1): {"q": 8.0, "_f": 9.0, "_is_edge": True}} + + def vertices(self): + return iter(self.vertex) + + def edges_where(self, **conditions): + return (edge for edge, attributes in self.edge.items() if all(attributes[name] == value for name, value in conditions.items())) + + def vertex_attributes(self, vertex, names, values=None): + if values is None: + return [self.vertex[vertex][name] for name in names] + for name, value in zip(names, values): + self.vertex[vertex][name] = value + + def edge_attributes(self, edge, names, values=None): + if values is None: + return [self.edge[edge][name] for name in names] + for name, value in zip(names, values): + self.edge[edge][name] = value + + +def test_invert_formdiagram_signs_is_involutive(): + formdiagram = FormDiagram() + + invert_formdiagram_signs(formdiagram) + + assert formdiagram.vertex[0] == {"px": -1.0, "py": -2.0, "pz": -3.0, "pzext": -7.0, "_rx": -4.0, "_ry": -5.0, "_rz": -6.0} + assert formdiagram.vertex[1] == {"px": -0.0, "py": -0.0, "pz": -7.0, "pzext": None, "_rx": -0.0, "_ry": -0.0, "_rz": -0.0} + assert formdiagram.edge[(0, 1)] == {"q": -8.0, "_f": -9.0, "_is_edge": True} + + invert_formdiagram_signs(formdiagram) + + assert formdiagram.vertex[0] == {"px": 1.0, "py": 2.0, "pz": 3.0, "pzext": 7.0, "_rx": 4.0, "_ry": 5.0, "_rz": 6.0} + assert formdiagram.vertex[1] == {"px": 0.0, "py": 0.0, "pz": 7.0, "pzext": None, "_rx": 0.0, "_ry": 0.0, "_rz": 0.0} + assert formdiagram.edge[(0, 1)] == {"q": 8.0, "_f": 9.0, "_is_edge": True} diff --git a/tests/test_single_formdiagram.py b/tests/test_single_formdiagram.py new file mode 100644 index 0000000..6ece753 --- /dev/null +++ b/tests/test_single_formdiagram.py @@ -0,0 +1,37 @@ +import ast +from pathlib import Path + + +HERE = Path(__file__).parent.parent + + +def attribute_names(filepath): + tree = ast.parse(filepath.read_text()) + return {node.attr for node in ast.walk(tree) if isinstance(node, ast.Attribute)} + + +def test_commands_do_not_use_deleted_thrustdiagram_lookup(): + for filepath in (HERE / "commands").glob("RV_*.py"): + assert "find_thrustdiagram" not in attribute_names(filepath), filepath.name + + +def test_commands_do_not_use_obsolete_thrust_visibility_attributes(): + obsolete = { + "show_vertices_3d", + "show_edges_3d", + "show_faces_3d", + "show_supports_3d", + "show_fixed_3d", + "show_free_3d", + } + + for filepath in (HERE / "commands").glob("RV_*.py"): + assert not obsolete.intersection(attribute_names(filepath)), filepath.name + + +def test_block_export_uses_the_formdiagram_as_thrust_geometry(): + source = (HERE / "commands" / "RV_dem_blocks.py").read_text() + + assert "session.find_formdiagram()" in source + assert "form.diagram.copy()" in source + assert "faces_where(_is_loaded=False)" in source