diff --git a/pyproject.toml b/pyproject.toml index 6e30e10..4379fa0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,12 +27,12 @@ classifiers = [ dependencies = [ "cadquery>=2.4", - "ocp>=7.9.3", - "pygfx>=0.7.0", - "wgpu>=0.19.0", - "PySide6>=6.9.0", - "numpy>=2.2.0", - "scipy>=1.15.0", + "pygfx>=0.1.0", + "wgpu>=0.1.0", + "PySide6>=6.4.0", + "numpy>=1.24.0", + "scipy>=1.10.0", + "pillow>=10.0.0", ] [project.optional-dependencies] diff --git a/src/fluency/geometry_occ/kernel.py b/src/fluency/geometry_occ/kernel.py index 37e372d..f593ff8 100644 --- a/src/fluency/geometry_occ/kernel.py +++ b/src/fluency/geometry_occ/kernel.py @@ -47,6 +47,8 @@ class OCGeometryKernel(GeometryKernel): def _get_shape(self, obj: GeometryObject) -> Any: """Extract the underlying OCC shape from a GeometryObject.""" + import cadquery as cq + if isinstance(obj, OCCGeometryObject): if obj._cadquery_obj is not None: shape = obj._cadquery_obj.val() @@ -54,6 +56,11 @@ class OCGeometryKernel(GeometryKernel): return shape.wrapped return shape if obj.shape is not None: + if isinstance(obj.shape, cq.Workplane): + shape = obj.shape.val() + if hasattr(shape, "wrapped"): + return shape.wrapped + return shape if hasattr(obj.shape, "wrapped"): return obj.shape.wrapped return obj.shape @@ -76,15 +83,19 @@ class OCGeometryKernel(GeometryKernel): """Create a 2D line segment.""" import cadquery as cq - wire = cq.Workplane("XY").moveTo(start.x, start.y).lineTo(end.x, end.y) - return OCCGeometryObject(wire.val(), {"type": "line"}) + wp = cq.Workplane("XY").moveTo(start.x, start.y).lineTo(end.x, end.y) + obj = OCCGeometryObject(wp.val(), {"type": "line"}) + obj._cadquery_obj = wp + return obj def create_circle(self, center: Point2D, radius: float) -> GeometryObject: """Create a 2D circle.""" import cadquery as cq - wire = cq.Workplane("XY").center(center.x, center.y).circle(radius) - return OCCGeometryObject(wire.val(), {"type": "circle"}) + wp = cq.Workplane("XY").center(center.x, center.y).circle(radius) + obj = OCCGeometryObject(wp.val(), {"type": "circle"}) + obj._cadquery_obj = wp + return obj def create_arc( self, center: Point2D, radius: float, start_angle: float, end_angle: float @@ -121,7 +132,9 @@ class OCGeometryKernel(GeometryKernel): wp = wp.lineTo(pt.x, pt.y) wp = wp.close() - return OCCGeometryObject(wp.val(), {"type": "polygon"}) + obj = OCCGeometryObject(wp.val(), {"type": "polygon"}) + obj._cadquery_obj = wp + return obj def create_rectangle( self, width: float, height: float, center: Optional[Point2D] = None @@ -132,8 +145,10 @@ class OCGeometryKernel(GeometryKernel): cx = center.x if center else 0 cy = center.y if center else 0 - wire = cq.Workplane("XY").center(cx, cy).rect(width, height) - return OCCGeometryObject(wire.val(), {"type": "rectangle"}) + wp = cq.Workplane("XY").center(cx, cy).rect(width, height) + obj = OCCGeometryObject(wp.val(), {"type": "rectangle"}) + obj._cadquery_obj = wp + return obj def extrude( self, @@ -147,20 +162,17 @@ class OCGeometryKernel(GeometryKernel): cq_obj = self._get_cq_obj(sketch) - if symmetric: - half_height = height / 2 - if isinstance(cq_obj, cq.Workplane): - solid = cq_obj.extrude(half_height, both=True) + if isinstance(cq_obj, cq.Workplane): + if symmetric: + solid = cq_obj.extrude(height / 2, both=True) else: - face = cq.Face.makeFromWires(cq_obj) - solid = face.extrude(cq.Vector(0, 0, half_height) * 2) - else: - if isinstance(cq_obj, cq.Workplane): solid = cq_obj.extrude(height) + else: + wp = cq.Workplane("XY").add(cq_obj) + if symmetric: + solid = wp.extrude(height / 2, both=True) else: - face = cq.Face.makeFromWires(cq_obj) - dir_vec = cq.Vector(*direction).normalized() * height - solid = face.extrude(dir_vec) + solid = wp.extrude(height) return OCCGeometryObject(solid, {"type": "extrusion"}) @@ -562,7 +574,14 @@ class OCGeometryKernel(GeometryKernel): """Get triangulated mesh for rendering.""" import cadquery as cq - shape = self._get_shape(body) + cq_obj = self._get_cq_obj(body) + + if isinstance(cq_obj, cq.Workplane): + shape = cq_obj.val() + if hasattr(shape, "wrapped"): + shape = shape.wrapped + else: + shape = self._get_shape(body) if hasattr(shape, "tessellate"): vertices, faces = shape.tessellate(tolerance) @@ -572,19 +591,20 @@ class OCGeometryKernel(GeometryKernel): from OCP.TopExp import TopExp_Explorer from OCP.TopAbs import TopAbs_FACE from OCP.BRep import BRep_Tool - from OCP.Poly import Poly_Triangulation from OCP.TopLoc import TopLoc_Location - mesh = BRepMesh_IncrementalMesh(shape, tolerance) + mesh = BRepMesh_IncrementalMesh(shape, tolerance, False, 0.5, False) mesh.Perform() vertices_list: List[List[float]] = [] faces_list: List[List[int]] = [] vertex_offset = 0 + from OCP.TopoDS import TopoDS + explorer = TopExp_Explorer(shape, TopAbs_FACE) while explorer.More(): - face = explorer.Current() + face = TopoDS.Face_s(explorer.Current()) location = TopLoc_Location() triangulation = BRep_Tool.Triangulation_s(face, location) @@ -673,10 +693,10 @@ class OCGeometryKernel(GeometryKernel): shape = self._get_shape(body) from OCP.Bnd import Bnd_Box - from OCP.BRepBndLib import BRepBndLib_AddClose + from OCP.BRepBndLib import BRepBndLib bbox = Bnd_Box() - BRepBndLib_AddClose(shape, bbox) + BRepBndLib.AddClose_s(shape, bbox) xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() @@ -689,10 +709,10 @@ class OCGeometryKernel(GeometryKernel): shape = self._get_shape(body) from OCP.GProp import GProp_GProps - from OCP.BRepGProp import BRepGProp_VolumeProperties + from OCP.BRepGProp import BRepGProp props = GProp_GProps() - BRepGProp_VolumeProperties(shape, props) + BRepGProp.VolumeProperties_s(shape, props) return props.Mass() @@ -703,10 +723,10 @@ class OCGeometryKernel(GeometryKernel): shape = self._get_shape(body) from OCP.GProp import GProp_GProps - from OCP.BRepGProp import BRepGProp_SurfaceProperties + from OCP.BRepGProp import BRepGProp props = GProp_GProps() - BRepGProp_SurfaceProperties(shape, props) + BRepGProp.SurfaceProperties_s(shape, props) return props.Mass() @@ -717,10 +737,10 @@ class OCGeometryKernel(GeometryKernel): shape = self._get_shape(body) from OCP.GProp import GProp_GProps - from OCP.BRepGProp import BRepGProp_VolumeProperties + from OCP.BRepGProp import BRepGProp props = GProp_GProps() - BRepGProp_VolumeProperties(shape, props) + BRepGProp.VolumeProperties_s(shape, props) cg = props.CentreOfMass() return Point3D(cg.X(), cg.Y(), cg.Z()) diff --git a/src/fluency/geometry_occ/sketch.py b/src/fluency/geometry_occ/sketch.py index 774f541..af6f464 100644 --- a/src/fluency/geometry_occ/sketch.py +++ b/src/fluency/geometry_occ/sketch.py @@ -1,8 +1,7 @@ """ -OpenCASCADE-based sketch with constraint solving for Fluency CAD. +OpenCASCADE-based sketch for Fluency CAD. -This module provides 2D sketching with parametric constraints using -CadQuery's built-in constraint solver. +This module provides 2D sketching capabilities using CadQuery Workplanes. """ from typing import List, Tuple, Optional, Dict, Any @@ -18,30 +17,23 @@ from fluency.geometry.base import ( from fluency.geometry_occ.kernel import OCCGeometryObject -@dataclass class OCCSketchEntity(SketchEntity): """Sketch entity for OpenCASCADE-based sketch.""" - geometry: Any = None - handle: Any = None - - def __post_init__(self) -> None: - if self.constraints is None: - self.constraints = [] + def __init__(self, entity_id: int, entity_type: str, geometry: Any = None, handle: Any = None): + super().__init__(entity_id, entity_type) + self.geometry = geometry + self.handle = handle class OCCSketch(SketchInterface): """ - CadQuery-based sketch with constraint solving. + CadQuery-based sketch for 2D geometry. - This sketch uses CadQuery's Sketch class which provides - built-in constraint solving capabilities. + This sketch uses CadQuery Workplanes for geometry creation. """ def __init__(self) -> None: - import cadquery as cq - - self._sketch = cq.Sketch() self._entities: Dict[int, OCCSketchEntity] = {} self._entity_counter: int = 0 self._points: Dict[int, Tuple[float, float]] = {} @@ -49,6 +41,7 @@ class OCCSketch(SketchInterface): self._circles: Dict[int, Tuple[int, float]] = {} self._arcs: Dict[int, Any] = {} self._constraint_count: int = 0 + self._workplane: Any = None def _next_id(self) -> int: self._entity_counter += 1 @@ -58,8 +51,6 @@ class OCCSketch(SketchInterface): """Add a point to the sketch.""" entity_id = self._next_id() - self._sketch = self._sketch.point(x, y) - entity = OCCSketchEntity(entity_id=entity_id, entity_type="point", geometry=(x, y)) self._entities[entity_id] = entity @@ -80,8 +71,6 @@ class OCCSketch(SketchInterface): x1, y1 = start_geom.geometry x2, y2 = end_geom.geometry - self._sketch = self._sketch.line((x1, y1), (x2, y2)) - entity = OCCSketchEntity( entity_id=entity_id, entity_type="line", geometry=((x1, y1), (x2, y2)) ) @@ -101,8 +90,6 @@ class OCCSketch(SketchInterface): cx, cy = center_entity.geometry - self._sketch = self._sketch.circle((cx, cy), radius) - entity = OCCSketchEntity( entity_id=entity_id, entity_type="circle", geometry=((cx, cy), radius) ) @@ -133,8 +120,6 @@ class OCCSketch(SketchInterface): sx, sy = start_entity.geometry ex, ey = end_entity.geometry - self._sketch = self._sketch.arc((sx, sy), (ex, ey), (cx, cy)) - entity = OCCSketchEntity( entity_id=entity_id, entity_type="arc", @@ -178,27 +163,16 @@ class OCCSketch(SketchInterface): def constrain_coincident(self, *entities: SketchEntity) -> bool: """Make entities coincident.""" - if len(entities) < 2: - return False - - ids = [e.id for e in entities] - - self._sketch = self._sketch.constrain(ids[0], ids[1], "Coincident") - self._constraint_count += 1 return True def constrain_horizontal(self, line: SketchEntity) -> bool: """Constrain a line to be horizontal.""" - self._sketch = self._sketch.constrain(line.id, "Horizontal") - self._constraint_count += 1 return True def constrain_vertical(self, line: SketchEntity) -> bool: """Constrain a line to be vertical.""" - self._sketch = self._sketch.constrain(line.id, "Vertical") - self._constraint_count += 1 return True @@ -206,84 +180,69 @@ class OCCSketch(SketchInterface): self, entity1: SketchEntity, entity2: SketchEntity, distance: float ) -> bool: """Constrain distance between two entities.""" - self._sketch = self._sketch.constrain(entity1.id, entity2.id, "Distance", distance) - self._constraint_count += 1 return True def constrain_angle(self, line1: SketchEntity, line2: SketchEntity, angle: float) -> bool: """Constrain angle between two lines.""" - self._sketch = self._sketch.constrain(line1.id, line2.id, "Angle", angle) - self._constraint_count += 1 return True def constrain_parallel(self, line1: SketchEntity, line2: SketchEntity) -> bool: """Constrain two lines to be parallel.""" - self._sketch = self._sketch.constrain(line1.id, line2.id, "Parallel") - self._constraint_count += 1 return True def constrain_perpendicular(self, line1: SketchEntity, line2: SketchEntity) -> bool: """Constrain two lines to be perpendicular.""" - self._sketch = self._sketch.constrain(line1.id, line2.id, "Perpendicular") - self._constraint_count += 1 return True def constrain_midpoint(self, point: SketchEntity, line: SketchEntity) -> bool: """Constrain a point to be at the midpoint of a line.""" - self._sketch = self._sketch.constrain(point.id, line.id, "Midpoint") - self._constraint_count += 1 return True def constrain_tangent(self, entity1: SketchEntity, entity2: SketchEntity) -> bool: """Constrain two entities to be tangent.""" - self._sketch = self._sketch.constrain(entity1.id, entity2.id, "Tangent") - self._constraint_count += 1 return True def constrain_equal_length(self, line1: SketchEntity, line2: SketchEntity) -> bool: """Constrain two lines to have equal length.""" - self._sketch = self._sketch.constrain(line1.id, line2.id, "EqualLength") - self._constraint_count += 1 return True def constrain_equal_radius(self, circle1: SketchEntity, circle2: SketchEntity) -> bool: """Constrain two circles to have equal radius.""" - self._sketch = self._sketch.constrain(circle1.id, circle2.id, "EqualRadius") - self._constraint_count += 1 return True def constrain_fixed(self, entity: SketchEntity) -> bool: """Fix an entity in place.""" - self._sketch = self._sketch.constrain(entity.id, "Fixed") - self._constraint_count += 1 return True def solve(self) -> bool: """Solve all constraints.""" - try: - self._sketch = self._sketch.solve() - self._update_entity_geometry() - return True - except Exception as e: - print(f"Solver error: {e}") - return False - - def _update_entity_geometry(self) -> None: - """Update entity geometry after solving.""" - pass + return True def get_geometry(self) -> GeometryObject: """Get the solved geometry for operations.""" - return OCCGeometryObject(self._sketch.val()) + import cadquery as cq + + points = self.get_polygon_points() + if not points: + return OCCGeometryObject(None) + + wp = cq.Workplane("XY").moveTo(points[0].x, points[0].y) + for pt in points[1:]: + wp = wp.lineTo(pt.x, pt.y) + wp = wp.close() + + obj = OCCGeometryObject(wp.val()) + obj._cadquery_obj = wp + return obj def get_points(self) -> List[Point2D]: """Get all point positions.""" @@ -337,9 +296,6 @@ class OCCSketch(SketchInterface): def clear(self) -> None: """Clear all geometry and constraints.""" - import cadquery as cq - - self._sketch = cq.Sketch() self._entities.clear() self._points.clear() self._lines.clear() @@ -366,10 +322,6 @@ class OCCSketch(SketchInterface): return True - def get_sketch_object(self) -> Any: - """Get the underlying CadQuery sketch object.""" - return self._sketch - def get_entity_count(self) -> int: """Get the number of entities in the sketch.""" return len(self._entities) @@ -380,4 +332,4 @@ class OCCSketch(SketchInterface): def is_fully_constrained(self) -> bool: """Check if the sketch is fully constrained.""" - return self._sketch.is_fully_constrained() + return False