Source code for sphedron.refine

# License: Non-Commercial Use Only
#
# Permission is granted to use, copy, modify, and distribute this software
# for non-commercial purposes only, with attribution to the original author.
# Commercial use requires explicit permission.
#
# This software is provided "as is", without warranty of any kind.
"""Triangle and rectangle subdivision algorithms for mesh refinement."""
from typing import Tuple, Literal
from numpy.typing import NDArray
import numpy as np


[docs] def split_edges( edge_extremes: NDArray, num_segments: int, use_angle: bool = False ): """ Splits edges defined by pairs of extremities into equally spaced points. Given an array of E pairs of extremities, this function returns new points that are equally distanced by edge_length/num_segments. If num_segments is set to 1, the function returns an empty array. When the extremities are located on a sphere, the new points can be split so that they are equally distanced by angle when use_angle is set to True, ensuring that the normalized points are also equally spaced. Args: edge_extremes: Extremities of shape (E, 2, 3). num_segments: Number of segments to split the edges into. use_angle: If set to True, the new points will be equally distanced by angle (arc length). Returns: Array of shape (E * (num_segments - 1), 3) containing the new points. """ if num_segments <= 1: return np.array([]) t = np.arange(1, num_segments) / num_segments if use_angle: # shape (E,) omegas = np.arccos( np.sum(edge_extremes[:, 0] * edge_extremes[:, 1], axis=-1) ) sin_om = np.sin(omegas)[:, None] u = np.sin(omegas[:, None] * (1 - t[None, :])) / sin_om v = np.sin(omegas[:, None] * t[None, :]) / sin_om u = u[:, :, None] # shape (E,num_segments-1, 1) v = v[:, :, None] # shape (E,num_segments-1, 1) else: # interpolation weights, shape (1, num_segments-1, 1) v = t[None, :, None] u = 1 - v # shape (E,ns-1,3) nodes_on_edges = u * edge_extremes[:, 0][:, None] # shape (E,ns-1,3) nodes_on_edges = nodes_on_edges + v * edge_extremes[:, 1][:, None] nodes_on_edges = nodes_on_edges.reshape(-1, 3) return nodes_on_edges
[docs] def refine_triangles( nodes: NDArray, triangles: NDArray, factor: int, use_angle: bool = False, ) -> Tuple[NDArray, NDArray]: """Refine a triangular mesh by subdividing each face. Vectorized implementation -- all edge splitting, interior computation, and connectivity wiring are batched into array operations with no per-face Python loop. Args: nodes: Coordinates of the mesh nodes, shape (N, 3). triangles: Triangular face indices, shape (T, 3). factor: Subdivision factor per edge. use_angle: Use angle-based (geodesic) interpolation. Returns: Tuple of (refined_nodes, refined_faces) where refined_nodes has shape (M, 3) and refined_faces has shape (T*factor^2, 3). """ if factor <= 1: return nodes, triangles nu = factor n_tri = triangles.shape[0] n_nodes = nodes.shape[0] # --- Unique edges --- edges = np.concatenate( [triangles[:, [0, 1]], triangles[:, [1, 2]], triangles[:, [0, 2]]], axis=0, ) edges_sorted = np.sort(edges, axis=1) edges_unique, inverse = np.unique( edges_sorted, axis=0, return_inverse=True ) n_edges = edges_unique.shape[0] # inverse maps each of the 3*n_tri directed edges to its unique index # Split into per-edge-type arrays: ab, bc, ac (each length n_tri) inv_ab = inverse[:n_tri] inv_bc = inverse[n_tri : 2 * n_tri] inv_ac = inverse[2 * n_tri :] # --- Node counts --- ns = nu - 1 # new nodes per edge n_interior = (nu - 1) * (nu - 2) // 2 # interior nodes per face total_new = n_edges * ns + n_tri * n_interior new_nodes = np.empty((n_nodes + total_new, 3)) new_nodes[:n_nodes] = nodes # --- Step 1: split all edges in one batch call --- edge_extremes = nodes[edges_unique] # (E, 2, 3) edge_node_coords = split_edges(edge_extremes, nu, use_angle) # shape (E*ns, 3) -- stored as E blocks of ns nodes each new_nodes[n_nodes : n_nodes + n_edges * ns] = edge_node_coords # --- Step 2: build per-face edge-node indices (vectorized) --- # For each triangle's 3 edges, compute the global indices of the ns # on-edge nodes, respecting direction. # # edge_base[e] = n_nodes + e * ns gives the start of edge e's nodes. # direction: if triangle edge (a,b) has a < b, nodes go forward; # if a > b, nodes go in reverse. edge_base = n_nodes + np.arange(n_edges) * ns # (E,) r = np.arange(ns) # (ns,) # For each face edge, the unsorted indices are edge_base[inv] + r # shape: (n_tri, ns) ab_idx = edge_base[inv_ab, None] + r[None, :] ac_idx = edge_base[inv_ac, None] + r[None, :] bc_idx = edge_base[inv_bc, None] + r[None, :] # Reverse where the triangle edge is "backwards" (a > b for that edge) ab_flip = triangles[:, 0] > triangles[:, 1] ac_flip = triangles[:, 0] > triangles[:, 2] bc_flip = triangles[:, 1] > triangles[:, 2] ab_idx[ab_flip] = ab_idx[ab_flip, ::-1] ac_idx[ac_flip] = ac_idx[ac_flip, ::-1] bc_idx[bc_flip] = bc_idx[bc_flip, ::-1] # --- Step 3: interior nodes (vectorized across faces) --- interior_start = n_nodes + n_edges * ns if n_interior > 0: # For each face f, interior nodes occupy indices # interior_start + f*n_interior ... interior_start + (f+1)*n_interior # # Interior row i (1 <= i <= ns-1) has i points interpolated between # ab[i] and ac[i]. We precompute all (row_index, t_value) pairs and # do a single batched interpolation. # # Total interior points per face = sum(1..ns-1) = n_interior # Build arrays: row_i[k] = which row (1..ns-1) this point belongs to # t_val[k] = interpolation parameter in (0, 1) row_ids = [] t_vals = [] for i in range(1, ns): t = np.arange(1, i + 1) / (i + 1) # i values in (0,1) row_ids.append(np.full(i, i, dtype=np.intp)) t_vals.append(t) row_ids = np.concatenate(row_ids) # (n_interior,) t_vals = np.concatenate(t_vals) # (n_interior,) # Gather the endpoint coords for all faces at once # ab_idx[:, row_ids] -> (n_tri, n_interior) indices into new_nodes ab_pts = new_nodes[ab_idx[:, row_ids]] # (n_tri, n_interior, 3) ac_pts = new_nodes[ac_idx[:, row_ids]] # (n_tri, n_interior, 3) # Interpolate: shape (n_interior,) broadcast over (n_tri, n_interior, 3) t_b = t_vals[None, :, None] # (1, n_interior, 1) if use_angle: dots = np.sum(ab_pts * ac_pts, axis=-1, keepdims=True) omegas = np.arccos(np.clip(dots, -1, 1)) # (n_tri, n_interior, 1) sin_om = np.sin(omegas) u = np.sin(omegas * (1 - t_b)) / sin_om v = np.sin(omegas * t_b) / sin_om face_interior = u * ab_pts + v * ac_pts else: face_interior = (1 - t_b) * ab_pts + t_b * ac_pts new_nodes[interior_start : interior_start + n_tri * n_interior] = ( face_interior.reshape(-1, 3) ) # --- Step 4: connectivity template (vectorized) --- template = triangle_template(nu) ordering = triangles_order(nu) reordered_template = ordering[template] # (nu^2, 3) # Build the sorted_nodes array for ALL faces at once: (n_tri, n_verts) # where n_verts = 3 + 3*ns + n_interior # Order: [corner0, corner1, corner2, ab_nodes, ac_nodes, bc_nodes, interior] face_interior_idx = ( interior_start + np.arange(n_tri)[:, None] * n_interior + np.arange(n_interior)[None, :] ) # (n_tri, n_interior) sorted_nodes = np.concatenate( [triangles, ab_idx, ac_idx, bc_idx, face_interior_idx], axis=1 ) # (n_tri, n_verts) # Apply template to all faces at once sub_triangles = sorted_nodes[:, reordered_template.ravel()].reshape( n_tri * nu**2, 3 ) # --- Normalize --- new_nodes /= np.linalg.norm(new_nodes, axis=1, keepdims=True) return new_nodes, sub_triangles
def _refine_triangles_reference( nodes: NDArray, triangles: NDArray, factor: int, use_angle: bool = False, ) -> Tuple[NDArray, NDArray]: """Reference (non-vectorized) implementation of refine_triangles. Kept for correctness validation. See :func:`refine_triangles` for the optimized version. """ if factor <= 1: return nodes, triangles edges = np.concatenate( [triangles[:, [0, 1]], triangles[:, [1, 2]], triangles[:, [0, 2]]], axis=0, ) edges = np.unique(np.sort(edges, axis=1), axis=0) n_triangles = triangles.shape[0] n_nodes = nodes.shape[0] n_edges = edges.shape[0] sub_triangles = np.empty((n_triangles * factor**2, 3), dtype=int) new_nodes = np.concatenate( [ nodes, np.empty( ( n_edges * (factor - 1) + n_triangles * (factor - 1) * (factor - 2) // 2, 3, ) ), ], axis=0, ) edge_index = {} for i in range(n_edges): edge_index[(edges[i, 0], edges[i, 1])] = i edge_index[(edges[i, 1], edges[i, 0])] = i template = triangle_template(factor) ordering = triangles_order(factor) reordered_template = ordering[template] edge_extremes = nodes[edges] nodes_on_edges = split_edges( edge_extremes, num_segments=factor, use_angle=use_angle, ) new_nodes[n_nodes : n_nodes + n_edges * (factor - 1)] = nodes_on_edges r = np.arange(factor - 1) + n_nodes start_idx = n_edges * (factor - 1) + n_nodes num_inside_nodes = (factor - 1) * (factor - 2) // 2 ref_triangle = np.arange(start_idx, num_inside_nodes + start_idx) for triangle_idx in range(n_triangles): triangle_nodes = ref_triangle + triangle_idx * num_inside_nodes triangle = triangles[triangle_idx] e_ab = (triangle[0], triangle[1]) e_ac = (triangle[0], triangle[2]) e_bc = (triangle[1], triangle[2]) sorted_ab = (edge_index[e_ab] * (factor - 1) + r)[:: e_direction(e_ab)] sorted_ac = (edge_index[e_ac] * (factor - 1) + r)[:: e_direction(e_ac)] sorted_bc = (edge_index[e_bc] * (factor - 1) + r)[:: e_direction(e_bc)] sorted_nodes = np.r_[ triangle, sorted_ab, sorted_ac, sorted_bc, triangle_nodes, ] sub_triangles[ triangle_idx * factor**2 : (triangle_idx + 1) * factor**2, : ] = sorted_nodes[reordered_template] new_nodes[triangle_nodes, :] = triangle_interior( new_nodes[sorted_ab, :], new_nodes[sorted_ac, :], use_angle=use_angle, ) new_nodes = new_nodes / np.linalg.norm(new_nodes, axis=1, keepdims=True) return (new_nodes, sub_triangles)
[docs] def triangle_template(nu: int) -> NDArray[np.int64]: r"""Template for linking subfaces in a subdivision of a triangular face. Returns faces with node indexing given by reading order. Illustration for nu=4:: 0 / \ 1---2 / \ / \ 3---4---5 / \ / \ / \ 6---7---8---9 / \ / \ / \ / \ 10--11--12--13--14 Args: nu: Subdivision factor. Returns: Faces template of shape (nu^2, 3). """ # Build all upward and downward triangles using vectorized index math. # Row i of the triangle grid has nodes node0..node0+i where # node0 = i*(i+1)//2. Row i has i upward + i downward + 1 tip triangle. rows = np.arange(nu) node0 = rows * (rows + 1) // 2 # start index of each row # Pre-allocate faces = np.empty((nu**2, 3), dtype=np.intp) idx = 0 for i in range(nu): n0 = node0[i] s = i + 1 if i > 0: j = np.arange(i) # Upward triangles faces[idx : idx + i, 0] = j + n0 faces[idx : idx + i, 1] = j + n0 + s faces[idx : idx + i, 2] = j + n0 + s + 1 idx += i # Downward triangles faces[idx : idx + i, 0] = j + n0 faces[idx : idx + i, 1] = j + n0 + s + 1 faces[idx : idx + i, 2] = j + n0 + 1 idx += i # Rightmost tip triangle faces[idx] = [i + n0, i + n0 + s, i + n0 + s + 1] idx += 1 return faces
[docs] def triangles_order(nu: int): r"""Permutation that reorders reading-order node indices. Reorders into: corners first, then on-edge nodes, then interior nodes. Illustration for nu=4:: 0 / \ 3---6 / \ / \ 4---12--7 / \ / \ / \ 5---13--14--8 / \ / \ / \ / \ 1---9--10--11---2 Args: nu: Subdivision factor. Returns: Ordering array of length (nu+1)*(nu+2)//2. """ n_verts = (nu + 1) * (nu + 2) // 2 o = np.empty(n_verts, dtype=np.intp) left = np.arange(3, nu + 2) right = np.arange(nu + 2, 2 * nu + 1) bottom = np.arange(2 * nu + 1, 3 * nu) inside = np.arange(3 * nu, n_verts) pos = 0 o[pos] = 0 # top corner pos += 1 for i in range(nu - 1): o[pos] = left[i] pos += 1 n_inside_row = i # row i has i interior nodes if n_inside_row > 0: o[pos : pos + n_inside_row] = inside[ i * (i - 1) // 2 : i * (i + 1) // 2 ] pos += n_inside_row o[pos] = right[i] pos += 1 # Bottom row: corner 1, bottom edges, corner 2 o[pos] = 1 pos += 1 n_bottom = len(bottom) o[pos : pos + n_bottom] = bottom pos += n_bottom o[pos] = 2 return o
[docs] def triangle_interior(ab: NDArray, ac: NDArray, use_angle: bool = False): r"""Compute interior nodes for a subdivided triangular face. Given on-edge nodes AB[i] and AC[i], computes the interior nodes (marked by ``*`` below):: A / \ AB0--AC0 / \ / \ AB1--*--AC1 / \ / \ / \ AB2---*---*---AC2 / \ / \ / \ / \ B-BC1--BC2--BC3-C Args: ab: On-edge nodes from A toward B, shape (factor-1, 3). ac: On-edge nodes from A toward C, shape (factor-1, 3). use_angle: Use angle-based (geodesic) interpolation between corresponding AB and AC nodes. Returns: Interior node coordinates, shape ((factor-1)*(factor-2)//2, 3). Returns None when factor <= 2 (no interior nodes). """ if ab.shape[0] <= 1: return None nodes = [] for i in range(1, ab.shape[0]): nodes.append( split_edges( np.concatenate([ab[None, [i]], ac[None, [i]]], axis=1), i + 1, use_angle, ) ) all_nodes = np.concatenate(nodes, axis=0) return all_nodes
[docs] def refine_rectrangles( nodes: NDArray, rectangles: NDArray, factor: int, use_angle: bool = False, ) -> Tuple[NDArray, NDArray]: """Refine a rectangular mesh by subdividing each face. Vectorized implementation -- all edge splitting, interior computation, and connectivity wiring are batched into array operations with no per-face Python loop. Args: nodes: Coordinates of the mesh nodes, shape (N, 3). rectangles: Rectangular face indices, shape (R, 4). factor: Subdivision factor per edge. use_angle: Use angle-based (geodesic) interpolation. Returns: Tuple of (refined_nodes, refined_faces) where refined_nodes has shape (M, 3) and refined_faces has shape (R*factor^2, 4). """ if factor <= 1: return nodes, rectangles nu = factor ns = nu - 1 # new nodes per edge n_rect = rectangles.shape[0] n_nodes = nodes.shape[0] # --- Unique edges --- # 4 edges per rect: AB(0-1), BC(1-2), CD(2-3), AD(0-3) all_edges = np.concatenate( [ rectangles[:, [0, 1]], rectangles[:, [1, 2]], rectangles[:, [2, 3]], rectangles[:, [0, 3]], ], axis=0, ) edges_sorted = np.sort(all_edges, axis=1) edges_unique, inverse = np.unique( edges_sorted, axis=0, return_inverse=True ) n_edges = edges_unique.shape[0] # Split inverse into per-edge-type arrays (each length n_rect) inv_ab = inverse[:n_rect] inv_bc = inverse[n_rect : 2 * n_rect] inv_cd = inverse[2 * n_rect : 3 * n_rect] inv_ad = inverse[3 * n_rect :] # --- Node counts --- n_interior = ns**2 # interior nodes per face total_new = n_edges * ns + n_rect * n_interior new_nodes = np.empty((n_nodes + total_new, 3)) new_nodes[:n_nodes] = nodes # --- Step 1: split all edges in one batch call --- edge_extremes = nodes[edges_unique] # (E, 2, 3) new_nodes[n_nodes : n_nodes + n_edges * ns] = split_edges( edge_extremes, nu, use_angle ) # --- Step 2: per-face edge-node indices (vectorized) --- edge_base = n_nodes + np.arange(n_edges) * ns # (E,) r = np.arange(ns) # (ns,) # shape: (n_rect, ns) ab_idx = edge_base[inv_ab, None] + r[None, :] bc_idx = edge_base[inv_bc, None] + r[None, :] cd_idx = edge_base[inv_cd, None] + r[None, :] ad_idx = edge_base[inv_ad, None] + r[None, :] # Reverse where the rect edge is "backwards" (a > b) ab_flip = rectangles[:, 0] > rectangles[:, 1] bc_flip = rectangles[:, 1] > rectangles[:, 2] cd_flip = rectangles[:, 2] > rectangles[:, 3] ad_flip = rectangles[:, 0] > rectangles[:, 3] ab_idx[ab_flip] = ab_idx[ab_flip, ::-1] bc_idx[bc_flip] = bc_idx[bc_flip, ::-1] cd_idx[cd_flip] = cd_idx[cd_flip, ::-1] ad_idx[ad_flip] = ad_idx[ad_flip, ::-1] # --- Step 3: interior nodes (vectorized) --- # Rectangle interior: for each row i (0..ns-1), interpolate ns points # between AD[i] and BC[i]. All rows use the same num_segments = ns+1. # So we can batch ALL rows of ALL faces into one split_edges call. interior_start = n_nodes + n_edges * ns if n_interior > 0: # ad_idx[:, i] and bc_idx[:, i] are the endpoints for row i # We need all (n_rect * ns) pairs, each split into ns+1 segments. # ad_idx shape: (n_rect, ns), bc_idx shape: (n_rect, ns) ad_pts = new_nodes[ad_idx] # (n_rect, ns, 3) bc_pts = new_nodes[bc_idx] # (n_rect, ns, 3) # Reshape to (n_rect * ns, 2, 3) for a single split_edges call pairs = np.stack( [ad_pts.reshape(-1, 3), bc_pts.reshape(-1, 3)], axis=1 ) interior_flat = split_edges(pairs, ns + 1, use_angle) # shape: (n_rect * ns * ns, 3) # Reshape to (n_rect, ns, ns, 3) -> face-major order interior = interior_flat.reshape(n_rect, ns, ns, 3) # Flatten interior dims: (n_rect, ns^2, 3) interior = interior.reshape(n_rect, n_interior, 3) new_nodes[interior_start : interior_start + n_rect * n_interior] = ( interior.reshape(-1, 3) ) # --- Step 4: connectivity template (vectorized) --- template = rectangle_template(nu) ordering = rectangles_order(nu) reordered_template = ordering[template] # (nu^2, 4) # Build sorted_nodes for ALL faces: (n_rect, n_verts) # Order: [4 corners, ab_nodes, ad_nodes, bc_nodes, cd_nodes, interior] face_interior_idx = ( interior_start + np.arange(n_rect)[:, None] * n_interior + np.arange(n_interior)[None, :] ) # (n_rect, n_interior) sorted_nodes = np.concatenate( [rectangles, ab_idx, ad_idx, bc_idx, cd_idx, face_interior_idx], axis=1, ) # (n_rect, n_verts) # Apply template to all faces at once subrects = sorted_nodes[:, reordered_template.ravel()].reshape( n_rect * nu**2, 4 ) # --- Normalize --- new_nodes /= np.linalg.norm(new_nodes, axis=1, keepdims=True) return new_nodes, subrects
def _refine_rectrangles_reference( nodes: NDArray, rectangles: NDArray, factor: int, use_angle: bool = False, ) -> Tuple[NDArray, NDArray]: """Reference (non-vectorized) implementation of refine_rectrangles. Kept for correctness validation. See :func:`refine_rectrangles` for the optimized version. """ if factor <= 1: return nodes, rectangles edges = np.concatenate( [ rectangles[:, [0, 1]], rectangles[:, [1, 2]], rectangles[:, [2, 3]], rectangles[:, [3, 0]], ], axis=0, ) edges = np.unique(np.sort(edges, axis=1), axis=0) num_rects = rectangles.shape[0] num_nodes = nodes.shape[0] num_edges = edges.shape[0] subrects = np.empty((num_rects * factor**2, 4), dtype=int) new_nodes = np.empty( ( num_nodes + num_edges * (factor - 1) + num_rects * (factor - 1) ** 2, 3, ) ) new_nodes[:num_nodes] = nodes edge_index = {} for i in range(num_edges): edge_index[tuple(edges[i])] = i edge_index[tuple(edges[i][::-1])] = i template = rectangle_template(factor) ordering = rectangles_order(factor) reordered_template = ordering[template] edge_extremes = nodes[edges] new_nodes[num_nodes : num_nodes + num_edges * (factor - 1)] = split_edges( edge_extremes, num_segments=factor, use_angle=use_angle, ) r = np.arange(factor - 1) + num_nodes start_idx = num_edges * (factor - 1) + num_nodes num_inside_nodes = (factor - 1) ** 2 ref_rect = np.arange(start_idx, num_inside_nodes + start_idx) for f in range(num_rects): rect_nodes = ref_rect + f * num_inside_nodes rectangle = rectangles[f] e_ab = rectangle[[0, 1]] e_bc = rectangle[[1, 2]] e_cd = rectangle[[2, 3]] e_ad = rectangle[[0, 3]] sorted_ab = (edge_index[tuple(e_ab)] * (factor - 1) + r)[ :: e_direction(e_ab) ] sorted_bc = (edge_index[tuple(e_bc)] * (factor - 1) + r)[ :: e_direction(e_bc) ] sorted_cd = (edge_index[tuple(e_cd)] * (factor - 1) + r)[ :: e_direction(e_cd) ] sorted_ad = (edge_index[tuple(e_ad)] * (factor - 1) + r)[ :: e_direction(e_ad) ] sorted_nodes = np.r_[ rectangles[f], sorted_ab, sorted_ad, sorted_bc, sorted_cd, rect_nodes, ] subrects[f * factor**2 : (f + 1) * factor**2, :] = sorted_nodes[ reordered_template ] new_nodes[rect_nodes, :] = rectangle_interior( new_nodes[sorted_ad, :], new_nodes[sorted_bc, :], use_angle=use_angle, ) new_nodes = new_nodes / np.linalg.norm(new_nodes, axis=1, keepdims=True) return (new_nodes, subrects)
[docs] def rectangle_template(nu: int) -> NDArray[np.int64]: r"""Template for linking subfaces in a subdivision of a rectangular face. Returns faces with node indexing given by reading order. Illustration for nu=3:: 0---1---2---3 | | | | 4---5---6---7 | | | | 8---9---10--11 | | | | 12--13--14--15 Args: nu: Subdivision factor. Returns: Faces template of shape (nu^2, 4). """ i, j = np.meshgrid(np.arange(nu), np.arange(nu), indexing="ij") i = i.ravel() j = j.ravel() row0 = i * (nu + 1) row1 = (i + 1) * (nu + 1) faces = np.column_stack([row0 + j, row0 + j + 1, row1 + j + 1, row1 + j]) return faces
[docs] def rectangles_order(nu: int): r"""Permutation that reorders reading-order node indices for rectangles. Reorders into: corners first, then on-edge nodes, then interior nodes. Illustration for nu=3:: 0---4---5---1 | | | | 6---12--13--8 | | | | 7---14--15--9 | | | | 3---11--10--2 Args: nu: Subdivision factor. Returns: Ordering array of length (nu+1)^2. """ top = np.arange(4, nu + 3) left = np.arange(nu + 3, 2 * nu + 2) right = np.arange(2 * nu + 2, 3 * nu + 1) bottom = np.arange(3 * nu + 1, 4 * nu)[::-1] inside = np.arange(4 * nu, (nu + 1) ** 2).reshape(nu - 1, nu - 1) n_verts = (nu + 1) ** 2 o = np.empty(n_verts, dtype=np.intp) pos = 0 # Top row: corner 0, top edge, corner 1 o[pos] = 0 pos += 1 o[pos : pos + len(top)] = top pos += len(top) o[pos] = 1 pos += 1 # Middle rows: left edge, interior, right edge for i in range(nu - 1): o[pos] = left[i] pos += 1 o[pos : pos + nu - 1] = inside[i] pos += nu - 1 o[pos] = right[i] pos += 1 # Bottom row: corner 3, bottom edge (reversed), corner 2 o[pos] = 3 pos += 1 o[pos : pos + len(bottom)] = bottom pos += len(bottom) o[pos] = 2 return o
[docs] def rectangle_interior(ad: NDArray, bc: NDArray, use_angle: bool = False): """Compute interior nodes for a subdivided rectangular face. Given on-edge nodes AD[i] and BC[i], computes the interior nodes (marked by ``*`` below):: A---AB0--AB1--AB2--B | | | | | AD0--*----*----*---BC0 | | | | | AD1--*----*----*---BC1 | | | | | AD2--*----*----*---BC2 | | | | | D---DC0--DC1--DC2--C Args: ad: On-edge nodes from A toward D, shape (factor-1, 3). bc: On-edge nodes from B toward C, shape (factor-1, 3). use_angle: Use angle-based (geodesic) interpolation. Returns: Interior node coordinates, shape ((factor-1)^2, 3). """ assert ad.shape[0] == bc.shape[0] extremities = np.concatenate([ad[:, None], bc[:, None]], axis=1) return split_edges( extremities, num_segments=ad.shape[0] + 1, use_angle=use_angle, )
[docs] def e_direction(edge) -> Literal[-1, 1]: """Determines the direction of an edge based on its node indices. Args: edge: A tuple containing the indices of the edge nodes. Returns: -1 if edge[0] > edge[1], 1 otherwise. """ return 1 - 2 * (edge[0] > edge[1])