Spaces:
Runtime error
Runtime error
| import math | |
| from compas.geometry import Point, Polyline, Vector, intersection_circle_circle_xy | |
| from compas.datastructures import Mesh | |
| from compas_tna.diagrams import FormDiagram | |
| from code.vault_shared import fanvault_middle_hc, crossvault_middle_hc | |
| def _get_vector(v1, v2): | |
| return (v2['x'] - v1['x'], v2['y'] - v1['y']) | |
| def _normalize(v): | |
| L = math.sqrt(v[0]**2 + v[1]**2) | |
| if L == 0: return (0, 0) | |
| return (v[0]/L, v[1]/L) | |
| def _dot(v1, v2): | |
| return v1[0]*v2[0] + v1[1]*v2[1] | |
| def extract_spokes(diagram, target_corner_coords=(0.0, 0.0), center_coords=(5.0, 5.0)): | |
| target_corner = None | |
| for v in diagram.vertices(): | |
| x, y = diagram.vertex_attributes(v, names=['x', 'y']) | |
| if abs(x - target_corner_coords[0]) < 1e-6 and abs(y - target_corner_coords[1]) < 1e-6: | |
| target_corner = v | |
| break | |
| if target_corner is None: return [] | |
| spokes = [] | |
| neighbors = diagram.vertex_neighbors(target_corner) | |
| for n in neighbors: | |
| spoke = [target_corner, n] | |
| curr, prev = n, target_corner | |
| while True: | |
| v_curr = diagram.vertex_attributes(curr) | |
| if abs(v_curr['x'] - center_coords[0]) < 1e-6 or abs(v_curr['y'] - center_coords[1]) < 1e-6: | |
| break | |
| next_neighbors = diagram.vertex_neighbors(curr) | |
| best_n, max_dot = None, -2.0 | |
| vec_prev = _normalize(_get_vector(diagram.vertex_attributes(prev), v_curr)) | |
| for nn in next_neighbors: | |
| if nn == prev: continue | |
| vec_next = _normalize(_get_vector(v_curr, diagram.vertex_attributes(nn))) | |
| d = _dot(vec_prev, vec_next) | |
| if d > max_dot: | |
| max_dot, best_n = d, nn | |
| if best_n is None or max_dot < 0.5: break | |
| prev, curr = curr, best_n | |
| spoke.append(curr) | |
| spokes.append(spoke) | |
| return spokes | |
| def cut_polyline_at_radius(pts, center_pt, radius): | |
| new_pts = [] | |
| cut_done = False | |
| cx, cy = center_pt.x, center_pt.y | |
| for i in range(len(pts) - 1): | |
| p1, p2 = pts[i], pts[i+1] | |
| if not cut_done: | |
| d1 = math.hypot(p1.x - cx, p1.y - cy) | |
| d2 = math.hypot(p2.x - cx, p2.y - cy) | |
| if d1 <= radius and d2 >= radius: | |
| dx, dy = p2.x - p1.x, p2.y - p1.y | |
| qx, qy = p1.x - cx, p1.y - cy | |
| A = dx**2 + dy**2 | |
| B = 2 * (qx*dx + qy*dy) | |
| C = qx**2 + qy**2 - radius**2 | |
| if A > 1e-9: | |
| det = B**2 - 4*A*C | |
| if det >= 0: | |
| t1 = (-B + math.sqrt(det)) / (2*A) | |
| t2 = (-B - math.sqrt(det)) / (2*A) | |
| t = t1 if 0 <= t1 <= 1 else t2 | |
| if 0 <= t <= 1: | |
| new_pts.append(Point(p1.x + t*dx, p1.y + t*dy, p1.z + t*(p2.z - p1.z))) | |
| new_pts.append(p2) | |
| cut_done = True | |
| continue | |
| else: | |
| new_pts.append(p2) | |
| return new_pts if new_pts else pts | |
| def develop_strip_to_plane(poly1_3d, poly2_3d, start_point_on_plane, initial_unroll_vec): | |
| if len(poly1_3d.points) != len(poly2_3d.points) or len(poly1_3d.points) < 2: | |
| return None, [] | |
| N = len(poly1_3d.points) | |
| z_plane = start_point_on_plane.z | |
| flat_vertices, flat_faces = [], [] | |
| quad_distortions = [] | |
| xy_plane_normal = Vector(0, 0, 1) | |
| P0_3d, Q0_3d = poly1_3d.points[0], poly2_3d.points[0] | |
| P_curr_flat = start_point_on_plane.copy() | |
| flat_vertices.append(P_curr_flat) | |
| dist_p0q0 = P0_3d.distance_to_point(Q0_3d) | |
| unroll_dir_xy = Vector(initial_unroll_vec.x, initial_unroll_vec.y, 0) | |
| if unroll_dir_xy.length < 1e-6: unroll_dir_xy = Vector(1,0,0) | |
| else: unroll_dir_xy.unitize() | |
| rung_dir_xy = Vector(-unroll_dir_xy.y, unroll_dir_xy.x, 0) | |
| Q_curr_flat = P_curr_flat + rung_dir_xy * dist_p0q0 | |
| Q_curr_flat.z = z_plane | |
| flat_vertices.append(Q_curr_flat) | |
| P_prev_flat = None | |
| for j in range(N - 1): | |
| P_curr_3d, P_next_3d = poly1_3d.points[j], poly1_3d.points[j+1] | |
| Q_curr_3d, Q_next_3d = poly2_3d.points[j], poly2_3d.points[j+1] | |
| d_pc_pn = P_curr_3d.distance_to_point(P_next_3d) | |
| d_qc_pn = Q_curr_3d.distance_to_point(P_next_3d) | |
| intersections_P = intersection_circle_circle_xy( | |
| ((P_curr_flat, xy_plane_normal), d_pc_pn), | |
| ((Q_curr_flat, xy_plane_normal), d_qc_pn) | |
| ) | |
| if not intersections_P: return None, [] | |
| P_next_flat_chosen_xy = intersections_P[0] | |
| if len(intersections_P) > 1: | |
| fwd_dir = unroll_dir_xy if P_prev_flat is None else Vector.from_start_end(P_prev_flat, P_curr_flat).unitized() | |
| v0 = Vector(intersections_P[0][0]-P_curr_flat.x, intersections_P[0][1]-P_curr_flat.y, 0).unitized() | |
| v1 = Vector(intersections_P[1][0]-P_curr_flat.x, intersections_P[1][1]-P_curr_flat.y, 0).unitized() | |
| P_next_flat_chosen_xy = intersections_P[0] if v0.dot(fwd_dir) >= v1.dot(fwd_dir) else intersections_P[1] | |
| P_next_flat = Point(P_next_flat_chosen_xy[0], P_next_flat_chosen_xy[1], z_plane) | |
| d_qc_qn = Q_curr_3d.distance_to_point(Q_next_3d) | |
| d_pn_qn = P_next_3d.distance_to_point(Q_next_3d) | |
| intersections_Q = intersection_circle_circle_xy( | |
| ((Q_curr_flat, xy_plane_normal), d_qc_qn), | |
| ((P_next_flat, xy_plane_normal), d_pn_qn) | |
| ) | |
| if not intersections_Q: return None, [] | |
| Q_next_flat_chosen_xy = intersections_Q[0] | |
| if len(intersections_Q) > 1: | |
| rung_prev_dir = Vector.from_start_end(P_curr_flat, Q_curr_flat).unitized() | |
| r0 = Vector(intersections_Q[0][0]-P_next_flat.x, intersections_Q[0][1]-P_next_flat.y, 0).unitized() | |
| r1 = Vector(intersections_Q[1][0]-P_next_flat.x, intersections_Q[1][1]-P_next_flat.y, 0).unitized() | |
| Q_next_flat_chosen_xy = intersections_Q[0] if r0.dot(rung_prev_dir) >= r1.dot(rung_prev_dir) else intersections_Q[1] | |
| Q_next_flat = Point(Q_next_flat_chosen_xy[0], Q_next_flat_chosen_xy[1], z_plane) | |
| diag_3d_length = P_curr_3d.distance_to_point(Q_next_3d) | |
| diag_flat_length = P_curr_flat.distance_to_point(Q_next_flat) | |
| current_quad_distortion = 0.0 | |
| if diag_3d_length > 1e-9: | |
| current_quad_distortion = abs(diag_3d_length - diag_flat_length) / diag_3d_length | |
| elif diag_flat_length > 1e-9: | |
| current_quad_distortion = 10.0 | |
| quad_distortions.append(current_quad_distortion) | |
| flat_vertices.extend([P_next_flat, Q_next_flat]) | |
| idx_Pc, idx_Qc = 2*j, 2*j + 1 | |
| idx_Pn, idx_Qn = len(flat_vertices)-2, len(flat_vertices)-1 | |
| flat_faces.append([idx_Pc, idx_Pn, idx_Qn, idx_Qc]) | |
| P_prev_flat = P_curr_flat | |
| P_curr_flat = P_next_flat | |
| Q_curr_flat = Q_next_flat | |
| return Mesh.from_vertices_and_faces(flat_vertices, flat_faces), quad_distortions | |
| def get_alternating_catenaries(form_min, form_max, corner_cut_radius=0.5, center_coords=(5.0, 5.0)): | |
| if isinstance(form_min, str): | |
| form_min = FormDiagram.from_json(form_min) | |
| if isinstance(form_max, str): | |
| form_max = FormDiagram.from_json(form_max) | |
| spokes_min_ids = extract_spokes(form_min, center_coords=center_coords) | |
| spokes_max_ids = extract_spokes(form_max, center_coords=center_coords) | |
| def get_spoke_angle(spoke, diagram): | |
| c_coords = diagram.vertex_attributes(spoke[0], names=['x', 'y']) | |
| n_coords = diagram.vertex_attributes(spoke[1], names=['x', 'y']) | |
| return math.atan2(n_coords[1] - c_coords[1], n_coords[0] - c_coords[0]) | |
| spokes_min_ids.sort(key=lambda s: get_spoke_angle(s, form_min)) | |
| spokes_max_ids.sort(key=lambda s: get_spoke_angle(s, form_max)) | |
| catenaries = [] | |
| n_spokes = min(len(spokes_min_ids), len(spokes_max_ids)) | |
| for i in range(n_spokes): | |
| if i % 2 == 0: | |
| ids, diagram = spokes_max_ids[i], form_max | |
| else: | |
| ids, diagram = spokes_min_ids[i], form_min | |
| pts = [Point(*diagram.vertex_coordinates(v)) for v in ids] | |
| if corner_cut_radius > 1e-6 and len(pts) > 1: | |
| pts = cut_polyline_at_radius(pts, pts[0], corner_cut_radius) | |
| catenaries.append(Polyline(pts)) | |
| return catenaries | |
| def compute_max_safe_cut_radius(catenaries): | |
| """ | |
| Computes the maximum radius that can be applied to all catenaries | |
| while ensuring they all retain the same number of nodes. | |
| """ | |
| if not catenaries: | |
| return 0.0 | |
| # For each catenary, find the distance to the furthest point that would | |
| # still leave at least 2 points (the minimum needed for a line segment). | |
| # However, to be safe and consistent, we want to find the radius | |
| # that doesn't "jump" a node on any catenary. | |
| # The limit is the distance to the 2nd node of the "shortest" spoke | |
| min_dist_to_second_node = float('inf') | |
| for cat in catenaries: | |
| if len(cat.points) < 2: | |
| continue | |
| p0 = cat.points[0] | |
| p1 = cat.points[1] | |
| dist = math.hypot(p1.x - p0.x, p1.y - p0.y) | |
| if dist < min_dist_to_second_node: | |
| min_dist_to_second_node = dist | |
| # We return slightly less than the absolute limit to avoid precision issues | |
| return max(0.0, min_dist_to_second_node - 0.01) | |
| def generate_envelope_catenaries(config, n_spokes=10, n_points=20, corner_cut_radius=0.0): | |
| """ | |
| Generates a fixed number of catenaries directly from the vault envelope | |
| instead of using the TNA thrust diagrams. | |
| """ | |
| xy_span = config['xy_span'] | |
| thickness = config['thickness'] | |
| hc = config['max_rise'] | |
| v_type = config['vault_type'] | |
| x0, x1 = xy_span[0] | |
| y0, y1 = xy_span[1] | |
| xm, ym = (x0 + x1) / 2, (y0 + y1) / 2 | |
| # We follow the existing logic of only using the primary corner (x0, y0) | |
| xc, yc = x0, y0 | |
| dx = xm - xc | |
| dy = ym - yc | |
| # Split n_spokes between the two boundary edges of the quadrant | |
| # nx + ny = n_spokes - 1 | |
| total_len = abs(dx) + abs(dy) | |
| nx = max(1, int(round((n_spokes - 1) * abs(dx) / total_len))) | |
| ny = (n_spokes - 1) - nx | |
| # Quadrant corners | |
| corners = [ | |
| (x0, y0), (x1, y0), (x1, y1), (x0, y1) | |
| ] | |
| all_catenaries = [] | |
| global_spoke_idx = 0 | |
| for ci, (xc, yc) in enumerate(corners): | |
| # Directions towards center lines for this quadrant | |
| qdx = xm - xc | |
| qdy = ym - yc | |
| qpts = [] | |
| # Edge 1: Along the boundary where x varies (at y=ym) | |
| for i in range(nx + 1): | |
| t = i / nx | |
| qpts.append((xc + t * qdx, ym)) | |
| # Edge 2: Along the boundary where y varies (at x=xm) | |
| for i in range(1, ny + 1): | |
| t = i / ny | |
| qpts.append((xm, ym - t * qdy)) | |
| quadrant_cats = [] | |
| for si, (px, py) in enumerate(qpts): | |
| pts = [] | |
| for j in range(n_points): | |
| u = j / (n_points - 1) | |
| x = xc + u * (px - xc) | |
| y = yc + u * (py - yc) | |
| # Z from envelope | |
| if v_type == 'fan': | |
| z_mid = fanvault_middle_hc([x], [y], [x0, x1], [y0, y1], hc)[0] | |
| else: | |
| z_mid = crossvault_middle_hc([x], [y], [x0, x1], [y0, y1], hc)[0] | |
| # Alternate Z based on intrados and extrados | |
| if si % 2 == 0: | |
| z = z_mid - thickness / 2 | |
| else: | |
| z = z_mid + thickness / 2 | |
| pts.append(Point(x, y, z)) | |
| if corner_cut_radius > 1e-6: | |
| pts = cut_polyline_at_radius(pts, Point(xc, yc, pts[0].z), corner_cut_radius) | |
| quadrant_cats.append(Polyline(pts)) | |
| global_spoke_idx += 1 | |
| all_catenaries.append(quadrant_cats) | |
| return all_catenaries | |
| def generate_support_beams(config, n_spokes=10, ply_thickness=0.012): | |
| """ | |
| Generates meshes for cross-shaped support beams running along the centerlines. | |
| The top edge matches the zigzag pattern of the corrugation, | |
| offset by ply_thickness to support the underside. | |
| The bottom edge is flat at z=0. | |
| """ | |
| xy_span = config['xy_span'] | |
| thickness = config['thickness'] | |
| hc = config['max_rise'] | |
| v_type = config['vault_type'] | |
| x0, x1 = xy_span[0] | |
| y0, y1 = xy_span[1] | |
| xm, ym = (x0 + x1) / 2, (y0 + y1) / 2 | |
| # Calculate how points are distributed, same logic as generate_envelope_catenaries | |
| dx = xm - x0 | |
| dy = ym - y0 | |
| total_len = abs(dx) + abs(dy) | |
| nx = max(1, int(round((n_spokes - 1) * abs(dx) / total_len))) | |
| ny = (n_spokes - 1) - nx | |
| # --- Generate X-Beam (along y=ym, from x0 to x1) --- | |
| x_beam_verts = [] | |
| x_beam_faces = [] | |
| # First half of X-Beam: x0 to xm (from Corner 0's spoke ends) | |
| pts_x_half1 = [] | |
| for i in range(nx + 1): | |
| t = i / nx | |
| px = x0 + t * dx | |
| z_mid = fanvault_middle_hc([px], [ym], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([px], [ym], [x0, x1], [y0, y1], hc)[0] | |
| # Same alternating logic: si corresponds to the point index on this edge | |
| si = i | |
| z_top = z_mid - thickness / 2 if si % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness # Offset for plywood material thickness | |
| pts_x_half1.append((px, z_top)) | |
| # Second half of X-Beam: xm to x1 (from Corner 1's spoke ends) | |
| # Important: Ensure the zigzag aligns at xm. | |
| # Corner 1 spokes on Edge 1 go from x1 to xm. We need to go from xm to x1, or construct it matching. | |
| pts_x_half2 = [] | |
| dx_corner1 = xm - x1 # negative | |
| for i in range(1, nx + 1): | |
| t = i / nx | |
| px = x1 + (1.0 - t) * dx_corner1 # Going from xm towards x1 | |
| z_mid = fanvault_middle_hc([px], [ym], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([px], [ym], [x0, x1], [y0, y1], hc)[0] | |
| # In Corner 1, the spoke index si for points on Edge 1 is also i. | |
| # But we are iterating backwards from xm (which was si=nx). | |
| # To match exactly, the point at px corresponds to si = nx - i. | |
| si = nx - i | |
| z_top = z_mid - thickness / 2 if si % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness # Offset for plywood material thickness | |
| pts_x_half2.append((px, z_top)) | |
| full_x_profile = pts_x_half1 + pts_x_half2 | |
| for i, (px, pz) in enumerate(full_x_profile): | |
| x_beam_verts.append([px, ym, pz]) # Top | |
| x_beam_verts.append([px, ym, 0.0]) # Bottom | |
| if i > 0: | |
| idx = i * 2 | |
| x_beam_faces.append([idx-2, idx, idx+1, idx-1]) | |
| x_beam_mesh = Mesh.from_vertices_and_faces(x_beam_verts, x_beam_faces) | |
| # --- Generate Y-Beam (along x=xm, from y0 to y1) --- | |
| y_beam_verts = [] | |
| y_beam_faces = [] | |
| # First half of Y-Beam: y0 to ym (from Corner 0's spoke ends on Edge 2) | |
| pts_y_half1 = [] | |
| # In Corner 0, Edge 2 starts from ym and goes down to y0. But let's build from y0 to ym. | |
| for i in range(ny, -1, -1): | |
| t = i / ny | |
| py = ym - t * dy | |
| z_mid = fanvault_middle_hc([xm], [py], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([xm], [py], [x0, x1], [y0, y1], hc)[0] | |
| # For Corner 0, Edge 2, spoke index is nx + i (since i goes 1 to ny). | |
| # Wait, if i=0, it's the center point. | |
| # In generate_envelope_catenaries: for i in range(1, ny+1): t = i/ny; py = ym - t*dy | |
| # So the center point (xm, ym) is actually handled by Edge 1 (si=nx). | |
| # To make it continuous, we can use si = nx + i | |
| si = nx + i | |
| z_top = z_mid - thickness / 2 if si % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness # Offset for plywood material thickness | |
| pts_y_half1.append((py, z_top)) | |
| # Second half of Y-Beam: ym to y1 (from Corner 3 (x0, y1) or Corner 2 (x1, y1)) | |
| pts_y_half2 = [] | |
| # Corner 3: x0, y1. dy_c3 = ym - y1 (negative) | |
| # Edge 2 for Corner 3 would be on x=xm, varying y. | |
| dy_c3 = ym - y1 | |
| for i in range(1, ny + 1): | |
| t = i / ny | |
| py = ym - t * dy_c3 # This goes from ym towards y1 | |
| z_mid = fanvault_middle_hc([xm], [py], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([xm], [py], [x0, x1], [y0, y1], hc)[0] | |
| # In Corner 3, to match symmetry, it's si = nx + i | |
| si = nx + i | |
| z_top = z_mid - thickness / 2 if si % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness # Offset for plywood material thickness | |
| pts_y_half2.append((py, z_top)) | |
| full_y_profile = pts_y_half1 + pts_y_half2 | |
| for i, (py, pz) in enumerate(full_y_profile): | |
| y_beam_verts.append([xm, py, pz]) # Top | |
| y_beam_verts.append([xm, py, 0.0]) # Bottom | |
| if i > 0: | |
| idx = i * 2 | |
| y_beam_faces.append([idx-2, idx, idx+1, idx-1]) | |
| y_beam_mesh = Mesh.from_vertices_and_faces(y_beam_verts, y_beam_faces) | |
| return [x_beam_mesh, y_beam_mesh] | |
| def generate_perimeter_beams(config, n_spokes=10, ply_thickness=0.012): | |
| """ | |
| Generates meshes for perimeter support beams running along the 4 outer edges. | |
| The top edge matches the boundary spokes of the fan quadrants. | |
| The bottom edge is flat at z=0. | |
| """ | |
| xy_span = config['xy_span'] | |
| thickness = config['thickness'] | |
| hc = config['max_rise'] | |
| v_type = config['vault_type'] | |
| x0, x1 = xy_span[0] | |
| y0, y1 = xy_span[1] | |
| xm, ym = (x0 + x1) / 2, (y0 + y1) / 2 | |
| # Resolution for the beam curve (matching n_points in generate_envelope_catenaries) | |
| # n_points is passed as discr+1 in app.py. We'll use a reasonable default or pass it. | |
| n_points = 21 # Default resolution | |
| beams = [] | |
| # Helper to generate a beam mesh along an edge | |
| def create_edge_beam(p_start, p_end, quadrant_indices, spoke_indices): | |
| verts = [] | |
| faces = [] | |
| # p_start to p_mid and p_mid to p_end | |
| # Each segment comes from one quadrant's boundary spoke | |
| for q_idx, s_idx in zip(quadrant_indices, spoke_indices): | |
| # Deterministic corners based on generate_envelope_catenaries | |
| corners = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)] | |
| xc, yc = corners[q_idx] | |
| # Reconstruct the boundary spoke points | |
| # Edge 1 (si=0) goes from xc,yc to xc,ym (or x1,ym etc) | |
| # Edge 2 (si=nx+ny) goes from xc,yc to xm,yc | |
| # We need to know if this segment goes from corner to middle or middle to corner | |
| # Let's simplify: we know the start and end of the segment. | |
| # Segment 1: p_start to p_mid. Segment 2: p_mid to p_end. | |
| if q_idx in [0, 1, 2, 3]: # Just a placeholder for logic | |
| pass | |
| # Simplified: Just sample the envelope along the edge and alternate z based on the global spoke index | |
| # But wait, the perimeter is ONLY the boundary spokes (si=0 or si=nx+ny). | |
| # So we just need the z of THAT specific spoke. | |
| pass | |
| # Actually, let's just do it manually for the 4 edges for clarity. | |
| dx = xm - x0 | |
| dy = ym - y0 | |
| total_len = abs(dx) + abs(dy) | |
| nx = max(1, int(round((n_spokes - 1) * abs(dx) / total_len))) | |
| ny = (n_spokes - 1) - nx | |
| # 1. Beam along y = y0 (x0 to x1) | |
| verts, faces = [], [] | |
| si = nx + ny | |
| z_bottom = (-thickness / 2 if si % 2 == 0 else thickness / 2) - ply_thickness | |
| # Half 1: x0 to xm (Quad 0, si = nx+ny) | |
| for i in range(n_points): | |
| u = i / (n_points - 1) | |
| px = x0 + u * (xm - x0) | |
| z_mid = fanvault_middle_hc([px], [y0], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([px], [y0], [x0, x1], [y0, y1], hc)[0] | |
| z_top = z_mid - thickness / 2 if si % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness | |
| verts.extend([[px, y0, z_top], [px, y0, z_bottom]]) | |
| # Half 2: xm to x1 (Quad 1, si = nx+ny) | |
| for i in range(1, n_points): | |
| u = i / (n_points - 1) | |
| px = xm + u * (x1 - xm) | |
| z_mid = fanvault_middle_hc([px], [y0], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([px], [y0], [x0, x1], [y0, y1], hc)[0] | |
| z_top = z_mid - thickness / 2 if si % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness | |
| verts.extend([[px, y0, z_top], [px, y0, z_bottom]]) | |
| for i in range(len(verts)//2 - 1): | |
| idx = i * 2 | |
| faces.append([idx, idx+2, idx+3, idx+1]) | |
| beams.append(Mesh.from_vertices_and_faces(verts, faces)) | |
| # 2. Beam along y = y1 (x0 to x1) | |
| verts, faces = [], [] | |
| si = nx + ny | |
| z_bottom = (-thickness / 2 if si % 2 == 0 else thickness / 2) - ply_thickness | |
| # Half 1: x0 to xm (Quad 3, si = nx+ny) | |
| for i in range(n_points): | |
| u = i / (n_points - 1) | |
| px = x0 + u * (xm - x0) | |
| z_mid = fanvault_middle_hc([px], [y1], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([px], [y1], [x0, x1], [y0, y1], hc)[0] | |
| z_top = z_mid - thickness / 2 if si % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness | |
| verts.extend([[px, y1, z_top], [px, y1, z_bottom]]) | |
| # Half 2: xm to x1 (Quad 2, si = nx+ny) | |
| for i in range(1, n_points): | |
| u = i / (n_points - 1) | |
| px = xm + u * (x1 - xm) | |
| z_mid = fanvault_middle_hc([px], [y1], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([px], [y1], [x0, x1], [y0, y1], hc)[0] | |
| z_top = z_mid - thickness / 2 if si % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness | |
| verts.extend([[px, y1, z_top], [px, y1, z_bottom]]) | |
| for i in range(len(verts)//2 - 1): | |
| idx = i * 2 | |
| faces.append([idx, idx+2, idx+3, idx+1]) | |
| beams.append(Mesh.from_vertices_and_faces(verts, faces)) | |
| # 3. Beam along x = x0 (y0 to y1) | |
| verts, faces = [], [] | |
| si = 0 | |
| z_bottom = (-thickness / 2 if si % 2 == 0 else thickness / 2) - ply_thickness | |
| # Half 1: y0 to ym (Quad 0, si = 0) | |
| for i in range(n_points): | |
| u = i / (n_points - 1) | |
| py = y0 + u * (ym - y0) | |
| z_mid = fanvault_middle_hc([x0], [py], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([x0], [py], [x0, x1], [y0, y1], hc)[0] | |
| z_top = z_mid - thickness / 2 if si % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness | |
| verts.extend([[x0, py, z_top], [x0, py, z_bottom]]) | |
| # Half 2: ym to y1 (Quad 3, si = 0) | |
| for i in range(1, n_points): | |
| u = i / (n_points - 1) | |
| py = ym + u * (y1 - ym) | |
| z_mid = fanvault_middle_hc([x0], [py], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([x0], [py], [x0, x1], [y0, y1], hc)[0] | |
| z_top = z_mid - thickness / 2 if si % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness | |
| verts.extend([[x0, py, z_top], [x0, py, z_bottom]]) | |
| for i in range(len(verts)//2 - 1): | |
| idx = i * 2 | |
| faces.append([idx, idx+2, idx+3, idx+1]) | |
| beams.append(Mesh.from_vertices_and_faces(verts, faces)) | |
| # 4. Beam along x = x1 (y0 to y1) | |
| verts, faces = [], [] | |
| si = 0 | |
| z_bottom = (-thickness / 2 if si % 2 == 0 else thickness / 2) - ply_thickness | |
| # Half 1: y0 to ym (Quad 1, si = 0) | |
| for i in range(n_points): | |
| u = i / (n_points - 1) | |
| py = y0 + u * (ym - y0) | |
| z_mid = fanvault_middle_hc([x1], [py], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([x1], [py], [x0, x1], [y0, y1], hc)[0] | |
| z_top = z_mid - thickness / 2 if si % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness | |
| verts.extend([[x1, py, z_top], [x1, py, z_bottom]]) | |
| # Half 2: ym to y1 (Quad 2, si = 0) | |
| for i in range(1, n_points): | |
| u = i / (n_points - 1) | |
| py = ym + u * (y1 - ym) | |
| z_mid = fanvault_middle_hc([x1], [py], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([x1], [py], [x0, x1], [y0, y1], hc)[0] | |
| z_top = z_mid - thickness / 2 if si % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness | |
| verts.extend([[x1, py, z_top], [x1, py, z_bottom]]) | |
| for i in range(len(verts)//2 - 1): | |
| idx = i * 2 | |
| faces.append([idx, idx+2, idx+3, idx+1]) | |
| beams.append(Mesh.from_vertices_and_faces(verts, faces)) | |
| return beams | |
| def generate_beam_patterns(config, n_spokes=10, ply_thickness=0.012): | |
| """ | |
| Generates 2D flattened meshes for one half of each support and perimeter beam. | |
| These are intended for the plywood layout. | |
| """ | |
| xy_span = config['xy_span'] | |
| thickness = config['thickness'] | |
| hc = config['max_rise'] | |
| v_type = config['vault_type'] | |
| x0, x1 = xy_span[0] | |
| y0, y1 = xy_span[1] | |
| xm, ym = (x0 + x1) / 2, (y0 + y1) / 2 | |
| dx = xm - x0 | |
| dy = ym - y0 | |
| total_len = abs(dx) + abs(dy) | |
| nx = max(1, int(round((n_spokes - 1) * abs(dx) / total_len))) | |
| ny = (n_spokes - 1) - nx | |
| n_points = 21 | |
| beam_patterns = [] | |
| # 1. Support Beam X (Half) | |
| pts_x_half = [] | |
| for i in range(nx + 1): | |
| t = i / nx | |
| px = x0 + t * dx | |
| z_mid = fanvault_middle_hc([px], [ym], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([px], [ym], [x0, x1], [y0, y1], hc)[0] | |
| si = i | |
| z_top = z_mid - thickness / 2 if si % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness | |
| pts_x_half.append((px - x0, z_top)) | |
| verts, faces = [], [] | |
| for i, (lx, pz) in enumerate(pts_x_half): | |
| verts.append([lx, pz, 0.0]) | |
| verts.append([lx, 0.0, 0.0]) | |
| if i > 0: | |
| idx = i * 2 | |
| faces.append([idx-2, idx, idx+1, idx-1]) | |
| m_sup_x = Mesh.from_vertices_and_faces(verts, faces) | |
| m_sup_x.attributes['name'] = "SUP_X" | |
| beam_patterns.append(m_sup_x) | |
| # 2. Support Beam Y (Half) | |
| pts_y_half = [] | |
| for i in range(ny + 1): | |
| t = i / ny | |
| py = ym - t * dy | |
| z_mid = fanvault_middle_hc([xm], [py], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([xm], [py], [x0, x1], [y0, y1], hc)[0] | |
| si = nx + i | |
| z_top = z_mid - thickness / 2 if si % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness | |
| pts_y_half.append((ym - py, z_top)) | |
| verts, faces = [], [] | |
| for i, (ly, pz) in enumerate(pts_y_half): | |
| verts.append([ly, pz, 0.0]) | |
| verts.append([ly, 0.0, 0.0]) | |
| if i > 0: | |
| idx = i * 2 | |
| faces.append([idx-2, idx, idx+1, idx-1]) | |
| m_sup_y = Mesh.from_vertices_and_faces(verts, faces) | |
| m_sup_y.attributes['name'] = "SUP_Y" | |
| beam_patterns.append(m_sup_y) | |
| # 3. Perimeter Beam X-Edge (Half) | |
| z_perim_bottom = (-thickness / 2 if (nx + ny) % 2 == 0 else thickness / 2) - ply_thickness | |
| pts_px_half = [] | |
| for i in range(n_points): | |
| u = i / (n_points - 1) | |
| px = x0 + u * (xm - x0) | |
| z_mid = fanvault_middle_hc([px], [y0], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([px], [y0], [x0, x1], [y0, y1], hc)[0] | |
| z_top = z_mid - thickness / 2 if (nx + ny) % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness | |
| pts_px_half.append((px - x0, z_top)) | |
| verts, faces = [], [] | |
| for i, (lx, pz) in enumerate(pts_px_half): | |
| verts.append([lx, pz, 0.0]) | |
| verts.append([lx, z_perim_bottom, 0.0]) | |
| if i > 0: | |
| idx = i * 2 | |
| faces.append([idx-2, idx, idx+1, idx-1]) | |
| m_per_x = Mesh.from_vertices_and_faces(verts, faces) | |
| m_per_x.attributes['name'] = "PERM_X" | |
| beam_patterns.append(m_per_x) | |
| # 4. Perimeter Beam Y-Edge (Half) | |
| z_perim_bottom_y = (-thickness / 2 if 0 % 2 == 0 else thickness / 2) - ply_thickness # si=0 for Y perimeter | |
| pts_py_half = [] | |
| for i in range(n_points): | |
| u = i / (n_points - 1) | |
| py = y0 + u * (ym - y0) | |
| z_mid = fanvault_middle_hc([x0], [py], [x0, x1], [y0, y1], hc)[0] if v_type == 'fan' else crossvault_middle_hc([x0], [py], [x0, x1], [y0, y1], hc)[0] | |
| z_top = z_mid - thickness / 2 if 0 % 2 == 0 else z_mid + thickness / 2 | |
| z_top -= ply_thickness | |
| pts_py_half.append((py - y0, z_top)) | |
| verts, faces = [], [] | |
| for i, (ly, pz) in enumerate(pts_py_half): | |
| verts.append([ly, pz, 0.0]) | |
| verts.append([ly, z_perim_bottom_y, 0.0]) | |
| if i > 0: | |
| idx = i * 2 | |
| faces.append([idx-2, idx, idx+1, idx-1]) | |
| m_per_y = Mesh.from_vertices_and_faces(verts, faces) | |
| m_per_y.attributes['name'] = "PERM_Y" | |
| beam_patterns.append(m_per_y) | |
| return beam_patterns | |
| def generate_vault_meshes(catenaries, flat_z_offset=-15.0): | |
| three_d_meshes = [] | |
| flat_meshes = [] | |
| distortions_list = [] | |
| num_strips = max(0, len(catenaries) - 1) | |
| for i in range(num_strips): | |
| poly1_3d, poly2_3d = catenaries[i], catenaries[i+1] | |
| # Validation | |
| n1, n2 = len(poly1_3d.points), len(poly2_3d.points) | |
| if n1 != n2: | |
| raise ValueError( | |
| f"Catenary length mismatch at strip {i}: " | |
| f"Left has {n1} points, Right has {n2} points. " | |
| "This usually happens when 'Corner Cut Radius' is too large, " | |
| "causing different numbers of segments to be trimmed from adjacent spokes." | |
| ) | |
| # 3D Mesh | |
| verts_3d = poly1_3d.points + poly2_3d.points | |
| faces_3d = [] | |
| offset = len(poly1_3d.points) | |
| for j in range(len(poly1_3d.points) - 1): | |
| faces_3d.append([j, j+1, offset + j + 1, offset + j]) | |
| three_d_meshes.append(Mesh.from_vertices_and_faces(verts_3d, faces_3d)) | |
| # Flat Mesh | |
| start_x, start_y = poly1_3d.points[0].x, poly1_3d.points[0].y | |
| flat_start = Point(start_x, start_y, flat_z_offset) | |
| unroll_vec = Vector.from_start_end(poly1_3d.points[0], poly1_3d.points[1]) | |
| flat_mesh, quad_distortions = develop_strip_to_plane(poly1_3d, poly2_3d, flat_start, unroll_vec) | |
| if flat_mesh: | |
| flat_mesh.attributes['name'] = f"S_{i+1:02d}" | |
| flat_meshes.append(flat_mesh) | |
| distortions_list.append(quad_distortions) | |
| return three_d_meshes, flat_meshes, distortions_list | |