- Tons of addtions

This commit is contained in:
bklronin
2026-06-28 22:51:52 +02:00
parent f8f16ea800
commit f6422e0847
7 changed files with 1010 additions and 149 deletions
+98 -31
View File
@@ -46,7 +46,11 @@ class OCGeometryKernel(GeometryKernel):
self._mesh_tolerance: float = 0.1
def _get_shape(self, obj: GeometryObject) -> Any:
"""Extract the underlying OCC shape from a GeometryObject."""
"""Extract the underlying OCC shape from a GeometryObject.
Returns *None* if the object carries no shape (e.g. an empty sketch) —
callers should check for None before using the result.
"""
import cadquery as cq
if isinstance(obj, OCCGeometryObject):
@@ -64,7 +68,13 @@ class OCGeometryKernel(GeometryKernel):
if hasattr(obj.shape, "wrapped"):
return obj.shape.wrapped
return obj.shape
return obj.shape if obj.shape else obj
# No cadquery obj and no raw shape → genuinely empty.
return None
# Non-OCCGeometryObject: return its shape if present, else None.
# (Use explicit identity/truth checks — some OCP TopoDS objects have a
# falsy __bool__, so ``obj.shape if obj.shape`` is unsafe.)
shape = getattr(obj, "shape", None)
return shape if shape is not None else None
def _get_cq_obj(self, obj: GeometryObject) -> Any:
"""Get CadQuery object from GeometryObject."""
@@ -157,30 +167,94 @@ class OCGeometryKernel(GeometryKernel):
direction: Tuple[float, float, float] = (0, 0, 1),
symmetric: bool = False,
) -> GeometryObject:
"""Extrude a 2D sketch into a 3D solid.
"""Extrude a sketch face into a 3D solid along the sketch plane normal.
*height* is extruded along *direction* (default +Z). A negative *height*
extrudes in the opposite direction. The *direction* argument is accepted
for API compatibility; currently only the sign of *height* is used for
direction (positive → +Z, negative → -Z).
The sketch's plane normal is read from ``sketch.metadata["normal"]``
(set by ``OCCSketch.build_face_geometry``); it defaults to +Z for
legacy objects that don't carry one. *direction* is accepted for API
compatibility but ignored — the plane normal is authoritative. A
negative *height* extrudes against the normal.
"""
import cadquery as cq
from OCP.gp import gp_Vec
from OCP.BRepPrimAPI import BRepPrimAPI_MakePrism
from OCP.BRepAlgoAPI import BRepAlgoAPI_Fuse
from OCP.TopoDS import TopoDS_Shape
cq_obj = self._get_cq_obj(sketch)
face = self._get_shape(sketch)
if face is None:
raise ValueError(
"Cannot extrude: sketch has no geometry. "
"Draw a closed profile before extruding."
)
# ``face`` may be a TopoDS_Face (new path) or a compound/wire from
# legacy cadquery objects. If it's not already a face, build one.
face = self._ensure_face(face)
if face is None:
raise ValueError(
"Cannot extrude: sketch geometry is not a valid face. "
"Ensure the profile is closed (no open ends)."
)
if isinstance(cq_obj, cq.Workplane):
if symmetric:
solid = cq_obj.extrude(height / 2, both=True)
else:
solid = cq_obj.extrude(height)
normal = self._sketch_normal(sketch)
nx, ny, nz = normal
def _prism(h: float):
vec = gp_Vec(nx * h, ny * h, nz * h)
maker = BRepPrimAPI_MakePrism(face, vec, False, True)
maker.Build()
return maker.Shape()
if symmetric:
half = height / 2.0
pos = _prism(half)
neg = _prism(-half)
fuse = BRepAlgoAPI_Fuse(pos, neg)
fuse.Build()
solid = fuse.Shape()
else:
wp = cq.Workplane("XY").add(cq_obj)
if symmetric:
solid = wp.extrude(height / 2, both=True)
else:
solid = wp.extrude(height)
solid = _prism(height)
return OCCGeometryObject(solid, {"type": "extrusion"})
return OCCGeometryObject(solid, {"type": "extrusion", "normal": normal})
@staticmethod
def _sketch_normal(obj: GeometryObject) -> Tuple[float, float, float]:
"""Return the normal stored on a sketch-derived geometry object, else +Z."""
import numpy as np
meta = getattr(obj, "metadata", None) or {}
n = meta.get("normal")
if n is None:
return (0.0, 0.0, 1.0)
arr = np.asarray(n, dtype=float)
norm = float(np.linalg.norm(arr))
if norm < 1e-12:
return (0.0, 0.0, 1.0)
arr = arr / norm
return (float(arr[0]), float(arr[1]), float(arr[2]))
@staticmethod
def _ensure_face(shape: Any) -> Any:
"""Return a ``TopoDS_Face`` from *shape*, or *None* if impossible.
If *shape* is already a face, return it unchanged; otherwise try to
build a planar face from it (wire/edge/compound). Returns *None* for
empty/invalid input so callers can surface a clear error instead of
feeding a non-face to ``BRepPrimAPI_MakePrism``.
"""
from OCP.TopoDS import TopoDS_Face
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace
if shape is None:
return None
if isinstance(shape, TopoDS_Face):
return shape
try:
maker = BRepBuilderAPI_MakeFace(shape, True)
maker.Build()
if maker.IsDone():
return maker.Face()
except Exception:
pass
return None
def revolve(
self,
@@ -189,22 +263,16 @@ class OCGeometryKernel(GeometryKernel):
axis: Tuple[float, float, float] = (0, 0, 1),
origin: Tuple[float, float, float] = (0, 0, 0),
) -> GeometryObject:
"""Revolve a 2D sketch around an axis."""
import cadquery as cq
"""Revolve a sketch face around an axis."""
import math
# Get the OCC shape directly
# Get the OCC shape directly (a TopoDS_Face for new sketch geometry).
shape = self._get_shape(sketch)
face = self._ensure_face(shape)
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace
from OCP.gp import gp_Ax1, gp_Pnt, gp_Dir
from OCP.BRepPrimAPI import BRepPrimAPI_MakeRevol
# Build a face from the wire/shape
face_maker = BRepBuilderAPI_MakeFace(shape, False)
face_maker.Build()
face = face_maker.Face()
# Revolve the face around the axis
revolve_axis = gp_Ax1(gp_Pnt(*origin), gp_Dir(*axis))
angle_rad = math.radians(angle)
@@ -212,8 +280,7 @@ class OCGeometryKernel(GeometryKernel):
revolver.Build()
solid_shape = revolver.Shape()
solid = cq.Shape(solid_shape)
return OCCGeometryObject(solid, {"type": "revolution"})
return OCCGeometryObject(solid_shape, {"type": "revolution"})
def loft(self, profiles: List[GeometryObject], ruled: bool = False) -> GeometryObject:
"""Create a loft between multiple profiles."""