Source code for yapcad.package.analysis.gmsh_mesher

"""Gmsh meshing integration for yapCAD analysis.

This module provides meshing capabilities using Gmsh's OCC integration,
enabling direct geometry transfer from yapCAD's OCC-based BREP representation.

The key advantage is that both yapCAD and Gmsh use the OpenCASCADE kernel,
so geometry can be passed directly without lossy STEP/IGES conversions.

Usage:
    from yapcad.package.analysis.gmsh_mesher import GmshMesher, MeshHints

    mesher = GmshMesher()
    mesh = mesher.mesh_from_solid(solid, hints=MeshHints(element_size=2.0))
    mesher.export_mesh(workspace / "model.msh")

Copyright (c) 2025 yapCAD contributors
MIT License
"""

from __future__ import annotations

import tempfile
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

# Gmsh import with graceful fallback
try:
    import gmsh
    _GMSH_AVAILABLE = True
except ImportError:
    gmsh = None  # type: ignore
    _GMSH_AVAILABLE = False

# OCC imports for geometry conversion
try:
    from OCC.Core.TopoDS import TopoDS_Shape, TopoDS_Solid
    from OCC.Core.BRepTools import breptools
    _OCC_AVAILABLE = True
except ImportError:
    TopoDS_Shape = TopoDS_Solid = Any
    breptools = None
    _OCC_AVAILABLE = False


[docs] def gmsh_available() -> bool: """Return True if Gmsh Python API is available.""" return _GMSH_AVAILABLE
[docs] def require_gmsh() -> None: """Raise error if Gmsh is not available.""" if not _GMSH_AVAILABLE: raise RuntimeError( "Gmsh Python API is not available. Install via conda: " "conda install -c conda-forge gmsh" )
[docs] @dataclass class MeshHints: """Mesh generation hints for Gmsh. Attributes: element_size: Target element size (mesh density) min_element_size: Minimum element size max_element_size: Maximum element size algorithm_2d: 2D meshing algorithm (1=MeshAdapt, 2=Auto, 5=Delaunay, 6=Frontal-Delaunay) algorithm_3d: 3D meshing algorithm (1=Delaunay, 4=Frontal, 10=HXT) element_order: Element polynomial order (1=linear, 2=quadratic) optimize: Whether to optimize mesh quality optimize_netgen: Use Netgen optimizer for 3D meshes refinement_fields: List of refinement field specifications geometry_tolerance: Tolerance for geometry healing (default 1e-4) recover_3d: If False, skip 3D mesh generation and do 2D only stl_fallback: If True, use STL intermediate format when BREP fails """ element_size: float = 5.0 min_element_size: Optional[float] = None max_element_size: Optional[float] = None algorithm_2d: int = 6 # Frontal-Delaunay algorithm_3d: int = 1 # Delaunay element_order: int = 1 optimize: bool = True optimize_netgen: bool = False refinement_fields: List[Dict[str, Any]] = field(default_factory=list) geometry_tolerance: float = 1e-4 recover_3d: bool = True use_stl: bool = False # Use STL-based meshing (robust but loses exact geometry) scale_factor: float = 1.0 # Scale geometry for better precision (results scaled back)
[docs] @dataclass class PhysicalGroup: """A named group of mesh entities (for boundary conditions). Attributes: name: Human-readable name for the group dim: Dimension (0=point, 1=edge, 2=face, 3=volume) tags: Gmsh entity tags in this group """ name: str dim: int tags: List[int]
[docs] class GmshMesher: """Primary meshing interface using Gmsh's OCC integration. This class provides meshing capabilities for yapCAD solids using Gmsh. It leverages the shared OCC kernel between yapCAD and Gmsh for direct geometry transfer without intermediate file formats. Example: mesher = GmshMesher() mesher.initialize() mesher.import_solid(solid) mesher.set_physical_groups({"fixed_face": [1, 2], "load_face": [3]}) mesher.generate_mesh(hints) mesher.export_mesh(Path("output.msh")) mesher.finalize() """ def __init__(self, model_name: str = "yapCAD_model"): """Initialize the mesher. Args: model_name: Name for the Gmsh model """ require_gmsh() self._model_name = model_name self._initialized = False self._physical_groups: Dict[str, PhysicalGroup] = {} self._face_map: Dict[int, str] = {} # Gmsh tag -> name
[docs] def initialize(self, verbose: bool = True, geometry_tolerance: float = 0.1) -> None: """Initialize Gmsh (must be called before other operations). Args: verbose: If True, enable terminal output for progress monitoring geometry_tolerance: Tolerance for geometry repair operations """ if self._initialized: return gmsh.initialize() gmsh.model.add(self._model_name) # Enable terminal output for progress monitoring gmsh.option.setNumber("General.Terminal", 1 if verbose else 0) gmsh.option.setNumber("General.Verbosity", 5 if verbose else 0) # Max verbosity # Set geometry healing options BEFORE importing geometry gmsh.option.setNumber("Geometry.Tolerance", geometry_tolerance) gmsh.option.setNumber("Geometry.ToleranceBoolean", geometry_tolerance) gmsh.option.setNumber("Geometry.OCCFixDegenerated", 1) gmsh.option.setNumber("Geometry.OCCFixSmallEdges", 1) gmsh.option.setNumber("Geometry.OCCFixSmallFaces", 1) gmsh.option.setNumber("Geometry.OCCSewFaces", 1) gmsh.option.setNumber("Geometry.OCCMakeSolids", 1) self._initialized = True self._verbose = verbose self._geometry_tolerance = geometry_tolerance
[docs] def finalize(self) -> None: """Finalize Gmsh and release resources.""" if self._initialized: gmsh.finalize() self._initialized = False
def __enter__(self) -> "GmshMesher": """Context manager entry.""" self.initialize(geometry_tolerance=getattr(self, '_geometry_tolerance', 0.1)) return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: """Context manager exit.""" self.finalize()
[docs] def import_solid(self, solid: Any, face_names: Optional[Dict[str, List[int]]] = None, use_stl: bool = False) -> List[Tuple[int, int]]: """Import a yapCAD solid into Gmsh. This uses Gmsh's OCC integration to import the geometry directly from the OCC representation, avoiding STEP/IGES conversion losses. Args: solid: yapCAD solid (must have OCC BREP representation) face_names: Optional mapping of face names to face indices use_stl: If True, use STL (tessellated) representation which may be more robust for complex geometries with topology issues Returns: List of (dim, tag) tuples for imported entities """ if not self._initialized: raise RuntimeError("GmshMesher not initialized. Call initialize() first.") # Get OCC shape from yapCAD solid occ_shape = self._get_occ_shape(solid) if use_stl or getattr(self, '_use_stl', False): return self._import_via_stl(occ_shape) # Import via temporary BREP file (Gmsh's importShapes needs a file) # TODO: Investigate direct OCC shape passing when gmsh Python API supports it with tempfile.NamedTemporaryFile(suffix=".brep", delete=False) as tmp: tmp_path = Path(tmp.name) try: # Apply OCC healing before export only if tolerance is large enough tol = getattr(self, '_geometry_tolerance', 0.1) if tol >= 0.01: # Only heal if tolerance >= 0.01mm if getattr(self, '_verbose', False): print(f" Meshing: healing OCC shape (tol={tol})...", flush=True) try: from OCC.Core.ShapeFix import ShapeFix_Shape, ShapeFix_Solid # Fix the shape fixer = ShapeFix_Shape(occ_shape) fixer.SetPrecision(tol) fixer.SetMaxTolerance(tol * 10) fixer.SetMinTolerance(tol / 10) fixer.Perform() fixed_shape = fixer.Shape() # Additional solid-specific fixing if applicable from OCC.Core.TopAbs import TopAbs_SOLID if fixed_shape.ShapeType() == TopAbs_SOLID: try: solid_fixer = ShapeFix_Solid(fixed_shape) solid_fixer.SetPrecision(tol) solid_fixer.Perform() fixed_shape = solid_fixer.Solid() except Exception: pass # Skip if solid fixing fails occ_shape = fixed_shape if getattr(self, '_verbose', False): print(" Meshing: OCC shape healed", flush=True) except Exception as occ_heal_err: if getattr(self, '_verbose', False): print(f" Meshing: OCC healing skipped: {occ_heal_err}", flush=True) else: if getattr(self, '_verbose', False): print(f" Meshing: skipping OCC healing (tol={tol} < 0.01)", flush=True) # Optionally scale geometry for better numerical precision scale_factor = getattr(self, '_scale_factor', 1.0) if scale_factor != 1.0: if getattr(self, '_verbose', False): print(f" Meshing: scaling geometry by {scale_factor}x...", flush=True) from OCC.Core.gp import gp_Trsf, gp_Pnt from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform trsf = gp_Trsf() trsf.SetScale(gp_Pnt(0, 0, 0), scale_factor) transformer = BRepBuilderAPI_Transform(occ_shape, trsf, True) transformer.Build() occ_shape = transformer.Shape() # If shape is a COMPOUND with multiple SOLIDs, fuse them into a single solid # This ensures Gmsh imports a proper volume instead of loose surfaces try: from OCC.Core.TopAbs import TopAbs_COMPOUND, TopAbs_SOLID from OCC.Core.TopExp import TopExp_Explorer from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Fuse from OCC.Core.TopoDS import topods if occ_shape.ShapeType() == TopAbs_COMPOUND: # Count and collect solids solids = [] exp = TopExp_Explorer(occ_shape, TopAbs_SOLID) while exp.More(): solids.append(topods.Solid(exp.Current())) exp.Next() if len(solids) > 1: if getattr(self, '_verbose', False): print(f" Meshing: fusing {len(solids)} solids into single body...", flush=True) # Fuse all solids together result = solids[0] for i, s in enumerate(solids[1:], 1): try: fuser = BRepAlgoAPI_Fuse(result, s) fuser.SetFuzzyValue(tol) fuser.Build() if fuser.IsDone(): result = fuser.Shape() except Exception as fuse_err: if getattr(self, '_verbose', False): print(f" Meshing: fuse {i} warning: {fuse_err}", flush=True) occ_shape = result if getattr(self, '_verbose', False): print(" Meshing: solids fused successfully", flush=True) except Exception as fuse_err: if getattr(self, '_verbose', False): print(f" Meshing: solid fusion skipped: {fuse_err}", flush=True) # Write OCC shape to BREP file if getattr(self, '_verbose', False): print(" Meshing: exporting geometry to BREP...", flush=True) breptools.Write(occ_shape, str(tmp_path)) # Import into Gmsh via OCC kernel if getattr(self, '_verbose', False): print(" Meshing: importing geometry into Gmsh...", flush=True) entities = gmsh.model.occ.importShapes(str(tmp_path)) # Heal and repair geometry topology only if tolerance is large enough if tol >= 0.01: if getattr(self, '_verbose', False): print(" Meshing: healing geometry in Gmsh...", flush=True) try: # Remove small edges and faces that cause meshing issues gmsh.model.occ.healShapes( dimTags=[], tolerance=tol, fixDegenerated=True, fixSmallEdges=True, fixSmallFaces=True, sewFaces=True, makeSolids=True ) except Exception as heal_err: if getattr(self, '_verbose', False): print(f" Meshing: heal warning: {heal_err}", flush=True) else: if getattr(self, '_verbose', False): print(" Meshing: skipping Gmsh healing (small tolerance)", flush=True) gmsh.model.occ.synchronize() # Remove duplicate entities (internal faces from fused solids) try: gmsh.model.occ.removeAllDuplicates() gmsh.model.occ.synchronize() except Exception as dup_err: if getattr(self, '_verbose', False): print(f" Meshing: removeAllDuplicates warning: {dup_err}", flush=True) if getattr(self, '_verbose', False): print(f" Meshing: imported {len(entities)} entities", flush=True) # Check if we have volumes - if not, try to create them from surfaces # This handles cases where healShapes "Could not make solid" during BREP import volumes = gmsh.model.getEntities(dim=3) if not volumes: surfaces = gmsh.model.getEntities(dim=2) if surfaces: if getattr(self, '_verbose', False): print(f" Meshing: no volumes found, creating from {len(surfaces)} surfaces...", flush=True) # Use healShapes with makeSolids to try creating volumes from the surface shells try: surface_tags = [(2, s[1]) for s in surfaces] gmsh.model.occ.healShapes( dimTags=surface_tags, tolerance=max(tol, 0.1), # Use at least 0.1mm tolerance for solid creation fixDegenerated=True, fixSmallEdges=True, fixSmallFaces=True, sewFaces=True, makeSolids=True ) gmsh.model.occ.synchronize() volumes = gmsh.model.getEntities(dim=3) if volumes: if getattr(self, '_verbose', False): print(f" Meshing: healShapes created {len(volumes)} volume(s)", flush=True) except Exception as heal_err: if getattr(self, '_verbose', False): print(f" Meshing: healShapes for volumes failed: {heal_err}", flush=True) # If healShapes didn't create volumes, try single surface loop approach if not volumes: try: surfaces = gmsh.model.getEntities(dim=2) surface_tags = [s[1] for s in surfaces] sl = gmsh.model.occ.addSurfaceLoop(surface_tags) vol = gmsh.model.occ.addVolume([sl]) gmsh.model.occ.synchronize() volumes = gmsh.model.getEntities(dim=3) if getattr(self, '_verbose', False): print(f" Meshing: created {len(volumes)} volume(s) from surface loop", flush=True) except Exception as vol_err: if getattr(self, '_verbose', False): print(f" Meshing: surface loop volume creation failed: {vol_err}", flush=True) finally: tmp_path.unlink(missing_ok=True) # Store face mapping if provided if face_names: # Get all faces from the model faces = gmsh.model.getEntities(dim=2) for name, indices in face_names.items(): for idx in indices: if idx < len(faces): self._face_map[faces[idx][1]] = name return entities
def _import_via_stl(self, occ_shape: "TopoDS_Shape") -> List[Tuple[int, int]]: """Import geometry via STL tessellation (bypasses BREP topology issues). Uses Gmsh's discrete model capabilities to mesh from triangulated surfaces. """ from OCC.Core.StlAPI import stlapi from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh if getattr(self, '_verbose', False): print(" Meshing: tessellating geometry to STL...", flush=True) # Tessellate the shape with finer resolution for FEA tol = getattr(self, '_geometry_tolerance', 0.1) linear_deflection = tol # Linear deviation tolerance angular_deflection = 0.25 # Angular deviation in radians (~14 degrees) mesh = BRepMesh_IncrementalMesh(occ_shape, linear_deflection, False, angular_deflection, True) mesh.Perform() # Export to STL with tempfile.NamedTemporaryFile(suffix=".stl", delete=False) as tmp: stl_path = Path(tmp.name) try: stlapi.Write(occ_shape, str(stl_path)) if getattr(self, '_verbose', False): print(" Meshing: importing STL into Gmsh...", flush=True) # Import STL using merge which creates surface triangulation gmsh.merge(str(stl_path)) # Create topology from the imported mesh if getattr(self, '_verbose', False): print(" Meshing: creating volume from STL surface...", flush=True) # Create edges and surfaces from the triangulation angle_deg = 40 # Feature angle threshold in degrees gmsh.model.mesh.classifySurfaces( angle_deg * 3.14159265 / 180.0, # angle True, # includeBoundary False, # forceParametrizablePatches 180 * 3.14159265 / 180.0 # curveAngle (don't split based on curves) ) # Create discrete model geometry from the classified mesh gmsh.model.mesh.createGeometry() # Get the surfaces from the discrete model surfaces = gmsh.model.getEntities(dim=2) if getattr(self, '_verbose', False): print(f" Meshing: classified into {len(surfaces)} discrete surfaces", flush=True) if surfaces: # Create a surface filling to get a closed volume surface_tags = [s[1] for s in surfaces] # Add a discrete surface loop sl = gmsh.model.geo.addSurfaceLoop(surface_tags) vol = gmsh.model.geo.addVolume([sl]) gmsh.model.geo.synchronize() if getattr(self, '_verbose', False): print(f" Meshing: created volume entity", flush=True) entities = gmsh.model.getEntities() if getattr(self, '_verbose', False): dim_counts = {} for dim, _ in entities: dim_counts[dim] = dim_counts.get(dim, 0) + 1 print(f" Meshing: final entities: {dim_counts}", flush=True) return entities finally: stl_path.unlink(missing_ok=True)
[docs] def import_step(self, step_path: Path) -> List[Tuple[int, int]]: """Import geometry from a STEP file. Args: step_path: Path to the STEP file Returns: List of (dim, tag) tuples for imported entities """ if not self._initialized: raise RuntimeError("GmshMesher not initialized.") entities = gmsh.model.occ.importShapes(str(step_path)) gmsh.model.occ.synchronize() return entities
[docs] def set_physical_groups(self, groups: Dict[str, List[int]], dim: int = 2) -> None: """Define physical groups for boundary conditions. Physical groups associate mesh entities with names that can be used to apply boundary conditions in the solver. Args: groups: Mapping of group names to entity tags dim: Dimension of entities (2 for faces, 3 for volumes) """ for name, tags in groups.items(): if tags: pg_tag = gmsh.model.addPhysicalGroup(dim, tags) gmsh.model.setPhysicalName(dim, pg_tag, name) self._physical_groups[name] = PhysicalGroup(name=name, dim=dim, tags=tags)
[docs] def set_physical_groups_by_normal( self, groups: Dict[str, Tuple[float, float, float]], tolerance_deg: float = 5.0 ) -> None: """Define physical groups by face normal direction. This is useful for automatically identifying faces like "top", "bottom", "front", etc. based on their orientation. Args: groups: Mapping of group names to normal vectors (x, y, z) tolerance_deg: Angular tolerance in degrees """ import math faces = gmsh.model.getEntities(dim=2) tol_rad = math.radians(tolerance_deg) for name, target_normal in groups.items(): # Normalize target mag = math.sqrt(sum(n*n for n in target_normal)) if mag < 1e-10: continue target = tuple(n/mag for n in target_normal) matching_tags = [] for dim, tag in faces: # Get face center and normal via Gmsh try: # Get parametric center bounds = gmsh.model.getParametrizationBounds(dim, tag) u_mid = (bounds[0][0] + bounds[1][0]) / 2 v_mid = (bounds[0][1] + bounds[1][1]) / 2 # Get normal at center normal = gmsh.model.getNormal(tag, [u_mid, v_mid]) # Check angle dot = sum(a*b for a, b in zip(target, normal)) angle = math.acos(max(-1, min(1, dot))) if angle < tol_rad: matching_tags.append(tag) except Exception: continue if matching_tags: pg_tag = gmsh.model.addPhysicalGroup(2, matching_tags) gmsh.model.setPhysicalName(2, pg_tag, name) self._physical_groups[name] = PhysicalGroup(name=name, dim=2, tags=matching_tags)
[docs] def generate_mesh(self, hints: Optional[MeshHints] = None, dim: int = 3) -> None: """Generate the mesh. Args: hints: Mesh generation hints dim: Mesh dimension (2 for surface, 3 for volume) """ if not self._initialized: raise RuntimeError("GmshMesher not initialized.") hints = hints or MeshHints() # Apply geometry tolerance for healing problematic geometry gmsh.option.setNumber("Geometry.Tolerance", hints.geometry_tolerance) gmsh.option.setNumber("Geometry.ToleranceBoolean", hints.geometry_tolerance) gmsh.option.setNumber("Geometry.OCCFixDegenerated", 1) gmsh.option.setNumber("Geometry.OCCFixSmallEdges", 1) gmsh.option.setNumber("Geometry.OCCFixSmallFaces", 1) gmsh.option.setNumber("Geometry.OCCSewFaces", 1) gmsh.option.setNumber("Geometry.OCCMakeSolids", 1) # More tolerant curve recovery during meshing gmsh.option.setNumber("Mesh.ToleranceEdgeLength", hints.geometry_tolerance * 10) gmsh.option.setNumber("Mesh.ToleranceInitialDelaunay", hints.geometry_tolerance * 100) # Allow meshing to proceed even with small topology issues gmsh.option.setNumber("Mesh.AngleToleranceFacetOverlap", 0.5) gmsh.option.setNumber("Mesh.AllowSwapAngle", 90) # Apply mesh size options gmsh.option.setNumber("Mesh.CharacteristicLengthMin", hints.min_element_size or hints.element_size * 0.1) gmsh.option.setNumber("Mesh.CharacteristicLengthMax", hints.max_element_size or hints.element_size * 10) gmsh.option.setNumber("Mesh.CharacteristicLengthFactor", 1.0) # Set default element size gmsh.option.setNumber("Mesh.MeshSizeMin", hints.min_element_size or hints.element_size * 0.1) gmsh.option.setNumber("Mesh.MeshSizeMax", hints.max_element_size or hints.element_size * 10) # Set meshing algorithms gmsh.option.setNumber("Mesh.Algorithm", hints.algorithm_2d) gmsh.option.setNumber("Mesh.Algorithm3D", hints.algorithm_3d) # Element order gmsh.option.setNumber("Mesh.ElementOrder", hints.element_order) # Optimization gmsh.option.setNumber("Mesh.Optimize", 1 if hints.optimize else 0) gmsh.option.setNumber("Mesh.OptimizeNetgen", 1 if hints.optimize_netgen else 0) # Apply refinement fields if specified for i, field_spec in enumerate(hints.refinement_fields): self._apply_refinement_field(i + 1, field_spec) # Set uniform mesh size on all entities entities = gmsh.model.getEntities(0) # vertices gmsh.model.mesh.setSize(entities, hints.element_size) # Determine actual dimension to mesh actual_dim = dim if not hints.recover_3d and dim == 3: if getattr(self, '_verbose', False): print(" Meshing: recover_3d=False, generating 2D surface mesh only", flush=True) actual_dim = 2 # Generate mesh with progress indication if getattr(self, '_verbose', False): print(f" Meshing: generating {actual_dim}D mesh (element size={hints.element_size}mm)...", flush=True) gmsh.model.mesh.generate(actual_dim) if getattr(self, '_verbose', False): print(f" Meshing: {actual_dim}D mesh generation complete", flush=True) # Optimize if requested if hints.optimize: if getattr(self, '_verbose', False): print(" Meshing: optimizing 2D mesh...", flush=True) gmsh.model.mesh.optimize("Relocate2D") if actual_dim == 3: if getattr(self, '_verbose', False): print(" Meshing: optimizing 3D mesh...", flush=True) # Use Netgen if requested, otherwise basic 3D relocation if hints.optimize_netgen: gmsh.model.mesh.optimize("Netgen") else: gmsh.model.mesh.optimize("Relocate3D") if getattr(self, '_verbose', False): print(" Meshing: optimization complete", flush=True)
def _apply_refinement_field(self, field_id: int, spec: Dict[str, Any]) -> None: """Apply a mesh refinement field.""" field_type = spec.get("type", "Box") gmsh.model.mesh.field.add(field_type, field_id) for key, value in spec.items(): if key == "type": continue if isinstance(value, (int, float)): gmsh.model.mesh.field.setNumber(field_id, key, value) elif isinstance(value, list): gmsh.model.mesh.field.setNumbers(field_id, key, value) elif isinstance(value, str): gmsh.model.mesh.field.setString(field_id, key, value)
[docs] def export_mesh(self, path: Path, format: Optional[str] = None) -> Path: """Export the mesh to a file. Args: path: Output path format: Optional format override (msh, vtk, xdmf, su2) Returns: Path to the exported file """ if not self._initialized: raise RuntimeError("GmshMesher not initialized.") path = Path(path) # Determine format from extension if not specified if format is None: format = path.suffix.lstrip(".").lower() # Handle special formats if format == "xdmf": # XDMF for FEniCSx - export as MSH2 then convert msh_path = path.with_suffix(".msh") gmsh.option.setNumber("Mesh.MshFileVersion", 2.2) gmsh.write(str(msh_path)) # Note: Actual XDMF conversion requires meshio or dolfinx return msh_path elif format == "su2": gmsh.write(str(path)) else: gmsh.write(str(path)) return path
[docs] def get_mesh_stats(self) -> Dict[str, Any]: """Get mesh statistics. Returns: Dictionary with node count, element counts by type, quality metrics """ if not self._initialized: raise RuntimeError("GmshMesher not initialized.") stats = { "nodes": 0, "elements": {}, "physical_groups": list(self._physical_groups.keys()), } # Count nodes node_tags, coords, _ = gmsh.model.mesh.getNodes() stats["nodes"] = len(node_tags) # Count elements by type element_types, element_tags, _ = gmsh.model.mesh.getElements() for elem_type, tags in zip(element_types, element_tags): elem_name = gmsh.model.mesh.getElementProperties(elem_type)[0] stats["elements"][elem_name] = len(tags) return stats
def _get_occ_shape(self, solid: Any) -> "TopoDS_Shape": """Extract OCC shape from yapCAD solid. Args: solid: yapCAD solid representation Returns: OCC TopoDS_Shape """ # Check if solid already has OCC representation if hasattr(solid, '_occ_shape'): return solid._occ_shape # Try to get from BREP cache or directly if it's a BrepSolid from yapcad.brep import BrepSolid, occ_available, brep_from_solid if occ_available(): if isinstance(solid, BrepSolid): return solid.shape # For list-based solids, try to get cached BrepSolid # This handles deserialized geometry that has BREP data in metadata brep = brep_from_solid(solid) if brep is not None: return brep.shape # Convert via native BREP (TopologyGraph) from yapcad.occ_native_convert import native_brep_to_occ, occ_available as occ_convert_available from yapcad.native_brep import TopologyGraph, has_native_brep if occ_convert_available(): # Check if solid has native BREP attached (TopologyGraph) if has_native_brep(solid): from yapcad.native_brep import native_brep_from_solid graph = native_brep_from_solid(solid) occ_shape = native_brep_to_occ(graph) if occ_shape is not None: return occ_shape raise ValueError( "Cannot extract OCC shape from solid. Ensure the solid has " "a valid BREP representation (created with OCC-enabled yapCAD)." )
[docs] def mesh_solid( solid: Any, output_path: Path, hints: Optional[MeshHints] = None, physical_groups: Optional[Dict[str, List[int]]] = None, dim: int = 3 ) -> Dict[str, Any]: """Convenience function to mesh a solid and export. Args: solid: yapCAD solid to mesh output_path: Path for output mesh file hints: Mesh generation hints physical_groups: Optional face groups for BCs dim: Mesh dimension Returns: Mesh statistics dictionary """ with GmshMesher() as mesher: mesher.import_solid(solid) if physical_groups: mesher.set_physical_groups(physical_groups) mesher.generate_mesh(hints, dim) mesher.export_mesh(output_path) return mesher.get_mesh_stats()
__all__ = [ "GmshMesher", "MeshHints", "PhysicalGroup", "gmsh_available", "require_gmsh", "mesh_solid", ]