Skip to content

Commit 3decb36

Browse files
authored
Merge pull request #20 from ricardoavelino/thrustdiagram
Beautifully Integrate TNO in RV
2 parents 6764c66 + 5259dfe commit 3decb36

43 files changed

Lines changed: 2646 additions & 1644 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
* Added envelope creation from parametric vaults, middle meshes, and bound meshes, with dedicated Rhino scene layers and drawing controls.
13+
* Added TNO analysis for minimum and maximum thrust, minimum thickness, best fit, maximum applied load, and support displacement objectives.
14+
* Added objective-specific optimisation constraints and variables, with SLSQP and IPOPT solver selection.
15+
* Added assessment load workflows for envelope self-weight, external point loads, fill weight, and clearing all loads.
16+
* Added 3D thrust-network drawing, editing, and explicit form/thrust vertex and edge selection on the `FormDiagram` scene object.
17+
* Added envelope bounds and crack visualisation, force and reaction labels, load vectors, support displacement vectors, and configurable thrust faces, edges, vertices, and pipes.
18+
* Added outward, inward, downward, and manual support displacement presets.
19+
* Added direct template-pattern creation through `compas_tna`, including pattern-specific discretisation options.
20+
* Added toolbar commands and icons for envelope creation, load assignment, TNO analysis, block export, thrust inspection, and session export.
21+
1222
### Changed
1323

24+
* 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.
25+
* Changed TNA vertical equilibrium, thrust editing, diagram export, and DEM block export to operate directly on the `FormDiagram`.
26+
* Changed envelope-based analysis loads to remain fixed during equilibrium updates and added an explicit RV/TNO sign-convention bridge.
27+
* Changed successful TNO analyses to update an existing `ForceDiagram` from the optimised form forces.
28+
* Changed maximum-load analysis to collect initial loads on selected thrust-network vertices and display the optimised load vectors.
29+
* Changed support-displacement analysis to collect and display displacement vectors on selected 3D supports.
30+
* Changed thrust-load drawing to display the sum of `pz` and the optimised `pzext` contribution.
31+
* 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.
32+
* Changed the minimum `compas_tno` requirement to `0.4.0`.
33+
* Changed the Rhino 8 toolbar order and icons to expose the TNA, TNO, modification, and session workflows.
34+
1435
### Removed
1536

37+
* Removed the separate `ThrustDiagram` data structure and `RhinoThrustObject` scene object.
38+
* Removed the obsolete `RV_form_solve` command.
39+
* Removed the local pattern-template implementations superseded by the `compas_tna` factories.
40+
1641

1742
## [0.9.5] 2025-07-04
1843

‎commands/RV_dem_blocks.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515
def RunCommand():
1616
session = RVSession()
1717

18-
thrust = session.find_thrustdiagram()
19-
if not thrust:
20-
print("There is no ThrustDiagram in the scene.")
18+
form = session.find_formdiagram()
19+
if not form:
20+
print("There is no FormDiagram in the scene.")
2121
return
2222

2323
option = rs.GetString(message="DEM Blocks From", strings=["Dual", "MeshPattern"])
2424

25-
mesh: Mesh = thrust.diagram.copy()
25+
mesh: Mesh = form.diagram.copy()
2626
for face in list(mesh.faces_where(_is_loaded=False)):
2727
mesh.delete_face(face)
2828

‎commands/RV_envelope.py‎

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
#! python3
2+
# venv: brg-csd
3+
# r: compas_rv>=0.9.5
4+
5+
import rhinoscriptsyntax as rs # type: ignore
6+
7+
import compas_rhino
8+
import compas_rhino.conversions
9+
import compas_rhino.objects
10+
from compas.datastructures import Mesh
11+
from compas_rv.session import RVSession
12+
from compas_tna.envelope import BarrelVaultEnvelope
13+
from compas_tna.envelope import CrossVaultEnvelope
14+
from compas_tna.envelope import DomeEnvelope
15+
from compas_tna.envelope import MeshEnvelope
16+
from compas_tna.envelope import PavillionVaultEnvelope
17+
from compas_tna.envelope import PointedVaultEnvelope
18+
19+
20+
ENVELOPE_LAYER = "RhinoVAULT::Envelope"
21+
22+
23+
def get_location():
24+
option = rs.GetString("Envelope location", "Origin", ["Origin", "Coordinates", "Point"])
25+
if not option:
26+
return
27+
if option == "Origin":
28+
return 0.0, 0.0
29+
if option == "Coordinates":
30+
x = rs.GetReal("X", 0.0)
31+
if x is None:
32+
return
33+
y = rs.GetReal("Y", 0.0)
34+
if y is None:
35+
return
36+
return x, y
37+
point = rs.GetPoint("Point")
38+
if not point:
39+
return
40+
return point[0], point[1]
41+
42+
43+
def get_size(default_x=10.0, default_y=10.0):
44+
x_size = rs.GetReal("X size", default_x, minimum=0.0)
45+
if x_size is None:
46+
return
47+
y_size = rs.GetReal("Y size", default_y, minimum=0.0)
48+
if y_size is None:
49+
return
50+
return x_size, y_size
51+
52+
53+
def get_thickness(default=0.5):
54+
return rs.GetReal("Thickness", default, minimum=0.0)
55+
56+
57+
def get_crossvault():
58+
point = get_location()
59+
if point is None:
60+
return
61+
size = get_size()
62+
if size is None:
63+
return
64+
thickness = get_thickness()
65+
if thickness is None:
66+
return
67+
return CrossVaultEnvelope(x_span=(point[0], point[0] + size[0]), y_span=(point[1], point[1] + size[1]), thickness=thickness)
68+
69+
70+
def get_barrelvault():
71+
point = get_location()
72+
if point is None:
73+
return
74+
span = rs.GetReal("Span", 10.0, minimum=0.0)
75+
if span is None:
76+
return
77+
depth = rs.GetReal("Depth", 10.0, minimum=0.0)
78+
if depth is None:
79+
return
80+
rise = rs.GetReal("Rise", 3.0, minimum=0.0)
81+
if rise is None:
82+
return
83+
thickness = get_thickness()
84+
if thickness is None:
85+
return
86+
return BarrelVaultEnvelope(rise=rise, span=span, x0=point[0], y_span=(point[1], point[1] + depth), thickness=thickness)
87+
88+
89+
def get_pointedvault():
90+
point = get_location()
91+
if point is None:
92+
return
93+
size = get_size()
94+
if size is None:
95+
return
96+
minimum_rise = 0.5 * max(size)
97+
message = "Rise must be greater than or equal to {0:.3f}".format(minimum_rise)
98+
rise = rs.GetReal(message, minimum_rise, minimum=minimum_rise)
99+
if rise is None:
100+
return
101+
thickness = get_thickness()
102+
if thickness is None:
103+
return
104+
return PointedVaultEnvelope(x_span=(point[0], point[0] + size[0]), y_span=(point[1], point[1] + size[1]), thickness=thickness, hc=rise)
105+
106+
107+
def get_pavilionvault():
108+
point = get_location()
109+
if point is None:
110+
return
111+
size = get_size()
112+
if size is None:
113+
return
114+
thickness = get_thickness()
115+
if thickness is None:
116+
return
117+
angle = rs.GetReal("Springing angle", 45.0, minimum=0.0, maximum=90.0)
118+
if angle is None:
119+
return
120+
return PavillionVaultEnvelope(x_span=(point[0], point[0] + size[0]), y_span=(point[1], point[1] + size[1]), thickness=thickness, spr_angle=angle)
121+
122+
123+
def get_dome():
124+
center = get_location()
125+
if center is None:
126+
return
127+
radius = rs.GetReal("Radius", 5.0, minimum=0.0)
128+
if radius is None:
129+
return
130+
thickness = get_thickness()
131+
if thickness is None:
132+
return
133+
r_oculus = rs.GetReal("Oculus radius", 0.5, minimum=0.0)
134+
if r_oculus is None or r_oculus >= radius:
135+
return
136+
return DomeEnvelope(center=center, radius=radius, thickness=thickness, n_hoops=40, n_parallels=40, r_oculus=r_oculus)
137+
138+
139+
def get_from_middle():
140+
guid = compas_rhino.objects.select_mesh("Select middle mesh")
141+
if not guid:
142+
return
143+
obj = compas_rhino.objects.find_object(guid)
144+
mesh = compas_rhino.conversions.mesh_to_compas(obj.Geometry, cls=Mesh)
145+
thickness = get_thickness()
146+
if thickness is None:
147+
return
148+
rs.HideObject(guid)
149+
return MeshEnvelope.from_middle_mesh(mesh, thickness)
150+
151+
152+
def get_from_bounds():
153+
guids = []
154+
155+
guid = compas_rhino.objects.select_mesh("Select intrados")
156+
rs.UnselectAllObjects()
157+
if not guid:
158+
return
159+
guids.append(guid)
160+
obj = compas_rhino.objects.find_object(guid)
161+
intrados = compas_rhino.conversions.mesh_to_compas(obj.Geometry, cls=Mesh)
162+
163+
guid = compas_rhino.objects.select_mesh("Select extrados")
164+
rs.UnselectAllObjects()
165+
if not guid:
166+
return
167+
guids.append(guid)
168+
obj = compas_rhino.objects.find_object(guid)
169+
extrados = compas_rhino.conversions.mesh_to_compas(obj.Geometry, cls=Mesh)
170+
171+
guid = compas_rhino.objects.select_mesh("Select middle (optional)")
172+
rs.UnselectAllObjects()
173+
if guid:
174+
guids.append(guid)
175+
obj = compas_rhino.objects.find_object(guid)
176+
middle = compas_rhino.conversions.mesh_to_compas(obj.Geometry, cls=Mesh)
177+
else:
178+
middle = None
179+
180+
guid = compas_rhino.objects.select_mesh("Select fill mesh (optional)")
181+
rs.UnselectAllObjects()
182+
if guid:
183+
guids.append(guid)
184+
obj = compas_rhino.objects.find_object(guid)
185+
fill = compas_rhino.conversions.mesh_to_compas(obj.Geometry, cls=Mesh)
186+
else:
187+
fill = None
188+
189+
rs.HideObjects(guids)
190+
191+
envelope = MeshEnvelope.from_meshes(intrados, extrados, middle)
192+
if fill:
193+
envelope.fill = fill
194+
return envelope
195+
196+
197+
LIBRARY = {
198+
"BarrelVault": get_barrelvault,
199+
"CrossVault": get_crossvault,
200+
"PointedVault": get_pointedvault,
201+
"PavilionVault": get_pavilionvault,
202+
"Dome": get_dome,
203+
}
204+
205+
206+
def RunCommand():
207+
session = RVSession()
208+
209+
option = rs.GetString("Envelope from", "FromLibrary", ["FromLibrary", "FromMiddle", "FromBounds"])
210+
if not option:
211+
return
212+
213+
if option == "FromLibrary":
214+
pattern = rs.GetString("Envelope pattern", "BarrelVault", list(LIBRARY.keys()))
215+
if not pattern:
216+
return
217+
envelope = LIBRARY[pattern]()
218+
elif option == "FromMiddle":
219+
envelope = get_from_middle()
220+
elif option == "FromBounds":
221+
envelope = get_from_bounds()
222+
else:
223+
return
224+
225+
if not envelope:
226+
return session.warn("Error creating Envelope. Try again.")
227+
228+
rho = rs.GetInteger("Density masonry (rho)", int(envelope.rho), minimum=0, maximum=200)
229+
if rho is None:
230+
return
231+
envelope.rho = rho
232+
233+
if envelope.fill:
234+
rho_fill = rs.GetInteger("Density masonry fill (rho_fill)", int(envelope.rho_fill), minimum=0, maximum=200)
235+
if rho_fill is None:
236+
return
237+
envelope.rho_fill = rho_fill
238+
239+
session.clear_envelope(redraw=False)
240+
session["envelope"] = envelope
241+
242+
settings = session.settings.envelope
243+
if envelope.intrados:
244+
session.scene.add(envelope.intrados, disjoint=True, show=settings.show_intrados, name="Intrados", layer=ENVELOPE_LAYER)
245+
if envelope.middle:
246+
session.scene.add(envelope.middle, disjoint=True, show=settings.show_middle, name="Middle", layer=ENVELOPE_LAYER)
247+
if envelope.extrados:
248+
session.scene.add(envelope.extrados, disjoint=True, show=settings.show_extrados, name="Extrados", layer=ENVELOPE_LAYER)
249+
if envelope.fill:
250+
session.scene.add(envelope.fill, disjoint=True, show=settings.show_fill, name="Fill", layer=ENVELOPE_LAYER)
251+
252+
session.scene.redraw()
253+
rs.Redraw()
254+
255+
print("Envelope successfully created.")
256+
257+
if session.settings.autosave:
258+
session.record(name="TNO Envelope")
259+
260+
261+
if __name__ == "__main__":
262+
RunCommand()

‎commands/RV_form.py‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import rhinoscriptsyntax as rs # type: ignore
66

77
from compas_rv.datastructures import FormDiagram
8-
from compas_rv.datastructures import ThrustDiagram
98
from compas_rv.session import RVSession
109

1110

@@ -40,11 +39,8 @@ def RunCommand():
4039
formdiagram.vertices_attribute(name="z", value=0)
4140
formdiagram.flip_cycles_if_normal_down()
4241

43-
thrustdiagram: ThrustDiagram = formdiagram.copy(cls=ThrustDiagram)
44-
thrustdiagram.name = "ThrustDiagram"
45-
46-
# set an initial value for zmax
47-
session.settings.tna.vertical_zmax = thrustdiagram.compute_zmax()
42+
# set an initial value for zmax using the form diagram
43+
session.settings.tna.vertical_zmax = formdiagram.compute_zmax()
4844

4945
# =============================================================================
5046
# Update scene
@@ -55,7 +51,6 @@ def RunCommand():
5551
pattern.show = False
5652

5753
session.scene.add(formdiagram, name=formdiagram.name, layer="RhinoVAULT::FormDiagram") # type: ignore
58-
session.scene.add(thrustdiagram, name=thrustdiagram.name, show=False, layer="RhinoVAULT::ThrustDiagram") # type: ignore
5954
session.scene.redraw()
6055
rs.Redraw()
6156

0 commit comments

Comments
 (0)