"""Surface modifier for OpenCASCADE geometry. Applies geometric patterns (pyramids, bumps, grooves) to 3D surfaces using boolean operations. This enables grip-enhancing textures and visual surface modifications on CAD models. """ from __future__ import annotations import logging import math from typing import Any, Optional, Tuple # OCC imports at module level for common types logger = logging.getLogger(__name__) class SurfaceModifier: """Applies geometric patterns to 3D surfaces using OCC boolean operations.""" def __init__(self): self._patterns_applied = [] def apply_pyramid_pattern( self, face_shape, pyramid_height: float = 1.0, base_radius: float = 2.0, spacing: float = 5.0, num_rings: Optional[int] = None, direction: Tuple[float, float, float] = (0, 0, 1), ) -> Optional[Any]: """Apply a pyramid pattern to a face surface. Args: face_shape: OCC TopoDS_Shape representing the face or solid pyramid_height: Height of each pyramid base_radius: Radius of pyramid base spacing: Distance between pyramids num_rings: Number of concentric rings (auto-calculated if None) direction: Normal direction for pyramids Returns: Modified shape on success, None on failure """ try: from OCP.TopAbs import TopAbs_FACE from OCP.TopoDS import TopoDS_Face, TopoDS_Shape from OCP.BRepAlgoAPI import BRepAlgoAPI_Fuse from OCP.TopExp import TopExp_Explorer from OCP.BRepAdaptor import BRepAdaptor_Surface # Validate face shape if not isinstance(face_shape, (TopoDS_Shape, TopoDS_Face)): logger.error("Invalid face shape type") return None # Extract the first face for surface parameterization if isinstance(face_shape, TopoDS_Shape): explorer = TopExp_Explorer(face_shape, TopAbs_FACE) if not explorer.More(): logger.error("No faces found in shape") return None from OCP import TopoDS face = TopoDS.TopoDS.Face_s(explorer.Current()) else: face = face_shape # Get face surface for UV parameterization surf = BRepAdaptor_Surface(face) u_min, u_max = surf.FirstUParameter(), surf.LastUParameter() v_min, v_max = surf.FirstVParameter(), surf.LastVParameter() # Calculate number of rings if not specified if num_rings is None: # Estimate based on face area and spacing u_range = u_max - u_min v_range = v_max - v_min avg_dim = (u_range + v_range) / 2.0 num_rings = max(1, min(int(avg_dim / spacing), 5)) logger.info( f"Applying pyramid pattern: {num_rings} rings, " f"{base_radius:.2f} radius, {pyramid_height:.2f} height" ) # Create pyramids distributed across the face UV space result_shape = face_shape pyramid_count = 0 for ring_idx in range(num_rings): # Distribute rings evenly across UV parameter space u_fraction = (ring_idx + 1) / (num_rings + 1) v_fraction = 0.5 # Center vertically # Map to actual UV coordinates on the face u_pos = u_min + u_fraction * (u_max - u_min) v_pos = v_min + v_fraction * (v_max - v_min) # Get 3D position and tangent vectors at this UV point from OCP.gp import gp_Pnt, gp_Vec center_pt = gp_Pnt() d1u = gp_Vec() d1v = gp_Vec() surf.D1(u_pos, v_pos, center_pt, d1u, d1v) # Normal is cross product of tangent vectors normal = d1u.Crossed(d1v) normal.Normalize() # Calculate number of pyramids in this ring based on spacing if ring_idx == 0: num_pyramids = 1 # Center pyramid else: circumference = 2.0 * math.pi * (ring_idx * spacing) num_pyramids = max(3, int(circumference / spacing)) for i in range(num_pyramids): if ring_idx == 0: # Center pyramid - place at face center place_u = u_pos place_v = v_pos else: angle = (2.0 * math.pi * i) / num_pyramids # Offset in UV space based on ring radius offset_u = (ring_idx * spacing / (u_max - u_min)) * math.cos(angle) offset_v = (ring_idx * spacing / (v_max - v_min)) * math.sin(angle) place_u = max(u_min, min(u_max, u_pos + offset_u)) place_v = max(v_min, min(v_max, v_pos + offset_v)) try: # Get 3D position and normal for this pyramid pyramid_pt = gp_Pnt() pd1u = gp_Vec() pd1v = gp_Vec() surf.D1(place_u, place_v, pyramid_pt, pd1u, pd1v) pyramid_normal = pd1u.Crossed(pd1v) pyramid_normal.Normalize() # Create solid pyramid at this position pyramid_shape = self._create_solid_pyramid( pyramid_pt, pyramid_normal, pyramid_height, base_radius, ) if pyramid_shape is not None: # Fuse with existing geometry fuse = BRepAlgoAPI_Fuse(result_shape, pyramid_shape) fuse.Build() if fuse.IsDone(): result_shape = fuse.Shape() pyramid_count += 1 else: logger.warning( f"Failed to fuse pyramid at ({place_u:.2f}, {place_v:.2f})" ) except Exception as e: logger.debug( f"Error creating pyramid at ring {ring_idx}, pyramid {i}: {e}" ) self._patterns_applied.append( { "type": "pyramid", "parameters": { "height": pyramid_height, "base_radius": base_radius, "spacing": spacing, "num_rings": num_rings, "direction": direction, }, } ) logger.info(f"Successfully applied {pyramid_count} pyramids") return result_shape except Exception as e: logger.error(f"Error applying pyramid pattern: {e}", exc_info=True) return None def _create_solid_pyramid( self, base_point, # gp_Pnt - position on the face normal_vec, # gp_Dir or gp_Vec - surface normal direction height: float, base_radius: float, ) -> Optional[Any]: """Create a solid pyramid at the specified position and orientation. Uses BRepPrimAPI_MakePrism to extrude a square base into a solid pyramid. Args: base_point: 3D point where pyramid base is centered normal_vec: Direction vector for pyramid growth (surface normal) height: Height of the pyramid from base to apex base_radius: Half-width of the square base Returns: OCC solid shape for the pyramid, or None on failure """ try: from OCP.gp import gp_Dir, gp_Ax2, gp_Vec from OCP.BRepBuilderAPI import ( BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeWire, ) from OCP.BRepPrimAPI import BRepPrimAPI_MakePrism half = base_radius / 2.0 # Build orthonormal basis from normal vector if isinstance(normal_vec, gp_Vec): n_dir = gp_Dir(normal_vec.XYZ()) else: n_dir = normal_vec # Create a local coordinate system at the base point local_ax2 = gp_Ax2(base_point, n_dir) # Get X and Y axes from the local coordinate system x_dir = local_ax2.XDirection() y_dir = local_ax2.YDirection() # Create 4 corners of the square base in the local plane corner_points = [ base_point + gp_Vec(x_dir).Multiplied(half) + gp_Vec(y_dir).Multiplied(half), base_point + gp_Vec(x_dir).Multiplied(-half) + gp_Vec(y_dir).Multiplied(half), base_point + gp_Vec(x_dir).Multiplied(-half) + gp_Vec(y_dir).Multiplied(-half), base_point + gp_Vec(x_dir).Multiplied(half) + gp_Vec(y_dir).Multiplied(-half), ] # Create edges connecting the corners wire_maker = BRepBuilderAPI_MakeWire() for idx in range(4): next_idx = (idx + 1) % 4 edge = BRepBuilderAPI_MakeEdge( corner_points[idx], corner_points[next_idx] ).Edge() wire_maker.Add(edge) if not wire_maker.IsDone(): logger.warning("Failed to create pyramid base wire") return None # Extrude the base wire in the normal direction by height to form a prism extrusion_vec = gp_Vec(n_dir).Multiplied(height) prism_maker = BRepPrimAPI_MakePrism( wire_maker.Wire(), extrusion_vec, False # no check intersection ) prism_maker.Build() if not prism_maker.IsDone(): logger.warning("Failed to create pyramid prism") return None return prism_maker.Shape() except Exception as e: logger.debug(f"Error creating solid pyramid: {e}") return None def apply_bump_pattern( self, face_shape, bump_height: float = 1.0, bump_radius: float = 2.0, spacing: float = 5.0, num_rings: Optional[int] = None, ) -> Optional[Any]: """Apply a simple bump pattern to a face surface. Args: face_shape: OCC TopoDS_Shape representing the face bump_height: Height of each bump bump_radius: Radius of each bump base spacing: Distance between bumps num_rings: Number of concentric rings Returns: Modified shape on success, None on failure """ return self.apply_pyramid_pattern( face_shape, pyramid_height=bump_height, base_radius=bump_radius, spacing=spacing, num_rings=num_rings, ) def apply_surface_modifier_to_body( body_geometry, modifier_type: str = "pyramid", **parameters ) -> Optional[Any]: """Apply a surface modifier to a body geometry. Args: body_geometry: OCCGeometryObject or similar geometry object modifier_type: Type of modifier ('pyramid', 'bump') **parameters: Modifier-specific parameters Returns: Modified shape, or None on failure """ from fluency.geometry_occ.kernel import OCGeometryKernel kernel = OCGeometryKernel() shape = kernel._get_shape(body_geometry) if shape is None: logger.error("No geometry found in body") return None modifier = SurfaceModifier() try: if modifier_type == "pyramid": success = modifier.apply_pyramid_pattern(shape, **parameters) elif modifier_type == "bump": success = modifier.apply_bump_pattern(shape, **parameters) else: logger.error(f"Unknown modifier type: {modifier_type}") return None if not success: logger.error("Surface modifier application failed") return None # Return the modified shape wrapped in OCCGeometryObject from fluency.geometry_occ.kernel import OCCGeometryObject return OCCGeometryObject(shape) except Exception as e: logger.error(f"Error applying surface modifier: {e}", exc_info=True) return None # Example usage and testing if __name__ == "__main__": # Create a simple test case from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox # Create a box to modify box_maker = BRepPrimAPI_MakeBox(50, 50, 10) box_maker.Build() if box_maker.IsDone(): print("Created test box") # Apply pyramid pattern to top face (Z direction) modifier = SurfaceModifier() success = modifier.apply_pyramid_pattern( box_maker.Shape(), pyramid_height=2.0, base_radius=3.0, spacing=8.0, num_rings=2, direction=(0, 0, 1), ) if success: print("Successfully applied pyramid pattern") # Export modified shape from OCP.StlAPI import StlAPI_Writer from OCP.BRepMesh import BRepMesh_IncrementalMesh tess = BRepMesh_IncrementalMesh(box_maker.Shape(), 0.1) tess.Perform() writer = StlAPI_Writer() writer.SetASCIIMode(False) writer.Write(box_maker.Shape(), "/tmp/test_pyramid_pattern.stl") print("Exported modified shape to STL") else: print("Failed to apply pyramid pattern") else: print("Failed to create test box")