Source code for yapcad.boolean.occ_engine

"""OCC-backed boolean engine operating on BrepSolid metadata."""

from __future__ import annotations

from typing import Optional

try:  # pragma: no cover - optional dependency
    from OCC.Core.BRepAlgoAPI import (
        BRepAlgoAPI_Common,
        BRepAlgoAPI_Cut,
        BRepAlgoAPI_Fuse,
    )
    from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeSolid
    from OCC.Core.TopExp import TopExp_Explorer
    from OCC.Core.TopAbs import TopAbs_SOLID, TopAbs_SHELL
    from OCC.Core.TopoDS import topods, TopoDS_Compound, TopoDS_Builder
    from OCC.Core.BRep import BRep_Builder
except ImportError:  # pragma: no cover
    BRepAlgoAPI_Common = BRepAlgoAPI_Cut = BRepAlgoAPI_Fuse = None
    BRepBuilderAPI_MakeSolid = None
    TopExp_Explorer = None
    TopAbs_SOLID = TopAbs_SHELL = None
    topods = TopoDS_Compound = TopoDS_Builder = BRep_Builder = None

from yapcad.brep import (
    BrepSolid,
    attach_brep_to_solid,
    brep_from_solid,
    occ_available,
    require_occ,
)


[docs] def is_available() -> bool: return occ_available() and BRepAlgoAPI_Fuse is not None
def _make_builder(op: str, a: BrepSolid, b: BrepSolid): require_occ() if BRepAlgoAPI_Fuse is None: raise RuntimeError("pythonocc-core boolean operators unavailable") if op == 'union': return BRepAlgoAPI_Fuse(a.shape, b.shape) if op == 'intersection': return BRepAlgoAPI_Common(a.shape, b.shape) if op == 'difference': return BRepAlgoAPI_Cut(a.shape, b.shape) raise ValueError(f"unsupported OCC boolean operation '{op}'")
[docs] def solid_boolean(a, b, operation: str): """Perform an OCC boolean, falling back to mesh engine if BREP unavailable. Handles both single-solid results and compound results (e.g., union of disconnected solids produces a compound containing multiple solids). For union operations, includes sanity check: if the result volume is significantly less than expected (more than 10% loss), falls back to creating a simple compound containing both shapes. This works around OCC boolean issues with certain lofted/swept shapes. If either solid lacks BREP metadata, automatically falls back to the native mesh boolean engine rather than raising an error. """ import os from OCC.Core.GProp import GProp_GProps from OCC.Core.BRepGProp import brepgprop _debug = os.environ.get('DEBUG_OCC_BOOLEAN', '').lower() in ('1', 'true', 'yes') if not is_available(): raise RuntimeError("OCC booleans unavailable; activate yapcad-brep environment") brep_a = brep_from_solid(a) brep_b = brep_from_solid(b) if brep_a is None or brep_b is None: # Fall back to native mesh engine when BREP metadata is missing if _debug: print(f'[OCC_BOOL] BREP missing (a={brep_a is not None}, b={brep_b is not None}), ' f'falling back to native mesh engine') from yapcad.boolean import native as native_engine return native_engine.solid_boolean(a, b, operation) # Get input volumes for sanity check props_a = GProp_GProps() brepgprop.VolumeProperties(brep_a.shape, props_a) vol_a = abs(props_a.Mass()) props_b = GProp_GProps() brepgprop.VolumeProperties(brep_b.shape, props_b) vol_b = abs(props_b.Mass()) if _debug: print(f'[OCC_BOOL] Input A: type={brep_a.shape.ShapeType()}, vol={vol_a:.2f}') print(f'[OCC_BOOL] Input B: type={brep_b.shape.ShapeType()}, vol={vol_b:.2f}') builder = _make_builder(operation.lower(), brep_a, brep_b) builder.Build() if not builder.IsDone(): raise RuntimeError("OCC boolean operation failed") result_shape = builder.Shape() props_r = GProp_GProps() brepgprop.VolumeProperties(result_shape, props_r) vol_r = abs(props_r.Mass()) if _debug: print(f'[OCC_BOOL] Result: type={result_shape.ShapeType()}, vol={vol_r:.2f}') # For union operations, sanity check the result volume # If we lost more than 10% of the expected volume, fall back to compound if operation.lower() == 'union': expected_vol = vol_a + vol_b # Upper bound (no overlap) min_expected_vol = max(vol_a, vol_b) # Lower bound (complete overlap) if vol_r < min_expected_vol * 0.9: if _debug: print(f'[OCC_BOOL] Volume sanity check failed! ' f'Result {vol_r:.2f} < min expected {min_expected_vol * 0.9:.2f}') print(f'[OCC_BOOL] Falling back to simple compound') # OCC fuse failed silently - create compound instead compound = TopoDS_Compound() builder_compound = BRep_Builder() builder_compound.MakeCompound(compound) # Add all solids from both inputs exp_a = TopExp_Explorer(brep_a.shape, TopAbs_SOLID) while exp_a.More(): builder_compound.Add(compound, exp_a.Current()) exp_a.Next() exp_b = TopExp_Explorer(brep_b.shape, TopAbs_SOLID) while exp_b.More(): builder_compound.Add(compound, exp_b.Current()) exp_b.Next() result_shape = compound if _debug: props_c = GProp_GProps() brepgprop.VolumeProperties(result_shape, props_c) print(f'[OCC_BOOL] Compound vol: {abs(props_c.Mass()):.2f}') # Collect all solids from the result (may be compound for disconnected unions) solids = [] exp = TopExp_Explorer(result_shape, TopAbs_SOLID) while exp.More(): solid = exp.Current() if topods is not None: solid = topods.Solid(solid) solids.append(solid) exp.Next() if not solids: raise RuntimeError("OCC boolean produced no solids") # If single solid, use it directly if len(solids) == 1: result_solid = solids[0] else: # Multiple solids: create a compound to hold them all compound = TopoDS_Compound() builder_compound = BRep_Builder() builder_compound.MakeCompound(compound) for solid in solids: builder_compound.Add(compound, solid) result_solid = compound result_brep = BrepSolid(result_solid) surface = result_brep.tessellate() from yapcad.geom3d import solid as geom3d_solid # circular-safe import result = geom3d_solid([surface]) attach_brep_to_solid(result, result_brep) return result
__all__ = ["solid_boolean", "is_available"]