"""OCC ↔ Native BREP conversion utilities.
This module provides bidirectional conversion between OCC (Open CASCADE) BREP
representations and yapCAD native BREP representations.
The native BREP representation is the primary/canonical form for yapCAD geometry.
OCC representations are derived and used for:
- STEP file import/export
- Boolean operations (when OCC engine is selected)
- Complex surface operations
Conversion functions:
- occ_surface_to_native: Convert OCC Geom_Surface to native analytic surface
- occ_solid_to_native_brep: Convert OCC TopoDS_Solid to native BREP topology
- native_surface_to_occ: Convert native analytic surface to OCC Geom_Surface
- native_brep_to_occ: Convert native BREP topology to OCC TopoDS_Solid
Copyright (c) 2025 Richard DeVaul
MIT License
"""
from math import sqrt, pi, atan2, acos
from typing import Optional, Any, Tuple, List
from yapcad.geom import point
# OCC imports - gracefully handle missing dependency
try:
from OCC.Core.TopoDS import (
TopoDS_Shape, TopoDS_Solid, TopoDS_Shell, TopoDS_Face,
TopoDS_Wire, TopoDS_Edge, TopoDS_Vertex, topods
)
from OCC.Core.TopExp import TopExp_Explorer, topexp
from OCC.Core.TopAbs import (
TopAbs_SOLID, TopAbs_SHELL, TopAbs_FACE, TopAbs_WIRE,
TopAbs_EDGE, TopAbs_VERTEX, TopAbs_FORWARD, TopAbs_REVERSED
)
from OCC.Core.BRep import BRep_Tool
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
from OCC.Core.GeomAbs import (
GeomAbs_Plane, GeomAbs_Cylinder, GeomAbs_Cone,
GeomAbs_Sphere, GeomAbs_Torus, GeomAbs_BSplineSurface,
GeomAbs_BezierSurface, GeomAbs_SurfaceOfRevolution,
GeomAbs_SurfaceOfExtrusion, GeomAbs_OffsetSurface,
GeomAbs_OtherSurface,
GeomAbs_Line, GeomAbs_Circle, GeomAbs_Ellipse,
GeomAbs_BSplineCurve, GeomAbs_BezierCurve, GeomAbs_OtherCurve
)
from OCC.Core.gp import gp_Pnt, gp_Vec, gp_Dir, gp_Ax1, gp_Ax2, gp_Ax3
from OCC.Core.TopLoc import TopLoc_Location
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
from OCC.Core.Geom import (
Geom_Plane, Geom_CylindricalSurface, Geom_ConicalSurface,
Geom_SphericalSurface, Geom_ToroidalSurface, Geom_BSplineSurface
)
from OCC.Core.BRepBuilderAPI import (
BRepBuilderAPI_MakeVertex, BRepBuilderAPI_MakeEdge,
BRepBuilderAPI_MakeWire, BRepBuilderAPI_MakeFace,
BRepBuilderAPI_MakeShell, BRepBuilderAPI_MakeSolid,
BRepBuilderAPI_Sewing
)
from OCC.Core.TColgp import TColgp_Array2OfPnt, TColgp_Array1OfPnt
from OCC.Core.TColStd import TColStd_Array1OfReal, TColStd_Array1OfInteger
from OCC.Core.ShapeFix import ShapeFix_Solid, ShapeFix_Shell
from OCC.Core.Geom import Geom_Line, Geom_Circle, Geom_BSplineCurve
from OCC.Core.GC import GC_MakeCircle, GC_MakeLine, GC_MakeArcOfCircle
_OCC_AVAILABLE = True
except ImportError:
_OCC_AVAILABLE = False
# Type stubs for when OCC is not available
TopoDS_Shape = TopoDS_Solid = TopoDS_Shell = TopoDS_Face = Any
TopoDS_Wire = TopoDS_Edge = TopoDS_Vertex = Any
BRepAdaptor_Surface = BRepAdaptor_Curve = Any
from yapcad.analytic_surfaces import (
plane_surface, sphere_surface, cylinder_surface, cone_surface, torus_surface,
bspline_surface, tessellated_surface, is_analytic_surface
)
from yapcad.native_brep import (
brep_vertex, brep_edge, brep_trim, brep_loop, brep_face, brep_shell, brep_solid,
line_edge, circle_edge, bspline_edge,
vertex_location, edge_vertices,
TopologyGraph
)
[docs]
def occ_available() -> bool:
"""Return True if OCC is available."""
return _OCC_AVAILABLE
[docs]
def require_occ() -> None:
"""Raise error if OCC is not available."""
if not _OCC_AVAILABLE:
raise RuntimeError(
"pythonocc-core is not available. Activate the yapcad-brep conda "
"environment to use OCC conversion features."
)
# -----------------------------------------------------------------------------
# OCC Surface → Native Surface Conversion
# -----------------------------------------------------------------------------
def _gp_pnt_to_point(pnt) -> list:
"""Convert gp_Pnt to yapCAD point."""
return point(pnt.X(), pnt.Y(), pnt.Z())
def _gp_dir_to_vector(d) -> list:
"""Convert gp_Dir to yapCAD vector."""
return [d.X(), d.Y(), d.Z(), 0.0]
[docs]
def occ_surface_to_native(face: TopoDS_Face, tessellate_fallback: bool = True):
"""Convert an OCC face's surface to a native analytic surface.
Parameters
----------
face : TopoDS_Face
The OCC face to convert.
tessellate_fallback : bool
If True, fall back to tessellation for unsupported surface types.
Returns
-------
native_surface
A native analytic surface representation, or None if conversion fails.
"""
require_occ()
adaptor = BRepAdaptor_Surface(face)
surface_type = adaptor.GetType()
# Get parameter bounds
u_min, u_max, v_min, v_max = adaptor.FirstUParameter(), adaptor.LastUParameter(), \
adaptor.FirstVParameter(), adaptor.LastVParameter()
if surface_type == GeomAbs_Plane:
return _convert_plane(adaptor, u_min, u_max, v_min, v_max)
elif surface_type == GeomAbs_Sphere:
return _convert_sphere(adaptor, u_min, u_max, v_min, v_max)
elif surface_type == GeomAbs_Cylinder:
return _convert_cylinder(adaptor, u_min, u_max, v_min, v_max)
elif surface_type == GeomAbs_Cone:
return _convert_cone(adaptor, u_min, u_max, v_min, v_max)
elif surface_type == GeomAbs_Torus:
return _convert_torus(adaptor, u_min, u_max, v_min, v_max)
elif surface_type == GeomAbs_BSplineSurface:
return _convert_bspline_surface(adaptor, u_min, u_max, v_min, v_max)
elif surface_type == GeomAbs_BezierSurface:
# Convert Bezier to BSpline representation
return _convert_bezier_surface(adaptor, u_min, u_max, v_min, v_max)
else:
# Fallback: tessellate the face
if tessellate_fallback:
return _tessellate_occ_face(face, u_min, u_max, v_min, v_max)
return None
def _convert_plane(adaptor, u_min, u_max, v_min, v_max):
"""Convert OCC plane to native plane surface."""
pln = adaptor.Plane()
loc = pln.Location()
axis = pln.Axis()
origin = _gp_pnt_to_point(loc)
normal = _gp_dir_to_vector(axis.Direction())
return plane_surface(origin, normal, u_range=(u_min, u_max), v_range=(v_min, v_max))
def _convert_sphere(adaptor, u_min, u_max, v_min, v_max):
"""Convert OCC sphere to native sphere surface."""
sph = adaptor.Sphere()
center = _gp_pnt_to_point(sph.Location())
radius = sph.Radius()
return sphere_surface(center, radius, u_range=(u_min, u_max), v_range=(v_min, v_max))
def _convert_cylinder(adaptor, u_min, u_max, v_min, v_max):
"""Convert OCC cylinder to native cylinder surface."""
cyl = adaptor.Cylinder()
axis = cyl.Axis()
loc = axis.Location()
direction = axis.Direction()
radius = cyl.Radius()
axis_point = _gp_pnt_to_point(loc)
axis_dir = _gp_dir_to_vector(direction)
return cylinder_surface(axis_point, axis_dir, radius,
u_range=(u_min, u_max), v_range=(v_min, v_max))
def _convert_cone(adaptor, u_min, u_max, v_min, v_max):
"""Convert OCC cone to native cone surface."""
cone = adaptor.Cone()
apex = cone.Apex()
axis = cone.Axis()
half_angle = cone.SemiAngle()
apex_pt = _gp_pnt_to_point(apex)
axis_dir = _gp_dir_to_vector(axis.Direction())
# OCC uses negative half_angle for cones pointing downward - use absolute value
half_angle = abs(half_angle)
return cone_surface(apex_pt, axis_dir, half_angle,
u_range=(u_min, u_max), v_range=(v_min, v_max))
def _convert_torus(adaptor, u_min, u_max, v_min, v_max):
"""Convert OCC torus to native torus surface."""
torus = adaptor.Torus()
axis = torus.Axis()
loc = axis.Location()
direction = axis.Direction()
major_radius = torus.MajorRadius()
minor_radius = torus.MinorRadius()
center = _gp_pnt_to_point(loc)
axis_dir = _gp_dir_to_vector(direction)
return torus_surface(center, axis_dir, major_radius, minor_radius,
u_range=(u_min, u_max), v_range=(v_min, v_max))
def _convert_bspline_surface(adaptor, u_min, u_max, v_min, v_max):
"""Convert OCC B-spline surface to native B-spline surface."""
bspl = adaptor.BSpline()
# Get degrees
u_degree = bspl.UDegree()
v_degree = bspl.VDegree()
# Get pole counts
n_u = bspl.NbUPoles()
n_v = bspl.NbVPoles()
# Get control points
control_points = []
for j in range(1, n_v + 1):
row = []
for i in range(1, n_u + 1):
pnt = bspl.Pole(i, j)
row.append([pnt.X(), pnt.Y(), pnt.Z()])
control_points.append(row)
# Get weights (if rational)
weights = None
if bspl.IsURational() or bspl.IsVRational():
weights = []
for j in range(1, n_v + 1):
row = []
for i in range(1, n_u + 1):
row.append(bspl.Weight(i, j))
weights.append(row)
# Get knot vectors
u_knots = []
for i in range(1, bspl.NbUKnots() + 1):
knot = bspl.UKnot(i)
mult = bspl.UMultiplicity(i)
u_knots.extend([knot] * mult)
v_knots = []
for i in range(1, bspl.NbVKnots() + 1):
knot = bspl.VKnot(i)
mult = bspl.VMultiplicity(i)
v_knots.extend([knot] * mult)
return bspline_surface(control_points, u_knots, v_knots, u_degree, v_degree,
weights=weights, u_range=(u_min, u_max), v_range=(v_min, v_max))
def _convert_bezier_surface(adaptor, u_min, u_max, v_min, v_max):
"""Convert OCC Bezier surface to native B-spline surface.
Bezier surfaces are a special case of B-splines with specific knot vectors.
"""
bez = adaptor.Bezier()
# Get degrees
u_degree = bez.UDegree()
v_degree = bez.VDegree()
# Get pole counts
n_u = bez.NbUPoles()
n_v = bez.NbVPoles()
# Get control points
control_points = []
for j in range(1, n_v + 1):
row = []
for i in range(1, n_u + 1):
pnt = bez.Pole(i, j)
row.append([pnt.X(), pnt.Y(), pnt.Z()])
control_points.append(row)
# Get weights (if rational)
weights = None
if bez.IsURational() or bez.IsVRational():
weights = []
for j in range(1, n_v + 1):
row = []
for i in range(1, n_u + 1):
row.append(bez.Weight(i, j))
weights.append(row)
# Bezier knot vectors: [0, 0, ..., 0, 1, 1, ..., 1]
# with (degree + 1) zeros and (degree + 1) ones
u_knots = [0.0] * (u_degree + 1) + [1.0] * (u_degree + 1)
v_knots = [0.0] * (v_degree + 1) + [1.0] * (v_degree + 1)
return bspline_surface(control_points, u_knots, v_knots, u_degree, v_degree,
weights=weights, u_range=(u_min, u_max), v_range=(v_min, v_max))
def _tessellate_occ_face(face: TopoDS_Face, u_min, u_max, v_min, v_max,
deflection: float = 0.1):
"""Tessellate an OCC face and return as tessellated_surface.
Used as fallback for unsupported surface types.
"""
require_occ()
# Ensure mesh is generated
BRepMesh_IncrementalMesh(face, deflection)
loc = TopLoc_Location()
triangulation = BRep_Tool.Triangulation(face, loc)
if triangulation is None:
return None
trsf = loc.Transformation()
orientation = face.Orientation()
reverse = (orientation == TopAbs_REVERSED)
vertices = []
normals = []
# Extract vertices
for i in range(1, triangulation.NbNodes() + 1):
pnt = triangulation.Node(i).Transformed(trsf)
vertices.append([pnt.X(), pnt.Y(), pnt.Z()])
# Extract normals if available
if triangulation.HasNormals():
for i in range(1, triangulation.NbNodes() + 1):
n = triangulation.Normal(i)
vec = gp_Vec(n.X(), n.Y(), n.Z())
vec.Transform(trsf)
if reverse:
normals.append([-vec.X(), -vec.Y(), -vec.Z()])
else:
normals.append([vec.X(), vec.Y(), vec.Z()])
else:
# Compute normals from triangles
accum = [[0.0, 0.0, 0.0] for _ in range(triangulation.NbNodes())]
for i in range(1, triangulation.NbTriangles() + 1):
t = triangulation.Triangle(i)
n1, n2, n3 = t.Get()
if reverse:
n2, n3 = n3, n2
v0 = vertices[n1 - 1]
v1 = vertices[n2 - 1]
v2 = vertices[n3 - 1]
# Cross product
nx = (v1[1] - v0[1]) * (v2[2] - v0[2]) - (v1[2] - v0[2]) * (v2[1] - v0[1])
ny = (v1[2] - v0[2]) * (v2[0] - v0[0]) - (v1[0] - v0[0]) * (v2[2] - v0[2])
nz = (v1[0] - v0[0]) * (v2[1] - v0[1]) - (v1[1] - v0[1]) * (v2[0] - v0[0])
for idx in [n1 - 1, n2 - 1, n3 - 1]:
accum[idx][0] += nx
accum[idx][1] += ny
accum[idx][2] += nz
for vec in accum:
length = sqrt(vec[0]**2 + vec[1]**2 + vec[2]**2)
if length > 1e-12:
normals.append([vec[0]/length, vec[1]/length, vec[2]/length])
else:
normals.append([0.0, 0.0, 1.0])
# Extract faces
faces = []
for i in range(1, triangulation.NbTriangles() + 1):
t = triangulation.Triangle(i)
n1, n2, n3 = t.Get()
if reverse:
n2, n3 = n3, n2
faces.append([n1 - 1, n2 - 1, n3 - 1])
return tessellated_surface(vertices, normals, faces,
u_range=(u_min, u_max), v_range=(v_min, v_max))
# -----------------------------------------------------------------------------
# OCC Edge → Native Edge Conversion
# -----------------------------------------------------------------------------
[docs]
def occ_edge_to_native(edge: TopoDS_Edge, vertex_map: dict = None):
"""Convert an OCC edge to a native BREP edge.
Parameters
----------
edge : TopoDS_Edge
The OCC edge to convert.
vertex_map : dict, optional
Map from OCC vertex hash to native vertex, for topology consistency.
Returns
-------
tuple
(native_edge, start_vertex, end_vertex)
"""
require_occ()
if vertex_map is None:
vertex_map = {}
adaptor = BRepAdaptor_Curve(edge)
curve_type = adaptor.GetType()
# Get vertices
v1 = topexp.FirstVertex(edge)
v2 = topexp.LastVertex(edge)
p1 = BRep_Tool.Pnt(v1)
p2 = BRep_Tool.Pnt(v2)
# Get or create native vertices
v1_hash = hash((round(p1.X(), 8), round(p1.Y(), 8), round(p1.Z(), 8)))
v2_hash = hash((round(p2.X(), 8), round(p2.Y(), 8), round(p2.Z(), 8)))
if v1_hash in vertex_map:
native_v1 = vertex_map[v1_hash]
else:
native_v1 = brep_vertex([p1.X(), p1.Y(), p1.Z()])
vertex_map[v1_hash] = native_v1
if v2_hash in vertex_map:
native_v2 = vertex_map[v2_hash]
else:
native_v2 = brep_vertex([p2.X(), p2.Y(), p2.Z()])
vertex_map[v2_hash] = native_v2
# Get parameter bounds
t_min, t_max = adaptor.FirstParameter(), adaptor.LastParameter()
if curve_type == GeomAbs_Line:
# Line edge
native_edge = line_edge(native_v1, native_v2)
elif curve_type == GeomAbs_Circle:
# Circle edge
circ = adaptor.Circle()
center = _gp_pnt_to_point(circ.Location())
axis = _gp_dir_to_vector(circ.Axis().Direction())
radius = circ.Radius()
native_edge = circle_edge(native_v1, native_v2, center, axis, radius,
t_start=t_min, t_end=t_max)
elif curve_type in (GeomAbs_BSplineCurve, GeomAbs_BezierCurve):
# B-spline edge
native_edge = _convert_bspline_edge(adaptor, native_v1, native_v2, t_min, t_max)
else:
# Fallback: sample the curve and create B-spline approximation
native_edge = _approximate_edge_as_bspline(adaptor, native_v1, native_v2,
t_min, t_max, num_samples=20)
return native_edge, native_v1, native_v2
def _convert_bspline_edge(adaptor, v1, v2, t_min, t_max):
"""Convert OCC B-spline curve to native B-spline edge."""
bspl = adaptor.BSpline()
degree = bspl.Degree()
n_poles = bspl.NbPoles()
# Get control points
control_points = []
for i in range(1, n_poles + 1):
pnt = bspl.Pole(i)
control_points.append([pnt.X(), pnt.Y(), pnt.Z()])
# Get weights
weights = None
if bspl.IsRational():
weights = [bspl.Weight(i) for i in range(1, n_poles + 1)]
# Get knot vector
knots = []
for i in range(1, bspl.NbKnots() + 1):
knot = bspl.Knot(i)
mult = bspl.Multiplicity(i)
knots.extend([knot] * mult)
return bspline_edge(v1, v2, control_points, knots, degree,
weights=weights, t_start=t_min, t_end=t_max)
def _approximate_edge_as_bspline(adaptor, v1, v2, t_min, t_max, num_samples=20):
"""Approximate an arbitrary OCC curve as a B-spline edge.
Samples the curve and fits a cubic B-spline through the points.
"""
# Sample points along the curve
points = []
for i in range(num_samples):
t = t_min + (t_max - t_min) * i / (num_samples - 1)
pnt = adaptor.Value(t)
points.append([pnt.X(), pnt.Y(), pnt.Z()])
# Create cubic B-spline approximation
# For simplicity, use clamped uniform knot vector
degree = 3
n = len(points)
if n <= degree:
# Too few points, use linear interpolation
return line_edge(v1, v2)
# Clamped uniform knot vector
knots = [0.0] * (degree + 1)
num_internal = n - degree - 1
for i in range(1, num_internal + 1):
knots.append(i / (num_internal + 1))
knots.extend([1.0] * (degree + 1))
# Use sampled points as control points (approximation)
return bspline_edge(v1, v2, points, knots, degree, t_start=t_min, t_end=t_max)
# -----------------------------------------------------------------------------
# OCC Solid → Native BREP Conversion
# -----------------------------------------------------------------------------
[docs]
def occ_solid_to_native_brep(shape: TopoDS_Shape, tessellate_fallback: bool = True):
"""Convert an OCC solid to native BREP representation.
Parameters
----------
shape : TopoDS_Shape
The OCC shape (solid) to convert.
tessellate_fallback : bool
If True, use tessellation for unsupported surface types.
Returns
-------
tuple
(native_solid, topology_graph) where native_solid is the BREP solid
and topology_graph is the TopologyGraph containing all entities.
"""
require_occ()
graph = TopologyGraph()
vertex_map = {} # OCC vertex hash -> native vertex
edge_map = {} # OCC edge hash -> native edge
# Get the solid (handle both Solid and Shell inputs)
if shape.ShapeType() == TopAbs_SOLID:
solid = topods.Solid(shape)
elif shape.ShapeType() == TopAbs_SHELL:
# Wrap shell in a solid
solid = shape
else:
raise ValueError(f"Expected SOLID or SHELL, got {shape.ShapeType()}")
shells = []
# Iterate over shells
shell_explorer = TopExp_Explorer(shape, TopAbs_SHELL)
while shell_explorer.More():
occ_shell = topods.Shell(shell_explorer.Current())
native_faces = []
# Iterate over faces in shell
face_explorer = TopExp_Explorer(occ_shell, TopAbs_FACE)
while face_explorer.More():
occ_face = topods.Face(face_explorer.Current())
# Convert surface
native_surface = occ_surface_to_native(occ_face, tessellate_fallback)
if native_surface is None:
face_explorer.Next()
continue
# Convert loops (wires)
native_loops = []
wire_explorer = TopExp_Explorer(occ_face, TopAbs_WIRE)
while wire_explorer.More():
occ_wire = topods.Wire(wire_explorer.Current())
native_trims = []
# Iterate over edges in wire
edge_explorer = TopExp_Explorer(occ_wire, TopAbs_EDGE)
while edge_explorer.More():
occ_edge = topods.Edge(edge_explorer.Current())
# Get or create native edge
edge_hash = occ_edge.__hash__()
if edge_hash in edge_map:
native_edge = edge_map[edge_hash]
else:
native_edge, v1, v2 = occ_edge_to_native(occ_edge, vertex_map)
edge_map[edge_hash] = native_edge
graph.add_vertex(v1)
graph.add_vertex(v2)
graph.add_edge(native_edge)
# Determine trim sense
sense = occ_edge.Orientation() != TopAbs_REVERSED
native_trim = brep_trim(native_edge, sense=sense)
graph.add_trim(native_trim)
native_trims.append(native_trim)
edge_explorer.Next()
if native_trims:
# Determine loop type (outer vs inner)
# First wire is typically outer loop
loop_type = 'outer' if not native_loops else 'inner'
native_loop = brep_loop(native_trims, loop_type=loop_type)
graph.add_loop(native_loop)
native_loops.append(native_loop)
wire_explorer.Next()
# Create native face
native_face = brep_face(native_surface, native_loops)
graph.add_face(native_face)
native_faces.append(native_face)
face_explorer.Next()
if native_faces:
# Create shell (auto-detect closure)
native_shell = brep_shell(native_faces, closed=None)
graph.add_shell(native_shell, validate_closure=False)
shells.append(native_shell)
shell_explorer.Next()
if not shells:
raise ValueError("No shells found in OCC shape")
# Create solid
native_solid = brep_solid(shells)
graph.add_solid(native_solid, validate=False)
return native_solid, graph
# -----------------------------------------------------------------------------
# Native Surface → OCC Surface Conversion
# -----------------------------------------------------------------------------
[docs]
def native_surface_to_occ(surf):
"""Convert a native analytic surface to an OCC Geom_Surface.
Parameters
----------
surf : native surface
A native analytic surface.
Returns
-------
Geom_Surface or None
The OCC surface, or None if conversion is not supported.
"""
require_occ()
if not is_analytic_surface(surf):
return None
surface_type = surf[0]
if surface_type == 'plane_surface':
return _native_plane_to_occ(surf)
elif surface_type == 'sphere_surface':
return _native_sphere_to_occ(surf)
elif surface_type == 'cylinder_surface':
return _native_cylinder_to_occ(surf)
elif surface_type == 'cone_surface':
return _native_cone_to_occ(surf)
elif surface_type == 'torus_surface':
return _native_torus_to_occ(surf)
elif surface_type == 'bspline_surface':
return _native_bspline_to_occ(surf)
else:
return None
def _native_plane_to_occ(surf):
"""Convert native plane surface to OCC Geom_Plane."""
origin = surf[1]
meta = surf[2]
normal = meta['normal']
pnt = gp_Pnt(origin[0], origin[1], origin[2])
direction = gp_Dir(normal[0], normal[1], normal[2])
return Geom_Plane(pnt, direction)
def _native_sphere_to_occ(surf):
"""Convert native sphere surface to OCC Geom_SphericalSurface."""
center = surf[1]
meta = surf[2]
radius = meta['radius']
pnt = gp_Pnt(center[0], center[1], center[2])
ax3 = gp_Ax3(pnt, gp_Dir(0, 0, 1)) # Default axis
return Geom_SphericalSurface(ax3, radius)
def _native_cylinder_to_occ(surf):
"""Convert native cylinder surface to OCC Geom_CylindricalSurface."""
origin = surf[1]
meta = surf[2]
axis = meta['axis']
radius = meta['radius']
pnt = gp_Pnt(origin[0], origin[1], origin[2])
direction = gp_Dir(axis[0], axis[1], axis[2])
ax3 = gp_Ax3(pnt, direction)
return Geom_CylindricalSurface(ax3, radius)
def _native_cone_to_occ(surf):
"""Convert native cone surface to OCC Geom_ConicalSurface."""
apex = surf[1]
meta = surf[2]
axis = meta['axis']
half_angle = meta['half_angle']
pnt = gp_Pnt(apex[0], apex[1], apex[2])
direction = gp_Dir(axis[0], axis[1], axis[2])
ax3 = gp_Ax3(pnt, direction)
return Geom_ConicalSurface(ax3, half_angle, 0.0) # Reference radius at apex
def _native_torus_to_occ(surf):
"""Convert native torus surface to OCC Geom_ToroidalSurface."""
center = surf[1]
meta = surf[2]
axis = meta['axis']
major_radius = meta['major_radius']
minor_radius = meta['minor_radius']
pnt = gp_Pnt(center[0], center[1], center[2])
direction = gp_Dir(axis[0], axis[1], axis[2])
ax3 = gp_Ax3(pnt, direction)
return Geom_ToroidalSurface(ax3, major_radius, minor_radius)
def _native_bspline_to_occ(surf):
"""Convert native B-spline surface to OCC Geom_BSplineSurface."""
cpts = surf[1]
meta = surf[2]
n_u = meta['n_u']
n_v = meta['n_v']
u_degree = meta['u_degree']
v_degree = meta['v_degree']
u_knots_flat = meta['u_knots']
v_knots_flat = meta['v_knots']
weights = meta['weights']
# Create control points array
poles = TColgp_Array2OfPnt(1, n_u, 1, n_v)
for j in range(n_v):
for i in range(n_u):
cp = cpts[j][i]
poles.SetValue(i + 1, j + 1, gp_Pnt(cp[0], cp[1], cp[2]))
# Convert flat knot vector to knots + multiplicities
def knots_to_occ(flat_knots):
"""Convert flat knot vector to unique knots and multiplicities."""
unique_knots = []
multiplicities = []
prev = None
for k in flat_knots:
if prev is None or abs(k - prev) > 1e-10:
unique_knots.append(k)
multiplicities.append(1)
else:
multiplicities[-1] += 1
prev = k
return unique_knots, multiplicities
u_knots, u_mults = knots_to_occ(u_knots_flat)
v_knots, v_mults = knots_to_occ(v_knots_flat)
# Create OCC arrays
u_knots_arr = TColStd_Array1OfReal(1, len(u_knots))
for i, k in enumerate(u_knots):
u_knots_arr.SetValue(i + 1, k)
v_knots_arr = TColStd_Array1OfReal(1, len(v_knots))
for i, k in enumerate(v_knots):
v_knots_arr.SetValue(i + 1, k)
u_mults_arr = TColStd_Array1OfInteger(1, len(u_mults))
for i, m in enumerate(u_mults):
u_mults_arr.SetValue(i + 1, m)
v_mults_arr = TColStd_Array1OfInteger(1, len(v_mults))
for i, m in enumerate(v_mults):
v_mults_arr.SetValue(i + 1, m)
# Check if rational (non-uniform weights)
is_rational = False
for row in weights:
for w in row:
if abs(w - 1.0) > 1e-10:
is_rational = True
break
if is_rational:
# Create weights array
from OCC.Core.TColStd import TColStd_Array2OfReal
weights_arr = TColStd_Array2OfReal(1, n_u, 1, n_v)
for j in range(n_v):
for i in range(n_u):
weights_arr.SetValue(i + 1, j + 1, weights[j][i])
return Geom_BSplineSurface(poles, weights_arr,
u_knots_arr, v_knots_arr,
u_mults_arr, v_mults_arr,
u_degree, v_degree)
else:
return Geom_BSplineSurface(poles,
u_knots_arr, v_knots_arr,
u_mults_arr, v_mults_arr,
u_degree, v_degree)
# -----------------------------------------------------------------------------
# Native BREP → OCC Conversion (for boolean operations)
# -----------------------------------------------------------------------------
[docs]
def native_vertex_to_occ(vertex):
"""Convert a native BREP vertex to an OCC TopoDS_Vertex.
Parameters
----------
vertex : brep_vertex
The native vertex to convert.
Returns
-------
TopoDS_Vertex
The OCC vertex.
"""
require_occ()
from yapcad.native_brep import vertex_location
loc = vertex_location(vertex)
pnt = gp_Pnt(loc[0], loc[1], loc[2])
return BRepBuilderAPI_MakeVertex(pnt).Vertex()
[docs]
def native_edge_to_occ(edge, graph):
"""Convert a native BREP edge to an OCC TopoDS_Edge.
Parameters
----------
edge : brep_edge
The native edge to convert.
graph : TopologyGraph
The topology graph containing the edge's vertices.
Returns
-------
TopoDS_Edge
The OCC edge.
"""
require_occ()
from yapcad.native_brep import (
edge_curve_type, edge_vertices, edge_curve_params, vertex_location
)
curve_type = edge_curve_type(edge)
start_vid, end_vid = edge_vertices(edge)
params = edge_curve_params(edge)
# Get vertex locations - handle missing vertices
if start_vid is None or start_vid not in graph.vertices:
raise ValueError(f"Edge missing start vertex: {start_vid}")
if end_vid is None or end_vid not in graph.vertices:
raise ValueError(f"Edge missing end vertex: {end_vid}")
start_loc = vertex_location(graph.vertices[start_vid])
end_loc = vertex_location(graph.vertices[end_vid])
p1 = gp_Pnt(start_loc[0], start_loc[1], start_loc[2])
p2 = gp_Pnt(end_loc[0], end_loc[1], end_loc[2])
if curve_type == 'line':
# Line edge
edge_builder = BRepBuilderAPI_MakeEdge(p1, p2)
elif curve_type == 'circle':
# Circle/arc edge
center = params.get('center', [0, 0, 0])
axis = params.get('axis', [0, 0, 1])
radius = params.get('radius', 1.0)
t_start = params.get('t_start', 0.0)
t_end = params.get('t_end', 2 * 3.141592653589793)
center_pnt = gp_Pnt(center[0], center[1], center[2])
axis_dir = gp_Dir(axis[0], axis[1], axis[2])
ax2 = gp_Ax2(center_pnt, axis_dir)
# Check if this is a full circle (start == end vertex)
is_full_circle = (start_vid == end_vid) or (p1.Distance(p2) < 1e-9)
try:
from OCC.Core.Geom import Geom_Circle
from OCC.Core.gp import gp_Circ
# Create a circle
circ = gp_Circ(ax2, radius)
geom_circle = Geom_Circle(circ)
if is_full_circle:
# Full circle - use parameter range
edge_builder = BRepBuilderAPI_MakeEdge(geom_circle, t_start, t_end)
else:
# Arc - use start/end points
edge_builder = BRepBuilderAPI_MakeEdge(geom_circle, p1, p2)
if not edge_builder.IsDone():
# Fallback: try arc maker
arc_maker = GC_MakeArcOfCircle(p1, p2, center_pnt)
if arc_maker.IsDone():
edge_builder = BRepBuilderAPI_MakeEdge(arc_maker.Value())
else:
edge_builder = BRepBuilderAPI_MakeEdge(p1, p2)
except Exception:
edge_builder = BRepBuilderAPI_MakeEdge(p1, p2)
elif curve_type == 'arc':
# Arc with bulge or center
if 'center' in params:
center = params['center']
center_pnt = gp_Pnt(center[0], center[1], center[2])
try:
arc_maker = GC_MakeArcOfCircle(p1, p2, center_pnt)
if arc_maker.IsDone():
edge_builder = BRepBuilderAPI_MakeEdge(arc_maker.Value())
else:
edge_builder = BRepBuilderAPI_MakeEdge(p1, p2)
except Exception:
edge_builder = BRepBuilderAPI_MakeEdge(p1, p2)
else:
# Bulge-based arc - compute center
edge_builder = BRepBuilderAPI_MakeEdge(p1, p2)
elif curve_type == 'bspline':
# B-spline edge
control_points = params.get('control_points', [])
knots = params.get('knots', [])
degree = params.get('degree', 3)
weights = params.get('weights', None)
if len(control_points) < 2:
edge_builder = BRepBuilderAPI_MakeEdge(p1, p2)
else:
try:
occ_curve = _native_bspline_curve_to_occ(
control_points, knots, degree, weights
)
if occ_curve is not None:
edge_builder = BRepBuilderAPI_MakeEdge(occ_curve)
else:
edge_builder = BRepBuilderAPI_MakeEdge(p1, p2)
except Exception:
edge_builder = BRepBuilderAPI_MakeEdge(p1, p2)
else:
# Unknown curve type - fallback to line
edge_builder = BRepBuilderAPI_MakeEdge(p1, p2)
if not edge_builder.IsDone():
# Fallback to simple line
edge_builder = BRepBuilderAPI_MakeEdge(p1, p2)
try:
return edge_builder.Edge()
except RuntimeError:
# Edge builder failed - return a minimal degenerate edge
# Create a line edge as last resort
edge_builder = BRepBuilderAPI_MakeEdge(p1, p2)
if edge_builder.IsDone():
return edge_builder.Edge()
# If even that fails, create edge from a very small offset
p2_offset = gp_Pnt(p1.X() + 1e-6, p1.Y(), p1.Z())
return BRepBuilderAPI_MakeEdge(p1, p2_offset).Edge()
def _native_bspline_curve_to_occ(control_points, knots, degree, weights=None):
"""Convert native B-spline curve data to OCC Geom_BSplineCurve."""
require_occ()
n = len(control_points)
if n < 2:
return None
# Create control points array
poles = TColgp_Array1OfPnt(1, n)
for i, cp in enumerate(control_points):
poles.SetValue(i + 1, gp_Pnt(cp[0], cp[1], cp[2]))
# Convert flat knot vector to unique knots + multiplicities
unique_knots = []
multiplicities = []
prev = None
for k in knots:
if prev is None or abs(k - prev) > 1e-10:
unique_knots.append(k)
multiplicities.append(1)
else:
multiplicities[-1] += 1
prev = k
knots_arr = TColStd_Array1OfReal(1, len(unique_knots))
for i, k in enumerate(unique_knots):
knots_arr.SetValue(i + 1, k)
mults_arr = TColStd_Array1OfInteger(1, len(multiplicities))
for i, m in enumerate(multiplicities):
mults_arr.SetValue(i + 1, m)
# Check if rational
is_rational = False
if weights is not None:
for w in weights:
if abs(w - 1.0) > 1e-10:
is_rational = True
break
if is_rational and weights is not None:
weights_arr = TColStd_Array1OfReal(1, n)
for i, w in enumerate(weights):
weights_arr.SetValue(i + 1, w)
return Geom_BSplineCurve(poles, weights_arr, knots_arr, mults_arr, degree)
else:
return Geom_BSplineCurve(poles, knots_arr, mults_arr, degree)
[docs]
def native_loop_to_occ_wire(loop, graph, edge_cache=None):
"""Convert a native BREP loop to an OCC TopoDS_Wire.
Parameters
----------
loop : brep_loop
The native loop to convert.
graph : TopologyGraph
The topology graph containing the loop's edges.
edge_cache : dict, optional
Cache of edge_id -> TopoDS_Edge for reuse.
Returns
-------
TopoDS_Wire
The OCC wire.
"""
require_occ()
from yapcad.native_brep import is_brep_loop, trim_edge_id
if edge_cache is None:
edge_cache = {}
# Get trim IDs from loop
trim_ids = loop[1] # ['brep_loop', trim_refs, metadata]
wire_builder = BRepBuilderAPI_MakeWire()
for tid in trim_ids:
trim = graph.trims.get(tid)
if trim is None:
continue
edge_id = trim_edge_id(trim)
edge = graph.edges.get(edge_id)
if edge is None:
continue
# Get or create OCC edge
if edge_id in edge_cache:
occ_edge = edge_cache[edge_id]
else:
occ_edge = native_edge_to_occ(edge, graph)
edge_cache[edge_id] = occ_edge
# Add edge to wire (handle sense/orientation)
sense = trim[2].get('sense', True)
if sense:
wire_builder.Add(occ_edge)
else:
# Reversed edge
reversed_edge = occ_edge.Reversed()
wire_builder.Add(topods.Edge(reversed_edge))
if wire_builder.IsDone():
return wire_builder.Wire()
else:
return None
[docs]
def native_face_to_occ(face, graph, edge_cache=None):
"""Convert a native BREP face to an OCC TopoDS_Face.
Parameters
----------
face : brep_face
The native face to convert.
graph : TopologyGraph
The topology graph containing the face's loops.
edge_cache : dict, optional
Cache of edge_id -> TopoDS_Edge for reuse.
Returns
-------
TopoDS_Face
The OCC face, or None if conversion fails.
"""
require_occ()
from math import pi
if edge_cache is None:
edge_cache = {}
# Get surface and loops
surface = face[1] # ['brep_face', surface, metadata]
meta = face[2]
loop_ids = meta.get('loop_ids', [])
face_sense = meta.get('sense', True)
# Convert surface to OCC
occ_surface = native_surface_to_occ(surface)
if occ_surface is None:
return None
# Check if this is a complete analytic surface that should be created
# without wire boundaries (e.g., full sphere, full torus)
# These surfaces have singularities (poles) that make wire reconstruction complex
surface_type = surface[0] if isinstance(surface, (list, tuple)) else None
is_complete_surface = False
if surface_type == 'sphere_surface':
# Check if it's a full sphere (u: 0 to 2π, v: -π/2 to π/2)
surf_meta = surface[2] if len(surface) > 2 else {}
u_range = surf_meta.get('u_range', (0, 2 * pi))
v_range = surf_meta.get('v_range', (-pi / 2, pi / 2))
if (abs(u_range[1] - u_range[0] - 2 * pi) < 0.01 and
abs(v_range[1] - v_range[0] - pi) < 0.01):
is_complete_surface = True
if is_complete_surface:
# Create face from surface natural bounds, ignoring wires
# (wires on surfaces with singularities are complex to reconstruct)
try:
face_builder = BRepBuilderAPI_MakeFace(occ_surface, 1e-6)
if face_builder.IsDone():
occ_face = face_builder.Face()
if not face_sense:
occ_face = topods.Face(occ_face.Reversed())
return occ_face
except Exception:
pass
return None
# Build wires from loops
wires = []
for lid in loop_ids:
loop = graph.loops.get(lid)
if loop is None:
continue
wire = native_loop_to_occ_wire(loop, graph, edge_cache)
if wire is not None:
wires.append(wire)
if not wires:
# No wires - create face from surface bounds
try:
face_builder = BRepBuilderAPI_MakeFace(occ_surface, 1e-6)
if face_builder.IsDone():
return face_builder.Face()
except Exception:
pass
return None
# First wire is outer boundary
try:
face_builder = BRepBuilderAPI_MakeFace(occ_surface, wires[0], True)
if not face_builder.IsDone():
return None
# Add inner wires (holes)
for inner_wire in wires[1:]:
face_builder.Add(inner_wire)
occ_face = face_builder.Face()
# Handle face sense
if not face_sense:
occ_face = topods.Face(occ_face.Reversed())
return occ_face
except Exception:
return None
[docs]
def native_brep_to_occ(graph, fix_shape=True):
"""Convert a native BREP TopologyGraph to an OCC TopoDS_Solid.
This function converts a complete native BREP representation back to
an OCC solid, which can then be used for boolean operations or export.
Parameters
----------
graph : TopologyGraph
The native BREP topology graph.
fix_shape : bool, optional
If True, apply shape fixing to ensure a valid solid. Default True.
Returns
-------
TopoDS_Solid or TopoDS_Shell
The OCC solid (or shell if conversion to solid fails).
"""
require_occ()
from yapcad.native_brep import TopologyGraph
if not isinstance(graph, TopologyGraph):
raise ValueError("Expected a TopologyGraph")
edge_cache = {}
# Convert all faces
occ_faces = []
for fid, face in graph.faces.items():
occ_face = native_face_to_occ(face, graph, edge_cache)
if occ_face is not None:
occ_faces.append(occ_face)
if not occ_faces:
raise ValueError("No faces could be converted")
# Sew faces into shell(s)
sewing = BRepBuilderAPI_Sewing(1e-6)
for occ_face in occ_faces:
sewing.Add(occ_face)
sewing.Perform()
sewn_shape = sewing.SewedShape()
# Try to create a solid from the sewn shell
if sewn_shape.ShapeType() == TopAbs_SHELL:
shell = topods.Shell(sewn_shape)
if fix_shape:
# Fix the shell (may fail for some geometries)
try:
shell_fixer = ShapeFix_Shell(shell)
shell_fixer.Perform()
shell = shell_fixer.Shell()
except RuntimeError:
# Shell fixing failed, use unfixed shell
pass
# Create solid from shell
solid_builder = BRepBuilderAPI_MakeSolid(shell)
if solid_builder.IsDone():
solid = solid_builder.Solid()
if fix_shape:
# Fix the solid (may fail for some geometries)
try:
solid_fixer = ShapeFix_Solid(solid)
solid_fixer.Perform()
solid = solid_fixer.Solid()
except RuntimeError:
# Solid fixing failed, use unfixed solid
pass
return solid
else:
# Return shell if solid creation fails
return shell
elif sewn_shape.ShapeType() == TopAbs_SOLID:
solid = topods.Solid(sewn_shape)
if fix_shape:
try:
solid_fixer = ShapeFix_Solid(solid)
solid_fixer.Perform()
solid = solid_fixer.Solid()
except RuntimeError:
# Solid fixing failed, use unfixed solid
pass
return solid
else:
# Return whatever we got
return sewn_shape
[docs]
def native_brep_to_occ_from_solid(yapcad_solid, fix_shape=True):
"""Convert a yapCAD solid's native BREP to OCC representation.
Convenience function that extracts the native BREP from a yapCAD solid
and converts it to OCC format.
Parameters
----------
yapcad_solid : solid
A yapCAD solid with native BREP data attached.
fix_shape : bool, optional
If True, apply shape fixing to ensure a valid solid. Default True.
Returns
-------
TopoDS_Solid or TopoDS_Shell or None
The OCC shape, or None if no native BREP is attached.
"""
require_occ()
from yapcad.native_brep import native_brep_from_solid, has_native_brep
if not has_native_brep(yapcad_solid):
return None
graph = native_brep_from_solid(yapcad_solid)
if graph is None:
return None
return native_brep_to_occ(graph, fix_shape=fix_shape)
__all__ = [
# OCC availability
'occ_available',
'require_occ',
# OCC → Native conversion
'occ_surface_to_native',
'occ_edge_to_native',
'occ_solid_to_native_brep',
# Native → OCC conversion
'native_surface_to_occ',
'native_vertex_to_occ',
'native_edge_to_occ',
'native_loop_to_occ_wire',
'native_face_to_occ',
'native_brep_to_occ',
'native_brep_to_occ_from_solid',
]