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