Source code for declaracad.occ.geom

"""
Copyright (c) 2020, CodeLV.

Distributed under the terms of the GPL v3 License.

The full license is in the file LICENSE, distributed with this software.

Created on Dec 26, 2020

@author: jrm
"""

import math
import warnings
from contextlib import contextmanager
from typing import Any, Optional, Union

from atom.api import Atom, Float, Property, Typed
from OCCT.BRep import BRep_Tool
from OCCT.Geom import Geom_Line
from OCCT.gp import gp, gp_Ax1, gp_Dir, gp_Pnt, gp_Vec
from OCCT.TopoDS import TopoDS_Shape

try:
    from SMESH.SMDS import SMDS_MeshNode
except ImportError as e:
    warnings.warn(f"{e}")

    class SMDS_MeshNode:  # type: ignore
        pass


class Settings(Atom):
    """Used to manage tolerance settings"""

    tolerance = Float(1e-6)


settings = Settings()


@contextmanager
def tolerance(tol: float):
    _tol = settings.tolerance
    settings.tolerance = tol
    yield tol
    settings.tolerance = _tol


[docs] class BBox(Atom): xmin = Float() ymin = Float() zmin = Float() xmax = Float() ymax = Float() zmax = Float() def _get_dx(self) -> float: return self.xmax - self.xmin dx = Property(_get_dx, cached=True) def _get_dy(self) -> float: return self.ymax - self.ymin dy = Property(_get_dy, cached=True) def _get_dz(self) -> float: return self.zmax - self.zmin dz = Property(_get_dz, cached=True) def __init__( self, xmin: float = 0, ymin: float = 0, zmin: float = 0, xmax: float = 0, ymax: float = 0, zmax: float = 0, ): super(BBox, self).__init__( xmin=xmin, ymin=ymin, zmin=zmin, xmax=xmax, ymax=ymax, zmax=zmax ) def __getitem__(self, key) -> Union[tuple, float]: return (self.xmin, self.ymin, self.zmin, self.xmax, self.ymax, self.zmax)[key] def _get_center(self) -> "Point": return Point( (self.xmin + self.xmax) / 2, (self.ymin + self.ymax) / 2, (self.zmin + self.zmax) / 2, ) center = Property(_get_center, cached=True) def _get_diagonal(self) -> float: return math.sqrt(self.dx**2 + self.dy**2 + self.dz**2) diagonal = Property(_get_diagonal, cached=True) def _get_min(self) -> "Point": return Point(self.xmin, self.ymin, self.zmin) def _get_max(self) -> "Point": return Point(self.xmax, self.ymax, self.zmax) min = Property(_get_min) max = Property(_get_max) def __repr__(self) -> str: return "<BBox: x=%s y=%s z=%s w=%s h=%s d=%s>" % ( self.xmin, self.ymin, self.zmin, self.dx, self.dy, self.dz, )
[docs] class Point(Atom): proxy = Typed(gp_Pnt) x = Float(0, strict=False) y = Float(0, strict=False) z = Float(0, strict=False) def __init__(self, x=0, y=0, z=0, **kwargs): if isinstance(x, TopoDS_Shape): pnt = BRep_Tool.Pnt_(x) x, y, z = pnt.X(), pnt.Y(), pnt.Z() elif isinstance(x, (gp_Pnt, gp_Dir, gp_Vec, SMDS_MeshNode)): x, y, z = x.X(), x.Y(), x.Z() super().__init__(x=x, y=y, z=z, **kwargs) def _default_proxy(self) -> gp_Pnt: return gp_Pnt(self.x, self.y, self.z) # ======================================================================== # Binds changes to the proxy # ======================================================================== def _observe_x(self, change): if change["type"] == "update": self.proxy.SetX(self.x) def _observe_y(self, change): if change["type"] == "update": self.proxy.SetY(self.y) def _observe_z(self, change): if change["type"] == "update": self.proxy.SetZ(self.z) # ======================================================================== # Slice support # ======================================================================== def __getitem__(self, key): return (self.x, self.y, self.z).__getitem__(key) def __setitem__(self, key, value): p = [self.x, self.y, self.z] p[key] = value self.x, self.y, self.z = p # ======================================================================== # Operations support # ======================================================================== def __add__(self, other): p = self.__coerce__(other) return self.__class__(self.x + p.x, self.y + p.y, self.z + p.z) def __sub__(self, other): p = self.__coerce__(other) return self.__class__(self.x - p.x, self.y - p.y, self.z - p.z) def __eq__(self, other): return self.is_equal(other) def is_equal(self, other, tol=None): p = self.__coerce__(other) return self.proxy.IsEqual(p.proxy, tol or settings.tolerance) def __mul__(self, other): return self.__class__(self.x * other, self.y * other, self.z * other) def __truediv__(self, other): return self.__class__(self.x / other, self.y / other, self.z / other) def cross(self, other) -> "Point": p = self.__coerce__(other) return self.__coerce__(self.proxy.Crossed(p.proxy)) def dot(self, other) -> float: p = self.__coerce__(other) return self.proxy.Dot(p.proxy) def midpoint(self, other) -> "Point": p = self.__coerce__(other) return self.__class__( (self.x + p.x) / 2, (self.y + p.y) / 2, (self.z + p.z) / 2 ) def distance(self, other) -> float: p = self.__coerce__(other) return self.proxy.Distance(p.proxy) def angle(self, other) -> float: """Returns the angle value between 0 and pi in radians""" v = gp_Vec(self.__coerce__(other).proxy.XYZ()) return gp_Vec(self.proxy.XYZ()).Angle(v) def angle_with_ref(self, other, ref) -> float: """Computes the angle, in radians, between this vector and vector ref. The result is a value between -pi and pi. """ ref = gp_Vec(self.__coerce__(ref).proxy.XYZ()) v = gp_Vec(self.__coerce__(other).proxy.XYZ()) return gp_Vec(self.proxy.XYZ()).AngleWithRef(v, ref) def angle2d(self, other) -> float: """Angle between the x and y components Returns the angle value between 0 and pi in radians """ p = self.__coerce__(other) v = gp_Vec(p.x, p.y, 0) return gp_Vec(self.x, self.y, 0).Angle(v) def magnitude(self) -> float: return self.proxy.Distance(gp_Pnt()) def distance2d(self, other) -> float: p = self.__coerce__(other) return math.sqrt((self.x - p.x) ** 2 + (self.y - p.y) ** 2) def replace(self, **kwargs) -> "Point": """Create a copy with the value replaced with the given parameters.""" p = Point(*self[:]) for k, v in kwargs.items(): setattr(p, k, v) return p def offset(self, distance: float, direction: "Direction") -> "Point": """Create a point offset by distance in the given direction Parameters ---------- distance: float The offset distance direction: Union[Tuple[float, ...], Direction, gp_Dir] The direction to offset. Will be coerced Returns ------- p: Point Point offset from this point """ # Use proxy to so it is normalized d = coerce_direction(direction).proxy return Point(Geom_Line(self.proxy, d).Value(distance)) def __hash__(self): return hash(self[:]) def coordinates_in_range(self, low: float, high: float) -> bool: """Check if all coordinates are in the given range""" return ( (low <= self.x <= high) and (low <= self.y <= high) and (low <= self.z <= high) ) def clip(self, low: float, high: float) -> "Point": """Clip to the given range""" return Point( x=min(high, max(low, self.x)), y=min(high, max(low, self.y)), z=min(high, max(low, self.z)), ) @classmethod def __coerce__(self, other): return coerce_point(other) def __str__(self): return str((round(self.x, 6), round(self.y, 6), round(self.z, 6))) def __repr__(self): return "<Point: x=%s y=%s z=%s>" % self[:]
[docs] class Direction(Point): proxy = Typed(gp_Dir) def _default_proxy(self): try: return gp_Dir(self.x, self.y, self.z) except RuntimeError as e: warnings.warn(f"Could not create proxy: {self}") raise e @classmethod def __coerce__(self, other) -> "Direction": return coerce_direction(other) def __repr__(self) -> str: return "<Direction: x=%s y=%s z=%s>" % self[:] def __neg__(self) -> "Direction": return self.reversed() def reversed(self) -> "Direction": """Return a reversed copy""" v = self.proxy.Reversed() return Direction(v.X(), v.Y(), v.Z()) def rotated( self, angle: float, axis: Optional[tuple[Point, "Direction"]] = None ) -> "Direction": """Rotate by angle radians about the given axis. The axis defaults to the origin and z direction. """ axis = gp_Ax1(axis[0].proxy, axis[1].proxy) if axis else gp.OZ_() v = self.proxy.Rotated(axis, angle) return Direction(v.X(), v.Y(), v.Z()) @classmethod def XY(cls, x: float, y: float) -> "Direction": # Create a direction in the 2d XY plane with Z normal v = gp.DZ_().Rotated(gp.OZ_(), math.atan2(y, x)) return Direction(v.X(), v.Y(), v.Z()) @classmethod def XZ(cls, x: float, y: float) -> "Direction": # Create a direction in the XY plane v = gp_Dir() v.Rotate(gp.OY_(), math.atan2(y, x)) return Direction(v.X(), v.Y(), v.Z()) @classmethod def YZ(cls, x: float, y: float) -> "Direction": # Create a direction in the XY plane v = gp_Dir() v.Rotate(gp.OX_(), math.atan2(y, x)) return Direction(v.X(), v.Y(), v.Z()) def is_parallel(self, other, tol: Optional[float] = None) -> bool: p = self.__coerce__(other) return self.proxy.IsParallel(p.proxy, tol or settings.tolerance) def is_opposite(self, other, tol: Optional[float] = None) -> bool: p = self.__coerce__(other) return self.proxy.IsOpposite(p.proxy, tol or settings.tolerance) def is_normal(self, other, tol: Optional[float] = None) -> bool: """Check if perpendicular""" p = self.__coerce__(other) return self.proxy.IsNormal(p.proxy, tol or settings.tolerance)
def coerce_point(arg: Any) -> Point: if isinstance(arg, TopoDS_Shape): arg = BRep_Tool.Pnt_(arg) if hasattr(arg, "XYZ"): # copy from gp_Pnt, gp_Vec, gp_Dir, etc.. return Point(arg.X(), arg.Y(), arg.Z()) if isinstance(arg, Point): return arg if isinstance(arg, dict): return Point(**arg) return Point(*arg) def coerce_direction(arg: Any) -> Direction: if isinstance(arg, TopoDS_Shape): arg = BRep_Tool.Pnt_(arg) if hasattr(arg, "XYZ"): # copy from gp_Pnt2d, gp_Vec2d, gp_Dir2d, etc.. return Direction(arg.X(), arg.Y(), arg.Z()) if isinstance(arg, Direction): return arg if isinstance(arg, dict): return Direction(**arg) return Direction(*arg) def coerce_rotation(arg: Union[float, int, tuple[float, float]]) -> float: if isinstance(arg, (int, float)): return float(arg) return float(math.atan2(*arg))