Skip to content

Commit 38b4bc1

Browse files
committed
add RV vault
1 parent 0c8a833 commit 38b4bc1

File tree

1 file changed

+327
-0
lines changed

1 file changed

+327
-0
lines changed

scripts/dem_vault_rv.py

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
#! python3
2+
# venv: brg-csd
3+
# r: compas_model
4+
5+
import pathlib
6+
7+
import compas
8+
from compas.colors import Color
9+
from compas.datastructures import Mesh
10+
from compas.geometry import KDTree
11+
from compas.geometry import Line
12+
from compas.geometry import NurbsCurve
13+
from compas.geometry import Plane
14+
from compas.geometry import bestfit_plane_numpy
15+
from compas.geometry import trimesh_remesh
16+
from compas.itertools import pairwise
17+
from compas.scene import Scene
18+
from compas_tna.diagrams import FormDiagram
19+
from compas_viewer import Viewer
20+
from compas_viewer.config import Config
21+
from scipy.interpolate import griddata
22+
23+
24+
def break_boundary(mesh: Mesh, breakpoints: list[int]) -> tuple[list[list[int]], list[int]]:
25+
boundary: list[int] = mesh.vertices_on_boundaries()[0]
26+
if boundary[0] == boundary[-1]:
27+
del boundary[-1]
28+
29+
breakpoints = sorted(breakpoints, key=lambda s: boundary.index(s))
30+
31+
start = boundary.index(breakpoints[0])
32+
boundary = boundary[start:] + boundary[:start]
33+
34+
borders = []
35+
for a, b in pairwise(breakpoints):
36+
start = boundary.index(a)
37+
end = boundary.index(b)
38+
borders.append(boundary[start : end + 1])
39+
borders.append(boundary[end:] + boundary[:1])
40+
41+
return borders, breakpoints
42+
43+
44+
# =============================================================================
45+
# Load data
46+
# =============================================================================
47+
48+
IFILE = pathlib.Path(__file__).parent.parent / "data" / "shell_final.json"
49+
50+
rv_session = compas.json_load(IFILE)
51+
rv_scene: Scene = rv_session["scene"]
52+
53+
thrustobject = rv_scene.find_by_name("ThrustDiagram")
54+
thrustdiagram: FormDiagram = thrustobject.mesh
55+
56+
# =============================================================================
57+
# Mesh
58+
#
59+
# - make a copy of the thrustdiagram
60+
# - remove the "TNA" faces cooresponding to boundary openings
61+
# - compute the average edge length for remeshing
62+
# =============================================================================
63+
64+
mesh: Mesh = thrustdiagram.copy(cls=Mesh)
65+
66+
for face in list(mesh.faces_where(_is_loaded=False)):
67+
mesh.delete_face(face)
68+
69+
length = sum(mesh.edge_length(edge) for edge in mesh.edges()) / mesh.number_of_edges()
70+
71+
# =============================================================================
72+
# Mesh: Borders
73+
# =============================================================================
74+
75+
supports = list(mesh.vertices_where(is_support=True))
76+
borders, supports = break_boundary(mesh, supports)
77+
78+
mesh.attributes["supports"] = supports
79+
mesh.attributes["borders"] = borders
80+
81+
# =============================================================================
82+
# Trimesh
83+
#
84+
# - convert a copy of the mesh to a trimesh using "quads to triangles"
85+
# - note that this doesn't work for other patterns
86+
# =============================================================================
87+
88+
trimesh: Mesh = mesh.copy()
89+
trimesh.quads_to_triangles()
90+
91+
# =============================================================================
92+
# Trimesh: Remeshing
93+
#
94+
# - remesh using CGAL
95+
# - use a percentage of the average edge length of the original mesh as target length
96+
# =============================================================================
97+
98+
M = trimesh.to_vertices_and_faces()
99+
V, F = trimesh_remesh(M, target_edge_length=0.9 * length, number_of_iterations=100)
100+
101+
trimesh = Mesh.from_vertices_and_faces(V, F)
102+
103+
# =============================================================================
104+
# Dual
105+
#
106+
# - construct a dual
107+
# - update default attributes
108+
# - flip the cycles because a dual has opposite cycles compared to the original
109+
# =============================================================================
110+
111+
dual: Mesh = trimesh.dual(include_boundary=True)
112+
113+
dual.update_default_edge_attributes(is_support=False)
114+
dual.update_default_face_attributes(number=None, batch=None)
115+
dual.update_default_vertex_attributes(thickness=0, is_corner=False, is_support=False)
116+
117+
dual.flip_cycles()
118+
119+
# =============================================================================
120+
# Dual: Reconnect corners
121+
#
122+
# - construct a KD tree for nearest neighbour search
123+
# - find the neares neighbours in the dual to the supports of the original
124+
# - snap the dual vertices to the location of the supports
125+
# - mark the corresponding vertices as "corners"
126+
# =============================================================================
127+
128+
vertices = dual.vertices_attributes("xyz")
129+
vertex_index = {vertex: index for index, vertex in enumerate(dual.vertices())}
130+
index_vertex = {index: vertex for index, vertex in enumerate(dual.vertices())}
131+
tree = KDTree(vertices)
132+
133+
for vertex in mesh.vertices_where(is_support=True):
134+
point = mesh.vertex_point(vertex)
135+
closest, nnbr, distance = tree.nearest_neighbor(point)
136+
dual_vertex = index_vertex[nnbr]
137+
if distance > 5:
138+
dual.vertex_attributes(dual_vertex, names="xyz", values=point)
139+
dual.vertex_attribute(dual_vertex, name="is_corner", value=True)
140+
141+
# =============================================================================
142+
# Dual: Collapse 2-valent boundary edges
143+
# =============================================================================
144+
145+
tofix = []
146+
147+
for vertex in dual.vertices_on_boundary():
148+
if dual.vertex_degree(vertex) > 2:
149+
continue
150+
tofix.append(vertex)
151+
152+
for vertex in tofix:
153+
nbrs = dual.vertex_neighbors(vertex)
154+
v0 = dual.edge_vector((vertex, nbrs[0]))
155+
v1 = dual.edge_vector((vertex, nbrs[1]))
156+
angle = v0.angle(v1, degrees=True)
157+
158+
if abs(angle - 180) > 30:
159+
continue
160+
161+
if dual.has_edge((vertex, nbrs[0])):
162+
is_corner = dual.vertex_attribute(nbrs[0], name="is_corner")
163+
dual.collapse_edge((vertex, nbrs[0]), t=1, allow_boundary=True)
164+
else:
165+
is_corner = dual.vertex_attribute(nbrs[1], name="is_corner")
166+
dual.collapse_edge((vertex, nbrs[1]), t=1, allow_boundary=True)
167+
168+
if is_corner:
169+
dual.vertex_attribute(vertex, name="is_corner", value=True)
170+
171+
# =============================================================================
172+
# Dual: Boundary smoothing
173+
# =============================================================================
174+
175+
corners = list(dual.vertices_where(is_corner=True))
176+
borders, corners = break_boundary(dual, corners)
177+
178+
curves: list[NurbsCurve] = []
179+
for border in borders:
180+
vertices = border[::2] if len(border) > 4 else border
181+
points = dual.vertices_points(vertices=vertices)
182+
curve: NurbsCurve = NurbsCurve.from_interpolation(points, precision=1)
183+
curves.append(curve)
184+
185+
for border, curve in zip(borders, curves):
186+
for vertex in border[1:-1]:
187+
nbrs = dual.vertex_neighbors(vertex)
188+
for nbr in nbrs:
189+
if nbr not in border:
190+
point = dual.vertex_point(nbr)
191+
closest = curve.closest_point(point)
192+
dual.vertex_attributes(vertex, "xyz", closest)
193+
break
194+
195+
# =============================================================================
196+
# Dual: Edge collapse
197+
# =============================================================================
198+
199+
tocollapse = []
200+
201+
for u, v in dual.edges_on_boundary():
202+
if dual.vertex_attribute(u, "is_corner") or dual.vertex_attribute(v, "is_corner"):
203+
continue
204+
face = dual.halfedge_face((v, u))
205+
vertices = dual.face_vertices(face)
206+
if len(vertices) == 4:
207+
vv = dual.face_vertex_ancestor(face, v)
208+
uu = dual.face_vertex_descendant(face, u)
209+
tocollapse.append((u, v))
210+
tocollapse.append((uu, vv))
211+
212+
for u, v in tocollapse:
213+
dual.collapse_edge((u, v), allow_boundary=True)
214+
215+
# =============================================================================
216+
# Dual: Borders
217+
# =============================================================================
218+
219+
corners = list(dual.vertices_where(is_corner=True))
220+
borders, corners = break_boundary(dual, corners)
221+
222+
for border in borders:
223+
if len(border) < 5:
224+
dual.vertices_attribute(name="is_support", value=True)
225+
for edge in pairwise(border):
226+
dual.edge_attribute(edge, name="is_support", value=True)
227+
228+
# =============================================================================
229+
# Blocks: Thickness interpolation griddata
230+
# =============================================================================
231+
232+
points = []
233+
values = []
234+
235+
supports_by_height = sorted(mesh.attributes["supports"], key=lambda v: mesh.vertex_attribute(v, "z"))
236+
237+
for support in supports_by_height[:4]:
238+
points.append(mesh.vertex_attributes(support, "xy"))
239+
values.append(50)
240+
241+
for support in supports_by_height[4:]:
242+
points.append(mesh.vertex_attributes(support, "xy"))
243+
values.append(30)
244+
245+
for border in mesh.attributes["borders"]:
246+
if len(border) > 4:
247+
midspan = border[len(border) // 2]
248+
points.append(mesh.vertex_attributes(midspan, "xy"))
249+
values.append(15)
250+
251+
vertices_by_height = sorted(mesh.vertices(), key=lambda v: mesh.vertex_attribute(v, "z"))
252+
253+
for vertex in vertices_by_height[-5:]:
254+
points.append(mesh.vertex_attributes(vertex, "xy"))
255+
values.append(10)
256+
257+
# =============================================================================
258+
# Blocks: Thickness interpolation sampling
259+
# =============================================================================
260+
261+
samples = dual.vertices_attributes("xy")
262+
thickness = griddata(points, values, samples)
263+
264+
for vertex, t in zip(dual.vertices(), thickness):
265+
dual.vertex_attribute(vertex, "thickness", t)
266+
267+
# =============================================================================
268+
# Blocks
269+
# =============================================================================
270+
271+
blocks = []
272+
273+
for face in dual.faces():
274+
vertices = dual.face_vertices(face)
275+
normals = [dual.vertex_normal(vertex) for vertex in vertices]
276+
thickness = dual.vertices_attribute("thickness", keys=vertices)
277+
278+
middle = dual.face_polygon(face)
279+
bottom = [point - vector * (0.5 * t) for point, vector, t in zip(middle, normals, thickness)]
280+
top = [point + vector * (0.5 * t) for point, vector, t in zip(middle, normals, thickness)]
281+
282+
plane = Plane(*bestfit_plane_numpy(top))
283+
284+
flattop = []
285+
for a, b in zip(bottom, top):
286+
b = plane.intersection_with_line(Line(a, b))
287+
flattop.append(b)
288+
289+
sides = []
290+
for (a, b), (aa, bb) in zip(pairwise(bottom + bottom[:1]), pairwise(flattop + flattop[:1])):
291+
sides.append([a, b, bb, aa])
292+
293+
polygons = [bottom[::-1]] + [flattop] + sides
294+
295+
block = Mesh.from_polygons(polygons)
296+
block.update_default_face_attributes(is_support=False, is_interface=False)
297+
298+
for index, (u, v) in enumerate(pairwise(vertices + vertices[:1])):
299+
is_support = dual.edge_attribute((u, v), name="is_support")
300+
if is_support:
301+
face = 2 + index
302+
block.face_attribute(face, "is_support", True)
303+
304+
blocks.append(block)
305+
306+
# =============================================================================
307+
# Block: Chamfering
308+
# =============================================================================
309+
310+
# =============================================================================
311+
# Visualisation
312+
# =============================================================================
313+
314+
config = Config()
315+
config.camera.target = [500, 500, 50]
316+
config.camera.position = [500, -500, 100]
317+
config.camera.near = 1
318+
config.camera.far = 10000
319+
config.renderer.gridsize = (1000, 10, 1000, 10)
320+
321+
viewer = Viewer(config=config)
322+
# viewer.scene.add(mesh, show_faces=False, show_edges=True)
323+
# viewer.scene.add(dual, facecolor={face: Color.red() for face in tocollapse})
324+
for block in blocks:
325+
viewer.scene.add(block, facecolor={face: Color.red() for face in block.faces_where(is_support=True)})
326+
# viewer.scene.add(curves, linewidth=3)
327+
viewer.show()

0 commit comments

Comments
 (0)