|
| 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