386 lines
14 KiB
Python
386 lines
14 KiB
Python
"""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")
|