Spaces:
Runtime error
Runtime error
| import math | |
| import numpy as np | |
| from compas.geometry import Point, Rotation, Translation, Vector, intersection_segment_segment_xy, is_point_in_polygon_xy | |
| def get_mesh_2d_bbox(mesh): | |
| """Returns (min_x, min_y, max_x, max_y) for a flat mesh.""" | |
| vertices = [mesh.vertex_coordinates(v) for v in mesh.vertices()] | |
| xs = [v[0] for v in vertices] | |
| ys = [v[1] for v in vertices] | |
| return min(xs), min(ys), max(xs), max(ys) | |
| def get_mesh_outline(mesh): | |
| """ | |
| Extracts the boundary polygon points for a quad strip mesh. | |
| Assumes vertices are ordered [P0, Q0, P1, Q1, ...] | |
| """ | |
| vertices = [mesh.vertex_coordinates(v) for v in mesh.vertices()] | |
| # P side: 0, 2, 4... | |
| p_side = vertices[0::2] | |
| # Q side: 1, 3, 5... (reversed to close the loop) | |
| q_side = vertices[1::2][::-1] | |
| return p_side + q_side | |
| def polygons_intersect(pts1, pts2, margin=0.0): | |
| """Checks if two polygons intersect using COMPAS primitives.""" | |
| # 1. BBox check (fast) | |
| b1 = [min(p[0] for p in pts1), min(p[1] for p in pts1), max(p[0] for p in pts1), max(p[1] for p in pts1)] | |
| b2 = [min(p[0] for p in pts2), min(p[1] for p in pts2), max(p[0] for p in pts2), max(p[1] for p in pts2)] | |
| # Apply margin to b2 for safety | |
| b2 = [b2[0]-margin, b2[1]-margin, b2[2]+margin, b2[3]+margin] | |
| if b1[2] < b2[0] or b1[0] > b2[2] or b1[3] < b2[1] or b1[1] > b2[3]: | |
| return False | |
| # 2. Segment-Segment intersection | |
| for i in range(len(pts1)): | |
| s1 = (pts1[i], pts1[(i+1)%len(pts1)]) | |
| for j in range(len(pts2)): | |
| s2 = (pts2[j], pts2[(j+1)%len(pts2)]) | |
| res = intersection_segment_segment_xy(s1, s2) | |
| if res: return True | |
| # 3. Point in Polygon (for containment) | |
| if is_point_in_polygon_xy(pts1[0], pts2): return True | |
| if is_point_in_polygon_xy(pts2[0], pts1): return True | |
| return False | |
| def get_profiles(pts, resolution=0.01): | |
| """ | |
| Computes top and bottom profiles of a polygon at given x-resolution. | |
| Returns (xs, bottoms, tops) arrays. | |
| """ | |
| min_x = min(p[0] for p in pts) | |
| max_x = max(p[0] for p in pts) | |
| # Use linspace for more predictable counts | |
| n_pts = int(round((max_x - min_x) / resolution)) + 1 | |
| xs = np.linspace(min_x, max_x, n_pts) | |
| bottoms = np.full(len(xs), float('inf')) | |
| tops = np.full(len(xs), float('-inf')) | |
| for i in range(len(pts)): | |
| p1, p2 = pts[i], pts[(i+1)%len(pts)] | |
| x1, y1 = p1[0], p1[1] | |
| x2, y2 = p2[0], p2[1] | |
| if abs(x2 - x1) < 1e-9: | |
| # Vertical segment: update at one x-bin | |
| idx = int(round((x1 - min_x) / resolution)) | |
| if 0 <= idx < len(xs): | |
| bottoms[idx] = min(bottoms[idx], y1, y2) | |
| tops[idx] = max(tops[idx], y1, y2) | |
| continue | |
| # Range of bins covered | |
| x_min, x_max = min(x1, x2), max(x1, x2) | |
| idx_start = int(math.ceil((x_min - min_x) / resolution)) | |
| idx_end = int(math.floor((x_max - min_x) / resolution)) | |
| for idx in range(max(0, idx_start), min(len(xs), idx_end + 1)): | |
| x = xs[idx] | |
| y = y1 + (y2 - y1) * (x - x1) / (x2 - x1) | |
| bottoms[idx] = min(bottoms[idx], y) | |
| tops[idx] = max(tops[idx], y) | |
| # Fill gaps by interpolation | |
| valid_mask = (bottoms != float('inf')) | |
| if not np.all(valid_mask): | |
| valid_indices = np.where(valid_mask)[0] | |
| if len(valid_indices) > 0: | |
| bottoms = np.interp(xs, xs[valid_mask], bottoms[valid_mask]) | |
| tops = np.interp(xs, xs[valid_mask], tops[valid_mask]) | |
| return xs, bottoms, tops | |
| def _prepare_items(meshes, optimize_rotation, res): | |
| items = [] | |
| for i, m in enumerate(meshes): | |
| m_oriented = m.copy() | |
| vertices = [m_oriented.vertex_coordinates(v) for v in m_oriented.vertices()] | |
| if len(vertices) >= 4: | |
| p_start = Point(*vertices[0]) | |
| p_end = Point(*vertices[-2]) | |
| vec = Vector.from_start_end(p_start, p_end) | |
| angle = math.atan2(vec.y, vec.x) | |
| R = Rotation.from_axis_and_angle([0, 0, 1], -angle, point=p_start) | |
| m_oriented.transform(R) | |
| variants = [] | |
| possible_angles = [0, math.pi/2, math.pi, 3*math.pi/2] if optimize_rotation else [0] | |
| for angle in possible_angles: | |
| m_rot = m_oriented.copy() | |
| if angle != 0: | |
| R = Rotation.from_axis_and_angle([0, 0, 1], angle, point=[0,0,0]) | |
| m_rot.transform(R) | |
| mxr, myr, _, _ = get_mesh_2d_bbox(m_rot) | |
| m_rot.transform(Translation.from_vector([-mxr, -myr, 0])) | |
| pts_r = get_mesh_outline(m_rot) | |
| xs_r, b_r, t_r = get_profiles(pts_r, resolution=res) | |
| variants.append({ | |
| 'mesh': m_rot, | |
| 'bottoms': b_r, | |
| 'tops': t_r, | |
| 'w': max(xs_r), | |
| 'n_bins': len(xs_r) | |
| }) | |
| items.append({'index': i, 'variants': variants}) | |
| return items | |
| def _pack_one_sheet(items, sheet_width, sheet_height, margin, res): | |
| """Internal helper to pack as many items as possible onto one sheet.""" | |
| total_bins = int(math.ceil(sheet_width / res)) + 1 | |
| skyline = np.zeros(total_bins) | |
| packed_meshes = [] | |
| packed_indices = [] | |
| for i, item in enumerate(items): | |
| best_h = float('inf') | |
| best_config = None | |
| for variant in item['variants']: | |
| w = variant['w'] | |
| n_bins = variant['n_bins'] | |
| max_ox_bin = int(math.floor((sheet_width - 2*margin - w) / res)) | |
| for ox_bin in range(max_ox_bin + 1): | |
| v = 0 | |
| for j in range(n_bins): | |
| needed = skyline[ox_bin + j] + margin - variant['bottoms'][j] | |
| if needed > v: v = needed | |
| # Check if this item fits within sheet height | |
| peak_h = 0 | |
| for j in range(n_bins): | |
| h_at_j = v + variant['tops'][j] | |
| if h_at_j > peak_h: peak_h = h_at_j | |
| if peak_h <= (sheet_height - 2*margin): | |
| if peak_h < best_h: | |
| best_h = peak_h | |
| best_config = (variant, ox_bin, v) | |
| if best_config: | |
| variant, ox_bin, v = best_config | |
| ox = ox_bin * res | |
| placed_m = variant['mesh'].copy() | |
| placed_m.transform(Translation.from_vector([margin + ox, margin + v, 0])) | |
| packed_meshes.append(placed_m) | |
| packed_indices.append(i) | |
| for j in range(variant['n_bins']): | |
| skyline[ox_bin + j] = max(skyline[ox_bin + j], v + variant['tops'][j]) | |
| max_h_used = np.max(skyline) if len(packed_meshes) > 0 else 0 | |
| max_w_used = 0 | |
| if packed_meshes: | |
| for m in packed_meshes: | |
| _, _, mxx, _ = get_mesh_2d_bbox(m) | |
| if mxx > max_w_used: max_w_used = mxx | |
| return packed_meshes, packed_indices, (max_w_used + margin, max_h_used + margin) | |
| def pack_strips(meshes, sheet_width, sheet_height, margin=0.02, optimize_rotation=True): | |
| """Legacy wrapper for single sheet packing. If height is too small, it still packs everything but returns success=False.""" | |
| res = 0.02 | |
| items = _prepare_items(meshes, optimize_rotation, res) | |
| items.sort(key=lambda x: x['variants'][0]['w'], reverse=True) | |
| # In legacy mode, we force everything to pack even if it exceeds height, | |
| # so we use a huge height for the internal call then check the result. | |
| packed_meshes, _, used_dims = _pack_one_sheet(items, sheet_width, 1e6, margin, res) | |
| success = used_dims[1] <= sheet_height | |
| return packed_meshes, success, used_dims | |
| def pack_strips_multi(meshes, sheet_width, sheet_height, margin=0.02, optimize_rotation=True): | |
| """Packs meshes onto as many sheets of fixed size as needed.""" | |
| res = 0.02 | |
| remaining_items = _prepare_items(meshes, optimize_rotation, res) | |
| remaining_items.sort(key=lambda x: x['variants'][0]['w'], reverse=True) | |
| sheets = [] | |
| while remaining_items: | |
| packed_meshes, packed_indices, used_dims = _pack_one_sheet(remaining_items, sheet_width, sheet_height, margin, res) | |
| if not packed_indices: | |
| # Could not fit even a single item on a fresh sheet | |
| # This happens if an item is wider than the sheet | |
| break | |
| sheets.append({ | |
| 'meshes': packed_meshes, | |
| 'dims': used_dims | |
| }) | |
| # Remove packed items | |
| remaining_items = [item for i, item in enumerate(remaining_items) if i not in packed_indices] | |
| return sheets | |