| import numpy as np |
|
|
|
|
| def _empty_edges_like(edges: np.ndarray) -> np.ndarray: |
| return np.zeros((0, 2), dtype=edges.dtype) |
|
|
|
|
| def _compact_vertices_for_edges(verts: np.ndarray, edges: np.ndarray) -> tuple[np.ndarray, np.ndarray]: |
| if len(edges) == 0: |
| return verts[:0], _empty_edges_like(edges) |
|
|
| used_vertex_indices, inverse = np.unique(edges.reshape(-1), return_inverse=True) |
| compact_verts = verts[used_vertex_indices] |
| compact_edges = inverse.reshape(-1, 2).astype(edges.dtype, copy=False) |
| return compact_verts, compact_edges |
|
|
|
|
| def _compact_vertices_and_features_for_edges( |
| verts: np.ndarray, edges: np.ndarray, vertex_feats: np.ndarray |
| ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: |
| if len(edges) == 0: |
| return verts[:0], _empty_edges_like(edges), vertex_feats[:0] |
|
|
| used_vertex_indices, inverse = np.unique(edges.reshape(-1), return_inverse=True) |
| compact_verts = verts[used_vertex_indices] |
| compact_edges = inverse.reshape(-1, 2).astype(edges.dtype, copy=False) |
| compact_feats = vertex_feats[used_vertex_indices] |
| return compact_verts, compact_edges, compact_feats |
|
|
|
|
| def _merged_vertex_from_incident_lines( |
| verts: np.ndarray, |
| edges: np.ndarray, |
| component_members: np.ndarray, |
| incident_edge_indices_by_vertex: list[list[int]], |
| average_position: np.ndarray, |
| intersection_angle_threshold_deg: float | None, |
| ) -> np.ndarray: |
| if len(component_members) < 2: |
| return average_position |
| if intersection_angle_threshold_deg is None or intersection_angle_threshold_deg <= 0: |
| return average_position |
|
|
| component_member_set = set(int(vertex) for vertex in component_members) |
| component_edge_indices: set[int] = set() |
| for vertex in component_members: |
| component_edge_indices.update(incident_edge_indices_by_vertex[int(vertex)]) |
|
|
| line_points: list[np.ndarray] = [] |
| line_dirs: list[np.ndarray] = [] |
| for edge_idx in component_edge_indices: |
| a, b = edges[edge_idx] |
| a = int(a) |
| b = int(b) |
| if (a in component_member_set) == (b in component_member_set): |
| continue |
|
|
| line_vec = verts[b] - verts[a] |
| line_length = np.linalg.norm(line_vec) |
| if line_length == 0: |
| continue |
|
|
| line_points.append(verts[a].astype(np.float64, copy=False)) |
| line_dirs.append(line_vec.astype(np.float64, copy=False) / line_length) |
|
|
| if len(line_dirs) < 2: |
| return average_position |
|
|
| directions = np.array(line_dirs, dtype=np.float64) |
| dot_products = np.abs(directions @ directions.T) |
| upper_triangle = np.triu_indices(len(directions), k=1) |
| angles_deg = np.degrees(np.arccos(np.clip(dot_products[upper_triangle], -1.0, 1.0))) |
| if not np.any(angles_deg >= intersection_angle_threshold_deg): |
| return average_position |
|
|
| system = np.zeros((3, 3), dtype=np.float64) |
| rhs = np.zeros(3, dtype=np.float64) |
| identity = np.eye(3, dtype=np.float64) |
| for point, direction in zip(line_points, line_dirs): |
| projection = identity - np.outer(direction, direction) |
| system += projection |
| rhs += projection @ point |
|
|
| try: |
| intersection = np.linalg.lstsq(system, rhs, rcond=None)[0] |
| except np.linalg.LinAlgError: |
| return average_position |
|
|
| if not np.all(np.isfinite(intersection)): |
| return average_position |
| return intersection.astype(average_position.dtype, copy=False) |
|
|
|
|
| def merge_close_vertices( |
| verts: np.ndarray, |
| edges: np.ndarray, |
| distance_threshold: float, |
| intersection_angle_threshold_deg: float | None = 30.0, |
| ) -> tuple[np.ndarray, np.ndarray]: |
| if len(verts) == 0 or distance_threshold <= 0: |
| return verts, edges |
|
|
| cell_size = float(distance_threshold) |
| cell_coords = np.floor(verts / cell_size).astype(np.int64) |
| buckets: dict[tuple[int, int, int], list[int]] = {} |
|
|
| parent = np.arange(len(verts), dtype=np.int64) |
| rank = np.zeros(len(verts), dtype=np.int8) |
| members = [set([i]) for i in range(len(verts))] |
| edge_neighbors = [set() for _ in range(len(verts))] |
| incident_edge_indices_by_vertex: list[list[int]] = [[] for _ in range(len(verts))] |
| for edge_idx, (a, b) in enumerate(edges): |
| a = int(a) |
| b = int(b) |
| incident_edge_indices_by_vertex[a].append(edge_idx) |
| if a != b: |
| incident_edge_indices_by_vertex[b].append(edge_idx) |
| if a == b: |
| continue |
| edge_neighbors[a].add(b) |
| edge_neighbors[b].add(a) |
|
|
| def find(i: int) -> int: |
| while parent[i] != i: |
| parent[i] = parent[parent[i]] |
| i = parent[i] |
| return i |
|
|
| def components_share_edge(a_members: set[int], b_members: set[int]) -> bool: |
| if len(a_members) > len(b_members): |
| a_members, b_members = b_members, a_members |
| return any(not edge_neighbors[vertex].isdisjoint(b_members) for vertex in a_members) |
|
|
| def union(a: int, b: int) -> None: |
| ra, rb = find(a), find(b) |
| if ra == rb or components_share_edge(members[ra], members[rb]): |
| return |
| if rank[ra] < rank[rb]: |
| ra, rb = rb, ra |
| parent[rb] = ra |
| if rank[ra] == rank[rb]: |
| rank[ra] += 1 |
| members[ra].update(members[rb]) |
| members[rb].clear() |
|
|
| for i, cell in enumerate(cell_coords): |
| key = tuple(cell.tolist()) |
| for dx in (-1, 0, 1): |
| for dy in (-1, 0, 1): |
| for dz in (-1, 0, 1): |
| neighbor_key = (key[0] + dx, key[1] + dy, key[2] + dz) |
| for j in buckets.get(neighbor_key, ()): |
| if np.linalg.norm(verts[i] - verts[j]) <= distance_threshold: |
| union(i, j) |
| buckets.setdefault(key, []).append(i) |
|
|
| roots = np.array([find(i) for i in range(len(verts))], dtype=np.int64) |
| unique_roots, inverse = np.unique(roots, return_inverse=True) |
| merged_verts = np.zeros((len(unique_roots), 3), dtype=verts.dtype) |
| for new_vertex_idx in range(len(unique_roots)): |
| component_members = np.flatnonzero(inverse == new_vertex_idx) |
| average_position = np.mean(verts[component_members], axis=0) |
| merged_verts[new_vertex_idx] = _merged_vertex_from_incident_lines( |
| verts, |
| edges, |
| component_members, |
| incident_edge_indices_by_vertex, |
| average_position, |
| intersection_angle_threshold_deg, |
| ) |
| old_to_new = inverse |
| merged_edges = old_to_new[edges] |
| keep = merged_edges[:, 0] != merged_edges[:, 1] |
| merged_edges = merged_edges[keep] |
| if len(merged_edges) == 0: |
| return merged_verts, np.zeros((0, 2), dtype=np.int64) |
|
|
| merged_edges = np.sort(merged_edges, axis=1) |
| merged_edges = np.unique(merged_edges, axis=0) |
| return merged_verts, merged_edges |
|
|
|
|
| def merge_vertices_with_features( |
| verts: np.ndarray, |
| edges: np.ndarray, |
| vertex_feats: np.ndarray, |
| alpha: float = 0.95, |
| distance_threshold: float | None = None, |
| vertex_scores: np.ndarray | None = None, |
| ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: |
| if len(verts) == 0 or len(vertex_feats) == 0: |
| return verts, edges, vertex_feats |
| if len(verts) != len(vertex_feats): |
| raise ValueError("verts and vertex_feats must have the same length") |
|
|
| feats = vertex_feats.astype(np.float32, copy=False) |
| sim = feats @ feats.T |
|
|
| parent = np.arange(len(verts), dtype=np.int64) |
| rank = np.zeros(len(verts), dtype=np.int8) |
| members = [set([i]) for i in range(len(verts))] |
| edge_neighbors = [set() for _ in range(len(verts))] |
| for a, b in edges: |
| if a == b: |
| continue |
| edge_neighbors[int(a)].add(int(b)) |
| edge_neighbors[int(b)].add(int(a)) |
|
|
| def find(i: int) -> int: |
| while parent[i] != i: |
| parent[i] = parent[parent[i]] |
| i = parent[i] |
| return i |
|
|
| def components_share_edge(a_members: set[int], b_members: set[int]) -> bool: |
| if len(a_members) > len(b_members): |
| a_members, b_members = b_members, a_members |
| return any(not edge_neighbors[vertex].isdisjoint(b_members) for vertex in a_members) |
|
|
| def union(a: int, b: int) -> None: |
| ra, rb = find(a), find(b) |
| if ra == rb or components_share_edge(members[ra], members[rb]): |
| return |
| if rank[ra] < rank[rb]: |
| ra, rb = rb, ra |
| parent[rb] = ra |
| if rank[ra] == rank[rb]: |
| rank[ra] += 1 |
| members[ra].update(members[rb]) |
| members[rb].clear() |
|
|
| rows, cols = np.where(np.triu(sim >= alpha, k=1)) |
| if distance_threshold is not None: |
| if distance_threshold <= 0: |
| rows, cols = np.array([], dtype=np.int64), np.array([], dtype=np.int64) |
| else: |
| distances = np.linalg.norm(verts[rows] - verts[cols], axis=1) |
| keep = distances <= distance_threshold |
| rows, cols = rows[keep], cols[keep] |
| for a, b in zip(rows.tolist(), cols.tolist()): |
| union(a, b) |
|
|
| roots = np.array([find(i) for i in range(len(verts))], dtype=np.int64) |
| unique_roots, inverse = np.unique(roots, return_inverse=True) |
| merged_verts = np.zeros((len(unique_roots), 3), dtype=verts.dtype) |
| counts = np.bincount(inverse) |
| np.add.at(merged_verts, inverse, verts) |
| merged_verts /= counts[:, None] |
|
|
| if vertex_scores is not None: |
| scores = vertex_scores.astype(np.float32, copy=False) |
| best_score = np.full(len(unique_roots), -np.inf, dtype=np.float32) |
| best_index = np.zeros(len(unique_roots), dtype=np.int64) |
| for idx, group_idx in enumerate(inverse): |
| score = scores[idx] |
| if score > best_score[group_idx]: |
| best_score[group_idx] = score |
| best_index[group_idx] = idx |
| merged_feats = vertex_feats[best_index] |
| else: |
| merged_feats = np.zeros((len(unique_roots), vertex_feats.shape[1]), dtype=vertex_feats.dtype) |
| np.add.at(merged_feats, inverse, vertex_feats) |
| merged_feats /= counts[:, None] |
|
|
| merged_edges = inverse[edges] |
| keep = merged_edges[:, 0] != merged_edges[:, 1] |
| merged_edges = merged_edges[keep] |
| if len(merged_edges) == 0: |
| return merged_verts, np.zeros((0, 2), dtype=np.int64), merged_feats |
|
|
| merged_edges = np.sort(merged_edges, axis=1) |
| merged_edges = np.unique(merged_edges, axis=0) |
| return merged_verts, merged_edges, merged_feats |
|
|
|
|
| def filter_edges_by_length( |
| verts: np.ndarray, |
| edges: np.ndarray, |
| min_length: float | None = 0.05, |
| max_length: float | None = 100.0, |
| ) -> tuple[np.ndarray, np.ndarray]: |
| if len(edges) == 0: |
| return verts[:0], _empty_edges_like(edges) |
|
|
| edge_segments = verts[edges] |
| edge_lengths = np.linalg.norm(edge_segments[:, 1] - edge_segments[:, 0], axis=1) |
| keep = np.ones(len(edges), dtype=bool) |
| if min_length is not None: |
| keep &= edge_lengths >= min_length |
| if max_length is not None: |
| keep &= edge_lengths <= max_length |
| return _compact_vertices_for_edges(verts, edges[keep]) |
|
|
|
|
| def add_gap_fill_edges_from_candidates( |
| verts: np.ndarray, |
| edges: np.ndarray, |
| candidate_edge_points: np.ndarray, |
| merge_distance_threshold: float, |
| ) -> tuple[np.ndarray, np.ndarray]: |
| if len(verts) == 0 or len(candidate_edge_points) == 0 or merge_distance_threshold < 0: |
| return verts, edges |
|
|
| candidate_edge_points = np.asarray(candidate_edge_points).reshape(-1, 2, verts.shape[1]) |
| endpoint_distances = np.linalg.norm( |
| candidate_edge_points.reshape(-1, verts.shape[1])[:, None, :] - verts[None, :, :], |
| axis=2, |
| ) |
| nearest_vertices = np.argmin(endpoint_distances, axis=1).reshape(-1, 2) |
| nearest_distances = np.min(endpoint_distances, axis=1).reshape(-1, 2) |
|
|
| known_edges = {tuple(sorted((int(a), int(b)))) for a, b in edges if int(a) != int(b)} |
| added_edges: list[tuple[int, int]] = [] |
| for edge_vertices, edge_distances in zip(nearest_vertices, nearest_distances): |
| if np.any(edge_distances > merge_distance_threshold): |
| continue |
|
|
| a, b = int(edge_vertices[0]), int(edge_vertices[1]) |
| if a == b: |
| continue |
|
|
| edge_key = tuple(sorted((a, b))) |
| if edge_key in known_edges: |
| continue |
|
|
| known_edges.add(edge_key) |
| added_edges.append(edge_key) |
|
|
| if len(added_edges) == 0: |
| return verts, edges |
|
|
| added_edges_array = np.array(added_edges, dtype=edges.dtype) |
| return verts, np.concatenate([edges, added_edges_array], axis=0) |
|
|
|
|
| def add_edges_by_translated_edge_symmetry( |
| verts: np.ndarray, |
| edges: np.ndarray, |
| distance_threshold: float = 0.2, |
| ) -> tuple[np.ndarray, np.ndarray]: |
| if len(verts) == 0 or len(edges) == 0 or distance_threshold < 0: |
| return verts, edges |
|
|
| template_edges = edges.copy() |
| known_edges = {tuple(sorted((int(a), int(b)))) for a, b in edges if int(a) != int(b)} |
| added_edges: list[tuple[int, int]] = [] |
| for a, b in template_edges: |
| a = int(a) |
| b = int(b) |
| edge_vector = verts[b] - verts[a] |
| if not np.any(edge_vector): |
| continue |
|
|
| for template_vector in (edge_vector, -edge_vector): |
| translated_endpoints = verts + template_vector |
| distances = np.linalg.norm(translated_endpoints[:, None, :] - verts[None, :, :], axis=2) |
| nearest_vertices = np.argmin(distances, axis=1) |
| nearest_distances = np.min(distances, axis=1) |
| for start_vertex, (end_vertex, end_distance) in enumerate(zip(nearest_vertices, nearest_distances)): |
| if end_distance > distance_threshold: |
| continue |
|
|
| end_vertex = int(end_vertex) |
| if start_vertex == end_vertex: |
| continue |
|
|
| edge_key = tuple(sorted((start_vertex, end_vertex))) |
| if edge_key in known_edges: |
| continue |
|
|
| known_edges.add(edge_key) |
| added_edges.append(edge_key) |
|
|
| if len(added_edges) == 0: |
| return verts, edges |
|
|
| added_edges_array = np.array(added_edges, dtype=edges.dtype) |
| return verts, np.concatenate([edges, added_edges_array], axis=0) |
|
|
|
|
| def _edge_angle_to_xz_plane_deg(edge_vecs: np.ndarray) -> np.ndarray: |
| xz_norms = np.linalg.norm(edge_vecs[:, [0, 2]], axis=1) |
| return np.degrees(np.arctan2(np.abs(edge_vecs[:, 1]), xz_norms)) |
|
|
|
|
| def _infer_principal_xz_directions( |
| edge_vecs: np.ndarray, |
| edge_lengths: np.ndarray, |
| horizontal_angle_threshold_deg: float, |
| ) -> tuple[np.ndarray, np.ndarray] | None: |
| xz_vecs = edge_vecs[:, [0, 2]] |
| xz_norms = np.linalg.norm(xz_vecs, axis=1) |
| angles = _edge_angle_to_xz_plane_deg(edge_vecs) |
| keep = (edge_lengths > 0) & (xz_norms > 0) & (angles <= horizontal_angle_threshold_deg) |
| if not np.any(keep): |
| return None |
|
|
| dirs = xz_vecs[keep] / xz_norms[keep, None] |
| weights = edge_lengths[keep] |
| covariance = np.einsum("i,ij,ik->jk", weights, dirs, dirs) |
| if not np.any(covariance): |
| return None |
|
|
| eigvals, eigvecs = np.linalg.eigh(covariance) |
| primary = eigvecs[:, int(np.argmax(eigvals))] |
| primary_norm = np.linalg.norm(primary) |
| if primary_norm == 0: |
| return None |
|
|
| primary = primary / primary_norm |
| secondary = np.array([-primary[1], primary[0]], dtype=primary.dtype) |
| return primary, secondary |
|
|
|
|
| def project_near_horizontal_edges_to_xz_plane( |
| verts: np.ndarray, |
| edges: np.ndarray, |
| horizontal_angle_threshold_deg: float = 5.0, |
| principal_direction_angle_threshold_deg: float = 5.0, |
| ) -> tuple[np.ndarray, np.ndarray]: |
| if len(edges) == 0: |
| return verts, edges |
|
|
| edge_vecs = verts[edges[:, 1]] - verts[edges[:, 0]] |
| edge_lengths = np.linalg.norm(edge_vecs, axis=1) |
| principal_dirs = _infer_principal_xz_directions(edge_vecs, edge_lengths, horizontal_angle_threshold_deg) |
| if principal_dirs is None: |
| return verts, edges |
|
|
| xz_vecs = edge_vecs[:, [0, 2]] |
| xz_norms = np.linalg.norm(xz_vecs, axis=1) |
| valid = (edge_lengths > 0) & (xz_norms > 0) |
| if not np.any(valid): |
| return verts, edges |
|
|
| angles_to_xz = _edge_angle_to_xz_plane_deg(edge_vecs) |
| dirs_xz = np.zeros_like(xz_vecs, dtype=np.result_type(verts.dtype, np.float64)) |
| dirs_xz[valid] = xz_vecs[valid] / xz_norms[valid, None] |
| dots = np.stack([np.abs(dirs_xz @ direction) for direction in principal_dirs], axis=1) |
| angles_to_principal = np.degrees(np.arccos(np.clip(np.max(dots, axis=1), -1.0, 1.0))) |
| eligible = ( |
| valid |
| & (angles_to_xz <= horizontal_angle_threshold_deg) |
| & (angles_to_principal <= principal_direction_angle_threshold_deg) |
| ) |
| eligible_edges = edges[eligible] |
| if len(eligible_edges) == 0: |
| return verts, edges |
|
|
| parent = np.arange(len(verts), dtype=np.int64) |
|
|
| def find(i: int) -> int: |
| while parent[i] != i: |
| parent[i] = parent[parent[i]] |
| i = parent[i] |
| return i |
|
|
| def union(a: int, b: int) -> None: |
| ra, rb = find(a), find(b) |
| if ra != rb: |
| parent[rb] = ra |
|
|
| for a, b in eligible_edges: |
| union(int(a), int(b)) |
|
|
| new_verts = verts.copy() |
| eligible_vertices = np.unique(eligible_edges.reshape(-1)) |
| roots = np.array([find(int(vertex)) for vertex in eligible_vertices], dtype=np.int64) |
| for root in np.unique(roots): |
| component_vertices = eligible_vertices[roots == root] |
| new_verts[component_vertices, 1] = np.mean(verts[component_vertices, 1]) |
| return new_verts, edges |
|
|
|
|
| def snap_edges_to_principal_xz_directions( |
| verts: np.ndarray, |
| edges: np.ndarray, |
| horizontal_angle_threshold_deg: float = 5.0, |
| principal_direction_angle_threshold_deg: float = 5.0, |
| ) -> tuple[np.ndarray, np.ndarray]: |
| if len(edges) == 0: |
| return verts, edges |
|
|
| edge_vecs = verts[edges[:, 1]] - verts[edges[:, 0]] |
| edge_lengths = np.linalg.norm(edge_vecs, axis=1) |
| principal_dirs = _infer_principal_xz_directions(edge_vecs, edge_lengths, horizontal_angle_threshold_deg) |
| if principal_dirs is None: |
| return verts, edges |
|
|
| xz_vecs = edge_vecs[:, [0, 2]] |
| xz_norms = np.linalg.norm(xz_vecs, axis=1) |
| valid = xz_norms > 0 |
| if not np.any(valid): |
| return verts, edges |
|
|
| dirs_xz = np.zeros_like(xz_vecs, dtype=np.result_type(verts.dtype, np.float64)) |
| dirs_xz[valid] = xz_vecs[valid] / xz_norms[valid, None] |
| dots = np.stack([np.abs(dirs_xz @ direction) for direction in principal_dirs], axis=1) |
| best_direction_indices = np.argmax(dots, axis=1) |
| angles_to_principal = np.degrees(np.arccos(np.clip(np.max(dots, axis=1), -1.0, 1.0))) |
| eligible = valid & (angles_to_principal <= principal_direction_angle_threshold_deg) |
| eligible_edges = edges[eligible] |
| if len(eligible_edges) == 0: |
| return verts, edges |
|
|
| eligible_vertices = np.unique(eligible_edges.reshape(-1)) |
| vertex_to_local = {int(vertex): idx for idx, vertex in enumerate(eligible_vertices)} |
| constraints = np.zeros((len(eligible_edges), 2 * len(eligible_vertices)), dtype=np.float64) |
| eligible_direction_indices = best_direction_indices[eligible] |
| for row, ((a, b), direction_idx) in enumerate(zip(eligible_edges, eligible_direction_indices)): |
| direction = principal_dirs[int(direction_idx)] |
| normal = np.array([-direction[1], direction[0]], dtype=np.float64) |
| a_col = 2 * vertex_to_local[int(a)] |
| b_col = 2 * vertex_to_local[int(b)] |
| constraints[row, a_col : a_col + 2] = -normal |
| constraints[row, b_col : b_col + 2] = normal |
|
|
| original_xz = verts[eligible_vertices][:, [0, 2]].astype(np.float64, copy=False).reshape(-1) |
| residual = constraints @ original_xz |
| if not np.any(residual): |
| return verts, edges |
|
|
| system = constraints @ constraints.T |
| correction_weights = np.linalg.lstsq(system, residual, rcond=None)[0] |
| snapped_xz = original_xz - constraints.T @ correction_weights |
|
|
| new_verts = verts.copy() |
| snapped_xz = snapped_xz.reshape(-1, 2) |
| new_verts[eligible_vertices, 0] = snapped_xz[:, 0] |
| new_verts[eligible_vertices, 2] = snapped_xz[:, 1] |
| return new_verts, edges |
|
|
|
|
| project_near_horizontal_edges_to_xy_plane = project_near_horizontal_edges_to_xz_plane |
| snap_edges_to_principal_xy_directions = snap_edges_to_principal_xz_directions |
|
|
|
|
| def _remove_solitary_edges(verts: np.ndarray, edges: np.ndarray) -> tuple[np.ndarray, np.ndarray]: |
| if len(edges) == 0: |
| return verts[:0], _empty_edges_like(edges) |
|
|
| degrees = np.bincount(edges.reshape(-1), minlength=len(verts)) |
| edge_degrees = degrees[edges] |
| keep = np.any(edge_degrees > 1, axis=1) |
| return _compact_vertices_for_edges(verts, edges[keep]) |
|
|
|
|
| def remove_solitary_edges(verts: np.ndarray, edges: np.ndarray) -> tuple[np.ndarray, np.ndarray]: |
| return _remove_solitary_edges(verts, edges) |
|
|
|
|
| def filter_edges_by_connectivity( |
| verts: np.ndarray, |
| edges: np.ndarray, |
| min_endpoint_degree: int = 2, |
| ) -> tuple[np.ndarray, np.ndarray]: |
| """Keep edges where at least one endpoint connects to >= min_endpoint_degree other edges. |
| |
| Boosts structurally connected edges; penalizes isolated ones. min_endpoint_degree=2 |
| keeps any edge touching a vertex with 2+ incident edges. |
| """ |
| if len(edges) == 0 or min_endpoint_degree <= 1: |
| return verts, edges |
|
|
| degrees = np.bincount(edges.reshape(-1), minlength=len(verts)) |
| endpoint_degrees = degrees[edges] |
| keep = np.any(endpoint_degrees >= min_endpoint_degree, axis=1) |
| return _compact_vertices_for_edges(verts, edges[keep]) |
|
|
|
|
| def _point_to_segments_distance(points: np.ndarray, seg_starts: np.ndarray, seg_ends: np.ndarray) -> np.ndarray: |
| dtype = np.result_type(points.dtype, seg_starts.dtype, seg_ends.dtype, np.float64) |
| points = points.astype(dtype, copy=False) |
| seg_starts = seg_starts.astype(dtype, copy=False) |
| seg_ends = seg_ends.astype(dtype, copy=False) |
|
|
| seg_vecs = seg_ends - seg_starts |
| seg_lens_sq = np.einsum("ij,ij->i", seg_vecs, seg_vecs) |
| offsets = points[:, None, :] - seg_starts[None, :, :] |
|
|
| t = np.zeros((len(points), len(seg_starts)), dtype=dtype) |
| valid = seg_lens_sq > 0 |
| if np.any(valid): |
| t[:, valid] = np.einsum("pkj,kj->pk", offsets[:, valid], seg_vecs[valid]) / seg_lens_sq[valid] |
| t = np.clip(t, 0.0, 1.0) |
|
|
| closest = seg_starts[None, :, :] + t[:, :, None] * seg_vecs[None, :, :] |
| return np.linalg.norm(points[:, None, :] - closest, axis=2) |
|
|
|
|
| def remove_edges_close_to_other_edges( |
| verts: np.ndarray, edges: np.ndarray, distance_threshold: float |
| ) -> tuple[np.ndarray, np.ndarray]: |
| if distance_threshold <= 0: |
| return verts, edges |
| if len(edges) == 0: |
| return verts[:0], _empty_edges_like(edges) |
|
|
| edge_segments = verts[edges] |
| edge_vecs = edge_segments[:, 1] - edge_segments[:, 0] |
| edge_lengths = np.linalg.norm(edge_vecs, axis=1) |
| edge_order = np.lexsort((np.arange(len(edges)), -edge_lengths)) |
|
|
| kept_edge_indices: list[int] = [] |
| for edge_idx in edge_order: |
| if len(kept_edge_indices) == 0: |
| kept_edge_indices.append(edge_idx) |
| continue |
|
|
| endpoints = edge_segments[edge_idx] |
| kept_segments = edge_segments[kept_edge_indices] |
| distances = _point_to_segments_distance(endpoints, kept_segments[:, 0], kept_segments[:, 1]) |
| if np.any(distances.mean(axis=0) <= distance_threshold): |
| continue |
| kept_edge_indices.append(edge_idx) |
|
|
| kept_edges = edges[sorted(kept_edge_indices)] |
| return _compact_vertices_for_edges(verts, kept_edges) |
|
|
|
|
| def post_process_wireframe( |
| verts: np.ndarray, |
| edges: np.ndarray, |
| merge_distance_threshold: float, |
| remove_solitary_edges: bool = False, |
| filter_by_length: bool = False, |
| project_near_horizontal_edges: bool = False, |
| snap_to_principal_directions: bool = False, |
| min_edge_length: float | None = 0.05, |
| max_edge_length: float | None = 100.0, |
| horizontal_angle_threshold_deg: float = 5.0, |
| principal_direction_angle_threshold_deg: float = 5.0, |
| intersection_angle_threshold_deg: float | None = 30.0, |
| gap_fill_candidate_edge_points: np.ndarray | None = None, |
| complete_symmetric_edges: bool = False, |
| symmetric_edge_distance_threshold: float = 0.2, |
| connectivity_filter_min_degree: int = 0, |
| ) -> tuple[np.ndarray, np.ndarray]: |
| verts, edges = merge_close_vertices( |
| verts, |
| edges, |
| merge_distance_threshold, |
| intersection_angle_threshold_deg=intersection_angle_threshold_deg, |
| ) |
| if complete_symmetric_edges: |
| verts, edges = add_edges_by_translated_edge_symmetry( |
| verts, |
| edges, |
| distance_threshold=symmetric_edge_distance_threshold, |
| ) |
| if filter_by_length: |
| verts, edges = filter_edges_by_length(verts, edges, min_edge_length, max_edge_length) |
| if project_near_horizontal_edges: |
| verts, edges = project_near_horizontal_edges_to_xz_plane( |
| verts, |
| edges, |
| horizontal_angle_threshold_deg=horizontal_angle_threshold_deg, |
| principal_direction_angle_threshold_deg=principal_direction_angle_threshold_deg, |
| ) |
| if snap_to_principal_directions: |
| verts, edges = snap_edges_to_principal_xz_directions( |
| verts, |
| edges, |
| horizontal_angle_threshold_deg=horizontal_angle_threshold_deg, |
| principal_direction_angle_threshold_deg=principal_direction_angle_threshold_deg, |
| ) |
| if remove_solitary_edges: |
| verts, edges = _remove_solitary_edges(verts, edges) |
| if gap_fill_candidate_edge_points is not None: |
| verts, edges = add_gap_fill_edges_from_candidates( |
| verts, |
| edges, |
| gap_fill_candidate_edge_points, |
| merge_distance_threshold, |
| ) |
| if connectivity_filter_min_degree > 1: |
| verts, edges = filter_edges_by_connectivity(verts, edges, min_endpoint_degree=connectivity_filter_min_degree) |
| return verts, edges |
|
|