Import CAD projects and cad-router v1
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""OCP-native kernel helpers for the 2.0 rearchitecture path."""
|
||||
@@ -0,0 +1,93 @@
|
||||
"""OCP-native boolean helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, Sequence
|
||||
|
||||
from OCP.BOPAlgo import BOPAlgo_GlueOff, BOPAlgo_GlueShift
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse
|
||||
from OCP.ShapeUpgrade import ShapeUpgrade_UnifySameDomain
|
||||
from OCP.TopAbs import TopAbs_SOLID
|
||||
from OCP.TopExp import TopExp_Explorer
|
||||
from OCP.TopoDS import TopoDS, TopoDS_Shape, TopoDS_Solid
|
||||
from OCP.TopTools import TopTools_ListOfShape
|
||||
|
||||
|
||||
def _list_of(shapes: Sequence[TopoDS_Shape]) -> TopTools_ListOfShape:
|
||||
out = TopTools_ListOfShape()
|
||||
for shape in shapes:
|
||||
out.Append(shape)
|
||||
return out
|
||||
|
||||
|
||||
def solids_of(shape: TopoDS_Shape) -> List[TopoDS_Solid]:
|
||||
out: List[TopoDS_Solid] = []
|
||||
explorer = TopExp_Explorer(shape, TopAbs_SOLID)
|
||||
while explorer.More():
|
||||
out.append(TopoDS.Solid_s(explorer.Current()))
|
||||
explorer.Next()
|
||||
if not out and shape.ShapeType() == TopAbs_SOLID:
|
||||
out.append(TopoDS.Solid_s(shape))
|
||||
return out
|
||||
|
||||
|
||||
def clean_shape(shape: TopoDS_Shape) -> TopoDS_Shape:
|
||||
unifier = ShapeUpgrade_UnifySameDomain(shape, True, True, True)
|
||||
unifier.Build()
|
||||
return unifier.Shape()
|
||||
|
||||
|
||||
def fuse_shapes(shapes: Sequence[TopoDS_Shape], *, glue: bool = True, tol: Optional[float] = None, clean: bool = True) -> TopoDS_Shape:
|
||||
if not shapes:
|
||||
raise ValueError("fuse_shapes requires at least one shape")
|
||||
if len(shapes) == 1:
|
||||
return shapes[0]
|
||||
builder = BRepAlgoAPI_Fuse()
|
||||
builder.SetRunParallel(True)
|
||||
builder.SetUseOBB(True)
|
||||
builder.SetArguments(_list_of([shapes[0]]))
|
||||
builder.SetTools(_list_of(list(shapes[1:])))
|
||||
if tol is not None:
|
||||
builder.SetFuzzyValue(float(tol))
|
||||
# Match CadQuery's Shape.fuse(glue=True) behavior: CadQuery maps glue=True
|
||||
# to OCC's GlueShift, not GlueFull. GlueFull can leave overlapping solids
|
||||
# separate in cases where CQ would return one fused solid.
|
||||
builder.SetGlue(BOPAlgo_GlueShift if glue else BOPAlgo_GlueOff)
|
||||
builder.Build()
|
||||
if not builder.IsDone():
|
||||
raise ValueError("OCP fuse failed")
|
||||
result = builder.Shape()
|
||||
return clean_shape(result) if clean else result
|
||||
|
||||
|
||||
def cut_shapes(body: TopoDS_Shape, tools: Sequence[TopoDS_Shape]) -> TopoDS_Shape:
|
||||
if not tools:
|
||||
return body
|
||||
builder = BRepAlgoAPI_Cut()
|
||||
builder.SetRunParallel(True)
|
||||
builder.SetUseOBB(True)
|
||||
builder.SetArguments(_list_of([body]))
|
||||
builder.SetTools(_list_of(tools))
|
||||
builder.Build()
|
||||
if not builder.IsDone():
|
||||
raise ValueError("OCP cut failed")
|
||||
return builder.Shape()
|
||||
|
||||
|
||||
def common_shapes(shapes: Sequence[TopoDS_Shape]) -> TopoDS_Shape:
|
||||
if not shapes:
|
||||
raise ValueError("common_shapes requires at least one shape")
|
||||
if len(shapes) == 1:
|
||||
return shapes[0]
|
||||
result = shapes[0]
|
||||
for tool in shapes[1:]:
|
||||
builder = BRepAlgoAPI_Common()
|
||||
builder.SetRunParallel(True)
|
||||
builder.SetUseOBB(True)
|
||||
builder.SetArguments(_list_of([result]))
|
||||
builder.SetTools(_list_of([tool]))
|
||||
builder.Build()
|
||||
if not builder.IsDone():
|
||||
raise ValueError("OCP common failed")
|
||||
result = builder.Shape()
|
||||
return result
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Thin OCP-native primitive builders used by the public API layer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from OCP.BRepPrimAPI import (
|
||||
BRepPrimAPI_MakeBox,
|
||||
BRepPrimAPI_MakeCone,
|
||||
BRepPrimAPI_MakeCylinder,
|
||||
BRepPrimAPI_MakeSphere,
|
||||
)
|
||||
from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt
|
||||
|
||||
|
||||
def _point(value: tuple[float, float, float]) -> gp_Pnt:
|
||||
return gp_Pnt(float(value[0]), float(value[1]), float(value[2]))
|
||||
|
||||
|
||||
def _axis2(
|
||||
origin: tuple[float, float, float], direction: tuple[float, float, float]
|
||||
) -> gp_Ax2:
|
||||
return gp_Ax2(
|
||||
_point(origin),
|
||||
gp_Dir(float(direction[0]), float(direction[1]), float(direction[2])),
|
||||
)
|
||||
|
||||
|
||||
def make_box_solid(corner: tuple[float, float, float], dx: float, dy: float, dz: float):
|
||||
builder = BRepPrimAPI_MakeBox(_point(corner), float(dx), float(dy), float(dz))
|
||||
builder.Build()
|
||||
if not builder.IsDone():
|
||||
raise ValueError("OCP box builder failed")
|
||||
return builder.Solid()
|
||||
|
||||
|
||||
def make_cylinder_solid(
|
||||
origin: tuple[float, float, float],
|
||||
axis: tuple[float, float, float],
|
||||
radius: float,
|
||||
height: float,
|
||||
):
|
||||
builder = BRepPrimAPI_MakeCylinder(
|
||||
_axis2(origin, axis), float(radius), float(height)
|
||||
)
|
||||
builder.Build()
|
||||
if not builder.IsDone():
|
||||
raise ValueError("OCP cylinder builder failed")
|
||||
return builder.Solid()
|
||||
|
||||
|
||||
def make_cone_solid(
|
||||
origin: tuple[float, float, float],
|
||||
axis: tuple[float, float, float],
|
||||
bottom_radius: float,
|
||||
top_radius: float,
|
||||
height: float,
|
||||
):
|
||||
builder = BRepPrimAPI_MakeCone(
|
||||
_axis2(origin, axis),
|
||||
float(bottom_radius),
|
||||
float(top_radius),
|
||||
float(height),
|
||||
)
|
||||
builder.Build()
|
||||
if not builder.IsDone():
|
||||
raise ValueError("OCP cone builder failed")
|
||||
return builder.Solid()
|
||||
|
||||
|
||||
def make_sphere_solid(center: tuple[float, float, float], radius: float):
|
||||
builder = BRepPrimAPI_MakeSphere(_point(center), float(radius))
|
||||
builder.Build()
|
||||
if not builder.IsDone():
|
||||
raise ValueError("OCP sphere builder failed")
|
||||
return builder.Solid()
|
||||
@@ -0,0 +1,72 @@
|
||||
"""OCP shape casting and type helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from OCP.TopAbs import (
|
||||
TopAbs_COMPOUND,
|
||||
TopAbs_EDGE,
|
||||
TopAbs_FACE,
|
||||
TopAbs_SOLID,
|
||||
TopAbs_VERTEX,
|
||||
TopAbs_WIRE,
|
||||
)
|
||||
from OCP.TopExp import TopExp_Explorer
|
||||
from OCP.TopoDS import TopoDS, TopoDS_Shape
|
||||
|
||||
|
||||
def shape_type_name(shape: TopoDS_Shape) -> str:
|
||||
st = shape.ShapeType()
|
||||
if st == TopAbs_VERTEX:
|
||||
return "vertex"
|
||||
if st == TopAbs_EDGE:
|
||||
return "edge"
|
||||
if st == TopAbs_WIRE:
|
||||
return "wire"
|
||||
if st == TopAbs_FACE:
|
||||
return "face"
|
||||
if st == TopAbs_SOLID:
|
||||
return "solid"
|
||||
if st == TopAbs_COMPOUND:
|
||||
return "compound"
|
||||
return str(st)
|
||||
|
||||
|
||||
def as_vertex(shape: TopoDS_Shape):
|
||||
return TopoDS.Vertex_s(shape)
|
||||
|
||||
|
||||
def as_edge(shape: TopoDS_Shape):
|
||||
return TopoDS.Edge_s(shape)
|
||||
|
||||
|
||||
def as_wire(shape: TopoDS_Shape):
|
||||
return TopoDS.Wire_s(shape)
|
||||
|
||||
|
||||
def as_face(shape: TopoDS_Shape):
|
||||
return TopoDS.Face_s(shape)
|
||||
|
||||
|
||||
def as_solid(shape: TopoDS_Shape):
|
||||
st = shape.ShapeType()
|
||||
if st == TopAbs_SOLID:
|
||||
return TopoDS.Solid_s(shape)
|
||||
explorer = TopExp_Explorer(shape, TopAbs_SOLID)
|
||||
if explorer.More():
|
||||
return TopoDS.Solid_s(explorer.Current())
|
||||
raise ValueError(f"Expected a solid-compatible OCP shape, got {shape_type_name(shape)}")
|
||||
|
||||
|
||||
def as_compound(shape: TopoDS_Shape):
|
||||
st = shape.ShapeType()
|
||||
if st == TopAbs_COMPOUND:
|
||||
return TopoDS.Compound_s(shape)
|
||||
raise ValueError(f"Expected a compound OCP shape, got {shape_type_name(shape)}")
|
||||
|
||||
|
||||
def require_shape(value: Any) -> TopoDS_Shape:
|
||||
if isinstance(value, TopoDS_Shape):
|
||||
return value
|
||||
raise TypeError(f"Expected an OCP TopoDS_Shape, got {type(value).__name__}")
|
||||
@@ -0,0 +1,143 @@
|
||||
"""Thin OCP-native curve and wire builders."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any, Iterable, Optional, Sequence
|
||||
|
||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeWire
|
||||
from OCP.BRepLib import BRepLib
|
||||
from OCP.GC import GC_MakeArcOfCircle, GC_MakeCircle
|
||||
from OCP.GCE2d import GCE2d_MakeSegment
|
||||
from OCP.Geom import Geom_BSplineCurve
|
||||
from OCP.Geom2d import Geom2d_Line
|
||||
from OCP.Geom import Geom_ConicalSurface, Geom_CylindricalSurface
|
||||
from OCP.TColgp import TColgp_Array1OfPnt
|
||||
from OCP.TColStd import TColStd_Array1OfInteger, TColStd_Array1OfReal
|
||||
from OCP.gp import (
|
||||
gp_Ax2,
|
||||
gp_Ax3,
|
||||
gp_Circ,
|
||||
gp_Dir,
|
||||
gp_Dir2d,
|
||||
gp_Pnt,
|
||||
gp_Pnt2d,
|
||||
)
|
||||
|
||||
|
||||
def _pnt(value: Sequence[float]) -> gp_Pnt:
|
||||
return gp_Pnt(float(value[0]), float(value[1]), float(value[2]))
|
||||
|
||||
|
||||
def _dir(value: Sequence[float]) -> gp_Dir:
|
||||
return gp_Dir(float(value[0]), float(value[1]), float(value[2]))
|
||||
|
||||
|
||||
def make_line_edge(start: Sequence[float], end: Sequence[float]):
|
||||
return BRepBuilderAPI_MakeEdge(_pnt(start), _pnt(end)).Edge()
|
||||
|
||||
|
||||
def make_circle_edge(center: Sequence[float], radius: float, normal: Sequence[float]):
|
||||
geom = GC_MakeCircle(gp_Ax2(_pnt(center), _dir(normal)), float(radius)).Value()
|
||||
return BRepBuilderAPI_MakeEdge(geom).Edge()
|
||||
|
||||
|
||||
def make_arc_three_point_edge(
|
||||
start: Sequence[float], middle: Sequence[float], end: Sequence[float]
|
||||
):
|
||||
geom = GC_MakeArcOfCircle(_pnt(start), _pnt(middle), _pnt(end)).Value()
|
||||
return BRepBuilderAPI_MakeEdge(geom).Edge()
|
||||
|
||||
|
||||
def make_arc_angle_edge(
|
||||
center: Sequence[float],
|
||||
radius: float,
|
||||
start_angle: float,
|
||||
end_angle: float,
|
||||
normal: Sequence[float],
|
||||
):
|
||||
circ = gp_Circ(gp_Ax2(_pnt(center), _dir(normal)), float(radius))
|
||||
geom = GC_MakeArcOfCircle(circ, float(start_angle), float(end_angle), True).Value()
|
||||
return BRepBuilderAPI_MakeEdge(geom).Edge()
|
||||
|
||||
|
||||
def make_bspline_edge(
|
||||
*,
|
||||
control_points: Sequence[Sequence[float]],
|
||||
degree: int,
|
||||
knots: Sequence[float],
|
||||
multiplicities: Sequence[int],
|
||||
weights: Optional[Sequence[float]] = None,
|
||||
periodic: bool = False,
|
||||
):
|
||||
poles = TColgp_Array1OfPnt(1, len(control_points))
|
||||
for idx, point in enumerate(control_points, start=1):
|
||||
poles.SetValue(idx, _pnt(point))
|
||||
|
||||
knot_array = TColStd_Array1OfReal(1, len(knots))
|
||||
for idx, knot in enumerate(knots, start=1):
|
||||
knot_array.SetValue(idx, float(knot))
|
||||
|
||||
mult_array = TColStd_Array1OfInteger(1, len(multiplicities))
|
||||
for idx, multiplicity in enumerate(multiplicities, start=1):
|
||||
mult_array.SetValue(idx, int(multiplicity))
|
||||
|
||||
if weights is None:
|
||||
curve = Geom_BSplineCurve(
|
||||
poles,
|
||||
knot_array,
|
||||
mult_array,
|
||||
int(degree),
|
||||
bool(periodic),
|
||||
)
|
||||
else:
|
||||
weight_array = TColStd_Array1OfReal(1, len(weights))
|
||||
for idx, weight in enumerate(weights, start=1):
|
||||
weight_array.SetValue(idx, float(weight))
|
||||
curve = Geom_BSplineCurve(
|
||||
poles,
|
||||
weight_array,
|
||||
knot_array,
|
||||
mult_array,
|
||||
int(degree),
|
||||
bool(periodic),
|
||||
)
|
||||
return BRepBuilderAPI_MakeEdge(curve).Edge()
|
||||
|
||||
|
||||
def make_wire_from_edges(edges: Iterable[Any]):
|
||||
builder = BRepBuilderAPI_MakeWire()
|
||||
for edge in edges:
|
||||
builder.Add(edge)
|
||||
return builder.Wire()
|
||||
|
||||
|
||||
def make_polyline_wire(points: Iterable[Sequence[float]], closed: bool = False):
|
||||
pts = list(points)
|
||||
edges = [make_line_edge(pts[i], pts[i + 1]) for i in range(len(pts) - 1)]
|
||||
if closed and len(pts) > 2:
|
||||
edges.append(make_line_edge(pts[-1], pts[0]))
|
||||
return make_wire_from_edges(edges)
|
||||
|
||||
|
||||
def make_helix_wire(
|
||||
pitch: float,
|
||||
height: float,
|
||||
radius: float,
|
||||
center: Sequence[float],
|
||||
direction: Sequence[float],
|
||||
):
|
||||
geom_surf = Geom_CylindricalSurface(
|
||||
gp_Ax3(_pnt(center), _dir(direction)), float(radius)
|
||||
)
|
||||
geom_line = Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(2 * math.pi, float(pitch)))
|
||||
n_turns = float(height) / float(pitch)
|
||||
u_start = geom_line.Value(0.0)
|
||||
u_stop = geom_line.Value(
|
||||
n_turns * math.sqrt((2 * math.pi) ** 2 + float(pitch) ** 2)
|
||||
)
|
||||
geom_seg = GCE2d_MakeSegment(u_start, u_stop).Value()
|
||||
edge = BRepBuilderAPI_MakeEdge(geom_seg, geom_surf).Edge()
|
||||
wire = BRepBuilderAPI_MakeWire(edge).Wire()
|
||||
BRepLib.BuildCurves3d_s(wire, 1e-6, MaxSegment=2000)
|
||||
return wire
|
||||
@@ -0,0 +1,53 @@
|
||||
"""OCP-native export helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Sequence
|
||||
|
||||
from OCP.BRep import BRep_Builder
|
||||
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCP.STEPControl import STEPControl_AsIs, STEPControl_Writer
|
||||
from OCP.StlAPI import StlAPI_Writer
|
||||
from OCP.TopoDS import TopoDS_Compound, TopoDS_Shape
|
||||
from OCP.IFSelect import IFSelect_RetDone
|
||||
|
||||
|
||||
def make_compound(shapes: Sequence[TopoDS_Shape]) -> TopoDS_Shape:
|
||||
if not shapes:
|
||||
raise ValueError("No shapes to export")
|
||||
if len(shapes) == 1:
|
||||
return shapes[0]
|
||||
return make_compound_always(shapes)
|
||||
|
||||
|
||||
def make_compound_always(shapes: Sequence[TopoDS_Shape]) -> TopoDS_Shape:
|
||||
if not shapes:
|
||||
raise ValueError("No shapes to export")
|
||||
builder = BRep_Builder()
|
||||
compound = TopoDS_Compound()
|
||||
builder.MakeCompound(compound)
|
||||
for shape in shapes:
|
||||
builder.Add(compound, shape)
|
||||
return compound
|
||||
|
||||
|
||||
def export_step_shapes(shapes: Sequence[TopoDS_Shape], filename: str) -> None:
|
||||
writer = STEPControl_Writer()
|
||||
compound = make_compound(shapes)
|
||||
status = writer.Transfer(compound, STEPControl_AsIs)
|
||||
# Some OCP builds return int-like statuses; keep failure detection conservative.
|
||||
if status != IFSelect_RetDone and int(status) != int(IFSelect_RetDone):
|
||||
raise ValueError(f"STEP transfer failed: {status}")
|
||||
path = str(Path(filename))
|
||||
write_status = writer.Write(path)
|
||||
if write_status != IFSelect_RetDone and int(write_status) != int(IFSelect_RetDone):
|
||||
raise ValueError(f"STEP write failed: {write_status}")
|
||||
|
||||
|
||||
def export_stl_shape(shape: TopoDS_Shape, filename: str) -> None:
|
||||
BRepMesh_IncrementalMesh(shape, 0.1).Perform()
|
||||
writer = StlAPI_Writer()
|
||||
ok = writer.Write(shape, str(Path(filename)))
|
||||
if ok is False:
|
||||
raise ValueError("STL write failed")
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Thin OCP-native feature builders for loft/sweep/helical sweep."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable, Sequence
|
||||
|
||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform
|
||||
from OCP.BRepOffsetAPI import BRepOffsetAPI_MakePipeShell, BRepOffsetAPI_ThruSections
|
||||
from OCP.gp import gp_Trsf, gp_Vec
|
||||
from OCP.TopoDS import TopoDS
|
||||
|
||||
from .ocp_curves import make_helix_wire
|
||||
|
||||
|
||||
def make_face_from_wire(wire):
|
||||
builder = BRepBuilderAPI_MakeFace(wire, True)
|
||||
if not builder.IsDone():
|
||||
raise ValueError("OCP face builder failed")
|
||||
return builder.Face()
|
||||
|
||||
|
||||
def make_face_from_wires(outer_wire, inner_wires: Sequence[Any]):
|
||||
builder = BRepBuilderAPI_MakeFace(outer_wire, True)
|
||||
if not builder.IsDone():
|
||||
raise ValueError("OCP face builder failed for outer wire")
|
||||
for inner_wire in inner_wires:
|
||||
builder.Add(TopoDS.Wire_s(inner_wire.Reversed()))
|
||||
if not builder.IsDone():
|
||||
raise ValueError("OCP face builder failed while adding inner wire")
|
||||
return builder.Face()
|
||||
|
||||
|
||||
def make_loft_solid(wires: Iterable[Any], ruled: bool = False):
|
||||
builder = BRepOffsetAPI_ThruSections(True, bool(ruled))
|
||||
builder.CheckCompatibility(True)
|
||||
for wire in wires:
|
||||
builder.AddWire(wire)
|
||||
builder.Build()
|
||||
if not builder.IsDone():
|
||||
raise ValueError("OCP loft builder failed")
|
||||
return builder.Shape()
|
||||
|
||||
|
||||
def make_sweep_solid(profile_wire, path_wire, is_frenet: bool = False):
|
||||
builder = BRepOffsetAPI_MakePipeShell(path_wire)
|
||||
builder.SetMode(bool(is_frenet))
|
||||
builder.Add(profile_wire, False, False)
|
||||
builder.Build()
|
||||
if not builder.IsDone():
|
||||
raise ValueError("OCP sweep builder failed")
|
||||
if not builder.MakeSolid():
|
||||
raise ValueError("OCP sweep solid conversion failed")
|
||||
return builder.Shape()
|
||||
|
||||
|
||||
def translate_shape(shape, vector: Sequence[float]):
|
||||
trsf = gp_Trsf()
|
||||
trsf.SetTranslation(gp_Vec(float(vector[0]), float(vector[1]), float(vector[2])))
|
||||
builder = BRepBuilderAPI_Transform(shape, trsf, True)
|
||||
builder.Build()
|
||||
if not builder.IsDone():
|
||||
raise ValueError("OCP feature translation failed")
|
||||
return builder.Shape()
|
||||
|
||||
|
||||
def make_helical_sweep_solid(
|
||||
profile_wire,
|
||||
pitch: float,
|
||||
height: float,
|
||||
radius: float,
|
||||
center: Sequence[float],
|
||||
direction: Sequence[float],
|
||||
):
|
||||
helix = make_helix_wire(pitch, height, radius, center, direction)
|
||||
moved_profile = translate_shape(profile_wire, (float(radius), 0.0, 0.0))
|
||||
return make_sweep_solid(moved_profile, helix, is_frenet=True)
|
||||
@@ -0,0 +1,72 @@
|
||||
"""OCP-native mesh/shell construction and tessellation helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, List, Sequence, Tuple
|
||||
|
||||
from OCP.BRep import BRep_Builder, BRep_Tool
|
||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_MakePolygon, BRepBuilderAPI_MakeSolid
|
||||
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCP.Poly import Poly_Triangulation
|
||||
from OCP.TopAbs import TopAbs_FORWARD, TopAbs_REVERSED
|
||||
from OCP.TopLoc import TopLoc_Location
|
||||
from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Shell
|
||||
from OCP.gp import gp_Pnt
|
||||
|
||||
from .ocp_properties import bounding_box
|
||||
from .ocp_topology import faces_of
|
||||
|
||||
|
||||
def make_triangle_face(points: Sequence[Sequence[float]]) -> TopoDS_Face:
|
||||
if len(points) != 3:
|
||||
raise ValueError("Triangle face requires exactly three points")
|
||||
polygon = BRepBuilderAPI_MakePolygon()
|
||||
for p in points:
|
||||
polygon.Add(gp_Pnt(float(p[0]), float(p[1]), float(p[2])))
|
||||
polygon.Close()
|
||||
if not polygon.IsDone():
|
||||
raise ValueError("OCP polygon builder failed")
|
||||
face = BRepBuilderAPI_MakeFace(polygon.Wire(), True)
|
||||
if not face.IsDone():
|
||||
raise ValueError("OCP triangle face builder failed")
|
||||
return face.Face()
|
||||
|
||||
|
||||
def shell_metric(shell) -> tuple[int, float]:
|
||||
bb = bounding_box(shell)
|
||||
volume = bb.xlen * bb.ylen * bb.zlen
|
||||
return (len(faces_of(shell)), float(volume))
|
||||
|
||||
|
||||
def shell_is_closed(shell) -> bool:
|
||||
return bool(TopoDS.Shell_s(shell).Closed())
|
||||
|
||||
|
||||
def solid_from_shell(shell):
|
||||
maker = BRepBuilderAPI_MakeSolid(TopoDS.Shell_s(shell))
|
||||
if not maker.IsDone():
|
||||
raise ValueError("OCP solid-from-shell builder failed")
|
||||
return maker.Solid()
|
||||
|
||||
|
||||
def tessellate_face(face: TopoDS_Face, tolerance: float = 0.35, angular_tolerance: float = 0.22):
|
||||
mesh = BRepMesh_IncrementalMesh(face, float(tolerance), False, float(angular_tolerance), True)
|
||||
mesh.Perform()
|
||||
loc = TopLoc_Location()
|
||||
tri = BRep_Tool.Triangulation_s(face, loc)
|
||||
if tri is None:
|
||||
return [], []
|
||||
trsf = loc.Transformation()
|
||||
vertices = []
|
||||
for idx in range(1, tri.NbNodes() + 1):
|
||||
p = tri.Node(idx).Transformed(trsf)
|
||||
vertices.append((float(p.X()), float(p.Y()), float(p.Z())))
|
||||
triangles = []
|
||||
reversed_face = face.Orientation() == TopAbs_REVERSED
|
||||
for idx in range(1, tri.NbTriangles() + 1):
|
||||
a, b, c = tri.Triangle(idx).Get()
|
||||
if reversed_face:
|
||||
triangles.append((a - 1, c - 1, b - 1))
|
||||
else:
|
||||
triangles.append((a - 1, b - 1, c - 1))
|
||||
return vertices, triangles
|
||||
@@ -0,0 +1,266 @@
|
||||
"""OCP-native geometry properties, bounding boxes, distance and normals."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Sequence, Tuple
|
||||
|
||||
from OCP.Bnd import Bnd_Box
|
||||
from OCP.BRep import BRep_Tool
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCP.BRepBndLib import BRepBndLib
|
||||
from OCP.BRepExtrema import BRepExtrema_DistShapeShape
|
||||
from OCP.BRepGProp import BRepGProp
|
||||
from OCP.BRepLProp import BRepLProp_SLProps
|
||||
from OCP.GProp import GProp_GProps
|
||||
from OCP.GeomAbs import GeomAbs_Plane
|
||||
import math
|
||||
|
||||
from OCP.TopAbs import (
|
||||
TopAbs_COMPOUND,
|
||||
TopAbs_COMPSOLID,
|
||||
TopAbs_EDGE,
|
||||
TopAbs_FACE,
|
||||
TopAbs_REVERSED,
|
||||
TopAbs_SHELL,
|
||||
TopAbs_SOLID,
|
||||
TopAbs_VERTEX,
|
||||
TopAbs_WIRE,
|
||||
)
|
||||
from OCP.TopoDS import TopoDS, TopoDS_Edge, TopoDS_Face, TopoDS_Shape, TopoDS_Solid
|
||||
from OCP.gp import gp_Pnt, gp_Vec
|
||||
|
||||
from .ocp_topology import vertex_point
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Vec3:
|
||||
x: float
|
||||
y: float
|
||||
z: float
|
||||
|
||||
def __iter__(self):
|
||||
yield self.x
|
||||
yield self.y
|
||||
yield self.z
|
||||
|
||||
def to_tuple(self) -> tuple[float, float, float]:
|
||||
return (self.x, self.y, self.z)
|
||||
|
||||
def dot(self, other: object) -> float:
|
||||
ox, oy, oz = _coerce_vec3(other)
|
||||
return self.x * ox + self.y * oy + self.z * oz
|
||||
|
||||
def norm(self) -> float:
|
||||
return math.sqrt(self.dot(self))
|
||||
|
||||
def normalized(self) -> "Vec3":
|
||||
n = self.norm()
|
||||
if n <= 1e-15:
|
||||
raise ValueError("Cannot normalize a zero-length vector")
|
||||
return Vec3(self.x / n, self.y / n, self.z / n)
|
||||
|
||||
def getAngle(self, other: object) -> float:
|
||||
ox, oy, oz = _coerce_vec3(other)
|
||||
on = math.sqrt(ox * ox + oy * oy + oz * oz)
|
||||
sn = self.norm()
|
||||
if sn <= 1e-15 or on <= 1e-15:
|
||||
raise ValueError("Cannot compute angle with a zero-length vector")
|
||||
value = max(-1.0, min(1.0, self.dot((ox, oy, oz)) / (sn * on)))
|
||||
return math.acos(value)
|
||||
|
||||
def __mul__(self, scalar: float) -> "Vec3":
|
||||
return Vec3(self.x * float(scalar), self.y * float(scalar), self.z * float(scalar))
|
||||
|
||||
__rmul__ = __mul__
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Vec3({self.x:.6g}, {self.y:.6g}, {self.z:.6g})"
|
||||
|
||||
|
||||
def _coerce_vec3(value: object) -> tuple[float, float, float]:
|
||||
if isinstance(value, Vec3):
|
||||
return value.to_tuple()
|
||||
if hasattr(value, "x") and hasattr(value, "y") and hasattr(value, "z"):
|
||||
return (float(getattr(value, "x")), float(getattr(value, "y")), float(getattr(value, "z")))
|
||||
if isinstance(value, (tuple, list)) and len(value) == 3:
|
||||
return (float(value[0]), float(value[1]), float(value[2]))
|
||||
raise TypeError(f"Expected a 3D vector-like value, got {type(value).__name__}")
|
||||
|
||||
|
||||
def _props_point(props: GProp_GProps) -> Vec3:
|
||||
p = props.CentreOfMass()
|
||||
return Vec3(float(p.X()), float(p.Y()), float(p.Z()))
|
||||
|
||||
|
||||
def linear_length(edge: TopoDS_Edge) -> float:
|
||||
props = GProp_GProps()
|
||||
BRepGProp.LinearProperties_s(edge, props)
|
||||
return float(props.Mass())
|
||||
|
||||
|
||||
def surface_area(face: TopoDS_Face) -> float:
|
||||
props = GProp_GProps()
|
||||
BRepGProp.SurfaceProperties_s(face, props)
|
||||
return float(props.Mass())
|
||||
|
||||
|
||||
def volume(solid: TopoDS_Solid) -> float:
|
||||
props = GProp_GProps()
|
||||
BRepGProp.VolumeProperties_s(solid, props)
|
||||
return float(props.Mass())
|
||||
|
||||
|
||||
def _bbox_center(shape: TopoDS_Shape) -> Vec3:
|
||||
bb = bounding_box(shape)
|
||||
return Vec3((bb.xmin + bb.xmax) / 2, (bb.ymin + bb.ymax) / 2, (bb.zmin + bb.zmax) / 2)
|
||||
|
||||
|
||||
def _props_mass_point(props: GProp_GProps, eps: float = 1e-12) -> Vec3 | None:
|
||||
"""Return the CentreOfMass when the reported mass is meaningfully nonzero."""
|
||||
try:
|
||||
mass = float(props.Mass())
|
||||
except Exception:
|
||||
return None
|
||||
if abs(mass) <= eps:
|
||||
return None
|
||||
return _props_point(props)
|
||||
|
||||
|
||||
def center_of_mass(shape: TopoDS_Shape) -> Vec3:
|
||||
"""Dimension-aware centre of mass.
|
||||
|
||||
Selects the OCP mass-properties routine that matches the shape's own
|
||||
topological dimension. This avoids the numerical residue that
|
||||
``VolumeProperties_s`` produces when applied to lower-dimensional shapes
|
||||
(e.g. a planar face), which previously leaked a bogus centre into
|
||||
``Face.get_center()``.
|
||||
"""
|
||||
|
||||
try:
|
||||
kind = shape.ShapeType()
|
||||
except Exception:
|
||||
kind = None
|
||||
|
||||
# Solids / compsolids / compounds carry a meaningful volume.
|
||||
if kind in (TopAbs_SOLID, TopAbs_COMPSOLID, TopAbs_COMPOUND):
|
||||
props = GProp_GProps()
|
||||
try:
|
||||
BRepGProp.VolumeProperties_s(shape, props)
|
||||
point = _props_mass_point(props)
|
||||
if point is not None:
|
||||
return point
|
||||
except Exception:
|
||||
pass
|
||||
# A compound may wrap only faces/edges; fall through to the
|
||||
# lower-dimensional properties below.
|
||||
if kind == TopAbs_COMPOUND:
|
||||
try:
|
||||
BRepGProp.SurfaceProperties_s(shape, props)
|
||||
point = _props_mass_point(props)
|
||||
if point is not None:
|
||||
return point
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
BRepGProp.LinearProperties_s(shape, props)
|
||||
point = _props_mass_point(props)
|
||||
if point is not None:
|
||||
return point
|
||||
except Exception:
|
||||
pass
|
||||
return _bbox_center(shape)
|
||||
|
||||
# Shells / faces are 2D -> use surface properties.
|
||||
if kind in (TopAbs_FACE, TopAbs_SHELL):
|
||||
props = GProp_GProps()
|
||||
try:
|
||||
BRepGProp.SurfaceProperties_s(shape, props)
|
||||
point = _props_mass_point(props)
|
||||
if point is not None:
|
||||
return point
|
||||
except Exception:
|
||||
pass
|
||||
return _bbox_center(shape)
|
||||
|
||||
# Wires / edges are 1D -> use linear properties.
|
||||
if kind in (TopAbs_WIRE, TopAbs_EDGE):
|
||||
props = GProp_GProps()
|
||||
try:
|
||||
BRepGProp.LinearProperties_s(shape, props)
|
||||
point = _props_mass_point(props)
|
||||
if point is not None:
|
||||
return point
|
||||
except Exception:
|
||||
pass
|
||||
return _bbox_center(shape)
|
||||
|
||||
# Vertex is a point.
|
||||
if kind == TopAbs_VERTEX:
|
||||
try:
|
||||
return Vec3(*vertex_point(TopoDS.Vertex_s(shape)))
|
||||
except Exception:
|
||||
pass
|
||||
return _bbox_center(shape)
|
||||
|
||||
# Unknown / undetermined kind: fall back to bbox centre.
|
||||
return _bbox_center(shape)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BoundingBox:
|
||||
xmin: float
|
||||
ymin: float
|
||||
zmin: float
|
||||
xmax: float
|
||||
ymax: float
|
||||
zmax: float
|
||||
|
||||
@property
|
||||
def xlen(self) -> float:
|
||||
return self.xmax - self.xmin
|
||||
|
||||
@property
|
||||
def ylen(self) -> float:
|
||||
return self.ymax - self.ymin
|
||||
|
||||
@property
|
||||
def zlen(self) -> float:
|
||||
return self.zmax - self.zmin
|
||||
|
||||
|
||||
def bounding_box(shape: TopoDS_Shape) -> BoundingBox:
|
||||
box = Bnd_Box()
|
||||
box.SetGap(0.0)
|
||||
BRepBndLib.AddOptimal_s(shape, box, False, False)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = box.Get()
|
||||
return BoundingBox(float(xmin), float(ymin), float(zmin), float(xmax), float(ymax), float(zmax))
|
||||
|
||||
|
||||
def distance(shape_a: TopoDS_Shape, shape_b: TopoDS_Shape) -> float:
|
||||
dist = BRepExtrema_DistShapeShape(shape_a, shape_b)
|
||||
dist.Perform()
|
||||
if not dist.IsDone():
|
||||
raise ValueError("OCP distance calculation failed")
|
||||
return float(dist.Value())
|
||||
|
||||
|
||||
def face_normal_at(face: TopoDS_Face, u: float = 0.5, v: float = 0.5) -> Vec3:
|
||||
adaptor = BRepAdaptor_Surface(face, True)
|
||||
umin = float(adaptor.FirstUParameter())
|
||||
umax = float(adaptor.LastUParameter())
|
||||
vmin = float(adaptor.FirstVParameter())
|
||||
vmax = float(adaptor.LastVParameter())
|
||||
uu = umin + (umax - umin) * float(u)
|
||||
vv = vmin + (vmax - vmin) * float(v)
|
||||
props = BRepLProp_SLProps(adaptor, uu, vv, 1, 1e-7)
|
||||
if not props.IsNormalDefined():
|
||||
raise ValueError("Face normal is not defined at the requested parameters")
|
||||
n = props.Normal()
|
||||
if face.Orientation() == TopAbs_REVERSED:
|
||||
n.Reverse()
|
||||
return Vec3(float(n.X()), float(n.Y()), float(n.Z()))
|
||||
|
||||
|
||||
def edge_center(edge: TopoDS_Edge) -> Vec3:
|
||||
return center_of_mass(edge)
|
||||
@@ -0,0 +1,69 @@
|
||||
"""OCP-native topology traversal helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List
|
||||
|
||||
from OCP.BRep import BRep_Tool
|
||||
from OCP.BRepTools import BRepTools
|
||||
from OCP.TopAbs import TopAbs_EDGE, TopAbs_FACE, TopAbs_VERTEX, TopAbs_WIRE
|
||||
from OCP.TopExp import TopExp_Explorer
|
||||
from OCP.TopoDS import TopoDS, TopoDS_Edge, TopoDS_Face, TopoDS_Shape, TopoDS_Vertex, TopoDS_Wire
|
||||
|
||||
|
||||
def vertices_of(shape: TopoDS_Shape) -> List[TopoDS_Vertex]:
|
||||
out: List[TopoDS_Vertex] = []
|
||||
explorer = TopExp_Explorer(shape, TopAbs_VERTEX)
|
||||
while explorer.More():
|
||||
out.append(TopoDS.Vertex_s(explorer.Current()))
|
||||
explorer.Next()
|
||||
return out
|
||||
|
||||
|
||||
def edges_of(shape: TopoDS_Shape) -> List[TopoDS_Edge]:
|
||||
out: List[TopoDS_Edge] = []
|
||||
explorer = TopExp_Explorer(shape, TopAbs_EDGE)
|
||||
while explorer.More():
|
||||
out.append(TopoDS.Edge_s(explorer.Current()))
|
||||
explorer.Next()
|
||||
return out
|
||||
|
||||
|
||||
def wires_of(shape: TopoDS_Shape) -> List[TopoDS_Wire]:
|
||||
out: List[TopoDS_Wire] = []
|
||||
explorer = TopExp_Explorer(shape, TopAbs_WIRE)
|
||||
while explorer.More():
|
||||
out.append(TopoDS.Wire_s(explorer.Current()))
|
||||
explorer.Next()
|
||||
return out
|
||||
|
||||
|
||||
def faces_of(shape: TopoDS_Shape) -> List[TopoDS_Face]:
|
||||
out: List[TopoDS_Face] = []
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
while explorer.More():
|
||||
out.append(TopoDS.Face_s(explorer.Current()))
|
||||
explorer.Next()
|
||||
return out
|
||||
|
||||
|
||||
def vertex_point(vertex: TopoDS_Vertex) -> tuple[float, float, float]:
|
||||
p = BRep_Tool.Pnt_s(vertex)
|
||||
return (float(p.X()), float(p.Y()), float(p.Z()))
|
||||
|
||||
|
||||
def is_wire_closed(wire: TopoDS_Wire) -> bool:
|
||||
return bool(BRep_Tool.IsClosed_s(wire))
|
||||
|
||||
|
||||
def outer_wire_of(face: TopoDS_Face) -> TopoDS_Wire:
|
||||
return BRepTools.OuterWire_s(face)
|
||||
|
||||
|
||||
def inner_wires_of(face: TopoDS_Face) -> List[TopoDS_Wire]:
|
||||
outer = outer_wire_of(face)
|
||||
out: List[TopoDS_Wire] = []
|
||||
for wire in wires_of(face):
|
||||
if not wire.IsSame(outer):
|
||||
out.append(wire)
|
||||
return out
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Thin OCP-native transform helpers for public geometry wrappers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Tuple
|
||||
|
||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform
|
||||
from OCP.TopAbs import (
|
||||
TopAbs_COMPOUND,
|
||||
TopAbs_EDGE,
|
||||
TopAbs_FACE,
|
||||
TopAbs_SOLID,
|
||||
TopAbs_VERTEX,
|
||||
TopAbs_WIRE,
|
||||
)
|
||||
from OCP.TopoDS import TopoDS
|
||||
from OCP.gp import gp_Ax1, gp_Ax2, gp_Dir, gp_Pnt, gp_Trsf, gp_Vec
|
||||
|
||||
from ..core import AnyShape, Compound, Edge, Face, Solid, Vertex, Wire
|
||||
|
||||
|
||||
def _shape_from_transformed(shape: AnyShape, transformed) -> AnyShape:
|
||||
shape_type = transformed.ShapeType()
|
||||
if shape_type == TopAbs_VERTEX:
|
||||
return Vertex(TopoDS.Vertex_s(transformed))
|
||||
if shape_type == TopAbs_EDGE:
|
||||
return Edge(TopoDS.Edge_s(transformed))
|
||||
if shape_type == TopAbs_WIRE:
|
||||
return Wire(TopoDS.Wire_s(transformed))
|
||||
if shape_type == TopAbs_FACE:
|
||||
return Face(TopoDS.Face_s(transformed))
|
||||
if shape_type == TopAbs_SOLID:
|
||||
return Solid(TopoDS.Solid_s(transformed))
|
||||
if shape_type == TopAbs_COMPOUND:
|
||||
return Compound(TopoDS.Compound_s(transformed))
|
||||
raise ValueError(f"Unsupported transformed shape type: {shape_type}")
|
||||
|
||||
|
||||
def apply_transform(shape: AnyShape, trsf: gp_Trsf) -> AnyShape:
|
||||
xform = BRepBuilderAPI_Transform(shape.wrapped, trsf, True)
|
||||
xform.Build()
|
||||
if not xform.IsDone():
|
||||
raise ValueError("OCP transform build failed")
|
||||
return _shape_from_transformed(shape, xform.Shape())
|
||||
|
||||
|
||||
def translate_shape_ocp(
|
||||
shape: AnyShape, vector: Tuple[float, float, float]
|
||||
) -> AnyShape:
|
||||
trsf = gp_Trsf()
|
||||
trsf.SetTranslation(gp_Vec(float(vector[0]), float(vector[1]), float(vector[2])))
|
||||
return apply_transform(shape, trsf)
|
||||
|
||||
|
||||
def rotate_shape_ocp(
|
||||
shape: AnyShape,
|
||||
angle_degrees: float,
|
||||
axis: Tuple[float, float, float],
|
||||
origin: Tuple[float, float, float],
|
||||
) -> AnyShape:
|
||||
trsf = gp_Trsf()
|
||||
trsf.SetRotation(
|
||||
gp_Ax1(
|
||||
gp_Pnt(float(origin[0]), float(origin[1]), float(origin[2])),
|
||||
gp_Dir(float(axis[0]), float(axis[1]), float(axis[2])),
|
||||
),
|
||||
math.radians(float(angle_degrees)),
|
||||
)
|
||||
return apply_transform(shape, trsf)
|
||||
|
||||
|
||||
def mirror_shape_ocp(
|
||||
shape: AnyShape,
|
||||
plane_origin: Tuple[float, float, float],
|
||||
plane_normal: Tuple[float, float, float],
|
||||
) -> AnyShape:
|
||||
trsf = gp_Trsf()
|
||||
trsf.SetMirror(
|
||||
gp_Ax2(
|
||||
gp_Pnt(
|
||||
float(plane_origin[0]), float(plane_origin[1]), float(plane_origin[2])
|
||||
),
|
||||
gp_Dir(
|
||||
float(plane_normal[0]),
|
||||
float(plane_normal[1]),
|
||||
float(plane_normal[2]),
|
||||
),
|
||||
)
|
||||
)
|
||||
return apply_transform(shape, trsf)
|
||||
|
||||
|
||||
def place_shape_ocp(
|
||||
shape: AnyShape,
|
||||
origin: Tuple[float, float, float],
|
||||
x_axis: Tuple[float, float, float],
|
||||
y_axis: Tuple[float, float, float],
|
||||
z_axis: Tuple[float, float, float],
|
||||
) -> AnyShape:
|
||||
trsf = gp_Trsf()
|
||||
trsf.SetValues(
|
||||
float(x_axis[0]),
|
||||
float(y_axis[0]),
|
||||
float(z_axis[0]),
|
||||
float(origin[0]),
|
||||
float(x_axis[1]),
|
||||
float(y_axis[1]),
|
||||
float(z_axis[1]),
|
||||
float(origin[1]),
|
||||
float(x_axis[2]),
|
||||
float(y_axis[2]),
|
||||
float(z_axis[2]),
|
||||
float(origin[2]),
|
||||
)
|
||||
return apply_transform(shape, trsf)
|
||||
Reference in New Issue
Block a user